# One-stop social recovery: separating logic from assets > Many thanks to [Matt](https://twitter.com/lightclients) for great discussion on that topic and for developing this [tool](https://github.com/lightclient/go-ethereum/tree/cmd-find-slot-zero) to parse every contract and put them into a sparse Merkle tree which simplifies prototyping by not having to do zk proves over the patricia tree. Also, thanks to [Vitalik](https://twitter.com/VitalikButerin) for great input. Managing multiple accounts can be challenging for several reasons, including issues with social recovery, privacy, L2s and overall user experience. Using [stealth addresses](https://eips.ethereum.org/EIPS/eip-5564) complicates it further, as each interaction needs a new account. Vitalik [suggested](https://vitalik.ca/general/2023/06/09/three_transitions.html) using zk-SNARKs to separate the account for transaction logic from the one holding assets. This allows for improved privacy, user experience and one-click social recovery. > For the following, it is recommended to first have a look at Vitalik's [The Three Transitions](https://vitalik.ca/general/2023/06/09/three_transitions.html) post for some background. **In short, what we try to achieve is the following:** *One-stop social recovery without compromising privacy.* ### Naive Approach A straightforward but privacy-compromising implementation would look like this: ![](https://hackmd.io/_uploads/B1pB0b-Ch.png) 1. User provides a signature and some intent/command to the asset-holding account. 2. The asset holding account forwards the signature to the logic-holding account. 3. The logic-holding account derives the pubkey from the signature and compares it with the pubkey it has stored. 4. If verified, the logic account tells the asset-holding account to proceed. 5. The asset-holding account executes the users's command. The downside is that this links the logic- and asset-holding accounts publicly, compromising privacy. ### Using ZK-SNARKs By using zk-SNARKs, a user can prove they have permission to spend without revealing the connection between the logic-holding and asset-holding accounts. ![](https://hackmd.io/_uploads/rJFDJfZ0h.png) **The workflow then looks like the following:** 1. Users locally construct a Merkle tree and identify the leaf that contains their contract. 1.1. The Merkle tree basically contains the slot0 and slot1 values of every existing contract sorted by date or name. 1.2. Every user is able to locally construct the Merkle tree from a recent state. 3. User constructs a zk-proof, proving to know a secret within a logic-holding account. More to the exact proof later. 4. User sends the zk-proof to the asset-holding account. 5. The asset-holding account validates the proof, confirming the following: 4.1. The user knows where the logic is held. 4.2. The user knows a secret value that, when hashed, maps to a value that is stored in the logic holding account. 4.3. The user can reconstruct a account state merkle tree root maintained in the canonical chain (e.g. a precompile) 6.3. The correct nonce (used for switching keys in the logic holding account) is used. Essentially, the user says, "*I have provable permission from a logic-holding account to perform this action, and I know where that logic account is within the state.*" #### Advantages * **User Experience:** One private key or one multisig setup controls multiple accounts, even if they are on different L2s. * **Recovery:** It's easier to recover accounts with a single contract update. * **Privacy:** No public link between various accounts. * **Compatibility:** This helps in popularizing Account Abstraction (AA) wallets and other features. In addition, by adding another (aggregator) contract between the logic and asset-holding contract, multiple proofs to different asset-holding accounts can be provided within a single trancation, allowing to treat accounts almost like UTXOs. The aggregator would be able to take multiple zk-proofs and forwards them to the respective asset-holding accounts for verification. Of course, such an aggregator may create a link between the individual asset-holding accounts - comprising privacy. > It is important to note that it's not necessarily a binary choice between employing SNARKs, thus relying on their security, and not using them at all and consequently missing out on the nice privacy properties. Instead of requiring a SNARK proof for spending, a compromise could be to use the SNARK proof to open a time window within the logic holding contract, preceded by a short delay, after which the owner of the logic-holding contract can change the slot0 value, thus change the spending logic. The delay until the time window opens can be used by the current owner of the contract to prevent the credential update. ## Technical Details The zk-SNARK setup includes private elements: * A **secret key** for verification. * **Logic-holding account address** to that the asset-holding account points to. * **Merkle branches** to identify specific state values. * A **nonce** to allow key rotation while invalidating old ones. Private elements, such as the plaintext logic-holding contract address and the secret, are not publicly disclosed but used in privately linking the logic-holding and asset-holding accounts. By generating the proof over the whole state, no central party for constructing the merkle tree to submit proofs against is required. ### Logic-Holding Accounts A prototype of the logic-holding account may look like this: ```solidity pragma solidity >=0.7.0 <0.9.0; contract LogicHoldingAccount is Ownable { uint256 public slot0 = 0x1234; // hashed secret uint256 public nonce = 0; // keep track of key changes address public owner; function updateOwner(uint256 newValue) public onlyOwner { nonce += 1; slot0 = newValue; } } ``` * **slot0:** A public variable that initially holds a hashed value. Only the owner knows the preimage of the hash. * **nonce:** A counter that keeps track of the number of times the owner information is updated. This ensures that old keys become invalid. * **updateOwner(uint256 newValue):** A function that updates the `slot0` value and increments the nonce. This contract keeps track of the owner's current spending logic (`slot0`) and allows for updates via the `updateOwner` function. ### Account-holding Account(s) ```solidity pragma solidity >=0.7.0 <0.9.0; contract AssetHoldingAccount { uint256 public logicHoldingAccountHash = 1234...; // Scalar field size, Base field size, Verification Key data, etc. // ... function verifyProof( uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[2] calldata _pubSignals ) public view returns (bool val) { // Snarkjs assembly code for proof verification... // ... } // _pubSignals[0] - the root of the contract-slot0||nonce Merkle tree // _pubSignals[1] - the hased logic-holder address function execute( address payable to, uint256 amount, uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[2] calldata _pubSignals ) public { contractRootPrecompile.getRoot(block.number) uint256 specifiedLogicHolder = _pubSignals[1]; require(specifiedLogicHolder == logicHoldingAccountHash, "Not allowed"); bool validProof = verifyProof(_pA, _pB, _pC, _pubSignals) == true; if (validProof) { (bool success,) = to.call{value:amount}(""); require(success); } } receive() external payable {} } ``` The asset-holding account stores assets like ETH and enables users to submit proofs for withdrawals. By verifying that `specifiedLogicHolder` matches `logicHoldingAccountHash`, the owner can ensure that the asset-holding contract accepts proofs only from the authorized logic-holding contract, rather than any arbitrary contract. The secret provided as a private signal when constructing the proof ensures that only the owner of the account, which contains the spending logic, can access funds from the asset-holding account. ### The circuit The following circuit was developed using [circom](https://docs.circom.io/). The complete code can be found [here](). ```circom pragma circom 2.0.2; include "./modules/merkleTree.circom"; include "./modules/commitmentHasher.circom"; template Main(levels) { signal input root; signal input logicHoldingAddressHash; signal input logicHoldingAddress; signal input secret; signal input nonce; signal input pathElements[levels]; signal input pathIndices[levels]; component secretHasher = SecretHasher(); secretHasher.secret <== secret; component hasher = CommitmentHasher(); hasher.logicHoldingAddress <== logicHoldingAddress; hasher.secret <== secretHasher.hashedSecret; hasher.nonce <== nonce; hasher.logicHoldingAddressHash === logicHoldingAddressHash; component tree = MerkleTreeChecker(levels); tree.leaf <== hasher.commitment; tree.root <== root; for (var i = 0; i < levels; i++) { tree.pathElements[i] <== pathElements[i]; tree.pathIndices[i] <== pathIndices[i]; } } component main {public [root,logicHoldingAddressHash]} = Main(N); ``` The circuit has a total of 7 signals, 2 of which are public, namely the Merkle tree root and the hashed address of the logic-holding account (it must be hashed before encoding it into the asset-holding contracts to prevent observers from clustering accounts based on the same logic-holder account). ## Conclusion In a world where users must manage multiple accounts, the need for a one-stop social recovery feature becomes increasingly important. Zk-SNARKs can be used to implement logic/asset-separated wallets, enabling users to use the 'logic' of account A to spend from account B without creating a link between the two. As an initial step, SNARK proofs could be utilized for actions less risky than asset spending. For instance, a good starting point might be to allow users to initiate a 'withdrawal request'. This request can be finalized by the user after a certain period, provided it is not disputed by the owner of the logic-holding contract. This way, the owner of the logic-holding contract can still intervene, albeit in a privacy-breaking manner, in case something unexpected happens.