From d2cf0e008b5efb4f79e47fd80257c4aec5f5eed6 Mon Sep 17 00:00:00 2001 From: Mister_Omelette Date: Sun, 22 Dec 2024 23:13:00 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D1=81oment:=201.=20Simplified=20complex=20?= =?UTF-8?q?and=20lengthy=20sentences=20to=20improve=20readability.=20For?= =?UTF-8?q?=20example,=20rephrased=20a=20sentence=20in=20the=20"Signing=20?= =?UTF-8?q?Key=20Indexer"=20section=20for=20better=20clarity.=202.=20Added?= =?UTF-8?q?=20context=20to=20the=20link=20for=20"[Safe=20Transaction=20Ser?= =?UTF-8?q?vice](https://docs.safe.global/core-api/api-safe-transaction-se?= =?UTF-8?q?rvice)"=20to=20help=20readers=20understand=20its=20purpose.=203?= =?UTF-8?q?.=20Suggested=20formatting=20the=20functionality=20of=20the=20"?= =?UTF-8?q?Multisig=20Transaction=20Service"=20as=20a=20list=20to=20make?= =?UTF-8?q?=20the=20structure=20clearer=20and=20easier=20to=20follow.=204.?= =?UTF-8?q?=20Discussed=20unifying=20headings=20and=20proposed=20renaming?= =?UTF-8?q?=20"Multisig=20Transaction=20Service"=20to=20"Multisig=20Transa?= =?UTF-8?q?ction=20Management=20Service"=20for=20consistency.=205.=20Recom?= =?UTF-8?q?mended=20splitting=20long=20paragraphs=20into=20shorter=20ones?= =?UTF-8?q?=20and=20adding=20more=20examples=20of=20service=20usage.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pages/index.mdx | 5 +++-- docs/pages/keystore-basics.mdx | 4 ++-- docs/pages/maintaining-wallets.mdx | 6 +++--- docs/pages/references.mdx | 2 +- docs/pages/releases.mdx | 13 ++++++++++++- docs/pages/revoking-signers.mdx | 6 +++--- docs/pages/roadmap.mdx | 8 ++++---- docs/pages/updating-keystore.mdx | 13 ++++++++----- docs/pages/using-new-signers.mdx | 6 +++--- docs/pages/web-services.mdx | 5 ++++- 10 files changed, 43 insertions(+), 25 deletions(-) diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index ea096a2..5d18ca0 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -5,7 +5,7 @@ description: How to use Keyspace to build cross-chain wallets and other applicat # Getting Started -Keyspace is a keystore for cross-chain smart wallets that keeps their configuration in sync. The primary goal of the project is to help wallet vendors build smart wallets that match the cross-chain user experience of externally-owned accounts in Ethereum: one address can be used on all current and future chains. Keyspace can also be used to build new wallet experiences where a user can manage many different accounts transparently share the same configuration. +Keyspace is a keystore for cross-chain smart wallets that keeps their configuration in sync. The primary goal of the project is to help wallet vendors build smart wallets that match the cross-chain user experience of externally-owned accounts in Ethereum: one address can be used on all current and future chains. Keyspace can also be used to build new wallet experiences where a user can manage many different accounts that transparently share the same configuration. We believe that ERC 4337 smart wallets are the future: passkey signers, paymasters, and batch transactions dramatically improve the experiences builders can provide for their users. But to deliver on that promise, we need users to be confident that assets sent to their Ethereum address are in their control, regardless of what chains those assets are on. @@ -14,6 +14,7 @@ Keyspace is built by the Base team as open, neutral infrastructure for all chain * [Keyspace on GitHub](https://github.com/base-org/keyspace) -* [Coinbase Smart Wallet](https://www.smartwallet.dev/) Keyspace integration +* Coinbase Smart Wallet * [JavaScript Client](https://github.com/base-org/keyspace-client) * [Smart Contracts](https://github.com/niran/smart-wallet/tree/keyspace) + diff --git a/docs/pages/keystore-basics.mdx b/docs/pages/keystore-basics.mdx index 8d84406..a8e56c9 100644 --- a/docs/pages/keystore-basics.mdx +++ b/docs/pages/keystore-basics.mdx @@ -9,7 +9,7 @@ description: Learn the fundamental concepts and operations of Keyspace keystores In Keyspace, a wallet's cross-chain keystore is typically embedded within the wallet's smart contract. When you inherit from the `Keystore` contract, your wallet gains the ability to sync its configuration across chains. -Since a `Keystore` needs know how to read its storage across chains, the logic for verifying cross-chain proofs from your wallet's master chain needs to be provided. The `OPStackKeystore` contract shipped with Keyspace provides this logic for OP Stack L2s. +Since a Keystore needs to know how to read its storage across chains, the logic for verifying cross-chain proofs from your wallet's master chain needs to be provided. The OPStackKeystore contract included with Keyspace provides this logic for OP Stack L2s. ## Configuration Hooks @@ -42,7 +42,7 @@ require(isNewConfigValid, InvalidNewKeystoreConfig()); `_hookIsNewConfigAuthorized(ConfigLib.Config calldata newConfig, bytes calldata authorizationProof)` -This hook is called before the configuration update is applied. It should verify that the caller is authorized to change the configuration. `authorizationProof` is typically a pair of ECDSA signatures for most wallets. Only the first signature is relevant for `_hookIsNewConfigAuthorized`. (The second signature is used for the `hookIsNewConfigValid`.) +This hook is called before the configuration update is applied. It should verify that the caller is authorized to change the configuration. `authorizationProof is typically a pair of ECDSA signatures for most wallets. Only the first signature is relevant for `_hookIsNewConfigAuthorized`. (The second signature is used for the `hookIsNewConfigValid`.) If the signature is valid, the hook should return successfully. Otherwise, it should revert. diff --git a/docs/pages/maintaining-wallets.mdx b/docs/pages/maintaining-wallets.mdx index c0bd836..066c981 100644 --- a/docs/pages/maintaining-wallets.mdx +++ b/docs/pages/maintaining-wallets.mdx @@ -9,12 +9,12 @@ Beyond adding and removing signers, cross-chain syncing introduces additional co ## Performing Wallet Upgrades -A wallet's configuration is deeply intertwined with the implementation contract that reads that configuration. Upgrading a wallet's implementation alone could break if it could not read the existing configuration, and updating the configuration without upgrading the implementation could break if the new implementation expects a new configuration. Keyspace can help wallet vendors implement atomic upgrades that ensure both the configuration and the implementation are upgraded at the same time. +A wallet's configuration is deeply intertwined with the implementation contract that reads that configuration. Upgrading a wallet's implementation alone could fail if it cannot read the existing configuration. Similarly, updating the configuration without upgrading the implementation may fail if the new implementation expects a different configuration. Keyspace can help wallet vendors implement atomic upgrades that ensure both the configuration and the implementation are upgraded at the same time. To do this, the implementation address for the wallet can be stored in the keystore configuration that is synced across chains. Your wallet's [`_hookApplyNewConfig`](/keystore-basics#_hookapplynewconfig) is responsible for detecting the new implementation address and performing the upgrade. This new implementation address needs to be deployed on each chain supported by the wallet or the wallet will not be usable on those chains after syncing. :::note -If your wallet provides direct access to `upgradeTo` or the equivalent for your proxy contract, the implementation can be overwritten without using Keyspace, and since this change will not be synced, the user will end up with inconsistent behavior across chains. Without care, this could lead to unexpected behavior. +If your wallet provides direct access to `upgradeTo` or the equivalent for your proxy contract, the implementation can be overwritten without using Keyspace, and since this change will not be synced, the user will end up with inconsistent behavior across chains. Without proper safeguards, this inconsistency could lead to unexpected behavior. ::: ## New Chains and Wallet Factories @@ -37,7 +37,7 @@ If cross-chain Merkle proofs have stopped working due to hard forks, another way #### Recovery Guardians -Recovery guardians affect whether the wallet can be activated on a new chain. Recovery guardians are typically stateful: the recovery is initiated on a blockchain, then can only be processed after a delay that is verified by proving the elapsed time since the recovery was initiated. Since recovery guardians require external state to be verified, they are not guaranteed to always successfully re-execute as preconfirmations on new replica chains indefinitely. +Recovery guardians affect whether the wallet can be activated on a new chain. Recovery guardians are typically stateful: the recovery is initiated on a blockchain, then can only be processed after a delay that is verified by proving the elapsed time since the recovery was initiated. Recovery guardians rely on external state, making their re-execution as preconfirmations on new replica chains unreliable. In the current version of Keyspace, the way to build a recovery guardian is to write it as a periphery contract that is added as a signer to the wallet. When the conditions for the guardian are met, it calls `setConfig` directly on the wallet. `_hookIsNewConfigAuthorized` can then check `msg.sender` to verify that the guardian's address is authorized in the wallet's configuration. diff --git a/docs/pages/references.mdx b/docs/pages/references.mdx index 722c2d5..f628e14 100644 --- a/docs/pages/references.mdx +++ b/docs/pages/references.mdx @@ -9,7 +9,7 @@ title: References * [Deeper dive on cross-L2 reading for wallets and other use cases](https://vitalik.eth.limo/general/2023/06/20/deeperdive.html), by Vitalik Buterin * [What kind of layer 3s make sense?](https://vitalik.eth.limo/general/2022/09/17/layer_3.html), by Vitalik Buterin * [The Three Transitions](https://vitalik.eth.limo/general/2023/06/09/three_transitions.html), by Vitalik Buterin - * [Possible futures of the Ethereum protocol, part 2: The Surge](https://vitalik.eth.limo/general/2024/10/17/futures2.html), by Vitalik Buterin + * [Possible Futures of the Ethereum Protocol, Part 2: The Surge](https://vitalik.eth.limo/general/2024/10/17/futures2.html), by Vitalik Buterin * Other Perspectives on the Keystore Problem * [Keystore Rollup: Revolutionizing Smart Account Interoperability](https://safe.global/blog/keystore-rollup-smart-account-interoperability), by Lukas Schor & Safe * [Towards the wallet endgame with Keystore](https://scroll.io/blog/towards-the-wallet-endgame-with-keystore), by Dom, Ye Zhang, & Scroll diff --git a/docs/pages/releases.mdx b/docs/pages/releases.mdx index 7390998..eabba80 100644 --- a/docs/pages/releases.mdx +++ b/docs/pages/releases.mdx @@ -14,7 +14,18 @@ Keyspace v0.1.0 is the first smart contract-based implementation of Keyspace. Th *June 18, 2024* -The Dedicated Rollup Beta (v0.0.2) release of Keyspace introduces [`keyspace-client`](https://github.com/base-org/keyspace-client/tree/v0.0.2), an example TypeScript client for Keyspace with an integrated smart wallet, and [`keyspace-recovery-service`](https://github.com/base-org/keyspace-recovery-service/tree/v0.0.2), an RPC service for generating SNARK proofs of the signatures users sign to change their keys. The supported chains have been expanded from Base Sepolia and Optimism Sepolia to include Arbitrum Sepolia, Gnosis Chiado, Polygon Amoy, BSC Testnet, and Avalanche Fuji. +The Dedicated Rollup Beta (v0.0.2) release of Keyspace introduces: +- [`keyspace-client`](https://github.com/base-org/keyspace-client/tree/v0.0.2): TypeScript client for Keyspace with an integrated smart wallet. +- [`keyspace-recovery-service`](https://github.com/base-org/keyspace-recovery-service/tree/v0.0.2): RPC service for generating SNARK proofs of the signatures users sign to change their keys. + +Supported chains have been expanded to include: +- Base Sepolia +- Optimism Sepolia +- Arbitrum Sepolia +- Gnosis Chiado +- Polygon Amoy +- BSC Testnet +- Avalanche Fuji. ## Dedicated Rollup Alpha (v0.0.1) diff --git a/docs/pages/revoking-signers.mdx b/docs/pages/revoking-signers.mdx index 4a524b4..6c51740 100644 --- a/docs/pages/revoking-signers.mdx +++ b/docs/pages/revoking-signers.mdx @@ -5,12 +5,12 @@ description: Learn how to safely revoke signers and handle compromised signer sc # Revoking Signers -One of the hardest problems for cross-chain wallets is revoking compromised signers. Most cross-chain wallets have no single source of truth for the wallet's configuration, so revoking a compromised signer requires broadcasting the revocation across all active chains. Keyspace introduces the ability to sync configuration changes across chains, which is the first step towards making revoking compromised signers easier. However, there are still two avenues for a compromised signer to take control of a wallet: +Revoking signers is a critical process in maintaining wallet security, especially in scenarios where a signer has been compromised. This process ensures that the compromised key cannot be used to access or control the wallet. 1. The target replica chain is **actively** used by the wallet, so it has been synced, but using data from 3.5+ days ago due to the master chain's settlement delay. 2. The target replica chain is **inactive** and has never been synced, so the configuration that contains the compromised signer is still valid on that chain, *no matter how much time has elapsed*. (This is not a Keyspace-specific issue: it's a problem for most cross-chain wallets.) -Keyspace handles the first scenario by allowing you to preconfirm revocations on active chains. The second scenario is currently infeasible to mitigate, so wallet vendors must understand the implications of revocations on inactive chains. +Keyspace handles the first scenario by allowing you to preconfirm revocations on active chains. The second scenario is currently difficult to mitigate, so wallet vendors must understand the implications of revocations on inactive chains. ## Preconfirming Revocations on Active Chains @@ -42,4 +42,4 @@ Cost-effective syncing depends on cross-chain Merkle proofs, but these can break The top two paths for addressing this currently seem to be introducing EVM opcodes for direct cross-chain storage reads, or standardizing cross-chain Merkle proof validation contracts that are upgraded with each hard fork, just like the withdrawal and deposit bridges are. The [L1SLOAD opcode proposal](https://ethereum-magicians.org/t/rip-7728-l1sload-precompile/20388) would eliminate the risk of L1 hard forks. L2 hard forks would require a similar opcode, either for a widely used master chain for keystores or for a dedicated rollup for keystores. Such a `KEYSTORESLOAD` opcode would eliminate the risk of L2 hard forks. -Once one of these paths are viable, we expect to change our recommendation to require a recent sync even before preconfirmations can be used, which would limit the window of time that a compromised signer can be used to the eventual consistency period plus the settlement delay of the master chain. +Once one of these paths is viable, we expect to change our recommendation to require a recent sync even before preconfirmations can be used, which would limit the window of time that a compromised signer can be used to the eventual consistency period plus the settlement delay of the master chain. diff --git a/docs/pages/roadmap.mdx b/docs/pages/roadmap.mdx index 005ae11..9feae06 100644 --- a/docs/pages/roadmap.mdx +++ b/docs/pages/roadmap.mdx @@ -7,16 +7,16 @@ title: Roadmap ## Cross-Chain Syncing Improvements ### Support for More Chains -Keyspace will expand beyond OP Stack L2s to support a wider range of chains for both master and replica functionality. We expect most replica chains to implement EIP-4788 to be able to verify proofs from L1. For alt-L1 replica chains, we plan to add support through multiple trusted oracles using the Hashi project, providing a more secure syncing solution for these chains. For master chains, we'll need to implement an `_extractConfigHashFromMasterChain` function for each rollup stack. +Keyspace will expand beyond OP Stack L2s to support a wider range of chains for both master and replica functionality. We expect most replica chains to implement EIP-4788 to be able to verify proofs from L1. For alt-L1 replica chains, we plan to add support through multiple trusted oracles using the Hashi project, which will provide a more secure syncing solution for these chains. For master chains, we'll need to implement an `_extractConfigHashFromMasterChain` function for each rollup stack. ### Resilient Fallback Syncing We will implement syncing via deposits and withdrawals as a resilient fallback method when other syncing methods aren't available or have stopped functioning due to hard forks. Once this has been implemented, wallets should be able to sync on each chain as long as each chain between the master and the replica continues to function. ### Standardized Beacon Root Oracle Access -ERC-4337 prohibits cross-contract storage access during the validation phase of user operations. We are working to standardize access to the EIP-4788 beacon root oracle access during ERC-4337 validation across all bundlers. Each slot can only be written to once per day, so it's straightforward for bundlers to defend themselves from the mass invalidation attacks that these restrictions were intended to prevent. This will allow new signers to sync their wallet's configuration to a replica chain without assistance. +ERC-4337 prohibits cross-contract storage access during the validation phase of user operations. We are working to standardize access to the EIP-4788 beacon root oracle access during ERC-4337 validation across all bundlers. Each slot can only be written to once per day, making it easier for bundlers to prevent mass invalidation attacks that these restrictions were designed to address. This will allow new signers to sync their wallet's configuration to a replica chain without assistance. ### L1SLOAD -We currently use either Merkle proofs or withdrawal and deposit transactions to sync wallets across chains. Merkle proofs are more efficient, but are fragile because each hard fork of an L1 or L2 can change the assumptions that the Merkle proof relies on, and would require a contract upgrade with new logic to verify cross-chain state. Withdrawal and deposit transactions are more resilient, but are costly and slow. `L1SLOAD` provides a way for rollups to read state directly from L1, which is fast, cheap, and resilient to L1 and replica chain hard forks. (Master chain hard forks would still be fragile.) It would also lower the gas costs for syncing wallets across chains: each Merkle proof is about 5kb of calldata that direct state reading eliminates. +We currently use either Merkle proofs or withdrawal and deposit transactions to sync wallets across chains. Merkle proofs are more efficient, but are fragile because each hard fork of an L1 or L2 can change the assumptions that the Merkle proof relies on, and would require a contract upgrade with new logic to verify cross-chain state. Withdrawal and deposit transactions are more resilient, but are costly and slow. `L1SLOAD` provides a way for rollups to read state directly from L1, which is fast, cheap, and resilient to L1 and replica chain hard forks. (Master chain hard forks would still be susceptible to issues.) It would also lower the gas costs for syncing wallets across chains: each Merkle proof is about 5kb of calldata that direct state reading eliminates. ### Standards for Cross-Chain Proofs Since deposits and withdrawals are costly and slow, we aim to push for standardized contracts for verifying cross-chain proofs that are updated with each hard fork, just like the deposit and withdrawal bridges are. When all rollups offer such contracts, it will always be possible to sync wallets between any two chains without slow and costly deposits and withdrawals. At this point, wallets may be able to **require** syncing for all wallet actions, which makes [revoked signers](/revoking-signers) permanently removed once the eventual consistency period has passed. @@ -33,4 +33,4 @@ General purpose L2s have frequent hard forks that often break cross-chain syncin Many general purpose L2s have long finality times (3.5+ days) because they are optimistic rollups. A dedicated keystore rollup would be a zero-knowledge rollup, so it would have much faster finality times. We expect that a configuration change made on a zero-knowledge rollup could be finalized on L1 within two minutes, and available on L2s within five minutes. This would reduce the need for preconfirmations in some cases. ### KEYSTORESLOAD -Just like `L1SLOAD` would do for L1 state, `KEYSTORESLOAD` would provide a way for rollups to read state directly from the keystore, which is fast, cheap, and resilient to master chain hard forks. This would make wallets resilient to hard forks of the dedicated keystore rollup and would eliminate more Merkle proofs from calldata when syncing wallets across chains. +Just as L1SLOAD improves L1 state access, KEYSTORESLOAD enables rollups to read state directly from the keystore, which is fast, cheap, and resilient to master chain hard forks. This would make wallets resilient to hard forks of the dedicated keystore rollup and would eliminate more Merkle proofs from calldata when syncing wallets across chains. diff --git a/docs/pages/updating-keystore.mdx b/docs/pages/updating-keystore.mdx index c337cc2..8e68ca1 100644 --- a/docs/pages/updating-keystore.mdx +++ b/docs/pages/updating-keystore.mdx @@ -15,7 +15,7 @@ Keyspace currently ships with support for any OP Stack L2 to be used as a master Configurations in Keyspace are defined by the `account` address for the keystore, the `data` stored in the keystore, and the `nonce` of the configuration. To make a change to a wallet's configuration, you first need to build the next data. -The format of the `data` byte string is defined by the wallet vendor. The most important consideration during an update is that the desired mutation is applied correctly to the configuration without accidentally changing or discarding any other parts of the configuration. +The format of the `data` byte string is defined by the wallet vendor. The most important consideration during an update is applying the desired mutation correctly. Ensure that no other parts of the configuration are accidentally changed or discarded. Once you have the new data, you need the next nonce for the configuration, which is the current nonce plus one. `keyspace-client`'s `buildNextConfig` will fetch the next nonce for you. It also takes the previous configuration data that you applied your mutations to and checks it against the configuration hash stored onchain to make sure you're not applying your changes to an outdated or incorrect configuration. @@ -31,10 +31,13 @@ function setConfig(ConfigLib.Config calldata newConfig, bytes calldata authoriza ## Syncing to Replica Chains -Keyspace helps you keep your wallet's configuration in sync across different chains. Syncing is optional, and is mainly recommended for Ethereum rollups. Even on chains where syncing is disabled, Keyspace's state-based configuration management helps you develop features that can replay their configuration changes on other chains without any feature-specific syncing work. +Keyspace enables you to keep your wallet's configuration in sync across different chains. Syncing is optional, and is mainly recommended for Ethereum rollups. Even on chains where syncing is disabled, Keyspace's state-based configuration management helps you develop features that can replay their configuration changes on other chains without any feature-specific syncing work. ```solidity -function confirmConfig(ConfigLib.Config calldata newConfirmedConfig, bytes calldata keystoreProof) +function setConfig(ConfigLib.Config calldata newConfig, bytes calldata authorizationProof) { + // Updates the wallet's configuration on the master chain. +} + ``` `Keystore.confirmConfig` is the function that confirms a configuration change on a replica chain. It takes the full new configuration struct and a `keystoreProof` with data to prove the configuration hash from the master chain. There are several methods for proving the configuration hash, and the `keystoreProof` will be different depending on the method. @@ -47,7 +50,7 @@ Cross-chain Merkle proofs are the most efficient syncing method, as they only ha #### Proving the L1 State Root -Rollups typically have some way to access the state of the L1 chain. OP Stack rollups have two methods: the `hash` storage slot of the [`L1Block` predeploy](https://specs.optimism.io/protocol/predeploys.html#l1block) and the EIP-4788 beacon root oracle, which can be used to prove the execution state root of a given block. We currently expect rollups to standardize on EIP-4788 as the method to access L1 state because its ring buffer design produces longer-lived proofs, and the beacon chain itself [includes a double-batched accumulator](https://eth2book.info/capella/part3/containers/state/) that makes proofs of any L1 state since the merge much more efficient. +Rollups typically have a defined mechanism to access the state of the L1 chain. OP Stack rollups have two methods: the `hash` storage slot of the [`L1Block` predeploy](https://specs.optimism.io/protocol/predeploys.html#l1block) and the EIP-4788 beacon root oracle, which can be used to prove the execution state root of a given block. We currently expect rollups to standardize on EIP-4788 as the method to access L1 state because its ring buffer design produces longer-lived proofs, and the beacon chain itself [includes a double-batched accumulator](https://eth2book.info/capella/part3/containers/state/) that makes proofs of any L1 state since the merge much more efficient. When using the `L1Block` predeploy, proofs rooted at `L1Block.hash` are only valid for one L1 block time (12 seconds). For longer-lived proofs, we prove the storage slot for `L1Block.hash` and use the `BLOCKHASH` opcode to provide the root for the proof, which lasts for 256 replica chain blocks. @@ -75,7 +78,7 @@ Once we have the state root for the master chain, we just need to prove storage Syncing via deposits and withdrawals has not been implemented as of v0.1.0. ::: -Deposits and withdrawals are the canonical method for sending messages between chains. That makes them extemely resilient: the whole ecosystem builds on top of withdrawals and deposits with the expectaction that they will succeed for the lifetime of the chains. Rollup teams ensure that their bridge contracts continue to function through each hard fork. +Deposits and withdrawals are the canonical method for sending messages between chains. That makes them extremely resilient: the whole ecosystem builds on top of withdrawals and deposits with the expectation that they will succeed for the lifetime of the chains. Rollup teams ensure that their bridge contracts continue to function through each hard fork. The downsides of deposits and withdrawals are that they require a transaction to be sent on a separate chain from the one the user is interacting with, and that these transactions have significant costs on L1. diff --git a/docs/pages/using-new-signers.mdx b/docs/pages/using-new-signers.mdx index 059ec68..99e0321 100644 --- a/docs/pages/using-new-signers.mdx +++ b/docs/pages/using-new-signers.mdx @@ -7,7 +7,7 @@ description: Learn how to add and manage new signers in your Keyspace wallet When a new signer is configured, it's written directly to the master chain. But what if the new signer wants to start sending transactions on another chain? If that chain hasn't been synced recently, a sync will be necessary. And if the new signer was just added, that master chain block won't settle on L1 for 3.5 days (and up to 10 days in the case of optimistic rollup disputes). -To get make new signers usable immediately, Keyspace supports *preconfirmations*, which replay the `setConfig` call from the master chain on a replica chain with the same arguments. The difference between `setConfig` on the master chain and `setConfig` on a replica chain is that if the replica chain's configuration conflicts with the master chain's configuration, the replica chain's configuration will eventually be overwritten during a sync. +To make new signers usable immediately, Keyspace supports *preconfirmations*, which replay the `setConfig` call from the master chain on a replica chain with the same arguments. The difference between `setConfig` on the master chain and `setConfig` on a replica chain is that if the replica chain's configuration conflicts with the master chain's configuration, the replica chain's configuration will eventually be overwritten during a sync. ## Sync and Preconfirm with the New Signer @@ -25,7 +25,7 @@ Executing a sync during the validation phase requires whitelisting cross-contrac ### ERC-4337 Validation Restrictions -The complications for handling new signers are caused by [ERC-4337's validation restrictions](https://eips.ethereum.org/EIPS/eip-4337), which are enumerated in [ERC 7562](https://eips.ethereum.org/EIPS/eip-7562). Syncing via Merkle proofs requires access to an L1 state root, which requires either cross-contract storage access or the `BLOCKHASH` opcode, which is also prohibited by ERC-4337. Preconfirmations do not require cross-contract storage access to process, but we expect that most preconfirmations will require a sync at the same time. +The complications for handling new signers are caused by [ERC-4337's validation restrictions](https://eips.ethereum.org/EIPS/eip-4337), which are enumerated in [ERC 7562](https://eips.ethereum.org/EIPS/eip-7562). Syncing via Merkle proofs requires access to an L1 state root, which depends on either cross-contract storage access or the BLOCKHASH opcode, both of which are prohibited by ERC-4337. Preconfirmations do not require cross-contract storage access to process, but we expect that most preconfirmations will require a sync at the same time. :::note ERC-4337 aggregators can take over validation duties for a wallet without any restrictions on cross-contract storage access because they are "staked entities." They can be blacklisted by bundlers when they misbehave, and ETH needs to be staked to set them up. Wallet vendors can write aggregators that handle syncing, which would allow new signers to pay the gas fees for their own configuration updates. Unfortunately, aggregators are not widely supported by bundlers yet. @@ -41,7 +41,7 @@ The large ring buffer gives proofs based on these roots a long shelf life. The b ##### L1Block.hash -On OP Stack chains, the latest hash for the parent chain is stored in the `L1Block` predeploy as `hash`. This value is updated once per parent chain block, which is too frequent for most bundlers to whitelist safely. (Note that while this predeploy is called `L1Block`, on L3 chains, it's expected to refer to the L2 chain.) +On OP Stack chains, the parent chain's latest hash is stored in the L1Block predeploy as hash. This value is updated once per parent chain block, which is too frequent for most bundlers to whitelist safely. (Note that while this predeploy is called `L1Block`, on L3 chains, it's expected to refer to the L2 chain.) ## Set, Sync, and Preconfirm via Wallet Vendor diff --git a/docs/pages/web-services.mdx b/docs/pages/web-services.mdx index 641664f..9613a62 100644 --- a/docs/pages/web-services.mdx +++ b/docs/pages/web-services.mdx @@ -10,7 +10,7 @@ For Keyspace-integrated wallets, wallet vendors can expect to run two support se ## Signing Key Indexer -Anyone can run a Keyspace node to have a local copy of the Keyspace database that can be queried directly or via the `mksr_get` JSON RPC call exposed by the node. However, we expect many wallet vendors to want to update their own databases when a user changes their wallet configuration. This helps provide APIs to connect the signing key present in the application with wallets that the key is authorized to access. +Anyone can run a Keyspace node to maintain a local copy of the Keyspace database, which can be queried directly or through the mksr_get JSON RPC call. However, we expect many wallet vendors to want to update their own databases when a user changes their wallet configuration. This allows wallet vendors to provide APIs. These APIs connect the signing key present in the application with the wallets it is authorized to access. ## Recovery Proof Service @@ -18,4 +18,7 @@ Keyspace is designed to store the configuration for any kind of wallet regardles ## Multisig Transaction Service +- Collect signatures from signers until the threshold is met. +- Support Keyspace recovery transactions beyond typical multisig transactions. + Smart wallets that authorize transactions with multiple signatures typically run a service to collect signatures from signers until the threshold has been reached, like [Safe Transaction Service](https://docs.safe.global/core-api/api-safe-transaction-service) does. Beyond supporting typical transactions, these services will need to be extended to support Keyspace recovery transactions. From 8a360dd787bf0ba95d1e25eccd624169b33d369c Mon Sep 17 00:00:00 2001 From: Mister_Omelette Date: Sun, 22 Dec 2024 23:31:57 +0300 Subject: [PATCH 2/2] fix: corrected typos in code and documentation --- README.md | 6 +-- scripts/lib/client.ts | 5 +- scripts/send-eth.ts | 77 ++++++++++++++++------------- scripts/sync-keystore.ts | 103 +++++++++++++-------------------------- 4 files changed, 84 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 05ccbdc..a57eeda 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ bun run scripts/get-account.ts | Argument | Environment Variable | Description | | --- | --- | --- | | --private-key | PRIVATE_KEY | secp256k1 private key or P256 JWK | -| --signature-type | | secp256k1 (default) or webauthn | +| --signature-type | | secp256k1 (default) or WebAuthn | ### Send ETH ```bash @@ -60,7 +60,7 @@ bun run scripts/send-eth.ts | --initial-config-data | | The initial config data needed to deploy the wallet | | --private-key | PRIVATE_KEY | secp256k1 private key or P256 JWK | | --to | | The address to send to | -| --signature-type | | secp256k1 (default) or webauthn | +| --signature-type | | secp256k1 (default) or WebAuthn | Make sure there's ETH in the account you're sending from. You can get the Ethereum address of the smart wallet by running `bun run scripts/get-account.ts`. @@ -78,7 +78,7 @@ bun run scripts/change-owner.ts | --private-key | PRIVATE_KEY | Current private key of the owner | | --config-data | | Current config data for the keystore wallet (hex string) | | --owner-bytes | | The owner bytes to change in the keystore wallet | -| --signature-type | | secp256k1 (default) or webauthn | +| --signature-type | | secp256k1 (default) or WebAuthn | | --remove | | Flag to remove the owner instead of adding (optional) | ## Build Documentation diff --git a/scripts/lib/client.ts b/scripts/lib/client.ts index bfc11b9..6a76292 100644 --- a/scripts/lib/client.ts +++ b/scripts/lib/client.ts @@ -7,11 +7,10 @@ export const chain = baseSepolia; export const client: PublicClient = createPublicClient({ chain, - transport: http( - process.env.RPC_URL || "" - ), + transport: http(process.env.RPC_URL || ""), }); + export const masterClient: PublicClient = client; export const l1Client: PublicClient = createPublicClient({ diff --git a/scripts/send-eth.ts b/scripts/send-eth.ts index 08af4a3..f54743a 100644 --- a/scripts/send-eth.ts +++ b/scripts/send-eth.ts @@ -10,31 +10,31 @@ async function main() { description: "Send 1 wei", }); - parser.add_argument("--account", { - help: "The account of the keystore wallet to send from", - required: true, - }); - parser.add_argument("--owner-index", { - help: "The index of the owner", - default: 0, - }); - parser.add_argument("--initial-config-data", { - help: "The initial config data needed to deploy the wallet as a hex string", - }); - parser.add_argument("--private-key", { - help: "The current private key of the owner", - ...defaultToEnv("PRIVATE_KEY"), - }); - parser.add_argument("--to", { - help: "The address to send to", - required: true, - }); - parser.add_argument("--signature-type", { - help: "The type of signature for the signing key", - default: "secp256k1", - }); + parser.add_argument("--account", { help: "The account of the keystore wallet to send from", required: true }); + parser.add_argument("--owner-index", { help: "The index of the owner", default: 0 }); + parser.add_argument("--initial-config-data", { help: "The initial config data needed to deploy the wallet as a hex string" }); + parser.add_argument("--private-key", { help: "The current private key of the owner", ...defaultToEnv("PRIVATE_KEY") }); + parser.add_argument("--to", { help: "The address to send to", required: true }); + parser.add_argument("--signature-type", { help: "The type of signature for the signing key", default: "secp256k1" }); const args = parser.parse_args(); + + if (!args.private_key) { + console.error("The --private-key argument is required."); + process.exit(1); + } + + if (!/^0x[a-fA-F0-9]{40}$/.test(args.to)) { + console.error("Invalid address format for --to. Must be a valid Ethereum address."); + process.exit(1); + } + + const ownerIndex = parseInt(args.owner_index, 10); + if (isNaN(ownerIndex)) { + console.error("Invalid value for --owner-index. Must be a number."); + process.exit(1); + } + let callsModule: any; let privateKey: any; if (args.signature_type === "secp256k1") { @@ -43,26 +43,37 @@ async function main() { privateKey = args.private_key; } else if (args.signature_type === "webauthn") { console.log("Using WebAuthn via keyspace..."); + try { + privateKey = P256.fromJWK(JSON.parse(args.private_key)); + } catch (error) { + console.error("Invalid private key JSON format for WebAuthn:", error.message); + process.exit(1); + } callsModule = callsWebAuthn; - privateKey = P256.fromJWK(JSON.parse(args.private_key)); } else { - console.error("Invalid circuit type"); + console.error("Invalid signature type. Supported types are 'secp256k1' and 'webauthn'."); + process.exit(1); } const amount = 1n; - const calls: Call[] = [{ - index: 0, - target: args.to, - data: "0x", - value: amount, - }]; - callsModule.makeCalls({ + const calls: Call[] = [ + { + index: 0, + target: args.to, + data: "0x", + value: amount, + }, + ]; + + await callsModule.makeCalls({ account: args.account, - ownerIndex: args.owner_index, + ownerIndex, initialConfigData: args.initial_config_data, privateKey, calls, }); + + console.log("Transaction submitted successfully."); } if (import.meta.main) { diff --git a/scripts/sync-keystore.ts b/scripts/sync-keystore.ts index 67d01b9..5e537dc 100644 --- a/scripts/sync-keystore.ts +++ b/scripts/sync-keystore.ts @@ -16,104 +16,71 @@ async function main() { description: "Sync the wallet's keystore config from the configured master chain to the replica chain", }); - parser.add_argument("--account", { - help: "The account of the keystore wallet to sync", - required: true, - }); - parser.add_argument("--private-key", { - help: "The current private key of the syncer", - ...defaultToEnv("PRIVATE_KEY"), - }); - parser.add_argument("--signature-type", { - help: "The type of signature for the private key", - default: "secp256k1", - }); - parser.add_argument("--config-data", { - help: "The current config data for the wallet to sync as a hex string", - required: true, - }); - parser.add_argument("--initial-config-data", { - help: "The initial config data needed to deploy the wallet as a hex string. Required if the wallet has not been deployed.", - }); - parser.add_argument("--target-chain", { - help: "The target chain to sync the wallet to", - default: "OP Sepolia", - }); + parser.add_argument("--account", { help: "The account of the keystore wallet to sync", required: true }); + parser.add_argument("--private-key", { help: "The current private key of the syncer", ...defaultToEnv("PRIVATE_KEY") }); + parser.add_argument("--signature-type", { help: "The type of signature for the private key", default: "secp256k1" }); + parser.add_argument("--config-data", { help: "The current config data for the wallet to sync as a hex string", required: true }); + parser.add_argument("--initial-config-data", { help: "The initial config data needed to deploy the wallet as a hex string. Required if the wallet has not been deployed." }); + parser.add_argument("--target-chain", { help: "The target chain to sync the wallet to", default: "OP Sepolia" }); const args = parser.parse_args(); + if (!["secp256k1", "webauthn"].includes(args.signature_type)) { + console.error("Invalid signature type. Supported types are 'secp256k1' and 'webauthn'."); + process.exit(1); + } + let privateKey: any; let callsModule: any; if (args.signature_type === "secp256k1") { - console.log("Using secp256k1 private key..."); privateKey = args.private_key; callsModule = callsSecp256k1; - } else if (args.signature_type === "webauthn") { - console.log("Using WebAuthn private key..."); - privateKey = P256.fromJWK(JSON.parse(args.private_key)); - callsModule = callsWebAuthn; } else { - console.error("Invalid signature type"); + try { + privateKey = P256.fromJWK(JSON.parse(args.private_key)); + } catch (error) { + console.error("Invalid private key JSON format for WebAuthn:", error.message); + process.exit(1); + } + callsModule = callsWebAuthn; } - // Using the data on the specified replica chain, detect the master chain ID. const replicaChain = Object.values(chains).find((chain) => chain.name === args.target_chain); - const replicaClient: PublicClient = createPublicClient({ - chain: replicaChain, - transport: http(), - }); + if (!replicaChain) { + console.error(`Target chain "${args.target_chain}" not found.`); + process.exit(1); + } + const replicaClient: PublicClient = createPublicClient({ chain: replicaChain, transport: http() }); const masterChainId = await getMasterChainId(replicaClient); const masterChain = Object.values(chains).find((chain) => BigInt(chain.id) === masterChainId); - const masterClient: PublicClient = createPublicClient({ - chain: masterChain, - transport: http(), - }); + if (!masterChain) { + console.error("Master chain could not be determined. Check the configuration on the target chain."); + process.exit(1); + } + const masterClient: PublicClient = createPublicClient({ chain: masterChain, transport: http() }); - // Query the master chain and L1 for proofs of the config hash. const keystoreProofs = await getMasterKeystoreProofs(args.account, masterClient, replicaClient, l1Client); - // Encode the nonce and --config-data into a Config struct, then hash it and - // compare it to the proof. - const currentConfig = { - account: args.account, - nonce: keystoreProofs.keystoreConfigNonce, - data: args.config_data, - }; + const currentConfig = { account: args.account, nonce: keystoreProofs.keystoreConfigNonce, data: args.config_data }; const currentConfigHash = hashConfig(currentConfig); - if (currentConfigHash !== keystoreProofs.keystoreConfigHash) { - if (fromHex(keystoreProofs.keystoreConfigHash, "bigint") === 0n) { - console.log("The config hash is empty on the master chain. Syncing the confirmed config timestamp..."); - } else { - console.warn(`The provided config data does not hash to the expected value. Expected ${keystoreProofs.keystoreConfigHash}, got ${currentConfigHash}.`); - } + if (currentConfigHash !== keystoreProofs.keystoreConfigHash && fromHex(keystoreProofs.keystoreConfigHash, "bigint") !== 0n) { + console.error("Config hash mismatch. Please verify the provided config data."); + process.exit(1); } - // Check if we need to deploy the wallet before syncing. if (!await getIsDeployed(replicaClient, args.account) && !args.initial_config_data) { console.error("Wallet is not deployed, and no initial config data was provided."); process.exit(1); } - // Call confirmConfig on the replica chain with the Config struct and the proof. const keystoreProof = encodeOPStackProof(keystoreProofs); const data = buildConfirmConfigCalldata(currentConfig, keystoreProof); + const calls: Call[] = [{ index: 0, target: args.account, data, value: 0n }]; - const calls: Call[] = [{ - index: 0, - target: args.account, - data, - value: 0n, - }]; - - await callsModule.makeCalls({ - account: args.account, - ownerIndex: args.owner_index, - initialConfigData: args.initial_config_data, - privateKey, - calls, - }); + await callsModule.makeCalls({ account: args.account, ownerIndex: 0, initialConfigData: args.initial_config_data, privateKey, calls }); + console.log("Wallet configuration successfully synced to the target chain."); } if (import.meta.main) {