Skip to content
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

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
171 changes: 171 additions & 0 deletions arbitrum-docs/devs-how-tos/how-to-get-l2block-on-l1.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
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 fraud proof system. This system can ensure that the state of child chain will be safely maintained by parent chain. In this system, the validator will send the state information on child chain to parent chain from time to time.
Jason-W123 marked this conversation as resolved.
Show resolved Hide resolved
Used by the challenge system, those information is included in a structure called rblock and assertion (see the [documentation](../inside-arbitrum-nitro/inside-arbitrum-nitro.mdx#arbitrum-rollup-protocol) for details), so we can read and decode the rblock/assertion on parent chain to extract the information which can be used for the parent chain on-chain contract or off-chain tool validation.
Jason-W123 marked this conversation as resolved.
Show resolved Hide resolved

Before we begin, we will introduce the key component: rblock, assertion and send roots.

# Rblock and Assertion
Copy link
Contributor

@symbolpunk symbolpunk Mar 7, 2024

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:

  1. Merge all of the information that explains "how it works" into the above-linked Inside Arbitrum Nitro concept document (so we're not fragmenting this content across multiple docs that may drift apart as Nitro evolves).
  2. Provide a condensed version of this upfront, linking to Inside Nitro.
  3. Jump right into "How to verify a confirmed child chain block hash".

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 👍


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).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have optimism's repo refer here?

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

@hkalodner hkalodner Mar 8, 2024

Choose a reason for hiding this comment

The 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()`
5 changes: 5 additions & 0 deletions website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ const sidebars = {
label: 'Estimate gas',
id: 'devs-how-tos/how-to-estimate-gas',
},
{
type: 'doc',
label: 'Verify state on parent chain',
id: 'devs-how-tos/how-to-get-l2block-on-l1',
},
{
type: 'doc',
label: 'Chains and testnets',
Expand Down