# How to implement SPV of Arbitrum with Solidity ###### tags: `Public` Author: celestialRiver ## 1. Antecedent Knowledge: * Arbitrum is divided into *[Classic stack and Nitro stack](https://developer.offchainlabs.com/why-nitro#nitro-vs-classic)*, which can also be called system, hereinafter referred to as’ Classic’ and ‘Nitro’ respectively. * Since August 31, 2022, no public arbitrum chain will continue to use the Arbitrum *[Classic](https://github.com/OffchainLabs/arbitrum)* stack, but the Arbitrum *[Nitro](https://github.com/OffchainLabs/nitro)* stack will be used instead * *[Definition of Patricia Merkle Tree(MPT)](https://ethereum.org/en/developers/docs/data-structures-and-encoding/patricia-merkle-trie/)* ## 2. Life Cycle ### 2.1 Arbitrum Sequencer ![](https://hackmd.io/_uploads/BkSj5wZcs.png) Source: [*offchainlabs*](https://developer.offchainlabs.com/assets/images/seq-then-exec-0ec8fef09d47390cee2d6c2f7b5634d3.png) ### 2.2 TxHash ``` TxHash => InboxMessageg => SequencerMessage => SequencerBatchItem => SequencerFeedItem => BroadcastFeedMessage => BroadcastMessage => SubmissionInputData ``` * In *SequencerBatchItem => SequencerFeedItem* part, Classic and Nitro both use **base64** format to process data. * In *SequencerFeedItem => BroadcastFeedMessage* part, Nitro uses a general data compression algorithm called **brotli** to compress batches --- ## 3. Concrete Implementation ### 3.1 Procedures ```solidity 1.TxHash => InputData => SubmissionInputData //Prove that the InputData generated based on the TxHash submitted by the user is included in the SubmissionInputData provided by the local client 2.SubmissionInputData => SubmissionTxHashRLP => NewSubmissionTxHash //Prove that the NewSubmissionTxHash generated by SubmissionTxHashRLP based on SubmissionInputData is consistent with the SubmissionTxHash submitted by the user 3.NewSubmissionTxHash + "MPT_proof" => L1Blockhash => Next //Prove that the generated NewSubmissionTxHash is proved by MPT in combination with the block data and the L1Blockhash is passed to the next process 4.L1Blockhash + "OtherProof" => NowL1BlockHash //Prove that L1Blockhash is a past block of NowL1BlockHash ``` ### 3.2 Details The user needs to provide TxHash and SubmissionTxHash. ***3.2.1 TxHash => InputData => SubmissionInputData*** ***Purpose***: *To verify the validity of an L2 transaction in L1 transaction parameters.* ***Analysis***: *In view of the importance of this step, we need to analyze this step in detail.* --- > **Classic** The following proof methods are only applicable to ***Classic***, but they are also prerequisites for understanding ***Nitro*** * Suggest a transaction on Arbitrum: ***[0x6a1d](https://arbiscan.io/tx/0x6a1d569ca0a8a64d868092ed42ad7d194293c3059cf9780991174f43dd8d3ba5)***, record the hash as ***TxHash0x6a1d*** * Batches containing ***TxHash0x6a1d*** are packaged, compressed, and published to L1 by the Sequencer, and that hash is recorded as ***[SubmissionTxHash0xb150](https://etherscan.io/tx/0xb15092818da5384eb1e846dd1b1f4b92065cd1b71482867c456960a8c67367b2)*** * The ***InputData*** can be obtained through ***SubmissionTxHash0xb150***, which is recorded as ***SubmissionInputData0xb150***. * The corresponding ***Transaction Info*** can be obtained through ***TxHash0x6a1d***, which is recorded as ***TxInfo0x6a1d*** ```solidity // TxInfo0x6a1d { hash: '0x6a1d569ca0a8a64d868092ed42ad7d194293c3059cf9780991174f43dd8d3ba5', type: 0, accessList: null, blockHash: '0x4dd97d8cbb5ef3e4fdddbc99ae813e8713f478902038dee8d1c672117ed35cfd', blockNumber: 17237171, transactionIndex: 0, confirmations: 22649108, from: '0xC0aF3690cCEdcc353AEb33cC0bB3eF63EF1070ba', gasPrice: BigNumber { _hex: '0x1c16ab5a', _isBigNumber: true }, gasLimit: BigNumber { _hex: '0x079366', _isBigNumber: true }, to: '0xC266551ef06b976AbC403F998Bd3Ae2398403937', value: BigNumber { _hex: '0x07911ad5a14714', _isBigNumber: true }, nonce: 2, data: '0x', r: '0x8698fd9762461c6b042007e99a236d50f6cdaf5b6498e56832cbdfb470de6669', s: '0x6058d9801636a3f6c90db74b55c8dc611030715989a3b896e1fa66b30d8f2c9e', v: 84358, creates: null, chainId: 42161, wait: [Function (anonymous)] } ``` * ***[L2Message data structure](https://github.com/OffchainLabs/arbitrum/blob/f565e0571b6883bd7021f926ba823e9d0b609b5f/packages/arb-evm/message/l2Message.go#L534)*** and ***Tx Info processing*** in Arbitrum: ```solidity txData := []interface{}{ t.SequenceNum, //Nonce t.GasPrice, //GasPrice t.GasLimit, //GasLimit dest, //To t.Payment, //Value t.Calldata, //Data v, //V t.R, //R t.S, //S } ``` * [Handling **Nonce, GasPrice, GasLimit, To, Value, Data** from Tx Info:](https://github.com/OffchainLabs/arbitrum/blob/f565e0571b6883bd7021f926ba823e9d0b609b5f/packages/arb-evm/message/data.go#L245) ```solidity func encodeUnsignedTx(tx CompressedTx) ([]byte, error) { nonceData, err := rlp.EncodeToBytes(tx.SequenceNum) if err != nil { return nil, err } gasPriceData, err := rlp.EncodeToBytes(tx.GasPrice) if err != nil { return nil, err } gasLimitData, err := rlp.EncodeToBytes(tx.GasLimit) if err != nil { return nil, err } paymentData, err := encodeAmount(tx.Payment) if err != nil { return nil, err } var data []byte data = append(data, nonceData...) data = append(data, gasPriceData...) data = append(data, gasLimitData...) if tx.To == nil { data = append(data, 0x80) } else { destData, err := tx.To.Encode() if err != nil { return nil, err } data = append(data, destData...) } data = append(data, paymentData...) data = append(data, tx.Calldata...) return data, nil } ``` * [Handling ***V,R,S*** from Tx Info:](https://github.com/OffchainLabs/arbitrum/blob/f565e0571b6883bd7021f926ba823e9d0b609b5f/packages/arb-evm/message/data.go#L245) ```solidity func encodeECDSASig(v byte, r, s *big.Int) []byte { data := make([]byte, 0, 65) data = append(data, ethmath.U256Bytes(new(big.Int).Set(r))...) data = append(data, ethmath.U256Bytes(new(big.Int).Set(s))...) data = append(data, v) return data } ``` * By applying RLP(Recursive Length Prefix) to partial data of ***TxInfo0x6a1d*** and processing ***V, R, S***, we shall obtain the following data, noted as ***TxInfoRLP0x6a1d*** ``` // TxInfoRLP0x6a1d 02841c16ab5a8307936694c266551ef06b976abc403f998bd3ae23984039378707911ad5a14714008698fd9762461c6b042007e99a236d50f6cdaf5b6498e56832cbdfb470de66696058d9801636a3f6c90db74b55c8dc611030715989a3b896e1fa66b30d8f2c9e ``` * Corresponding: ```solidity 02 // SequenceNum/Nonce 84 // 0x84 - 0x80 = 4 bytes 1c16ab5a // GasPrice 83 // 0x83 - 0x80 = 3 bytes 079366 // GasLimit 94 // 0x94 - 0x80 = 20 bytes c266551ef06b976abc403f998bd3ae2398403937 // To 87 // 0x87 - 0x80 = 7 bytes 07911ad5a14714 // Value 00 // V 8698fd9762461c6b042007e99a236d50f6cdaf5b6498e56832cbdfb470de6669 // R 6058d9801636a3f6c90db74b55c8dc611030715989a3b896e1fa66b30d8f2c9e // S ``` * Find it in ***SubmissionInputData0xb150***: ![](https://hackmd.io/_uploads/SJ84TvZ5j.png) * Hence, In ***Classic***, you can use ***TxHash*** to prove whether an L2 transaction exists in L1, that is, you can verify the validity of an L2 transaction in L1 transaction parameters. --- > **Nitro** * The life cycle of *Nitro* is basically the same as that of *Classic*, but in the process of ***SequencerFeedItem => BroadcastFeedMessage***, the ***brotli*** algorithm is used to compress the entire ***batch***. Because of this, the proof method applicable to Classic is not applicable to *Nitro*. * Certification scheme applicable to Nitro (Scheme 3 is valid after testing): 1. Get ***SubmissionInputData0xb150***, deconstruct its data, get ***Batch*** data in it, and finally judge whether it contains ***TxInfoRLP0x6a1d***. (Failed) 2. Get all other ***txs*** in ***the same Batch block*** of ***Tx0x6a1d***, base64 all of them, and then assemble them into Batch data. Use ***brotli*** to compress them. Finally, compare the compressed data with ***SubmissionInputData0xb150***. (Failed) 3. Running an ***[Arbitrum Nitro Node](https://developer.offchainlabs.com/node-running/running-a-node)*** can directly synchronize the L1 status of each L2 transaction without proving through ‘Sequencer’ and other steps. Arguments of Nitro: * ***TxHash***: Transaction from Arbitrum. (User provided) * ***SubmissionTxHash***: SubmissionTx on Ethereum L1. (User provided) * ***SubmissionInputData***: InputData from SubmissionTxHash. (Local client) * Run the ***[Arbitrum Nitro Node](https://developer.offchainlabs.com/node-running/running-a-node)*** off-chain, supply the ***TxHash*** provided by the user, and prove whether the ***TxHash*** is included in ***SubmissionInputData*** through the node data. If it is proven, go to the next step. --- > Supplementary note — Assumption on verifying the effectiveness of TxHash: > * We plan to add a new step to verify the effectiveness of **TxHash**. This step will be after Step 1 and before Step 2 > * Since the transaction **status** of TxHash on L1 exists in the **Arbitrum Nitro Node**, it can be assumed that it is based on the data provided by the Arbitrum Nitro Node. > * Dropped the failed transactions, and build a Merkel tree off-chain by using the successful transaction hash. At the same time, build a Merkel tree on-chain. After the user provides **TxHash** and **proves** through the first step, verify the **MPT data** provided by the **TxHash** on-chain client. First, verify whether the **two Merkel trees** is consistent, and then verify whether **TxHash** exists in the Merkel tree. > > Question: > 1. In what form is the transaction hash packed and compressed? > 2. How often is it packed and compressed? > > Reference: > [cirom and snarkjs](https://learnblockchain.cn/article/1078) --- ***3.1.2 SubmissionInputData => SubmissionTxHashRLP => NewSubmissionTxHash*** ***Purpose**: To verify the validity of the submitted L1 transaction **parameter** that exists in the L1 transaction.* ***Arguments:*** * ***SubmissionTxHash***: from the last procedure * ***SubmissionInputData***: from the last procedure * ***SubmissionTxHashRLP***: RLP encode of SubmissionInputData ***Procedures:*** Upload ***SubmissionTxHash*** and ***SubmissionTxHashRLP*** on-chain for verification. Because ***SubmissionTxHashRLP*** contains ***SubmissionInputData***, ***Keccak256*** is used in the contract to calculate the hash of ***SubmissionTxHashRLP***, which is recorded as ***NewSubmissionTxHash***, and to verify whether ***NewSubmissionTxHash*** is consistent with ***SubmissionTxHash***. If it is consistent, the verification is passed. ```solidity //Solidity function validateRLPTxInfoBySubTxHash( bytes memory rlpTxInfo, bytes[] memory header ) internal view; ``` --- ***3.1.3 NewSubmissionTxHash + MPTproof => L1Blockhash => Next*** ***Purpose**: To verify the validity of the submitted **L1 transaction** that exists in the **L1 Block**.* ***Arguments:*** * ***SubmissionTxHashRLP***: from the last procedure * ***NewSubmissionTxHash***: from the last procedure * ***L1BlockNumberMPTProof***: ***MPT*** data from SubmissionTx’s L1 Block * ***L1BlockTxRootHash***: ***Root Hash*** from SubmissionTx’s L1 Block * ***L1BlockHash***: ***Block Hash*** from SubmissionTx’s L1 Block Upload five arguments on-chain for verification. The verification here is divided into four parts. If all four parts are passed, the verification is passed. 1. Verify the connection between ***SubmissionTxHashRLP*** and ***L1BlockNumberMPTProof*** ```solidity // Solidity function validateRLPTxInfoByProof( bytes memory rlpTxInfo, bytes[] memory proof ) internal view; ``` 2. Verify ***L1BlockTxRootHash*** ```solidity // Solidity function validateTxRootHash(bytes[] memory proof, bytes[] memory header) internal view returns (bytes32 rootHash); ``` 3. Verify ***L1BlockHash*** ```solidity // Solidity function validateBlockHash(bytes[] memory header) internal view; ``` 4. Verify ***L1BlockNumberMPTProof*** ```solidity // Solidity function validateMPTProof( bytes32 rootHash, bytes memory mptKey, bytes[] memory proof ) internal view returns (bytes memory value); ``` --- ***3.1.4 L1Blockhash + OtherProof => NowL1BlockHash*** ***Purpose**: To verify the validity of the **L1 Block** that exists in the **L1 Blockchain**.* ***Arguments:*** * ***L1Blockhash***: from the last procedure * ***L1BlockNumber***: corresponding block number * Here we are divided into two parts: > **Off-chain:** 1. Get the block information from ***L1BlockNumber*** to ***the latest block number***, and sort them in order, 1.. 2.. 3… n. 2. Process the Raw Data of the second block to get the ***RLP*** code, and then use ***Keccak256*** to hash it to get the ***BlockHash*** of the second block. 3. Compare this ***off-chain BlockHash*** with the ***on-chain BlockHash***. If the same, the verification is passed. 4. Get ***ParentHash*** from the second block, and compare this to ***the first BlockHash***. If the same, the verification is passed. 5. Prove to the ***n-th*** block in turn. 6. It is required that the calculation time of the off-chain part is less than ***1h***, and if it exceeds ***1h***, it will continue to prove from the n-th block to the latest block number. 7. Arguments: * ***ChainOffL1BlockHash***: BlockHash of the n-th block. * ***ChainOffL1BlockNumber***: BlockNumber of the n-th block. > ***On-chain:*** * Use ***blockhash (uint blockNumber)*** function in the contract to obtain the block hash with the block number of ***ChainOffL1BlockNumber***, which is recorded as ***NowL1BlockHash***. * Compare ***NowL1BlockHash*** is equal to ***ChainOffL1BlockHash***. If the same, the verification is passed. ```solidity // Solidity function validateBlockNumberhash( uint256 blockNumberPast, bytes32 blockHashPast ) internal view; ``` --- **3.1.5** The L2 transaction hash submitted by the user can be considered **valid** after the above **4** steps are proven.