-
Notifications
You must be signed in to change notification settings - Fork 355
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
[Draft] Add how to verify l2 state on l1 page #1067
base: master
Are you sure you want to change the base?
Changes from all commits
34054c5
0771b49
921fbc0
699b874
60714d9
e1ce475
88a8195
db4e210
f5ae697
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 |
---|---|---|
@@ -0,0 +1,173 @@ | ||
--- | ||
title: 'How to verify child chain state on parent chain' | ||
description: Learn how to verify child chain state on its parent chain | ||
user_story: As a developer, I want to understand how to verify child chain state on parent chain. | ||
content_type: how-to | ||
--- | ||
|
||
Arbitrum implements a **fraud proof** system that ensures that the state of any given child chain is safely maintained by its parent chain. In this system, a validator is responsible for periodically transmitting information about the child chain's state to its parent chain. | ||
This information is included in "rollup blocks", which we refer to as **RBlocks** throughout our docs. See [Inside Arbitrum Nitro](../inside-arbitrum-nitro/inside-arbitrum-nitro.mdx#arbitrum-rollup-protocol) to learn more about RBlocks. | ||
|
||
These RBlocks are effectively _assertions about the impact that a series of child chain blocks (containing transactions) ought to have on its parent chain's state_. When received by the parent chain, these RBlocks are decoded by a contract hosted by the parent chain (an "on-chain contract"); this information can then be optionally relayed to off-chain tools for arbitrary purposes (for example, off-chain validation). | ||
|
||
Before we begin, we will introduce the key component: rblock, assertion and send roots. | ||
|
||
# Rblock and Assertion | ||
|
||
The rollup contract contains a series of components used to maintain the operation of the layer2 network, including rblocks. | ||
|
||
Here is what an rblock contains: | ||
|
||
``` | ||
struct Node { | ||
// Hash of the state of the chain as of this node | ||
bytes32 stateHash; | ||
// Hash of the data that can be challenged | ||
bytes32 challengeHash; | ||
// Hash of the data that will be committed if this node is confirmed | ||
bytes32 confirmData; | ||
// Index of the node previous to this one | ||
uint64 prevNum; | ||
// Deadline at which this node can be confirmed | ||
uint64 deadlineBlock; | ||
// Deadline at which a child of this node can be confirmed | ||
uint64 noChildConfirmedBeforeBlock; | ||
// Number of stakers staked on this node. This includes real stakers and zombies | ||
uint64 stakerCount; | ||
// Number of stakers staked on a child node. This includes real stakers and zombies | ||
uint64 childStakerCount; | ||
// This value starts at zero and is set to a value when the first child is created. After that it is constant until the node is destroyed or the owner destroys pending nodes | ||
uint64 firstChildBlock; | ||
// The number of the latest child of this node to be created | ||
uint64 latestChildNumber; | ||
// The block number when this node was created | ||
uint64 createdAtBlock; | ||
// A hash of all the data needed to determine this node's validity, to protect against reorgs | ||
bytes32 nodeHash; | ||
} | ||
``` | ||
|
||
When creating a new rblock, a new assertion will be made too: | ||
|
||
``` | ||
struct Assertion { | ||
ExecutionState beforeState; | ||
ExecutionState afterState; | ||
uint64 numBlocks; | ||
} | ||
``` | ||
|
||
As we can see above, an rblock has a series of field, they are useful when validators try to challenge or confirm this rblock. | ||
What we can use here is the `confirmData`, the `confirmData` is the keccak256 hash of child chain block Hash and sendRoot. | ||
As for Assertion, it has 2 `ExecutionState` which is the start state and the end state of this assertion, and `ExecutionState` contains the information about child chain blockhash and related sendroot, so we can extract `blockhash` from there. | ||
|
||
# Send roots | ||
|
||
The send root mapping is stored in the outbox contract. This mapping is used to store the Merkle root of each batch of child chain -> parent chain transactions called send root and its corresponding child chain block hash. | ||
|
||
When an rblock is confirmed, the corresponding send root will be recorded to outbox contract from rollup contract so when an user wants to triger the child chain -> parent chain transaction on parent chain the transaction requests can be verified. | ||
|
||
``` | ||
mapping(bytes32 => bytes32) public roots; // maps root hashes => child chain block hash | ||
``` | ||
|
||
This mapping will save the `blockhash`, so we can get the child chain blockhash from the outbox contract too. | ||
|
||
# Verify child chain state on parent chain | ||
|
||
Assume that there is a contract called `foo` on child chain, and its contract address is `fooAddress`, now we want to prove its state value at storage `slot`. | ||
|
||
To verify the state, we need a Merkle Trie Verifier contract, one example is [Lib_MerkleTrie.sol](https://github.com/ethereum-optimism/optimism-legacy/blob/8205f678b7b4ac4625c2afe351b9c82ffaa2e795/packages/contracts/contracts/libraries/trie/Lib_MerkleTrie.sol). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have optimism's repo refer here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... is an example available that does not point to a competitor's assets? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's open source so I think we're happy to use it |
||
|
||
## 1. How to verify a confirmed child chain block hash | ||
|
||
For the security of verification, we will use the latest confirmation instead of the latest proposed rblock for verification: | ||
|
||
- Obtain the latest confirmed rblock from rollup contract: `nodeIndex = rollup.latestConfirmed()`, this step will return the corresponding rblock number: `nodeIndex` | ||
- Filter the event with the obtained rblock number: `nodeEvent = NodeCreated(nodeIndex)`, and get the corresponding assertion information: `assertion = nodeEvents[0].args.assertion` | ||
- Fetch blockhash via `blockhash = GlobalStateLib.getBlockHash(assertion.afterState.globalState)` (As mentioned above, you can also get the block hash from the outbox contract) | ||
- Fetch sendRoot via `sendRoot = GlobalStateLib.getSendRoot(assertion.afterState.globalState)` | ||
- After getting the blockhash, we need to compare it with the confirmdata in rblock, to get the confirm data: `confirmdata = keccak256(solidityPack(['bytes32','bytes32'], [blockHash, sendRoot]))` | ||
- Get the corresponding rblock: `rblock = rollup.getNode(nodeIndex)` | ||
- Compare if they have the same value: `rblock.confirmData == confirmdata` | ||
|
||
## 2. Proof the state root belong to the child chain block hash by supplying the blockheader | ||
|
||
After we obtain the block hash, we can obtain the corresponding block information from child chain provider: `l2blockRaw = eth_getBlockByHash (blockhash)` | ||
|
||
Next, we need to manually derive blockhash by hashing block header fields. | ||
|
||
``` | ||
blockarray = [ | ||
l2blockRaw.parentHash, | ||
l2blockRaw.sha3Uncles, | ||
l2blockRaw.miner, | ||
l2blockRaw.stateroot, | ||
l2blockRaw.transactionsRoot, | ||
l2blockRaw.receiptsRoot, | ||
l2blockRaw.logsBloom, | ||
BigNumber.from(l2blockRaw.difficulty).toHexString(), | ||
BigNumber.from(l2blockRaw.number).toHexString(), | ||
BigNumber.from(l2blockRaw.gasLimit).toHexString(), | ||
BigNumber.from(l2blockRaw.gasUsed).toHexString(), | ||
BigNumber.from(l2blockRaw.timestamp).toHexString(), | ||
l2blockRaw.extraData, | ||
l2blockRaw.mixHash, | ||
l2blockRaw.nonce, | ||
BigNumber.from(l2blockRaw.baseFeePerGas).toHexString(), | ||
] | ||
``` | ||
|
||
- Calculate the block hash to verify whether the information in the obtained block is correct: `calculated_blockhash = keccak256(RLP.encode(blockarray))` | ||
- Verify whether the block hash is same with what we got from assertion or outbox contract: `calculated_blockhash === blockHash` | ||
|
||
If it is same, it can be used to prove that the information in the block header, especially the stateroot, is correct. | ||
|
||
## 3. Proof the account storage inside the state root | ||
|
||
After we obtain the correct state root, we can continue to verify the storage slot. | ||
|
||
- First, we need to obtain the proof of the corresponding state root from child chain: | ||
|
||
``` | ||
proof = l2provider.send('eth_getProof', [ | ||
fooAddress, | ||
[slot], | ||
{blockHash} | ||
]); | ||
``` | ||
|
||
- Get account proof: `accountProof = RLP.encode(proof.accountProof)` | ||
- Get proofKey: `proofKey = ethers.utils.keccak256(fooAddress)` | ||
- Call the verifier contract to verify: | ||
|
||
``` | ||
[acctExists, acctEncoded] = verifier.get( | ||
proofKey, accountProof, stateroot | ||
) | ||
``` | ||
|
||
- Check for equality: `acctExists == true` | ||
|
||
## 4. Proof the storage slot is in the account root | ||
|
||
- Get storage root: `storageRoot = RLP.decode(acctEncoded)[2]` | ||
- Get storage slot key: `slotKey = ethers.utils.keccak256(slot)` | ||
- Get storageProof: `storageProof = ethers.utils.RLP.encode((proof.storageProof as any[]).filter((x)=>x.key===slot)[0].proof)` | ||
- Call the merkle verifier contract to verify: | ||
|
||
``` | ||
const [storageExists, storageEncoded] = await verifier.get( | ||
slotKey, storageProof, storageRoot | ||
) | ||
``` | ||
|
||
- Check for equality: `storageExists == true` | ||
- Obtain the value of the storage as `slot`: `storageValue = ethers.utils.RLP.decode(storageEncoded)` | ||
|
||
Then we can successfuly prove and get a certain state value at a specific block height on child chain through parent chain. | ||
|
||
### Let's check this value on child chain directly | ||
|
||
- Call child chain rpc provider to get the value of the corresponding block number: `actualValue = l2provider.getStorageAt(fooAddress, slot, l2blockRaw.number)` | ||
- Check for equality: `storageValue === BigNumber.from(actualValue).toHexString()` |
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.
There's some duplication between this content and The Rollup Chain in Inside Arbitrum Nitro.
This is also explaining how things work, not how you, dear developer, can do something.
One option would be to:
It could also be useful to explicitly state - when and why would a developer need to complete these steps? When do they need to care about manually verifying state? Is this a "just in case you don't trust the underlying protocol" thing? I imagine some developers may expect this to be baked into the protocol and effectively abstracted away from the happy path, so getting more precise about the "user story" above can help us align on a more precisely targeted content objective.
As a developer who ______, I want to learn how to ________ when/because ________
I'll pause this review until we're aligned on the above nits 👍