In the recent [blog](https://vitalik.ca/general/2023/06/09/three_transitions.html) post by Vitalik, he discusses the evolving landscape of Ethereum as it concurrently undergoes three significant transitions. One of the pivotal transitions highlighted is the wallet transition. Ideally, there is a move towards everyone adopting smart contract wallets as a means for enhanced security. We at SoulWallet aims to simplify the wallet transition for users. However, there are many chanllenges as well. * Smart contract wallets introduce increased complexity, making it challenging to maintain the same address across Layer 1 (L1) and various Layer 2 (L2) networks. [More details](https://safe.mirror.xyz/4GcGAOFno-suTCjBewiYH4k4yXPDdIukC5woO5Bjc4w) * Smart contract wallets on each chain have their unique state and data storage. When users employ social recovery to execute key changes, they are required to carry out the recovery process separately on each chain. This means that the guardian's address must be accessible on every individual chain. Only then can the guardians perform recovery signatures for users. Addressing these issues is indeed challenging. However, Vitalik has proposed an elegant solution that demonstrates reasonable effectiveness: an architectural approach that distinguishes between verification logic and asset holdings. In the keystore implementation, each user is equipped with a "keystore contract" that stores the user's verification key along with its alteration rules (such as social recovery). Smart contract wallets are deployed on L1 and numerous L2s, designed to retrieve the verification key through cross-chain communication. Drawing inspiration from this, we at SoulWallet have developed our own interpretation and implementation of the keystore solution. ## Keystore desgin overview The keystore design is aiming to streamline the management of verification keys of the smart contract wallet across Ethereum L1 and L2s. It integrates the separation of verification logic and asset holdings into a single cohesive system. This architecture allows users to maintain their verification keys on the L1 Keystore contract. Wallets created on various L2s can then sync with the latest verification key from the L1 keystore contract, either during the wallet's creation or after its deployment. Furthermore, this system provides significant advantages for guardians managing recovery operations. They are no longer required to sign messages on each chain to recover a user's wallet. Instead, all guardian management operations are conducted on L1 only, which significantly reduces complexity and increases manageability. This architecture negates the need to consider whether the guardian's address is deployed or usable across different L2s. ![](https://hackmd.io/_uploads/rJPCyfA5h.png) The diagram above illustrates the overall workflow of the Keystore system, which can essentially be divided into three integral parts. 1. Keystore on L1, where it securely stores and manages the verification keys. It provides social recovery mechanisms to seamlessly replace verification keys when necessary, enhancing the system's flexibility and security for users. 2. The cross-layer communication, specifically the message passing from L1 to L2, facilitated by native Layer 2 bridges. The L2 needs to be aware of the `blockhash` on L1 as well as the Keystore `storagehash` that needs to be proven. This cross-layer communication, supported by Layer 2 bridges, enables the system to maintain the synchronization between L1 and L2 and ensures the integrity and validity of the proof process. 3. Keystore verification on L2, where through the use of Merkle proof, the system can validate the most recent verification key. Moreover, it facilitates the replacement of the signing key on the L2 wallet, ensuring that the L2 wallet remains synchronized with the L1 Keystore state. The tripartite workflow of the Keystore system collectively ensures the secure, efficient, and consistent management of the verification keys across both layers. ## Keystore on L1 implementaion Our SoulWallet's [keystore contract](https://github.com/proofofsoulprotocol/soul-wallet-contract/tree/develop/contracts/keystore/L1), implemented on L1, primarily serves two main functions: 1. It records the verification key of the user's wallet. 2. It allows users to modify the verification key based on social recovery. ### Keystore storage slot desgin When users create a wallet, the slot position of the wallet's verification key in the keystore contract is determined. the verification key in the L1 keystore can be lazy set, meaning users on either L1 or L2 don't need to interact with the keystore contract unless they need to perform specific operations, such as using social recovery to replace a key. The slot calculation uses the user's initial sign key, initial guardian hash, and guardian safe period. ```solidity /* initialGuardianHash = keccak256(rawGuardian list) guardianSafePeriod ####################################################################################################################################### # # # Why do we need `uint64 initialGuardianSafePeriod`? # # There are two implementations: # # 1. Set a fixed guardianSafePeriod for all users, such as 48 hours. # # 2. Provide the ability for each individual user to set their own guardianSafePeriod. # # Using a fixed guardianSafePeriod for all users may lead to future issues regarding availability and even security. # # This is especially true considering the diversity in social recovery scenarios in the future, as people in different # # regions may have different vacation habits (during which the guardian may be unable to respond to social recovery # # requests). Even centralized guardians may face similar issues, such as social recovery solution providers based on KYC, # # where each service provider may have inconsistent response times. # # Therefore, providing the option for each user to set their own independent guardianSafePeriod is necessary. # # # ####################################################################################################################################### */ slot = keccak256(abi.encode(initialKey, initialGuardianHash, guardianSafePeriod)); ``` ![](https://hackmd.io/_uploads/Sya4JYY92.png) Referring to the slot layout depicted in the keystore contract diagram above, the signing key for a specific wallet is stored in a precalculate slot with an offset of 0. This standardized arrangement enables an L2 to ascertain that a particular wallet's verification key is housed in a predetermined storage slot of the keystore contract. This specific slot can be leveraged to create a Merkle proof for the current verification key. ![](https://hackmd.io/_uploads/HkI0_r953.png) The proof for the verification key can be procured through the `eth_getProof` API, which returns the account and storage values of the specified account, inclusive of the Merkle proof. ### keystore replace key using social recovery In our technical design for the Keystore system, we have introduced a Social Recovery feature for replacing the verification keys within the Keystore contract. When a user's wallet needs to undergo social recovery, if it's the first time they're using this feature, the storage slot for the user's wallet in the Keystore will not have the corresponding verification key set yet. This is due to the design of the system: if the Keystore does not contain the user's verification key, the verification key for the L2 wallet is determined by the signing key from the initialization parameters at the time of wallet creation. Users are not required to interact with the Keystore contract when setting up their wallets for the first time. It's only when there's a need to replace the initial signing key that interaction with the Keystore becomes necessary. If a user loses their verification key or needs to replace the verification key via their guardians, they would then need to use the Social Recovery feature in the Keystore to set a new signing key. As per the previous explanations of how a wallet's storage slot is calculated in the Keystore, the user's guardians are using hashed value. This implies that the guardians will only be revealed on-chain when social recovery is used. ![](https://hackmd.io/_uploads/rJJmbIC52.png) ### keystore recovery by example Here is an example to get the verification key proof for a [wallet created](https://goerli.etherscan.io/address/0x93b2391cb4ac9ba0593f7ef65c6f34d20c40efa2) on goerli testnet. We can decode the initialized parameter on [tenderl](https://dashboard.tenderly.co/tx/goerli/0xd5f73c95d14c376a45608688a6e1270eef3f77bc1f0b1e05ee1f42d61e189c0e?trace=0.1.0.5.0). ![](https://hackmd.io/_uploads/B1fhxL59h.png) The values for initialKey, initialGuardianHash, and guardianSafePeriod can be extracted from the parameters provided during wallet creation. ![](https://hackmd.io/_uploads/SkIXWI95n.png) ```js initialKey = 0x00000000000000000000000043d1089285a94bf481e1f6b1a7a114acbc833796 initialGuardianHash= 0xfa99cd30d12a235169eef4f3d5c96fc1619c60bc9d8028dfea0f89c7ec5e3f27 guardianSafePeriod = 172800 ``` The calculation for the slot position follows: ```sh slot = keccak256(abi.encode(initialKey, initialGuardianHash, guardianSafePeriod)); ``` Compute the slot postion using keccak256: ```sh cast keccak 0x00000000000000000000000043d1089285a94bf481e1f6b1a7a114acbc833796fa99cd30d12a235169eef4f3d5c96fc1619c60bc9d8028dfea0f89c7ec5e3f27000000000000000000000000000000000000000000000000000000000002a300 0x30c04d81b7c8fcca55a64a5ffbf2ced4517744fd2b68928b8080a96465ec9f6f ``` then we know the [wallet](https://goerli.etherscan.io/address/0x93b2391cb4ac9ba0593f7ef65c6f34d20c40efa2) verification key is on slot postion `0x30c04d81b7c8fcca55a64a5ffbf2ced4517744fd2b68928b8080a96465ec9f6f` in the keystore contract. Using the `eth_getProof` API to get the slot position `0x30c04d81b7c8fcca55a64a5ffbf2ced4517744fd2b68928b8080a96465ec9f6f` merkel proof. The `eth_getProof` API returns two types of proofs: Account Proof and Storage Proof. * Account Proof: This represents the State Trie proof, a global structure updated each time a client processes a block. In the State Trie, a path corresponds to keccak256(ethereumAddress), and a value equates to RLP(ethereumAccount). An ethereumAccount is an array of four elements: [nonce, balance, storageRoot, codeHash]. The `storageRoot` is notable as it is the root of another Patricia Trie, the Storage Trie. In our keystore context, the account proof needs to retrieve the `storageRoot` of the keystore address, which is subsequently used for the storage proof. * Storage Proof: This represents the Storage Trie proof, where all contract data resides. In our keystore context, the storage proof verifies the verification key at specific storage positions of the keystore address. ```bash curl https://eth-goerli.g.alchemy.com/v2/demo \ -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_getProof","params":["0x7a7d3f06a81fe2e4a4c3955be074fe67d9dd91e3",["0x30c04d81b7c8fcca55a64a5ffbf2ced4517744fd2b68928b8080a96465ec9f6f"], "0x8F1626"],"id":1}' ``` We can get the Merkle proof of the verification key at the slot position `0x30c04d81b7c8fcca55a64a5ffbf2ced4517744fd2b68928b8080a96465ec9f6f` in the keystore contract. ```json { "jsonrpc": "2.0", "id": 1, "result": { "address": "0x7a7d3f06a81fe2e4a4c3955be074fe67d9dd91e3", "accountProof": ["0xf90211a053803e9d9a695a470ec0968ef6e55785be83829de66d74fdd19e9c892cdd46f4a096088de20781c436e922505f960c1735d3e171d916c6b9225782509f26806bbfa064726896ded62655e3194c5daca6eb7599dd7619afaf92a65fa04dab85318a94a00261aaeae801926463e0ca31a13a1409539bc6dbabead8df050432d2eb21ffcfa0517bbe63a49d914af54498d899fd95c23b4284af8de709c9b4a5e96cf4748a1da05c9ea5a79de5160e5d9d9cffaa56a12c9059f8c8a8095cda986602e669d12c70a0b511d11dc8b8a90407823c76523b4e7b582b497fea898d9a70ae491820f8ba28a02112ef41d205095790bdbfb2bf49d8c5f74cebe3bc609ce517754d3ab65d588ca09a115de4eef54feb38e40d9f7019cc7996114264ccc87ea4e0df6734ae0eec47a0b34098623f3f79370214147d8c4e7d48b2461bea563ad23bbb82189b893c1e48a00bace14828d738aa4fd4d1a4b59ee5f27634ac677e33c319cddb03f208262b72a01f6066857371543856b666cdd3feba5f8539b054907bc583fb259bdc8bbd5cf9a036cf46281a341a8a6a9e81b4b7c28db61a5f21f56b31a797c704994e75fb24c8a0827346cb0c6ef84d20550bead63cc91fdfdf9a396b75232a1d6f7e97704333cfa07b84897c2006a09b2579d76e88465fd8dcd950cb652ad0c3a54824fca0be1967a0e9dcf4e5807d2d0d6d9ea5052b4f92c13bbdf86dc4e717bb7c94be6895745faf80", "0xf90211a0d0a3fb6c8426104c600885bd9b19db0eb1ee187de67597a46561b0c1edf9927aa0ab216ef0251bbd4ad89ee4090724454c0fd96ccb044af7274212442d8916aec5a04967cacfbb212d24cf204ff4378763cd2beffb8239d8725b133734573f06603ba0b9c05cb3929a615b343020e76eaba8cf055e38cabc339a54c5fced9f5e267b1ea0a359d35b1accb445e09a00ed73013ff5727392aee473899c48ecdf9ff9f20416a0867c044483facd64dda901176c7b0306251dd5605c3969c2c2712de6b14a79fba080a4a7bc48dc8fda121b5fdfff1e6b7adb85000dce6294da1098477ef141bb19a088fd967a7a5830c0d9da81d5f839dfcaccefb9e0b1f8538d31e855262b85917aa0537eadc05b268c7ea49210cd983a147354274639434d0b37f47360ac716f70dca06f9b990712b5cbe732282e87d0d161e65419e592010d1782f318d6d41352f69ba082c281a2b90016ce1b09a472d56840862ca33bc6de76d57e6926d4f586e2e5eaa0e5f5b63990986d642c5173ed9bce52f9986ddc933ce8a1fb3bd30b65936af168a05e719f1377cf3261110c46b8c5c722a46023245eee8263807c093eae3a77b95da0f7fe730fe463d427737661b5cf2843ce89010f305b767dce95d4a3f5544a550ea004dc45d68e062bdc18452267d36799d87b1b620273b6f8f8434c0f06d0f0b0a9a030637902d848d595e1a9e6e35e598bdfd9a4cdc2d7487ff9d451bbeafad60bf980", "0xf90211a09cd40b513bc4adf5183c13e62da8ac8ab7ea1ae398cc7440d4286037cf22af2ea0545b4198cdc01bd2fbed4587c05b28945dc686c93dbf20f16c9adb2266f5326aa0135413fe296db4bf1fdc2269e051ba9fce78f22cdb36b6d664d842aa22449688a05a35c8281f747208c9fed891ef628931558109529975aa9c46a9dc0df7e2b1b4a0c30837323f0e42fb18b35cab20a1bab48ff704935592fb4c7ac2f69d29dcfa06a0400743630bee560c8c5abc6ee42c5b372d984165bc958e124323b4943572a0dda028d65bd8441ce112f9f346b3eea5bdc6684b52f266971a0d3b11218495946061a065a0df289403a96cfa3ba7824185dc9960414f6669519b5c383a4262d67e5b41a0bb6c8b8325a88bafb5968d33400d6fc463ea155b5b51fe332ed2e227e1357ce2a025b83e5720e422d6ba52572d225161d44d662464949ef999c27ec545a2b1c49fa042e41fe1233b82149e825e440de54a802d6a8db493b6df43150512572222b1daa0314cf7302e3c9af7bf894c3f569f361d0b6d972fdbd39ca9b522e61e0dd6a3b8a0b636ff51cad6cf3ad324506ecd41fad332d19cb17bd92ec0f4e417139ce7fa9ba0af330e8a622e0565856dd1ae774ffcd889e2c6334b2b274a530df46fc237e71ca02251b6379a5db3d91f8211d8c17bf405a1eab8888b4c758f0e7b232343aee15ca0a07b2dc4d2919e26766fe6470196c1fd90f4154b828ecd7c9d1482ad61e7506680", "0xf90211a02df2281c9f75d0b7bd5ab16cdf089eed114edee92420b27a88f44cb338f1e85fa02f35e4f3bf1f9d8d579805ce909db97771cf4bfff544f66edc02888ccdeee425a0cad89696ce01429ee07eeb504d36bf80e523ff2960cf9f639ac0c8aec25df23aa0eab69f9236ccac55b9825b6c680168873a930c807509390bab965a4ae7b837d8a0f4065fd45b3ffca2bd7dc377e3b5a64ade858df811d593a8a6951fad34188140a0dbb7f8081430fb21db781e393dc22bee0590476882a3fd295f3a9fdbe37ae6a3a0d1d4f9b5786f2ef7464cc54db3c746771a4f608892b3b7ba9145d3ced6a44629a09f0542fb27fa245dc51e50d3069042bc0774b713eebb3f260995fe31d9f284dea06cef7d3e8586ee17a4a654189a88a1d1d41af67a98a5fd6f5a2830d50fe1ec15a0d78a0c8c4ce0560640104bd915bd23ba2b9c476a5503dd427d8cb9ef87ec80d4a0aee1a2be9dabc7ee6091d19d19a31c6121f84c0fb8e0162a271a9d5bc1d45a0ba0f1be237618858210b7739ea49415361ba0ea4cbf573050cc622911bc809a803ea01588e319d699ac5c32c5f4d02fdfd8c157706df24fa40ab78cac50785ea18697a028e77b9179a96f0177e547c383561e9eedc69beb56cd3a978734ae407146498da0103d9e8f85143f2c5a9d49f9effb3080edd80cf57f4089c7c430ddbd3ad3d786a0aa892c5d83ddce56e941c6c2ae3c2ae9ca8cd53ec60985dfa372e0e5326c43c880", "0xf90211a0e41bf64445596b40748c950da36c1c56ce0e34b1bd750cc2274a5b39d93d7111a081d3eac911730120b2da2e9f8d229b30be5eafeba654fd2035a713454d8fbb8fa03e2e7e398fd69b3c1b85a502fa481b75c43bac12bea9079ff016a0287f2e30fba03680bbc8f49119c718ee0d9749333b9df21b282154fdaf4db71792e1eca1cad1a0e5a6d138b1b591bbc0c7466e90ebd44395d8f5a2123252e9c2a2af176708086aa0864a22d1e4f835d1d99e961622ab119f805c36b08aed8c18bd176fe11ba0f5d7a05bd1302fb5ca77508a661a26f330f9d53b6df46fe1dc8ef5feee98212e7f39e7a01e4bcab216b745b5b22b90cc4202802af1542bdc3d3d5496ae5561dbf478a271a01dd5d85cbea94c43e85e32acb27988fea20154547cee417eecd11e11e74de7aaa0210784ee98a443c5647317d649eaf2b63619deeca2f098e24901485fe1de9d78a0db4e6d5f93951f7e063a8cfb90d430b20c1b608fefc4ba5f5cd184dd154a995ea0a558b2a52b703d615319c7944e58a961d1a593dc795f82466a69d10f1b8872c3a0fd8b6420f420fac93d47e8df12e0e49e6049926aff5f572b1b30136c9ceb96a8a0c7b4570dddd9e21c5f79ed2f83c595f5ed953c27688d5efbd3975f80cb27812ea0abdcd372e8d45d6acb51e8fc73bb61565ee70d4733ee09f52730b53b3a0dbaf8a0da67eda97aae78b0a548379bfe6f850f2af4f7bdec9db13956fb9f44b14fd23180", "0xf901f1a010d08739e83b36b6961412d5cde580c058032e6a44126bbf25435a57554e12f1a07bd4587ab891775c30bb38c7b4846e3b58d5042ebd72b1e3e01e2e534e7c2b63a032b777447195b8491ddc9ed3446769f5f214f550ce2111c1cf543265f41aeaf5a0280e172c7580841dd0eb8209f051cd633a3a6b97729958affdfdfed2cb0f5ac2a0b222037efe6aec78fa8ed0807b7a5da7c4b4b167b36c9f25a91720cdd0615d55a03381fba8adf6286d3ba1fba0749977ddbf8f3aacf5faefd0e396ac5a43d2a894a0f93ba963bd12f2d3e3a56aa44be2b554fb1e2364964f60bf4fd0bc509fe4f658a04c6c807f43e9d65de71bd8b78428de19975fd1d9cf5fe40b40d7798bc5db4c2ba01ac0b585fb8b9a7d05063f901d38cfd02957f47fbb624c8a8092c933796bfd34a09b61d6604f3eafd86e896779e23867d4ee21f82d2bda3318fd4844f45ff11252a0233db780735e01b8550b779bdd6a491b2d525c5d959aec84d3877b29ad58ce05a08883c2c613c64769e215b64010f0ab2d37348b42995baa109fb129082d4c43aca03a2b568296f88d34d05babd35bd38c04625a121fac192b8e1a4f4f0da693d102a03d2292948a4a9172b7fd31eff517b3c7cc4218ef4bd111cabf8f3cfeee03d7fd80a0e1a3d53cf5a362472aad8629d8fdbe76e01ad23872b95af89fbc41e0ff82d23580", "0xf871a00af7fcd74735a55429e376044d6fa6d24a1990bb3a464b3d714a20f5ba75e7e780808080808080808080a07f992b1a0b5924409a1cd369b379b0c57059de89f1fe9b1a9bd36a9984cfd782808080a0b6b294a373b22f8f327ff8d241743884bab3c8692bbccc303744a97e99a7488780", "0xf8669d378d5a240eab142c63aa541e84135302ed710e24edc09da0dc120966e3b846f8440180a0550148708cbc5f574beaa196a007667b693d9862de5b8edf1cfc7860aab9a736a0867882272a050829439d92c1fc26f5a759404746428ee98e0079b660f98162c2"], "balance": "0x0", "codeHash": "0x867882272a050829439d92c1fc26f5a759404746428ee98e0079b660f98162c2", "nonce": "0x1", "storageHash": "0x550148708cbc5f574beaa196a007667b693d9862de5b8edf1cfc7860aab9a736", "storageProof": [{ "key": "0x30c04d81b7c8fcca55a64a5ffbf2ced4517744fd2b68928b8080a96465ec9f6f", "value": "0x68d9fb9427175af4d3c65fc9754c7383c70277bc", "proof": ["0xf8918080808080a02155da169730f5d618ef5c813543c49d66415688a7d0f7c81dd7237bce87fb9e808080a0be05134e40d2fc74e8e4e62b8766ffa4b44f402501c8b29c59c3e929b64162dc8080a060d8ed27c8022702fc0cadf1ce5d49a59371442e5f5ab15b008fdcd5a963418880a0aeeeee702f51c895c5c005029966cc66cf16052d7930446e36563dcde2f1a9b38080", "0xf7a03f025a1553395601db3a10ebbbbbb1e6e2a959fa5964eaa63a45a14e3dcb994b959468d9fb9427175af4d3c65fc9754c7383c70277bc"] }] } } ``` The result reveals that the signing key `0x68d9fb9427175af4d3c65fc9754c7383c70277bc` is stored at the slot position `0x30c04d81b7c8fcca55a64a5ffbf2ced4517744fd2b68928b8080a96465ec9f6f` within the keystore contract, accompanied by the corresponding Merkle proof in the proof field. Then the wallet on L2 can utilize this Merkle proof to validate the current verification keys present on the L1 keystore contract. ## The cross-layer communication The L2 wallets require access to the most recent key on the L1 Keystore in order to synchronize the verification key. This necessitates access to the `stateRoot` of the L1 block. The key on the L1 Keystore can be proven by using the Keystore's `storageRoot`, which is derived from the account proof of the `storageRoot` of the L1 block. However, it's important to note that the actual implementation of this scheme may vary across different L2s. ### Optimism implementation Fortunately, Optimism provides a pre-deployed contract that facilitates direct access to the latest L1 block through the use of the [L1Block contract](https://community.optimism.io/docs/developers/build/differences/#opcode-differences). we using this feature to record the L1 blockhash [on our contract](https://github.com/proofofsoulprotocol/soul-wallet-contract/blob/develop/contracts/modules/keystore/OptimismKeyStoreProofModule/OpKnownStateRootWithHistory.sol) ```solidity function setBlockHash() external { bytes32 l1BlockHash = L1_BLOCK.hash(); uint256 l1BlockNumber = L1_BLOCK.number(); require(l1BlockNumber != 0, "l1 block number is 0"); require(l1BlockHash != bytes32(0), "l1 block hash is 0"); require(blockHashs[l1BlockNumber] == bytes32(0), "l1 blockhash already set"); blockHashs[l1BlockNumber] = l1BlockHash; emit L1BlockSyncd(l1BlockNumber, l1BlockHash); } ``` Anyone can call this function to register the L1 blockhash in our State Root contract. Once done, the complete block header of that specific L1 block can be retrieved and verified. This process enables the extraction of the state root from L1, which can then be stored on the L2 side using the following [function](https://github.com/proofofsoulprotocol/soul-wallet-contract/blob/128aeeb54ac8a72b6cf2c2c1a8b5f41ad6b7b86d/contracts/modules/keystore/KnownStateRootWithHistoryBase.sol#L37): ``` solidity function insertNewStateRoot(uint256 _blockNumber, bytes memory _blockInfo) external { bytes32 _blockHash = blockHashs[_blockNumber]; require(_blockHash != bytes32(0), "blockhash not set"); (bytes32 stateRoot, uint256 blockTimestamp, uint256 blockNumber) = BlockVerifier.extractStateRootAndTimestamp(_blockInfo, _blockHash); require(!isKnownStateRoot(stateRoot), "duplicate state root"); BlockInfo memory currentBlockInfo = stateRoots[currentRootIndex]; require(blockNumber > currentBlockInfo.blockNumber, "blockNumber too old"); uint256 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE; currentRootIndex = newRootIndex; stateRoots[newRootIndex].blockHash = _blockHash; stateRoots[newRootIndex].storageRootHash = stateRoot; stateRoots[newRootIndex].blockNumber = blockNumber; stateRoots[newRootIndex].blockTimestamp = blockTimestamp; emit NewStateRoot(stateRoot, blockNumber, msg.sender); } ``` Once the state root value is stored on the L2 side, the Merkle proof can be utilized for both the account proof and the storage proof. ### Arbitrum implementation Unlike Optimism, Arbitrum does not have a precompiled contract to fetch recent block information from L1. Therefore, we use to [cross-chain messaging](https://developer.arbitrum.io/arbos/l1-to-l2-messaging) to pass the L1 blockhash on Arbitrum. We've employed "Retryable tickets", which serve as Arbitrum's standard method for transmitting messages from L1 to L2, namely L1 transactions that trigger a message execution on L2. The main concept is to utilize a contract on L1 to invoke the `BLOCKHASH` opcode, and then forward it to Arbitrum using `Retryable tickets`. Below is the contract on L1 and using beblow function to pass L1 `BLOCKHASH` to Arbitrum. ``` solidity function passBlockHashInL2(uint256 _maxSubmissionCost, uint256 _maxGas, uint256 _gasPriceBid) public payable returns (uint256) { // should not get the current blockhash, // block.blockhash(uint blockNumber) returns (bytes32): hash of the given block - only works for 256 most recent, excluding current, blocks // check https://github.com/foundry-rs/foundry/pull/1890 uint256 _blockNumber = block.number - 1; bytes32 _blockhash = blockhash(_blockNumber); bytes memory data = abi.encodeCall(ArbKnownStateRootWithHistory.setBlockHash, (_blockNumber, _blockhash)); uint256 ticketID = inbox.createRetryableTicket{value: msg.value}( l2Target, 0, _maxSubmissionCost, msg.sender, msg.sender, _maxGas, _gasPriceBid, data ); emit BlockHashPassingTickedCreated(ticketID, _blockNumber, _blockhash); return ticketID; } ``` Consequently, the contract on Arbitrum retrieves the L1 blockhash using the following [implementation](https://github.com/proofofsoulprotocol/soul-wallet-contract/blob/develop/contracts/modules/keystore/ArbitrumKeyStoreModule/ArbKnownStateRootWithHistory.sol). ```solidity function setBlockHash(uint256 l1BlockNumber, bytes32 l1BlockHash) external { // To check that message came from L1, we check that the sender is the L1 contract's L2 alias. require(msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Target), "blockhash only updateable by L1Target"); require(l1BlockNumber != 0, "l1 block number is 0"); require(l1BlockHash != bytes32(0), "l1 block hash is 0"); blockHashs[l1BlockNumber] = l1BlockHash; emit L1BlockSyncd(l1BlockNumber, l1BlockHash); } ``` Once Arbitrum is able to obtain the L1 `blockhash`, the implementation of the Merkle proof is identical to that on Optimism. ## Keystore verification on L2 In our design, we separate the [account proof](https://github.com/proofofsoulprotocol/soul-wallet-contract/blob/128aeeb54ac8a72b6cf2c2c1a8b5f41ad6b7b86d/contracts/modules/keystore/KeystoreProof.sol#L23) and [the storage proof](https://github.com/proofofsoulprotocol/soul-wallet-contract/blob/128aeeb54ac8a72b6cf2c2c1a8b5f41ad6b7b86d/contracts/modules/keystore/KeystoreProof.sol#L37). This approach is taken because, once the account proof is validated, the storage hash of the keystore account is verified and extracted, eliminating the need for repeated submissions. Account to vitalik's blog: *The larger problem is cost. Merkle proofs are long, and Patricia trees are unfortunately ~3.9x longer than necessary (precisely: an ideal Merkle proof into a tree holding N objects is 32 * log2(N) bytes long, and because Ethereum's Patricia trees have 16 leaves per child, proofs for those trees are 32 * 15 * log16(N) ~= 125 * log2(N) bytes long). In a state with roughly 250 million (~2²⁸) accounts, this makes each proof 125 * 28 = 3500 bytes, or about 56,000 gas, plus extra costs for decoding and verifying hashes.* While the account proof approximates around 3500 bytes, the size of the storage proof varies and depends on the data present in the keystore contract. Originally, this cannot be significantly large, and thus, the storage proof remains relatively small. This separation allows users to submit the storage proof only if the account proof has been successfully verified, optimizing both cost and data usage. The contract architecht on differenct L2 follows: ![](https://hackmd.io/_uploads/Hk6FPwqcn.png) Verification key sync logic on L2 follows: ![](https://hackmd.io/_uploads/SkK86BAc3.png) ## Summary and Future work While the Keystore solution presents significant advantages, it's important to acknowledge its limitations and potential challenges. 1. Account Model Dependency: The first challenge lies in the dependency of SoulWallet's current Keystore implementation on Ethereum's existing account model, the Merkel Patricia Tree. If Ethereum's account model were to upgrade to [Verkle Trees](https://vitalik.ca/general/2021/06/18/verkle.html) in the future, the current solution would no longer be viable, necessitating a corresponding upgrade. Such an upgrade process would come with its own set of challenges, such as how to maintain compatibility with the original Keystore solution, and how to migrate existing user wallets within the current technical implementation of the Keystore. 2. Cost Concerns on Ethereum's L1: The second challenge arises from the fact that the Keystore is deployed on Ethereum's L1, which is costlier to interact with compared to L2. This includes the process of social recovery, which is conducted on L1. Although social recovery is typically a low-frequency event, and users are unlikely to perform it frequently on L1, the potential user cost is still a concern and represents another challenge to the Keystore's design. Despite these challenges, with the integration of the keystore in SoulWallet, users can manage their verification keys on the L1 keystore contract. This feature allows wallets created on multiple L2s to synchronize with the most recent verification key from the L1 contract, regardless of whether it is during the wallet's creation or after it deployed. The advantages of this approach are clear. Guardians are no longer required to sign messages on each chain to recover a user's wallet. Instead, the management of guardians occurs solely on L1, significantly reducing complexity and ensuring greater manageability. This eliminates the need to ascertain whether the guardians' addresses have been deployed or are operational on different L2s.