From 9339ab4aafe2ce110ac88cbc3f51deaacae58296 Mon Sep 17 00:00:00 2001 From: Kemperino <33121795+Kemperino@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:51:45 +0200 Subject: [PATCH 1/4] chore: compile v1 with old poseidon wrapper --- contracts/src/core/WorldIDRegistry.sol | 19 ++- .../src/core/WorldIDRegistryV2Unreleased.sol | 18 ++ contracts/src/core/hash/Poseidon2V1.sol | 11 ++ .../core/libraries/FullStorageBinaryIMT.sol | 4 +- .../core/libraries/WorldIDRegistryV1Tree.sol | 156 ++++++++++++++++++ 5 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 contracts/src/core/hash/Poseidon2V1.sol create mode 100644 contracts/src/core/libraries/WorldIDRegistryV1Tree.sol diff --git a/contracts/src/core/WorldIDRegistry.sol b/contracts/src/core/WorldIDRegistry.sol index d9b3a6c0c..78245d162 100644 --- a/contracts/src/core/WorldIDRegistry.sol +++ b/contracts/src/core/WorldIDRegistry.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.13; import {FullStorageBinaryIMT, FullBinaryIMTData} from "./libraries/FullStorageBinaryIMT.sol"; +import {WorldIDRegistryV1Tree} from "./libraries/WorldIDRegistryV1Tree.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; import {PackedAccountData} from "./libraries/PackedAccountData.sol"; @@ -121,7 +122,7 @@ contract WorldIDRegistry is WorldIDBase, IWorldIDRegistry { // Insert a sentinel leaf to start leaf indexes at 1. // The 0-index of the tree is RESERVED. - _tree.insert(uint256(0)); + _insertLeaf(uint256(0)); _nextLeafIndex = 1; _recordCurrentRoot(); @@ -365,10 +366,20 @@ contract WorldIDRegistry is WorldIDBase, IWorldIDRegistry { uint256 oldOffchainSignerCommitment, uint256 newOffchainSignerCommitment ) internal virtual { - _tree.update(uint256(leafIndex), oldOffchainSignerCommitment, newOffchainSignerCommitment); + WorldIDRegistryV1Tree.update( + _tree, uint256(leafIndex), oldOffchainSignerCommitment, newOffchainSignerCommitment + ); _recordCurrentRoot(); } + function _insertLeaf(uint256 offchainSignerCommitment) internal virtual { + WorldIDRegistryV1Tree.insert(_tree, offchainSignerCommitment); + } + + function _insertManyLeaves(uint256[] memory offchainSignerCommitments) internal virtual { + WorldIDRegistryV1Tree.insertMany(_tree, offchainSignerCommitments); + } + /** * @dev Internal function to register an account. * @param recoveryAddress The recovery address for the new account. @@ -457,7 +468,7 @@ contract WorldIDRegistry is WorldIDBase, IWorldIDRegistry { revert InsufficientFunds(); } _registerAccount(recoveryAddress, authenticatorAddresses, authenticatorPubkeys, offchainSignerCommitment); - _tree.insert(offchainSignerCommitment); + _insertLeaf(offchainSignerCommitment); _recordCurrentRoot(); } @@ -493,7 +504,7 @@ contract WorldIDRegistry is WorldIDBase, IWorldIDRegistry { } // Update tree - _tree.insertMany(offchainSignerCommitments); + _insertManyLeaves(offchainSignerCommitments); _recordCurrentRoot(); } diff --git a/contracts/src/core/WorldIDRegistryV2Unreleased.sol b/contracts/src/core/WorldIDRegistryV2Unreleased.sol index 363f62e1f..bf8e64443 100644 --- a/contracts/src/core/WorldIDRegistryV2Unreleased.sol +++ b/contracts/src/core/WorldIDRegistryV2Unreleased.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import {WorldIDRegistry} from "./WorldIDRegistry.sol"; import {IWorldIDRegistry} from "./interfaces/IWorldIDRegistry.sol"; import {IWorldIDRegistryV2} from "./interfaces/IWorldIDRegistryV2.sol"; +import {FullStorageBinaryIMT} from "./libraries/FullStorageBinaryIMT.sol"; import {PackedAccountData} from "./libraries/PackedAccountData.sol"; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; @@ -93,6 +94,23 @@ contract WorldIDRegistryV2 is IWorldIDRegistryV2, WorldIDRegistry { return ts + _rootValidityWindow; } + function _updateLeafAndRecord( + uint64 leafIndex, + uint256 oldOffchainSignerCommitment, + uint256 newOffchainSignerCommitment + ) internal virtual override { + FullStorageBinaryIMT.update(_tree, uint256(leafIndex), oldOffchainSignerCommitment, newOffchainSignerCommitment); + _recordCurrentRoot(); + } + + function _insertLeaf(uint256 offchainSignerCommitment) internal virtual override { + FullStorageBinaryIMT.insert(_tree, offchainSignerCommitment); + } + + function _insertManyLeaves(uint256[] memory offchainSignerCommitments) internal virtual override { + FullStorageBinaryIMT.insertMany(_tree, offchainSignerCommitments); + } + //////////////////////////////////////////////////////////// // AUTHENTICATOR MANAGEMENT // //////////////////////////////////////////////////////////// diff --git a/contracts/src/core/hash/Poseidon2V1.sol b/contracts/src/core/hash/Poseidon2V1.sol new file mode 100644 index 000000000..a217f64d7 --- /dev/null +++ b/contracts/src/core/hash/Poseidon2V1.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Poseidon2T2} from "./Poseidon2.sol"; + +/// @dev V1 compatibility wrapper that keeps Poseidon behind a public library call. +library Poseidon2T2V1 { + function compress(uint256[2] memory inputs) public pure returns (uint256) { + return Poseidon2T2.compress(inputs); + } +} diff --git a/contracts/src/core/libraries/FullStorageBinaryIMT.sol b/contracts/src/core/libraries/FullStorageBinaryIMT.sol index 70f4e7a08..2783cd959 100644 --- a/contracts/src/core/libraries/FullStorageBinaryIMT.sol +++ b/contracts/src/core/libraries/FullStorageBinaryIMT.sol @@ -101,14 +101,14 @@ library FullStorageBinaryIMT { } /// @dev Encode (level, idx) into a single mapping key. - function _key(uint256 level, uint256 idx) private pure returns (uint256) { + function _key(uint256 level, uint256 idx) internal pure returns (uint256) { return (level << 32) | idx; } /// @dev Returns the node value at (level, index). Unset nodes return the /// default zero for that level. function _getNode(FullBinaryIMTData storage self, uint256 level, uint256 idx, uint256 numLeaves) - private + internal view returns (uint256) { diff --git a/contracts/src/core/libraries/WorldIDRegistryV1Tree.sol b/contracts/src/core/libraries/WorldIDRegistryV1Tree.sol new file mode 100644 index 000000000..d9e8584b6 --- /dev/null +++ b/contracts/src/core/libraries/WorldIDRegistryV1Tree.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Poseidon2T2V1 as Poseidon2T2} from "../hash/Poseidon2V1.sol"; +import { + FullBinaryIMTData, + FullStorageBinaryIMT, + LeafDoesNotExist, + LeafIndexOutOfRange, + NewLeafCannotEqualOldLeaf, + SNARK_SCALAR_FIELD, + TreeIsFull, + ValueGreaterThanSnarkScalarField +} from "./FullStorageBinaryIMT.sol"; + +/// @dev V1 registry write paths copied from FullStorageBinaryIMT, with Poseidon2T2 aliased to the V1 public wrapper. +library WorldIDRegistryV1Tree { + function insert(FullBinaryIMTData storage self, uint256 leaf) internal returns (uint256) { + uint256 depth = self.depth; + + if (leaf >= SNARK_SCALAR_FIELD) { + revert ValueGreaterThanSnarkScalarField(); + } + if (self.numberOfLeaves >= uint256(1) << depth) { + revert TreeIsFull(); + } + + uint256 numLeaves = self.numberOfLeaves; + uint256 idx = numLeaves; + + self.nodes[FullStorageBinaryIMT._key(0, idx)] = leaf; + + uint256 hash = leaf; + for (uint256 level = 0; level < depth;) { + uint256 siblingIdx = idx ^ 1; + uint256 sibling = FullStorageBinaryIMT._getNode(self, level, siblingIdx, numLeaves); + + if ((idx & 1) == 0) { + hash = Poseidon2T2.compress([hash, sibling]); + } else { + hash = Poseidon2T2.compress([sibling, hash]); + } + + idx >>= 1; + unchecked { + ++level; + } + + // Write the parent node + self.nodes[FullStorageBinaryIMT._key(level, idx)] = hash; + } + + self.root = hash; + self.numberOfLeaves += 1; + return hash; + } + + function insertMany(FullBinaryIMTData storage self, uint256[] memory leaves) internal returns (uint256) { + uint256 k = leaves.length; + if (k == 0) return self.root; + + uint256 depth = self.depth; + uint256 start = self.numberOfLeaves; + uint256 cap = uint256(1) << depth; + if (start >= cap || k > cap - start) revert TreeIsFull(); + + for (uint256 i = 0; i < k;) { + uint256 leaf = leaves[i]; + if (leaf >= SNARK_SCALAR_FIELD) { + revert ValueGreaterThanSnarkScalarField(); + } + self.nodes[FullStorageBinaryIMT._key(0, start + i)] = leaf; + unchecked { + ++i; + } + } + + uint256 levelStart = start; + uint256 levelEnd = start + k - 1; + uint256 effectiveLeaves = start + k; + + for (uint256 level = 0; level < depth;) { + uint256 parentStart = levelStart >> 1; + uint256 parentEnd = levelEnd >> 1; + uint256 parentLevel; + unchecked { + parentLevel = level + 1; + } + + for (uint256 p = parentStart; p <= parentEnd;) { + uint256 leftChild = p << 1; + uint256 left = FullStorageBinaryIMT._getNode(self, level, leftChild, effectiveLeaves); + uint256 right = FullStorageBinaryIMT._getNode(self, level, leftChild | 1, effectiveLeaves); + + self.nodes[FullStorageBinaryIMT._key(parentLevel, p)] = Poseidon2T2.compress([left, right]); + unchecked { + ++p; + } + } + + levelStart = parentStart; + levelEnd = parentEnd; + unchecked { + ++level; + } + } + + uint256 newRoot = self.nodes[FullStorageBinaryIMT._key(depth, 0)]; + self.root = newRoot; + self.numberOfLeaves = start + k; + return newRoot; + } + + function update(FullBinaryIMTData storage self, uint256 index, uint256 oldLeaf, uint256 newLeaf) internal { + if (newLeaf == oldLeaf) { + revert NewLeafCannotEqualOldLeaf(); + } + if (newLeaf >= SNARK_SCALAR_FIELD) { + revert ValueGreaterThanSnarkScalarField(); + } + uint256 numLeaves = self.numberOfLeaves; + if (index >= numLeaves) { + revert LeafIndexOutOfRange(); + } + + uint256 stored = FullStorageBinaryIMT._getNode(self, 0, index, numLeaves); + if (stored != oldLeaf) { + revert LeafDoesNotExist(); + } + + uint256 depth = self.depth; + uint256 idx = index; + + self.nodes[FullStorageBinaryIMT._key(0, idx)] = newLeaf; + + uint256 hash = newLeaf; + for (uint256 level = 0; level < depth;) { + uint256 siblingIdx = idx ^ 1; + uint256 sibling = FullStorageBinaryIMT._getNode(self, level, siblingIdx, numLeaves); + + if ((idx & 1) == 0) { + hash = Poseidon2T2.compress([hash, sibling]); + } else { + hash = Poseidon2T2.compress([sibling, hash]); + } + + idx >>= 1; + unchecked { + ++level; + } + self.nodes[FullStorageBinaryIMT._key(level, idx)] = hash; + } + + self.root = hash; + } +} From c64012d8a3b695f89c46f5987cb6209041c47974 Mon Sep 17 00:00:00 2001 From: Kemperino <33121795+Kemperino@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:39:07 +0200 Subject: [PATCH 2/4] fix: linkage in fixtures --- crates/test-utils/src/anvil.rs | 39 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/crates/test-utils/src/anvil.rs b/crates/test-utils/src/anvil.rs index 529094e8e..783f46190 100644 --- a/crates/test-utils/src/anvil.rs +++ b/crates/test-utils/src/anvil.rs @@ -49,10 +49,10 @@ sol!( sol!( #[sol(rpc)] - Poseidon2T2, + Poseidon2T2V1, concat!( env!("CARGO_MANIFEST_DIR"), - "/../../contracts/out/Poseidon2.sol/Poseidon2T2.json" + "/../../contracts/out/Poseidon2V1.sol/Poseidon2T2V1.json" ) ); @@ -415,9 +415,9 @@ impl TestAnvil { .wallet(EthereumWallet::from(signer.clone())) .connect_http(self.rpc_url.parse().context("invalid anvil endpoint URL")?); - let poseidon = Poseidon2T2::deploy(provider.clone()) + let poseidon_v1 = Poseidon2T2V1::deploy(provider.clone()) .await - .context("failed to deploy Poseidon2T2 library")?; + .context("failed to deploy Poseidon2T2V1 library")?; let packed_account_data = PackedAccountData::deploy(provider.clone()) .await .context("failed to deploy PackedAccountData library")?; @@ -429,7 +429,7 @@ impl TestAnvil { let implementation_address = Self::deploy_linked_registry_impl( provider.clone(), v1_json, - *poseidon.address(), + Some(*poseidon_v1.address()), *packed_account_data.address(), ) .await @@ -464,10 +464,10 @@ impl TestAnvil { .wallet(EthereumWallet::from(signer.clone())) .connect_http(self.rpc_url.parse().context("invalid anvil endpoint URL")?); - // 1. Deploy the libraries shared by both V1 and V2 bytecode. - let poseidon = Poseidon2T2::deploy(provider.clone()) + // 1. Deploy the libraries used by the registry bytecode. + let poseidon_v1 = Poseidon2T2V1::deploy(provider.clone()) .await - .context("failed to deploy Poseidon2T2 library")?; + .context("failed to deploy Poseidon2T2V1 library")?; let packed_account_data = PackedAccountData::deploy(provider.clone()) .await .context("failed to deploy PackedAccountData library")?; @@ -480,7 +480,7 @@ impl TestAnvil { let v1_impl = Self::deploy_linked_registry_impl( provider.clone(), v1_json, - *poseidon.address(), + Some(*poseidon_v1.address()), *packed_account_data.address(), ) .await @@ -500,7 +500,7 @@ impl TestAnvil { let v2_impl = Self::deploy_linked_registry_impl( provider.clone(), v2_json, - *poseidon.address(), + None, *packed_account_data.address(), ) .await @@ -539,12 +539,11 @@ impl TestAnvil { ) } - /// Links Poseidon2T2 + PackedAccountData into a registry implementation - /// bytecode (`WorldIDRegistry` or `WorldIDRegistryV2`) and deploys it. + /// Links the registry implementation bytecode and deploys it. async fn deploy_linked_registry_impl( provider: P, impl_json: &str, - poseidon_addr: Address, + poseidon_v1_addr: Option
, packed_account_data_addr: Address, ) -> Result
{ let json_value: serde_json::Value = serde_json::from_str(impl_json)?; @@ -559,12 +558,14 @@ impl TestAnvil { }) .to_string(); - bytecode_str = Self::link_bytecode_hex( - impl_json, - &bytecode_str, - "src/core/hash/Poseidon2.sol:Poseidon2T2", - poseidon_addr, - )?; + if let Some(poseidon_v1_addr) = poseidon_v1_addr { + bytecode_str = Self::link_bytecode_hex( + impl_json, + &bytecode_str, + "src/core/hash/Poseidon2V1.sol:Poseidon2T2V1", + poseidon_v1_addr, + )?; + } bytecode_str = Self::link_bytecode_hex( impl_json, &bytecode_str, From 3115c107ce222fd97f72b79c38bd74d25eb2f63e Mon Sep 17 00:00:00 2001 From: Kemperino <33121795+Kemperino@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:34:10 +0200 Subject: [PATCH 3/4] public library on tree operation --- contracts/src/core/libraries/FullStorageBinaryIMT.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/core/libraries/FullStorageBinaryIMT.sol b/contracts/src/core/libraries/FullStorageBinaryIMT.sol index 2783cd959..0e1e56a48 100644 --- a/contracts/src/core/libraries/FullStorageBinaryIMT.sol +++ b/contracts/src/core/libraries/FullStorageBinaryIMT.sol @@ -135,7 +135,7 @@ library FullStorageBinaryIMT { /// @dev Inserts a leaf at the next available position. /// Writes the leaf and every internal node on its path to the root. - function insert(FullBinaryIMTData storage self, uint256 leaf) internal returns (uint256) { + function insert(FullBinaryIMTData storage self, uint256 leaf) public returns (uint256) { uint256 depth = self.depth; if (leaf >= SNARK_SCALAR_FIELD) { @@ -185,7 +185,7 @@ library FullStorageBinaryIMT { /// [start >> (L+1), (start+k-1) >> (L+1)], so the total work /// is k + k/2 + k/4 + … + 1 + (D − log₂k) ≈ 2k + D /// hashes and the same number of SSTOREs. - function insertMany(FullBinaryIMTData storage self, uint256[] memory leaves) internal returns (uint256) { + function insertMany(FullBinaryIMTData storage self, uint256[] memory leaves) public returns (uint256) { uint256 k = leaves.length; if (k == 0) return self.root; @@ -253,7 +253,7 @@ library FullStorageBinaryIMT { /// @dev Updates a leaf in the tree. No caller-supplied proof needed. /// Reads siblings from storage, verifies the old leaf, writes the new /// path, and updates the root. - function update(FullBinaryIMTData storage self, uint256 index, uint256 oldLeaf, uint256 newLeaf) internal { + function update(FullBinaryIMTData storage self, uint256 index, uint256 oldLeaf, uint256 newLeaf) public { if (newLeaf == oldLeaf) { revert NewLeafCannotEqualOldLeaf(); } From 5a5622b4f2724c8afe796fa71eb11122f826e33f Mon Sep 17 00:00:00 2001 From: Kemperino <33121795+Kemperino@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:45:18 +0200 Subject: [PATCH 4/4] fix: link full storage tree in fixtures --- .../registries/abi/WorldIDRegistryV2Abi.json | 20 ---------------- crates/test-utils/src/anvil.rs | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/registries/abi/WorldIDRegistryV2Abi.json b/crates/registries/abi/WorldIDRegistryV2Abi.json index dd9da5cca..e9fa2e578 100644 --- a/crates/registries/abi/WorldIDRegistryV2Abi.json +++ b/crates/registries/abi/WorldIDRegistryV2Abi.json @@ -1799,11 +1799,6 @@ "name": "InvalidSignature", "inputs": [] }, - { - "type": "error", - "name": "LeafDoesNotExist", - "inputs": [] - }, { "type": "error", "name": "LeafIndexOutOfRange", @@ -1909,11 +1904,6 @@ "name": "MismatchingArrayLengths", "inputs": [] }, - { - "type": "error", - "name": "NewLeafCannotEqualOldLeaf", - "inputs": [] - }, { "type": "error", "name": "NoActiveRecoveryAgentUpdate", @@ -2079,11 +2069,6 @@ } ] }, - { - "type": "error", - "name": "TreeIsFull", - "inputs": [] - }, { "type": "error", "name": "UUPSUnauthorizedCallContext", @@ -2116,11 +2101,6 @@ "name": "UnmanageableNotAllowed", "inputs": [] }, - { - "type": "error", - "name": "ValueGreaterThanSnarkScalarField", - "inputs": [] - }, { "type": "error", "name": "WrongDefaultZeroIndex", diff --git a/crates/test-utils/src/anvil.rs b/crates/test-utils/src/anvil.rs index 783f46190..cdfdb5ac5 100644 --- a/crates/test-utils/src/anvil.rs +++ b/crates/test-utils/src/anvil.rs @@ -65,6 +65,15 @@ sol!( ) ); +sol!( + #[sol(rpc)] + FullStorageBinaryIMT, + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../contracts/out/FullStorageBinaryIMT.sol/FullStorageBinaryIMT.json" + ) +); + sol!( #[sol(rpc, ignore_unlinked)] BinaryIMT, @@ -430,6 +439,7 @@ impl TestAnvil { provider.clone(), v1_json, Some(*poseidon_v1.address()), + None, *packed_account_data.address(), ) .await @@ -481,6 +491,7 @@ impl TestAnvil { provider.clone(), v1_json, Some(*poseidon_v1.address()), + None, *packed_account_data.address(), ) .await @@ -497,10 +508,14 @@ impl TestAnvil { env!("CARGO_MANIFEST_DIR"), "/../../contracts/out/WorldIDRegistryV2Unreleased.sol/WorldIDRegistryV2.json" )); + let full_storage_binary_imt = FullStorageBinaryIMT::deploy(provider.clone()) + .await + .context("failed to deploy FullStorageBinaryIMT library")?; let v2_impl = Self::deploy_linked_registry_impl( provider.clone(), v2_json, None, + Some(*full_storage_binary_imt.address()), *packed_account_data.address(), ) .await @@ -544,6 +559,7 @@ impl TestAnvil { provider: P, impl_json: &str, poseidon_v1_addr: Option
, + full_storage_binary_imt_addr: Option
, packed_account_data_addr: Address, ) -> Result
{ let json_value: serde_json::Value = serde_json::from_str(impl_json)?; @@ -566,6 +582,14 @@ impl TestAnvil { poseidon_v1_addr, )?; } + if let Some(full_storage_binary_imt_addr) = full_storage_binary_imt_addr { + bytecode_str = Self::link_bytecode_hex( + impl_json, + &bytecode_str, + "src/core/libraries/FullStorageBinaryIMT.sol:FullStorageBinaryIMT", + full_storage_binary_imt_addr, + )?; + } bytecode_str = Self::link_bytecode_hex( impl_json, &bytecode_str,