From 642edc0b9d017318f6ffcaf769cd72f83ff399b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 10:56:06 +0100 Subject: [PATCH 01/20] Implement Operation.expiresAt -> Operation.btl --- contracts/Entity.sol | 19 +++++++++++-------- contracts/EntityRegistry.sol | 23 +++++++++++++---------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/contracts/Entity.sol b/contracts/Entity.sol index 235aa73..07e3d91 100644 --- a/contracts/Entity.sol +++ b/contracts/Entity.sol @@ -40,19 +40,22 @@ library Entity { /// @dev Batch element: describes a single entity operation within an /// `execute()` call. Fields are interpreted according to `operationType`: - /// - CREATE: payload, contentType, attributes, expiresAt + /// - CREATE: payload, contentType, attributes, btl /// - UPDATE: entityKey, payload, contentType, attributes - /// - EXTEND: entityKey, expiresAt + /// - EXTEND: entityKey, btl /// - TRANSFER: entityKey, newOwner /// - DELETE: entityKey /// - EXPIRE: entityKey + /// + /// `btl` (blocks-to-live) is a relative duration: `expiresAt = currentBlock + btl`. + /// Must be non-zero for CREATE and EXTEND. struct Operation { uint8 operationType; bytes32 entityKey; bytes payload; Mime128 contentType; Attribute[] attributes; - BlockNumber expiresAt; + BlockNumber btl; address newOwner; } @@ -112,8 +115,8 @@ library Entity { error InvalidValueType(Ident32 name, uint8 valueType); /// @dev Reverted when operationType is unrecognized (including 0 / uninitialized). error InvalidOpType(uint8 operationType); - /// @dev Reverted when expiresAt is not strictly after the current block. - error ExpiryInPast(BlockNumber expiresAt, BlockNumber currentBlock); + /// @dev Reverted when btl is zero (entity would expire at the creation block). + error ZeroBtl(); /// @dev Reverted when the attribute count exceeds MAX_ATTRIBUTES. error TooManyAttributes(uint256 count, uint256 maxCount); /// @dev Reverted when an entity key does not exist in storage. @@ -193,9 +196,9 @@ library Entity { if (newExpiresAt <= currentExpiresAt) revert ExpiryNotExtended(key, newExpiresAt, currentExpiresAt); } - /// @dev Require that the expiry is strictly in the future. - function requireFutureExpiry(BlockNumber expiresAt, BlockNumber current) internal pure { - if (expiresAt <= current) revert ExpiryInPast(expiresAt, current); + /// @dev Require that btl is non-zero (entity must live for at least one block). + function requirePositiveBtl(BlockNumber btl) internal pure { + if (btl == BlockNumber.wrap(0)) revert ZeroBtl(); } // ------------------------------------------------------------------------- diff --git a/contracts/EntityRegistry.sol b/contracts/EntityRegistry.sol index e99853e..5e57d74 100644 --- a/contracts/EntityRegistry.sol +++ b/contracts/EntityRegistry.sol @@ -283,7 +283,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// /// Validation: /// 1. contentType must be valid MIME - /// 2. expiresAt must be strictly in the future + /// 2. btl must be non-zero /// 3. Attributes validated inside coreHash (count, sorting, value type/length) function _create(Entity.Operation calldata op, BlockNumber current) internal @@ -291,23 +291,24 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { returns (bytes32 key, bytes32 entityHash_) { validateMime128(op.contentType); - Entity.requireFutureExpiry(op.expiresAt, current); + Entity.requirePositiveBtl(op.btl); + BlockNumber expiresAt = current + op.btl; key = _createEntityKey(msg.sender); bytes32 coreHash_; - (coreHash_, entityHash_) = _computeEntityHash(key, msg.sender, current, msg.sender, current, op.expiresAt, op); + (coreHash_, entityHash_) = _computeEntityHash(key, msg.sender, current, msg.sender, current, expiresAt, op); _commitments[key] = Entity.Commitment({ creator: msg.sender, createdAt: current, updatedAt: current, - expiresAt: op.expiresAt, + expiresAt: expiresAt, owner: msg.sender, coreHash: coreHash_ }); - emit EntityOperation(key, Entity.CREATE, msg.sender, op.expiresAt, entityHash_); + emit EntityOperation(key, Entity.CREATE, msg.sender, expiresAt, entityHash_); } /// @dev Update an existing entity's payload, contentType, and attributes. @@ -345,7 +346,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// Validation: /// 1. Entity must exist and be active /// 2. Caller must be the owner - /// 3. New expiresAt must be strictly greater than current expiresAt + /// 3. New expiresAt (current + btl) must be strictly greater than stored expiresAt function _extend(Entity.Operation calldata op, BlockNumber current) internal virtual returns (bytes32, bytes32) { bytes32 key = op.entityKey; Entity.Commitment storage c = _commitments[key]; @@ -353,14 +354,16 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { Entity.requireExists(key, c); Entity.requireActive(key, c, current); Entity.requireOwner(key, c); - Entity.requireExpiryIncreased(key, op.expiresAt, c.expiresAt); - c.expiresAt = op.expiresAt; + BlockNumber newExpiresAt = current + op.btl; + Entity.requireExpiryIncreased(key, newExpiresAt, c.expiresAt); + + c.expiresAt = newExpiresAt; c.updatedAt = current; - bytes32 entityHash_ = _wrapEntityHash(c.coreHash, c.owner, current, op.expiresAt); + bytes32 entityHash_ = _wrapEntityHash(c.coreHash, c.owner, current, newExpiresAt); - emit EntityOperation(key, Entity.EXTEND, c.owner, op.expiresAt, entityHash_); + emit EntityOperation(key, Entity.EXTEND, c.owner, newExpiresAt, entityHash_); return (key, entityHash_); } From bef3601a60cddec70206013fc70b5a177c7bb7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:02:13 +0100 Subject: [PATCH 02/20] Fix test compilation issues --- test/unit/ComputeEntityHash.t.sol | 22 +++++------ test/unit/Dispatch.t.sol | 2 +- test/unit/Execute.t.sol | 2 +- test/unit/Views.t.sol | 8 ++-- test/unit/guards/RequireFutureExpiry.t.sol | 34 ----------------- test/unit/guards/RequirePositiveBtl.t.sol | 26 +++++++++++++ test/unit/ops/Create.t.sol | 43 +++++----------------- test/utils/Lib.sol | 16 ++++---- 8 files changed, 61 insertions(+), 92 deletions(-) delete mode 100644 test/unit/guards/RequireFutureExpiry.t.sol create mode 100644 test/unit/guards/RequirePositiveBtl.t.sol diff --git a/test/unit/ComputeEntityHash.t.sol b/test/unit/ComputeEntityHash.t.sol index 06aebf2..740be55 100644 --- a/test/unit/ComputeEntityHash.t.sol +++ b/test/unit/ComputeEntityHash.t.sol @@ -55,7 +55,7 @@ contract ComputeEntityHashTest is Test, EntityRegistry { Entity.Operation memory op = Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000)); - (bytes32 coreHash_,) = this.doComputeEntityHash(key, alice, current, alice, current, op.expiresAt, op); + (bytes32 coreHash_,) = this.doComputeEntityHash(key, alice, current, alice, current, current + op.btl, op); bytes32 expected = this.doCoreHash(key, alice, current, textPlain, "hello", attrs); assertEq(coreHash_, expected); @@ -74,10 +74,10 @@ contract ComputeEntityHashTest is Test, EntityRegistry { Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000)); (bytes32 coreHash_, bytes32 entityHash_) = - this.doComputeEntityHash(key, alice, current, alice, current, op.expiresAt, op); + this.doComputeEntityHash(key, alice, current, alice, current, current + op.btl, op); // Verify entityHash matches _wrapEntityHash for the same inputs - bytes32 expected = _wrapEntityHash(coreHash_, alice, current, op.expiresAt); + bytes32 expected = _wrapEntityHash(coreHash_, alice, current, current + op.btl); assertEq(entityHash_, expected); } @@ -95,9 +95,9 @@ contract ComputeEntityHashTest is Test, EntityRegistry { Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000)); (bytes32 coreA, bytes32 entityA) = - this.doComputeEntityHash(key, alice, current, alice, current, op.expiresAt, op); + this.doComputeEntityHash(key, alice, current, alice, current, current + op.btl, op); (bytes32 coreB, bytes32 entityB) = - this.doComputeEntityHash(key, alice, current, alice, current, op.expiresAt, op); + this.doComputeEntityHash(key, alice, current, alice, current, current + op.btl, op); assertEq(coreA, coreB); assertEq(entityA, entityB); @@ -116,8 +116,8 @@ contract ComputeEntityHashTest is Test, EntityRegistry { Entity.Operation memory opA = Lib.createOp("hello", textPlain, attrs, expiry); Entity.Operation memory opB = Lib.createOp("world", textPlain, attrs, expiry); - (bytes32 coreA,) = this.doComputeEntityHash(key, alice, current, alice, current, opA.expiresAt, opA); - (bytes32 coreB,) = this.doComputeEntityHash(key, alice, current, alice, current, opB.expiresAt, opB); + (bytes32 coreA,) = this.doComputeEntityHash(key, alice, current, alice, current, current + opA.btl, opA); + (bytes32 coreB,) = this.doComputeEntityHash(key, alice, current, alice, current, current + opB.btl, opB); assertNotEq(coreA, coreB); } @@ -131,9 +131,9 @@ contract ComputeEntityHashTest is Test, EntityRegistry { Entity.Operation memory opB = Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(600)); (bytes32 coreA, bytes32 entityA) = - this.doComputeEntityHash(key, alice, current, alice, current, opA.expiresAt, opA); + this.doComputeEntityHash(key, alice, current, alice, current, current + opA.btl, opA); (bytes32 coreB, bytes32 entityB) = - this.doComputeEntityHash(key, alice, current, alice, current, opB.expiresAt, opB); + this.doComputeEntityHash(key, alice, current, alice, current, current + opB.btl, opB); // Same content → same coreHash assertEq(coreA, coreB); @@ -155,8 +155,8 @@ contract ComputeEntityHashTest is Test, EntityRegistry { Entity.Operation memory opA = Lib.createOp("hello", textPlain, attrsA, expiry); Entity.Operation memory opB = Lib.createOp("hello", textPlain, attrsB, expiry); - (bytes32 coreA,) = this.doComputeEntityHash(key, alice, current, alice, current, opA.expiresAt, opA); - (bytes32 coreB,) = this.doComputeEntityHash(key, alice, current, alice, current, opB.expiresAt, opB); + (bytes32 coreA,) = this.doComputeEntityHash(key, alice, current, alice, current, current + opA.btl, opA); + (bytes32 coreB,) = this.doComputeEntityHash(key, alice, current, alice, current, current + opB.btl, opB); assertNotEq(coreA, coreB); } diff --git a/test/unit/Dispatch.t.sol b/test/unit/Dispatch.t.sol index 9903221..4d203a1 100644 --- a/test/unit/Dispatch.t.sol +++ b/test/unit/Dispatch.t.sol @@ -61,7 +61,7 @@ contract DispatchTest is Test, EntityRegistry { payload: "", contentType: encodeMime128("text/plain"), attributes: attrs, - expiresAt: BlockNumber.wrap(0), + btl: BlockNumber.wrap(0), newOwner: address(0) }); } diff --git a/test/unit/Execute.t.sol b/test/unit/Execute.t.sol index afdc4d4..ad41ffc 100644 --- a/test/unit/Execute.t.sol +++ b/test/unit/Execute.t.sol @@ -46,7 +46,7 @@ contract ExecuteTest is Test, EntityRegistry { payload: "", contentType: encodeMime128("text/plain"), attributes: attrs, - expiresAt: BlockNumber.wrap(0), + btl: BlockNumber.wrap(0), newOwner: address(0) }); } diff --git a/test/unit/Views.t.sol b/test/unit/Views.t.sol index 8bbf8bf..917b30c 100644 --- a/test/unit/Views.t.sol +++ b/test/unit/Views.t.sol @@ -16,12 +16,12 @@ contract ViewsTest is Test { address alice = makeAddr("alice"); bytes32 testKey; BlockNumber deployBlock; - BlockNumber expiresAt; + BlockNumber btl; function setUp() public { registry = new EntityRegistry(); deployBlock = registry.genesisBlock(); - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation[] memory ops = new Entity.Operation[](1); @@ -31,7 +31,7 @@ contract ViewsTest is Test { payload: "hello", contentType: encodeMime128("text/plain"), attributes: attrs, - expiresAt: expiresAt, + btl: btl, newOwner: address(0) }); @@ -128,7 +128,7 @@ contract ViewsTest is Test { assertEq(c.creator, alice); assertEq(BlockNumber.unwrap(c.createdAt), BlockNumber.unwrap(deployBlock)); assertEq(BlockNumber.unwrap(c.updatedAt), BlockNumber.unwrap(deployBlock)); - assertEq(BlockNumber.unwrap(c.expiresAt), BlockNumber.unwrap(expiresAt)); + assertEq(BlockNumber.unwrap(c.expiresAt), BlockNumber.unwrap(deployBlock + btl)); assertTrue(c.coreHash != bytes32(0)); } diff --git a/test/unit/guards/RequireFutureExpiry.t.sol b/test/unit/guards/RequireFutureExpiry.t.sol deleted file mode 100644 index b1a0425..0000000 --- a/test/unit/guards/RequireFutureExpiry.t.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; -import {Test} from "forge-std/Test.sol"; -import {Entity} from "../../../contracts/Entity.sol"; -import {EntityRegistry} from "../../../contracts/EntityRegistry.sol"; - -contract RequireFutureExpiryTest is Test, EntityRegistry { - BlockNumber constant CURRENT = BlockNumber.wrap(1000); - - function doRequireFutureExpiry(BlockNumber expiresAt, BlockNumber current) external pure { - Entity.requireFutureExpiry(expiresAt, current); - } - - function test_futureExpiry_succeeds() public view { - this.doRequireFutureExpiry(BlockNumber.wrap(1001), CURRENT); - } - - function test_farFutureExpiry_succeeds() public view { - this.doRequireFutureExpiry(BlockNumber.wrap(999999), CURRENT); - } - - function test_equalToCurrentBlock_reverts() public { - vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryInPast.selector, CURRENT, CURRENT)); - this.doRequireFutureExpiry(CURRENT, CURRENT); - } - - function test_beforeCurrentBlock_reverts() public { - BlockNumber past = BlockNumber.wrap(500); - vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryInPast.selector, past, CURRENT)); - this.doRequireFutureExpiry(past, CURRENT); - } -} diff --git a/test/unit/guards/RequirePositiveBtl.t.sol b/test/unit/guards/RequirePositiveBtl.t.sol new file mode 100644 index 0000000..e060397 --- /dev/null +++ b/test/unit/guards/RequirePositiveBtl.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {Test} from "forge-std/Test.sol"; +import {Entity} from "../../../contracts/Entity.sol"; +import {EntityRegistry} from "../../../contracts/EntityRegistry.sol"; + +contract RequirePositiveBtlTest is Test, EntityRegistry { + function doRequirePositiveBtl(BlockNumber btl) external pure { + Entity.requirePositiveBtl(btl); + } + + function test_positiveBtl_succeeds() public view { + this.doRequirePositiveBtl(BlockNumber.wrap(1)); + } + + function test_largeBtl_succeeds() public view { + this.doRequirePositiveBtl(BlockNumber.wrap(999999)); + } + + function test_zeroBtl_reverts() public { + vm.expectRevert(Entity.ZeroBtl.selector); + this.doRequirePositiveBtl(BlockNumber.wrap(0)); + } +} diff --git a/test/unit/ops/Create.t.sol b/test/unit/ops/Create.t.sol index 61c6216..0f92771 100644 --- a/test/unit/ops/Create.t.sol +++ b/test/unit/ops/Create.t.sol @@ -14,7 +14,7 @@ contract CreateTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber expiresAt; + BlockNumber btl; bytes32 constant STUB_KEY = keccak256("stub-entity-key"); bytes32 constant STUB_CORE_HASH = keccak256("stub-core-hash"); @@ -41,53 +41,30 @@ contract CreateTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); } function _defaultOp() internal view returns (Entity.Operation memory) { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - return Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + return Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); } // ========================================================================= // Validation — expiry // ========================================================================= - function test_create_expiryEqualToCurrentBlock_reverts() public { + function test_create_zeroBtl_reverts() public { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = - Lib.createOp("hello", encodeMime128("text/plain"), attrs, BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, BlockNumber.wrap(0)); vm.prank(alice); - vm.expectRevert( - abi.encodeWithSelector( - Entity.ExpiryInPast.selector, - BlockNumber.wrap(uint32(block.number)), - BlockNumber.wrap(uint32(block.number)) - ) - ); + vm.expectRevert(Entity.ZeroBtl.selector); this.doCreate(op); } - function test_create_expiryInPast_reverts() public { - vm.roll(block.number + 100); - - BlockNumber pastBlock = BlockNumber.wrap(1); - Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, pastBlock); - - vm.prank(alice); - vm.expectRevert( - abi.encodeWithSelector(Entity.ExpiryInPast.selector, pastBlock, BlockNumber.wrap(uint32(block.number))) - ); - this.doCreate(op); - } - - function test_create_expiryOneBlockAhead_succeeds() public { + function test_create_btlOne_succeeds() public { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = Lib.createOp( - "hello", encodeMime128("text/plain"), attrs, BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1) - ); + Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, BlockNumber.wrap(1)); vm.prank(alice); (bytes32 key,) = this.doCreate(op); @@ -109,7 +86,7 @@ contract CreateTest is Test, EntityRegistry { assertEq(c.owner, alice); assertEq(BlockNumber.unwrap(c.createdAt), uint32(block.number)); assertEq(BlockNumber.unwrap(c.updatedAt), uint32(block.number)); - assertEq(BlockNumber.unwrap(c.expiresAt), BlockNumber.unwrap(expiresAt)); + assertEq(BlockNumber.unwrap(c.expiresAt), uint32(block.number) + BlockNumber.unwrap(btl)); assertEq(c.coreHash, STUB_CORE_HASH); } @@ -131,7 +108,7 @@ contract CreateTest is Test, EntityRegistry { assertEq(logs[0].topics[2], bytes32(uint256(Entity.CREATE))); assertEq(logs[0].topics[3], bytes32(uint256(uint160(alice)))); (BlockNumber emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber, bytes32)); - assertEq(BlockNumber.unwrap(emittedExpiry), BlockNumber.unwrap(expiresAt)); + assertEq(BlockNumber.unwrap(emittedExpiry), uint32(block.number) + BlockNumber.unwrap(btl)); assertEq(emittedHash, STUB_ENTITY_HASH); } diff --git a/test/utils/Lib.sol b/test/utils/Lib.sol index d9fce74..b7ad6d4 100644 --- a/test/utils/Lib.sol +++ b/test/utils/Lib.sol @@ -16,7 +16,7 @@ library Lib { bytes memory payload_, Mime128 memory contentType_, Entity.Attribute[] memory attributes_, - BlockNumber expiresAt_ + BlockNumber btl_ ) internal pure returns (Entity.Operation memory) { return Entity.Operation({ operationType: Entity.CREATE, @@ -24,7 +24,7 @@ library Lib { payload: payload_, contentType: contentType_, attributes: attributes_, - expiresAt: expiresAt_, + btl: btl_, newOwner: address(0) }); } @@ -41,7 +41,7 @@ library Lib { payload: payload_, contentType: contentType_, attributes: attributes_, - expiresAt: BlockNumber.wrap(0), + btl: BlockNumber.wrap(0), newOwner: address(0) }); } @@ -55,7 +55,7 @@ library Lib { payload: "", contentType: emptyCt, attributes: empty, - expiresAt: BlockNumber.wrap(0), + btl: BlockNumber.wrap(0), newOwner: address(0) }); } @@ -69,7 +69,7 @@ library Lib { payload: "", contentType: emptyCt, attributes: empty, - expiresAt: BlockNumber.wrap(0), + btl: BlockNumber.wrap(0), newOwner: newOwner_ }); } @@ -83,12 +83,12 @@ library Lib { payload: "", contentType: emptyCt, attributes: empty, - expiresAt: BlockNumber.wrap(0), + btl: BlockNumber.wrap(0), newOwner: address(0) }); } - function extendOp(bytes32 entityKey_, BlockNumber expiresAt_) internal pure returns (Entity.Operation memory) { + function extendOp(bytes32 entityKey_, BlockNumber btl_) internal pure returns (Entity.Operation memory) { Entity.Attribute[] memory empty = new Entity.Attribute[](0); Mime128 memory emptyCt; return Entity.Operation({ @@ -97,7 +97,7 @@ library Lib { payload: "", contentType: emptyCt, attributes: empty, - expiresAt: expiresAt_, + btl: btl_, newOwner: address(0) }); } From 3314752c5ff28571ac8d9f0e245fb447b0091d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:07:52 +0100 Subject: [PATCH 03/20] Fix guards --- test/unit/guards/RequireActive.t.sol | 6 ++++-- test/unit/guards/RequireExpired.t.sol | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/unit/guards/RequireActive.t.sol b/test/unit/guards/RequireActive.t.sol index 3e4b1ab..ba3d1cd 100644 --- a/test/unit/guards/RequireActive.t.sol +++ b/test/unit/guards/RequireActive.t.sol @@ -11,6 +11,7 @@ import {encodeMime128} from "../../../contracts/types/Mime128.sol"; contract RequireActiveTest is Test, EntityRegistry { address alice = makeAddr("alice"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -24,10 +25,11 @@ contract RequireActiveTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(op); } diff --git a/test/unit/guards/RequireExpired.t.sol b/test/unit/guards/RequireExpired.t.sol index e908af5..68d858c 100644 --- a/test/unit/guards/RequireExpired.t.sol +++ b/test/unit/guards/RequireExpired.t.sol @@ -12,6 +12,7 @@ contract RequireExpiredTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -25,10 +26,11 @@ contract RequireExpiredTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(op); } From 999ca4a1a41f230a937b3afa4833204e0105084b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:08:42 +0100 Subject: [PATCH 04/20] Fix delete --- test/unit/ops/Delete.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/ops/Delete.t.sol b/test/unit/ops/Delete.t.sol index aad1124..07bd523 100644 --- a/test/unit/ops/Delete.t.sol +++ b/test/unit/ops/Delete.t.sol @@ -12,6 +12,7 @@ contract DeleteTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -24,10 +25,11 @@ contract DeleteTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(createOp); } From 67d5c06b32cd860d434f5abd4d4d32b7bd439ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:09:26 +0100 Subject: [PATCH 05/20] Fix expire --- test/unit/ops/Expire.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/ops/Expire.t.sol b/test/unit/ops/Expire.t.sol index 82c8faa..5c31cad 100644 --- a/test/unit/ops/Expire.t.sol +++ b/test/unit/ops/Expire.t.sol @@ -12,6 +12,7 @@ contract ExpireTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -24,10 +25,11 @@ contract ExpireTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(createOp); } From c0fb51bfbfea21bbf4cb5d7c8b69a2cb50926546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:12:43 +0100 Subject: [PATCH 06/20] Fix extend --- test/unit/ops/Extend.t.sol | 50 ++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/test/unit/ops/Extend.t.sol b/test/unit/ops/Extend.t.sol index d9604db..c4c4aae 100644 --- a/test/unit/ops/Extend.t.sol +++ b/test/unit/ops/Extend.t.sol @@ -12,6 +12,7 @@ contract ExtendTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -24,10 +25,11 @@ contract ExtendTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(createOp); } @@ -37,7 +39,9 @@ contract ExtendTest is Test, EntityRegistry { // ========================================================================= function test_extend_sameExpiry_reverts() public { - Entity.Operation memory op = Lib.extendOp(testKey, expiresAt); + // btl that lands on the already-stored expiresAt: expiresAt - current + Entity.Operation memory op = + Lib.extendOp(testKey, expiresAt - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryNotExtended.selector, testKey, expiresAt, expiresAt)); @@ -45,8 +49,10 @@ contract ExtendTest is Test, EntityRegistry { } function test_extend_lowerExpiry_reverts() public { + // absolute target lower than current stored expiresAt BlockNumber lower = expiresAt - BlockNumber.wrap(100); - Entity.Operation memory op = Lib.extendOp(testKey, lower); + Entity.Operation memory op = + Lib.extendOp(testKey, lower - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryNotExtended.selector, testKey, lower, expiresAt)); @@ -59,7 +65,8 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_updatesExpiresAt() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry); + Entity.Operation memory op = + Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); @@ -72,7 +79,8 @@ contract ExtendTest is Test, EntityRegistry { vm.roll(block.number + 10); BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry); + Entity.Operation memory op = + Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); @@ -85,7 +93,8 @@ contract ExtendTest is Test, EntityRegistry { Entity.Commitment memory before_ = commitment(testKey); BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry); + Entity.Operation memory op = + Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); @@ -103,7 +112,8 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_returnsEntityKey() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry); + Entity.Operation memory op = + Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); (bytes32 returnedKey,) = this.doExtend(op); @@ -117,7 +127,8 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_entityHashUsesNewExpiry() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry); + Entity.Operation memory op = + Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 entityHash_) = this.doExtend(op); @@ -131,11 +142,13 @@ contract ExtendTest is Test, EntityRegistry { BlockNumber expiry1 = expiresAt + BlockNumber.wrap(100); BlockNumber expiry2 = expiresAt + BlockNumber.wrap(200); - Entity.Operation memory op1 = Lib.extendOp(testKey, expiry1); + Entity.Operation memory op1 = + Lib.extendOp(testKey, expiry1 - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 hash1) = this.doExtend(op1); - Entity.Operation memory op2 = Lib.extendOp(testKey, expiry2); + Entity.Operation memory op2 = + Lib.extendOp(testKey, expiry2 - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 hash2) = this.doExtend(op2); @@ -148,7 +161,8 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_emitsEntityOperation() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry); + Entity.Operation memory op = + Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.recordLogs(); @@ -170,7 +184,9 @@ contract ExtendTest is Test, EntityRegistry { // ========================================================================= function test_extend_revertsIfNotFound() public { - Entity.Operation memory op = Lib.extendOp(keccak256("bogus"), expiresAt + BlockNumber.wrap(500)); + BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); + Entity.Operation memory op = + Lib.extendOp(keccak256("bogus"), newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotFound.selector, keccak256("bogus"))); this.doExtend(op); @@ -178,14 +194,18 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_revertsIfExpired() public { vm.roll(BlockNumber.unwrap(expiresAt)); - Entity.Operation memory op = Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500)); + BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); + Entity.Operation memory op = + Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); this.doExtend(op); } function test_extend_revertsIfNotOwner() public { - Entity.Operation memory op = Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500)); + BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); + Entity.Operation memory op = + Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(bob); vm.expectRevert(abi.encodeWithSelector(Entity.NotOwner.selector, testKey, bob, alice)); this.doExtend(op); From 5694ba8787bf5b79c909b5aba4241d4f9289cbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:13:30 +0100 Subject: [PATCH 07/20] Fix transfer --- test/unit/ops/Transfer.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/ops/Transfer.t.sol b/test/unit/ops/Transfer.t.sol index 4be7fb2..58278c7 100644 --- a/test/unit/ops/Transfer.t.sol +++ b/test/unit/ops/Transfer.t.sol @@ -13,6 +13,7 @@ contract TransferTest is Test, EntityRegistry { address bob = makeAddr("bob"); address charlie = makeAddr("charlie"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -25,10 +26,11 @@ contract TransferTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(createOp); } From b1bdcd174561946bc8194efab8756c8683bf22c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:14:03 +0100 Subject: [PATCH 08/20] Fix update --- test/unit/ops/Update.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/ops/Update.t.sol b/test/unit/ops/Update.t.sol index 658845d..e5f59d8 100644 --- a/test/unit/ops/Update.t.sol +++ b/test/unit/ops/Update.t.sol @@ -12,6 +12,7 @@ contract UpdateTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -42,11 +43,12 @@ contract UpdateTest is Test, EntityRegistry { textPlain = encodeMime128("text/plain"); appJson = encodeMime128("application/json"); - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; // Create an entity owned by alice. Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("hello", textPlain, attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("hello", textPlain, attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(createOp); } From 9126540b49f7935e8015c621da5c79044a63aec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:15:14 +0100 Subject: [PATCH 09/20] Fix expiryLifecycle --- test/integration/ExpiryLifecycle.t.sol | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/integration/ExpiryLifecycle.t.sol b/test/integration/ExpiryLifecycle.t.sol index 5996b70..8a162f8 100644 --- a/test/integration/ExpiryLifecycle.t.sol +++ b/test/integration/ExpiryLifecycle.t.sol @@ -14,6 +14,7 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -38,10 +39,11 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(100); + btl = BlockNumber.wrap(100); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(createOp); } @@ -53,12 +55,12 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { function test_extendMultipleTimes() public { BlockNumber expiry1 = expiresAt + BlockNumber.wrap(100); vm.prank(alice); - this.doExtend(Lib.extendOp(testKey, expiry1)); + this.doExtend(Lib.extendOp(testKey, expiry1 - BlockNumber.wrap(uint32(block.number)))); assertEq(BlockNumber.unwrap(commitment(testKey).expiresAt), BlockNumber.unwrap(expiry1)); BlockNumber expiry2 = expiry1 + BlockNumber.wrap(100); vm.prank(alice); - this.doExtend(Lib.extendOp(testKey, expiry2)); + this.doExtend(Lib.extendOp(testKey, expiry2 - BlockNumber.wrap(uint32(block.number)))); assertEq(BlockNumber.unwrap(commitment(testKey).expiresAt), BlockNumber.unwrap(expiry2)); } @@ -79,9 +81,10 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { function test_expiredEntityCannotBeExtended() public { vm.roll(BlockNumber.unwrap(expiresAt)); + BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); - this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500))); + this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number)))); } function test_expiredEntityCannotBeDeleted() public { @@ -111,7 +114,7 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { function test_extendThenOperateAfterOriginalExpiry() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); vm.prank(alice); - this.doExtend(Lib.extendOp(testKey, newExpiry)); + this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number)))); // Roll past original expiry but before new expiry. vm.roll(BlockNumber.unwrap(expiresAt) + 1); @@ -132,7 +135,7 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { // Extend. BlockNumber newExpiry = expiresAt + BlockNumber.wrap(200); vm.prank(alice); - this.doExtend(Lib.extendOp(testKey, newExpiry)); + this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number)))); // Roll to new expiry. vm.roll(BlockNumber.unwrap(newExpiry)); From 4fdc8f2665736d521a0a3b2539f927437962b1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:16:53 +0100 Subject: [PATCH 10/20] Fix operationSequencing --- test/integration/OperationSequencing.t.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/integration/OperationSequencing.t.sol b/test/integration/OperationSequencing.t.sol index a0ecdd6..968a4f6 100644 --- a/test/integration/OperationSequencing.t.sol +++ b/test/integration/OperationSequencing.t.sol @@ -13,6 +13,7 @@ import {encodeMime128} from "../../contracts/types/Mime128.sol"; contract OperationSequencingTest is Test, EntityRegistry { address alice = makeAddr("alice"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -41,10 +42,11 @@ contract OperationSequencingTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(createOp); } @@ -59,7 +61,7 @@ contract OperationSequencingTest is Test, EntityRegistry { vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotFound.selector, testKey)); - this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500))); + this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500) - BlockNumber.wrap(uint32(block.number)))); } function test_deleteThenUpdate_reverts() public { @@ -113,7 +115,7 @@ contract OperationSequencingTest is Test, EntityRegistry { vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotFound.selector, testKey)); - this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500))); + this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500) - BlockNumber.wrap(uint32(block.number)))); } function test_expireThenExpire_reverts() public { @@ -131,7 +133,7 @@ contract OperationSequencingTest is Test, EntityRegistry { function test_deleteInSameBlockAsCreate_succeeds() public { // Create a fresh entity (same block as setUp's create). Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("ephemeral", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("ephemeral", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (bytes32 key,) = this.doCreate(createOp); From 4756adc2ebd4ab326cb1f71c54f8e80ac91cf746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:17:41 +0100 Subject: [PATCH 11/20] Fix ownershipLifecycle --- test/integration/OwnershipLifecycle.t.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/integration/OwnershipLifecycle.t.sol b/test/integration/OwnershipLifecycle.t.sol index 1ac3b1a..45dfbfd 100644 --- a/test/integration/OwnershipLifecycle.t.sol +++ b/test/integration/OwnershipLifecycle.t.sol @@ -15,6 +15,7 @@ contract OwnershipLifecycleTest is Test, EntityRegistry { address bob = makeAddr("bob"); address charlie = makeAddr("charlie"); + BlockNumber btl; BlockNumber expiresAt; bytes32 testKey; @@ -39,10 +40,11 @@ contract OwnershipLifecycleTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); + Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); vm.prank(alice); (testKey,) = this.doCreate(createOp); } @@ -84,7 +86,7 @@ contract OwnershipLifecycleTest is Test, EntityRegistry { vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.NotOwner.selector, testKey, alice, bob)); - this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500))); + this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500) - BlockNumber.wrap(uint32(block.number)))); } function test_previousOwnerCannotDelete() public { @@ -126,7 +128,7 @@ contract OwnershipLifecycleTest is Test, EntityRegistry { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); vm.prank(bob); - this.doExtend(Lib.extendOp(testKey, newExpiry)); + this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number)))); assertEq(BlockNumber.unwrap(commitment(testKey).expiresAt), BlockNumber.unwrap(newExpiry)); } From 5fbf2939a6d75b894a8e308819f4b867e77327ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:18:58 +0100 Subject: [PATCH 12/20] Fix entityLifecycle --- test/e2e/EntityLifecycle.t.sol | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/e2e/EntityLifecycle.t.sol b/test/e2e/EntityLifecycle.t.sol index cb42435..4e41fbf 100644 --- a/test/e2e/EntityLifecycle.t.sol +++ b/test/e2e/EntityLifecycle.t.sol @@ -18,6 +18,7 @@ contract EntityLifecycleTest is Test { address bob; Mime128 textPlain; + BlockNumber btl; BlockNumber expiresAt; function setUp() public { @@ -25,7 +26,8 @@ contract EntityLifecycleTest is Test { alice = makeAddr("alice"); bob = makeAddr("bob"); textPlain = encodeMime128("text/plain"); - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + btl = BlockNumber.wrap(1000); + expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; } /// @dev Helper — build a single-op array and execute as sender. @@ -44,7 +46,7 @@ contract EntityLifecycleTest is Test { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); // Create. - _exec(alice, Lib.createOp("v1", textPlain, attrs, expiresAt)); + _exec(alice, Lib.createOp("v1", textPlain, attrs, btl)); bytes32 key = registry.entityKey(alice, 0); Entity.Commitment memory c = registry.commitment(key); @@ -64,7 +66,7 @@ contract EntityLifecycleTest is Test { // Extend. BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - _exec(alice, Lib.extendOp(key, newExpiry)); + _exec(alice, Lib.extendOp(key, newExpiry - BlockNumber.wrap(uint32(block.number)))); assertEq(BlockNumber.unwrap(registry.commitment(key).expiresAt), BlockNumber.unwrap(newExpiry)); bytes32 hashAfterExtend = registry.changeSetHash(); assertNotEq(hashAfterExtend, hashAfterUpdate); @@ -92,8 +94,8 @@ contract EntityLifecycleTest is Test { // We can't reference the key before it's created, but we can create // two entities in one batch. Entity.Operation[] memory ops = new Entity.Operation[](2); - ops[0] = Lib.createOp("first", textPlain, attrs, expiresAt); - ops[1] = Lib.createOp("second", textPlain, attrs, expiresAt); + ops[0] = Lib.createOp("first", textPlain, attrs, btl); + ops[1] = Lib.createOp("second", textPlain, attrs, btl); vm.prank(alice); registry.execute(ops); @@ -129,7 +131,7 @@ contract EntityLifecycleTest is Test { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); // Block 1: create. - _exec(alice, Lib.createOp("hello", textPlain, attrs, expiresAt)); + _exec(alice, Lib.createOp("hello", textPlain, attrs, btl)); bytes32 key = registry.entityKey(alice, 0); BlockNumber block1 = registry.headBlock(); bytes32 hashBlock1 = registry.changeSetHash(); @@ -165,7 +167,7 @@ contract EntityLifecycleTest is Test { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); // Create. - _exec(alice, Lib.createOp("ephemeral", textPlain, attrs, expiresAt)); + _exec(alice, Lib.createOp("ephemeral", textPlain, attrs, btl)); bytes32 key = registry.entityKey(alice, 0); assertTrue(registry.commitment(key).creator != address(0)); @@ -184,8 +186,8 @@ contract EntityLifecycleTest is Test { function test_multipleOwners_independentEntities() public { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - _exec(alice, Lib.createOp("alice-doc", textPlain, attrs, expiresAt)); - _exec(bob, Lib.createOp("bob-doc", textPlain, attrs, expiresAt)); + _exec(alice, Lib.createOp("alice-doc", textPlain, attrs, btl)); + _exec(bob, Lib.createOp("bob-doc", textPlain, attrs, btl)); bytes32 aliceKey = registry.entityKey(alice, 0); bytes32 bobKey = registry.entityKey(bob, 0); From f9cd5f03051859f447b62dc18e2347bcef5595f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:25:51 +0100 Subject: [PATCH 13/20] formatting --- test/unit/ops/Extend.t.sol | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/test/unit/ops/Extend.t.sol b/test/unit/ops/Extend.t.sol index c4c4aae..e98d262 100644 --- a/test/unit/ops/Extend.t.sol +++ b/test/unit/ops/Extend.t.sol @@ -40,8 +40,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_sameExpiry_reverts() public { // btl that lands on the already-stored expiresAt: expiresAt - current - Entity.Operation memory op = - Lib.extendOp(testKey, expiresAt - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, expiresAt - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryNotExtended.selector, testKey, expiresAt, expiresAt)); @@ -51,8 +50,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_lowerExpiry_reverts() public { // absolute target lower than current stored expiresAt BlockNumber lower = expiresAt - BlockNumber.wrap(100); - Entity.Operation memory op = - Lib.extendOp(testKey, lower - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, lower - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryNotExtended.selector, testKey, lower, expiresAt)); @@ -65,8 +63,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_updatesExpiresAt() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = - Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); @@ -79,8 +76,7 @@ contract ExtendTest is Test, EntityRegistry { vm.roll(block.number + 10); BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = - Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); @@ -93,8 +89,7 @@ contract ExtendTest is Test, EntityRegistry { Entity.Commitment memory before_ = commitment(testKey); BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = - Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); @@ -112,8 +107,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_returnsEntityKey() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = - Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); (bytes32 returnedKey,) = this.doExtend(op); @@ -127,8 +121,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_entityHashUsesNewExpiry() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = - Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 entityHash_) = this.doExtend(op); @@ -142,13 +135,11 @@ contract ExtendTest is Test, EntityRegistry { BlockNumber expiry1 = expiresAt + BlockNumber.wrap(100); BlockNumber expiry2 = expiresAt + BlockNumber.wrap(200); - Entity.Operation memory op1 = - Lib.extendOp(testKey, expiry1 - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op1 = Lib.extendOp(testKey, expiry1 - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 hash1) = this.doExtend(op1); - Entity.Operation memory op2 = - Lib.extendOp(testKey, expiry2 - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op2 = Lib.extendOp(testKey, expiry2 - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 hash2) = this.doExtend(op2); @@ -161,8 +152,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_emitsEntityOperation() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = - Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.recordLogs(); @@ -195,8 +185,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_revertsIfExpired() public { vm.roll(BlockNumber.unwrap(expiresAt)); BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = - Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); this.doExtend(op); @@ -204,8 +193,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_revertsIfNotOwner() public { BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = - Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); vm.prank(bob); vm.expectRevert(abi.encodeWithSelector(Entity.NotOwner.selector, testKey, bob, alice)); this.doExtend(op); From 090512ed5338b2307c7dc7c2ab343588dff821b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:29:17 +0100 Subject: [PATCH 14/20] update bindings --- src/lib.rs | 4 ++-- src/wire.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8324a89..a8c2f1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,7 +67,7 @@ mod tests { data: [FixedBytes::ZERO; 4], }, attributes: vec![], - expiresAt: 1000, + btl: 1000, newOwner: Address::ZERO, }; @@ -100,7 +100,7 @@ mod tests { data: [FixedBytes::ZERO; 4], }, attributes: vec![attr.clone()], - expiresAt: 500, + btl: 500, newOwner: Address::ZERO, }; diff --git a/src/wire.rs b/src/wire.rs index c5f33d3..a675695 100644 --- a/src/wire.rs +++ b/src/wire.rs @@ -175,6 +175,11 @@ pub enum Attribute { /// - `entity_event`: the `EntityOperation` event emitted for this op /// - `hash_event`: the `ChangeSetHashUpdate` event emitted for this op /// +/// The `expires_at` field on `CreateOp` and `ExtendOp` is sourced from +/// `entity_event.expiresAt` — the absolute block number computed by the +/// contract as `currentBlock + op.btl` and emitted on-chain. The raw `btl` +/// from calldata is not exposed in the wire format. +/// /// Errors: /// - `entity_event.operationType` doesn't match `calldata.operationType` /// - Unknown operation type @@ -198,7 +203,7 @@ pub fn decode_operation( let owner = entity_event.owner; let entity_hash = entity_event.entityHash; let changeset_hash = hash_event.changeSetHash; - let expires_at = u64::from(calldata.expiresAt); + let expires_at = u64::from(entity_event.expiresAt); Ok(match calldata.operationType { OP_CREATE => Operation::Create(CreateOp { @@ -394,7 +399,7 @@ mod tests { payload: Bytes::from_static(b"hello"), contentType: mime128("text/plain"), attributes: vec![], - expiresAt: 1234, + btl: 1234, newOwner: Address::ZERO, } } From 66b9be497877306d1230d61c7cbc70bd5c904c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 11:35:20 +0100 Subject: [PATCH 15/20] update IEntityRegistry --- contracts/IEntityRegistry.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/IEntityRegistry.sol b/contracts/IEntityRegistry.sol index 6c4ef62..b470069 100644 --- a/contracts/IEntityRegistry.sol +++ b/contracts/IEntityRegistry.sol @@ -28,7 +28,7 @@ interface IEntityRegistry { error AttributesNotSorted(); error InvalidValueType(Ident32 name, uint8 valueType); error InvalidOpType(uint8 operationType); - error ExpiryInPast(BlockNumber expiresAt, BlockNumber currentBlock); + error ZeroBtl(); error TooManyAttributes(uint256 count, uint256 maxCount); error EntityNotFound(bytes32 entityKey); error NotOwner(bytes32 entityKey, address caller, address owner); From 3456f80a00e18d68e3deb45decc292277671de47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 12:01:52 +0100 Subject: [PATCH 16/20] Rename BlockNumber -> BlockNumber32 --- contracts/Entity.sol | 43 ++++----- contracts/EntityRegistry.sol | 74 +++++++-------- contracts/IEntityRegistry.sol | 24 ++--- contracts/types/BlockNumber.sol | 60 ------------- contracts/types/BlockNumber32.sol | 60 +++++++++++++ src/storage_layout.rs | 8 +- test/e2e/EntityLifecycle.t.sol | 32 +++---- test/integration/ExpiryLifecycle.t.sol | 56 ++++++------ test/integration/OperationSequencing.t.sol | 36 ++++---- test/integration/OwnershipLifecycle.t.sol | 30 ++++--- test/unit/ComputeEntityHash.t.sol | 45 +++++----- test/unit/Dispatch.t.sol | 18 ++-- test/unit/Execute.t.sol | 90 +++++++++---------- test/unit/Views.t.sol | 36 ++++---- test/unit/WrapEntityHash.t.sol | 36 ++++---- test/unit/guards/RequireActive.t.sol | 24 ++--- test/unit/guards/RequireExists.t.sol | 8 +- test/unit/guards/RequireExpired.t.sol | 32 +++---- test/unit/guards/RequireExpiryIncreased.t.sol | 12 +-- test/unit/guards/RequireOwner.t.sol | 8 +- test/unit/guards/RequirePositiveBtl.t.sol | 10 +-- test/unit/hashing/CoreHash.t.sol | 50 ++++++----- test/unit/hashing/EntityStructHash.t.sol | 40 +++++---- test/unit/hashing/Pack.t.sol | 44 ++++----- test/unit/ops/Create.t.sol | 28 +++--- test/unit/ops/Delete.t.sol | 26 +++--- test/unit/ops/Expire.t.sol | 34 +++---- test/unit/ops/Extend.t.sol | 76 ++++++++-------- test/unit/ops/Transfer.t.sol | 26 +++--- test/unit/ops/Update.t.sol | 28 +++--- test/utils/Lib.sol | 14 +-- 31 files changed, 564 insertions(+), 544 deletions(-) delete mode 100644 contracts/types/BlockNumber.sol create mode 100644 contracts/types/BlockNumber32.sol diff --git a/contracts/Entity.sol b/contracts/Entity.sol index 07e3d91..0fd2a81 100644 --- a/contracts/Entity.sol +++ b/contracts/Entity.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "./types/BlockNumber.sol"; +import {BlockNumber32} from "./types/BlockNumber32.sol"; import {Ident32, validateIdent32} from "./types/Ident32.sol"; import {Mime128} from "./types/Mime128.sol"; @@ -55,7 +55,7 @@ library Entity { bytes payload; Mime128 contentType; Attribute[] attributes; - BlockNumber btl; + BlockNumber32 btl; address newOwner; } @@ -87,9 +87,9 @@ library Entity { /// slot 2: coreHash (32) struct Commitment { address creator; - BlockNumber createdAt; - BlockNumber updatedAt; - BlockNumber expiresAt; + BlockNumber32 createdAt; + BlockNumber32 updatedAt; + BlockNumber32 expiresAt; address owner; bytes32 coreHash; } @@ -98,8 +98,8 @@ library Entity { /// Only blocks containing at least one mutation have an entry. /// All fields pack into a single slot (12 bytes). struct BlockNode { - BlockNumber prevBlock; - BlockNumber nextBlock; + BlockNumber32 prevBlock; + BlockNumber32 nextBlock; uint32 txCount; } @@ -124,15 +124,15 @@ library Entity { /// @dev Reverted when the caller is not the entity owner. error NotOwner(bytes32 entityKey, address caller, address owner); /// @dev Reverted when an operation targets an expired entity. - error EntityExpired(bytes32 entityKey, BlockNumber expiresAt); + error EntityExpired(bytes32 entityKey, BlockNumber32 expiresAt); /// @dev Reverted when new expiresAt is not strictly greater than current. - error ExpiryNotExtended(bytes32 entityKey, BlockNumber newExpiresAt, BlockNumber currentExpiresAt); + error ExpiryNotExtended(bytes32 entityKey, BlockNumber32 newExpiresAt, BlockNumber32 currentExpiresAt); /// @dev Reverted when transfer target is the zero address. error TransferToZeroAddress(bytes32 entityKey); /// @dev Reverted when transfer target is the current owner (no-op). error TransferToSelf(bytes32 entityKey); /// @dev Reverted when expire is called on an entity that hasn't expired yet. - error EntityNotExpired(bytes32 entityKey, BlockNumber expiresAt); + error EntityNotExpired(bytes32 entityKey, BlockNumber32 expiresAt); // ------------------------------------------------------------------------- // Constants @@ -167,12 +167,12 @@ library Entity { } /// @dev Require that the entity has not expired (expiresAt > current). - function requireActive(bytes32 key, Commitment storage c, BlockNumber current) internal view { + function requireActive(bytes32 key, Commitment storage c, BlockNumber32 current) internal view { if (c.expiresAt <= current) revert EntityExpired(key, c.expiresAt); } /// @dev Require that the entity has expired (expiresAt <= current). - function requireExpired(bytes32 key, Commitment storage c, BlockNumber current) internal view { + function requireExpired(bytes32 key, Commitment storage c, BlockNumber32 current) internal view { if (c.expiresAt > current) revert EntityNotExpired(key, c.expiresAt); } @@ -192,13 +192,16 @@ library Entity { } /// @dev Require that the new expiry is strictly greater than the current one. - function requireExpiryIncreased(bytes32 key, BlockNumber newExpiresAt, BlockNumber currentExpiresAt) internal pure { + function requireExpiryIncreased(bytes32 key, BlockNumber32 newExpiresAt, BlockNumber32 currentExpiresAt) + internal + pure + { if (newExpiresAt <= currentExpiresAt) revert ExpiryNotExtended(key, newExpiresAt, currentExpiresAt); } /// @dev Require that btl is non-zero (entity must live for at least one block). - function requirePositiveBtl(BlockNumber btl) internal pure { - if (btl == BlockNumber.wrap(0)) revert ZeroBtl(); + function requirePositiveBtl(BlockNumber32 btl) internal pure { + if (btl == BlockNumber32.wrap(0)) revert ZeroBtl(); } // ------------------------------------------------------------------------- @@ -234,7 +237,7 @@ library Entity { function coreHash( bytes32 key, address creator, - BlockNumber createdAt, + BlockNumber32 createdAt, Mime128 calldata contentType, bytes calldata payload, Attribute[] calldata attributes @@ -270,7 +273,7 @@ library Entity { /// @param updatedAt Block number of last update. /// @param expiresAt Expiry block number. /// @return The keccak256 EIP-712 struct hash (unwrapped). - function entityStructHash(bytes32 coreHash_, address owner, BlockNumber updatedAt, BlockNumber expiresAt) + function entityStructHash(bytes32 coreHash_, address owner, BlockNumber32 updatedAt, BlockNumber32 expiresAt) internal pure returns (bytes32) @@ -303,14 +306,14 @@ library Entity { /// @notice Pack a (block, tx) pair into a TransactionKey for the `_txOpCount` /// mapping. Layout: block in bits [32..95], tx in bits [0..31]. - function transactionKey(BlockNumber blockNumber, uint32 txSeq) internal pure returns (TransactionKey) { - return TransactionKey.wrap((uint256(BlockNumber.unwrap(blockNumber)) << 32) | txSeq); + function transactionKey(BlockNumber32 blockNumber, uint32 txSeq) internal pure returns (TransactionKey) { + return TransactionKey.wrap((uint256(BlockNumber32.unwrap(blockNumber)) << 32) | txSeq); } /// @notice Pack a (block, tx, op) triple into an OperationKey for the `_hashAt` /// mapping. Layout: block in bits [64..127], tx in bits [32..63], op in /// bits [0..31]. Extends transactionKey with the op dimension. - function operationKey(BlockNumber blockNumber, uint32 txSeq, uint32 opSeq) internal pure returns (OperationKey) { + function operationKey(BlockNumber32 blockNumber, uint32 txSeq, uint32 opSeq) internal pure returns (OperationKey) { return OperationKey.wrap((TransactionKey.unwrap(transactionKey(blockNumber, txSeq)) << 32) | opSeq); } } diff --git a/contracts/EntityRegistry.sol b/contracts/EntityRegistry.sol index 5e57d74..33c8137 100644 --- a/contracts/EntityRegistry.sol +++ b/contracts/EntityRegistry.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "./types/BlockNumber.sol"; +import {BlockNumber32} from "./types/BlockNumber32.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {Entity, OperationKey, TransactionKey} from "./Entity.sol"; import {validateMime128} from "./types/Mime128.sol"; @@ -12,7 +12,7 @@ import {validateMime128} from "./types/Mime128.sol"; /// and the changeset hash chain. contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { constructor() { - BlockNumber genesis = BlockNumber.wrap(uint32(block.number)); + BlockNumber32 genesis = BlockNumber32.wrap(uint32(block.number)); GENESIS_BLOCK = genesis; _headBlock = genesis; } @@ -50,7 +50,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { // Block-level linked list: only blocks with mutations have entries. // Enables O(1) traversal across sparse blocks. - mapping(BlockNumber blockNumber => Entity.BlockNode node) internal _blocks; + mapping(BlockNumber32 blockNumber => Entity.BlockNode node) internal _blocks; // ------------------------------------------------------------------------- // Events @@ -60,7 +60,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { bytes32 indexed entityKey, uint8 indexed operationType, address indexed owner, - BlockNumber expiresAt, + BlockNumber32 expiresAt, bytes32 entityHash ); @@ -70,8 +70,8 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { // State — block chain pointers // ------------------------------------------------------------------------- - BlockNumber internal immutable GENESIS_BLOCK; - BlockNumber internal _headBlock; + BlockNumber32 internal immutable GENESIS_BLOCK; + BlockNumber32 internal _headBlock; // ------------------------------------------------------------------------- // Public view functions @@ -99,7 +99,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { // Block bookkeeping: advance the linked list on a new block, // or continue the current block's tx sequence. - BlockNumber current = BlockNumber.wrap(uint32(block.number)); + BlockNumber32 current = BlockNumber32.wrap(uint32(block.number)); uint32 txSeq; if (current != _headBlock) { _blocks[_headBlock].nextBlock = current; @@ -141,37 +141,37 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// (the linked list skips empty blocks O(1) per hop). The `>= headBlock` /// fast path is O(1), so on-chain callers (e.g. `changeSetHash()`) are /// unaffected. - function changeSetHashAtBlock(BlockNumber blockNumber) public view returns (bytes32) { - BlockNumber head = _headBlock; - BlockNumber cursor = blockNumber >= head ? head : _findMutatedBlockAtOrBefore(blockNumber); + function changeSetHashAtBlock(BlockNumber32 blockNumber) public view returns (bytes32) { + BlockNumber32 head = _headBlock; + BlockNumber32 cursor = blockNumber >= head ? head : _findMutatedBlockAtOrBefore(blockNumber); return _hashAtMutatedBlock(cursor); } /// @notice Changeset hash after the last operation in the given transaction. - function changeSetHashAtTx(BlockNumber blockNumber, uint32 txSeq) public view returns (bytes32) { + function changeSetHashAtTx(BlockNumber32 blockNumber, uint32 txSeq) public view returns (bytes32) { uint32 opCount = _txOpCount[Entity.transactionKey(blockNumber, txSeq)]; if (opCount == 0) return bytes32(0); return _hashAt[Entity.operationKey(blockNumber, txSeq, opCount - 1)]; } /// @notice Changeset hash after a specific operation. - function changeSetHashAtOp(BlockNumber blockNumber, uint32 txSeq, uint32 opSeq) public view returns (bytes32) { + function changeSetHashAtOp(BlockNumber32 blockNumber, uint32 txSeq, uint32 opSeq) public view returns (bytes32) { return _hashAt[Entity.operationKey(blockNumber, txSeq, opSeq)]; } - function genesisBlock() public view returns (BlockNumber) { + function genesisBlock() public view returns (BlockNumber32) { return GENESIS_BLOCK; } - function headBlock() public view returns (BlockNumber) { + function headBlock() public view returns (BlockNumber32) { return _headBlock; } - function getBlockNode(BlockNumber blockNumber) public view returns (Entity.BlockNode memory) { + function getBlockNode(BlockNumber32 blockNumber) public view returns (Entity.BlockNode memory) { return _blocks[blockNumber]; } - function txOpCount(BlockNumber blockNumber, uint32 txSeq) public view returns (uint32) { + function txOpCount(BlockNumber32 blockNumber, uint32 txSeq) public view returns (uint32) { return _txOpCount[Entity.transactionKey(blockNumber, txSeq)]; } @@ -188,15 +188,15 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { // ------------------------------------------------------------------------- /// @dev Walk the mutated-blocks linked list backwards from `_headBlock` - /// to the largest stored block `<= target`. Returns `BlockNumber.wrap(0)` + /// to the largest stored block `<= target`. Returns `BlockNumber32.wrap(0)` /// if `target` predates every stored block (including genesis). /// /// The chain is strictly ascending: `genesis < first mutation < ... < head`. /// `genesis` may itself be unmutated (when the first `execute` happened /// after the deploy block); in that case the walk still lands on it for /// any `target >= genesis`, and `_hashAtMutatedBlock` returns `bytes32(0)`. - function _findMutatedBlockAtOrBefore(BlockNumber target) internal view returns (BlockNumber) { - BlockNumber cursor = _headBlock; + function _findMutatedBlockAtOrBefore(BlockNumber32 target) internal view returns (BlockNumber32) { + BlockNumber32 cursor = _headBlock; while (cursor > target) { cursor = _blocks[cursor].prevBlock; } @@ -206,7 +206,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// @dev Last-operation hash recorded in `blockNumber`, or `bytes32(0)` /// if the block has no mutations. Safe to call with the zero sentinel /// or an unmutated genesis — both yield `bytes32(0)`. - function _hashAtMutatedBlock(BlockNumber blockNumber) internal view returns (bytes32) { + function _hashAtMutatedBlock(BlockNumber32 blockNumber) internal view returns (bytes32) { uint32 txCount = _blocks[blockNumber].txCount; if (txCount == 0) return bytes32(0); uint32 lastTx = txCount - 1; @@ -220,7 +220,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// @dev Domain-wrap an entity struct hash. Used by both _computeEntityHash /// and operations that recompute entityHash without changing coreHash (extend, transfer). - function _wrapEntityHash(bytes32 coreHash_, address owner, BlockNumber updatedAt, BlockNumber expiresAt) + function _wrapEntityHash(bytes32 coreHash_, address owner, BlockNumber32 updatedAt, BlockNumber32 expiresAt) internal view virtual @@ -235,10 +235,10 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { function _computeEntityHash( bytes32 key, address creator, - BlockNumber createdAt, + BlockNumber32 createdAt, address owner, - BlockNumber updatedAt, - BlockNumber expiresAt, + BlockNumber32 updatedAt, + BlockNumber32 expiresAt, Entity.Operation calldata op ) internal view virtual returns (bytes32 coreHash_, bytes32 entityHash_) { coreHash_ = Entity.coreHash(key, creator, createdAt, op.contentType, op.payload, op.attributes); @@ -258,7 +258,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// @dev Route an op to the correct handler by operationType. /// Reverts with InvalidOpType for unrecognised values. - function _dispatch(Entity.Operation calldata op, BlockNumber current) + function _dispatch(Entity.Operation calldata op, BlockNumber32 current) internal virtual returns (bytes32 key, bytes32 entityHash_) @@ -285,7 +285,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// 1. contentType must be valid MIME /// 2. btl must be non-zero /// 3. Attributes validated inside coreHash (count, sorting, value type/length) - function _create(Entity.Operation calldata op, BlockNumber current) + function _create(Entity.Operation calldata op, BlockNumber32 current) internal virtual returns (bytes32 key, bytes32 entityHash_) @@ -293,7 +293,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { validateMime128(op.contentType); Entity.requirePositiveBtl(op.btl); - BlockNumber expiresAt = current + op.btl; + BlockNumber32 expiresAt = current + op.btl; key = _createEntityKey(msg.sender); bytes32 coreHash_; @@ -320,7 +320,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// 2. Caller must be the owner /// 3. contentType must be valid MIME /// 4. Attributes validated inside coreHash (count, sorting, value type/length) - function _update(Entity.Operation calldata op, BlockNumber current) internal virtual returns (bytes32, bytes32) { + function _update(Entity.Operation calldata op, BlockNumber32 current) internal virtual returns (bytes32, bytes32) { bytes32 key = op.entityKey; Entity.Commitment storage c = _commitments[key]; @@ -347,7 +347,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// 1. Entity must exist and be active /// 2. Caller must be the owner /// 3. New expiresAt (current + btl) must be strictly greater than stored expiresAt - function _extend(Entity.Operation calldata op, BlockNumber current) internal virtual returns (bytes32, bytes32) { + function _extend(Entity.Operation calldata op, BlockNumber32 current) internal virtual returns (bytes32, bytes32) { bytes32 key = op.entityKey; Entity.Commitment storage c = _commitments[key]; @@ -355,7 +355,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { Entity.requireActive(key, c, current); Entity.requireOwner(key, c); - BlockNumber newExpiresAt = current + op.btl; + BlockNumber32 newExpiresAt = current + op.btl; Entity.requireExpiryIncreased(key, newExpiresAt, c.expiresAt); c.expiresAt = newExpiresAt; @@ -374,7 +374,11 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// 1. Entity must exist and be active /// 2. Caller must be the current owner /// 3. New owner must not be the zero address or current owner - function _transfer(Entity.Operation calldata op, BlockNumber current) internal virtual returns (bytes32, bytes32) { + function _transfer(Entity.Operation calldata op, BlockNumber32 current) + internal + virtual + returns (bytes32, bytes32) + { bytes32 key = op.entityKey; Entity.Commitment storage c = _commitments[key]; @@ -399,7 +403,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// Validation: /// 1. Entity must exist and be active /// 2. Caller must be the owner - function _delete(Entity.Operation calldata op, BlockNumber current) internal virtual returns (bytes32, bytes32) { + function _delete(Entity.Operation calldata op, BlockNumber32 current) internal virtual returns (bytes32, bytes32) { bytes32 key = op.entityKey; Entity.Commitment storage c = _commitments[key]; @@ -410,7 +414,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { // Snapshot before deletion. bytes32 entityHash_ = _wrapEntityHash(c.coreHash, c.owner, c.updatedAt, c.expiresAt); address owner = c.owner; - BlockNumber expiresAt = c.expiresAt; + BlockNumber32 expiresAt = c.expiresAt; delete _commitments[key]; @@ -424,7 +428,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { /// Validation: /// 1. Entity must exist /// 2. Entity must have expired (expiresAt <= current) - function _expire(Entity.Operation calldata op, BlockNumber current) internal virtual returns (bytes32, bytes32) { + function _expire(Entity.Operation calldata op, BlockNumber32 current) internal virtual returns (bytes32, bytes32) { bytes32 key = op.entityKey; Entity.Commitment storage c = _commitments[key]; @@ -433,7 +437,7 @@ contract EntityRegistry is EIP712("Arkiv EntityRegistry", "1") { bytes32 entityHash_ = _wrapEntityHash(c.coreHash, c.owner, c.updatedAt, c.expiresAt); address owner = c.owner; - BlockNumber expiresAt = c.expiresAt; + BlockNumber32 expiresAt = c.expiresAt; delete _commitments[key]; diff --git a/contracts/IEntityRegistry.sol b/contracts/IEntityRegistry.sol index b470069..505d20d 100644 --- a/contracts/IEntityRegistry.sol +++ b/contracts/IEntityRegistry.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "./types/BlockNumber.sol"; +import {BlockNumber32} from "./types/BlockNumber32.sol"; import {Ident32} from "./types/Ident32.sol"; import {Entity, OperationKey} from "./Entity.sol"; @@ -16,7 +16,7 @@ interface IEntityRegistry { bytes32 indexed entityKey, uint8 indexed operationType, address indexed owner, - BlockNumber expiresAt, + BlockNumber32 expiresAt, bytes32 entityHash ); @@ -32,11 +32,11 @@ interface IEntityRegistry { error TooManyAttributes(uint256 count, uint256 maxCount); error EntityNotFound(bytes32 entityKey); error NotOwner(bytes32 entityKey, address caller, address owner); - error EntityExpired(bytes32 entityKey, BlockNumber expiresAt); - error ExpiryNotExtended(bytes32 entityKey, BlockNumber newExpiresAt, BlockNumber currentExpiresAt); + error EntityExpired(bytes32 entityKey, BlockNumber32 expiresAt); + error ExpiryNotExtended(bytes32 entityKey, BlockNumber32 newExpiresAt, BlockNumber32 currentExpiresAt); error TransferToZeroAddress(bytes32 entityKey); error TransferToSelf(bytes32 entityKey); - error EntityNotExpired(bytes32 entityKey, BlockNumber expiresAt); + error EntityNotExpired(bytes32 entityKey, BlockNumber32 expiresAt); // ── Write ─────────────────────────────────────────────────── @@ -45,14 +45,14 @@ interface IEntityRegistry { // ── Read ──────────────────────────────────────────────────── function changeSetHash() external view returns (bytes32); - function changeSetHashAtBlock(BlockNumber blockNumber) external view returns (bytes32); - function changeSetHashAtTx(BlockNumber blockNumber, uint32 txSeq) external view returns (bytes32); - function changeSetHashAtOp(BlockNumber blockNumber, uint32 txSeq, uint32 opSeq) external view returns (bytes32); + function changeSetHashAtBlock(BlockNumber32 blockNumber) external view returns (bytes32); + function changeSetHashAtTx(BlockNumber32 blockNumber, uint32 txSeq) external view returns (bytes32); + function changeSetHashAtOp(BlockNumber32 blockNumber, uint32 txSeq, uint32 opSeq) external view returns (bytes32); function commitment(bytes32 key) external view returns (Entity.Commitment memory); function entityKey(address owner, uint32 nonce) external view returns (bytes32); - function genesisBlock() external view returns (BlockNumber); - function headBlock() external view returns (BlockNumber); - function getBlockNode(BlockNumber blockNumber) external view returns (Entity.BlockNode memory); + function genesisBlock() external view returns (BlockNumber32); + function headBlock() external view returns (BlockNumber32); + function getBlockNode(BlockNumber32 blockNumber) external view returns (Entity.BlockNode memory); function nonces(address owner) external view returns (uint32); - function txOpCount(BlockNumber blockNumber, uint32 txSeq) external view returns (uint32); + function txOpCount(BlockNumber32 blockNumber, uint32 txSeq) external view returns (uint32); } diff --git a/contracts/types/BlockNumber.sol b/contracts/types/BlockNumber.sol deleted file mode 100644 index e4ea7af..0000000 --- a/contracts/types/BlockNumber.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -/// @dev Block number encoded as uint32. -/// -/// uint32 overflows at block ~4.3 billion — ~272 years at 2s blocks, -/// ~136 years at 1s blocks (L2). Sufficient for any foreseeable chain. -/// -/// The small width is intentional: three BlockNumbers (12 bytes) pack -/// alongside an address (20 bytes) into a single 32-byte storage slot. -/// This enables the Commitment struct to fit in 3 slots and BlockNode -/// in 1 slot. A uint256 would double storage costs across the registry. -/// -/// BlockNumbers also pack into OperationKey and TransactionKey via bit -/// shifts, enabling O(1) composite key computation without hashing. -type BlockNumber is uint32; - -using { - blockNumberEq as ==, - blockNumberNeq as !=, - blockNumberLt as <, - blockNumberLte as <=, - blockNumberGt as >, - blockNumberGte as >=, - blockNumberAdd as +, - blockNumberSub as - -} for BlockNumber global; - -function blockNumberEq(BlockNumber a, BlockNumber b) pure returns (bool) { - return BlockNumber.unwrap(a) == BlockNumber.unwrap(b); -} - -function blockNumberNeq(BlockNumber a, BlockNumber b) pure returns (bool) { - return BlockNumber.unwrap(a) != BlockNumber.unwrap(b); -} - -function blockNumberLt(BlockNumber a, BlockNumber b) pure returns (bool) { - return BlockNumber.unwrap(a) < BlockNumber.unwrap(b); -} - -function blockNumberLte(BlockNumber a, BlockNumber b) pure returns (bool) { - return BlockNumber.unwrap(a) <= BlockNumber.unwrap(b); -} - -function blockNumberGt(BlockNumber a, BlockNumber b) pure returns (bool) { - return BlockNumber.unwrap(a) > BlockNumber.unwrap(b); -} - -function blockNumberGte(BlockNumber a, BlockNumber b) pure returns (bool) { - return BlockNumber.unwrap(a) >= BlockNumber.unwrap(b); -} - -function blockNumberAdd(BlockNumber a, BlockNumber b) pure returns (BlockNumber) { - return BlockNumber.wrap(BlockNumber.unwrap(a) + BlockNumber.unwrap(b)); -} - -function blockNumberSub(BlockNumber a, BlockNumber b) pure returns (BlockNumber) { - return BlockNumber.wrap(BlockNumber.unwrap(a) - BlockNumber.unwrap(b)); -} - diff --git a/contracts/types/BlockNumber32.sol b/contracts/types/BlockNumber32.sol new file mode 100644 index 0000000..097cefb --- /dev/null +++ b/contracts/types/BlockNumber32.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +/// @dev Block number encoded as uint32. +/// +/// uint32 overflows at block ~4.3 billion — ~272 years at 2s blocks, +/// ~136 years at 1s blocks (L2). Sufficient for any foreseeable chain. +/// +/// The small width is intentional: three BlockNumber32s (12 bytes) pack +/// alongside an address (20 bytes) into a single 32-byte storage slot. +/// This enables the Commitment struct to fit in 3 slots and BlockNode +/// in 1 slot. A uint256 would double storage costs across the registry. +/// +/// BlockNumber32s also pack into OperationKey and TransactionKey via bit +/// shifts, enabling O(1) composite key computation without hashing. +type BlockNumber32 is uint32; + +using { + blockNumberEq as ==, + blockNumberNeq as !=, + blockNumberLt as <, + blockNumberLte as <=, + blockNumberGt as >, + blockNumberGte as >=, + blockNumberAdd as +, + blockNumberSub as - +} for BlockNumber32 global; + +function blockNumberEq(BlockNumber32 a, BlockNumber32 b) pure returns (bool) { + return BlockNumber32.unwrap(a) == BlockNumber32.unwrap(b); +} + +function blockNumberNeq(BlockNumber32 a, BlockNumber32 b) pure returns (bool) { + return BlockNumber32.unwrap(a) != BlockNumber32.unwrap(b); +} + +function blockNumberLt(BlockNumber32 a, BlockNumber32 b) pure returns (bool) { + return BlockNumber32.unwrap(a) < BlockNumber32.unwrap(b); +} + +function blockNumberLte(BlockNumber32 a, BlockNumber32 b) pure returns (bool) { + return BlockNumber32.unwrap(a) <= BlockNumber32.unwrap(b); +} + +function blockNumberGt(BlockNumber32 a, BlockNumber32 b) pure returns (bool) { + return BlockNumber32.unwrap(a) > BlockNumber32.unwrap(b); +} + +function blockNumberGte(BlockNumber32 a, BlockNumber32 b) pure returns (bool) { + return BlockNumber32.unwrap(a) >= BlockNumber32.unwrap(b); +} + +function blockNumberAdd(BlockNumber32 a, BlockNumber32 b) pure returns (BlockNumber32) { + return BlockNumber32.wrap(BlockNumber32.unwrap(a) + BlockNumber32.unwrap(b)); +} + +function blockNumberSub(BlockNumber32 a, BlockNumber32 b) pure returns (BlockNumber32) { + return BlockNumber32.wrap(BlockNumber32.unwrap(a) - BlockNumber32.unwrap(b)); +} + diff --git a/src/storage_layout.rs b/src/storage_layout.rs index 1d8400f..c829961 100644 --- a/src/storage_layout.rs +++ b/src/storage_layout.rs @@ -32,10 +32,10 @@ pub const HASH_AT_SLOT: u64 = 4; /// `mapping(TransactionKey transactionKey => uint32 opCount) _txOpCount`. pub const TX_OP_COUNT_SLOT: u64 = 5; -/// `mapping(BlockNumber blockNumber => Entity.BlockNode node) _blocks`. +/// `mapping(BlockNumber32 blockNumber => Entity.BlockNode node) _blocks`. pub const BLOCKS_SLOT: u64 = 6; -/// `BlockNumber _headBlock` — single-value slot (uint32 in the low 4 bytes). +/// `BlockNumber32 _headBlock` — single-value slot (uint32 in the low 4 bytes). pub const HEAD_BLOCK_SLOT: u64 = 7; // ----------------------------------------------------------------------------- @@ -62,7 +62,7 @@ pub fn operation_key(block: u32, tx: u32, op: u32) -> U256 { /// `keccak256(abi.encode(key, slot))`. /// /// `key` is the 32-byte ABI-encoded mapping key. For value types narrower than -/// 32 bytes (e.g. `address`, `uint32`, `BlockNumber`, `OperationKey`), the +/// 32 bytes (e.g. `address`, `uint32`, `BlockNumber32`, `OperationKey`), the /// caller must left-pad to 32 bytes per Solidity ABI rules. pub fn mapping_slot(slot: u64, key: B256) -> B256 { let mut buf = [0u8; 64]; @@ -485,7 +485,7 @@ mod tests { let (created, updated, expires) = (100u32, 200u32, 300u32); // Slot 0: [zero(20)][expiresAt(4)][updatedAt(4)][createdAt(4)][creator(20)] - // Wait — creator is 20 bytes at offset 0, then BlockNumbers at 20/24/28. + // Wait — creator is 20 bytes at offset 0, then BlockNumber32s at 20/24/28. // BE word: [expiresAt][updatedAt][createdAt][creator] let mut s0 = [0u8; 32]; s0[0..4].copy_from_slice(&expires.to_be_bytes()); // offset 28 diff --git a/test/e2e/EntityLifecycle.t.sol b/test/e2e/EntityLifecycle.t.sol index 4e41fbf..69fe5bb 100644 --- a/test/e2e/EntityLifecycle.t.sol +++ b/test/e2e/EntityLifecycle.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../utils/Lib.sol"; import {Entity} from "../../contracts/Entity.sol"; @@ -18,16 +18,16 @@ contract EntityLifecycleTest is Test { address bob; Mime128 textPlain; - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; function setUp() public { registry = new EntityRegistry(); alice = makeAddr("alice"); bob = makeAddr("bob"); textPlain = encodeMime128("text/plain"); - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; } /// @dev Helper — build a single-op array and execute as sender. @@ -65,9 +65,9 @@ contract EntityLifecycleTest is Test { assertNotEq(hashAfterUpdate, hashAfterCreate); // Extend. - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - _exec(alice, Lib.extendOp(key, newExpiry - BlockNumber.wrap(uint32(block.number)))); - assertEq(BlockNumber.unwrap(registry.commitment(key).expiresAt), BlockNumber.unwrap(newExpiry)); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + _exec(alice, Lib.extendOp(key, newExpiry - BlockNumber32.wrap(uint32(block.number)))); + assertEq(BlockNumber32.unwrap(registry.commitment(key).expiresAt), BlockNumber32.unwrap(newExpiry)); bytes32 hashAfterExtend = registry.changeSetHash(); assertNotEq(hashAfterExtend, hashAfterUpdate); @@ -109,7 +109,7 @@ contract EntityLifecycleTest is Test { assertEq(registry.nonces(alice), 2); // Both ops recorded in the same tx. - BlockNumber head = registry.headBlock(); + BlockNumber32 head = registry.headBlock(); assertEq(registry.txOpCount(head, 0), 2); // Per-op snapshots differ. @@ -133,27 +133,27 @@ contract EntityLifecycleTest is Test { // Block 1: create. _exec(alice, Lib.createOp("hello", textPlain, attrs, btl)); bytes32 key = registry.entityKey(alice, 0); - BlockNumber block1 = registry.headBlock(); + BlockNumber32 block1 = registry.headBlock(); bytes32 hashBlock1 = registry.changeSetHash(); // Block 2: update. vm.roll(block.number + 5); _exec(alice, Lib.updateOp(key, "updated", textPlain, attrs)); - BlockNumber block2 = registry.headBlock(); + BlockNumber32 block2 = registry.headBlock(); bytes32 hashBlock2 = registry.changeSetHash(); // Chain advanced. assertNotEq(hashBlock2, hashBlock1); // Head moved. - assertEq(BlockNumber.unwrap(registry.headBlock()), BlockNumber.unwrap(block2)); + assertEq(BlockNumber32.unwrap(registry.headBlock()), BlockNumber32.unwrap(block2)); // Linked list intact. Entity.BlockNode memory node1 = registry.getBlockNode(block1); - assertEq(BlockNumber.unwrap(node1.nextBlock), BlockNumber.unwrap(block2)); + assertEq(BlockNumber32.unwrap(node1.nextBlock), BlockNumber32.unwrap(block2)); Entity.BlockNode memory node2 = registry.getBlockNode(block2); - assertEq(BlockNumber.unwrap(node2.prevBlock), BlockNumber.unwrap(block1)); + assertEq(BlockNumber32.unwrap(node2.prevBlock), BlockNumber32.unwrap(block1)); // Historical hash still accessible. assertEq(registry.changeSetHashAtBlock(block1), hashBlock1); @@ -172,7 +172,7 @@ contract EntityLifecycleTest is Test { assertTrue(registry.commitment(key).creator != address(0)); // Roll to expiry. - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); // Anyone can expire through execute. _exec(bob, Lib.expireOp(key)); @@ -208,7 +208,7 @@ contract EntityLifecycleTest is Test { function test_initialState() public view { assertEq(registry.changeSetHash(), bytes32(0)); - assertEq(BlockNumber.unwrap(registry.genesisBlock()), BlockNumber.unwrap(registry.headBlock())); + assertEq(BlockNumber32.unwrap(registry.genesisBlock()), BlockNumber32.unwrap(registry.headBlock())); assertEq(registry.nonces(alice), 0); } } diff --git a/test/integration/ExpiryLifecycle.t.sol b/test/integration/ExpiryLifecycle.t.sol index 8a162f8..56870ee 100644 --- a/test/integration/ExpiryLifecycle.t.sol +++ b/test/integration/ExpiryLifecycle.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../utils/Lib.sol"; import {Entity} from "../../contracts/Entity.sol"; @@ -14,33 +14,33 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doExtend(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _extend(op, BlockNumber.wrap(uint32(block.number))); + return _extend(op, BlockNumber32.wrap(uint32(block.number))); } function doExpire(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _expire(op, BlockNumber.wrap(uint32(block.number))); + return _expire(op, BlockNumber32.wrap(uint32(block.number))); } function doUpdate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _update(op, BlockNumber.wrap(uint32(block.number))); + return _update(op, BlockNumber32.wrap(uint32(block.number))); } function doDelete(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _delete(op, BlockNumber.wrap(uint32(block.number))); + return _delete(op, BlockNumber32.wrap(uint32(block.number))); } function setUp() public { - btl = BlockNumber.wrap(100); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(100); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -53,15 +53,15 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { // ========================================================================= function test_extendMultipleTimes() public { - BlockNumber expiry1 = expiresAt + BlockNumber.wrap(100); + BlockNumber32 expiry1 = expiresAt + BlockNumber32.wrap(100); vm.prank(alice); - this.doExtend(Lib.extendOp(testKey, expiry1 - BlockNumber.wrap(uint32(block.number)))); - assertEq(BlockNumber.unwrap(commitment(testKey).expiresAt), BlockNumber.unwrap(expiry1)); + this.doExtend(Lib.extendOp(testKey, expiry1 - BlockNumber32.wrap(uint32(block.number)))); + assertEq(BlockNumber32.unwrap(commitment(testKey).expiresAt), BlockNumber32.unwrap(expiry1)); - BlockNumber expiry2 = expiry1 + BlockNumber.wrap(100); + BlockNumber32 expiry2 = expiry1 + BlockNumber32.wrap(100); vm.prank(alice); - this.doExtend(Lib.extendOp(testKey, expiry2 - BlockNumber.wrap(uint32(block.number)))); - assertEq(BlockNumber.unwrap(commitment(testKey).expiresAt), BlockNumber.unwrap(expiry2)); + this.doExtend(Lib.extendOp(testKey, expiry2 - BlockNumber32.wrap(uint32(block.number)))); + assertEq(BlockNumber32.unwrap(commitment(testKey).expiresAt), BlockNumber32.unwrap(expiry2)); } // ========================================================================= @@ -69,7 +69,7 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { // ========================================================================= function test_expiredEntityCannotBeUpdated() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory op = Lib.updateOp(testKey, "new", encodeMime128("text/plain"), attrs); @@ -79,16 +79,16 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { } function test_expiredEntityCannotBeExtended() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); - this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number)))); + this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number)))); } function test_expiredEntityCannotBeDeleted() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); @@ -100,7 +100,7 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { // ========================================================================= function test_nonOwnerCanExpire() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); vm.prank(bob); this.doExpire(Lib.expireOp(testKey)); @@ -112,12 +112,12 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { // ========================================================================= function test_extendThenOperateAfterOriginalExpiry() public { - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); vm.prank(alice); - this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number)))); + this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number)))); // Roll past original expiry but before new expiry. - vm.roll(BlockNumber.unwrap(expiresAt) + 1); + vm.roll(BlockNumber32.unwrap(expiresAt) + 1); // Update should succeed — entity is still active. Entity.Attribute[] memory attrs = new Entity.Attribute[](0); @@ -133,12 +133,12 @@ contract ExpiryLifecycleTest is Test, EntityRegistry { function test_fullExpiryLifecycle() public { // Extend. - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(200); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(200); vm.prank(alice); - this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number)))); + this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number)))); // Roll to new expiry. - vm.roll(BlockNumber.unwrap(newExpiry)); + vm.roll(BlockNumber32.unwrap(newExpiry)); // Expire (by non-owner). vm.prank(bob); diff --git a/test/integration/OperationSequencing.t.sol b/test/integration/OperationSequencing.t.sol index 968a4f6..7ce23ae 100644 --- a/test/integration/OperationSequencing.t.sol +++ b/test/integration/OperationSequencing.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../utils/Lib.sol"; import {Entity} from "../../contracts/Entity.sol"; @@ -13,37 +13,37 @@ import {encodeMime128} from "../../contracts/types/Mime128.sol"; contract OperationSequencingTest is Test, EntityRegistry { address alice = makeAddr("alice"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doDelete(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _delete(op, BlockNumber.wrap(uint32(block.number))); + return _delete(op, BlockNumber32.wrap(uint32(block.number))); } function doExpire(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _expire(op, BlockNumber.wrap(uint32(block.number))); + return _expire(op, BlockNumber32.wrap(uint32(block.number))); } function doExtend(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _extend(op, BlockNumber.wrap(uint32(block.number))); + return _extend(op, BlockNumber32.wrap(uint32(block.number))); } function doUpdate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _update(op, BlockNumber.wrap(uint32(block.number))); + return _update(op, BlockNumber32.wrap(uint32(block.number))); } function doTransfer(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _transfer(op, BlockNumber.wrap(uint32(block.number))); + return _transfer(op, BlockNumber32.wrap(uint32(block.number))); } function setUp() public { - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -61,7 +61,9 @@ contract OperationSequencingTest is Test, EntityRegistry { vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotFound.selector, testKey)); - this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500) - BlockNumber.wrap(uint32(block.number)))); + this.doExtend( + Lib.extendOp(testKey, expiresAt + BlockNumber32.wrap(500) - BlockNumber32.wrap(uint32(block.number))) + ); } function test_deleteThenUpdate_reverts() public { @@ -99,7 +101,7 @@ contract OperationSequencingTest is Test, EntityRegistry { // ========================================================================= function test_expireThenUpdate_reverts() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); this.doExpire(Lib.expireOp(testKey)); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); @@ -110,16 +112,18 @@ contract OperationSequencingTest is Test, EntityRegistry { } function test_expireThenExtend_reverts() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); this.doExpire(Lib.expireOp(testKey)); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotFound.selector, testKey)); - this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500) - BlockNumber.wrap(uint32(block.number)))); + this.doExtend( + Lib.extendOp(testKey, expiresAt + BlockNumber32.wrap(500) - BlockNumber32.wrap(uint32(block.number))) + ); } function test_expireThenExpire_reverts() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); this.doExpire(Lib.expireOp(testKey)); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotFound.selector, testKey)); diff --git a/test/integration/OwnershipLifecycle.t.sol b/test/integration/OwnershipLifecycle.t.sol index 45dfbfd..9579cbd 100644 --- a/test/integration/OwnershipLifecycle.t.sol +++ b/test/integration/OwnershipLifecycle.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../utils/Lib.sol"; import {Entity} from "../../contracts/Entity.sol"; @@ -15,33 +15,33 @@ contract OwnershipLifecycleTest is Test, EntityRegistry { address bob = makeAddr("bob"); address charlie = makeAddr("charlie"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doTransfer(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _transfer(op, BlockNumber.wrap(uint32(block.number))); + return _transfer(op, BlockNumber32.wrap(uint32(block.number))); } function doUpdate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _update(op, BlockNumber.wrap(uint32(block.number))); + return _update(op, BlockNumber32.wrap(uint32(block.number))); } function doDelete(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _delete(op, BlockNumber.wrap(uint32(block.number))); + return _delete(op, BlockNumber32.wrap(uint32(block.number))); } function doExtend(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _extend(op, BlockNumber.wrap(uint32(block.number))); + return _extend(op, BlockNumber32.wrap(uint32(block.number))); } function setUp() public { - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -86,7 +86,9 @@ contract OwnershipLifecycleTest is Test, EntityRegistry { vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.NotOwner.selector, testKey, alice, bob)); - this.doExtend(Lib.extendOp(testKey, expiresAt + BlockNumber.wrap(500) - BlockNumber.wrap(uint32(block.number)))); + this.doExtend( + Lib.extendOp(testKey, expiresAt + BlockNumber32.wrap(500) - BlockNumber32.wrap(uint32(block.number))) + ); } function test_previousOwnerCannotDelete() public { @@ -126,10 +128,10 @@ contract OwnershipLifecycleTest is Test, EntityRegistry { vm.prank(alice); this.doTransfer(Lib.transferOp(testKey, bob)); - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); vm.prank(bob); - this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number)))); - assertEq(BlockNumber.unwrap(commitment(testKey).expiresAt), BlockNumber.unwrap(newExpiry)); + this.doExtend(Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number)))); + assertEq(BlockNumber32.unwrap(commitment(testKey).expiresAt), BlockNumber32.unwrap(newExpiry)); } function test_newOwnerCanDelete() public { diff --git a/test/unit/ComputeEntityHash.t.sol b/test/unit/ComputeEntityHash.t.sol index 740be55..0126fb9 100644 --- a/test/unit/ComputeEntityHash.t.sol +++ b/test/unit/ComputeEntityHash.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../utils/Lib.sol"; import {Entity} from "../../contracts/Entity.sol"; @@ -22,10 +22,10 @@ contract ComputeEntityHashTest is Test, EntityRegistry { function doComputeEntityHash( bytes32 key, address creator, - BlockNumber createdAt, + BlockNumber32 createdAt, address owner, - BlockNumber updatedAt, - BlockNumber expiresAt, + BlockNumber32 updatedAt, + BlockNumber32 expiresAt, Entity.Operation calldata op ) external view returns (bytes32, bytes32) { return _computeEntityHash(key, creator, createdAt, owner, updatedAt, expiresAt, op); @@ -34,7 +34,7 @@ contract ComputeEntityHashTest is Test, EntityRegistry { function doCoreHash( bytes32 key, address creator, - BlockNumber createdAt, + BlockNumber32 createdAt, Mime128 calldata contentType, bytes calldata payload, Entity.Attribute[] calldata attributes @@ -48,12 +48,13 @@ contract ComputeEntityHashTest is Test, EntityRegistry { function test_coreHashMatchesLibrary() public { bytes32 key = keccak256("key"); - BlockNumber current = BlockNumber.wrap(100); + BlockNumber32 current = BlockNumber32.wrap(100); Entity.Attribute[] memory attrs = new Entity.Attribute[](1); attrs[0] = Lib.uintAttr("count", 42); - Entity.Operation memory op = - Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000)); + Entity.Operation memory op = Lib.createOp( + "hello", textPlain, attrs, BlockNumber32.wrap(uint32(block.number)) + BlockNumber32.wrap(1000) + ); (bytes32 coreHash_,) = this.doComputeEntityHash(key, alice, current, alice, current, current + op.btl, op); bytes32 expected = this.doCoreHash(key, alice, current, textPlain, "hello", attrs); @@ -67,11 +68,12 @@ contract ComputeEntityHashTest is Test, EntityRegistry { function test_entityHashWrapsWithDomainSeparator() public { bytes32 key = keccak256("key"); - BlockNumber current = BlockNumber.wrap(100); + BlockNumber32 current = BlockNumber32.wrap(100); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = - Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000)); + Entity.Operation memory op = Lib.createOp( + "hello", textPlain, attrs, BlockNumber32.wrap(uint32(block.number)) + BlockNumber32.wrap(1000) + ); (bytes32 coreHash_, bytes32 entityHash_) = this.doComputeEntityHash(key, alice, current, alice, current, current + op.btl, op); @@ -88,11 +90,12 @@ contract ComputeEntityHashTest is Test, EntityRegistry { function test_deterministic() public { bytes32 key = keccak256("key"); - BlockNumber current = BlockNumber.wrap(100); + BlockNumber32 current = BlockNumber32.wrap(100); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = - Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000)); + Entity.Operation memory op = Lib.createOp( + "hello", textPlain, attrs, BlockNumber32.wrap(uint32(block.number)) + BlockNumber32.wrap(1000) + ); (bytes32 coreA, bytes32 entityA) = this.doComputeEntityHash(key, alice, current, alice, current, current + op.btl, op); @@ -109,8 +112,8 @@ contract ComputeEntityHashTest is Test, EntityRegistry { function test_differentPayload_differs() public { bytes32 key = keccak256("key"); - BlockNumber current = BlockNumber.wrap(100); - BlockNumber expiry = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + BlockNumber32 current = BlockNumber32.wrap(100); + BlockNumber32 expiry = BlockNumber32.wrap(uint32(block.number)) + BlockNumber32.wrap(1000); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory opA = Lib.createOp("hello", textPlain, attrs, expiry); @@ -124,11 +127,11 @@ contract ComputeEntityHashTest is Test, EntityRegistry { function test_differentExpiry_entityHashDiffers() public { bytes32 key = keccak256("key"); - BlockNumber current = BlockNumber.wrap(100); + BlockNumber32 current = BlockNumber32.wrap(100); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory opA = Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(500)); - Entity.Operation memory opB = Lib.createOp("hello", textPlain, attrs, BlockNumber.wrap(600)); + Entity.Operation memory opA = Lib.createOp("hello", textPlain, attrs, BlockNumber32.wrap(500)); + Entity.Operation memory opB = Lib.createOp("hello", textPlain, attrs, BlockNumber32.wrap(600)); (bytes32 coreA, bytes32 entityA) = this.doComputeEntityHash(key, alice, current, alice, current, current + opA.btl, opA); @@ -143,8 +146,8 @@ contract ComputeEntityHashTest is Test, EntityRegistry { function test_differentAttributes_coreHashDiffers() public { bytes32 key = keccak256("key"); - BlockNumber current = BlockNumber.wrap(100); - BlockNumber expiry = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + BlockNumber32 current = BlockNumber32.wrap(100); + BlockNumber32 expiry = BlockNumber32.wrap(uint32(block.number)) + BlockNumber32.wrap(1000); Entity.Attribute[] memory attrsA = new Entity.Attribute[](1); attrsA[0] = Lib.uintAttr("count", 1); diff --git a/test/unit/Dispatch.t.sol b/test/unit/Dispatch.t.sol index 4d203a1..6798d53 100644 --- a/test/unit/Dispatch.t.sol +++ b/test/unit/Dispatch.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Entity} from "../../contracts/Entity.sol"; import {EntityRegistry} from "../../contracts/EntityRegistry.sol"; @@ -17,32 +17,32 @@ contract DispatchTest is Test, EntityRegistry { return (keccak256("key"), keccak256("hash")); } - function _create(Entity.Operation calldata, BlockNumber) internal override returns (bytes32, bytes32) { + function _create(Entity.Operation calldata, BlockNumber32) internal override returns (bytes32, bytes32) { _calledOpType = Entity.CREATE; return _stubReturn(); } - function _update(Entity.Operation calldata, BlockNumber) internal override returns (bytes32, bytes32) { + function _update(Entity.Operation calldata, BlockNumber32) internal override returns (bytes32, bytes32) { _calledOpType = Entity.UPDATE; return _stubReturn(); } - function _extend(Entity.Operation calldata, BlockNumber) internal override returns (bytes32, bytes32) { + function _extend(Entity.Operation calldata, BlockNumber32) internal override returns (bytes32, bytes32) { _calledOpType = Entity.EXTEND; return _stubReturn(); } - function _transfer(Entity.Operation calldata, BlockNumber) internal override returns (bytes32, bytes32) { + function _transfer(Entity.Operation calldata, BlockNumber32) internal override returns (bytes32, bytes32) { _calledOpType = Entity.TRANSFER; return _stubReturn(); } - function _delete(Entity.Operation calldata, BlockNumber) internal override returns (bytes32, bytes32) { + function _delete(Entity.Operation calldata, BlockNumber32) internal override returns (bytes32, bytes32) { _calledOpType = Entity.DELETE; return _stubReturn(); } - function _expire(Entity.Operation calldata, BlockNumber) internal override returns (bytes32, bytes32) { + function _expire(Entity.Operation calldata, BlockNumber32) internal override returns (bytes32, bytes32) { _calledOpType = Entity.EXPIRE; return _stubReturn(); } @@ -50,7 +50,7 @@ contract DispatchTest is Test, EntityRegistry { /// @dev External wrapper so we can call _dispatch via this.doDispatch() /// to get calldata encoding. function doDispatch(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _dispatch(op, BlockNumber.wrap(uint32(block.number))); + return _dispatch(op, BlockNumber32.wrap(uint32(block.number))); } function _op(uint8 operationType) internal pure returns (Entity.Operation memory) { @@ -61,7 +61,7 @@ contract DispatchTest is Test, EntityRegistry { payload: "", contentType: encodeMime128("text/plain"), attributes: attrs, - btl: BlockNumber.wrap(0), + btl: BlockNumber32.wrap(0), newOwner: address(0) }); } diff --git a/test/unit/Execute.t.sol b/test/unit/Execute.t.sol index ad41ffc..d518e70 100644 --- a/test/unit/Execute.t.sol +++ b/test/unit/Execute.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test, Vm} from "forge-std/Test.sol"; import {Entity, OperationKey} from "../../contracts/Entity.sol"; import {EntityRegistry} from "../../contracts/EntityRegistry.sol"; @@ -17,7 +17,7 @@ contract ExecuteTest is Test, EntityRegistry { uint256 internal _stubIndex; uint256 internal _stubSeed; - function _dispatch(Entity.Operation calldata, BlockNumber) internal override returns (bytes32, bytes32) { + function _dispatch(Entity.Operation calldata, BlockNumber32) internal override returns (bytes32, bytes32) { bytes32 key = _stubKeys[_stubIndex]; bytes32 hash = _stubHashes[_stubIndex]; _stubIndex++; @@ -46,7 +46,7 @@ contract ExecuteTest is Test, EntityRegistry { payload: "", contentType: encodeMime128("text/plain"), attributes: attrs, - btl: BlockNumber.wrap(0), + btl: BlockNumber32.wrap(0), newOwner: address(0) }); } @@ -125,7 +125,7 @@ contract ExecuteTest is Test, EntityRegistry { ops[2] = _op(Entity.DELETE); this.execute(ops); - BlockNumber head = headBlock(); + BlockNumber32 head = headBlock(); bytes32 chain0 = Entity.chainOperationHash(bytes32(0), Entity.CREATE, k0, h0); bytes32 chain1 = Entity.chainOperationHash(chain0, Entity.UPDATE, k1, h1); bytes32 chain2 = Entity.chainOperationHash(chain1, Entity.DELETE, k2, h2); @@ -165,20 +165,20 @@ contract ExecuteTest is Test, EntityRegistry { function test_execute_newBlock_headBlockUpdated() public { vm.roll(block.number + 10); - BlockNumber newBlock = BlockNumber.wrap(uint32(block.number)); + BlockNumber32 newBlock = BlockNumber32.wrap(uint32(block.number)); _pushStubs(1); Entity.Operation[] memory ops = new Entity.Operation[](1); ops[0] = _op(Entity.CREATE); this.execute(ops); - assertEq(BlockNumber.unwrap(headBlock()), BlockNumber.unwrap(newBlock)); + assertEq(BlockNumber32.unwrap(headBlock()), BlockNumber32.unwrap(newBlock)); } function test_execute_newBlock_linkedListPointers() public { - BlockNumber genesis = genesisBlock(); + BlockNumber32 genesis = genesisBlock(); vm.roll(block.number + 10); - BlockNumber newBlock = BlockNumber.wrap(uint32(block.number)); + BlockNumber32 newBlock = BlockNumber32.wrap(uint32(block.number)); _pushStubs(1); Entity.Operation[] memory ops = new Entity.Operation[](1); @@ -186,10 +186,10 @@ contract ExecuteTest is Test, EntityRegistry { this.execute(ops); Entity.BlockNode memory genesisNode = getBlockNode(genesis); - assertEq(BlockNumber.unwrap(genesisNode.nextBlock), BlockNumber.unwrap(newBlock)); + assertEq(BlockNumber32.unwrap(genesisNode.nextBlock), BlockNumber32.unwrap(newBlock)); Entity.BlockNode memory newNode = getBlockNode(newBlock); - assertEq(BlockNumber.unwrap(newNode.prevBlock), BlockNumber.unwrap(genesis)); + assertEq(BlockNumber32.unwrap(newNode.prevBlock), BlockNumber32.unwrap(genesis)); } function test_execute_newBlock_txCountIsOne() public { @@ -242,7 +242,7 @@ contract ExecuteTest is Test, EntityRegistry { ops2[0] = _op(Entity.DELETE); this.execute(ops2); - BlockNumber head = headBlock(); + BlockNumber32 head = headBlock(); assertEq(txOpCount(head, 0), 2); assertEq(txOpCount(head, 1), 1); } @@ -272,53 +272,53 @@ contract ExecuteTest is Test, EntityRegistry { // ========================================================================= function test_execute_crossBlock_linkedListMaintained() public { - BlockNumber genesis = genesisBlock(); + BlockNumber32 genesis = genesisBlock(); vm.roll(block.number + 10); _pushStubs(1); Entity.Operation[] memory ops1 = new Entity.Operation[](1); ops1[0] = _op(Entity.CREATE); this.execute(ops1); - BlockNumber blockA = headBlock(); + BlockNumber32 blockA = headBlock(); vm.roll(block.number + 5); _pushStubs(1); Entity.Operation[] memory ops2 = new Entity.Operation[](1); ops2[0] = _op(Entity.CREATE); this.execute(ops2); - BlockNumber blockB = headBlock(); + BlockNumber32 blockB = headBlock(); // genesis → blockA → blockB Entity.BlockNode memory genesisNode = getBlockNode(genesis); - assertEq(BlockNumber.unwrap(genesisNode.nextBlock), BlockNumber.unwrap(blockA)); + assertEq(BlockNumber32.unwrap(genesisNode.nextBlock), BlockNumber32.unwrap(blockA)); Entity.BlockNode memory nodeA = getBlockNode(blockA); - assertEq(BlockNumber.unwrap(nodeA.prevBlock), BlockNumber.unwrap(genesis)); - assertEq(BlockNumber.unwrap(nodeA.nextBlock), BlockNumber.unwrap(blockB)); + assertEq(BlockNumber32.unwrap(nodeA.prevBlock), BlockNumber32.unwrap(genesis)); + assertEq(BlockNumber32.unwrap(nodeA.nextBlock), BlockNumber32.unwrap(blockB)); Entity.BlockNode memory nodeB = getBlockNode(blockB); - assertEq(BlockNumber.unwrap(nodeB.prevBlock), BlockNumber.unwrap(blockA)); - assertEq(BlockNumber.unwrap(nodeB.nextBlock), 0); + assertEq(BlockNumber32.unwrap(nodeB.prevBlock), BlockNumber32.unwrap(blockA)); + assertEq(BlockNumber32.unwrap(nodeB.nextBlock), 0); } function test_execute_crossBlock_headBlockUpdates() public { - BlockNumber genesis = genesisBlock(); + BlockNumber32 genesis = genesisBlock(); vm.roll(block.number + 10); _pushStubs(1); Entity.Operation[] memory ops1 = new Entity.Operation[](1); ops1[0] = _op(Entity.CREATE); this.execute(ops1); - BlockNumber blockA = headBlock(); - assertTrue(BlockNumber.unwrap(blockA) > BlockNumber.unwrap(genesis)); + BlockNumber32 blockA = headBlock(); + assertTrue(BlockNumber32.unwrap(blockA) > BlockNumber32.unwrap(genesis)); vm.roll(block.number + 5); _pushStubs(1); Entity.Operation[] memory ops2 = new Entity.Operation[](1); ops2[0] = _op(Entity.CREATE); this.execute(ops2); - BlockNumber blockB = headBlock(); - assertTrue(BlockNumber.unwrap(blockB) > BlockNumber.unwrap(blockA)); + BlockNumber32 blockB = headBlock(); + assertTrue(BlockNumber32.unwrap(blockB) > BlockNumber32.unwrap(blockA)); } function test_execute_crossBlock_hashChainContinues() public { @@ -392,7 +392,7 @@ contract ExecuteTest is Test, EntityRegistry { bytes32 chain0 = Entity.chainOperationHash(bytes32(0), Entity.CREATE, tx0k0, tx0h0); bytes32 chain1 = Entity.chainOperationHash(chain0, Entity.UPDATE, tx0k1, tx0h1); - BlockNumber head = headBlock(); + BlockNumber32 head = headBlock(); assertEq(changeSetHashAtTx(head, 0), chain1); bytes32 chain2 = Entity.chainOperationHash(chain1, Entity.DELETE, tx1k0, tx1h0); @@ -400,7 +400,7 @@ contract ExecuteTest is Test, EntityRegistry { } function test_changeSetHashAtBlock_uninitializedBlock_returnsZero() public view { - assertEq(changeSetHashAtBlock(BlockNumber.wrap(999999)), bytes32(0)); + assertEq(changeSetHashAtBlock(BlockNumber32.wrap(999999)), bytes32(0)); } // ========================================================================= @@ -415,12 +415,12 @@ contract ExecuteTest is Test, EntityRegistry { this.execute(ops); bytes32 current = changeSetHash(); - BlockNumber head = headBlock(); + BlockNumber32 head = headBlock(); // Querying head returns the current rolling hash. assertEq(changeSetHashAtBlock(head), current); // Any block past head returns the same. - BlockNumber future = BlockNumber.wrap(BlockNumber.unwrap(head) + 100); + BlockNumber32 future = BlockNumber32.wrap(BlockNumber32.unwrap(head) + 100); assertEq(changeSetHashAtBlock(future), current); } @@ -437,7 +437,7 @@ contract ExecuteTest is Test, EntityRegistry { Entity.Operation[] memory opsA = new Entity.Operation[](1); opsA[0] = _op(Entity.CREATE); this.execute(opsA); - BlockNumber blockA = headBlock(); + BlockNumber32 blockA = headBlock(); bytes32 hashAtA = changeSetHash(); // Skip empty blocks, then mutate at blockB. @@ -446,33 +446,33 @@ contract ExecuteTest is Test, EntityRegistry { Entity.Operation[] memory opsB = new Entity.Operation[](1); opsB[0] = _op(Entity.UPDATE); this.execute(opsB); - BlockNumber blockB = headBlock(); + BlockNumber32 blockB = headBlock(); // Sanity: there is at least one empty block between A and B. - assertGt(BlockNumber.unwrap(blockB), BlockNumber.unwrap(blockA) + 1); + assertGt(BlockNumber32.unwrap(blockB), BlockNumber32.unwrap(blockA) + 1); // Any empty block in (A, B) reads back as A's hash. - BlockNumber justAfterA = BlockNumber.wrap(BlockNumber.unwrap(blockA) + 1); + BlockNumber32 justAfterA = BlockNumber32.wrap(BlockNumber32.unwrap(blockA) + 1); assertEq(changeSetHashAtBlock(justAfterA), hashAtA); - BlockNumber justBeforeB = BlockNumber.wrap(BlockNumber.unwrap(blockB) - 1); + BlockNumber32 justBeforeB = BlockNumber32.wrap(BlockNumber32.unwrap(blockB) - 1); assertEq(changeSetHashAtBlock(justBeforeB), hashAtA); } function test_changeSetHashAtBlock_beforeFirstMutation_returnsZero() public { // Roll past genesis so the first mutation is strictly after deploy. - BlockNumber genesis = genesisBlock(); + BlockNumber32 genesis = genesisBlock(); vm.roll(block.number + 10); _pushStubs(1); Entity.Operation[] memory ops = new Entity.Operation[](1); ops[0] = _op(Entity.CREATE); this.execute(ops); - BlockNumber blockA = headBlock(); + BlockNumber32 blockA = headBlock(); // Genesis is in the linked list but unmutated; querying any block // in [genesis, blockA) returns zero. - BlockNumber between = BlockNumber.wrap(BlockNumber.unwrap(genesis) + 1); - assertLt(BlockNumber.unwrap(between), BlockNumber.unwrap(blockA)); + BlockNumber32 between = BlockNumber32.wrap(BlockNumber32.unwrap(genesis) + 1); + assertLt(BlockNumber32.unwrap(between), BlockNumber32.unwrap(blockA)); assertEq(changeSetHashAtBlock(between), bytes32(0)); assertEq(changeSetHashAtBlock(genesis), bytes32(0)); } @@ -483,18 +483,18 @@ contract ExecuteTest is Test, EntityRegistry { } function test_changeSetHashAtBlock_priorToGenesis_returnsZero() public { - // Walk falls off the start of the chain (cursor reaches BlockNumber 0). + // Walk falls off the start of the chain (cursor reaches BlockNumber32 0). vm.roll(block.number + 5); _pushStubs(1); Entity.Operation[] memory ops = new Entity.Operation[](1); ops[0] = _op(Entity.CREATE); this.execute(ops); - assertEq(changeSetHashAtBlock(BlockNumber.wrap(0)), bytes32(0)); + assertEq(changeSetHashAtBlock(BlockNumber32.wrap(0)), bytes32(0)); } function test_changeSetHashAtTx_uninitializedTx_returnsZero() public view { - assertEq(changeSetHashAtTx(BlockNumber.wrap(999999), 0), bytes32(0)); + assertEq(changeSetHashAtTx(BlockNumber32.wrap(999999), 0), bytes32(0)); } // ========================================================================= @@ -502,14 +502,14 @@ contract ExecuteTest is Test, EntityRegistry { // ========================================================================= function test_execute_atDeployBlock_noBlockTransition() public { - BlockNumber genesis = genesisBlock(); + BlockNumber32 genesis = genesisBlock(); _pushStubs(1); Entity.Operation[] memory ops = new Entity.Operation[](1); ops[0] = _op(Entity.CREATE); this.execute(ops); - assertEq(BlockNumber.unwrap(headBlock()), BlockNumber.unwrap(genesis)); + assertEq(BlockNumber32.unwrap(headBlock()), BlockNumber32.unwrap(genesis)); assertEq(getBlockNode(genesis).txCount, 1); } @@ -526,11 +526,11 @@ contract ExecuteTest is Test, EntityRegistry { // ========================================================================= function test_genesisBlock_equalsDeployBlock() public view { - assertEq(BlockNumber.unwrap(genesisBlock()), uint32(block.number)); + assertEq(BlockNumber32.unwrap(genesisBlock()), uint32(block.number)); } function test_headBlock_initiallyEqualsGenesisBlock() public view { - assertEq(BlockNumber.unwrap(headBlock()), BlockNumber.unwrap(genesisBlock())); + assertEq(BlockNumber32.unwrap(headBlock()), BlockNumber32.unwrap(genesisBlock())); } // ========================================================================= @@ -585,7 +585,7 @@ contract ExecuteTest is Test, EntityRegistry { assertEq(logs.length, 3); - BlockNumber head = headBlock(); + BlockNumber32 head = headBlock(); bytes32 chain0 = Entity.chainOperationHash(bytes32(0), Entity.CREATE, k0, h0); bytes32 chain1 = Entity.chainOperationHash(chain0, Entity.UPDATE, k1, h1); bytes32 chain2 = Entity.chainOperationHash(chain1, Entity.DELETE, k2, h2); diff --git a/test/unit/Views.t.sol b/test/unit/Views.t.sol index 917b30c..28fb4c4 100644 --- a/test/unit/Views.t.sol +++ b/test/unit/Views.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../utils/Lib.sol"; import {Entity} from "../../contracts/Entity.sol"; @@ -15,13 +15,13 @@ contract ViewsTest is Test { address alice = makeAddr("alice"); bytes32 testKey; - BlockNumber deployBlock; - BlockNumber btl; + BlockNumber32 deployBlock; + BlockNumber32 btl; function setUp() public { registry = new EntityRegistry(); deployBlock = registry.genesisBlock(); - btl = BlockNumber.wrap(1000); + btl = BlockNumber32.wrap(1000); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation[] memory ops = new Entity.Operation[](1); @@ -57,15 +57,15 @@ contract ViewsTest is Test { function test_changeSetHashAtBlock_pastHead_returnsCurrentHash() public view { // Carry-forward semantics: any block >= headBlock returns the current // rolling hash, not bytes32(0). - assertEq(registry.changeSetHashAtBlock(BlockNumber.wrap(999999)), registry.changeSetHash()); + assertEq(registry.changeSetHashAtBlock(BlockNumber32.wrap(999999)), registry.changeSetHash()); } function test_changeSetHashAtTx_uninitializedReturnsZero() public view { - assertEq(registry.changeSetHashAtTx(BlockNumber.wrap(999999), 0), bytes32(0)); + assertEq(registry.changeSetHashAtTx(BlockNumber32.wrap(999999), 0), bytes32(0)); } function test_changeSetHashAtOp_uninitializedReturnsZero() public view { - assertEq(registry.changeSetHashAtOp(BlockNumber.wrap(999999), 0, 0), bytes32(0)); + assertEq(registry.changeSetHashAtOp(BlockNumber32.wrap(999999), 0, 0), bytes32(0)); } // ========================================================================= @@ -84,26 +84,26 @@ contract ViewsTest is Test { // ========================================================================= function test_genesisBlock() public view { - assertEq(BlockNumber.unwrap(registry.genesisBlock()), BlockNumber.unwrap(deployBlock)); + assertEq(BlockNumber32.unwrap(registry.genesisBlock()), BlockNumber32.unwrap(deployBlock)); } function test_headBlock() public view { - assertEq(BlockNumber.unwrap(registry.headBlock()), BlockNumber.unwrap(deployBlock)); + assertEq(BlockNumber32.unwrap(registry.headBlock()), BlockNumber32.unwrap(deployBlock)); } function test_getBlockNode() public view { Entity.BlockNode memory node = registry.getBlockNode(deployBlock); assertEq(node.txCount, 1); // Deploy block is both genesis and head — no neighbours. - assertEq(BlockNumber.unwrap(node.prevBlock), 0); - assertEq(BlockNumber.unwrap(node.nextBlock), 0); + assertEq(BlockNumber32.unwrap(node.prevBlock), 0); + assertEq(BlockNumber32.unwrap(node.nextBlock), 0); } function test_getBlockNode_uninitializedReturnsZero() public view { - Entity.BlockNode memory node = registry.getBlockNode(BlockNumber.wrap(999999)); + Entity.BlockNode memory node = registry.getBlockNode(BlockNumber32.wrap(999999)); assertEq(node.txCount, 0); - assertEq(BlockNumber.unwrap(node.prevBlock), 0); - assertEq(BlockNumber.unwrap(node.nextBlock), 0); + assertEq(BlockNumber32.unwrap(node.prevBlock), 0); + assertEq(BlockNumber32.unwrap(node.nextBlock), 0); } // ========================================================================= @@ -115,7 +115,7 @@ contract ViewsTest is Test { } function test_txOpCount_uninitializedReturnsZero() public view { - assertEq(registry.txOpCount(BlockNumber.wrap(999999), 0), 0); + assertEq(registry.txOpCount(BlockNumber32.wrap(999999), 0), 0); } // ========================================================================= @@ -126,9 +126,9 @@ contract ViewsTest is Test { Entity.Commitment memory c = registry.commitment(testKey); assertEq(c.owner, alice); assertEq(c.creator, alice); - assertEq(BlockNumber.unwrap(c.createdAt), BlockNumber.unwrap(deployBlock)); - assertEq(BlockNumber.unwrap(c.updatedAt), BlockNumber.unwrap(deployBlock)); - assertEq(BlockNumber.unwrap(c.expiresAt), BlockNumber.unwrap(deployBlock + btl)); + assertEq(BlockNumber32.unwrap(c.createdAt), BlockNumber32.unwrap(deployBlock)); + assertEq(BlockNumber32.unwrap(c.updatedAt), BlockNumber32.unwrap(deployBlock)); + assertEq(BlockNumber32.unwrap(c.expiresAt), BlockNumber32.unwrap(deployBlock + btl)); assertTrue(c.coreHash != bytes32(0)); } diff --git a/test/unit/WrapEntityHash.t.sol b/test/unit/WrapEntityHash.t.sol index 1895a3b..285a35e 100644 --- a/test/unit/WrapEntityHash.t.sol +++ b/test/unit/WrapEntityHash.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Entity} from "../../contracts/Entity.sol"; import {EntityRegistry} from "../../contracts/EntityRegistry.sol"; @@ -11,7 +11,7 @@ import {EntityRegistry} from "../../contracts/EntityRegistry.sol"; contract WrapEntityHashTest is Test, EntityRegistry { address alice = makeAddr("alice"); - function doWrap(bytes32 coreHash_, address owner, BlockNumber updatedAt, BlockNumber expiresAt) + function doWrap(bytes32 coreHash_, address owner, BlockNumber32 updatedAt, BlockNumber32 expiresAt) external view returns (bytes32) @@ -25,8 +25,8 @@ contract WrapEntityHashTest is Test, EntityRegistry { function test_matchesManualDomainWrapping() public { bytes32 coreHash_ = keccak256("core"); - BlockNumber updatedAt = BlockNumber.wrap(100); - BlockNumber expiresAt = BlockNumber.wrap(500); + BlockNumber32 updatedAt = BlockNumber32.wrap(100); + BlockNumber32 expiresAt = BlockNumber32.wrap(500); bytes32 structHash = Entity.entityStructHash(coreHash_, alice, updatedAt, expiresAt); bytes32 expected = _hashTypedDataV4(structHash); @@ -40,8 +40,8 @@ contract WrapEntityHashTest is Test, EntityRegistry { function test_deterministic() public { bytes32 coreHash_ = keccak256("core"); - BlockNumber updatedAt = BlockNumber.wrap(100); - BlockNumber expiresAt = BlockNumber.wrap(500); + BlockNumber32 updatedAt = BlockNumber32.wrap(100); + BlockNumber32 expiresAt = BlockNumber32.wrap(500); assertEq( this.doWrap(coreHash_, alice, updatedAt, expiresAt), this.doWrap(coreHash_, alice, updatedAt, expiresAt) @@ -53,8 +53,8 @@ contract WrapEntityHashTest is Test, EntityRegistry { // ========================================================================= function test_differentCoreHash_differs() public { - BlockNumber updatedAt = BlockNumber.wrap(100); - BlockNumber expiresAt = BlockNumber.wrap(500); + BlockNumber32 updatedAt = BlockNumber32.wrap(100); + BlockNumber32 expiresAt = BlockNumber32.wrap(500); assertNotEq( this.doWrap(keccak256("a"), alice, updatedAt, expiresAt), @@ -64,8 +64,8 @@ contract WrapEntityHashTest is Test, EntityRegistry { function test_differentOwner_differs() public { bytes32 coreHash_ = keccak256("core"); - BlockNumber updatedAt = BlockNumber.wrap(100); - BlockNumber expiresAt = BlockNumber.wrap(500); + BlockNumber32 updatedAt = BlockNumber32.wrap(100); + BlockNumber32 expiresAt = BlockNumber32.wrap(500); address bob = makeAddr("bob"); assertNotEq( @@ -75,21 +75,21 @@ contract WrapEntityHashTest is Test, EntityRegistry { function test_differentUpdatedAt_differs() public { bytes32 coreHash_ = keccak256("core"); - BlockNumber expiresAt = BlockNumber.wrap(500); + BlockNumber32 expiresAt = BlockNumber32.wrap(500); assertNotEq( - this.doWrap(coreHash_, alice, BlockNumber.wrap(100), expiresAt), - this.doWrap(coreHash_, alice, BlockNumber.wrap(200), expiresAt) + this.doWrap(coreHash_, alice, BlockNumber32.wrap(100), expiresAt), + this.doWrap(coreHash_, alice, BlockNumber32.wrap(200), expiresAt) ); } function test_differentExpiresAt_differs() public { bytes32 coreHash_ = keccak256("core"); - BlockNumber updatedAt = BlockNumber.wrap(100); + BlockNumber32 updatedAt = BlockNumber32.wrap(100); assertNotEq( - this.doWrap(coreHash_, alice, updatedAt, BlockNumber.wrap(500)), - this.doWrap(coreHash_, alice, updatedAt, BlockNumber.wrap(600)) + this.doWrap(coreHash_, alice, updatedAt, BlockNumber32.wrap(500)), + this.doWrap(coreHash_, alice, updatedAt, BlockNumber32.wrap(600)) ); } @@ -98,8 +98,8 @@ contract WrapEntityHashTest is Test, EntityRegistry { // ========================================================================= function test_fuzz(bytes32 coreHash_, address owner, uint32 rawUpdatedAt, uint32 rawExpiresAt) public { - BlockNumber updatedAt = BlockNumber.wrap(rawUpdatedAt); - BlockNumber expiresAt = BlockNumber.wrap(rawExpiresAt); + BlockNumber32 updatedAt = BlockNumber32.wrap(rawUpdatedAt); + BlockNumber32 expiresAt = BlockNumber32.wrap(rawExpiresAt); bytes32 expected = _hashTypedDataV4(Entity.entityStructHash(coreHash_, owner, updatedAt, expiresAt)); assertEq(this.doWrap(coreHash_, owner, updatedAt, expiresAt), expected); diff --git a/test/unit/guards/RequireActive.t.sol b/test/unit/guards/RequireActive.t.sol index ba3d1cd..56c6b8e 100644 --- a/test/unit/guards/RequireActive.t.sol +++ b/test/unit/guards/RequireActive.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -11,22 +11,22 @@ import {encodeMime128} from "../../../contracts/types/Mime128.sol"; contract RequireActiveTest is Test, EntityRegistry { address alice = makeAddr("alice"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } - function doRequireActive(bytes32 key, BlockNumber current) external view { + function doRequireActive(bytes32 key, BlockNumber32 current) external view { Entity.Commitment storage c = _commitments[key]; Entity.requireActive(key, c, current); } function setUp() public { - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -35,20 +35,20 @@ contract RequireActiveTest is Test, EntityRegistry { } function test_beforeExpiry_succeeds() public view { - this.doRequireActive(testKey, BlockNumber.wrap(uint32(block.number))); + this.doRequireActive(testKey, BlockNumber32.wrap(uint32(block.number))); } function test_atExpiryBlock_reverts() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); - this.doRequireActive(testKey, BlockNumber.wrap(uint32(block.number))); + this.doRequireActive(testKey, BlockNumber32.wrap(uint32(block.number))); } function test_afterExpiry_reverts() public { - vm.roll(BlockNumber.unwrap(expiresAt) + 1); + vm.roll(BlockNumber32.unwrap(expiresAt) + 1); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); - this.doRequireActive(testKey, BlockNumber.wrap(uint32(block.number))); + this.doRequireActive(testKey, BlockNumber32.wrap(uint32(block.number))); } } diff --git a/test/unit/guards/RequireExists.t.sol b/test/unit/guards/RequireExists.t.sol index d27549f..b3d17fd 100644 --- a/test/unit/guards/RequireExists.t.sol +++ b/test/unit/guards/RequireExists.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -11,11 +11,11 @@ import {encodeMime128} from "../../../contracts/types/Mime128.sol"; contract RequireExistsTest is Test, EntityRegistry { address alice = makeAddr("alice"); - BlockNumber expiresAt; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doRequireExists(bytes32 key) external view { @@ -24,7 +24,7 @@ contract RequireExistsTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + BlockNumber32.wrap(1000); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); diff --git a/test/unit/guards/RequireExpired.t.sol b/test/unit/guards/RequireExpired.t.sol index 68d858c..bb8dc1c 100644 --- a/test/unit/guards/RequireExpired.t.sol +++ b/test/unit/guards/RequireExpired.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -12,22 +12,22 @@ contract RequireExpiredTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } - function doRequireExpired(bytes32 key, BlockNumber current) external view { + function doRequireExpired(bytes32 key, BlockNumber32 current) external view { Entity.Commitment storage c = _commitments[key]; Entity.requireExpired(key, c, current); } function setUp() public { - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -36,31 +36,31 @@ contract RequireExpiredTest is Test, EntityRegistry { } function test_atExactExpiryBlock_succeeds() public { - vm.roll(BlockNumber.unwrap(expiresAt)); - this.doRequireExpired(testKey, BlockNumber.wrap(uint32(block.number))); + vm.roll(BlockNumber32.unwrap(expiresAt)); + this.doRequireExpired(testKey, BlockNumber32.wrap(uint32(block.number))); } function test_afterExpiryBlock_succeeds() public { - vm.roll(BlockNumber.unwrap(expiresAt) + 100); - this.doRequireExpired(testKey, BlockNumber.wrap(uint32(block.number))); + vm.roll(BlockNumber32.unwrap(expiresAt) + 100); + this.doRequireExpired(testKey, BlockNumber32.wrap(uint32(block.number))); } function test_beforeExpiry_reverts() public { vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotExpired.selector, testKey, expiresAt)); - this.doRequireExpired(testKey, BlockNumber.wrap(uint32(block.number))); + this.doRequireExpired(testKey, BlockNumber32.wrap(uint32(block.number))); } function test_oneBlockBeforeExpiry_reverts() public { - vm.roll(BlockNumber.unwrap(expiresAt) - 1); + vm.roll(BlockNumber32.unwrap(expiresAt) - 1); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotExpired.selector, testKey, expiresAt)); - this.doRequireExpired(testKey, BlockNumber.wrap(uint32(block.number))); + this.doRequireExpired(testKey, BlockNumber32.wrap(uint32(block.number))); } function test_callableByNonOwner() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); vm.prank(bob); - this.doRequireExpired(testKey, BlockNumber.wrap(uint32(block.number))); + this.doRequireExpired(testKey, BlockNumber32.wrap(uint32(block.number))); } } diff --git a/test/unit/guards/RequireExpiryIncreased.t.sol b/test/unit/guards/RequireExpiryIncreased.t.sol index 6c48fa5..f55dcf6 100644 --- a/test/unit/guards/RequireExpiryIncreased.t.sol +++ b/test/unit/guards/RequireExpiryIncreased.t.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Entity} from "../../../contracts/Entity.sol"; import {EntityRegistry} from "../../../contracts/EntityRegistry.sol"; contract RequireExpiryIncreasedTest is Test, EntityRegistry { bytes32 constant KEY = keccak256("test-key"); - BlockNumber constant CURRENT = BlockNumber.wrap(1000); + BlockNumber32 constant CURRENT = BlockNumber32.wrap(1000); - function doRequireExpiryIncreased(bytes32 key, BlockNumber newExpiresAt, BlockNumber currentExpiresAt) + function doRequireExpiryIncreased(bytes32 key, BlockNumber32 newExpiresAt, BlockNumber32 currentExpiresAt) external pure { @@ -18,11 +18,11 @@ contract RequireExpiryIncreasedTest is Test, EntityRegistry { } function test_increased_succeeds() public view { - this.doRequireExpiryIncreased(KEY, BlockNumber.wrap(1500), CURRENT); + this.doRequireExpiryIncreased(KEY, BlockNumber32.wrap(1500), CURRENT); } function test_increasedByOne_succeeds() public view { - this.doRequireExpiryIncreased(KEY, BlockNumber.wrap(1001), CURRENT); + this.doRequireExpiryIncreased(KEY, BlockNumber32.wrap(1001), CURRENT); } function test_sameExpiry_reverts() public { @@ -31,7 +31,7 @@ contract RequireExpiryIncreasedTest is Test, EntityRegistry { } function test_decreased_reverts() public { - BlockNumber lower = BlockNumber.wrap(500); + BlockNumber32 lower = BlockNumber32.wrap(500); vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryNotExtended.selector, KEY, lower, CURRENT)); this.doRequireExpiryIncreased(KEY, lower, CURRENT); } diff --git a/test/unit/guards/RequireOwner.t.sol b/test/unit/guards/RequireOwner.t.sol index 7a3f287..ad01afa 100644 --- a/test/unit/guards/RequireOwner.t.sol +++ b/test/unit/guards/RequireOwner.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -12,11 +12,11 @@ contract RequireOwnerTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber expiresAt; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doRequireOwner(bytes32 key) external view { @@ -25,7 +25,7 @@ contract RequireOwnerTest is Test, EntityRegistry { } function setUp() public { - expiresAt = BlockNumber.wrap(uint32(block.number)) + BlockNumber.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + BlockNumber32.wrap(1000); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, expiresAt); diff --git a/test/unit/guards/RequirePositiveBtl.t.sol b/test/unit/guards/RequirePositiveBtl.t.sol index e060397..f65238b 100644 --- a/test/unit/guards/RequirePositiveBtl.t.sol +++ b/test/unit/guards/RequirePositiveBtl.t.sol @@ -1,26 +1,26 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Entity} from "../../../contracts/Entity.sol"; import {EntityRegistry} from "../../../contracts/EntityRegistry.sol"; contract RequirePositiveBtlTest is Test, EntityRegistry { - function doRequirePositiveBtl(BlockNumber btl) external pure { + function doRequirePositiveBtl(BlockNumber32 btl) external pure { Entity.requirePositiveBtl(btl); } function test_positiveBtl_succeeds() public view { - this.doRequirePositiveBtl(BlockNumber.wrap(1)); + this.doRequirePositiveBtl(BlockNumber32.wrap(1)); } function test_largeBtl_succeeds() public view { - this.doRequirePositiveBtl(BlockNumber.wrap(999999)); + this.doRequirePositiveBtl(BlockNumber32.wrap(999999)); } function test_zeroBtl_reverts() public { vm.expectRevert(Entity.ZeroBtl.selector); - this.doRequirePositiveBtl(BlockNumber.wrap(0)); + this.doRequirePositiveBtl(BlockNumber32.wrap(0)); } } diff --git a/test/unit/hashing/CoreHash.t.sol b/test/unit/hashing/CoreHash.t.sol index 5f82fcc..15b7b08 100644 --- a/test/unit/hashing/CoreHash.t.sol +++ b/test/unit/hashing/CoreHash.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Ident32} from "../../../contracts/types/Ident32.sol"; import {Mime128, encodeMime128} from "../../../contracts/types/Mime128.sol"; import {Test} from "forge-std/Test.sol"; @@ -24,7 +24,7 @@ contract CoreHashTest is Test, EntityRegistry { function hashCore( bytes32 key, address creator, - BlockNumber createdAt, + BlockNumber32 createdAt, Mime128 calldata contentType, bytes calldata payload, Entity.Attribute[] calldata attributes @@ -41,8 +41,8 @@ contract CoreHashTest is Test, EntityRegistry { Entity.Attribute[] memory attrs = new Entity.Attribute[](1); attrs[0] = Lib.uintAttr("count", 1); - bytes32 hashA = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); - bytes32 hashB = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); + bytes32 hashA = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); + bytes32 hashB = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); assertEq(hashA, hashB); } @@ -54,8 +54,8 @@ contract CoreHashTest is Test, EntityRegistry { function test_coreHash_differentKey_differs() public { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - bytes32 hashA = this.hashCore(keccak256("key1"), alice, BlockNumber.wrap(100), textPlain, "hello", attrs); - bytes32 hashB = this.hashCore(keccak256("key2"), alice, BlockNumber.wrap(100), textPlain, "hello", attrs); + bytes32 hashA = this.hashCore(keccak256("key1"), alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); + bytes32 hashB = this.hashCore(keccak256("key2"), alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); assertNotEq(hashA, hashB); } @@ -64,8 +64,8 @@ contract CoreHashTest is Test, EntityRegistry { bytes32 key = keccak256("key"); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - bytes32 hashA = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); - bytes32 hashB = this.hashCore(key, bob, BlockNumber.wrap(100), textPlain, "hello", attrs); + bytes32 hashA = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); + bytes32 hashB = this.hashCore(key, bob, BlockNumber32.wrap(100), textPlain, "hello", attrs); assertNotEq(hashA, hashB); } @@ -74,8 +74,8 @@ contract CoreHashTest is Test, EntityRegistry { bytes32 key = keccak256("key"); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - bytes32 hashA = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); - bytes32 hashB = this.hashCore(key, alice, BlockNumber.wrap(200), textPlain, "hello", attrs); + bytes32 hashA = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); + bytes32 hashB = this.hashCore(key, alice, BlockNumber32.wrap(200), textPlain, "hello", attrs); assertNotEq(hashA, hashB); } @@ -84,8 +84,8 @@ contract CoreHashTest is Test, EntityRegistry { bytes32 key = keccak256("key"); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - bytes32 hashA = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); - bytes32 hashB = this.hashCore(key, alice, BlockNumber.wrap(100), appJson, "hello", attrs); + bytes32 hashA = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); + bytes32 hashB = this.hashCore(key, alice, BlockNumber32.wrap(100), appJson, "hello", attrs); assertNotEq(hashA, hashB); } @@ -94,8 +94,8 @@ contract CoreHashTest is Test, EntityRegistry { bytes32 key = keccak256("key"); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - bytes32 hashA = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); - bytes32 hashB = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "world", attrs); + bytes32 hashA = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); + bytes32 hashB = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "world", attrs); assertNotEq(hashA, hashB); } @@ -109,8 +109,8 @@ contract CoreHashTest is Test, EntityRegistry { Entity.Attribute[] memory attrsB = new Entity.Attribute[](1); attrsB[0] = Lib.uintAttr("count", 2); - bytes32 hashA = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrsA); - bytes32 hashB = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrsB); + bytes32 hashA = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrsA); + bytes32 hashB = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrsB); assertNotEq(hashA, hashB); } @@ -122,8 +122,8 @@ contract CoreHashTest is Test, EntityRegistry { Entity.Attribute[] memory one = new Entity.Attribute[](1); one[0] = Lib.uintAttr("count", 1); - bytes32 hashA = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", empty); - bytes32 hashB = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", one); + bytes32 hashA = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", empty); + bytes32 hashB = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", one); assertNotEq(hashA, hashB); } @@ -140,7 +140,7 @@ contract CoreHashTest is Test, EntityRegistry { attrs[1] = Lib.uintAttr("aaa", 1); vm.expectRevert(Entity.AttributesNotSorted.selector); - this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); + this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); } // ------------------------------------------------------------------------- @@ -162,7 +162,7 @@ contract CoreHashTest is Test, EntityRegistry { } vm.expectRevert(abi.encodeWithSelector(Entity.TooManyAttributes.selector, count, 32)); - this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); + this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); } function test_coreHash_maxAttributes_succeeds() public { @@ -178,7 +178,7 @@ contract CoreHashTest is Test, EntityRegistry { attrs[i] = Entity.Attribute({name: Ident32.wrap(name), valueType: Entity.ATTR_UINT, value: v}); } - bytes32 hash = this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "hello", attrs); + bytes32 hash = this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "hello", attrs); assertTrue(hash != bytes32(0)); } @@ -188,7 +188,7 @@ contract CoreHashTest is Test, EntityRegistry { function test_coreHash_matchesManualEIP712Encoding() public { bytes32 key = keccak256("key"); - BlockNumber createdAt = BlockNumber.wrap(100); + BlockNumber32 createdAt = BlockNumber32.wrap(100); bytes memory payload = "hello"; Entity.Attribute[] memory attrs = new Entity.Attribute[](2); @@ -231,10 +231,12 @@ contract CoreHashTest is Test, EntityRegistry { keccak256(abi.encode(textPlain.data[0], textPlain.data[1], textPlain.data[2], textPlain.data[3])); bytes32 expected = keccak256( - abi.encode(Entity.CORE_HASH_TYPEHASH, key, alice, BlockNumber.wrap(100), ctHash, keccak256(""), bytes32(0)) + abi.encode( + Entity.CORE_HASH_TYPEHASH, key, alice, BlockNumber32.wrap(100), ctHash, keccak256(""), bytes32(0) + ) ); Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - assertEq(this.hashCore(key, alice, BlockNumber.wrap(100), textPlain, "", attrs), expected); + assertEq(this.hashCore(key, alice, BlockNumber32.wrap(100), textPlain, "", attrs), expected); } } diff --git a/test/unit/hashing/EntityStructHash.t.sol b/test/unit/hashing/EntityStructHash.t.sol index d800aba..f7951ea 100644 --- a/test/unit/hashing/EntityStructHash.t.sol +++ b/test/unit/hashing/EntityStructHash.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -17,8 +17,8 @@ contract EntityStructHashTest is Test { bytes32 core = keccak256("core"); // WHEN computing entityStructHash twice - bytes32 hashA = Entity.entityStructHash(core, alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); - bytes32 hashB = Entity.entityStructHash(core, alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); + bytes32 hashA = Entity.entityStructHash(core, alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); + bytes32 hashB = Entity.entityStructHash(core, alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); // THEN the hashes are equal assertEq(hashA, hashB); @@ -30,8 +30,10 @@ contract EntityStructHashTest is Test { function test_entityStructHash_differentCoreHash_differs() public view { // GIVEN two calls differing only in coreHash - bytes32 hashA = Entity.entityStructHash(keccak256("core1"), alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); - bytes32 hashB = Entity.entityStructHash(keccak256("core2"), alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); + bytes32 hashA = + Entity.entityStructHash(keccak256("core1"), alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); + bytes32 hashB = + Entity.entityStructHash(keccak256("core2"), alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); // THEN the hashes differ assertNotEq(hashA, hashB); @@ -41,8 +43,8 @@ contract EntityStructHashTest is Test { // GIVEN two calls differing only in owner bytes32 core = keccak256("core"); - bytes32 hashA = Entity.entityStructHash(core, alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); - bytes32 hashB = Entity.entityStructHash(core, bob, BlockNumber.wrap(100), BlockNumber.wrap(200)); + bytes32 hashA = Entity.entityStructHash(core, alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); + bytes32 hashB = Entity.entityStructHash(core, bob, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); // THEN the hashes differ assertNotEq(hashA, hashB); @@ -52,8 +54,8 @@ contract EntityStructHashTest is Test { // GIVEN two calls differing only in updatedAt bytes32 core = keccak256("core"); - bytes32 hashA = Entity.entityStructHash(core, alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); - bytes32 hashB = Entity.entityStructHash(core, alice, BlockNumber.wrap(150), BlockNumber.wrap(200)); + bytes32 hashA = Entity.entityStructHash(core, alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); + bytes32 hashB = Entity.entityStructHash(core, alice, BlockNumber32.wrap(150), BlockNumber32.wrap(200)); // THEN the hashes differ assertNotEq(hashA, hashB); @@ -63,8 +65,8 @@ contract EntityStructHashTest is Test { // GIVEN two calls differing only in expiresAt bytes32 core = keccak256("core"); - bytes32 hashA = Entity.entityStructHash(core, alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); - bytes32 hashB = Entity.entityStructHash(core, alice, BlockNumber.wrap(100), BlockNumber.wrap(300)); + bytes32 hashA = Entity.entityStructHash(core, alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); + bytes32 hashB = Entity.entityStructHash(core, alice, BlockNumber32.wrap(100), BlockNumber32.wrap(300)); // THEN the hashes differ assertNotEq(hashA, hashB); @@ -77,8 +79,8 @@ contract EntityStructHashTest is Test { function test_entityStructHash_matchesManualEIP712Encoding() public view { // GIVEN inputs bytes32 core = keccak256("core"); - BlockNumber updatedAt = BlockNumber.wrap(100); - BlockNumber expiresAt = BlockNumber.wrap(200); + BlockNumber32 updatedAt = BlockNumber32.wrap(100); + BlockNumber32 expiresAt = BlockNumber32.wrap(200); // WHEN computing manually per EIP-712 bytes32 expected = keccak256(abi.encode(Entity.ENTITY_HASH_TYPEHASH, core, alice, updatedAt, expiresAt)); @@ -96,8 +98,8 @@ contract EntityStructHashTest is Test { bytes32 core = keccak256("core"); // WHEN computing entityStructHash with different owners - bytes32 hashAlice = Entity.entityStructHash(core, alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); - bytes32 hashBob = Entity.entityStructHash(core, bob, BlockNumber.wrap(100), BlockNumber.wrap(200)); + bytes32 hashAlice = Entity.entityStructHash(core, alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); + bytes32 hashBob = Entity.entityStructHash(core, bob, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); // THEN the struct hashes differ (owner is in the outer hash) assertNotEq(hashAlice, hashBob); @@ -108,8 +110,8 @@ contract EntityStructHashTest is Test { bytes32 core = keccak256("core"); // WHEN computing entityStructHash with different expiry (simulating extend) - bytes32 hashOriginal = Entity.entityStructHash(core, alice, BlockNumber.wrap(100), BlockNumber.wrap(200)); - bytes32 hashExtended = Entity.entityStructHash(core, alice, BlockNumber.wrap(150), BlockNumber.wrap(300)); + bytes32 hashOriginal = Entity.entityStructHash(core, alice, BlockNumber32.wrap(100), BlockNumber32.wrap(200)); + bytes32 hashExtended = Entity.entityStructHash(core, alice, BlockNumber32.wrap(150), BlockNumber32.wrap(300)); // THEN the struct hashes differ assertNotEq(hashOriginal, hashExtended); @@ -124,8 +126,8 @@ contract EntityStructHashTest is Test { pure { // GIVEN arbitrary inputs - BlockNumber updatedAt = BlockNumber.wrap(rawUpdatedAt); - BlockNumber expiresAt = BlockNumber.wrap(rawExpiresAt); + BlockNumber32 updatedAt = BlockNumber32.wrap(rawUpdatedAt); + BlockNumber32 expiresAt = BlockNumber32.wrap(rawExpiresAt); // WHEN computing via the assembly implementation bytes32 actual = Entity.entityStructHash(coreHash_, owner, updatedAt, expiresAt); diff --git a/test/unit/hashing/Pack.t.sol b/test/unit/hashing/Pack.t.sol index b3cf2e3..4366f85 100644 --- a/test/unit/hashing/Pack.t.sol +++ b/test/unit/hashing/Pack.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test} from "forge-std/Test.sol"; import {Entity, OperationKey, TransactionKey} from "../../../contracts/Entity.sol"; @@ -19,8 +19,8 @@ contract PackTest is Test { // WHEN packing twice // THEN the results are equal assertEq( - OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(1), 2, 3)), - OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(1), 2, 3)) + OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(1), 2, 3)), + OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(1), 2, 3)) ); } @@ -30,22 +30,22 @@ contract PackTest is Test { function test_operationKey_differentBlock_differs() public pure { assertNotEq( - OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(1), 1, 1)), - OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(2), 1, 1)) + OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(1), 1, 1)), + OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(2), 1, 1)) ); } function test_operationKey_differentTx_differs() public pure { assertNotEq( - OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(1), 1, 1)), - OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(1), 2, 1)) + OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(1), 1, 1)), + OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(1), 2, 1)) ); } function test_operationKey_differentOp_differs() public pure { assertNotEq( - OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(1), 1, 1)), - OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(1), 1, 2)) + OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(1), 1, 1)), + OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(1), 1, 2)) ); } @@ -55,7 +55,7 @@ contract PackTest is Test { function test_operationKey_layout() public pure { // GIVEN known inputs - BlockNumber blockNumber = BlockNumber.wrap(0xAB); + BlockNumber32 blockNumber = BlockNumber32.wrap(0xAB); uint32 txSeq = 0xCD; uint32 opSeq = 0xEF; @@ -70,7 +70,7 @@ contract PackTest is Test { function test_operationKey_zeroInputs() public pure { // GIVEN all zeros // THEN the packed key is zero - assertEq(OperationKey.unwrap(Entity.operationKey(BlockNumber.wrap(0), 0, 0)), 0); + assertEq(OperationKey.unwrap(Entity.operationKey(BlockNumber32.wrap(0), 0, 0)), 0); } // ------------------------------------------------------------------------- @@ -79,7 +79,7 @@ contract PackTest is Test { function test_operationKey_fuzz(uint32 rawBlock, uint32 txSeq, uint32 opSeq) public pure { // GIVEN arbitrary inputs - BlockNumber blockNumber = BlockNumber.wrap(rawBlock); + BlockNumber32 blockNumber = BlockNumber32.wrap(rawBlock); // WHEN packing via the library uint256 actual = OperationKey.unwrap(Entity.operationKey(blockNumber, txSeq, opSeq)); @@ -95,7 +95,7 @@ contract PackTest is Test { function test_operationKey_extendsTransactionKey(uint32 rawBlock, uint32 txSeq, uint32 opSeq) public pure { // GIVEN an operationKey and its corresponding transactionKey - BlockNumber blockNumber = BlockNumber.wrap(rawBlock); + BlockNumber32 blockNumber = BlockNumber32.wrap(rawBlock); uint256 ok = OperationKey.unwrap(Entity.operationKey(blockNumber, txSeq, opSeq)); uint256 tk = TransactionKey.unwrap(Entity.transactionKey(blockNumber, txSeq)); @@ -116,8 +116,8 @@ contract PackTest is Test { // WHEN packing twice // THEN the results are equal assertEq( - TransactionKey.unwrap(Entity.transactionKey(BlockNumber.wrap(1), 2)), - TransactionKey.unwrap(Entity.transactionKey(BlockNumber.wrap(1), 2)) + TransactionKey.unwrap(Entity.transactionKey(BlockNumber32.wrap(1), 2)), + TransactionKey.unwrap(Entity.transactionKey(BlockNumber32.wrap(1), 2)) ); } @@ -127,15 +127,15 @@ contract PackTest is Test { function test_transactionKey_differentBlock_differs() public pure { assertNotEq( - TransactionKey.unwrap(Entity.transactionKey(BlockNumber.wrap(1), 1)), - TransactionKey.unwrap(Entity.transactionKey(BlockNumber.wrap(2), 1)) + TransactionKey.unwrap(Entity.transactionKey(BlockNumber32.wrap(1), 1)), + TransactionKey.unwrap(Entity.transactionKey(BlockNumber32.wrap(2), 1)) ); } function test_transactionKey_differentTx_differs() public pure { assertNotEq( - TransactionKey.unwrap(Entity.transactionKey(BlockNumber.wrap(1), 1)), - TransactionKey.unwrap(Entity.transactionKey(BlockNumber.wrap(1), 2)) + TransactionKey.unwrap(Entity.transactionKey(BlockNumber32.wrap(1), 1)), + TransactionKey.unwrap(Entity.transactionKey(BlockNumber32.wrap(1), 2)) ); } @@ -145,7 +145,7 @@ contract PackTest is Test { function test_transactionKey_layout() public pure { // GIVEN known inputs - BlockNumber blockNumber = BlockNumber.wrap(0xAB); + BlockNumber32 blockNumber = BlockNumber32.wrap(0xAB); uint32 txSeq = 0xCD; // WHEN packing @@ -159,7 +159,7 @@ contract PackTest is Test { function test_transactionKey_zeroInputs() public pure { // GIVEN all zeros // THEN the packed key is zero - assertEq(TransactionKey.unwrap(Entity.transactionKey(BlockNumber.wrap(0), 0)), 0); + assertEq(TransactionKey.unwrap(Entity.transactionKey(BlockNumber32.wrap(0), 0)), 0); } // ------------------------------------------------------------------------- @@ -168,7 +168,7 @@ contract PackTest is Test { function test_transactionKey_fuzz(uint32 rawBlock, uint32 txSeq) public pure { // GIVEN arbitrary inputs - BlockNumber blockNumber = BlockNumber.wrap(rawBlock); + BlockNumber32 blockNumber = BlockNumber32.wrap(rawBlock); // WHEN packing via the library uint256 actual = TransactionKey.unwrap(Entity.transactionKey(blockNumber, txSeq)); diff --git a/test/unit/ops/Create.t.sol b/test/unit/ops/Create.t.sol index 0f92771..d24d495 100644 --- a/test/unit/ops/Create.t.sol +++ b/test/unit/ops/Create.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test, Vm} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -14,7 +14,7 @@ contract CreateTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber btl; + BlockNumber32 btl; bytes32 constant STUB_KEY = keccak256("stub-entity-key"); bytes32 constant STUB_CORE_HASH = keccak256("stub-core-hash"); @@ -27,21 +27,21 @@ contract CreateTest is Test, EntityRegistry { function _computeEntityHash( bytes32, address, - BlockNumber, + BlockNumber32, address, - BlockNumber, - BlockNumber, + BlockNumber32, + BlockNumber32, Entity.Operation calldata ) internal pure override returns (bytes32, bytes32) { return (STUB_CORE_HASH, STUB_ENTITY_HASH); } function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function setUp() public { - btl = BlockNumber.wrap(1000); + btl = BlockNumber32.wrap(1000); } function _defaultOp() internal view returns (Entity.Operation memory) { @@ -55,7 +55,7 @@ contract CreateTest is Test, EntityRegistry { function test_create_zeroBtl_reverts() public { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, BlockNumber.wrap(0)); + Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, BlockNumber32.wrap(0)); vm.prank(alice); vm.expectRevert(Entity.ZeroBtl.selector); @@ -64,7 +64,7 @@ contract CreateTest is Test, EntityRegistry { function test_create_btlOne_succeeds() public { Entity.Attribute[] memory attrs = new Entity.Attribute[](0); - Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, BlockNumber.wrap(1)); + Entity.Operation memory op = Lib.createOp("hello", encodeMime128("text/plain"), attrs, BlockNumber32.wrap(1)); vm.prank(alice); (bytes32 key,) = this.doCreate(op); @@ -84,9 +84,9 @@ contract CreateTest is Test, EntityRegistry { Entity.Commitment memory c = commitment(STUB_KEY); assertEq(c.creator, alice); assertEq(c.owner, alice); - assertEq(BlockNumber.unwrap(c.createdAt), uint32(block.number)); - assertEq(BlockNumber.unwrap(c.updatedAt), uint32(block.number)); - assertEq(BlockNumber.unwrap(c.expiresAt), uint32(block.number) + BlockNumber.unwrap(btl)); + assertEq(BlockNumber32.unwrap(c.createdAt), uint32(block.number)); + assertEq(BlockNumber32.unwrap(c.updatedAt), uint32(block.number)); + assertEq(BlockNumber32.unwrap(c.expiresAt), uint32(block.number) + BlockNumber32.unwrap(btl)); assertEq(c.coreHash, STUB_CORE_HASH); } @@ -107,8 +107,8 @@ contract CreateTest is Test, EntityRegistry { assertEq(logs[0].topics[1], STUB_KEY); assertEq(logs[0].topics[2], bytes32(uint256(Entity.CREATE))); assertEq(logs[0].topics[3], bytes32(uint256(uint160(alice)))); - (BlockNumber emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber, bytes32)); - assertEq(BlockNumber.unwrap(emittedExpiry), uint32(block.number) + BlockNumber.unwrap(btl)); + (BlockNumber32 emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber32, bytes32)); + assertEq(BlockNumber32.unwrap(emittedExpiry), uint32(block.number) + BlockNumber32.unwrap(btl)); assertEq(emittedHash, STUB_ENTITY_HASH); } diff --git a/test/unit/ops/Delete.t.sol b/test/unit/ops/Delete.t.sol index 07bd523..e4e59a2 100644 --- a/test/unit/ops/Delete.t.sol +++ b/test/unit/ops/Delete.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test, Vm} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -12,21 +12,21 @@ contract DeleteTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doDelete(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _delete(op, BlockNumber.wrap(uint32(block.number))); + return _delete(op, BlockNumber32.wrap(uint32(block.number))); } function setUp() public { - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -48,9 +48,9 @@ contract DeleteTest is Test, EntityRegistry { assertEq(c.creator, address(0)); assertEq(c.owner, address(0)); assertEq(c.coreHash, bytes32(0)); - assertEq(BlockNumber.unwrap(c.createdAt), 0); - assertEq(BlockNumber.unwrap(c.updatedAt), 0); - assertEq(BlockNumber.unwrap(c.expiresAt), 0); + assertEq(BlockNumber32.unwrap(c.createdAt), 0); + assertEq(BlockNumber32.unwrap(c.updatedAt), 0); + assertEq(BlockNumber32.unwrap(c.expiresAt), 0); } // ========================================================================= @@ -98,8 +98,8 @@ contract DeleteTest is Test, EntityRegistry { assertEq(logs[0].topics[1], testKey); assertEq(logs[0].topics[2], bytes32(uint256(Entity.DELETE))); assertEq(logs[0].topics[3], bytes32(uint256(uint160(alice)))); - (BlockNumber emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber, bytes32)); - assertEq(BlockNumber.unwrap(emittedExpiry), BlockNumber.unwrap(expiresAt)); + (BlockNumber32 emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber32, bytes32)); + assertEq(BlockNumber32.unwrap(emittedExpiry), BlockNumber32.unwrap(expiresAt)); assertEq(emittedHash, entityHash_); } @@ -115,7 +115,7 @@ contract DeleteTest is Test, EntityRegistry { } function test_delete_revertsIfExpired() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); Entity.Operation memory op = Lib.deleteOp(testKey); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); diff --git a/test/unit/ops/Expire.t.sol b/test/unit/ops/Expire.t.sol index 5c31cad..e3b9688 100644 --- a/test/unit/ops/Expire.t.sol +++ b/test/unit/ops/Expire.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test, Vm} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -12,21 +12,21 @@ contract ExpireTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doExpire(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _expire(op, BlockNumber.wrap(uint32(block.number))); + return _expire(op, BlockNumber32.wrap(uint32(block.number))); } function setUp() public { - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -39,16 +39,16 @@ contract ExpireTest is Test, EntityRegistry { // ========================================================================= function test_expire_removesCommitment() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); this.doExpire(Lib.expireOp(testKey)); Entity.Commitment memory c = commitment(testKey); assertEq(c.creator, address(0)); assertEq(c.owner, address(0)); assertEq(c.coreHash, bytes32(0)); - assertEq(BlockNumber.unwrap(c.createdAt), 0); - assertEq(BlockNumber.unwrap(c.updatedAt), 0); - assertEq(BlockNumber.unwrap(c.expiresAt), 0); + assertEq(BlockNumber32.unwrap(c.createdAt), 0); + assertEq(BlockNumber32.unwrap(c.updatedAt), 0); + assertEq(BlockNumber32.unwrap(c.expiresAt), 0); } // ========================================================================= @@ -56,7 +56,7 @@ contract ExpireTest is Test, EntityRegistry { // ========================================================================= function test_expire_returnsEntityKey() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); (bytes32 returnedKey,) = this.doExpire(Lib.expireOp(testKey)); assertEq(returnedKey, testKey); } @@ -69,7 +69,7 @@ contract ExpireTest is Test, EntityRegistry { Entity.Commitment memory c = commitment(testKey); bytes32 expected = _wrapEntityHash(c.coreHash, c.owner, c.updatedAt, c.expiresAt); - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); (, bytes32 entityHash_) = this.doExpire(Lib.expireOp(testKey)); assertEq(entityHash_, expected); } @@ -79,7 +79,7 @@ contract ExpireTest is Test, EntityRegistry { // ========================================================================= function test_expire_emitsEntityOperation() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); vm.recordLogs(); (, bytes32 entityHash_) = this.doExpire(Lib.expireOp(testKey)); @@ -89,8 +89,8 @@ contract ExpireTest is Test, EntityRegistry { assertEq(logs[0].topics[1], testKey); assertEq(logs[0].topics[2], bytes32(uint256(Entity.EXPIRE))); assertEq(logs[0].topics[3], bytes32(uint256(uint160(alice)))); - (BlockNumber emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber, bytes32)); - assertEq(BlockNumber.unwrap(emittedExpiry), BlockNumber.unwrap(expiresAt)); + (BlockNumber32 emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber32, bytes32)); + assertEq(BlockNumber32.unwrap(emittedExpiry), BlockNumber32.unwrap(expiresAt)); assertEq(emittedHash, entityHash_); } @@ -100,7 +100,7 @@ contract ExpireTest is Test, EntityRegistry { function test_expire_revertsIfNotFound() public { bytes32 bogus = keccak256("bogus"); - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotFound.selector, bogus)); this.doExpire(Lib.expireOp(bogus)); } diff --git a/test/unit/ops/Extend.t.sol b/test/unit/ops/Extend.t.sol index e98d262..0656479 100644 --- a/test/unit/ops/Extend.t.sol +++ b/test/unit/ops/Extend.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test, Vm} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -12,21 +12,21 @@ contract ExtendTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doExtend(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _extend(op, BlockNumber.wrap(uint32(block.number))); + return _extend(op, BlockNumber32.wrap(uint32(block.number))); } function setUp() public { - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -40,7 +40,7 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_sameExpiry_reverts() public { // btl that lands on the already-stored expiresAt: expiresAt - current - Entity.Operation memory op = Lib.extendOp(testKey, expiresAt - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op = Lib.extendOp(testKey, expiresAt - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryNotExtended.selector, testKey, expiresAt, expiresAt)); @@ -49,8 +49,8 @@ contract ExtendTest is Test, EntityRegistry { function test_extend_lowerExpiry_reverts() public { // absolute target lower than current stored expiresAt - BlockNumber lower = expiresAt - BlockNumber.wrap(100); - Entity.Operation memory op = Lib.extendOp(testKey, lower - BlockNumber.wrap(uint32(block.number))); + BlockNumber32 lower = expiresAt - BlockNumber32.wrap(100); + Entity.Operation memory op = Lib.extendOp(testKey, lower - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.ExpiryNotExtended.selector, testKey, lower, expiresAt)); @@ -62,34 +62,34 @@ contract ExtendTest is Test, EntityRegistry { // ========================================================================= function test_extend_updatesExpiresAt() public { - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); Entity.Commitment memory c = commitment(testKey); - assertEq(BlockNumber.unwrap(c.expiresAt), BlockNumber.unwrap(newExpiry)); + assertEq(BlockNumber32.unwrap(c.expiresAt), BlockNumber32.unwrap(newExpiry)); } function test_extend_updatesUpdatedAt() public { vm.roll(block.number + 10); - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); Entity.Commitment memory c = commitment(testKey); - assertEq(BlockNumber.unwrap(c.updatedAt), uint32(block.number)); + assertEq(BlockNumber32.unwrap(c.updatedAt), uint32(block.number)); } function test_extend_preservesCoreHashAndOwner() public { Entity.Commitment memory before_ = commitment(testKey); - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); this.doExtend(op); @@ -98,7 +98,7 @@ contract ExtendTest is Test, EntityRegistry { assertEq(after_.coreHash, before_.coreHash); assertEq(after_.creator, before_.creator); assertEq(after_.owner, before_.owner); - assertEq(BlockNumber.unwrap(after_.createdAt), BlockNumber.unwrap(before_.createdAt)); + assertEq(BlockNumber32.unwrap(after_.createdAt), BlockNumber32.unwrap(before_.createdAt)); } // ========================================================================= @@ -106,8 +106,8 @@ contract ExtendTest is Test, EntityRegistry { // ========================================================================= function test_extend_returnsEntityKey() public { - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); (bytes32 returnedKey,) = this.doExtend(op); @@ -120,8 +120,8 @@ contract ExtendTest is Test, EntityRegistry { // ========================================================================= function test_extend_entityHashUsesNewExpiry() public { - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 entityHash_) = this.doExtend(op); @@ -132,14 +132,14 @@ contract ExtendTest is Test, EntityRegistry { } function test_extend_differentExpiry_differentEntityHash() public { - BlockNumber expiry1 = expiresAt + BlockNumber.wrap(100); - BlockNumber expiry2 = expiresAt + BlockNumber.wrap(200); + BlockNumber32 expiry1 = expiresAt + BlockNumber32.wrap(100); + BlockNumber32 expiry2 = expiresAt + BlockNumber32.wrap(200); - Entity.Operation memory op1 = Lib.extendOp(testKey, expiry1 - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op1 = Lib.extendOp(testKey, expiry1 - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 hash1) = this.doExtend(op1); - Entity.Operation memory op2 = Lib.extendOp(testKey, expiry2 - BlockNumber.wrap(uint32(block.number))); + Entity.Operation memory op2 = Lib.extendOp(testKey, expiry2 - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); (, bytes32 hash2) = this.doExtend(op2); @@ -151,8 +151,8 @@ contract ExtendTest is Test, EntityRegistry { // ========================================================================= function test_extend_emitsEntityOperation() public { - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); vm.recordLogs(); @@ -164,8 +164,8 @@ contract ExtendTest is Test, EntityRegistry { assertEq(logs[0].topics[1], testKey); assertEq(logs[0].topics[2], bytes32(uint256(Entity.EXTEND))); assertEq(logs[0].topics[3], bytes32(uint256(uint160(alice)))); - (BlockNumber emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber, bytes32)); - assertEq(BlockNumber.unwrap(emittedExpiry), BlockNumber.unwrap(newExpiry)); + (BlockNumber32 emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber32, bytes32)); + assertEq(BlockNumber32.unwrap(emittedExpiry), BlockNumber32.unwrap(newExpiry)); assertEq(emittedHash, entityHash_); } @@ -174,26 +174,26 @@ contract ExtendTest is Test, EntityRegistry { // ========================================================================= function test_extend_revertsIfNotFound() public { - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); Entity.Operation memory op = - Lib.extendOp(keccak256("bogus"), newExpiry - BlockNumber.wrap(uint32(block.number))); + Lib.extendOp(keccak256("bogus"), newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityNotFound.selector, keccak256("bogus"))); this.doExtend(op); } function test_extend_revertsIfExpired() public { - vm.roll(BlockNumber.unwrap(expiresAt)); - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + vm.roll(BlockNumber32.unwrap(expiresAt)); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); this.doExtend(op); } function test_extend_revertsIfNotOwner() public { - BlockNumber newExpiry = expiresAt + BlockNumber.wrap(500); - Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber.wrap(uint32(block.number))); + BlockNumber32 newExpiry = expiresAt + BlockNumber32.wrap(500); + Entity.Operation memory op = Lib.extendOp(testKey, newExpiry - BlockNumber32.wrap(uint32(block.number))); vm.prank(bob); vm.expectRevert(abi.encodeWithSelector(Entity.NotOwner.selector, testKey, bob, alice)); this.doExtend(op); diff --git a/test/unit/ops/Transfer.t.sol b/test/unit/ops/Transfer.t.sol index 58278c7..eb60b85 100644 --- a/test/unit/ops/Transfer.t.sol +++ b/test/unit/ops/Transfer.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test, Vm} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -13,21 +13,21 @@ contract TransferTest is Test, EntityRegistry { address bob = makeAddr("bob"); address charlie = makeAddr("charlie"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doTransfer(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _transfer(op, BlockNumber.wrap(uint32(block.number))); + return _transfer(op, BlockNumber32.wrap(uint32(block.number))); } function setUp() public { - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; Entity.Attribute[] memory attrs = new Entity.Attribute[](0); Entity.Operation memory createOp = Lib.createOp("hello", encodeMime128("text/plain"), attrs, btl); @@ -77,7 +77,7 @@ contract TransferTest is Test, EntityRegistry { this.doTransfer(op); Entity.Commitment memory c = commitment(testKey); - assertEq(BlockNumber.unwrap(c.updatedAt), uint32(block.number)); + assertEq(BlockNumber32.unwrap(c.updatedAt), uint32(block.number)); } function test_transfer_preservesCoreHashCreatorExpiry() public { @@ -90,8 +90,8 @@ contract TransferTest is Test, EntityRegistry { Entity.Commitment memory after_ = commitment(testKey); assertEq(after_.coreHash, before_.coreHash); assertEq(after_.creator, before_.creator); - assertEq(BlockNumber.unwrap(after_.createdAt), BlockNumber.unwrap(before_.createdAt)); - assertEq(BlockNumber.unwrap(after_.expiresAt), BlockNumber.unwrap(before_.expiresAt)); + assertEq(BlockNumber32.unwrap(after_.createdAt), BlockNumber32.unwrap(before_.createdAt)); + assertEq(BlockNumber32.unwrap(after_.expiresAt), BlockNumber32.unwrap(before_.expiresAt)); } // ========================================================================= @@ -172,8 +172,8 @@ contract TransferTest is Test, EntityRegistry { assertEq(logs[0].topics[1], testKey); assertEq(logs[0].topics[2], bytes32(uint256(Entity.TRANSFER))); assertEq(logs[0].topics[3], bytes32(uint256(uint160(bob)))); - (BlockNumber emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber, bytes32)); - assertEq(BlockNumber.unwrap(emittedExpiry), BlockNumber.unwrap(expiresAt)); + (BlockNumber32 emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber32, bytes32)); + assertEq(BlockNumber32.unwrap(emittedExpiry), BlockNumber32.unwrap(expiresAt)); assertEq(emittedHash, entityHash_); } @@ -189,7 +189,7 @@ contract TransferTest is Test, EntityRegistry { } function test_transfer_revertsIfExpired() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); Entity.Operation memory op = Lib.transferOp(testKey, bob); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); diff --git a/test/unit/ops/Update.t.sol b/test/unit/ops/Update.t.sol index e5f59d8..618a6b4 100644 --- a/test/unit/ops/Update.t.sol +++ b/test/unit/ops/Update.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../../contracts/types/BlockNumber32.sol"; import {Test, Vm} from "forge-std/Test.sol"; import {Lib} from "../../utils/Lib.sol"; import {Entity} from "../../../contracts/Entity.sol"; @@ -12,8 +12,8 @@ contract UpdateTest is Test, EntityRegistry { address alice = makeAddr("alice"); address bob = makeAddr("bob"); - BlockNumber btl; - BlockNumber expiresAt; + BlockNumber32 btl; + BlockNumber32 expiresAt; bytes32 testKey; Mime128 textPlain; @@ -21,17 +21,17 @@ contract UpdateTest is Test, EntityRegistry { // Calldata wrappers. function doCreate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _create(op, BlockNumber.wrap(uint32(block.number))); + return _create(op, BlockNumber32.wrap(uint32(block.number))); } function doUpdate(Entity.Operation calldata op) external returns (bytes32, bytes32) { - return _update(op, BlockNumber.wrap(uint32(block.number))); + return _update(op, BlockNumber32.wrap(uint32(block.number))); } function hashCore( bytes32 key, address creator, - BlockNumber createdAt, + BlockNumber32 createdAt, Mime128 calldata contentType, bytes calldata payload, Entity.Attribute[] calldata attributes @@ -43,8 +43,8 @@ contract UpdateTest is Test, EntityRegistry { textPlain = encodeMime128("text/plain"); appJson = encodeMime128("application/json"); - btl = BlockNumber.wrap(1000); - expiresAt = BlockNumber.wrap(uint32(block.number)) + btl; + btl = BlockNumber32.wrap(1000); + expiresAt = BlockNumber32.wrap(uint32(block.number)) + btl; // Create an entity owned by alice. Entity.Attribute[] memory attrs = new Entity.Attribute[](0); @@ -85,7 +85,7 @@ contract UpdateTest is Test, EntityRegistry { this.doUpdate(op); Entity.Commitment memory after_ = commitment(testKey); - assertEq(BlockNumber.unwrap(after_.updatedAt), uint32(block.number)); + assertEq(BlockNumber32.unwrap(after_.updatedAt), uint32(block.number)); } function test_update_preservesImmutableFields() public { @@ -98,8 +98,8 @@ contract UpdateTest is Test, EntityRegistry { Entity.Commitment memory after_ = commitment(testKey); assertEq(after_.creator, before_.creator); assertEq(after_.owner, before_.owner); - assertEq(BlockNumber.unwrap(after_.createdAt), BlockNumber.unwrap(before_.createdAt)); - assertEq(BlockNumber.unwrap(after_.expiresAt), BlockNumber.unwrap(before_.expiresAt)); + assertEq(BlockNumber32.unwrap(after_.createdAt), BlockNumber32.unwrap(before_.createdAt)); + assertEq(BlockNumber32.unwrap(after_.expiresAt), BlockNumber32.unwrap(before_.expiresAt)); } // ========================================================================= @@ -159,8 +159,8 @@ contract UpdateTest is Test, EntityRegistry { assertEq(logs[0].topics[1], testKey); assertEq(logs[0].topics[2], bytes32(uint256(Entity.UPDATE))); assertEq(logs[0].topics[3], bytes32(uint256(uint160(alice)))); - (BlockNumber emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber, bytes32)); - assertEq(BlockNumber.unwrap(emittedExpiry), BlockNumber.unwrap(expiresAt)); + (BlockNumber32 emittedExpiry, bytes32 emittedHash) = abi.decode(logs[0].data, (BlockNumber32, bytes32)); + assertEq(BlockNumber32.unwrap(emittedExpiry), BlockNumber32.unwrap(expiresAt)); assertEq(emittedHash, entityHash_); } @@ -226,7 +226,7 @@ contract UpdateTest is Test, EntityRegistry { } function test_update_revertsIfExpired() public { - vm.roll(BlockNumber.unwrap(expiresAt)); + vm.roll(BlockNumber32.unwrap(expiresAt)); Entity.Operation memory op = _simpleUpdateOp(); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(Entity.EntityExpired.selector, testKey, expiresAt)); diff --git a/test/utils/Lib.sol b/test/utils/Lib.sol index b7ad6d4..f2976f7 100644 --- a/test/utils/Lib.sol +++ b/test/utils/Lib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {BlockNumber} from "../../contracts/types/BlockNumber.sol"; +import {BlockNumber32} from "../../contracts/types/BlockNumber32.sol"; import {Entity} from "../../contracts/Entity.sol"; import {Ident32, encodeIdent32} from "../../contracts/types/Ident32.sol"; import {Mime128} from "../../contracts/types/Mime128.sol"; @@ -16,7 +16,7 @@ library Lib { bytes memory payload_, Mime128 memory contentType_, Entity.Attribute[] memory attributes_, - BlockNumber btl_ + BlockNumber32 btl_ ) internal pure returns (Entity.Operation memory) { return Entity.Operation({ operationType: Entity.CREATE, @@ -41,7 +41,7 @@ library Lib { payload: payload_, contentType: contentType_, attributes: attributes_, - btl: BlockNumber.wrap(0), + btl: BlockNumber32.wrap(0), newOwner: address(0) }); } @@ -55,7 +55,7 @@ library Lib { payload: "", contentType: emptyCt, attributes: empty, - btl: BlockNumber.wrap(0), + btl: BlockNumber32.wrap(0), newOwner: address(0) }); } @@ -69,7 +69,7 @@ library Lib { payload: "", contentType: emptyCt, attributes: empty, - btl: BlockNumber.wrap(0), + btl: BlockNumber32.wrap(0), newOwner: newOwner_ }); } @@ -83,12 +83,12 @@ library Lib { payload: "", contentType: emptyCt, attributes: empty, - btl: BlockNumber.wrap(0), + btl: BlockNumber32.wrap(0), newOwner: address(0) }); } - function extendOp(bytes32 entityKey_, BlockNumber btl_) internal pure returns (Entity.Operation memory) { + function extendOp(bytes32 entityKey_, BlockNumber32 btl_) internal pure returns (Entity.Operation memory) { Entity.Attribute[] memory empty = new Entity.Attribute[](0); Mime128 memory emptyCt; return Entity.Operation({ From 38f313fe81471037a2c14c5fe2f4af79f5eab796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 13:28:50 +0100 Subject: [PATCH 17/20] arkiv-bindings improvements --- Cargo.lock | 3 + Cargo.toml | 2 +- build.rs | 342 +++++++++++++++++++++-------------------- src/lib.rs | 57 ++++--- src/storage_layout.rs | 6 +- src/types/ident32.rs | 148 ++++++++++-------- src/types/mime128.rs | 193 ++++++++++-------------- src/types/mod.rs | 3 - src/wire.rs | 344 +++++++++++++++++++++++++----------------- 9 files changed, 583 insertions(+), 515 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e141e11..482264b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,7 @@ version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" dependencies = [ + "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck", @@ -446,12 +447,14 @@ version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" dependencies = [ + "alloy-json-abi", "const-hex", "dunce", "heck", "macro-string", "proc-macro2", "quote", + "serde_json", "syn 2.0.117", "syn-solidity", ] diff --git a/Cargo.toml b/Cargo.toml index 097359a..9d2f9a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ serde-wire = ["dep:serde", "alloy-primitives/serde"] [dependencies] alloy-primitives = "1.5" -alloy-sol-types = "1.5" +alloy-sol-types = { version = "1.5", features = ["json"] } alloy-contract = "2.0" eyre = "0.6" serde = { version = "1", features = ["derive"], optional = true } diff --git a/build.rs b/build.rs index e26fa91..7c47be5 100644 --- a/build.rs +++ b/build.rs @@ -12,7 +12,7 @@ fn main() { println!("cargo:rerun-if-changed={}", registry_artifact.display()); println!("cargo:rerun-if-changed={}", interface_artifact.display()); - // Run forge build if artifacts are missing or stale + // Run forge build if artifacts are missing or stale. let needs_build = !registry_artifact.exists() || !interface_artifact.exists() || { let artifact_modified = fs::metadata(®istry_artifact) .and_then(|m| m.modified()) @@ -36,18 +36,24 @@ fn main() { let out_dir = env::var("OUT_DIR").unwrap(); - // --- Generate inline sol! from IEntityRegistry ABI --- + // --- Generate sol.rs --- + // + // Generate a sol! block with inline Solidity rather than a JSON file path. + // Inline Solidity lets us control declaration order: UDVTs must be defined + // before any struct or interface member that references them, and structs + // must be defined before structs that contain them. The JSON file path + // approach hands ordering to alloy's macro, which processes ABI items + // sequentially and cannot resolve forward references to UDVTs. let interface_json = fs::read_to_string(&interface_artifact) .unwrap_or_else(|e| panic!("failed to read {}: {}", interface_artifact.display(), e)); - let interface: serde_json::Value = serde_json::from_str(&interface_json) - .expect("failed to parse IEntityRegistry artifact JSON"); + let interface: serde_json::Value = + serde_json::from_str(&interface_json).expect("failed to parse IEntityRegistry artifact"); let abi = &interface["abi"]; let sol_code = generate_sol_from_abi(abi); - let sol_path = Path::new(&out_dir).join("sol.rs"); - fs::write(&sol_path, sol_code).expect("failed to write sol.rs"); + fs::write(Path::new(&out_dir).join("sol.rs"), sol_code).expect("failed to write sol.rs"); - // --- Extract creation bytecode from EntityRegistry artifact --- + // --- Embed creation bytecode --- let registry_json = fs::read_to_string(®istry_artifact) .unwrap_or_else(|e| panic!("failed to read {}: {}", registry_artifact.display(), e)); let registry: serde_json::Value = @@ -59,215 +65,229 @@ fn main() { .strip_prefix("0x") .expect("bytecode should start with 0x"); - let bytecode_path = Path::new(&out_dir).join("bytecode.rs"); fs::write( - &bytecode_path, + Path::new(&out_dir).join("bytecode.rs"), format!( "/// EntityRegistry creation bytecode from Foundry artifact.\n\ - pub const ENTITY_REGISTRY_CREATION_CODE: &str = \"{}\";\n", - bytecode_hex, + pub const ENTITY_REGISTRY_CREATION_CODE: &str = \"{bytecode_hex}\";\n", ), ) .expect("failed to write bytecode.rs"); } -/// Generate a Rust file containing `sol!` with inline Solidity -/// derived from the Foundry ABI JSON. +// ----------------------------------------------------------------------------- +// sol! code generator +// ----------------------------------------------------------------------------- + +/// Generate a `sol!` block with inline Solidity from the compiled ABI. +/// +/// Declaration order: +/// 1. UDVTs (`type X is Y`) — must precede any reference to X +/// 2. Structs — inner structs before outer (dependency order via recursion) +/// 3. Interface — functions, events, errors fn generate_sol_from_abi(abi: &serde_json::Value) -> String { - let items = abi.as_array().expect("ABI should be an array"); + let items = abi.as_array().expect("ABI is not an array"); - // Collect struct definitions from function inputs/outputs - let mut structs = Vec::new(); - let mut seen_structs = std::collections::HashSet::new(); + let mut seen_udvts = std::collections::HashSet::::new(); + let mut udvts: Vec<(String, String)> = Vec::new(); // (UDVT name, underlying sol type) + let mut seen_structs = std::collections::HashSet::::new(); + let mut structs: Vec<(String, Vec)> = Vec::new(); // (name, components) for item in items { - collect_structs(item, &mut structs, &mut seen_structs); + collect_types(item, &mut seen_udvts, &mut udvts, &mut seen_structs, &mut structs); } - // Generate the sol! block - let mut sol = String::new(); - sol.push_str("alloy_sol_types::sol! {\n"); + let mut out = String::from( + "// Auto-generated from IEntityRegistry.sol ABI — do not edit.\n\ + alloy_sol_types::sol! {\n", + ); - // Emit struct definitions first - for s in &structs { - sol.push_str(" #[derive(Debug, Default, PartialEq, Eq)]\n"); - sol.push_str(&format!(" struct {} {{\n", s.name)); - for field in &s.fields { - sol.push_str(&format!(" {} {};\n", field.sol_type, field.name)); - } - sol.push_str(" }\n\n"); + // 1. UDVTs + for (name, underlying) in &udvts { + out.push_str(" #[derive(Debug, PartialEq, Eq, Hash)]\n"); + out.push_str(&format!(" type {} is {};\n", name, underlying)); + } + if !udvts.is_empty() { + out.push('\n'); } - // Emit interface with #[sol(rpc)] - sol.push_str(" #[sol(rpc)]\n"); - sol.push_str(" interface IEntityRegistry {\n"); + // 2. Structs + for (name, components) in &structs { + out.push_str(" #[derive(Debug, PartialEq, Eq)]\n"); + out.push_str(&format!(" struct {} {{\n", name)); + for comp in components { + let sol_type = param_sol_type(comp); + let field_name = comp["name"].as_str().unwrap_or("_"); + out.push_str(&format!(" {} {};\n", sol_type, field_name)); + } + out.push_str(" }\n\n"); + } + // 3. Interface + out.push_str(" #[sol(rpc)]\n"); + out.push_str(" interface IEntityRegistry {\n"); for item in items { match item["type"].as_str() { - Some("function") => { - let name = item["name"].as_str().unwrap(); - let mutability = item["stateMutability"].as_str().unwrap_or("nonpayable"); - let inputs = render_params(&item["inputs"]); - let outputs = render_params(&item["outputs"]); - - let mut sig = format!(" function {}({}) external", name, inputs); - if mutability == "view" || mutability == "pure" { - sig.push_str(&format!(" {}", mutability)); - } - if !outputs.is_empty() { - sig.push_str(&format!(" returns ({})", outputs)); - } - sig.push_str(";\n"); - sol.push_str(&sig); - } - Some("event") => { - let name = item["name"].as_str().unwrap(); - let params = render_event_params(&item["inputs"]); - sol.push_str(&format!(" event {}({});\n", name, params)); - } - Some("error") => { - let name = item["name"].as_str().unwrap(); - let params = render_params(&item["inputs"]); - sol.push_str(&format!(" error {}({});\n", name, params)); - } + Some("function") => out.push_str(&render_function(item)), + Some("event") => out.push_str(&render_event(item)), + Some("error") => out.push_str(&render_error(item)), _ => {} } } + out.push_str(" }\n}\n"); - sol.push_str(" }\n"); - sol.push_str("}\n"); - - format!( - "// Auto-generated from IEntityRegistry.sol ABI — do not edit.\n\ - {}\n", - sol - ) -} - -struct SolStruct { - name: String, - fields: Vec, -} - -struct SolField { - name: String, - sol_type: String, + out } -fn collect_structs( +/// Recursively collect UDVT and struct definitions from an ABI item. +/// Structs are inserted after their component types (dependency order). +fn collect_types( value: &serde_json::Value, - structs: &mut Vec, - seen: &mut std::collections::HashSet, + seen_udvts: &mut std::collections::HashSet, + udvts: &mut Vec<(String, String)>, + seen_structs: &mut std::collections::HashSet, + structs: &mut Vec<(String, Vec)>, ) { - // Check inputs and outputs for key in &["inputs", "outputs", "components"] { - if let Some(params) = value.get(key).and_then(|v| v.as_array()) { - for param in params { - if (param["type"].as_str() == Some("tuple") - || param["type"].as_str() == Some("tuple[]")) - && let Some(internal) = param["internalType"].as_str() - { - let struct_name = extract_struct_name(internal); - if !struct_name.is_empty() - && seen.insert(struct_name.clone()) - && let Some(components) = param["components"].as_array() - { - // Recurse into nested structs first - for comp in components { - collect_structs(comp, structs, seen); + let Some(params) = value.get(key).and_then(|v| v.as_array()) else { + continue; + }; + for param in params { + let abi_type = param["type"].as_str().unwrap_or(""); + + if abi_type == "tuple" || abi_type == "tuple[]" { + // Recurse into components first — inner structs must be declared before outer. + collect_types(param, seen_udvts, udvts, seen_structs, structs); + + if let Some(internal) = param["internalType"].as_str() { + let name = extract_struct_name(internal); + if !name.is_empty() && seen_structs.insert(name.clone()) { + if let Some(comps) = param["components"].as_array() { + structs.push((name, comps.clone())); } - - let fields: Vec = components - .iter() - .map(|c| SolField { - name: c["name"].as_str().unwrap_or("_").to_string(), - sol_type: param_to_sol_type(c), - }) - .collect(); - - structs.push(SolStruct { - name: struct_name, - fields, - }); } } - // Recurse - collect_structs(param, structs, seen); + } else { + if let Some(internal) = param["internalType"].as_str() { + // UDVT: internalType differs from the ABI primitive and contains + // no spaces (ruling out "struct X" / "enum X"). + if internal != abi_type + && !internal.contains(' ') + && seen_udvts.insert(internal.to_string()) + { + udvts.push((internal.to_string(), abi_type.to_string())); + } + } + collect_types(param, seen_udvts, udvts, seen_structs, structs); } } } } -/// Extract a clean struct name from internalType like "struct Entity.Operation[]" → "Operation" -fn extract_struct_name(internal_type: &str) -> String { - let s = internal_type - .trim_start_matches("struct ") - .trim_end_matches("[]"); - // Take the part after the last dot (strips library prefix) - s.rsplit('.').next().unwrap_or(s).to_string() -} +/// Convert an ABI parameter to its Solidity type string, preserving UDVT names. +fn param_sol_type(param: &serde_json::Value) -> String { + let abi_type = param["type"].as_str().unwrap_or("uint256"); -/// Convert an ABI parameter to a Solidity type string. -fn param_to_sol_type(param: &serde_json::Value) -> String { - let base_type = param["type"].as_str().unwrap_or("uint256"); - - if base_type == "tuple" || base_type == "tuple[]" { + if abi_type == "tuple" || abi_type == "tuple[]" { if let Some(internal) = param["internalType"].as_str() { let name = extract_struct_name(internal); - if base_type.ends_with("[]") { + if abi_type.ends_with("[]") { format!("{}[]", name) } else { name } } else { - base_type.to_string() + abi_type.to_string() + } + } else if let Some(internal) = param["internalType"].as_str() { + if internal != abi_type && !internal.contains(' ') { + internal.to_string() // UDVT name + } else { + abi_type.to_string() } } else { - base_type.to_string() + abi_type.to_string() } } -/// Render function parameters as a Solidity parameter list. -fn render_params(params: &serde_json::Value) -> String { - let arr = match params.as_array() { - Some(a) => a, - None => return String::new(), - }; +fn render_function(item: &serde_json::Value) -> String { + let name = item["name"].as_str().unwrap_or("_"); + let mutability = item["stateMutability"].as_str().unwrap_or("nonpayable"); + let inputs = render_params(&item["inputs"]); + let outputs = render_params(&item["outputs"]); - arr.iter() - .map(|p| { - let sol_type = param_to_sol_type(p); - let name = p["name"].as_str().unwrap_or(""); - if name.is_empty() { - sol_type - } else { - format!("{} {}", sol_type, name) - } - }) - .collect::>() - .join(", ") + let mut sig = format!(" function {}({}) external", name, inputs); + if matches!(mutability, "view" | "pure") { + sig.push_str(&format!(" {}", mutability)); + } + if !outputs.is_empty() { + sig.push_str(&format!(" returns ({})", outputs)); + } + sig.push_str(";\n"); + sig } -/// Render event parameters (with indexed keyword). -fn render_event_params(params: &serde_json::Value) -> String { - let arr = match params.as_array() { - Some(a) => a, - None => return String::new(), - }; +fn render_event(item: &serde_json::Value) -> String { + let name = item["name"].as_str().unwrap_or("_"); + let params = render_indexed_params(&item["inputs"]); + format!(" event {}({});\n", name, params) +} - arr.iter() - .map(|p| { - let sol_type = param_to_sol_type(p); - let name = p["name"].as_str().unwrap_or(""); - let indexed = if p["indexed"].as_bool() == Some(true) { - " indexed" - } else { - "" - }; - format!("{}{} {}", sol_type, indexed, name) +fn render_error(item: &serde_json::Value) -> String { + let name = item["name"].as_str().unwrap_or("_"); + let params = render_params(&item["inputs"]); + format!(" error {}({});\n", name, params) +} + +fn render_params(params: &serde_json::Value) -> String { + params + .as_array() + .map(|arr| { + arr.iter() + .map(|p| { + let t = param_sol_type(p); + match p["name"].as_str().filter(|n| !n.is_empty()) { + Some(name) => format!("{} {}", t, name), + None => t, + } + }) + .collect::>() + .join(", ") + }) + .unwrap_or_default() +} + +fn render_indexed_params(params: &serde_json::Value) -> String { + params + .as_array() + .map(|arr| { + arr.iter() + .map(|p| { + let t = param_sol_type(p); + let idx = if p["indexed"].as_bool() == Some(true) { + " indexed" + } else { + "" + }; + match p["name"].as_str().filter(|n| !n.is_empty()) { + Some(name) => format!("{}{} {}", t, idx, name), + None => format!("{}{}", t, idx), + } + }) + .collect::>() + .join(", ") }) - .collect::>() - .join(", ") + .unwrap_or_default() +} + +/// Extract a clean struct name from an `internalType` string. +/// `"struct Entity.BlockNode[]"` → `"BlockNode"` +fn extract_struct_name(internal_type: &str) -> String { + let s = internal_type + .trim_start_matches("struct ") + .trim_start_matches("enum ") + .trim_end_matches("[]"); + s.rsplit('.').next().unwrap_or(s).to_string() } /// Find the newest modification time in a directory tree. diff --git a/src/lib.rs b/src/lib.rs index a8c2f1a..2267172 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,10 @@ pub mod types; pub mod wire; // Generated from IEntityRegistry.sol ABI by build.rs. -// Contains struct definitions (Operation, Attribute, Mime128, Commitment, BlockNode) -// and the IEntityRegistry interface with all functions, events, and errors. +// Emits top-level types at the crate root: +// UDVTs: BlockNumber32, Ident32, OperationKey +// Structs: Mime128, Attribute, Operation, Commitment, BlockNode +// Module: IEntityRegistry (functions, events, errors) include!(concat!(env!("OUT_DIR"), "/sol.rs")); // EntityRegistry creation bytecode embedded at build time. @@ -67,7 +69,7 @@ mod tests { data: [FixedBytes::ZERO; 4], }, attributes: vec![], - btl: 1000, + btl: 1000u32, newOwner: Address::ZERO, }; @@ -82,12 +84,12 @@ mod tests { #[test] fn operation_with_attributes_roundtrip() { - let name = types::Ident32::encode("status").unwrap(); + let name = Ident32::encode("status").unwrap(); let mut value = [FixedBytes::ZERO; 4]; value[0] = B256::from(U256::from(42)); let attr = Attribute { - name: name.as_b256().into(), + name: name.0, // Attribute.name is FixedBytes<32>; .0 unwraps the Ident32 valueType: ATTR_UINT, value, }; @@ -100,7 +102,7 @@ mod tests { data: [FixedBytes::ZERO; 4], }, attributes: vec![attr.clone()], - btl: 500, + btl: 500u32, newOwner: Address::ZERO, }; @@ -119,7 +121,7 @@ mod tests { entityKey: B256::repeat_byte(0x01), operationType: OP_CREATE, owner: Address::repeat_byte(0xAA), - expiresAt: 1000, + expiresAt: 1000u32, entityHash: B256::repeat_byte(0x02), }; @@ -131,7 +133,7 @@ mod tests { assert_eq!(decoded.entityKey, B256::repeat_byte(0x01)); assert_eq!(decoded.operationType, OP_CREATE); assert_eq!(decoded.owner, Address::repeat_byte(0xAA)); - assert_eq!(decoded.expiresAt, 1000); + assert_eq!(decoded.expiresAt, 1000u32); } #[test] @@ -156,9 +158,9 @@ mod tests { fn commitment_roundtrip() { let c = Commitment { creator: Address::repeat_byte(0x01), - createdAt: 100, - updatedAt: 200, - expiresAt: 300, + createdAt: 100u32, + updatedAt: 200u32, + expiresAt: 300u32, owner: Address::repeat_byte(0x02), coreHash: B256::repeat_byte(0xAA), }; @@ -170,32 +172,29 @@ mod tests { #[test] fn ident32_encode_validates() { - assert!(types::Ident32::encode("valid.name").is_ok()); - assert!(types::Ident32::encode("").is_err()); - assert!(types::Ident32::encode("UPPER").is_err()); - assert!(types::Ident32::encode("1digit").is_err()); + assert!(Ident32::encode("valid.name").is_ok()); + assert!(Ident32::encode("").is_err()); + assert!(Ident32::encode("UPPER").is_err()); + assert!(Ident32::encode("1digit").is_err()); } #[test] fn mime128_encode_validates() { - assert!(types::Mime128Str::encode("application/json").is_ok()); - assert!(types::Mime128Str::encode("text/plain; charset=utf-8").is_ok()); - assert!(types::Mime128Str::encode("").is_err()); - assert!(types::Mime128Str::encode("Application/JSON").is_err()); - assert!(types::Mime128Str::encode("text").is_err()); + assert!(Mime128::encode("application/json").is_ok()); + assert!(Mime128::encode("text/plain; charset=utf-8").is_ok()); + assert!(Mime128::encode("").is_err()); + assert!(Mime128::encode("Application/JSON").is_err()); + assert!(Mime128::encode("text").is_err()); } #[test] fn mime128_encode_produces_valid_abi_data() { - let m = types::Mime128Str::encode("application/json").unwrap(); - let raw = m.to_bytes32x4(); - let decoded = types::Mime128Str::decode(&raw).unwrap(); - assert_eq!(decoded, "application/json"); - - // Also works with the sol!-generated Mime128 type via TryFrom - let mime = Mime128 { data: raw }; - let recovered = types::Mime128Str::try_from(&mime).unwrap(); - assert_eq!(recovered.as_str(), "application/json"); + let m = Mime128::encode("application/json").unwrap(); + assert_eq!(m.as_str().unwrap(), "application/json"); + + // Roundtrip through raw data + let m2 = Mime128 { data: m.data }; + assert_eq!(m2.as_str().unwrap(), "application/json"); } #[test] diff --git a/src/storage_layout.rs b/src/storage_layout.rs index c829961..7724214 100644 --- a/src/storage_layout.rs +++ b/src/storage_layout.rs @@ -460,7 +460,7 @@ mod tests { #[test] fn decode_head_block_reads_low_u32() { let word = slot_word(&0x1234_5678u32.to_be_bytes()); - assert_eq!(decode_head_block(word), 0x1234_5678); + assert_eq!(decode_head_block(word), 0x1234_5678u32); } #[test] @@ -472,8 +472,8 @@ mod tests { bytes[24..28].copy_from_slice(&0xBEEFu32.to_be_bytes()); // nextBlock @ offset 4 bytes[28..32].copy_from_slice(&0xDEADu32.to_be_bytes()); // prevBlock @ offset 0 let node = decode_block_node(B256::from(bytes)); - assert_eq!(node.prevBlock, 0xDEAD); - assert_eq!(node.nextBlock, 0xBEEF); + assert_eq!(node.prevBlock, 0xDEADu32); + assert_eq!(node.nextBlock, 0xBEEFu32); assert_eq!(node.txCount, 0xCAFE); } diff --git a/src/types/ident32.rs b/src/types/ident32.rs index b779f59..584e30c 100644 --- a/src/types/ident32.rs +++ b/src/types/ident32.rs @@ -1,25 +1,33 @@ -use alloy_primitives::B256; +//! Validation and string-conversion impls for the ABI-generated [`Ident32`] UDVT. +//! +//! [`Ident32`] is a left-aligned, null-padded 32-byte ASCII identifier. +//! Valid characters: `a-z`, `0-9`, `.`, `-`, `_`. Must start with `a-z`. +//! +//! These impl blocks mirror the validation rules in `contracts/types/Ident32.sol`. + +use alloy_primitives::FixedBytes; use eyre::{Result, bail}; +use crate::Ident32; + /// Valid character bitmap: a-z, 0-9, '.', '-', '_'. -/// Mirrors IDENT_CHARSET in Ident32.sol. -const IDENT_CHARSET: u128 = (1 << 0x2D) | (1 << 0x2E) // hyphen, dot - | (((1 << 10) - 1) << 0x30) // digits - | (1 << 0x5F) // underscore - | (((1u128 << 26) - 1) << 0x61); // lowercase a-z +/// Mirrors `IDENT_CHARSET` in Ident32.sol. +const IDENT_CHARSET: u128 = (1 << 0x2D) | (1 << 0x2E) + | (((1 << 10) - 1) << 0x30) + | (1 << 0x5F) + | (((1u128 << 26) - 1) << 0x61); /// Leading byte bitmap: a-z only. +/// Mirrors `IDENT_LEADING` in Ident32.sol. const IDENT_LEADING: u128 = ((1u128 << 26) - 1) << 0x61; -/// A validated 32-byte left-aligned ASCII identifier, mirroring the Solidity `Ident32` UDVT. -/// -/// Valid characters: `a-z`, `0-9`, `.`, `-`, `_`. Must start with `a-z`. -/// Stored as left-aligned bytes in a `B256`, null-padded on the right. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Ident32(B256); - impl Ident32 { - /// Encode a string into an Ident32, validating the charset. + /// Encode a string into an `Ident32`, validating the charset. + /// + /// Rules (mirrors `validateIdent32` in Ident32.sol): + /// - Non-empty, at most 32 bytes + /// - First byte must be `a-z` + /// - Remaining bytes must be in `a-z 0-9 . - _` pub fn encode(s: &str) -> Result { let bytes = s.as_bytes(); if bytes.is_empty() { @@ -28,43 +36,53 @@ impl Ident32 { if bytes.len() > 32 { bail!("Ident32 too long: {} bytes (max 32)", bytes.len()); } - - // Leading byte must be a-z if (IDENT_LEADING >> bytes[0]) & 1 == 0 { - bail!( - "Ident32 invalid leading byte at position 0: 0x{:02x}", - bytes[0] - ); + bail!("Ident32 invalid leading byte at position 0: 0x{:02x}", bytes[0]); } - - // Remaining bytes must be in IDENT_CHARSET for (i, &b) in bytes.iter().enumerate().skip(1) { if (IDENT_CHARSET >> b) & 1 == 0 { bail!("Ident32 invalid byte at position {}: 0x{:02x}", i, b); } } - let mut buf = [0u8; 32]; buf[..bytes.len()].copy_from_slice(bytes); - Ok(Self(B256::from(buf))) + Ok(Self(FixedBytes::from(buf))) } - /// Decode an Ident32 back to a string, stripping null padding. - pub fn decode(raw: B256) -> Result { - let bytes: &[u8] = raw.as_ref(); - let end = bytes.iter().position(|b| *b == 0).unwrap_or(32); + /// Decode an `Ident32` to its string representation, stripping null padding. + pub fn decode(&self) -> Result { + let bytes = self.0.as_slice(); + let end = bytes.iter().position(|&b| b == 0).unwrap_or(32); String::from_utf8(bytes[..end].to_vec()) .map_err(|e| eyre::eyre!("invalid UTF-8 in Ident32: {}", e)) } - /// Get the raw B256 representation. - pub fn as_b256(&self) -> B256 { - self.0 - } - - /// Decode this Ident32 to a string. - pub fn to_string(&self) -> Result { - Self::decode(self.0) + /// Validate raw bytes as an `Ident32`, returning `self` if valid. + /// + /// In addition to charset validation, enforces that once a null byte + /// appears all subsequent bytes must also be null (no embedded nulls). + /// This matches the stricter check in `validateIdent32` in Ident32.sol. + pub fn validate(self) -> Result { + let bytes = self.0.as_slice(); + if bytes[0] == 0 { + bail!("Ident32 cannot be empty"); + } + if (IDENT_LEADING >> bytes[0]) & 1 == 0 { + bail!("Ident32 invalid leading byte at position 0: 0x{:02x}", bytes[0]); + } + let mut found_null = false; + for (i, &b) in bytes.iter().enumerate().skip(1) { + if found_null { + if b != 0 { + bail!("Ident32 embedded null: non-zero byte 0x{:02x} at position {}", b, i); + } + } else if b == 0 { + found_null = true; + } else if (IDENT_CHARSET >> b) & 1 == 0 { + bail!("Ident32 invalid byte at position {}: 0x{:02x}", i, b); + } + } + Ok(self) } } @@ -75,24 +93,9 @@ impl TryFrom<&str> for Ident32 { } } -impl TryFrom for Ident32 { - type Error = eyre::Error; - fn try_from(raw: B256) -> Result { - // Validate the raw bytes - let s = Self::decode(raw)?; - Self::encode(&s) - } -} - -impl From for B256 { - fn from(id: Ident32) -> B256 { - id.0 - } -} - impl std::fmt::Display for Ident32 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.to_string() { + match self.decode() { Ok(s) => write!(f, "{}", s), Err(_) => write!(f, "{}", self.0), } @@ -101,12 +104,11 @@ impl std::fmt::Display for Ident32 { #[cfg(feature = "serde-wire")] impl serde::Serialize for Ident32 { - fn serialize(&self, serializer: S) -> Result { - // Always Ok by construction — `Ident32::encode` and the - // `TryFrom` impl both validate UTF-8 + charset before - // building the value, so `to_string` cannot fail here. - let s = self.to_string().map_err(serde::ser::Error::custom)?; - serializer.serialize_str(&s) + fn serialize(&self, s: S) -> std::result::Result { + // Always Ok by construction — values in scope are either produced by + // `encode` (validated charset) or `validate` (same rules). + let string = self.decode().map_err(serde::ser::Error::custom)?; + s.serialize_str(&string) } } @@ -117,8 +119,7 @@ mod tests { #[test] fn encode_decode_roundtrip() { let id = Ident32::encode("my.attribute").unwrap(); - let decoded = id.to_string().unwrap(); - assert_eq!(decoded, "my.attribute"); + assert_eq!(id.decode().unwrap(), "my.attribute"); } #[test] @@ -148,9 +149,32 @@ mod tests { } #[test] - fn full_length() { + fn full_length_32_bytes() { let s = "a".repeat(32); let id = Ident32::encode(&s).unwrap(); - assert_eq!(id.to_string().unwrap().len(), 32); + assert_eq!(id.decode().unwrap().len(), 32); + } + + #[test] + fn validate_rejects_embedded_null() { + // TryFrom> is provided by alloy (non-validating From). + // Use Ident32(raw).validate() to enforce the contract's stricter check. + let mut buf = [0u8; 32]; + buf[0] = b'a'; + buf[1] = b'b'; + buf[2] = 0; // null terminator + buf[3] = b'c'; // non-null after null — contract rejects this + let raw = FixedBytes::<32>::from(buf); + assert!(Ident32(raw).validate().is_err()); + } + + #[test] + fn validate_accepts_null_terminated() { + let mut buf = [0u8; 32]; + buf[0] = b'a'; + buf[1] = b'b'; + // remaining bytes are zero — valid + let raw = FixedBytes::<32>::from(buf); + assert!(Ident32(raw).validate().is_ok()); } } diff --git a/src/types/mime128.rs b/src/types/mime128.rs index 697d5fb..4c49882 100644 --- a/src/types/mime128.rs +++ b/src/types/mime128.rs @@ -1,8 +1,25 @@ +//! Validation and string-conversion impls for the ABI-generated [`Mime128`] struct. +//! +//! [`Mime128`] stores a MIME type string as 4 × `bytes32` (left-aligned, null-padded). +//! Validates per RFC 2045: `type/subtype[; param=value]*`, lowercase only, max 128 bytes. +//! +//! These impl blocks mirror the validation rules in `contracts/types/Mime128.sol`. + use alloy_primitives::FixedBytes; use eyre::{Result, bail}; +use crate::Mime128; + +/// Printable ASCII (0x20–0x7E) excluding uppercase A-Z (0x41–0x5A). +/// bits 32–64 (0x20–0x40): set — space through @ +/// bits 91–126 (0x5B–0x7E): set — [ through ~ (uppercase gap 0x41–0x5A is absent) +/// Mirrors `LOWER_PRINTABLE_ASCII` in Mime128.sol. +const LOWER_PRINTABLE_ASCII: u128 = (((1u128 << 33) - 1) << 32) | (((1u128 << 36) - 1) << 91); + /// Valid MIME token characters (RFC 2045, lowercase only). -/// Mirrors MIME_TOKEN in Mime128.sol. +/// Token chars = printable ASCII minus tspecials and uppercase. +/// tspecials: SPACE " ( ) , / : ; < = > ? @ [ \ ] +/// Mirrors `MIME_TOKEN` in Mime128.sol. const MIME_TOKEN: u128 = LOWER_PRINTABLE_ASCII & !((1 << 0x20) // space | (1 << 0x22) // " @@ -21,9 +38,6 @@ const MIME_TOKEN: u128 = LOWER_PRINTABLE_ASCII | (1 << 0x5C) // \ | (1 << 0x5D)); // ] -/// Printable ASCII excluding uppercase. -const LOWER_PRINTABLE_ASCII: u128 = (((1u128 << 33) - 1) << 32) | (((1u128 << 36) - 1) << 91); - const S_TYPE: u8 = 0; const S_SUBTYPE: u8 = 1; const S_OWS: u8 = 2; @@ -34,15 +48,11 @@ fn is_token(b: u8) -> bool { b < 128 && (MIME_TOKEN >> b) & 1 == 1 } -/// A validated 128-byte MIME type string, mirroring the Solidity `Mime128` struct. -/// -/// Stored as 4 × bytes32 (left-aligned, null-padded). -/// Validated per RFC 2045: `type/subtype[; param=value]*`, lowercase only. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Mime128Str(String); - -impl Mime128Str { - /// Encode and validate a MIME string. +impl Mime128 { + /// Encode and validate a MIME type string into a `Mime128`. + /// + /// Validates per RFC 2045 (mirrors `validateMime128` in Mime128.sol): + /// `type/subtype[; param=value]*`, lowercase only, at most 128 bytes. pub fn encode(s: &str) -> Result { let bytes = s.as_bytes(); if bytes.is_empty() { @@ -51,77 +61,68 @@ impl Mime128Str { if bytes.len() > 128 { bail!("MIME type too long: {} bytes (max 128)", bytes.len()); } - validate_mime(bytes)?; - Ok(Self(s.to_string())) + validate_mime_bytes(bytes)?; + let mut data = [FixedBytes::ZERO; 4]; + for (i, chunk) in bytes.chunks(32).enumerate() { + let mut buf = [0u8; 32]; + buf[..chunk.len()].copy_from_slice(chunk); + data[i] = FixedBytes::from(buf); + } + Ok(Self { data }) } - /// Decode from the raw 4 × bytes32 representation. - pub fn decode(data: &[FixedBytes<32>; 4]) -> Option { + /// Decode a `Mime128` to its string representation, stripping null padding. + pub fn as_str(&self) -> Result { let mut bytes = Vec::with_capacity(128); - for b32 in data { - bytes.extend_from_slice(&b32[..]); + for b32 in &self.data { + bytes.extend_from_slice(b32.as_slice()); } - if let Some(end) = bytes.iter().position(|b| *b == 0) { + if let Some(end) = bytes.iter().position(|&b| b == 0) { bytes.truncate(end); } - String::from_utf8(bytes).ok() + String::from_utf8(bytes).map_err(|e| eyre::eyre!("invalid UTF-8 in Mime128: {}", e)) } - /// Encode into the raw 4 × bytes32 representation. - pub fn to_bytes32x4(&self) -> [FixedBytes<32>; 4] { - let bytes = self.0.as_bytes(); - let mut data = [FixedBytes::ZERO; 4]; - for (i, chunk) in bytes.chunks(32).enumerate() { - if i >= 4 { - break; - } - let mut buf = [0u8; 32]; - buf[..chunk.len()].copy_from_slice(chunk); - data[i] = FixedBytes::from(buf); + /// Validate a `Mime128`'s raw bytes, returning `self` if valid. + /// + /// Mirrors `validateMime128` in Mime128.sol. + pub fn validate(self) -> Result { + let s = self.as_str()?; + if s.is_empty() { + bail!("MIME type cannot be empty"); } - data - } - - /// Get the string value. - pub fn as_str(&self) -> &str { - &self.0 + validate_mime_bytes(s.as_bytes())?; + Ok(self) } } -impl TryFrom<&str> for Mime128Str { +impl TryFrom<&str> for Mime128 { type Error = eyre::Error; fn try_from(s: &str) -> Result { Self::encode(s) } } -/// Decode + validate the sol!-generated `Mime128` struct in one step. -/// -/// Symmetric counterpart of [`Mime128Str::to_bytes32x4`] composed into the -/// `Mime128 { data }` wrapper: bytes → string → validated MIME. -impl TryFrom<&crate::Mime128> for Mime128Str { - type Error = eyre::Error; - fn try_from(mime: &crate::Mime128) -> Result { - let s = Self::decode(&mime.data).ok_or_else(|| eyre::eyre!("invalid UTF-8 in Mime128"))?; - Self::encode(&s) - } -} - -impl std::fmt::Display for Mime128Str { +impl std::fmt::Display for Mime128 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) + match self.as_str() { + Ok(s) => write!(f, "{}", s), + Err(_) => write!(f, ""), + } } } #[cfg(feature = "serde-wire")] -impl serde::Serialize for Mime128Str { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(self.as_str()) +impl serde::Serialize for Mime128 { + fn serialize(&self, s: S) -> std::result::Result { + let string = self.as_str().map_err(serde::ser::Error::custom)?; + s.serialize_str(&string) } } -/// Validate MIME structure per RFC 2045 state machine (mirrors Mime128.sol). -fn validate_mime(bytes: &[u8]) -> Result<()> { +/// Validate raw MIME bytes using the RFC 2045 state machine. +/// Mirrors `validateMime128` in Mime128.sol. +fn validate_mime_bytes(bytes: &[u8]) -> Result<()> { let mut state = S_TYPE; let mut seg_len: usize = 0; @@ -181,7 +182,6 @@ fn validate_mime(bytes: &[u8]) -> Result<()> { } } - // Must end in SUBTYPE or PVALUE with non-empty segment if (state == S_SUBTYPE || state == S_PVALUE) && seg_len > 0 { Ok(()) } else { @@ -195,88 +195,53 @@ mod tests { #[test] fn encode_decode_roundtrip() { - let m = Mime128Str::encode("application/json").unwrap(); - let raw = m.to_bytes32x4(); - let decoded = Mime128Str::decode(&raw).unwrap(); - assert_eq!(decoded, "application/json"); + let m = Mime128::encode("application/json").unwrap(); + assert_eq!(m.as_str().unwrap(), "application/json"); } #[test] fn with_params() { - let m = Mime128Str::encode("text/plain; charset=utf-8").unwrap(); - assert_eq!(m.as_str(), "text/plain; charset=utf-8"); + let m = Mime128::encode("text/plain; charset=utf-8").unwrap(); + assert_eq!(m.as_str().unwrap(), "text/plain; charset=utf-8"); } #[test] fn rejects_empty() { - assert!(Mime128Str::encode("").is_err()); + assert!(Mime128::encode("").is_err()); } #[test] fn rejects_uppercase() { - assert!(Mime128Str::encode("Application/JSON").is_err()); + assert!(Mime128::encode("Application/JSON").is_err()); } #[test] fn rejects_missing_subtype() { - assert!(Mime128Str::encode("text").is_err()); + assert!(Mime128::encode("text").is_err()); } #[test] fn rejects_incomplete_param() { - assert!(Mime128Str::encode("text/plain; charset").is_err()); - } - - #[test] - fn max_length() { - let s = format!("{}/{}", "a".repeat(63), "b".repeat(64)); - assert!(Mime128Str::encode(&s).is_ok()); - } - - #[test] - fn rejects_too_long() { - let s = format!("{}/{}", "a".repeat(64), "b".repeat(64)); - assert!(Mime128Str::encode(&s).is_err()); - } - - #[test] - fn try_from_mime128_roundtrip() { - let m = Mime128Str::encode("text/plain; charset=utf-8").unwrap(); - let mime = crate::Mime128 { - data: m.to_bytes32x4(), - }; - let recovered = Mime128Str::try_from(&mime).unwrap(); - assert_eq!(recovered, m); + assert!(Mime128::encode("text/plain; charset").is_err()); } #[test] - fn try_from_mime128_rejects_invalid_utf8() { - let mut w0 = [0u8; 32]; - w0[0] = 0xFF; // invalid UTF-8 leading byte - let mime = crate::Mime128 { - data: [ - FixedBytes::from(w0), - FixedBytes::ZERO, - FixedBytes::ZERO, - FixedBytes::ZERO, - ], + fn validate_rejects_invalid_bytes() { + let mut buf = [0u8; 32]; + buf[0] = 0xFF; // invalid UTF-8 + let m = Mime128 { + data: [FixedBytes::from(buf), FixedBytes::ZERO, FixedBytes::ZERO, FixedBytes::ZERO], }; - assert!(Mime128Str::try_from(&mime).is_err()); + assert!(m.validate().is_err()); } #[test] - fn try_from_mime128_rejects_invalid_mime_structure() { - // Valid UTF-8 bytes that fail MIME validation (uppercase, lowercase-only rule). - let mut w0 = [0u8; 32]; - w0[..16].copy_from_slice(b"Application/JSON"); - let mime = crate::Mime128 { - data: [ - FixedBytes::from(w0), - FixedBytes::ZERO, - FixedBytes::ZERO, - FixedBytes::ZERO, - ], + fn validate_rejects_invalid_mime_structure() { + let mut buf = [0u8; 32]; + buf[..4].copy_from_slice(b"text"); // missing slash + let m = Mime128 { + data: [FixedBytes::from(buf), FixedBytes::ZERO, FixedBytes::ZERO, FixedBytes::ZERO], }; - assert!(Mime128Str::try_from(&mime).is_err()); + assert!(m.validate().is_err()); } } diff --git a/src/types/mod.rs b/src/types/mod.rs index f7868ef..1a82910 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,2 @@ mod ident32; mod mime128; - -pub use ident32::Ident32; -pub use mime128::Mime128Str; diff --git a/src/wire.rs b/src/wire.rs index a675695..7b6866e 100644 --- a/src/wire.rs +++ b/src/wire.rs @@ -1,19 +1,39 @@ -//! Wire-format types matching the v2 ExEx → EntityDB JSON-RPC interface +//! Wire-format types for the v2 ExEx → EntityDB JSON-RPC interface //! (`arkiv-op-reth/docs/exex-jsonrpc-interface-v2.md`). //! -//! These are the **typed data target** for decoded EntityRegistry -//! operations: a tagged enum per operation type plus a typed attribute -//! enum, both serializing to the JSON shape the EntityDB consumes. +//! # Type hierarchy //! -//! Scope is intentionally narrow: just operations and attributes. Block, -//! transaction, and block-ref envelopes live in the consumer (op-reth) -//! because they're built from reth-specific inputs (`RecoveredBlock`, -//! signature recovery, etc.). +//! ```text +//! ArkivBlock +//! └─ ArkivBlockHeader (block number, hash, changeset hash) +//! └─ ArkivTransaction[] +//! └─ ArkivOperation[] (tagged by op type) +//! └─ ArkivAttribute[] (on create / update) +//! ``` //! -//! Decoding is byte-exact and non-lossy. ATTR_STRING is exposed as -//! `FixedBytes<128>` — the protocol treats those 128 bytes as opaque, -//! so the bindings preserve them verbatim. UTF-8 (or any other charset) -//! interpretation is the consumer's choice. +//! [`ArkivOperation`] and [`ArkivAttribute`] are the decoded, validated +//! representations of the raw ABI [`Operation`] / [`Attribute`] calldata +//! structs. Block and transaction envelopes are defined here so that all +//! wire-format shapes live in one place; consumers build them from +//! reth-specific inputs (`RecoveredBlock`, signature recovery, etc.) and +//! forward the complete [`ArkivBlock`] to the EntityDB. +//! +//! [`Operation`]: crate::Operation +//! [`Attribute`]: crate::Attribute +//! +//! # Decoding +//! +//! [`decode_operation`] pairs one raw calldata [`Operation`] with its two +//! emitted events (`EntityOperation` and `ChangeSetHashUpdate`) to produce +//! an [`ArkivOperation`]. It validates [`Mime128`] content types and +//! [`Ident32`] attribute names on the way in. +//! +//! `expires_at` on [`CreateOp`] and [`ExtendOp`] is sourced from +//! `EntityOperation.expiresAt` — the absolute block number the contract +//! computed as `currentBlock + op.btl`. The raw `btl` is not exposed. +//! +//! `ATTR_STRING` values are `FixedBytes<128>` — the protocol treats those +//! 128 bytes as opaque; UTF-8 interpretation is the consumer's choice. //! //! Serde annotations are gated behind the `serde-wire` feature (default on). @@ -24,23 +44,69 @@ use eyre::{Result, bail}; use serde::Serialize; use crate::IEntityRegistry::{ChangeSetHashUpdate, EntityOperation}; -use crate::types::{Ident32, Mime128Str}; use crate::{ - ATTR_ENTITY_KEY, ATTR_STRING, ATTR_UINT, Attribute as CalldataAttribute, OP_CREATE, OP_DELETE, - OP_EXPIRE, OP_EXTEND, OP_TRANSFER, OP_UPDATE, Operation as CalldataOp, + ATTR_ENTITY_KEY, ATTR_STRING, ATTR_UINT, Attribute, Ident32, Mime128, OP_CREATE, OP_DELETE, + OP_EXPIRE, OP_EXTEND, OP_TRANSFER, OP_UPDATE, Operation, }; // ----------------------------------------------------------------------------- -// Operation enum + per-op structs +// Block / transaction envelopes +// ----------------------------------------------------------------------------- + +/// Block header subset forwarded to the EntityDB. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde-wire", derive(Serialize))] +#[cfg_attr(feature = "serde-wire", serde(rename_all = "camelCase"))] +pub struct ArkivBlockHeader { + #[cfg_attr(feature = "serde-wire", serde(with = "hex_u64"))] + pub number: u64, + pub hash: B256, + pub parent_hash: B256, + /// Rolling changeset hash as of the end of this block. `B256::ZERO` only + /// when no operation has ever been recorded at or before this block. + pub changeset_hash: B256, +} + +/// A block with its decoded Arkiv transactions (may be empty). +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde-wire", derive(Serialize))] +pub struct ArkivBlock { + pub header: ArkivBlockHeader, + pub transactions: Vec, +} + +/// A transaction targeting the EntityRegistry with decoded operations. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde-wire", derive(Serialize))] +#[cfg_attr(feature = "serde-wire", serde(rename_all = "camelCase"))] +pub struct ArkivTransaction { + pub hash: B256, + pub index: u32, + pub sender: Address, + pub operations: Vec, +} + +/// Minimal block identifier for revert payloads. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde-wire", derive(Serialize))] +#[cfg_attr(feature = "serde-wire", serde(rename_all = "camelCase"))] +pub struct ArkivBlockRef { + #[cfg_attr(feature = "serde-wire", serde(with = "hex_u64"))] + pub number: u64, + pub hash: B256, +} + +// ----------------------------------------------------------------------------- +// ArkivOperation — decoded, validated, tagged by op type // ----------------------------------------------------------------------------- -/// A decoded EntityRegistry operation, tagged by type. +/// A decoded EntityRegistry operation. /// -/// JSON shape per v2 wire spec: `{"type": "create" | "update" | …, …fields}`. +/// JSON shape: `{"type": "create" | "update" | …, …fields}`. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde-wire", derive(Serialize))] #[cfg_attr(feature = "serde-wire", serde(tag = "type", rename_all = "camelCase"))] -pub enum Operation { +pub enum ArkivOperation { Create(CreateOp), Update(UpdateOp), Extend(ExtendOp), @@ -61,8 +127,8 @@ pub struct CreateOp { pub entity_hash: B256, pub changeset_hash: B256, pub payload: Bytes, - pub content_type: Mime128Str, - pub attributes: Vec, + pub content_type: Mime128, + pub attributes: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -75,8 +141,8 @@ pub struct UpdateOp { pub entity_hash: B256, pub changeset_hash: B256, pub payload: Bytes, - pub content_type: Mime128Str, - pub attributes: Vec, + pub content_type: Mime128, + pub attributes: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -126,71 +192,48 @@ pub struct ExpireOp { } // ----------------------------------------------------------------------------- -// Attribute enum +// ArkivAttribute — validated, typed attribute value // ----------------------------------------------------------------------------- -/// A typed attribute, mirroring the on-chain `Attribute { name, valueType, -/// value }` shape. The variant carries the value's natural Rust type at -/// the natural size for that `valueType`: +/// A typed attribute decoded from the on-chain `Attribute { name, valueType, +/// value }` shape. /// /// - `Uint` — `U256` (right-aligned in `data[0]` on-chain) -/// - `String` — `FixedBytes<128>` (the full `bytes32[4]` container, -/// byte-exact). The protocol treats ATTR_STRING as opaque bytes; UTF-8 -/// is convention only and is the consumer's call to interpret. -/// - `EntityKey` — `B256` (== `FixedBytes<32>`, in `data[0]` on-chain) -/// -/// `valueType` is reified on the wire as the serde tag so JSON consumers -/// see exactly the same three fields the contract defines. +/// - `String` — `FixedBytes<128>` (full `bytes32[4]`, byte-exact and opaque) +/// - `EntityKey` — `B256` (`data[0]` on-chain) #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde-wire", derive(Serialize))] #[cfg_attr( feature = "serde-wire", serde(tag = "valueType", rename_all = "camelCase") )] -pub enum Attribute { - Uint { - name: Ident32, - value: U256, - }, - String { - name: Ident32, - value: FixedBytes<128>, - }, - EntityKey { - name: Ident32, - value: B256, - }, +pub enum ArkivAttribute { + Uint { name: Ident32, value: U256 }, + String { name: Ident32, value: FixedBytes<128> }, + EntityKey { name: Ident32, value: B256 }, } // ----------------------------------------------------------------------------- // Decoder // ----------------------------------------------------------------------------- -/// Build a typed [`Operation`] from a single decoded calldata op plus the -/// two paired event records (`EntityOperation` and `ChangeSetHashUpdate`). +/// Build a typed [`ArkivOperation`] from a single decoded calldata op plus +/// the two paired event records (`EntityOperation` and `ChangeSetHashUpdate`). /// -/// Inputs: -/// - `op_index`: the operation's position within its transaction's `ops[]` -/// - `calldata`: the `Operation` struct from `executeCall::abi_decode` +/// - `op_index`: position within the transaction's `ops[]` +/// - `calldata`: the [`Operation`] struct from `executeCall::abi_decode` /// - `entity_event`: the `EntityOperation` event emitted for this op /// - `hash_event`: the `ChangeSetHashUpdate` event emitted for this op /// -/// The `expires_at` field on `CreateOp` and `ExtendOp` is sourced from -/// `entity_event.expiresAt` — the absolute block number computed by the -/// contract as `currentBlock + op.btl` and emitted on-chain. The raw `btl` -/// from calldata is not exposed in the wire format. -/// -/// Errors: -/// - `entity_event.operationType` doesn't match `calldata.operationType` -/// - Unknown operation type -/// - Attribute name fails Ident32 decode -/// - Attribute value_type is unknown or violates natural-size invariants +/// Errors if operationType mismatches, the op type is unknown, the +/// `Mime128` content type is invalid, or any attribute name fails +/// `Ident32` validation. pub fn decode_operation( op_index: u32, - calldata: &CalldataOp, + calldata: &Operation, entity_event: &EntityOperation, hash_event: &ChangeSetHashUpdate, -) -> Result { +) -> Result { if entity_event.operationType != calldata.operationType { bail!( "event/calldata operationType mismatch: event={}, calldata={}", @@ -203,10 +246,11 @@ pub fn decode_operation( let owner = entity_event.owner; let entity_hash = entity_event.entityHash; let changeset_hash = hash_event.changeSetHash; + // expiresAt is u32 in the event struct (alloy uses the underlying RustType). let expires_at = u64::from(entity_event.expiresAt); Ok(match calldata.operationType { - OP_CREATE => Operation::Create(CreateOp { + OP_CREATE => ArkivOperation::Create(CreateOp { op_index, entity_key, owner, @@ -214,20 +258,20 @@ pub fn decode_operation( entity_hash, changeset_hash, payload: calldata.payload.clone(), - content_type: Mime128Str::try_from(&calldata.contentType)?, + content_type: calldata.contentType.clone().validate()?, attributes: decode_attributes(&calldata.attributes)?, }), - OP_UPDATE => Operation::Update(UpdateOp { + OP_UPDATE => ArkivOperation::Update(UpdateOp { op_index, entity_key, owner, entity_hash, changeset_hash, payload: calldata.payload.clone(), - content_type: Mime128Str::try_from(&calldata.contentType)?, + content_type: calldata.contentType.clone().validate()?, attributes: decode_attributes(&calldata.attributes)?, }), - OP_EXTEND => Operation::Extend(ExtendOp { + OP_EXTEND => ArkivOperation::Extend(ExtendOp { op_index, entity_key, owner, @@ -235,21 +279,21 @@ pub fn decode_operation( entity_hash, changeset_hash, }), - OP_TRANSFER => Operation::Transfer(TransferOp { + OP_TRANSFER => ArkivOperation::Transfer(TransferOp { op_index, entity_key, owner, entity_hash, changeset_hash, }), - OP_DELETE => Operation::Delete(DeleteOp { + OP_DELETE => ArkivOperation::Delete(DeleteOp { op_index, entity_key, owner, entity_hash, changeset_hash, }), - OP_EXPIRE => Operation::Expire(ExpireOp { + OP_EXPIRE => ArkivOperation::Expire(ExpireOp { op_index, entity_key, owner, @@ -260,37 +304,36 @@ pub fn decode_operation( }) } -fn decode_attributes(attrs: &[CalldataAttribute]) -> Result> { +fn decode_attributes(attrs: &[Attribute]) -> Result> { attrs.iter().map(decode_attribute).collect() } -fn decode_attribute(attr: &CalldataAttribute) -> Result { - let name = Ident32::try_from(attr.name)?; +fn decode_attribute(attr: &Attribute) -> Result { + // attr.name is FixedBytes<32> — alloy unwraps UDVTs to primitives in + // struct fields. Wrap into Ident32 and validate charset + null-termination. + let name = Ident32(attr.name).validate()?; match attr.valueType { ATTR_UINT => { require_single_word(&attr.value, attr.valueType)?; - Ok(Attribute::Uint { + Ok(ArkivAttribute::Uint { name, value: U256::from_be_bytes(attr.value[0].0), }) } ATTR_STRING => { - // Concat 4 words into a single 128-byte buffer, byte-exact. - // No NUL-truncation, no UTF-8 — the protocol is opaque on the - // contents of this field. let mut buf = [0u8; 128]; for (i, w) in attr.value.iter().enumerate() { buf[i * 32..(i + 1) * 32].copy_from_slice(w.as_slice()); } - Ok(Attribute::String { + Ok(ArkivAttribute::String { name, value: FixedBytes::from(buf), }) } ATTR_ENTITY_KEY => { require_single_word(&attr.value, attr.valueType)?; - Ok(Attribute::EntityKey { + Ok(ArkivAttribute::EntityKey { name, value: attr.value[0], }) @@ -299,7 +342,7 @@ fn decode_attribute(attr: &CalldataAttribute) -> Result { } } -/// Enforce the bytes32-sized invariant (natural size of UINT and ENTITY_KEY): +/// Enforce the bytes32-sized invariant for UINT and ENTITY_KEY: /// `value[1..=3]` must be zero. fn require_single_word(value: &[FixedBytes<32>; 4], value_type: u8) -> Result<()> { for (i, w) in value.iter().enumerate().skip(1) { @@ -315,7 +358,7 @@ fn require_single_word(value: &[FixedBytes<32>; 4], value_type: u8) -> Result<() } // ----------------------------------------------------------------------------- -// Hex u64 serializer — block numbers, expiry are JSON hex strings ("0x…"). +// Hex u64 serializer — block numbers and expiry as JSON hex strings ("0x…"). // ----------------------------------------------------------------------------- #[cfg(feature = "serde-wire")] @@ -333,10 +376,10 @@ mod tests { use crate::Mime128; use alloy_primitives::FixedBytes; - fn ident32(name: &str) -> B256 { + fn ident32(name: &str) -> FixedBytes<32> { let mut bytes = [0u8; 32]; bytes[..name.len()].copy_from_slice(name.as_bytes()); - B256::from(bytes) + FixedBytes::from(bytes) } fn mime128(s: &str) -> Mime128 { @@ -356,7 +399,6 @@ mod tests { data } - /// Build a `bytes32[4]` calldata value from a 128-byte buffer. fn calldata_value(buf: [u8; 128]) -> [FixedBytes<32>; 4] { let mut data = [FixedBytes::ZERO; 4]; for (i, w) in data.iter_mut().enumerate() { @@ -365,9 +407,6 @@ mod tests { data } - /// Pack a string into the on-chain ATTR_STRING wire shape (left-aligned, - /// zero-padded). The wire decoder preserves the full 128 bytes; tests - /// likewise compare the full 128-byte buffer. fn string_buf(s: &str) -> [u8; 128] { let mut buf = [0u8; 128]; buf[..s.len()].copy_from_slice(s.as_bytes()); @@ -379,7 +418,7 @@ mod tests { entityKey: B256::repeat_byte(0xE1), operationType: op_type, owner: Address::repeat_byte(0xAA), - expiresAt: 1234, + expiresAt: 1234u32, entityHash: B256::repeat_byte(0xE2), } } @@ -392,23 +431,23 @@ mod tests { } } - fn calldata_op(op_type: u8) -> CalldataOp { - CalldataOp { + fn raw_op(op_type: u8) -> Operation { + Operation { operationType: op_type, entityKey: B256::ZERO, payload: Bytes::from_static(b"hello"), contentType: mime128("text/plain"), attributes: vec![], - btl: 1234, + btl: 1234u32, newOwner: Address::ZERO, } } #[test] fn decode_create_populates_body_fields() { - let op = calldata_op(OP_CREATE); + let op = raw_op(OP_CREATE); let decoded = decode_operation(0, &op, &entity_event(OP_CREATE), &hash_event()).unwrap(); - let Operation::Create(c) = decoded else { + let ArkivOperation::Create(c) = decoded else { panic!("expected Create variant"); }; assert_eq!(c.op_index, 0); @@ -418,27 +457,52 @@ mod tests { assert_eq!(c.entity_hash, B256::repeat_byte(0xE2)); assert_eq!(c.changeset_hash, B256::repeat_byte(0xC1)); assert_eq!(c.payload, Bytes::from_static(b"hello")); - assert_eq!(c.content_type.as_str(), "text/plain"); + assert_eq!(c.content_type.as_str().unwrap(), "text/plain"); assert!(c.attributes.is_empty()); } + #[test] + fn decode_fails_on_invalid_mime_content_type() { + let mut op = raw_op(OP_CREATE); + op.contentType = mime128("text"); + let err = decode_operation(0, &op, &entity_event(OP_CREATE), &hash_event()) + .unwrap_err() + .to_string(); + assert!(err.contains("MIME"), "{}", err); + } + + #[test] + fn decode_fails_on_invalid_ident32_attribute_name() { + let mut op = raw_op(OP_CREATE); + let mut name_bytes = [0u8; 32]; + name_bytes[..5].copy_from_slice(b"UPPER"); + op.attributes.push(Attribute { + name: FixedBytes::from(name_bytes), + valueType: ATTR_UINT, + value: u256_word(1), + }); + let err = decode_operation(0, &op, &entity_event(OP_CREATE), &hash_event()) + .unwrap_err() + .to_string(); + assert!(err.contains("Ident32"), "{}", err); + } + #[test] fn decode_update_omits_expires_at() { - let op = calldata_op(OP_UPDATE); + let op = raw_op(OP_UPDATE); let decoded = decode_operation(3, &op, &entity_event(OP_UPDATE), &hash_event()).unwrap(); - let Operation::Update(u) = decoded else { + let ArkivOperation::Update(u) = decoded else { panic!("expected Update"); }; assert_eq!(u.op_index, 3); assert_eq!(u.payload, Bytes::from_static(b"hello")); - // No expires_at on UpdateOp by design. } #[test] fn decode_extend_carries_expires_at_no_body() { - let op = calldata_op(OP_EXTEND); + let op = raw_op(OP_EXTEND); let decoded = decode_operation(1, &op, &entity_event(OP_EXTEND), &hash_event()).unwrap(); - let Operation::Extend(e) = decoded else { + let ArkivOperation::Extend(e) = decoded else { panic!("expected Extend"); }; assert_eq!(e.expires_at, 1234); @@ -447,12 +511,12 @@ mod tests { #[test] fn decode_transfer_delete_expire_have_no_body() { for ty in [OP_TRANSFER, OP_DELETE, OP_EXPIRE] { - let op = calldata_op(ty); + let op = raw_op(ty); let decoded = decode_operation(0, &op, &entity_event(ty), &hash_event()).unwrap(); match (ty, &decoded) { - (OP_TRANSFER, Operation::Transfer(_)) => {} - (OP_DELETE, Operation::Delete(_)) => {} - (OP_EXPIRE, Operation::Expire(_)) => {} + (OP_TRANSFER, ArkivOperation::Transfer(_)) => {} + (OP_DELETE, ArkivOperation::Delete(_)) => {} + (OP_EXPIRE, ArkivOperation::Expire(_)) => {} _ => panic!("variant mismatch for op_type {}: {:?}", ty, decoded), } } @@ -460,7 +524,7 @@ mod tests { #[test] fn decode_rejects_event_calldata_mismatch() { - let op = calldata_op(OP_CREATE); + let op = raw_op(OP_CREATE); let err = decode_operation(0, &op, &entity_event(OP_DELETE), &hash_event()) .unwrap_err() .to_string(); @@ -469,7 +533,7 @@ mod tests { #[test] fn decode_rejects_unknown_op_type() { - let mut op = calldata_op(OP_CREATE); + let mut op = raw_op(OP_CREATE); op.operationType = 99; let mut ev = entity_event(OP_CREATE); ev.operationType = 99; @@ -481,13 +545,13 @@ mod tests { #[test] fn decode_uint_attribute() { - let mut op = calldata_op(OP_CREATE); - op.attributes.push(CalldataAttribute { + let mut op = raw_op(OP_CREATE); + op.attributes.push(Attribute { name: ident32("count"), valueType: ATTR_UINT, value: u256_word(42), }); - let Operation::Create(c) = + let ArkivOperation::Create(c) = decode_operation(0, &op, &entity_event(OP_CREATE), &hash_event()).unwrap() else { unreachable!(); @@ -495,7 +559,7 @@ mod tests { assert_eq!(c.attributes.len(), 1); assert_eq!( c.attributes[0], - Attribute::Uint { + ArkivAttribute::Uint { name: Ident32::encode("count").unwrap(), value: U256::from(42u64), }, @@ -505,20 +569,20 @@ mod tests { #[test] fn decode_string_attribute_byte_exact() { let buf = string_buf("hello"); - let mut op = calldata_op(OP_CREATE); - op.attributes.push(CalldataAttribute { + let mut op = raw_op(OP_CREATE); + op.attributes.push(Attribute { name: ident32("title"), valueType: ATTR_STRING, value: calldata_value(buf), }); - let Operation::Create(c) = + let ArkivOperation::Create(c) = decode_operation(0, &op, &entity_event(OP_CREATE), &hash_event()).unwrap() else { unreachable!(); }; assert_eq!( c.attributes[0], - Attribute::String { + ArkivAttribute::String { name: Ident32::encode("title").unwrap(), value: FixedBytes::from(buf), }, @@ -527,26 +591,24 @@ mod tests { #[test] fn decode_string_attribute_preserves_arbitrary_bytes() { - // Non-UTF-8 bytes pass through verbatim — no UTF-8 attempted, no - // NUL-truncation. The full 128 bytes are preserved. let mut buf = [0u8; 128]; buf[0] = 0xFF; buf[1] = 0xFE; - buf[10] = 0x00; // NUL in the middle - buf[20] = b'x'; // bytes after the NUL must survive + buf[10] = 0x00; + buf[20] = b'x'; buf[127] = 0xAA; - let mut op = calldata_op(OP_CREATE); - op.attributes.push(CalldataAttribute { + let mut op = raw_op(OP_CREATE); + op.attributes.push(Attribute { name: ident32("garbage"), valueType: ATTR_STRING, value: calldata_value(buf), }); - let Operation::Create(c) = + let ArkivOperation::Create(c) = decode_operation(0, &op, &entity_event(OP_CREATE), &hash_event()).unwrap() else { unreachable!(); }; - let Attribute::String { value, .. } = &c.attributes[0] else { + let ArkivAttribute::String { value, .. } = &c.attributes[0] else { panic!("expected String"); }; assert_eq!(value.as_slice(), &buf); @@ -557,20 +619,20 @@ mod tests { let key = B256::repeat_byte(0x77); let mut value = [FixedBytes::ZERO; 4]; value[0] = key; - let mut op = calldata_op(OP_CREATE); - op.attributes.push(CalldataAttribute { + let mut op = raw_op(OP_CREATE); + op.attributes.push(Attribute { name: ident32("linked.to"), valueType: ATTR_ENTITY_KEY, value, }); - let Operation::Create(c) = + let ArkivOperation::Create(c) = decode_operation(0, &op, &entity_event(OP_CREATE), &hash_event()).unwrap() else { unreachable!(); }; assert_eq!( c.attributes[0], - Attribute::EntityKey { + ArkivAttribute::EntityKey { name: Ident32::encode("linked.to").unwrap(), value: key, }, @@ -581,8 +643,8 @@ mod tests { fn decode_attribute_rejects_uint_with_nonzero_higher_words() { let mut value = u256_word(7); value[2] = FixedBytes::repeat_byte(0xFF); - let mut op = calldata_op(OP_CREATE); - op.attributes.push(CalldataAttribute { + let mut op = raw_op(OP_CREATE); + op.attributes.push(Attribute { name: ident32("bad"), valueType: ATTR_UINT, value, @@ -595,8 +657,8 @@ mod tests { #[test] fn decode_attribute_rejects_unknown_value_type() { - let mut op = calldata_op(OP_CREATE); - op.attributes.push(CalldataAttribute { + let mut op = raw_op(OP_CREATE); + op.attributes.push(Attribute { name: ident32("unknown"), valueType: 99, value: [FixedBytes::ZERO; 4], @@ -608,19 +670,19 @@ mod tests { } // ------------------------------------------------------------------------- - // Serde JSON shape tests (gated on feature) + // Serde JSON shape tests // ------------------------------------------------------------------------- #[cfg(feature = "serde-wire")] #[test] fn create_op_json_shape() { - let mut op = calldata_op(OP_CREATE); - op.attributes.push(CalldataAttribute { + let mut op = raw_op(OP_CREATE); + op.attributes.push(Attribute { name: ident32("priority"), valueType: ATTR_UINT, value: u256_word(42), }); - op.attributes.push(CalldataAttribute { + op.attributes.push(Attribute { name: ident32("title"), valueType: ATTR_STRING, value: calldata_value(string_buf("note")), @@ -630,17 +692,15 @@ mod tests { assert_eq!(json["type"], "create"); assert_eq!(json["opIndex"], 0); - assert_eq!(json["expiresAt"], "0x4d2"); // 1234 in hex - assert_eq!(json["payload"], "0x68656c6c6f"); // "hello" + assert_eq!(json["expiresAt"], "0x4d2"); + assert_eq!(json["payload"], "0x68656c6c6f"); assert_eq!(json["contentType"], "text/plain"); let attr0 = &json["attributes"][0]; assert_eq!(attr0["valueType"], "uint"); assert_eq!(attr0["name"], "priority"); - assert_eq!(attr0["value"], "0x2a"); // 42 in hex + assert_eq!(attr0["value"], "0x2a"); - // String value is the full 128 bytes (left-aligned "note" + zero - // padding) as a single hex string — byte-exact, no truncation. let attr1 = &json["attributes"][1]; assert_eq!(attr1["valueType"], "string"); assert_eq!(attr1["name"], "title"); @@ -659,7 +719,7 @@ mod tests { (OP_DELETE, "delete"), (OP_EXPIRE, "expire"), ] { - let op = calldata_op(ty); + let op = raw_op(ty); let decoded = decode_operation(0, &op, &entity_event(ty), &hash_event()).unwrap(); let json = serde_json::to_value(&decoded).unwrap(); assert_eq!(json["type"], expected_tag, "wrong tag for op_type {}", ty); @@ -670,7 +730,7 @@ mod tests { #[test] fn entity_key_attribute_json_shape() { let key = B256::repeat_byte(0x42); - let attr = Attribute::EntityKey { + let attr = ArkivAttribute::EntityKey { name: Ident32::encode("linked.to").unwrap(), value: key, }; From 66d6a9143b28fd36a19b52cbdf98de0ba1a97cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 13:38:59 +0100 Subject: [PATCH 18/20] More improvements --- src/wire.rs | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 2 deletions(-) diff --git a/src/wire.rs b/src/wire.rs index 7b6866e..0805b00 100644 --- a/src/wire.rs +++ b/src/wire.rs @@ -37,13 +37,14 @@ //! //! Serde annotations are gated behind the `serde-wire` feature (default on). -use alloy_primitives::{Address, B256, Bytes, FixedBytes, U256}; +use alloy_primitives::{Address, B256, Bytes, FixedBytes, Log, U256}; +use alloy_sol_types::{SolCall, SolEvent}; use eyre::{Result, bail}; #[cfg(feature = "serde-wire")] use serde::Serialize; -use crate::IEntityRegistry::{ChangeSetHashUpdate, EntityOperation}; +use crate::IEntityRegistry::{ChangeSetHashUpdate, EntityOperation, executeCall}; use crate::{ ATTR_ENTITY_KEY, ATTR_STRING, ATTR_UINT, Attribute, Ident32, Mime128, OP_CREATE, OP_DELETE, OP_EXPIRE, OP_EXTEND, OP_TRANSFER, OP_UPDATE, Operation, @@ -357,6 +358,139 @@ fn require_single_word(value: &[FixedBytes<32>; 4], value_type: u8) -> Result<() Ok(()) } +// ----------------------------------------------------------------------------- +// ParsedRegistryTx — intermediate between raw on-chain data and ArkivOperation +// ----------------------------------------------------------------------------- + +/// A decoded EntityRegistry `execute()` call with calldata ops paired 1:1 +/// with their emitted events, validated for count and entityKey correspondence. +/// +/// # Usage +/// +/// ```text +/// ParsedRegistryTx::parse(calldata, logs) // decode + pair + validate +/// .decode(tx_hash, tx_index, sender) // validate content, produce ArkivTransaction +/// ``` +/// +/// `logs` must be pre-filtered to only those emitted by the EntityRegistry +/// address for the transaction being decoded. The caller owns the address +/// filter; this type is deployment-agnostic. +pub struct ParsedRegistryTx { + ops: Vec, +} + +struct PairedOp { + calldata: Operation, + entity_event: EntityOperation, + hash_event: ChangeSetHashUpdate, +} + +impl ParsedRegistryTx { + /// Decode raw calldata and pre-filtered registry logs, pairing and + /// validating each calldata op against its two emitted events. + /// + /// Pairing contract (mirrors `EntityRegistry.sol::execute`): + /// each calldata op causes `_dispatch` to emit one `EntityOperation` + /// followed by one `ChangeSetHashUpdate`. The three sequences are + /// therefore 1:1 in emission order. Anything else is contract drift. + /// + /// Fails if: + /// - calldata is not a valid `execute()` call + /// - any log is not `EntityOperation` or `ChangeSetHashUpdate` + /// - the three sequences are not the same length + /// - any `(EntityOperation, ChangeSetHashUpdate)` pair has mismatched `entityKey` + pub fn parse(input: &[u8], logs: &[Log]) -> Result { + let call = executeCall::abi_decode(input)?; + + let mut entity_events: Vec = Vec::new(); + let mut hash_events: Vec = Vec::new(); + + for log in logs { + match log.topics().first() { + Some(t) if *t == EntityOperation::SIGNATURE_HASH => { + entity_events.push(EntityOperation::decode_log_data(&log.data)?); + } + Some(t) if *t == ChangeSetHashUpdate::SIGNATURE_HASH => { + hash_events.push(ChangeSetHashUpdate::decode_log_data(&log.data)?); + } + other => bail!("unexpected log from EntityRegistry: topic0={:?}", other), + } + } + + if call.ops.len() != entity_events.len() || call.ops.len() != hash_events.len() { + bail!( + "event/calldata length mismatch: ops={}, entity_events={}, hash_events={}", + call.ops.len(), + entity_events.len(), + hash_events.len(), + ); + } + + let mut ops = Vec::with_capacity(call.ops.len()); + for (i, ((calldata, entity_event), hash_event)) in call + .ops + .into_iter() + .zip(entity_events) + .zip(hash_events) + .enumerate() + { + if entity_event.entityKey != hash_event.entityKey { + bail!( + "entityKey mismatch at op_index={}: EntityOperation={}, ChangeSetHashUpdate={}", + i, + entity_event.entityKey, + hash_event.entityKey, + ); + } + ops.push(PairedOp { calldata, entity_event, hash_event }); + } + + Ok(Self { ops }) + } + + /// Decode paired ops into a complete [`ArkivTransaction`]. + /// + /// Takes the transaction-level fields that only the caller can provide + /// (`hash`, `index`, `sender`) and returns the fully assembled transaction + /// alongside the changeset hash after the last op (`None` if no ops). + /// The caller threads that hash through the block-level rolling value. + /// + /// Fails if any op's `Mime128` content type or `Ident32` attribute name + /// is invalid. + pub fn decode( + self, + hash: B256, + index: u32, + sender: Address, + ) -> Result<(ArkivTransaction, Option)> { + let mut operations = Vec::with_capacity(self.ops.len()); + let mut last_hash: Option = None; + + for (i, paired) in self.ops.into_iter().enumerate() { + let op = decode_operation( + i as u32, + &paired.calldata, + &paired.entity_event, + &paired.hash_event, + )?; + last_hash = Some(paired.hash_event.changeSetHash); + operations.push(op); + } + + Ok((ArkivTransaction { hash, index, sender, operations }, last_hash)) + } + + /// Returns `true` if there are no operations in this transaction. + pub fn is_empty(&self) -> bool { + self.ops.is_empty() + } + + /// Number of operations in this transaction. + pub fn len(&self) -> usize { + self.ops.len() + } +} + // ----------------------------------------------------------------------------- // Hex u64 serializer — block numbers and expiry as JSON hex strings ("0x…"). // ----------------------------------------------------------------------------- From f58ee17ee5a83abcc68bb0901ca4f35a0cf45df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 13:48:40 +0100 Subject: [PATCH 19/20] added encoding helpers and builder patterns --- build.rs | 9 +- src/encode.rs | 268 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/encode.rs diff --git a/build.rs b/build.rs index 7c47be5..225504c 100644 --- a/build.rs +++ b/build.rs @@ -113,7 +113,14 @@ fn generate_sol_from_abi(abi: &serde_json::Value) -> String { // 2. Structs for (name, components) in &structs { - out.push_str(" #[derive(Debug, PartialEq, Eq)]\n"); + let derives = if name == "Attribute" { + // Default for Attribute would give valueType=0 (UNINITIALIZED), + // which the contract rejects. Omit it intentionally. + " #[derive(Debug, PartialEq, Eq)]\n" + } else { + " #[derive(Debug, Default, PartialEq, Eq)]\n" + }; + out.push_str(derives); out.push_str(&format!(" struct {} {{\n", name)); for comp in components { let sol_type = param_sol_type(comp); diff --git a/src/encode.rs b/src/encode.rs new file mode 100644 index 0000000..a064961 --- /dev/null +++ b/src/encode.rs @@ -0,0 +1,268 @@ +//! Encoding helpers — Rust types → [`Operation`] / [`Attribute`] calldata. +//! +//! These methods are the primary interface for building `execute()` calldata. +//! Each op type has a constructor that accepts only the fields relevant to +//! that op and zeros the rest, so callers never need to know the full flat +//! struct layout. +//! +//! # Attributes +//! +//! [`Attribute`] constructors handle the `bytes32[4]` value packing for each +//! `valueType`. The contract requires attributes to be sorted ascending by +//! name for deterministic hashing and name-uniqueness enforcement; +//! [`Operation::create`] and [`Operation::update`] sort automatically. +//! Call [`Attribute::sort`] before passing a pre-built slice to the raw +//! struct if you bypass the factory methods. + +use alloy_primitives::{Address, B256, Bytes, FixedBytes, U256}; +use eyre::{Result, bail}; + +use crate::{ + ATTR_ENTITY_KEY, ATTR_STRING, ATTR_UINT, Attribute, Ident32, Mime128, OP_CREATE, OP_DELETE, + OP_EXPIRE, OP_EXTEND, OP_TRANSFER, OP_UPDATE, Operation, +}; + +// ----------------------------------------------------------------------------- +// Operation constructors +// ----------------------------------------------------------------------------- + +impl Operation { + /// Create a new entity. + /// + /// `btl` is blocks-to-live from the current block + /// (`expiresAt = currentBlock + btl`). Attributes are sorted by name + /// ascending automatically. + pub fn create(btl: u32, payload: Bytes, content_type: Mime128, mut attributes: Vec) -> Self { + Attribute::sort(&mut attributes); + Self { + operationType: OP_CREATE, + entityKey: B256::ZERO, + payload, + contentType: content_type, + attributes, + btl, + newOwner: Address::ZERO, + } + } + + /// Update an existing entity's payload, content type, and attributes. + /// + /// Attributes are sorted by name ascending automatically. + pub fn update(entity_key: B256, payload: Bytes, content_type: Mime128, mut attributes: Vec) -> Self { + Attribute::sort(&mut attributes); + Self { + operationType: OP_UPDATE, + entityKey: entity_key, + payload, + contentType: content_type, + attributes, + ..Default::default() + } + } + + /// Extend an entity's expiry by `btl` blocks from the current block. + pub fn extend(entity_key: B256, btl: u32) -> Self { + Self { + operationType: OP_EXTEND, + entityKey: entity_key, + btl, + ..Default::default() + } + } + + /// Transfer entity ownership. + pub fn transfer(entity_key: B256, new_owner: Address) -> Self { + Self { + operationType: OP_TRANSFER, + entityKey: entity_key, + newOwner: new_owner, + ..Default::default() + } + } + + /// Delete an entity before its expiry. + pub fn delete(entity_key: B256) -> Self { + Self { + operationType: OP_DELETE, + entityKey: entity_key, + ..Default::default() + } + } + + /// Remove an expired entity from storage. + pub fn expire(entity_key: B256) -> Self { + Self { + operationType: OP_EXPIRE, + entityKey: entity_key, + ..Default::default() + } + } +} + +// ----------------------------------------------------------------------------- +// Attribute constructors +// ----------------------------------------------------------------------------- + +impl Attribute { + /// Build a `ATTR_UINT` attribute. Value is right-aligned in `data[0]`. + pub fn uint(name: Ident32, value: U256) -> Self { + let mut v = [FixedBytes::ZERO; 4]; + v[0] = FixedBytes::from(value.to_be_bytes::<32>()); + Self { name: name.0, valueType: ATTR_UINT, value: v } + } + + /// Build an `ATTR_STRING` attribute from raw bytes. + /// + /// At most 128 bytes; the protocol treats the value as opaque — UTF-8 + /// is convention only. Bytes are left-aligned across the four slots. + pub fn string(name: Ident32, value: &[u8]) -> Result { + if value.len() > 128 { + bail!("ATTR_STRING value exceeds 128 bytes ({})", value.len()); + } + let mut v = [FixedBytes::ZERO; 4]; + for (i, chunk) in value.chunks(32).enumerate() { + let mut buf = [0u8; 32]; + buf[..chunk.len()].copy_from_slice(chunk); + v[i] = FixedBytes::from(buf); + } + Ok(Self { name: name.0, valueType: ATTR_STRING, value: v }) + } + + /// Build an `ATTR_ENTITY_KEY` attribute. Key is stored in `data[0]`. + pub fn entity_key(name: Ident32, key: B256) -> Self { + let mut v = [FixedBytes::ZERO; 4]; + v[0] = key; + Self { name: name.0, valueType: ATTR_ENTITY_KEY, value: v } + } + + /// Sort attributes by name ascending. + /// + /// The contract requires strict ascending order for deterministic hashing + /// and to enforce name uniqueness. Called automatically by + /// [`Operation::create`] and [`Operation::update`]. + pub fn sort(attrs: &mut [Self]) { + attrs.sort_by_key(|a| a.name); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn name(s: &str) -> Ident32 { + Ident32::encode(s).unwrap() + } + + // ------------------------------------------------------------------------- + // Operation constructors + // ------------------------------------------------------------------------- + + #[test] + fn create_sets_op_type_and_zeroes_entity_key() { + let op = Operation::create(100, Bytes::new(), Mime128::default(), vec![]); + assert_eq!(op.operationType, OP_CREATE); + assert_eq!(op.entityKey, B256::ZERO); + assert_eq!(op.btl, 100); + assert_eq!(op.newOwner, Address::ZERO); + } + + #[test] + fn create_sorts_attributes() { + let attrs = vec![ + Attribute::uint(name("z.attr"), U256::from(1)), + Attribute::uint(name("a.attr"), U256::from(2)), + ]; + let op = Operation::create(100, Bytes::new(), Mime128::default(), attrs); + // a.attr < z.attr lexicographically + assert!(op.attributes[0].name < op.attributes[1].name); + } + + #[test] + fn update_sets_op_type() { + let op = Operation::update(B256::repeat_byte(1), Bytes::new(), Mime128::default(), vec![]); + assert_eq!(op.operationType, OP_UPDATE); + assert_eq!(op.entityKey, B256::repeat_byte(1)); + assert_eq!(op.btl, 0); + } + + #[test] + fn extend_sets_btl() { + let key = B256::repeat_byte(2); + let op = Operation::extend(key, 500); + assert_eq!(op.operationType, OP_EXTEND); + assert_eq!(op.entityKey, key); + assert_eq!(op.btl, 500); + } + + #[test] + fn transfer_sets_new_owner() { + let key = B256::repeat_byte(3); + let owner = Address::repeat_byte(0xAB); + let op = Operation::transfer(key, owner); + assert_eq!(op.operationType, OP_TRANSFER); + assert_eq!(op.newOwner, owner); + } + + #[test] + fn delete_and_expire_set_op_types() { + let key = B256::repeat_byte(4); + assert_eq!(Operation::delete(key).operationType, OP_DELETE); + assert_eq!(Operation::expire(key).operationType, OP_EXPIRE); + } + + // ------------------------------------------------------------------------- + // Attribute constructors + // ------------------------------------------------------------------------- + + #[test] + fn uint_attr_packs_value_in_slot_zero() { + let attr = Attribute::uint(name("count"), U256::from(42)); + assert_eq!(attr.valueType, ATTR_UINT); + assert_eq!(attr.value[0], FixedBytes::from(U256::from(42).to_be_bytes::<32>())); + assert_eq!(attr.value[1], FixedBytes::ZERO); + } + + #[test] + fn string_attr_packs_bytes_left_aligned() { + let attr = Attribute::string(name("title"), b"hello").unwrap(); + assert_eq!(attr.valueType, ATTR_STRING); + let expected: [u8; 32] = { + let mut buf = [0u8; 32]; + buf[..5].copy_from_slice(b"hello"); + buf + }; + assert_eq!(attr.value[0], FixedBytes::from(expected)); + assert_eq!(attr.value[1], FixedBytes::ZERO); + } + + #[test] + fn string_attr_rejects_over_128_bytes() { + assert!(Attribute::string(name("big"), &[0u8; 129]).is_err()); + } + + #[test] + fn string_attr_accepts_exactly_128_bytes() { + assert!(Attribute::string(name("full"), &[0u8; 128]).is_ok()); + } + + #[test] + fn entity_key_attr_packs_key_in_slot_zero() { + let key = B256::repeat_byte(0x77); + let attr = Attribute::entity_key(name("linked.to"), key); + assert_eq!(attr.valueType, ATTR_ENTITY_KEY); + assert_eq!(attr.value[0], key); + assert_eq!(attr.value[1], FixedBytes::ZERO); + } + + #[test] + fn sort_orders_by_name_ascending() { + let mut attrs = vec![ + Attribute::uint(name("z.last"), U256::ZERO), + Attribute::uint(name("a.first"), U256::ZERO), + Attribute::uint(name("m.mid"), U256::ZERO), + ]; + Attribute::sort(&mut attrs); + assert!(attrs[0].name < attrs[1].name); + assert!(attrs[1].name < attrs[2].name); + } +} diff --git a/src/lib.rs b/src/lib.rs index 2267172..85eae01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod encode; pub mod storage_layout; pub mod types; pub mod wire; From f0a6e62a2c8f42d3a1266f5675cd7c6200cee14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20=C3=93=20Mhuiris?= Date: Fri, 8 May 2026 14:01:31 +0100 Subject: [PATCH 20/20] update architecture.md --- docs/architecture.md | 199 ++++++++++++++++++++++++++++++------------- 1 file changed, 142 insertions(+), 57 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 7ec9a40..1024e26 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -144,18 +144,21 @@ point that accepts batches. ### Operations **CREATE** — Mint a new entity. The caller becomes both creator and owner. -Requires a future expiry block, valid content type, and valid attributes. -The entity key is derived deterministically from the caller's address and -a monotonic nonce. +Requires a non-zero `btl` (blocks-to-live), valid content type, and valid +attributes. The entity key is derived deterministically from the caller's +address and a monotonic nonce. The absolute expiry is computed on-chain as +`currentBlock + btl`; callers supply a relative duration, never an absolute +block number. **UPDATE** — Replace the entity's content (payload, content type, attributes). Only the owner can update. Does not change ownership or expiry. The content hash (`coreHash`) is fully recomputed from the new content. **EXTEND** — Push the expiry further into the future. Only the owner can -extend. The new expiry must be strictly greater than the current one. -Content and ownership are untouched — only `expiresAt` and `updatedAt` -change. +extend. Accepts a `btl` (blocks-to-live); the new absolute expiry is +computed as `currentBlock + btl` and must be strictly greater than the +currently stored `expiresAt`. Content and ownership are untouched — only +`expiresAt` and `updatedAt` change. **TRANSFER** — Change the entity's owner. Only the current owner can transfer. The previous owner loses all access immediately — they cannot @@ -369,26 +372,28 @@ Every operation emits **two** events, indexed and consumed together: ``` event EntityOperation( - bytes32 indexed entityKey, - uint8 indexed operationType, - address indexed owner, - BlockNumber expiresAt, - bytes32 entityHash + bytes32 indexed entityKey, + uint8 indexed operationType, + address indexed owner, + BlockNumber32 expiresAt, -- absolute: currentBlock + op.btl + bytes32 entityHash ) event ChangeSetHashUpdate( - bytes32 indexed entityKey, + bytes32 indexed entityKey, OperationKey indexed operationKey, - bytes32 changeSetHash + bytes32 changeSetHash ) ``` -`EntityOperation` carries the per-op state needed by indexers; the -resulting changeset hash and the packed `(block, tx, op)` operation key -are emitted alongside it as `ChangeSetHashUpdate`. Off-chain decoders -pair the two events to reconstruct each operation without an extra RPC -round-trip — the changeset hash arrives in-band with the operation -logs. +`EntityOperation` carries the per-op state needed by indexers. The +`expiresAt` field is the **absolute** expiry block the contract computed +as `currentBlock + op.btl` — the raw `btl` calldata field is not +emitted. The resulting changeset hash and the packed `(block, tx, op)` +operation key are emitted alongside it as `ChangeSetHashUpdate`. +Off-chain decoders pair the two events to reconstruct each operation +without an extra RPC round-trip — the changeset hash arrives in-band +with the operation logs. ```mermaid flowchart LR @@ -462,44 +467,124 @@ flowchart LR Sol --> Forge --> Artifacts --> Build --> Gen --> Crate --> Consumer ``` -`build.rs` invokes `forge build` if the artifacts are stale, parses the -Foundry output, and emits generated Rust into `OUT_DIR` which `lib.rs` -`include!`s. The crate exposes four layers: - -1. **Auto-generated ABI types** — an `alloy_sol_types::sol!` block - derived from the `IEntityRegistry` ABI covers every function, event, - error, and struct (`Operation`, `Attribute`, `Mime128`, `Commitment`, - `BlockNode`). The `EntityRegistry` creation bytecode is embedded as - a string constant (`ENTITY_REGISTRY_CREATION_CODE`) for deployment - from Rust. Op-type and attribute-type discriminators are re-exported - as `OP_*` / `ATTR_*` constants pinned to the contract. - -2. **Validating identifier types** — `types::Ident32` and - `types::Mime128Str` mirror the on-chain validation rules (lowercase - `a-z 0-9 . - _`, leading-letter requirement, max length, RFC 2045 - MIME grammar) so Rust SDKs reject bad input *before* submitting a - transaction. Encoders and decoders round-trip through the - `bytes32 / bytes32[4]` containers the contract uses. - -3. **Storage layout helpers** — `storage_layout` exposes slot indices - (accounting for OpenZeppelin's `EIP712` base occupying slots 0–1) - and key-packing functions (`operation_key`, `transaction_key`, - `mapping_slot`) that mirror `Entity.sol` exactly. This lets the ExEx - recompute the rolling changeset hash by reading slots directly at - historical block state, without spinning up an EVM. A test asserts - the constants match the Foundry `storageLayout` artifact, so - contract drift is caught at build time. - -4. **Wire decoder** — `wire::decode_operation` pairs a decoded - calldata `Operation` with its two emitted events (`EntityOperation` - + `ChangeSetHashUpdate`) into a tagged `Operation` enum (`Create`, - `Update`, `Extend`, `Transfer`, `Delete`, `Expire`) and decodes - attributes into a typed `Attribute` enum (`Uint` → `U256`, `String` - → opaque `FixedBytes<128>`, `EntityKey` → `B256`). When the - `serde-wire` cargo feature is enabled (default on), the types - serialize to the JSON shape the EntityDB consumes over its v2 - ExEx → EntityDB JSON-RPC interface (block numbers and expiries as - `0x…` hex strings, attribute values tagged by `valueType`). +`build.rs` invokes `forge build` if the artifacts are stale, then emits +an `alloy_sol_types::sol!` block as inline Solidity into `OUT_DIR/sol.rs` +which `lib.rs` `include!`s. The generator resolves type declaration order +correctly: UDVTs first, then structs that reference them, then the +interface. The crate exposes five layers: + +### 1. Auto-generated ABI types + +An `alloy_sol_types::sol!` block derived from `IEntityRegistry` covers +every function, event, error, and data structure. At the crate root: + +- **UDVTs**: `BlockNumber32` (`uint32`), `Ident32` (`bytes32`), + `OperationKey` (`uint256`) — Solidity user-defined value types, + generated as Rust newtypes. +- **Structs**: `Operation`, `Attribute`, `Mime128`, `Commitment`, + `BlockNode` — ABI-decoded structs. Fields use the underlying primitive + RustType (`u32` for `BlockNumber32`, `FixedBytes<32>` for `Ident32`); + the UDVT wrapper types are used at the encoding boundary. +- **Interface module**: `IEntityRegistry` contains typed call/event/error + types for every contract entry point. + +The `EntityRegistry` creation bytecode is embedded as +`ENTITY_REGISTRY_CREATION_CODE`. Op-type and attribute-type +discriminators are re-exported as `OP_*` / `ATTR_*` constants. + +### 2. Validated types (`src/types/`) + +Validation logic is added directly to the alloy-generated types as `impl` +blocks, keeping the ABI types as the single source of truth: + +- **`Ident32`** — `encode(s: &str)` validates the charset (`a-z 0-9 . - _`, + must start `a-z`, max 32 bytes). `validate(self)` additionally enforces + that once a null byte appears all remaining bytes are also null (no + embedded nulls), matching `validateIdent32` in `Ident32.sol`. + Implements `Display` and `Serialize` (serialises as ASCII string). + +- **`Mime128`** — `encode(s: &str)` validates the RFC 2045 structure + (`type/subtype[; param=value]*`, lowercase only, max 128 bytes), + matching `validateMime128` in `Mime128.sol`. `as_str()` decodes the + `bytes32[4]` container back to a string. Implements `Display` and + `Serialize` (serialises as string). + +### 3. Calldata encoding (`src/encode.rs`) + +Factory methods on the ABI-generated types for building `execute()` +calldata without manually filling the flat `Operation` struct: + +```rust +// Per-op constructors: only required fields, rest zeroed +Operation::create(btl, payload, content_type, attributes) +Operation::update(entity_key, payload, content_type, attributes) +Operation::extend(entity_key, btl) +Operation::transfer(entity_key, new_owner) +Operation::delete(entity_key) +Operation::expire(entity_key) + +// Attribute constructors: handles bytes32[4] packing per valueType +Attribute::uint(name, value) // right-aligns U256 in data[0] +Attribute::string(name, bytes) // left-aligns up to 128 bytes +Attribute::entity_key(name, key) // stores B256 in data[0] +Attribute::sort(attrs) // sort ascending by name (required) +``` + +`Operation::create` and `Operation::update` call `Attribute::sort` +automatically. All constructors are infallible except `Attribute::string` +(validates the 128-byte limit). + +### 4. Storage layout helpers (`src/storage_layout.rs`) + +Slot indices, key-packing functions (`operation_key`, `transaction_key`, +`mapping_slot`), and slot decoders that mirror `Entity.sol` exactly. This +lets the ExEx recompute the rolling changeset hash by reading storage slots +directly at historical block state without spinning up an EVM. A test +asserts all constants match the Foundry `storageLayout` artifact, catching +contract drift at build time. + +### 5. Wire decoder (`src/wire.rs`) + +Types and decoding logic for the ExEx → EntityDB JSON-RPC interface: + +**Envelope types** (the full block/transaction hierarchy): +- `ArkivBlock` — block header + transactions +- `ArkivBlockHeader` — number, hash, parent hash, rolling changeset hash +- `ArkivTransaction` — tx hash, index, sender, decoded operations +- `ArkivBlockRef` — minimal block identifier for revert payloads + +**Decoded operation types**: +- `ArkivOperation` — tagged enum (`Create`, `Update`, `Extend`, + `Transfer`, `Delete`, `Expire`). `expires_at` on `CreateOp`/`ExtendOp` + is the absolute block number sourced from the `EntityOperation` event + (i.e. `currentBlock + op.btl`); the raw `btl` from calldata is not + exposed. +- `ArkivAttribute` — tagged enum (`Uint` → `U256`, `String` → + `FixedBytes<128>` opaque, `EntityKey` → `B256`). Attribute names are + `Ident32` values serialising as ASCII strings. + +**`ParsedRegistryTx`** — the intermediate between raw on-chain data and +a fully decoded `ArkivTransaction`: + +```rust +// Step 1: decode calldata, pair events, validate 1:1 correspondence +let parsed = ParsedRegistryTx::parse(calldata_bytes, ®istry_logs)?; + +// Step 2: validate Mime128 / Ident32, produce ArkivTransaction +let (tx, last_changeset_hash) = parsed.decode(tx_hash, tx_index, sender)?; +``` + +Logs must be pre-filtered to the EntityRegistry address by the caller; +`parse` handles event separation, count validation, and per-op `entityKey` +cross-checks. `decode` validates content types and attribute names, +returning the complete `ArkivTransaction` alongside the final changeset +hash for the caller to thread through the block-level rolling value. + +When the `serde-wire` feature is enabled (default on), all wire types +serialise to the JSON shape the EntityDB consumes (block numbers and +expiries as `0x…` hex strings, attributes tagged by `valueType`). + +--- The crate is the contract's primary off-chain consumer surface — every type, event, error, and storage layout that a Rust client needs is