-
Notifications
You must be signed in to change notification settings - Fork 27
Update session key documentation #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,12 +40,12 @@ module.exports = { | |
solidity: "0.8.10", | ||
networks: { | ||
hardhat: { | ||
// Configuration for the Hardhat Network | ||
// Configuration for the Hardhat Network | ||
}, | ||
ten: { | ||
url: "https://testnet.ten.xyz/v1/", | ||
chainId: 443, | ||
accounts: ["your-private-key"], | ||
url: "https://testnet.ten.xyz/v1/", | ||
chainId: 443, | ||
accounts: ["your-private-key"], | ||
}, | ||
}, | ||
}; | ||
|
@@ -57,14 +57,14 @@ Once configured, you can start writing or migrating your smart contracts. | |
|
||
## 2. Writing Smart Contracts for TEN | ||
|
||
TEN executes smart contracts within the EVM similarly to Ethereum, so you can reuse your existing code. | ||
TEN executes smart contracts within the EVM similarly to Ethereum, so you can reuse your existing code. | ||
However, the execution and the internal state are hidden from everyone, including node operators and the sequencer. | ||
|
||
:::info | ||
TEN encrypts both the execution and its internal database using Trusted Execution Environments (TEEs). | ||
::: | ||
|
||
The [getStorageAt](https://docs.alchemy.com/reference/eth-getstorageat) method is disabled by default on TEN, so data access relies on view functions that you define. | ||
The [getStorageAt](https://docs.alchemy.com/reference/eth-getstorageat) method is disabled by default on TEN, so data access relies on view functions that you define. | ||
Public variables remain accessible as Solidity automatically creates getters for them. | ||
|
||
Let's illustrate with a basic storage dApp example where users can store and retrieve a number. | ||
|
@@ -90,9 +90,9 @@ contract StorageExample { | |
|
||
#### Explanation | ||
|
||
In this step, we created a public variable `storedValues` that maps the provided value to the address of the user who called the `storeValue` function. | ||
In this step, we created a public variable `storedValues` that maps the provided value to the address of the user who called the `storeValue` function. | ||
|
||
Because the variable is public, Solidity will provide a default public getter for it. | ||
Because the variable is public, Solidity will provide a default public getter for it. | ||
|
||
Since there are no data access restrictions, on both Ethereum and TEN, everyone will be able to read the values of all users. | ||
|
||
|
@@ -107,7 +107,7 @@ contract StorageExample { | |
function storeValue(uint256 value) public { | ||
_storedValues[tx.origin] = value; | ||
} | ||
function getValue(address account) public view returns (uint256) { | ||
return _storedValues[account]; | ||
} | ||
|
@@ -116,14 +116,14 @@ contract StorageExample { | |
|
||
#### Explanation | ||
|
||
The `storedValues` variable is now private, and we added a basic `getValue` function for users to retrieve their value. | ||
The `storedValues` variable is now private, and we added a basic `getValue` function for users to retrieve their value. | ||
|
||
On both Ethereum and TEN, anyone can call `getValue` to retrieve any value. | ||
On both Ethereum and TEN, anyone can call `getValue` to retrieve any value. | ||
On Ethereum, `_storedValues` can also be accessed directly with `getStorageAt` | ||
|
||
### Step 3: Data Access Control | ||
### Step 3: Data Access Control | ||
|
||
In this step, we'll add restrictions so users can only access their own data. | ||
In this step, we'll add restrictions so users can only access their own data. | ||
|
||
#### Code | ||
|
||
|
@@ -144,17 +144,17 @@ contract StorageExample { | |
|
||
#### Explanation | ||
|
||
The key line is: ``require(tx.origin == account, "Not authorized!");``, which ensures that the caller of the view function is the owner of the data. | ||
The key line is: `require(tx.origin == account, "Not authorized!");`, which ensures that the caller of the view function is the owner of the data. | ||
|
||
:::info | ||
TEN uses "Viewing Keys" to authenticate view function calls. | ||
TEN uses "Viewing Keys" to authenticate view function calls. | ||
::: | ||
|
||
**When deployed on TEN, this code guarantees that all users can only access their own values, and nobody can read the `_storedValues`.** | ||
|
||
### Step 4: Emitting Events - Default Visibility | ||
|
||
Event logs notify UIs about state changes in smart contracts. | ||
Event logs notify UIs about state changes in smart contracts. | ||
|
||
To improve our smart contract, we’ll emit an event when a user stores a value and milestone events when a specific size threshold is met. | ||
|
||
|
@@ -196,15 +196,15 @@ Rule 1: Event logs that contain EOAs as indexed fields (topics) are only visible | |
Rule 2: Event logs that don't contain any EOA are visible to everyone. | ||
|
||
In our case, the default rules ensure that: | ||
|
||
- `DataChanged` is visible only to the address that is storing the value. | ||
- `MilestoneReached` is publicly visible. | ||
|
||
|
||
### Step 5: Customizing Event Visibility | ||
|
||
The default visibility rules are a good starting point, but complex dApps require greater flexibility. | ||
The default visibility rules are a good starting point, but complex dApps require greater flexibility. | ||
|
||
TEN give you explicit control over event visibility. | ||
TEN give you explicit control over event visibility. | ||
|
||
#### Code | ||
|
||
|
@@ -270,19 +270,19 @@ contract StorageExample is ContractTransparencyConfig { | |
|
||
#### Explanation | ||
|
||
The `ContractTransparencyConfig` interface is known by the TEN platform. | ||
The `ContractTransparencyConfig` interface is known by the TEN platform. | ||
When a contract is deployed, the platform will call the `visibilityRules` function, and store the `VisibilityConfig`. | ||
|
||
For each event type, you can configure which fields can access it. | ||
This allows the developer to configure an event to be public even if it has EOAs or to allow the sender of the transaction to access events emitted even if the address is not in the event. | ||
For each event type, you can configure which fields can access it. | ||
This allows the developer to configure an event to be public even if it has EOAs or to allow the sender of the transaction to access events emitted even if the address is not in the event. | ||
|
||
Notice how in the `visibilityRules` above, we configure the `DataChanged` event to be visible to the first field and the sender, and the `MilestoneReached` to be visible to everyone. | ||
Notice how in the `visibilityRules` above, we configure the `DataChanged` event to be visible to the first field and the sender, and the `MilestoneReached` to be visible to everyone. | ||
|
||
The other configuration: `VisibilityConfig.contractCfg` applies to the entire contract: | ||
|
||
- `ContractCfg.TRANSPARENT`: The contracts will have public storage and events, behaving exactly like Ethereum. | ||
- `ContractCfg.PRIVATE`: The default TEN behaviour, where the storage is not accessible and the events are individually configurable. | ||
|
||
|
||
## Account Abstraction - Native Session Keys | ||
|
||
The key feature of ["Account Abstraction"](https://medium.com/p/2e85bde4c54d) (EIP-4337) is "Session keys"(SK) through a proxy smart contract. | ||
|
@@ -298,7 +298,7 @@ Imagine you're developing an on-chain game, and you want a smooth UX without the | |
|
||
Conceptually, the game will create a session key (SK) for the user, then ask the user to move some funds to that address, and then create "move" transactions signed with the SK. | ||
|
||
If the game were to create the SK in the browser, there would be a risk of the user losing the SK, and the funds associated with it, in case of an accidental exit. | ||
If the game were to create the SK in the browser, there would be a risk of the user losing the SK, and the funds associated with it, in case of an accidental exit. | ||
With TEN, the dApp developer doesn't have to worry about this, because the SKs are managed by TEEs. | ||
|
||
### Usage | ||
|
@@ -308,67 +308,157 @@ Note that it can be used for any dApp that requires a no-click UX. | |
|
||
#### When the game starts | ||
|
||
Before the user can start playing, the game must create the SK and ask the user to move some funds to that address. | ||
The funds will be used to pay for moves. | ||
Before the user can start playing, the game must create the SK and ask the user to move some funds to that address. | ||
The funds will be used to pay for moves. | ||
|
||
- Call the RPC ``sessionkeys_Create`` - without any parameters. This will return a hex-encoded address of the SK. | ||
- Create a normal transaction that transfers some ETH to the SK. The amount depends on how many "moves" the user is prepared to prepay for. | ||
- Ask the user to sign this transaction with their normal wallet, and submit it to the network using the library of your choice. | ||
- Once the receipt is received, call ``sessionkeys_Activate``. | ||
- Call the RPC `eth_getStorageAt` with address `0x0000000000000000000000000000000000000003` (CreateSessionKeyCQMethod) - this will return the hex-encoded address of the SK. | ||
|
||
- Create a normal transaction that transfers some ETH to the SK. The amount depends on how many "moves" the user is prepared to prepay for. | ||
- Ask the user to sign this transaction with their normal wallet, and submit it to the network using the library of your choice. | ||
- Once the receipt is received, the session key is automatically active and ready to use. | ||
|
||
#### The game | ||
|
||
After activation of the SK, create a transaction for each move, but don't ask the user to sign them. | ||
Instead, submit them to the network unsigned using the RPCs: ``eth_sendRawTransaction`` or ``eth_sendTransaction``. | ||
After creation of the SK, create a transaction for each move, but don't ask the user to sign them. | ||
Instead, submit them to the network unsigned using the RPC `eth_getStorageAt` with address `0x0000000000000000000000000000000000000005` (SendUnsignedTxCQMethod) and the following parameters: | ||
|
||
```json | ||
{ | ||
"sessionKeyAddress": "0x...", // The session key address | ||
"tx": "base64_encoded_transaction" // The unsigned transaction encoded as base64 | ||
} | ||
``` | ||
|
||
Because the SK is active, the platform will sign the transactions on behalf of the user. | ||
|
||
As a game developer, you are responsible to keep track of the balance of the SK. You can also query the network for the balance of the address. | ||
If the SK runs out of balance, you have to ask the user to move more funds to the SK. | ||
|
||
|
||
#### Managing Session Keys | ||
|
||
TEN provides additional RPC endpoints for managing session keys: | ||
|
||
- `sessionkeys_Delete` - Permanently removes the session key. This can only be called after deactivating the key. This is useful when you want to clean up unused session keys or when a user wants to completely remove their session key. | ||
|
||
- `sessionkeys_List` - Returns the address of the current session key, or an empty response if no session key exists. This is useful for checking if a user has an active session key and getting its address. | ||
- `eth_getStorageAt` with address `0x0000000000000000000000000000000000000004` (DeleteSessionKeyCQMethod) - Permanently removes the session key. This requires the following parameters: | ||
|
||
```json | ||
{ | ||
"sessionKeyAddress": "0x..." // The session key address to delete | ||
} | ||
``` | ||
|
||
The session key management endpoints can be called through both HTTP API and RPC methods. For RPC, you can use `eth_getStorageAt` with specific addresses: | ||
|
||
- Create: `0x0000000000000000000000000000000000000003` | ||
- Activate: `0x0000000000000000000000000000000000000004` | ||
- Deactivate: `0x0000000000000000000000000000000000000005` | ||
- Delete: `0x0000000000000000000000000000000000000006` | ||
- List: `0x0000000000000000000000000000000000000007` | ||
- Delete: `0x0000000000000000000000000000000000000004` | ||
- Send Unsigned Transaction: `0x0000000000000000000000000000000000000005` | ||
|
||
#### Finishing the game | ||
|
||
When a game ends, you have to move the remaining funds back to the main address and delete the session key. | ||
|
||
#### Finishing the game | ||
|
||
When a game ends, you have to move the remaining funds back to the main address and deactivate the key. | ||
- Create a transaction that moves the funds back from the SK to the main address. Submit it unsigned using the SendUnsignedTxCQMethod, because the funds are controlled by the SK. | ||
- Call `eth_getStorageAt` with address `0x0000000000000000000000000000000000000004` (DeleteSessionKeyCQMethod) to permanently remove the session key. | ||
|
||
|
||
- create a Tx that moves the funds back from the SK to the main address. Submit it unsigned, because the funds are controlled by the SK. | ||
- call the RPC: ``sessionkeys_Deactivate``- from now on, unsigned transactions will no longer be signed by the SK. | ||
### Example Implementation | ||
|
||
Here's a complete example of how to implement session keys in a JavaScript dApp: | ||
|
||
```javascript | ||
// 1. Create a session key | ||
async function createSessionKey() { | ||
const response = await fetch("https://testnet.ten.xyz/v1/", { | ||
method: "POST", | ||
headers: { "Content-Type": "application/json" }, | ||
body: JSON.stringify({ | ||
jsonrpc: "2.0", | ||
method: "eth_getStorageAt", | ||
params: ["0x0000000000000000000000000000000000000003", "0x0", "latest"], | ||
id: 1, | ||
}), | ||
}); | ||
|
||
const data = await response.json(); | ||
return data.result; // Returns the session key address | ||
} | ||
|
||
// 2. Fund the session key (user signs this transaction) | ||
async function fundSessionKey(sessionKeyAddress, amount) { | ||
// This would be a normal transaction signed by the user's wallet | ||
// transferring ETH to the session key address | ||
} | ||
|
||
// 3. Send unsigned transactions using the session key | ||
async function sendUnsignedTransaction(sessionKeyAddress, unsignedTx) { | ||
const txBase64 = btoa(JSON.stringify(unsignedTx)); // Convert to base64 | ||
|
||
const response = await fetch("https://testnet.ten.xyz/v1/", { | ||
method: "POST", | ||
headers: { "Content-Type": "application/json" }, | ||
body: JSON.stringify({ | ||
jsonrpc: "2.0", | ||
method: "eth_getStorageAt", | ||
params: [ | ||
"0x0000000000000000000000000000000000000005", | ||
JSON.stringify({ | ||
sessionKeyAddress: sessionKeyAddress, | ||
tx: txBase64, | ||
}), | ||
"latest", | ||
], | ||
id: 1, | ||
}), | ||
}); | ||
|
||
const data = await response.json(); | ||
return data.result; // Returns the transaction hash | ||
} | ||
|
||
// 4. Delete the session key when done | ||
async function deleteSessionKey(sessionKeyAddress) { | ||
const response = await fetch("https://testnet.ten.xyz/v1/", { | ||
method: "POST", | ||
headers: { "Content-Type": "application/json" }, | ||
body: JSON.stringify({ | ||
jsonrpc: "2.0", | ||
method: "eth_getStorageAt", | ||
params: [ | ||
"0x0000000000000000000000000000000000000004", | ||
JSON.stringify({ | ||
sessionKeyAddress: sessionKeyAddress, | ||
}), | ||
"latest", | ||
], | ||
id: 1, | ||
}), | ||
}); | ||
|
||
const data = await response.json(); | ||
return data.result; // Returns 0x01 for success, 0x00 for failure | ||
} | ||
``` | ||
|
||
### Key Benefits | ||
|
||
- **No Activation/Deactivation**: Session keys are implicitly active when created and remain active until deleted | ||
|
||
- **Multiple Session Keys**: Users can have up to 100 session keys (one per dApp) | ||
- **Simplified API**: Uses `eth_getStorageAt` with custom addresses instead of separate RPC methods | ||
- **Secure Management**: Session keys are managed by TEEs, eliminating the risk of losing private keys | ||
- **Base64 Transaction Encoding**: Unsigned transactions must be base64 encoded when sent via SendUnsignedTxCQMethod | ||
|
||
|
||
## Game Security | ||
|
||
Every on-chain game developer knows that every move that relies on entropy must be executed in two steps. | ||
Every on-chain game developer knows that every move that relies on entropy must be executed in two steps. | ||
|
||
Imagine you implement an on-chain coin flip game. The player pays 0.1ETH to choose `Heads` or `Tails`. | ||
Imagine you implement an on-chain coin flip game. The player pays 0.1ETH to choose `Heads` or `Tails`. | ||
If they win, they receive 0.2ETH, otherwise they lose the 0.1ETH. | ||
Even if randomness is unpredictable, this simple game can be exploited in several ways: | ||
|
||
- The attacker can create a “proxy” smart contract to play on their behalf. Using a similar mechanism to flash loans in DeFi: the proxy is programmed to make multiple actions, and only “commit” if it can obtain a profit. In our case, if the coin flip is losing, the proxy can just revert. The only cost will be the gas burned. | ||
- The attacker can create a “proxy” smart contract to play on their behalf. Using a similar mechanism to flash loans in DeFi: the proxy is programmed to make multiple actions, and only “commit” if it can obtain a profit. In our case, if the coin flip is losing, the proxy can just revert. The only cost will be the gas burned. | ||
- Transactions consume gas, and the gas cost can inadvertently reveal information. For instance, if a winning move is more computationally intensive than a losing one, players could deduce optimal moves by estimating gas costs for various actions. | ||
|
||
The typical solution is to use a commit-reveal scheme. The player commits to a move, and then reveals it. This way, the player can't change their mind after seeing the result. | ||
This solution has the major drawback that it introduces extra complexity, latency and cost. | ||
|
||
### The on-block-end callback | ||
### The on-block-end callback | ||
|
||
The best solution is to decouple the move from the execution without increasing the latency or the cost. | ||
This way, the side-channel attacks are no longer possible because the move is not executed immediately. | ||
|
@@ -401,7 +491,7 @@ contract CoinFlip { | |
modifier onlyTenSystemCall() { | ||
modifier onlyTenSystemCall() { | ||
require(msg.sender == address(tenCallbacks)); | ||
_; | ||
} | ||
|
@@ -411,7 +501,7 @@ contract CoinFlip { | |
tenCallbacks = TenCallbacks(_tenCallbacksAddress); | ||
} | ||
// Function to initiate a coin flip. | ||
// Function to initiate a coin flip. | ||
// Notice how it doesn't execute the coin flip directly, but instead registers a callback. | ||
function flipCoin(bool isHeads) external payable { | ||
// Assume doFlipCoin costs 50_000 gas; | ||
|
@@ -459,6 +549,6 @@ contract CoinFlip { | |
(bool success, ) = payable(msg.sender).call{value: refundAmount}(""); | ||
require(success, "Transfer failed"); | ||
} | ||
} | ||
``` | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure "CreateSessionKeyCQMethod" means anything for an external dev
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true, removed