From e977993f6dfd5d5487e8d9da5736921c4bc44858 Mon Sep 17 00:00:00 2001 From: Julink Date: Thu, 18 Jul 2024 22:38:26 +0200 Subject: [PATCH 01/13] chore: wip --- .../clones-with-immutable-args/Clone.sol | 89 +++ .../ClonesWithImmutableArgs.sol | 513 ++++++++++++++++++ .../DelegatableResolver.sol | 134 +++++ .../DelegatableResolverFactory.sol | 43 ++ .../IDelegatableResolver.sol | 18 + .../delegatableResolver/IMulticallable.sol | 13 + .../delegatableResolver/Multicallable.sol | 53 ++ .../linea-ens-resolver/contracts/deps.sol | 1 + packages/linea-ens-resolver/package.json | 9 +- .../test/testL1Resolver.spec.ts | 152 ++++-- pnpm-lock.yaml | 78 ++- 11 files changed, 1028 insertions(+), 75 deletions(-) create mode 100644 packages/linea-ens-resolver/contracts/clones-with-immutable-args/Clone.sol create mode 100644 packages/linea-ens-resolver/contracts/clones-with-immutable-args/ClonesWithImmutableArgs.sol create mode 100644 packages/linea-ens-resolver/contracts/delegatableResolver/DelegatableResolver.sol create mode 100644 packages/linea-ens-resolver/contracts/delegatableResolver/DelegatableResolverFactory.sol create mode 100644 packages/linea-ens-resolver/contracts/delegatableResolver/IDelegatableResolver.sol create mode 100644 packages/linea-ens-resolver/contracts/delegatableResolver/IMulticallable.sol create mode 100644 packages/linea-ens-resolver/contracts/delegatableResolver/Multicallable.sol diff --git a/packages/linea-ens-resolver/contracts/clones-with-immutable-args/Clone.sol b/packages/linea-ens-resolver/contracts/clones-with-immutable-args/Clone.sol new file mode 100644 index 000000000..c8bd990af --- /dev/null +++ b/packages/linea-ens-resolver/contracts/clones-with-immutable-args/Clone.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BSD +pragma solidity ^0.8.4; + +/// @title Clone +/// @author zefram.eth +/// @notice Provides helper functions for reading immutable args from calldata +contract Clone { + /// @notice Reads an immutable arg with type address + /// @param argOffset The offset of the arg in the packed data + /// @return arg The arg value + function _getArgAddress( + uint256 argOffset + ) internal pure returns (address arg) { + uint256 offset = _getImmutableArgsOffset(); + // solhint-disable-next-line no-inline-assembly + assembly { + arg := shr(0x60, calldataload(add(offset, argOffset))) + } + } + + /// @notice Reads an immutable arg with type uint256 + /// @param argOffset The offset of the arg in the packed data + /// @return arg The arg value + function _getArgUint256( + uint256 argOffset + ) internal pure returns (uint256 arg) { + uint256 offset = _getImmutableArgsOffset(); + // solhint-disable-next-line no-inline-assembly + assembly { + arg := calldataload(add(offset, argOffset)) + } + } + + /// @notice Reads a uint256 array stored in the immutable args. + /// @param argOffset The offset of the arg in the packed data + /// @param arrLen Number of elements in the array + /// @return arr The array + function _getArgUint256Array( + uint256 argOffset, + uint64 arrLen + ) internal pure returns (uint256[] memory arr) { + uint256 offset = _getImmutableArgsOffset(); + uint256 el; + arr = new uint256[](arrLen); + for (uint64 i = 0; i < arrLen; i++) { + // solhint-disable-next-line no-inline-assembly + assembly { + el := calldataload(add(add(offset, argOffset), mul(i, 32))) + } + arr[i] = el; + } + return arr; + } + + /// @notice Reads an immutable arg with type uint64 + /// @param argOffset The offset of the arg in the packed data + /// @return arg The arg value + function _getArgUint64( + uint256 argOffset + ) internal pure returns (uint64 arg) { + uint256 offset = _getImmutableArgsOffset(); + // solhint-disable-next-line no-inline-assembly + assembly { + arg := shr(0xc0, calldataload(add(offset, argOffset))) + } + } + + /// @notice Reads an immutable arg with type uint8 + /// @param argOffset The offset of the arg in the packed data + /// @return arg The arg value + function _getArgUint8(uint256 argOffset) internal pure returns (uint8 arg) { + uint256 offset = _getImmutableArgsOffset(); + // solhint-disable-next-line no-inline-assembly + assembly { + arg := shr(0xf8, calldataload(add(offset, argOffset))) + } + } + + /// @return offset The offset of the packed immutable args in calldata + function _getImmutableArgsOffset() internal pure returns (uint256 offset) { + // solhint-disable-next-line no-inline-assembly + assembly { + offset := sub( + calldatasize(), + add(shr(240, calldataload(sub(calldatasize(), 2))), 2) + ) + } + } +} diff --git a/packages/linea-ens-resolver/contracts/clones-with-immutable-args/ClonesWithImmutableArgs.sol b/packages/linea-ens-resolver/contracts/clones-with-immutable-args/ClonesWithImmutableArgs.sol new file mode 100644 index 000000000..608cce382 --- /dev/null +++ b/packages/linea-ens-resolver/contracts/clones-with-immutable-args/ClonesWithImmutableArgs.sol @@ -0,0 +1,513 @@ +// SPDX-License-Identifier: BSD + +pragma solidity ^0.8.4; + +/// @title ClonesWithImmutableArgs +/// @author wighawag, zefram.eth, nick.eth +/// @notice Enables creating clone contracts with immutable args +library ClonesWithImmutableArgs { + /// @dev The CREATE3 proxy bytecode. + uint256 private constant _CREATE3_PROXY_BYTECODE = + 0x67363d3d37363d34f03d5260086018f3; + + /// @dev Hash of the `_CREATE3_PROXY_BYTECODE`. + /// Equivalent to `keccak256(abi.encodePacked(hex"67363d3d37363d34f03d5260086018f3"))`. + bytes32 private constant _CREATE3_PROXY_BYTECODE_HASH = + 0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f; + + error CreateFail(); + error InitializeFail(); + + enum CloneType { + CREATE, + CREATE2, + PREDICT_CREATE2 + } + + /// @notice Creates a clone proxy of the implementation contract, with immutable args + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @return instance The address of the created clone + function clone( + address implementation, + bytes memory data + ) internal returns (address payable instance) { + return clone(implementation, data, 0); + } + + /// @notice Creates a clone proxy of the implementation contract, with immutable args + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @param value The amount of wei to transfer to the created clone + /// @return instance The address of the created clone + function clone( + address implementation, + bytes memory data, + uint256 value + ) internal returns (address payable instance) { + bytes memory creationcode = getCreationBytecode(implementation, data); + // solhint-disable-next-line no-inline-assembly + assembly { + instance := create( + value, + add(creationcode, 0x20), + mload(creationcode) + ) + } + if (instance == address(0)) { + revert CreateFail(); + } + } + + /// @notice Creates a clone proxy of the implementation contract, with immutable args, + /// using CREATE2 + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @return instance The address of the created clone + function clone2( + address implementation, + bytes memory data + ) internal returns (address payable instance) { + return clone2(implementation, data, 0); + } + + /// @notice Creates a clone proxy of the implementation contract, with immutable args, + /// using CREATE2 + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @param value The amount of wei to transfer to the created clone + /// @return instance The address of the created clone + function clone2( + address implementation, + bytes memory data, + uint256 value + ) internal returns (address payable instance) { + bytes memory creationcode = getCreationBytecode(implementation, data); + // solhint-disable-next-line no-inline-assembly + assembly { + instance := create2( + value, + add(creationcode, 0x20), + mload(creationcode), + 0 + ) + } + if (instance == address(0)) { + revert CreateFail(); + } + } + + /// @notice Computes the address of a clone created using CREATE2 + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @return instance The address of the clone + function addressOfClone2( + address implementation, + bytes memory data + ) internal view returns (address payable instance) { + bytes memory creationcode = getCreationBytecode(implementation, data); + bytes32 bytecodeHash = keccak256(creationcode); + instance = payable( + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + bytes32(0), + bytecodeHash + ) + ) + ) + ) + ) + ); + } + + /// @notice Computes bytecode for a clone + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @return ret Creation bytecode for the clone contract + function getCreationBytecode( + address implementation, + bytes memory data + ) internal pure returns (bytes memory ret) { + // unrealistic for memory ptr or data length to exceed 256 bits + unchecked { + uint256 extraLength = data.length + 2; // +2 bytes for telling how much data there is appended to the call + uint256 creationSize = 0x41 + extraLength; + uint256 runSize = creationSize - 10; + uint256 dataPtr; + uint256 ptr; + + // solhint-disable-next-line no-inline-assembly + assembly { + ret := mload(0x40) + mstore(ret, creationSize) + mstore(0x40, add(ret, creationSize)) + ptr := add(ret, 0x20) + + // ------------------------------------------------------------------------------------------------------------- + // CREATION (10 bytes) + // ------------------------------------------------------------------------------------------------------------- + + // 61 runtime | PUSH2 runtime (r) | r | – + mstore( + ptr, + 0x6100000000000000000000000000000000000000000000000000000000000000 + ) + mstore(add(ptr, 0x01), shl(240, runSize)) // size of the contract running bytecode (16 bits) + + // creation size = 0a + // 3d | RETURNDATASIZE | 0 r | – + // 81 | DUP2 | r 0 r | – + // 60 creation | PUSH1 creation (c) | c r 0 r | – + // 3d | RETURNDATASIZE | 0 c r 0 r | – + // 39 | CODECOPY | 0 r | [0-runSize): runtime code + // f3 | RETURN | | [0-runSize): runtime code + + // ------------------------------------------------------------------------------------------------------------- + // RUNTIME (55 bytes + extraLength) + // ------------------------------------------------------------------------------------------------------------- + + // 3d | RETURNDATASIZE | 0 | – + // 3d | RETURNDATASIZE | 0 0 | – + // 3d | RETURNDATASIZE | 0 0 0 | – + // 3d | RETURNDATASIZE | 0 0 0 0 | – + // 36 | CALLDATASIZE | cds 0 0 0 0 | – + // 3d | RETURNDATASIZE | 0 cds 0 0 0 0 | – + // 3d | RETURNDATASIZE | 0 0 cds 0 0 0 0 | – + // 37 | CALLDATACOPY | 0 0 0 0 | [0, cds) = calldata + // 61 | PUSH2 extra | extra 0 0 0 0 | [0, cds) = calldata + mstore( + add(ptr, 0x03), + 0x3d81600a3d39f33d3d3d3d363d3d376100000000000000000000000000000000 + ) + mstore(add(ptr, 0x13), shl(240, extraLength)) + + // 60 0x37 | PUSH1 0x37 | 0x37 extra 0 0 0 0 | [0, cds) = calldata // 0x37 (55) is runtime size - data + // 36 | CALLDATASIZE | cds 0x37 extra 0 0 0 0 | [0, cds) = calldata + // 39 | CODECOPY | 0 0 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + // 36 | CALLDATASIZE | cds 0 0 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + // 61 extra | PUSH2 extra | extra cds 0 0 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + mstore( + add(ptr, 0x15), + 0x6037363936610000000000000000000000000000000000000000000000000000 + ) + mstore(add(ptr, 0x1b), shl(240, extraLength)) + + // 01 | ADD | cds+extra 0 0 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + // 3d | RETURNDATASIZE | 0 cds+extra 0 0 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + // 73 addr | PUSH20 0x123… | addr 0 cds+extra 0 0 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + mstore( + add(ptr, 0x1d), + 0x013d730000000000000000000000000000000000000000000000000000000000 + ) + mstore(add(ptr, 0x20), shl(0x60, implementation)) + + // 5a | GAS | gas addr 0 cds+extra 0 0 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + // f4 | DELEGATECALL | success 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + // 3d | RETURNDATASIZE | rds success 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + // 3d | RETURNDATASIZE | rds rds success 0 0 | [0, cds) = calldata, [cds, cds+extra) = extraData + // 93 | SWAP4 | 0 rds success 0 rds | [0, cds) = calldata, [cds, cds+extra) = extraData + // 80 | DUP1 | 0 0 rds success 0 rds | [0, cds) = calldata, [cds, cds+extra) = extraData + // 3e | RETURNDATACOPY | success 0 rds | [0, rds) = return data (there might be some irrelevant leftovers in memory [rds, cds+0x37) when rds < cds+0x37) + // 60 0x35 | PUSH1 0x35 | 0x35 sucess 0 rds | [0, rds) = return data + // 57 | JUMPI | 0 rds | [0, rds) = return data + // fd | REVERT | – | [0, rds) = return data + // 5b | JUMPDEST | 0 rds | [0, rds) = return data + // f3 | RETURN | – | [0, rds) = return data + mstore( + add(ptr, 0x34), + 0x5af43d3d93803e603557fd5bf300000000000000000000000000000000000000 + ) + } + + // ------------------------------------------------------------------------------------------------------------- + // APPENDED DATA (Accessible from extcodecopy) + // (but also send as appended data to the delegatecall) + // ------------------------------------------------------------------------------------------------------------- + + extraLength -= 2; + uint256 counter = extraLength; + uint256 copyPtr = ptr + 0x41; + // solhint-disable-next-line no-inline-assembly + assembly { + dataPtr := add(data, 32) + } + for (; counter >= 32; counter -= 32) { + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(copyPtr, mload(dataPtr)) + } + + copyPtr += 32; + dataPtr += 32; + } + uint256 mask = ~(256 ** (32 - counter) - 1); + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(copyPtr, and(mload(dataPtr), mask)) + } + copyPtr += counter; + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(copyPtr, shl(240, extraLength)) + } + } + } + + /// @notice Creates a clone proxy of the implementation contract, with immutable args. Uses CREATE3 + /// to implement deterministic deployment. + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @param salt The salt used by the CREATE3 deployment + /// @return deployed The address of the created clone + function clone3( + address implementation, + bytes memory data, + bytes32 salt + ) internal returns (address deployed) { + return clone3(implementation, data, salt, 0); + } + + /// @notice Creates a clone proxy of the implementation contract, with immutable args. Uses CREATE3 + /// to implement deterministic deployment. + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @param salt The salt used by the CREATE3 deployment + /// @param value The amount of wei to transfer to the created clone + /// @return deployed The address of the created clone + function clone3( + address implementation, + bytes memory data, + bytes32 salt, + uint256 value + ) internal returns (address deployed) { + // unrealistic for memory ptr or data length to exceed 256 bits + unchecked { + uint256 extraLength = data.length + 2; // +2 bytes for telling how much data there is appended to the call + uint256 creationSize = 0x43 + extraLength; + uint256 ptr; + // solhint-disable-next-line no-inline-assembly + assembly { + ptr := mload(0x40) + + // ------------------------------------------------------------------------------------------------------------- + // CREATION (11 bytes) + // ------------------------------------------------------------------------------------------------------------- + + // 3d | RETURNDATASIZE | 0 | – + // 61 runtime | PUSH2 runtime (r) | r 0 | – + mstore( + ptr, + 0x3d61000000000000000000000000000000000000000000000000000000000000 + ) + mstore(add(ptr, 0x02), shl(240, sub(creationSize, 11))) // size of the contract running bytecode (16 bits) + + // creation size = 0b + // 80 | DUP1 | r r 0 | – + // 60 creation | PUSH1 creation (c) | c r r 0 | – + // 3d | RETURNDATASIZE | 0 c r r 0 | – + // 39 | CODECOPY | r 0 | [0-2d]: runtime code + // 81 | DUP2 | 0 c 0 | [0-2d]: runtime code + // f3 | RETURN | 0 | [0-2d]: runtime code + mstore( + add(ptr, 0x04), + 0x80600b3d3981f300000000000000000000000000000000000000000000000000 + ) + + // ------------------------------------------------------------------------------------------------------------- + // RUNTIME + // ------------------------------------------------------------------------------------------------------------- + + // 36 | CALLDATASIZE | cds | – + // 3d | RETURNDATASIZE | 0 cds | – + // 3d | RETURNDATASIZE | 0 0 cds | – + // 37 | CALLDATACOPY | – | [0, cds] = calldata + // 61 | PUSH2 extra | extra | [0, cds] = calldata + mstore( + add(ptr, 0x0b), + 0x363d3d3761000000000000000000000000000000000000000000000000000000 + ) + mstore(add(ptr, 0x10), shl(240, extraLength)) + + // 60 0x38 | PUSH1 0x38 | 0x38 extra | [0, cds] = calldata // 0x38 (56) is runtime size - data + // 36 | CALLDATASIZE | cds 0x38 extra | [0, cds] = calldata + // 39 | CODECOPY | _ | [0, cds] = calldata + // 3d | RETURNDATASIZE | 0 | [0, cds] = calldata + // 3d | RETURNDATASIZE | 0 0 | [0, cds] = calldata + // 3d | RETURNDATASIZE | 0 0 0 | [0, cds] = calldata + // 36 | CALLDATASIZE | cds 0 0 0 | [0, cds] = calldata + // 61 extra | PUSH2 extra | extra cds 0 0 0 | [0, cds] = calldata + mstore( + add(ptr, 0x12), + 0x603836393d3d3d36610000000000000000000000000000000000000000000000 + ) + mstore(add(ptr, 0x1b), shl(240, extraLength)) + + // 01 | ADD | cds+extra 0 0 0 | [0, cds] = calldata + // 3d | RETURNDATASIZE | 0 cds 0 0 0 | [0, cds] = calldata + // 73 addr | PUSH20 0x123… | addr 0 cds 0 0 0 | [0, cds] = calldata + mstore( + add(ptr, 0x1d), + 0x013d730000000000000000000000000000000000000000000000000000000000 + ) + mstore(add(ptr, 0x20), shl(0x60, implementation)) + + // 5a | GAS | gas addr 0 cds 0 0 0 | [0, cds] = calldata + // f4 | DELEGATECALL | success 0 | [0, cds] = calldata + // 3d | RETURNDATASIZE | rds success 0 | [0, cds] = calldata + // 82 | DUP3 | 0 rds success 0 | [0, cds] = calldata + // 80 | DUP1 | 0 0 rds success 0 | [0, cds] = calldata + // 3e | RETURNDATACOPY | success 0 | [0, rds] = return data (there might be some irrelevant leftovers in memory [rds, cds] when rds < cds) + // 90 | SWAP1 | 0 success | [0, rds] = return data + // 3d | RETURNDATASIZE | rds 0 success | [0, rds] = return data + // 91 | SWAP2 | success 0 rds | [0, rds] = return data + // 60 0x36 | PUSH1 0x36 | 0x36 sucess 0 rds | [0, rds] = return data + // 57 | JUMPI | 0 rds | [0, rds] = return data + // fd | REVERT | – | [0, rds] = return data + // 5b | JUMPDEST | 0 rds | [0, rds] = return data + // f3 | RETURN | – | [0, rds] = return data + + mstore( + add(ptr, 0x34), + 0x5af43d82803e903d91603657fd5bf30000000000000000000000000000000000 + ) + } + + // ------------------------------------------------------------------------------------------------------------- + // APPENDED DATA (Accessible from extcodecopy) + // (but also send as appended data to the delegatecall) + // ------------------------------------------------------------------------------------------------------------- + + extraLength -= 2; + uint256 counter = extraLength; + uint256 copyPtr = ptr + 0x43; + uint256 dataPtr; + // solhint-disable-next-line no-inline-assembly + assembly { + dataPtr := add(data, 32) + } + for (; counter >= 32; counter -= 32) { + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(copyPtr, mload(dataPtr)) + } + + copyPtr += 32; + dataPtr += 32; + } + uint256 mask = ~(256 ** (32 - counter) - 1); + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(copyPtr, and(mload(dataPtr), mask)) + } + copyPtr += counter; + // solhint-disable-next-line no-inline-assembly + assembly { + mstore(copyPtr, shl(240, extraLength)) + } + + /// @solidity memory-safe-assembly + // solhint-disable-next-line no-inline-assembly + assembly { + // Store the `_PROXY_BYTECODE` into scratch space. + mstore(0x00, _CREATE3_PROXY_BYTECODE) + // Deploy a new contract with our pre-made bytecode via CREATE2. + let proxy := create2(0, 0x10, 0x10, salt) + + // If the result of `create2` is the zero address, revert. + if iszero(proxy) { + // Store the function selector of `CreateFail()`. + mstore(0x00, 0xebfef188) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + + // Store the proxy's address. + mstore(0x14, proxy) + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01). + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex). + mstore(0x00, 0xd694) + // Nonce of the proxy contract (1). + mstore8(0x34, 0x01) + + deployed := and( + keccak256(0x1e, 0x17), + 0xffffffffffffffffffffffffffffffffffffffff + ) + + // If the `call` fails or the code size of `deployed` is zero, revert. + // The second argument of the or() call is evaluated first, which is important + // here because extcodesize(deployed) is only non-zero after the call() to the proxy + // is made and the contract is successfully deployed. + if or( + iszero(extcodesize(deployed)), + iszero( + call( + gas(), // Gas remaining. + proxy, // Proxy's address. + value, // Ether value. + ptr, // Pointer to the creation code + creationSize, // Size of the creation code + 0x00, // Offset of output. + 0x00 // Length of output. + ) + ) + ) { + // Store the function selector of `InitializeFail()`. + mstore(0x00, 0x8f86d2f1) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + } + } + } + + /// @notice Returns the CREATE3 deterministic address of the contract deployed via cloneDeterministic(). + /// @dev Forked from https://github.com/Vectorized/solady/blob/main/src/utils/CREATE3.sol + /// @param salt The salt used by the CREATE3 deployment + function addressOfClone3( + bytes32 salt + ) internal view returns (address deployed) { + /// @solidity memory-safe-assembly + // solhint-disable-next-line no-inline-assembly + assembly { + // Cache the free memory pointer. + let m := mload(0x40) + // Store `address(this)`. + mstore(0x00, address()) + // Store the prefix. + mstore8(0x0b, 0xff) + // Store the salt. + mstore(0x20, salt) + // Store the bytecode hash. + mstore(0x40, _CREATE3_PROXY_BYTECODE_HASH) + + // Store the proxy's address. + mstore(0x14, keccak256(0x0b, 0x55)) + // Restore the free memory pointer. + mstore(0x40, m) + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01). + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex). + mstore(0x00, 0xd694) + // Nonce of the proxy contract (1). + mstore8(0x34, 0x01) + + deployed := and( + keccak256(0x1e, 0x17), + 0xffffffffffffffffffffffffffffffffffffffff + ) + } + } +} diff --git a/packages/linea-ens-resolver/contracts/delegatableResolver/DelegatableResolver.sol b/packages/linea-ens-resolver/contracts/delegatableResolver/DelegatableResolver.sol new file mode 100644 index 000000000..0200367ed --- /dev/null +++ b/packages/linea-ens-resolver/contracts/delegatableResolver/DelegatableResolver.sol @@ -0,0 +1,134 @@ +pragma solidity >=0.8.4; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/ABIResolver.sol"; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/AddrResolver.sol"; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/ContentHashResolver.sol"; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/DNSResolver.sol"; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/InterfaceResolver.sol"; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/NameResolver.sol"; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/PubkeyResolver.sol"; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/TextResolver.sol"; +import "@ensdomains/ens-contracts/contracts/resolvers/profiles/ExtendedResolver.sol"; +import "./Multicallable.sol"; +import "./IDelegatableResolver.sol"; +import {Clone} from "../clones-with-immutable-args/Clone.sol"; + +/** + * A delegated resolver that allows the resolver owner to add an operator to update records of a node on behalf of the owner. + * address. + */ +contract DelegatableResolver is + Clone, + Multicallable, + ABIResolver, + AddrResolver, + ContentHashResolver, + DNSResolver, + InterfaceResolver, + NameResolver, + PubkeyResolver, + TextResolver, + ExtendedResolver +{ + using BytesUtils for bytes; + + // Logged when an operator is added or removed. + event Approval( + bytes32 indexed node, + address indexed operator, + bytes name, + bool approved + ); + + error NotAuthorized(bytes32 node); + + //node => (delegate => isAuthorised) + mapping(bytes32 => mapping(address => bool)) operators; + + /* + * Check to see if the operator has been approved by the owner for the node. + * @param name The ENS node to query + * @param offset The offset of the label to query recursively. Start from the 0 position and kepp adding the length of each label as it traverse. The function exits when len is 0. + * @param operator The address of the operator to query + * @return node The node of the name passed as an argument + * @return authorized The boolean state of whether the operator is approved to update record of the name + */ + function getAuthorisedNode( + bytes memory name, + uint256 offset, + address operator + ) public view returns (bytes32 node, bool authorized) { + uint256 len = name.readUint8(offset); + node = bytes32(0); + if (len > 0) { + bytes32 label = name.keccak(offset + 1, len); + (node, authorized) = getAuthorisedNode( + name, + offset + len + 1, + operator + ); + node = keccak256(abi.encodePacked(node, label)); + } else { + return ( + node, + authorized || operators[node][operator] || owner() == operator + ); + } + return (node, authorized || operators[node][operator]); + } + + /** + * @dev Approve an operator to be able to updated records on a node. + */ + function approve( + bytes memory name, + address operator, + bool approved + ) external { + (bytes32 node, bool authorized) = getAuthorisedNode( + name, + 0, + msg.sender + ); + if (!authorized) { + revert NotAuthorized(node); + } + operators[node][operator] = approved; + emit Approval(node, operator, name, approved); + } + + /* + * Returns the owner address passed set by the Factory + * @return address The owner address + */ + function owner() public view returns (address) { + return _getArgAddress(0); + } + + function isAuthorised(bytes32 node) internal view override returns (bool) { + return msg.sender == owner() || operators[node][msg.sender]; + } + + function supportsInterface( + bytes4 interfaceID + ) + public + view + virtual + override( + Multicallable, + ABIResolver, + AddrResolver, + ContentHashResolver, + DNSResolver, + InterfaceResolver, + NameResolver, + PubkeyResolver, + TextResolver + ) + returns (bool) + { + return + interfaceID == type(IDelegatableResolver).interfaceId || + super.supportsInterface(interfaceID); + } +} diff --git a/packages/linea-ens-resolver/contracts/delegatableResolver/DelegatableResolverFactory.sol b/packages/linea-ens-resolver/contracts/delegatableResolver/DelegatableResolverFactory.sol new file mode 100644 index 000000000..d6d4948f7 --- /dev/null +++ b/packages/linea-ens-resolver/contracts/delegatableResolver/DelegatableResolverFactory.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "./DelegatableResolver.sol"; +import {ClonesWithImmutableArgs} from "../clones-with-immutable-args/ClonesWithImmutableArgs.sol"; + +/** + * A resolver factory that creates a dedicated resolver for each user + */ + +contract DelegatableResolverFactory { + using ClonesWithImmutableArgs for address; + + DelegatableResolver public implementation; + event NewDelegatableResolver(address resolver, address owner); + + constructor(DelegatableResolver _implementation) { + implementation = _implementation; + } + + /* + * Create the unique address unique to the owner + * @param address The address of the resolver owner + * @return address The address of the newly created Resolver + */ + function create( + address owner + ) external returns (DelegatableResolver clone) { + bytes memory data = abi.encodePacked(owner); + clone = DelegatableResolver(address(implementation).clone2(data)); + emit NewDelegatableResolver(address(clone), owner); + } + + /* + * Returns the unique address unique to the owner + * @param address The address of the resolver owner + * @return address The address of the newly created Resolver + */ + function predictAddress(address owner) external returns (address clone) { + bytes memory data = abi.encodePacked(owner); + clone = address(implementation).addressOfClone2(data); + } +} diff --git a/packages/linea-ens-resolver/contracts/delegatableResolver/IDelegatableResolver.sol b/packages/linea-ens-resolver/contracts/delegatableResolver/IDelegatableResolver.sol new file mode 100644 index 000000000..f01b0c35b --- /dev/null +++ b/packages/linea-ens-resolver/contracts/delegatableResolver/IDelegatableResolver.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4; + +interface IDelegatableResolver { + function approve( + bytes memory name, + address operator, + bool approved + ) external; + + function getAuthorisedNode( + bytes memory name, + uint256 offset, + address operator + ) external returns (bytes32 node, bool authorized); + + function owner() external view returns (address); +} diff --git a/packages/linea-ens-resolver/contracts/delegatableResolver/IMulticallable.sol b/packages/linea-ens-resolver/contracts/delegatableResolver/IMulticallable.sol new file mode 100644 index 000000000..9f51fb4c6 --- /dev/null +++ b/packages/linea-ens-resolver/contracts/delegatableResolver/IMulticallable.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +interface IMulticallable { + function multicall( + bytes[] calldata data + ) external returns (bytes[] memory results); + + function multicallWithNodeCheck( + bytes32, + bytes[] calldata data + ) external returns (bytes[] memory results); +} diff --git a/packages/linea-ens-resolver/contracts/delegatableResolver/Multicallable.sol b/packages/linea-ens-resolver/contracts/delegatableResolver/Multicallable.sol new file mode 100644 index 000000000..6200430ea --- /dev/null +++ b/packages/linea-ens-resolver/contracts/delegatableResolver/Multicallable.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./IMulticallable.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +abstract contract Multicallable is IMulticallable, ERC165 { + function _multicall( + bytes32 nodehash, + bytes[] calldata data + ) internal returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + if (nodehash != bytes32(0)) { + bytes32 txNamehash = bytes32(data[i][4:36]); + require( + txNamehash == nodehash, + "multicall: All records must have a matching namehash" + ); + } + (bool success, bytes memory result) = address(this).delegatecall( + data[i] + ); + require(success); + results[i] = result; + } + return results; + } + + // This function provides an extra security check when called + // from priviledged contracts (such as EthRegistrarController) + // that can set records on behalf of the node owners + function multicallWithNodeCheck( + bytes32 nodehash, + bytes[] calldata data + ) external returns (bytes[] memory results) { + return _multicall(nodehash, data); + } + + function multicall( + bytes[] calldata data + ) public override returns (bytes[] memory results) { + return _multicall(bytes32(0), data); + } + + function supportsInterface( + bytes4 interfaceID + ) public view virtual override returns (bool) { + return + interfaceID == type(IMulticallable).interfaceId || + super.supportsInterface(interfaceID); + } +} diff --git a/packages/linea-ens-resolver/contracts/deps.sol b/packages/linea-ens-resolver/contracts/deps.sol index 6583d1549..d3a29f79e 100644 --- a/packages/linea-ens-resolver/contracts/deps.sol +++ b/packages/linea-ens-resolver/contracts/deps.sol @@ -4,6 +4,7 @@ import "@ensdomains/ens-contracts/contracts/registry/ENSRegistry.sol"; import "@ensdomains/ens-contracts/contracts/wrapper/NameWrapper.sol"; import "@ensdomains/ens-contracts/contracts/ethregistrar/BaseRegistrarImplementation.sol"; import "@ensdomains/ens-contracts/contracts/wrapper/StaticMetadataService.sol"; +import {L2ReverseResolver} from "@ensdomains/ens-contracts/contracts/reverseRegistrar/L2ReverseResolver.sol"; import {ReverseRegistrar} from "@ensdomains/ens-contracts/contracts/reverseRegistrar/ReverseRegistrar.sol"; import {PublicResolver} from "@ensdomains/ens-contracts/contracts/resolvers/PublicResolver.sol"; import {LineaSparseProofVerifier} from "linea-state-verifier/contracts/LineaSparseProofVerifier.sol"; diff --git a/packages/linea-ens-resolver/package.json b/packages/linea-ens-resolver/package.json index 4c4f8a87b..4fb017afc 100644 --- a/packages/linea-ens-resolver/package.json +++ b/packages/linea-ens-resolver/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "L1 contracts to resolve Linea ENS domains stored on Linea from L1", "scripts": { - "test": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1Resolver.spec.ts --timeout 10000 --exit", + "test": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1Resolver.spec.ts --timeout 120000 --exit", "compile": "hardhat compile", "clean": "rm -fr artifacts cache node_modules typechain-types" }, @@ -13,7 +13,7 @@ "dependencies": { "@chainlink/ccip-read-server": "^0.2.1", "@ensdomains/buffer": "^0.1.1", - "@ensdomains/ens-contracts": "^1.1.4", + "@ensdomains/ens-contracts": "ensdomains/ens-contracts#feature/crosschain-resolver-with-reverse-registrar", "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", "@nomicfoundation/hardhat-ethers": "^3.0.0", "@nomicfoundation/hardhat-ignition": "^0.15.0", @@ -27,7 +27,6 @@ "@typechain/hardhat": "^8.0.0", "@types/chai": "^4.2.0", "chai": "^4.2.0", - "clones-with-immutable-args": "Arachnid/clones-with-immutable-args#feature/create2", "dns-packet": "^5.6.1", "dotenv": "^16.0.3", "ethers": "^6.11.1", @@ -35,14 +34,14 @@ "hardhat-deploy": "^0.11.12", "hardhat-gas-reporter": "^1.0.8", "linea-ccip-gateway": "link:../linea-ccip-gateway/", + "linea-state-verifier": "link:../linea-state-verifier/", "mocha": "^10.3.0", "solidity-coverage": "^0.8.12", "supertest": "^6.3.3", "ts-node": "^10.9.2", "tslib": "^2.6.2", "typechain": "^8.2.0", - "typescript": "^5.4.3", - "linea-state-verifier": "link:../linea-state-verifier/" + "typescript": "^5.4.3" }, "devDependencies": { "@types/mocha": ">=9.1.0", diff --git a/packages/linea-ens-resolver/test/testL1Resolver.spec.ts b/packages/linea-ens-resolver/test/testL1Resolver.spec.ts index 6cc19bb18..d145379af 100644 --- a/packages/linea-ens-resolver/test/testL1Resolver.spec.ts +++ b/packages/linea-ens-resolver/test/testL1Resolver.spec.ts @@ -37,7 +37,7 @@ const baseDomain = `${domainName}.eth`; const node = ethers.namehash(baseDomain); const encodedname = encodeName(baseDomain); -const registrantAddr = "0x4a8e79E5258592f208ddba8A8a0d3ffEB051B10A"; +const registrantAddr = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; const subDomain = "testpoh.linea-test.eth"; const subDomainNode = ethers.namehash(subDomain); const encodedSubDomain = encodeName(subDomain); @@ -66,7 +66,7 @@ declare module "hardhat/types/runtime" { } describe("Crosschain Resolver", () => { - let l1Provider: BrowserProvider; + let l1Provider: JsonRpcProvider; let l2Provider: JsonRpcProvider; let l1SepoliaProvider: JsonRpcProvider; let signer: Signer; @@ -77,46 +77,67 @@ describe("Crosschain Resolver", () => { let wrapper: Contract; let baseRegistrar: Contract; let rollup: Contract; - let signerAddress, l2ResolverAddress, wrapperAddress; + let signerAddress, signerL2, signerL2Address, resolverAddress, wrapperAddress; before(async () => { // Hack to get a 'real' ethers provider from hardhat. The default `HardhatProvider` // doesn't support CCIP-read. - l1Provider = new ethers.BrowserProvider(ethers.provider._hardhatProvider); - // Those test work only with a specific contract deployed on linea sepolia - l2Provider = new ethers.JsonRpcProvider( - "https://rpc.sepolia.linea.build/", - 59140, + l1Provider = new ethers.JsonRpcProvider( + "http://localhost:8445/", + 31648428, { staticNetwork: true, } ); + // Those test work only with a specific contract deployed on linea sepolia + l2Provider = new ethers.JsonRpcProvider("http://localhost:8845/", 1337, { + staticNetwork: true, + }); // We need this provider to get the latest L2BlockNumber along with the the linea state root hash l1SepoliaProvider = new ethers.JsonRpcProvider( - "https://gateway.tenderly.co/public/sepolia", - 11155111, + "http://localhost:8445/", + 31648428, { staticNetwork: true, } ); - signer = await l1Provider.getSigner(0); + signer = new ethers.Wallet( + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", + l1Provider + ); signerAddress = await signer.getAddress(); + signerL2 = new ethers.Wallet( + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + l2Provider + ); + signerL2Address = await signerL2.getAddress(); + + console.log(signerL2Address); + const Rollup = await ethers.getContractFactory("RollupMock", signer); // We query the latest block number and state root hash on the actual L1 sepolia chain // because otherwise if we hard code a block number and state root hash the test is no longer // working after a while as linea_getProof stops working for older blocks const rollupSepolia = new ethers.Contract( - "0xB218f8A4Bc926cF1cA7b3423c154a0D627Bdb7E5", + "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", Rollup.interface, - l1SepoliaProvider + l1Provider ); const currentL2BlockNumber = await rollupSepolia.currentL2BlockNumber(); const stateRootHash = await rollupSepolia.stateRootHashes(currentL2BlockNumber); rollup = await Rollup.deploy(currentL2BlockNumber, stateRootHash); + await rollup.waitForDeployment(); + const shomeiFrontend = new ethers.JsonRpcProvider( + "http://localhost:8889/", + 31648428, + { + staticNetwork: true, + } + ); const gateway = makeL2Gateway( l1Provider as unknown as JsonRpcProvider, l2Provider, @@ -145,8 +166,11 @@ describe("Crosschain Resolver", () => { }, }; }); + const ensFactory = await ethers.getContractFactory("ENSRegistry", signer); ens = await ensFactory.deploy(); + await ens.waitForDeployment(); + const ensAddress = await ens.getAddress(); const baseRegistrarFactory = await ethers.getContractFactory( "BaseRegistrarImplementation", @@ -156,8 +180,10 @@ describe("Crosschain Resolver", () => { ensAddress, ethers.namehash("eth") ); + await baseRegistrar.waitForDeployment(); const baseRegistrarAddress = await baseRegistrar.getAddress(); - await baseRegistrar.addController(signerAddress); + let tx = await baseRegistrar.addController(signer); + await tx.wait(); const metaDataserviceFactory = await ethers.getContractFactory( "StaticMetadataService", signer @@ -165,33 +191,35 @@ describe("Crosschain Resolver", () => { const metaDataservice = await metaDataserviceFactory.deploy( "https://ens.domains" ); + await metaDataservice.waitForDeployment(); const metaDataserviceAddress = await metaDataservice.getAddress(); const reverseRegistrarFactory = await ethers.getContractFactory( "ReverseRegistrar", signer ); const reverseRegistrar = await reverseRegistrarFactory.deploy(ensAddress); + await reverseRegistrar.waitForDeployment(); const reverseRegistrarAddress = await reverseRegistrar.getAddress(); - await ens.setSubnodeOwner( - EMPTY_BYTES32, - labelhash("reverse"), - signerAddress - ); - await ens.setSubnodeOwner( + tx = await ens.setSubnodeOwner(EMPTY_BYTES32, labelhash("reverse"), signer); + await tx.wait(); + tx = await ens.setSubnodeOwner( ethers.namehash("reverse"), labelhash("addr"), reverseRegistrarAddress ); - await ens.setSubnodeOwner( + await tx.wait(); + tx = await ens.setSubnodeOwner( EMPTY_BYTES32, labelhash("eth"), baseRegistrarAddress ); - await baseRegistrar.register( + await tx.wait(); + tx = await baseRegistrar.register( labelhash(domainName), signerAddress, 100000000 ); + await tx.wait(); const publicResolverFactory = await ethers.getContractFactory( "PublicResolver", signer @@ -202,32 +230,37 @@ describe("Crosschain Resolver", () => { "0x0000000000000000000000000000000000000000", reverseRegistrarAddress ); + await publicResolver.waitForDeployment(); const publicResolverAddress = await publicResolver.getAddress(); - await reverseRegistrar.setDefaultResolver(publicResolverAddress); + tx = await reverseRegistrar.setDefaultResolver(publicResolverAddress); + await tx.wait(); const wrapperFactory = await ethers.getContractFactory( "NameWrapper", signer ); - await l1Provider.send("evm_mine", []); + wrapper = await wrapperFactory.deploy( ensAddress, baseRegistrarAddress, metaDataserviceAddress ); + await wrapper.waitForDeployment(); wrapperAddress = await wrapper.getAddress(); - const impl = await ethers.getContractFactory("PublicResolver", signer); - l2ResolverAddress = "0x28F15B034f9744d43548ac64DCE04ed77BdBd832"; const Mimc = await ethers.getContractFactory("Mimc", signer); const mimc = await Mimc.deploy(); + await mimc.waitForDeployment(); + const SparseMerkleProof = await ethers.getContractFactory( "SparseMerkleProof", { libraries: { Mimc: await mimc.getAddress() }, signer } ); const sparseMerkleProof = await SparseMerkleProof.deploy(); + await sparseMerkleProof.waitForDeployment(); + const verifierFactory = await ethers.getContractFactory( "LineaSparseProofVerifier", { @@ -241,6 +274,28 @@ describe("Crosschain Resolver", () => { ["test:"], await rollup.getAddress() ); + await verifier.waitForDeployment(); + + const impl = await ethers.getContractFactory( + "DelegatableResolver", + signerL2 + ); + const implContract = await impl.deploy(); + await implContract.waitForDeployment(); + const testL2Factory = await ethers.getContractFactory( + "DelegatableResolverFactory", + signerL2 + ); + const l2factoryContract = await testL2Factory.deploy( + await implContract.getAddress() + ); + await l2factoryContract.waitForDeployment(); + tx = await l2factoryContract.create(await signerL2.getAddress()); + await tx.wait(); + const logs = await l2factoryContract.queryFilter("NewDelegatableResolver"); + //@ts-ignore + const [resolver] = logs[0].args; + resolverAddress = resolver; const l1ResolverFactory = await ethers.getContractFactory( "L1Resolver", @@ -254,21 +309,25 @@ describe("Crosschain Resolver", () => { "https://api.studio.thegraph.com/query/69290/ens-linea-sepolia/version/latest", 59141 ); - // Mine an empty block so we have something to prove against - await l1Provider.send("evm_mine", []); - l2contract = new ethers.Contract( - l2ResolverAddress, - impl.interface, - l2Provider + await target.waitForDeployment(); + + l2contract = impl.attach(resolverAddress); + tx = await l2contract["setAddr(bytes32,address)"]( + subDomainNode, + registrantAddr ); + await tx.wait(); + console.log("TEST"); }); it("should not allow non owner to set target", async () => { const incorrectname = encodeName("notowned.eth"); try { - await target.setTarget(incorrectname, l2ResolverAddress); + const tx = await target.setTarget(incorrectname, resolverAddress); + await tx.wait(); throw "Should have reverted"; } catch (e) { + console.log(e); expect(e.reason).equal("Not authorized to set target for this node"); } @@ -301,30 +360,33 @@ describe("Crosschain Resolver", () => { EMPTY_ADDRESS ); const wrappedtname = encodeName(`${label}.eth`); - await target.setTarget(wrappedtname, l2ResolverAddress); + await target.setTarget(wrappedtname, resolverAddress); const encodedname = encodeName(`${label}.eth`); const result = await target.getTarget(encodedname); - expect(result[1]).to.equal(l2ResolverAddress); + expect(result[1]).to.equal(resolverAddress); }); - it("should resolve empty ETH Address", async () => { - await target.setTarget(encodedname, l2ResolverAddress); + it.only("should resolve empty ETH Address", async () => { + let tx = await target.setTarget(encodedname, resolverAddress); + await tx.wait(); const addr = "0x0000000000000000000000000000000000000000"; const result = await l2contract["addr(bytes32)"](node); + console.log("0"); expect(result).to.equal(addr); - await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [node]); + console.log("1"); const result2 = await target.resolve(encodedname, calldata, { enableCcipRead: true, }); + console.log("2"); const decoded = i.decodeFunctionResult("addr", result2); expect(decoded[0]).to.equal(addr); }); it("should resolve ETH Address", async () => { - await target.setTarget(encodedname, l2ResolverAddress); + await target.setTarget(encodedname, resolverAddress); const result = await l2contract["addr(bytes32)"](subDomainNode); expect(ethers.getAddress(result)).to.equal(registrantAddr); await l1Provider.send("evm_mine", []); @@ -341,7 +403,7 @@ describe("Crosschain Resolver", () => { }); it("should resolve non ETH Address", async () => { - await target.setTarget(encodedname, l2ResolverAddress); + await target.setTarget(encodedname, resolverAddress); const addr = "0x76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac"; const coinType = 0; // BTC await l1Provider.send("evm_mine", []); @@ -357,7 +419,7 @@ describe("Crosschain Resolver", () => { }); it("should resolve text record", async () => { - await target.setTarget(encodedname, l2ResolverAddress); + await target.setTarget(encodedname, resolverAddress); const key = "name"; const value = "test.eth"; await l1Provider.send("evm_mine", []); @@ -374,7 +436,7 @@ describe("Crosschain Resolver", () => { }); it("should resolve contenthash", async () => { - await target.setTarget(encodedname, l2ResolverAddress); + await target.setTarget(encodedname, resolverAddress); const contenthash = "0xe3010170122029f2d17be6139079dc48696d1f582a8530eb9805b561eda517e22a892c7e3f1f"; await l1Provider.send("evm_mine", []); @@ -391,7 +453,7 @@ describe("Crosschain Resolver", () => { }); it("should revert when the functions's selector is invalid", async () => { - await target.setTarget(encodedname, l2ResolverAddress); + await target.setTarget(encodedname, resolverAddress); const addr = "0x0000000000000000000000000000000000000000"; const result = await l2contract["addr(bytes32)"](node); expect(result).to.equal(addr); @@ -411,7 +473,7 @@ describe("Crosschain Resolver", () => { }); it("should revert if the calldata is too short", async () => { - await target.setTarget(encodedname, l2ResolverAddress); + await target.setTarget(encodedname, resolverAddress); const addr = "0x0000000000000000000000000000000000000000"; const result = await l2contract["addr(bytes32)"](node); expect(result).to.equal(addr); @@ -440,7 +502,7 @@ describe("Crosschain Resolver", () => { ); try { await verifier.getStorageValues( - l2ResolverAddress, + resolverAddress, commands2Test, constantsTest, proofsEncoded, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a0facdd9..bbde1be2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -562,8 +562,8 @@ importers: specifier: ^0.1.1 version: 0.1.1 '@ensdomains/ens-contracts': - specifier: ^1.1.4 - version: 1.1.4 + specifier: ensdomains/ens-contracts#feature/crosschain-resolver-with-reverse-registrar + version: https://codeload.github.com/ensdomains/ens-contracts/tar.gz/9d6d5b727db18247d74937aa3c6ed162f9121c52(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)) '@nomicfoundation/hardhat-chai-matchers': specifier: ^2.0.0 version: 2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)))(chai@4.4.1)(ethers@6.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)) @@ -603,9 +603,6 @@ importers: chai: specifier: ^4.2.0 version: 4.4.1 - clones-with-immutable-args: - specifier: Arachnid/clones-with-immutable-args#feature/create2 - version: https://codeload.github.com/Arachnid/clones-with-immutable-args/tar.gz/23768824cdc037f361f7065538b8f949cae9d3d1 dns-packet: specifier: ^5.6.1 version: 5.6.1 @@ -1765,12 +1762,13 @@ packages: resolution: {integrity: sha512-nfm4ggpK5YBVwVwLZKF9WPjRGRTL9aUxX2O4pqv/AnQCz3WeGHsW7VhVFLj2s4EoWSzCXwR1E6nuqgUwnH692w==} engines: {node: '>=16.8'} - '@ensdomains/ens-contracts@1.1.4': - resolution: {integrity: sha512-kjdcjaznMtE2lwjAVTX2irs8mgNgJCVuB5hnhFhiMaO8dR/tlHQ5UhtZjhSYRhkZd0hLXYrMkXp6thnwpG+ltg==} - '@ensdomains/ens-contracts@file:packages/linea-ens-contracts': resolution: {directory: packages/linea-ens-contracts, type: directory} + '@ensdomains/ens-contracts@https://codeload.github.com/ensdomains/ens-contracts/tar.gz/9d6d5b727db18247d74937aa3c6ed162f9121c52': + resolution: {tarball: https://codeload.github.com/ensdomains/ens-contracts/tar.gz/9d6d5b727db18247d74937aa3c6ed162f9121c52} + version: 0.0.21 + '@ensdomains/ens-test-env@0.4.0-beta.0': resolution: {integrity: sha512-B/Kv0EhPQnwHbgHV1yH178es3EIejYf5LbRtTN1N69jbyoxjT+0cUE74YfaH9EZFQ6sLmpJP/Yk7vE3wTpBglQ==} hasBin: true @@ -3018,6 +3016,11 @@ packages: peerDependencies: hardhat: ^2.0.4 + '@nomicfoundation/hardhat-verify@2.0.8': + resolution: {integrity: sha512-x/OYya7A2Kcz+3W/J78dyDHxr0ezU23DKTrRKfy5wDPCnePqnr79vm8EXqX3gYps6IjPBYyGPZ9K6E5BnrWx5Q==} + peerDependencies: + hardhat: ^2.0.4 + '@nomicfoundation/ignition-core@0.15.1': resolution: {integrity: sha512-/AZO0YHRv1+yQSOtSSbg4GEH9YhU8EVePSfByU2PZW2bsAK0SA8GdoLYFbVNl140dogem5lrE+bCKtX0eN/n+A==} @@ -3115,6 +3118,9 @@ packages: '@openzeppelin/contract-loader@0.6.3': resolution: {integrity: sha512-cOFIjBjwbGgZhDZsitNgJl0Ye1rd5yu/Yx5LMgeq3u0ZYzldm4uObzHDFq4gjDdoypvyORjjJa3BlFA7eAnVIg==} + '@openzeppelin/contracts@4.9.3': + resolution: {integrity: sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg==} + '@openzeppelin/contracts@4.9.6': resolution: {integrity: sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA==} @@ -15090,23 +15096,28 @@ snapshots: dns-packet: 5.6.1 typescript-logging: 1.0.1 - '@ensdomains/ens-contracts@1.1.4': + '@ensdomains/ens-contracts@file:packages/linea-ens-contracts': dependencies: '@ensdomains/buffer': 0.1.1 '@ensdomains/solsha1': 0.0.3 '@openzeppelin/contracts': 4.9.6 + axios: 1.7.2 + csv-parse: 5.5.6 dns-packet: 5.6.1 + transitivePeerDependencies: + - debug - '@ensdomains/ens-contracts@file:packages/linea-ens-contracts': + '@ensdomains/ens-contracts@https://codeload.github.com/ensdomains/ens-contracts/tar.gz/9d6d5b727db18247d74937aa3c6ed162f9121c52(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10))': dependencies: '@ensdomains/buffer': 0.1.1 '@ensdomains/solsha1': 0.0.3 - '@openzeppelin/contracts': 4.9.6 - axios: 1.7.2 - csv-parse: 5.5.6 + '@nomicfoundation/hardhat-verify': 2.0.8(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)) + '@openzeppelin/contracts': 4.9.3 + clones-with-immutable-args: https://codeload.github.com/Arachnid/clones-with-immutable-args/tar.gz/23768824cdc037f361f7065538b8f949cae9d3d1 dns-packet: 5.6.1 transitivePeerDependencies: - - debug + - hardhat + - supports-color '@ensdomains/ens-test-env@0.4.0-beta.0': dependencies: @@ -16833,7 +16844,7 @@ snapshots: '@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10))': dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) ethers: 6.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) lodash.isequal: 4.5.0 @@ -16854,7 +16865,7 @@ snapshots: '@nomicfoundation/ignition-core': 0.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@nomicfoundation/ignition-ui': 0.15.1 chalk: 4.1.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) fs-extra: 10.1.0 hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) prompts: 2.4.2 @@ -16935,7 +16946,22 @@ snapshots: '@ethersproject/address': 5.7.0 cbor: 8.1.0 chalk: 2.4.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) + hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) + lodash.clonedeep: 4.5.0 + semver: 6.3.1 + table: 6.8.2 + undici: 5.28.4 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-verify@2.0.8(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10))': + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/address': 5.7.0 + cbor: 8.1.0 + chalk: 2.4.2 + debug: 4.3.4(supports-color@8.1.1) hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) lodash.clonedeep: 4.5.0 semver: 6.3.1 @@ -16949,7 +16975,7 @@ snapshots: '@ethersproject/address': 5.6.1 '@nomicfoundation/solidity-analyzer': 0.1.1 cbor: 9.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) ethers: 6.13.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) fs-extra: 10.1.0 immer: 10.0.2 @@ -17116,6 +17142,8 @@ snapshots: find-up: 4.1.0 fs-extra: 8.1.0 + '@openzeppelin/contracts@4.9.3': {} + '@openzeppelin/contracts@4.9.6': {} '@openzeppelin/test-helpers@0.5.16(bn.js@5.2.1)(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@6.0.3)': @@ -19862,7 +19890,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -23415,7 +23443,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) for-each@0.3.3: dependencies: @@ -24024,7 +24052,7 @@ snapshots: axios: 0.21.4(debug@4.3.4) chalk: 4.1.2 chokidar: 3.6.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) enquirer: 2.4.1 ethers: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) form-data: 4.0.0 @@ -24189,7 +24217,7 @@ snapshots: chalk: 2.4.2 chokidar: 3.6.0 ci-info: 2.0.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) enquirer: 2.4.1 env-paths: 2.2.1 ethereum-cryptography: 1.2.0 @@ -24485,7 +24513,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -29808,7 +29836,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 2.1.2 @@ -30516,7 +30544,7 @@ snapshots: typechain@8.3.2(typescript@5.4.5): dependencies: '@types/prettier': 2.7.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) fs-extra: 7.0.1 glob: 7.1.7 js-sha3: 0.8.0 From 5f4609a57209f933613d9b153dab57a97267c4e1 Mon Sep 17 00:00:00 2001 From: Julink Date: Fri, 19 Jul 2024 00:16:24 +0200 Subject: [PATCH 02/13] chore: wip --- .../linea-ccip-gateway/src/L2ProofService.ts | 5 +- .../src/evm-gateway/EVMProofHelper.ts | 9 +- packages/linea-ccip-gateway/src/index.ts | 5 +- packages/linea-ens-resolver/package.json | 1 + .../test/testL1Resolver.spec.ts | 235 +++++++++--------- 5 files changed, 129 insertions(+), 126 deletions(-) diff --git a/packages/linea-ccip-gateway/src/L2ProofService.ts b/packages/linea-ccip-gateway/src/L2ProofService.ts index 456260bab..40a41cc37 100644 --- a/packages/linea-ccip-gateway/src/L2ProofService.ts +++ b/packages/linea-ccip-gateway/src/L2ProofService.ts @@ -25,9 +25,10 @@ export class L2ProofService implements IProofService { constructor( providerL1: JsonRpcProvider, providerL2: JsonRpcProvider, - rollupAddress: string + rollupAddress: string, + shomeiNode?: JsonRpcProvider ) { - this.helper = new EVMProofHelper(providerL2); + this.helper = new EVMProofHelper(providerL2, shomeiNode); const currentL2BlockNumberIface = new ethers.Interface([ currentL2BlockNumberSig, ]); diff --git a/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts b/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts index dc58c6f54..1813867ed 100644 --- a/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts +++ b/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts @@ -26,9 +26,12 @@ export interface StateProof { */ export class EVMProofHelper { private readonly providerL2: JsonRpcProvider; + private readonly shomeiNode: JsonRpcProvider; - constructor(providerL2: JsonRpcProvider) { + constructor(providerL2: JsonRpcProvider, shomeiNode?: JsonRpcProvider) { this.providerL2 = providerL2; + // shomeiNode optional since an rpc infura nodes can support both eth_getStorageAt and linea_getProof + this.shomeiNode = shomeiNode ? shomeiNode : providerL2; } /** @@ -68,8 +71,8 @@ export class EVMProofHelper { // We have to reinitilize the provider L2 because of an issue when multiple // requests are sent at the same time, the provider becomes not aware of // the linea_getProof method - const providerUrl = await this.providerL2._getConnection().url; - const providerChainId = await this.providerL2._network.chainId; + const providerUrl = await this.shomeiNode._getConnection().url; + const providerChainId = await this.shomeiNode._network.chainId; const providerL2 = new ethers.JsonRpcProvider( providerUrl, providerChainId, diff --git a/packages/linea-ccip-gateway/src/index.ts b/packages/linea-ccip-gateway/src/index.ts index 28b70795b..5c8227449 100644 --- a/packages/linea-ccip-gateway/src/index.ts +++ b/packages/linea-ccip-gateway/src/index.ts @@ -7,10 +7,11 @@ export type L1Gateway = EVMGateway; export function makeL2Gateway( providerL1: JsonRpcProvider, providerL2: JsonRpcProvider, - rollupAddress: string + rollupAddress: string, + shomeiNode?: JsonRpcProvider ): L1Gateway { return new EVMGateway( - new L2ProofService(providerL1, providerL2, rollupAddress) + new L2ProofService(providerL1, providerL2, rollupAddress, shomeiNode) ); } diff --git a/packages/linea-ens-resolver/package.json b/packages/linea-ens-resolver/package.json index 4fb017afc..3ee6b29b4 100644 --- a/packages/linea-ens-resolver/package.json +++ b/packages/linea-ens-resolver/package.json @@ -4,6 +4,7 @@ "description": "L1 contracts to resolve Linea ENS domains stored on Linea from L1", "scripts": { "test": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1Resolver.spec.ts --timeout 120000 --exit", + "test:local-stack": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1ResolverLocalStack.spec.ts --timeout 120000 --exit", "compile": "hardhat compile", "clean": "rm -fr artifacts cache node_modules typechain-types" }, diff --git a/packages/linea-ens-resolver/test/testL1Resolver.spec.ts b/packages/linea-ens-resolver/test/testL1Resolver.spec.ts index d145379af..4d94dea59 100644 --- a/packages/linea-ens-resolver/test/testL1Resolver.spec.ts +++ b/packages/linea-ens-resolver/test/testL1Resolver.spec.ts @@ -1,25 +1,14 @@ import { makeL2Gateway } from "linea-ccip-gateway"; import { Server } from "@chainlink/ccip-read-server"; -import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider"; -import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; import { expect } from "chai"; -import { - AbiCoder, - BrowserProvider, - Contract, - JsonRpcProvider, - Signer, - ethers as ethersT, -} from "ethers"; +import { AbiCoder, Contract, JsonRpcProvider, ethers as ethersT } from "ethers"; import { FetchRequest } from "ethers"; import { ethers } from "hardhat"; -import { EthereumProvider } from "hardhat/types"; import request from "supertest"; import packet from "dns-packet"; import { blockNo, commands2Test, - commandsTest, constantsTest, proofTest, extraDataTest, @@ -30,6 +19,10 @@ import { retValueTest, retValueLongTest, } from "./testData"; +import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider"; +import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; +import { EthereumProvider } from "hardhat/types"; + const labelhash = (label) => ethers.keccak256(ethers.toUtf8Bytes(label)); const encodeName = (name) => "0x" + packet.name.encode(name).toString("hex"); const domainName = "linea-test"; @@ -37,10 +30,18 @@ const baseDomain = `${domainName}.eth`; const node = ethers.namehash(baseDomain); const encodedname = encodeName(baseDomain); -const registrantAddr = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; -const subDomain = "testpoh.linea-test.eth"; -const subDomainNode = ethers.namehash(subDomain); -const encodedSubDomain = encodeName(subDomain); +// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" +const SIGNER_L1_PK = + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"; +// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" +const SIGNER_L2_PK = + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; + +const REGISTRANT_ADDR = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; + +const SUB_DOMAIN = "testpoh.linea-test.eth"; +const subDomainNode = ethers.namehash(SUB_DOMAIN); +const encodedSubDomain = encodeName(SUB_DOMAIN); const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; const EMPTY_BYTES32 = @@ -51,6 +52,14 @@ const PROOF_ENCODING_PADDING = const ACCEPTED_L2_BLOCK_RANGE_LENGTH = 86400; +const ROLLUP_CONTRACT_ADDR = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"; + +const L1_NODE_URL = "http://localhost:8445/"; +const L1_CHAIN_ID = 31648428; +const L2_NODE_URL = "http://localhost:8845/"; +const L2_CHAIN_ID = 1337; +const SHOMEI_NODE_URL = "http://localhost:8889/"; + type ethersObj = typeof ethersT & Omit & { provider: Omit & { @@ -59,6 +68,7 @@ type ethersObj = typeof ethersT & }; declare module "hardhat/types/runtime" { + //@ts-ignore const ethers: ethersObj; interface HardhatRuntimeEnvironment { ethers: ethersObj; @@ -68,72 +78,43 @@ declare module "hardhat/types/runtime" { describe("Crosschain Resolver", () => { let l1Provider: JsonRpcProvider; let l2Provider: JsonRpcProvider; - let l1SepoliaProvider: JsonRpcProvider; - let signer: Signer; let verifier: Contract; let target: Contract; - let l2contract: Contract; + let l2Resolver: Contract; let ens: Contract; let wrapper: Contract; let baseRegistrar: Contract; let rollup: Contract; - let signerAddress, signerL2, signerL2Address, resolverAddress, wrapperAddress; + let signerL1, + signerL2, + signerL1Address, + signerL2Address, + l2ResolverAddress, + wrapperAddress; before(async () => { - // Hack to get a 'real' ethers provider from hardhat. The default `HardhatProvider` - // doesn't support CCIP-read. - l1Provider = new ethers.JsonRpcProvider( - "http://localhost:8445/", - 31648428, - { - staticNetwork: true, - } - ); - // Those test work only with a specific contract deployed on linea sepolia - l2Provider = new ethers.JsonRpcProvider("http://localhost:8845/", 1337, { + l1Provider = new ethers.JsonRpcProvider(L1_NODE_URL, L1_CHAIN_ID, { staticNetwork: true, }); - // We need this provider to get the latest L2BlockNumber along with the the linea state root hash - l1SepoliaProvider = new ethers.JsonRpcProvider( - "http://localhost:8445/", - 31648428, - { - staticNetwork: true, - } - ); - signer = new ethers.Wallet( - "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", - l1Provider - ); - signerAddress = await signer.getAddress(); - signerL2 = new ethers.Wallet( - "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", - l2Provider - ); - signerL2Address = await signerL2.getAddress(); - - console.log(signerL2Address); + l2Provider = new ethers.JsonRpcProvider(L2_NODE_URL, L2_CHAIN_ID, { + staticNetwork: true, + }); + signerL1 = new ethers.Wallet(SIGNER_L1_PK, l1Provider); + signerL1Address = await signerL1.getAddress(); - const Rollup = await ethers.getContractFactory("RollupMock", signer); + signerL2 = new ethers.Wallet(SIGNER_L2_PK, l2Provider); + signerL2Address = await signerL2.getAddress(); - // We query the latest block number and state root hash on the actual L1 sepolia chain - // because otherwise if we hard code a block number and state root hash the test is no longer - // working after a while as linea_getProof stops working for older blocks - const rollupSepolia = new ethers.Contract( - "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", - Rollup.interface, - l1Provider + rollup = await ethers.getContractAt( + "RollupMock", + ROLLUP_CONTRACT_ADDR, + signerL1 ); - const currentL2BlockNumber = await rollupSepolia.currentL2BlockNumber(); - const stateRootHash = - await rollupSepolia.stateRootHashes(currentL2BlockNumber); - rollup = await Rollup.deploy(currentL2BlockNumber, stateRootHash); - await rollup.waitForDeployment(); - const shomeiFrontend = new ethers.JsonRpcProvider( - "http://localhost:8889/", - 31648428, + const shomeiNode = new ethers.JsonRpcProvider( + SHOMEI_NODE_URL, + L1_CHAIN_ID, { staticNetwork: true, } @@ -141,7 +122,8 @@ describe("Crosschain Resolver", () => { const gateway = makeL2Gateway( l1Provider as unknown as JsonRpcProvider, l2Provider, - await rollup.getAddress() + await rollup.getAddress(), + shomeiNode ); const server = new Server(); gateway.add(server); @@ -167,62 +149,74 @@ describe("Crosschain Resolver", () => { }; }); - const ensFactory = await ethers.getContractFactory("ENSRegistry", signer); + const ensFactory = await ethers.getContractFactory("ENSRegistry", signerL1); ens = await ensFactory.deploy(); await ens.waitForDeployment(); const ensAddress = await ens.getAddress(); const baseRegistrarFactory = await ethers.getContractFactory( "BaseRegistrarImplementation", - signer + signerL1 ); baseRegistrar = await baseRegistrarFactory.deploy( ensAddress, ethers.namehash("eth") ); await baseRegistrar.waitForDeployment(); + const baseRegistrarAddress = await baseRegistrar.getAddress(); - let tx = await baseRegistrar.addController(signer); + let tx = await baseRegistrar.addController(signerL1Address); await tx.wait(); + const metaDataserviceFactory = await ethers.getContractFactory( "StaticMetadataService", - signer + signerL1 ); const metaDataservice = await metaDataserviceFactory.deploy( "https://ens.domains" ); await metaDataservice.waitForDeployment(); + const metaDataserviceAddress = await metaDataservice.getAddress(); const reverseRegistrarFactory = await ethers.getContractFactory( "ReverseRegistrar", - signer + signerL1 ); const reverseRegistrar = await reverseRegistrarFactory.deploy(ensAddress); await reverseRegistrar.waitForDeployment(); + const reverseRegistrarAddress = await reverseRegistrar.getAddress(); - tx = await ens.setSubnodeOwner(EMPTY_BYTES32, labelhash("reverse"), signer); + tx = await ens.setSubnodeOwner( + EMPTY_BYTES32, + labelhash("reverse"), + signerL1 + ); await tx.wait(); + tx = await ens.setSubnodeOwner( ethers.namehash("reverse"), labelhash("addr"), reverseRegistrarAddress ); await tx.wait(); + tx = await ens.setSubnodeOwner( EMPTY_BYTES32, labelhash("eth"), baseRegistrarAddress ); await tx.wait(); + tx = await baseRegistrar.register( labelhash(domainName), - signerAddress, + signerL1Address, 100000000 ); await tx.wait(); + const publicResolverFactory = await ethers.getContractFactory( "PublicResolver", - signer + signerL1 ); const publicResolver = await publicResolverFactory.deploy( ensAddress, @@ -231,34 +225,33 @@ describe("Crosschain Resolver", () => { reverseRegistrarAddress ); await publicResolver.waitForDeployment(); + const publicResolverAddress = await publicResolver.getAddress(); tx = await reverseRegistrar.setDefaultResolver(publicResolverAddress); await tx.wait(); const wrapperFactory = await ethers.getContractFactory( "NameWrapper", - signer + signerL1 ); - wrapper = await wrapperFactory.deploy( ensAddress, baseRegistrarAddress, metaDataserviceAddress ); await wrapper.waitForDeployment(); + wrapperAddress = await wrapper.getAddress(); - const Mimc = await ethers.getContractFactory("Mimc", signer); + const Mimc = await ethers.getContractFactory("Mimc", signerL1); const mimc = await Mimc.deploy(); - await mimc.waitForDeployment(); const SparseMerkleProof = await ethers.getContractFactory( "SparseMerkleProof", - { libraries: { Mimc: await mimc.getAddress() }, signer } + { libraries: { Mimc: await mimc.getAddress() }, signer: signerL1 } ); const sparseMerkleProof = await SparseMerkleProof.deploy(); - await sparseMerkleProof.waitForDeployment(); const verifierFactory = await ethers.getContractFactory( @@ -267,7 +260,7 @@ describe("Crosschain Resolver", () => { libraries: { SparseMerkleProof: await sparseMerkleProof.getAddress(), }, - signer, + signer: signerL1, } ); verifier = await verifierFactory.deploy( @@ -282,6 +275,7 @@ describe("Crosschain Resolver", () => { ); const implContract = await impl.deploy(); await implContract.waitForDeployment(); + const testL2Factory = await ethers.getContractFactory( "DelegatableResolverFactory", signerL2 @@ -290,16 +284,18 @@ describe("Crosschain Resolver", () => { await implContract.getAddress() ); await l2factoryContract.waitForDeployment(); + tx = await l2factoryContract.create(await signerL2.getAddress()); await tx.wait(); + const logs = await l2factoryContract.queryFilter("NewDelegatableResolver"); //@ts-ignore const [resolver] = logs[0].args; - resolverAddress = resolver; + l2ResolverAddress = resolver; const l1ResolverFactory = await ethers.getContractFactory( "L1Resolver", - signer + signerL1 ); const verifierAddress = await verifier.getAddress(); target = await l1ResolverFactory.deploy( @@ -311,23 +307,21 @@ describe("Crosschain Resolver", () => { ); await target.waitForDeployment(); - l2contract = impl.attach(resolverAddress); - tx = await l2contract["setAddr(bytes32,address)"]( + l2Resolver = impl.attach(l2ResolverAddress); + tx = await l2Resolver["setAddr(bytes32,address)"]( subDomainNode, - registrantAddr + REGISTRANT_ADDR ); await tx.wait(); - console.log("TEST"); }); it("should not allow non owner to set target", async () => { const incorrectname = encodeName("notowned.eth"); try { - const tx = await target.setTarget(incorrectname, resolverAddress); + const tx = await target.setTarget(incorrectname, l2ResolverAddress); await tx.wait(); throw "Should have reverted"; } catch (e) { - console.log(e); expect(e.reason).equal("Not authorized to set target for this node"); } @@ -336,59 +330,62 @@ describe("Crosschain Resolver", () => { }); it("should allow owner to set target", async () => { - await target.setTarget(encodedname, signerAddress); + const tx = await target.setTarget(encodedname, signerL1Address); + await tx.wait(); const result = await target.getTarget(encodeName(baseDomain)); - expect(result[1]).to.equal(signerAddress); + expect(result[1]).to.equal(signerL1Address); }); it("subname should get target of its parent", async () => { - await target.setTarget(encodedname, signerAddress); + const tx = await target.setTarget(encodedname, signerL1Address); + await tx.wait(); const result = await target.getTarget(encodedSubDomain); expect(result[0]).to.equal(subDomainNode); - expect(result[1]).to.equal(signerAddress); + expect(result[1]).to.equal(signerL1Address); }); it("should allow wrapped owner to set target", async () => { const label = "wrapped"; const tokenId = labelhash(label); - await baseRegistrar.setApprovalForAll(wrapperAddress, true); - await baseRegistrar.register(tokenId, signerAddress, 100000000); - await wrapper.wrapETH2LD( + let tx = await baseRegistrar.setApprovalForAll(wrapperAddress, true); + await tx.wait(); + tx = await baseRegistrar.register(tokenId, signerL1Address, 100000000); + await tx.wait(); + tx = await wrapper.wrapETH2LD( label, - signerAddress, + signerL1Address, 0, // CAN_DO_EVERYTHING EMPTY_ADDRESS ); + await tx.wait(); const wrappedtname = encodeName(`${label}.eth`); - await target.setTarget(wrappedtname, resolverAddress); + tx = await target.setTarget(wrappedtname, l2ResolverAddress); + await tx.wait(); const encodedname = encodeName(`${label}.eth`); const result = await target.getTarget(encodedname); - expect(result[1]).to.equal(resolverAddress); + expect(result[1]).to.equal(l2ResolverAddress); }); - it.only("should resolve empty ETH Address", async () => { - let tx = await target.setTarget(encodedname, resolverAddress); + it("should resolve empty ETH Address", async () => { + let tx = await target.setTarget(encodedname, l2ResolverAddress); await tx.wait(); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2contract["addr(bytes32)"](node); - console.log("0"); + const result = await l2Resolver["addr(bytes32)"](node); expect(result).to.equal(addr); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [node]); - console.log("1"); const result2 = await target.resolve(encodedname, calldata, { enableCcipRead: true, }); - console.log("2"); const decoded = i.decodeFunctionResult("addr", result2); expect(decoded[0]).to.equal(addr); }); it("should resolve ETH Address", async () => { - await target.setTarget(encodedname, resolverAddress); - const result = await l2contract["addr(bytes32)"](subDomainNode); - expect(ethers.getAddress(result)).to.equal(registrantAddr); + await target.setTarget(encodedname, l2ResolverAddress); + const result = await l2Resolver["addr(bytes32)"](subDomainNode); + expect(ethers.getAddress(result)).to.equal(REGISTRANT_ADDR); await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); @@ -398,12 +395,12 @@ describe("Crosschain Resolver", () => { }); const decoded = i.decodeFunctionResult("addr", result2); expect(ethers.getAddress(decoded[0])).to.equal( - ethers.getAddress(registrantAddr) + ethers.getAddress(REGISTRANT_ADDR) ); }); it("should resolve non ETH Address", async () => { - await target.setTarget(encodedname, resolverAddress); + await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac"; const coinType = 0; // BTC await l1Provider.send("evm_mine", []); @@ -419,7 +416,7 @@ describe("Crosschain Resolver", () => { }); it("should resolve text record", async () => { - await target.setTarget(encodedname, resolverAddress); + await target.setTarget(encodedname, l2ResolverAddress); const key = "name"; const value = "test.eth"; await l1Provider.send("evm_mine", []); @@ -436,7 +433,7 @@ describe("Crosschain Resolver", () => { }); it("should resolve contenthash", async () => { - await target.setTarget(encodedname, resolverAddress); + await target.setTarget(encodedname, l2ResolverAddress); const contenthash = "0xe3010170122029f2d17be6139079dc48696d1f582a8530eb9805b561eda517e22a892c7e3f1f"; await l1Provider.send("evm_mine", []); @@ -453,9 +450,9 @@ describe("Crosschain Resolver", () => { }); it("should revert when the functions's selector is invalid", async () => { - await target.setTarget(encodedname, resolverAddress); + await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2contract["addr(bytes32)"](node); + const result = await l2Resolver["addr(bytes32)"](node); expect(result).to.equal(addr); await l1Provider.send("evm_mine", []); const i = new ethers.Interface([ @@ -473,9 +470,9 @@ describe("Crosschain Resolver", () => { }); it("should revert if the calldata is too short", async () => { - await target.setTarget(encodedname, resolverAddress); + await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2contract["addr(bytes32)"](node); + const result = await l2Resolver["addr(bytes32)"](node); expect(result).to.equal(addr); await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); @@ -502,7 +499,7 @@ describe("Crosschain Resolver", () => { ); try { await verifier.getStorageValues( - resolverAddress, + l2ResolverAddress, commands2Test, constantsTest, proofsEncoded, From e3f52828c76463caafdf938eee5db458a7a700c5 Mon Sep 17 00:00:00 2001 From: Julink Date: Tue, 23 Jul 2024 16:35:56 +0200 Subject: [PATCH 03/13] feat: make e2e L1Resolver tests work on local stack --- .../linea-ccip-gateway/src/L2ProofService.ts | 6 + packages/linea-ens-resolver/.mocharc.json | 9 +- packages/linea-ens-resolver/package.json | 4 +- .../test/testL1Resolver.spec.ts | 256 ++++----- .../test/testL1ResolverLocal.spec.ts | 488 ++++++++++++++++++ packages/linea-ens-resolver/test/utils.ts | 72 +++ pnpm-lock.yaml | 26 +- 7 files changed, 684 insertions(+), 177 deletions(-) create mode 100644 packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts create mode 100644 packages/linea-ens-resolver/test/utils.ts diff --git a/packages/linea-ccip-gateway/src/L2ProofService.ts b/packages/linea-ccip-gateway/src/L2ProofService.ts index 40a41cc37..bc9103d6e 100644 --- a/packages/linea-ccip-gateway/src/L2ProofService.ts +++ b/packages/linea-ccip-gateway/src/L2ProofService.ts @@ -90,6 +90,12 @@ export class L2ProofService implements IProofService { ): Promise { try { let proof = await this.helper.getProofs(blockNo, address, slots); + if (!proof.accountProof) { + throw `No account proof on contract ${address} for block number ${blockNo}`; + } + if (proof.storageProofs.length === 0) { + throw `No storage proofs on contract ${address} for block number ${blockNo}`; + } proof = this.checkStorageInitialized(proof); return AbiCoder.defaultAbiCoder().encode( [ diff --git a/packages/linea-ens-resolver/.mocharc.json b/packages/linea-ens-resolver/.mocharc.json index 48586d7d7..6eb3b2b78 100644 --- a/packages/linea-ens-resolver/.mocharc.json +++ b/packages/linea-ens-resolver/.mocharc.json @@ -1,7 +1,8 @@ { "require": "ts-node/register", "loader": "ts-node/esm", - "extensions": ["ts", "tsx"], - "spec": ["test/**/*.spec.*"], - "watch-files": ["src"] -} + "extensions": [ + "ts", + "tsx" + ] +} \ No newline at end of file diff --git a/packages/linea-ens-resolver/package.json b/packages/linea-ens-resolver/package.json index 3ee6b29b4..0072ea955 100644 --- a/packages/linea-ens-resolver/package.json +++ b/packages/linea-ens-resolver/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "L1 contracts to resolve Linea ENS domains stored on Linea from L1", "scripts": { - "test": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1Resolver.spec.ts --timeout 120000 --exit", - "test:local-stack": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1ResolverLocalStack.spec.ts --timeout 120000 --exit", + "test": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1Resolver.spec.ts --timeout 300000 --exit", + "test:local": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1ResolverLocal.spec.ts --timeout 300000 --exit", "compile": "hardhat compile", "clean": "rm -fr artifacts cache node_modules typechain-types" }, diff --git a/packages/linea-ens-resolver/test/testL1Resolver.spec.ts b/packages/linea-ens-resolver/test/testL1Resolver.spec.ts index 4d94dea59..d741ac24d 100644 --- a/packages/linea-ens-resolver/test/testL1Resolver.spec.ts +++ b/packages/linea-ens-resolver/test/testL1Resolver.spec.ts @@ -1,9 +1,19 @@ import { makeL2Gateway } from "linea-ccip-gateway"; import { Server } from "@chainlink/ccip-read-server"; +import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider"; +import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; import { expect } from "chai"; -import { AbiCoder, Contract, JsonRpcProvider, ethers as ethersT } from "ethers"; +import { + AbiCoder, + BrowserProvider, + Contract, + JsonRpcProvider, + Signer, + ethers as ethersT, +} from "ethers"; import { FetchRequest } from "ethers"; import { ethers } from "hardhat"; +import { EthereumProvider } from "hardhat/types"; import request from "supertest"; import packet from "dns-packet"; import { @@ -19,10 +29,6 @@ import { retValueTest, retValueLongTest, } from "./testData"; -import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider"; -import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; -import { EthereumProvider } from "hardhat/types"; - const labelhash = (label) => ethers.keccak256(ethers.toUtf8Bytes(label)); const encodeName = (name) => "0x" + packet.name.encode(name).toString("hex"); const domainName = "linea-test"; @@ -30,18 +36,10 @@ const baseDomain = `${domainName}.eth`; const node = ethers.namehash(baseDomain); const encodedname = encodeName(baseDomain); -// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" -const SIGNER_L1_PK = - "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"; -// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" -const SIGNER_L2_PK = - "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; - -const REGISTRANT_ADDR = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; - -const SUB_DOMAIN = "testpoh.linea-test.eth"; -const subDomainNode = ethers.namehash(SUB_DOMAIN); -const encodedSubDomain = encodeName(SUB_DOMAIN); +const registrantAddr = "0x4a8e79E5258592f208ddba8A8a0d3ffEB051B10A"; +const subDomain = "testpoh.linea-test.eth"; +const subDomainNode = ethers.namehash(subDomain); +const encodedSubDomain = encodeName(subDomain); const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; const EMPTY_BYTES32 = @@ -52,14 +50,6 @@ const PROOF_ENCODING_PADDING = const ACCEPTED_L2_BLOCK_RANGE_LENGTH = 86400; -const ROLLUP_CONTRACT_ADDR = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"; - -const L1_NODE_URL = "http://localhost:8445/"; -const L1_CHAIN_ID = 31648428; -const L2_NODE_URL = "http://localhost:8845/"; -const L2_CHAIN_ID = 1337; -const SHOMEI_NODE_URL = "http://localhost:8889/"; - type ethersObj = typeof ethersT & Omit & { provider: Omit & { @@ -68,7 +58,6 @@ type ethersObj = typeof ethersT & }; declare module "hardhat/types/runtime" { - //@ts-ignore const ethers: ethersObj; interface HardhatRuntimeEnvironment { ethers: ethersObj; @@ -76,54 +65,61 @@ declare module "hardhat/types/runtime" { } describe("Crosschain Resolver", () => { - let l1Provider: JsonRpcProvider; + let l1Provider: BrowserProvider; let l2Provider: JsonRpcProvider; + let l1SepoliaProvider: JsonRpcProvider; + let signer: Signer; let verifier: Contract; let target: Contract; - let l2Resolver: Contract; + let l2contract: Contract; let ens: Contract; let wrapper: Contract; let baseRegistrar: Contract; let rollup: Contract; - let signerL1, - signerL2, - signerL1Address, - signerL2Address, - l2ResolverAddress, - wrapperAddress; + let signerAddress, l2ResolverAddress, wrapperAddress; before(async () => { - l1Provider = new ethers.JsonRpcProvider(L1_NODE_URL, L1_CHAIN_ID, { - staticNetwork: true, - }); - - l2Provider = new ethers.JsonRpcProvider(L2_NODE_URL, L2_CHAIN_ID, { - staticNetwork: true, - }); - signerL1 = new ethers.Wallet(SIGNER_L1_PK, l1Provider); - signerL1Address = await signerL1.getAddress(); - - signerL2 = new ethers.Wallet(SIGNER_L2_PK, l2Provider); - signerL2Address = await signerL2.getAddress(); - - rollup = await ethers.getContractAt( - "RollupMock", - ROLLUP_CONTRACT_ADDR, - signerL1 + // Hack to get a 'real' ethers provider from hardhat. The default `HardhatProvider` + // doesn't support CCIP-read. + l1Provider = new ethers.BrowserProvider(ethers.provider._hardhatProvider); + // Those test work only with a specific contract deployed on linea sepolia + l2Provider = new ethers.JsonRpcProvider( + "https://rpc.sepolia.linea.build/", + 59140, + { + staticNetwork: true, + } ); - - const shomeiNode = new ethers.JsonRpcProvider( - SHOMEI_NODE_URL, - L1_CHAIN_ID, + // We need this provider to get the latest L2BlockNumber along with the the linea state root hash + l1SepoliaProvider = new ethers.JsonRpcProvider( + "https://gateway.tenderly.co/public/sepolia", + 11155111, { staticNetwork: true, } ); + signer = await l1Provider.getSigner(0); + signerAddress = await signer.getAddress(); + + const Rollup = await ethers.getContractFactory("RollupMock", signer); + + // We query the latest block number and state root hash on the actual L1 sepolia chain + // because otherwise if we hard code a block number and state root hash the test is no longer + // working after a while as linea_getProof stops working for older blocks + const rollupSepolia = new ethers.Contract( + "0xB218f8A4Bc926cF1cA7b3423c154a0D627Bdb7E5", + Rollup.interface, + l1SepoliaProvider + ); + const currentL2BlockNumber = await rollupSepolia.currentL2BlockNumber(); + const stateRootHash = + await rollupSepolia.stateRootHashes(currentL2BlockNumber); + rollup = await Rollup.deploy(currentL2BlockNumber, stateRootHash); + const gateway = makeL2Gateway( l1Provider as unknown as JsonRpcProvider, l2Provider, - await rollup.getAddress(), - shomeiNode + await rollup.getAddress() ); const server = new Server(); gateway.add(server); @@ -148,75 +144,56 @@ describe("Crosschain Resolver", () => { }, }; }); - - const ensFactory = await ethers.getContractFactory("ENSRegistry", signerL1); + const ensFactory = await ethers.getContractFactory("ENSRegistry", signer); ens = await ensFactory.deploy(); - await ens.waitForDeployment(); - const ensAddress = await ens.getAddress(); const baseRegistrarFactory = await ethers.getContractFactory( "BaseRegistrarImplementation", - signerL1 + signer ); baseRegistrar = await baseRegistrarFactory.deploy( ensAddress, ethers.namehash("eth") ); - await baseRegistrar.waitForDeployment(); - const baseRegistrarAddress = await baseRegistrar.getAddress(); - let tx = await baseRegistrar.addController(signerL1Address); - await tx.wait(); - + await baseRegistrar.addController(signerAddress); const metaDataserviceFactory = await ethers.getContractFactory( "StaticMetadataService", - signerL1 + signer ); const metaDataservice = await metaDataserviceFactory.deploy( "https://ens.domains" ); - await metaDataservice.waitForDeployment(); - const metaDataserviceAddress = await metaDataservice.getAddress(); const reverseRegistrarFactory = await ethers.getContractFactory( "ReverseRegistrar", - signerL1 + signer ); const reverseRegistrar = await reverseRegistrarFactory.deploy(ensAddress); - await reverseRegistrar.waitForDeployment(); - const reverseRegistrarAddress = await reverseRegistrar.getAddress(); - tx = await ens.setSubnodeOwner( + await ens.setSubnodeOwner( EMPTY_BYTES32, labelhash("reverse"), - signerL1 + signerAddress ); - await tx.wait(); - - tx = await ens.setSubnodeOwner( + await ens.setSubnodeOwner( ethers.namehash("reverse"), labelhash("addr"), reverseRegistrarAddress ); - await tx.wait(); - - tx = await ens.setSubnodeOwner( + await ens.setSubnodeOwner( EMPTY_BYTES32, labelhash("eth"), baseRegistrarAddress ); - await tx.wait(); - - tx = await baseRegistrar.register( + await baseRegistrar.register( labelhash(domainName), - signerL1Address, + signerAddress, 100000000 ); - await tx.wait(); - const publicResolverFactory = await ethers.getContractFactory( "PublicResolver", - signerL1 + signer ); const publicResolver = await publicResolverFactory.deploy( ensAddress, @@ -224,35 +201,31 @@ describe("Crosschain Resolver", () => { "0x0000000000000000000000000000000000000000", reverseRegistrarAddress ); - await publicResolver.waitForDeployment(); - const publicResolverAddress = await publicResolver.getAddress(); - tx = await reverseRegistrar.setDefaultResolver(publicResolverAddress); - await tx.wait(); + await reverseRegistrar.setDefaultResolver(publicResolverAddress); const wrapperFactory = await ethers.getContractFactory( "NameWrapper", - signerL1 + signer ); + await l1Provider.send("evm_mine", []); wrapper = await wrapperFactory.deploy( ensAddress, baseRegistrarAddress, metaDataserviceAddress ); - await wrapper.waitForDeployment(); - wrapperAddress = await wrapper.getAddress(); + const impl = await ethers.getContractFactory("PublicResolver", signer); + l2ResolverAddress = "0x28F15B034f9744d43548ac64DCE04ed77BdBd832"; - const Mimc = await ethers.getContractFactory("Mimc", signerL1); + const Mimc = await ethers.getContractFactory("Mimc", signer); const mimc = await Mimc.deploy(); - await mimc.waitForDeployment(); const SparseMerkleProof = await ethers.getContractFactory( "SparseMerkleProof", - { libraries: { Mimc: await mimc.getAddress() }, signer: signerL1 } + { libraries: { Mimc: await mimc.getAddress() }, signer } ); const sparseMerkleProof = await SparseMerkleProof.deploy(); - await sparseMerkleProof.waitForDeployment(); const verifierFactory = await ethers.getContractFactory( "LineaSparseProofVerifier", @@ -260,42 +233,17 @@ describe("Crosschain Resolver", () => { libraries: { SparseMerkleProof: await sparseMerkleProof.getAddress(), }, - signer: signerL1, + signer, } ); verifier = await verifierFactory.deploy( ["test:"], await rollup.getAddress() ); - await verifier.waitForDeployment(); - - const impl = await ethers.getContractFactory( - "DelegatableResolver", - signerL2 - ); - const implContract = await impl.deploy(); - await implContract.waitForDeployment(); - - const testL2Factory = await ethers.getContractFactory( - "DelegatableResolverFactory", - signerL2 - ); - const l2factoryContract = await testL2Factory.deploy( - await implContract.getAddress() - ); - await l2factoryContract.waitForDeployment(); - - tx = await l2factoryContract.create(await signerL2.getAddress()); - await tx.wait(); - - const logs = await l2factoryContract.queryFilter("NewDelegatableResolver"); - //@ts-ignore - const [resolver] = logs[0].args; - l2ResolverAddress = resolver; const l1ResolverFactory = await ethers.getContractFactory( "L1Resolver", - signerL1 + signer ); const verifierAddress = await verifier.getAddress(); target = await l1ResolverFactory.deploy( @@ -305,21 +253,19 @@ describe("Crosschain Resolver", () => { "https://api.studio.thegraph.com/query/69290/ens-linea-sepolia/version/latest", 59141 ); - await target.waitForDeployment(); - - l2Resolver = impl.attach(l2ResolverAddress); - tx = await l2Resolver["setAddr(bytes32,address)"]( - subDomainNode, - REGISTRANT_ADDR + // Mine an empty block so we have something to prove against + await l1Provider.send("evm_mine", []); + l2contract = new ethers.Contract( + l2ResolverAddress, + impl.interface, + l2Provider ); - await tx.wait(); }); it("should not allow non owner to set target", async () => { const incorrectname = encodeName("notowned.eth"); try { - const tx = await target.setTarget(incorrectname, l2ResolverAddress); - await tx.wait(); + await target.setTarget(incorrectname, l2ResolverAddress); throw "Should have reverted"; } catch (e) { expect(e.reason).equal("Not authorized to set target for this node"); @@ -330,48 +276,42 @@ describe("Crosschain Resolver", () => { }); it("should allow owner to set target", async () => { - const tx = await target.setTarget(encodedname, signerL1Address); - await tx.wait(); + await target.setTarget(encodedname, signerAddress); const result = await target.getTarget(encodeName(baseDomain)); - expect(result[1]).to.equal(signerL1Address); + expect(result[1]).to.equal(signerAddress); }); it("subname should get target of its parent", async () => { - const tx = await target.setTarget(encodedname, signerL1Address); - await tx.wait(); + await target.setTarget(encodedname, signerAddress); const result = await target.getTarget(encodedSubDomain); expect(result[0]).to.equal(subDomainNode); - expect(result[1]).to.equal(signerL1Address); + expect(result[1]).to.equal(signerAddress); }); it("should allow wrapped owner to set target", async () => { const label = "wrapped"; const tokenId = labelhash(label); - let tx = await baseRegistrar.setApprovalForAll(wrapperAddress, true); - await tx.wait(); - tx = await baseRegistrar.register(tokenId, signerL1Address, 100000000); - await tx.wait(); - tx = await wrapper.wrapETH2LD( + await baseRegistrar.setApprovalForAll(wrapperAddress, true); + await baseRegistrar.register(tokenId, signerAddress, 100000000); + await wrapper.wrapETH2LD( label, - signerL1Address, + signerAddress, 0, // CAN_DO_EVERYTHING EMPTY_ADDRESS ); - await tx.wait(); const wrappedtname = encodeName(`${label}.eth`); - tx = await target.setTarget(wrappedtname, l2ResolverAddress); - await tx.wait(); + await target.setTarget(wrappedtname, l2ResolverAddress); const encodedname = encodeName(`${label}.eth`); const result = await target.getTarget(encodedname); expect(result[1]).to.equal(l2ResolverAddress); }); it("should resolve empty ETH Address", async () => { - let tx = await target.setTarget(encodedname, l2ResolverAddress); - await tx.wait(); + await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2Resolver["addr(bytes32)"](node); + const result = await l2contract["addr(bytes32)"](node); expect(result).to.equal(addr); + await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [node]); @@ -384,8 +324,8 @@ describe("Crosschain Resolver", () => { it("should resolve ETH Address", async () => { await target.setTarget(encodedname, l2ResolverAddress); - const result = await l2Resolver["addr(bytes32)"](subDomainNode); - expect(ethers.getAddress(result)).to.equal(REGISTRANT_ADDR); + const result = await l2contract["addr(bytes32)"](subDomainNode); + expect(ethers.getAddress(result)).to.equal(registrantAddr); await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); @@ -395,7 +335,7 @@ describe("Crosschain Resolver", () => { }); const decoded = i.decodeFunctionResult("addr", result2); expect(ethers.getAddress(decoded[0])).to.equal( - ethers.getAddress(REGISTRANT_ADDR) + ethers.getAddress(registrantAddr) ); }); @@ -452,7 +392,7 @@ describe("Crosschain Resolver", () => { it("should revert when the functions's selector is invalid", async () => { await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2Resolver["addr(bytes32)"](node); + const result = await l2contract["addr(bytes32)"](node); expect(result).to.equal(addr); await l1Provider.send("evm_mine", []); const i = new ethers.Interface([ @@ -472,7 +412,7 @@ describe("Crosschain Resolver", () => { it("should revert if the calldata is too short", async () => { await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2Resolver["addr(bytes32)"](node); + const result = await l2contract["addr(bytes32)"](node); expect(result).to.equal(addr); await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts new file mode 100644 index 000000000..736620357 --- /dev/null +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -0,0 +1,488 @@ +import { makeL2Gateway } from "linea-ccip-gateway"; +import { Server } from "@chainlink/ccip-read-server"; +import { expect } from "chai"; +import { Contract, JsonRpcProvider, ethers as ethersT } from "ethers"; +import { FetchRequest } from "ethers"; +import { ethers } from "hardhat"; +import request from "supertest"; +import packet from "dns-packet"; +import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider"; +import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; +import { EthereumProvider } from "hardhat/types"; +import { + deployContract, + getAndIncreaseFeeData, + sendTransactionsWithInterval, + waitForL2BlockNumberFinalized, + waitForLatestL2BlockNumberFinalizedToChange, +} from "./utils"; + +const labelhash = (label) => ethers.keccak256(ethers.toUtf8Bytes(label)); +const encodeName = (name) => "0x" + packet.name.encode(name).toString("hex"); +const domainName = "linea-test"; +const baseDomain = `${domainName}.eth`; +const node = ethers.namehash(baseDomain); +const encodedname = encodeName(baseDomain); +const contenthash = + "0xe3010170122029f2d17be6139079dc48696d1f582a8530eb9805b561eda517e22a892c7e3f1f"; +const testAddr = "0x76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac"; +const coinType = 0; + +// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" +const SIGNER_L1_PK = + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"; +// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" +const SIGNER_L2_PK = + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; + +const REGISTRANT_ADDR = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; + +const SUB_DOMAIN = "testpoh.linea-test.eth"; +const subDomainNode = ethers.namehash(SUB_DOMAIN); +const encodedSubDomain = encodeName(SUB_DOMAIN); + +const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; +const EMPTY_BYTES32 = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + +const ROLLUP_CONTRACT_ADDR = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"; + +const L1_NODE_URL = "http://localhost:8445/"; +const L1_CHAIN_ID = 31648428; +const L2_NODE_URL = "http://localhost:8845/"; +const L2_CHAIN_ID = 1337; +const SHOMEI_NODE_URL = "http://localhost:8889/"; + +type ethersObj = typeof ethersT & + Omit & { + provider: Omit & { + _hardhatProvider: EthereumProvider; + }; + }; + +declare module "hardhat/types/runtime" { + //@ts-ignore + const ethers: ethersObj; + interface HardhatRuntimeEnvironment { + ethers: ethersObj; + } +} + +// These tests need to be run along the Linea local stack running +describe("Crosschain Resolver Local", () => { + let l1Provider: JsonRpcProvider; + let l2Provider: JsonRpcProvider; + let verifier: Contract; + let target: Contract; + let l2Resolver: Contract; + let wrapper: Contract; + let baseRegistrar: Contract; + let rollup: Contract; + let signerL1, + signerL2, + signerL1Address, + signerL2Address, + l2ResolverAddress, + wrapperAddress; + let lastSetupTxBlockNumber: number; + let sendTransactionsPromise: NodeJS.Timeout; + + before(async () => { + // Setup providers and signers + l1Provider = new ethers.JsonRpcProvider(L1_NODE_URL, L1_CHAIN_ID, { + staticNetwork: true, + }); + l2Provider = new ethers.JsonRpcProvider(L2_NODE_URL, L2_CHAIN_ID, { + staticNetwork: true, + }); + signerL1 = new ethers.Wallet(SIGNER_L1_PK, l1Provider); + signerL1Address = await signerL1.getAddress(); + signerL2 = new ethers.Wallet(SIGNER_L2_PK, l2Provider); + signerL2Address = await signerL2.getAddress(); + rollup = await ethers.getContractAt( + "RollupMock", + ROLLUP_CONTRACT_ADDR, + signerL1 + ); + + // Setup CCIP Gateway + const shomeiNode = new ethers.JsonRpcProvider( + SHOMEI_NODE_URL, + L2_CHAIN_ID, + { + staticNetwork: true, + } + ); + const gateway = makeL2Gateway( + l1Provider as unknown as JsonRpcProvider, + l2Provider, + await rollup.getAddress(), + shomeiNode + ); + const server = new Server(); + gateway.add(server); + const app = server.makeApp("/"); + const getUrl = FetchRequest.createGetUrlFunc(); + ethers.FetchRequest.registerGetUrl(async (req: FetchRequest) => { + if (req.url != "test:") return getUrl(req); + + const r = request(app).post("/"); + if (req.hasBody()) { + r.set("Content-Type", "application/json").send( + ethers.toUtf8String(req.body) + ); + } + const response = await r; + return { + statusCode: response.statusCode, + statusMessage: response.ok ? "OK" : response.statusCode.toString(), + body: ethers.toUtf8Bytes(JSON.stringify(response.body)), + headers: { + "Content-Type": "application/json", + }, + }; + }); + + // Deploy and configure contracts + const ens = await deployContract("ENSRegistry", signerL1); + baseRegistrar = await deployContract( + "BaseRegistrarImplementation", + signerL1, + await ens.getAddress(), + ethers.namehash("eth") + ); + const baseRegistrarAddress = await baseRegistrar.getAddress(); + await baseRegistrar.addController(signerL1Address).then((tx) => tx.wait()); + const metaDataservice = await deployContract( + "StaticMetadataService", + signerL1, + "" + ); + const reverseRegistrar = await deployContract( + "ReverseRegistrar", + signerL1, + await ens.getAddress() + ); + const reverseRegistrarAddress = await reverseRegistrar.getAddress(); + + await ens + .setSubnodeOwner(EMPTY_BYTES32, labelhash("reverse"), signerL1) + .then((tx) => tx.wait()); + await ens + .setSubnodeOwner( + ethers.namehash("reverse"), + labelhash("addr"), + reverseRegistrarAddress + ) + .then((tx) => tx.wait()); + await ens + .setSubnodeOwner(EMPTY_BYTES32, labelhash("eth"), baseRegistrarAddress) + .then((tx) => tx.wait()); + await baseRegistrar + .register(labelhash(domainName), signerL1Address, 100000000) + .then((tx) => tx.wait()); + + const publicResolver = await deployContract( + "PublicResolver", + signerL1, + await ens.getAddress(), + EMPTY_ADDRESS, + EMPTY_ADDRESS, + await reverseRegistrar.getAddress() + ); + const publicResolverAddress = await publicResolver.getAddress(); + await reverseRegistrar + .setDefaultResolver(publicResolverAddress) + .then((tx) => tx.wait()); + + wrapper = await deployContract( + "NameWrapper", + signerL1, + await ens.getAddress(), + await baseRegistrar.getAddress(), + await metaDataservice.getAddress() + ); + wrapperAddress = await wrapper.getAddress(); + + const mimc = await deployContract("Mimc", signerL1); + const SparseMerkleProof = await ethers.getContractFactory( + "SparseMerkleProof", + { libraries: { Mimc: await mimc.getAddress() }, signer: signerL1 } + ); + const sparseMerkleProof = await SparseMerkleProof.deploy(); + await sparseMerkleProof.waitForDeployment(); + const verifierFactory = await ethers.getContractFactory( + "LineaSparseProofVerifier", + { + libraries: { + SparseMerkleProof: await sparseMerkleProof.getAddress(), + }, + signer: signerL1, + } + ); + verifier = await verifierFactory.deploy( + ["test:"], + await rollup.getAddress() + ); + await verifier.waitForDeployment(); + + const implContract = await deployContract("DelegatableResolver", signerL2); + const l2factoryContract = await deployContract( + "DelegatableResolverFactory", + signerL2, + await implContract.getAddress() + ); + await l2factoryContract + .create(await signerL2.getAddress()) + .then((tx) => tx.wait()); + const logs = await l2factoryContract.queryFilter("NewDelegatableResolver"); + //@ts-ignore + l2ResolverAddress = logs[0].args[0]; + + target = await deployContract( + "L1Resolver", + signerL1, + await verifier.getAddress(), + await ens.getAddress(), + wrapperAddress, + "", + 59141 + ); + + const delegatableResolverImpl = await ethers.getContractFactory( + "DelegatableResolver", + signerL2 + ); + l2Resolver = delegatableResolverImpl.attach(l2ResolverAddress); + await l2Resolver["setAddr(bytes32,address)"]( + subDomainNode, + REGISTRANT_ADDR + ).then((tx) => tx.wait()); + + await l2Resolver["setAddr(bytes32,uint256,bytes)"]( + subDomainNode, + coinType, + testAddr + ).then((tx) => tx.wait()); + + await l2Resolver + .setText(subDomainNode, "name", "test.eth") + .then((tx) => tx.wait()); + + const tx = await l2Resolver.setContenthash(subDomainNode, contenthash); + + const txReceipt = await tx.wait(); + lastSetupTxBlockNumber = txReceipt.blockNumber; + + // Generate activity on Linea to make finalization events happen + const [maxPriorityFeePerGas, maxFeePerGas] = getAndIncreaseFeeData( + await l2Provider.getFeeData() + ); + sendTransactionsPromise = sendTransactionsWithInterval( + signerL2, + { + to: signerL2Address, + value: ethers.parseEther("0.0001"), + maxPriorityFeePerGas, + maxFeePerGas, + }, + 1_000 + ); + }); + + it("should revert when querying L1Resolver and the currentL2BlockNumber is older than the L2 block number we are fetching the data from", async () => { + const currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ + blockTag: "finalized", + }); + expect(currentL2BlockNumberFinalized < lastSetupTxBlockNumber); + await target + .setTarget(encodedname, l2ResolverAddress) + .then((tx) => tx.wait()); + const result = await l2Resolver["addr(bytes32)"](subDomainNode); + expect(ethers.getAddress(result)).to.equal(REGISTRANT_ADDR); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + try { + await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (e) { + expect(e.shortMessage).contain( + `error encountered during CCIP fetch: "Internal server error: No storage proofs on contract ${l2ResolverAddress}` + ); + } + }); + + it("should not allow non owner to set target", async () => { + const incorrectname = encodeName("notowned.eth"); + try { + await target + .setTarget(incorrectname, l2ResolverAddress) + .then((tx) => tx.wait()); + throw "Should have reverted"; + } catch (e) { + expect(e.reason).equal("Not authorized to set target for this node"); + } + + const result = await target.getTarget(incorrectname); + expect(result[1]).to.equal(EMPTY_ADDRESS); + }); + + it("should allow owner to set target", async () => { + await target + .setTarget(encodedname, signerL1Address) + .then((tx) => tx.wait()); + const result = await target.getTarget(encodeName(baseDomain)); + expect(result[1]).to.equal(signerL1Address); + }); + + it("subname should get target of its parent", async () => { + await target + .setTarget(encodedname, signerL1Address) + .then((tx) => tx.wait()); + const result = await target.getTarget(encodedSubDomain); + expect(result[0]).to.equal(subDomainNode); + expect(result[1]).to.equal(signerL1Address); + }); + + it("should allow wrapped owner to set target", async () => { + const label = "wrapped"; + const tokenId = labelhash(label); + await baseRegistrar + .setApprovalForAll(wrapperAddress, true) + .then((tx) => tx.wait()); + await baseRegistrar + .register(tokenId, signerL1Address, 100000000) + .then((tx) => tx.wait()); + await wrapper + .wrapETH2LD( + label, + signerL1Address, + 0, // CAN_DO_EVERYTHING + EMPTY_ADDRESS + ) + .then((tx) => tx.wait()); + const wrappedtname = encodeName(`${label}.eth`); + await target + .setTarget(wrappedtname, l2ResolverAddress) + .then((tx) => tx.wait()); + const encodedname = encodeName(`${label}.eth`); + const result = await target.getTarget(encodedname); + expect(result[1]).to.equal(l2ResolverAddress); + }); + + it("should resolve empty ETH Address", async () => { + // Wait for the latest L2 finalized block to more recent than the L2 block number we are fetching the data from + await waitForL2BlockNumberFinalized(rollup, lastSetupTxBlockNumber, 5000); + + let tx = await target + .setTarget(encodedname, l2ResolverAddress) + .then((tx) => tx.wait()); + const result = await l2Resolver["addr(bytes32)"](node); + expect(result).to.equal(EMPTY_ADDRESS); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [node]); + const result2 = await target.resolve(encodedname, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(decoded[0]).to.equal(EMPTY_ADDRESS); + }); + + it("should resolve ETH Address", async () => { + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(ethers.getAddress(decoded[0])).to.equal( + ethers.getAddress(REGISTRANT_ADDR) + ); + }); + + it("should resolve non ETH Address", async () => { + const i = new ethers.Interface([ + "function addr(bytes32,uint256) returns(bytes)", + ]); + const calldata = i.encodeFunctionData("addr", [subDomainNode, coinType]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(decoded[0]).to.equal(testAddr); + }); + + it("should resolve text record", async () => { + const i = new ethers.Interface([ + "function text(bytes32,string) returns(string)", + ]); + const calldata = i.encodeFunctionData("text", [subDomainNode, "name"]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("text", result2); + expect(decoded[0]).to.equal("test.eth"); + }); + + it("should resolve contenthash", async () => { + const i = new ethers.Interface([ + "function contenthash(bytes32) returns(bytes)", + ]); + const calldata = i.encodeFunctionData("contenthash", [subDomainNode]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("contenthash", result2); + expect(decoded[0]).to.equal(contenthash); + }); + + it("should revert when the functions's selector is invalid", async () => { + const i = new ethers.Interface([ + "function unknown(bytes32) returns(address)", + ]); + const calldata = i.encodeFunctionData("unknown", [node]); + try { + await target.resolve(encodedname, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (error) { + expect(error.reason).to.equal("invalid selector"); + } + }); + + it("should revert if the calldata is too short", async () => { + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = "0x"; + try { + await target.resolve(encodedname, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (error) { + expect(error.reason).to.equal("param data too short"); + } + }); + + it("should not revert when querying L1Resolver right after a finalization has occured", async () => { + waitForLatestL2BlockNumberFinalizedToChange(rollup, 2000); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(ethers.getAddress(decoded[0])).to.equal( + ethers.getAddress(REGISTRANT_ADDR) + ); + }); + + after(async () => { + clearInterval(sendTransactionsPromise); + }); +}); diff --git a/packages/linea-ens-resolver/test/utils.ts b/packages/linea-ens-resolver/test/utils.ts new file mode 100644 index 000000000..9972b3dba --- /dev/null +++ b/packages/linea-ens-resolver/test/utils.ts @@ -0,0 +1,72 @@ +import { + BigNumberish, + Contract, + FeeData, + TransactionRequest, + Wallet, +} from "ethers"; +import { ethers } from "hardhat"; +import { setTimeout } from "timers/promises"; + +export function sendTransactionsWithInterval( + signer: Wallet, + transactionRequest: TransactionRequest, + pollingInterval: number +) { + return setInterval(async function () { + const tx = await signer.sendTransaction(transactionRequest); + await tx.wait(); + }, pollingInterval); +} + +export function getAndIncreaseFeeData( + feeData: FeeData +): [BigNumberish, BigNumberish, BigNumberish] { + const maxPriorityFeePerGas = BigInt( + (parseFloat(feeData.maxPriorityFeePerGas!.toString()) * 1.1).toFixed(0) + ); + const maxFeePerGas = BigInt( + (parseFloat(feeData.maxFeePerGas!.toString()) * 1.1).toFixed(0) + ); + const gasPrice = BigInt( + (parseFloat(feeData.gasPrice!.toString()) * 1.1).toFixed(0) + ); + return [maxPriorityFeePerGas, maxFeePerGas, gasPrice]; +} + +export async function waitForL2BlockNumberFinalized( + rollup: Contract, + afterBlockNo: number, + pollingInterval: number +) { + let currentL2BlockNumberFinalized; + do { + currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ + blockTag: "finalized", + }); + await setTimeout(pollingInterval); + } while (currentL2BlockNumberFinalized < afterBlockNo); +} + +export async function waitForLatestL2BlockNumberFinalizedToChange( + rollup: Contract, + pollingInterval: number +) { + const currentL2BlockNumber = await rollup.currentL2BlockNumber(); + let newL2BlockNumber; + do { + newL2BlockNumber = await rollup.currentL2BlockNumber(); + await setTimeout(pollingInterval); + } while (currentL2BlockNumber === newL2BlockNumber); +} + +export const deployContract = async ( + name: string, + provider: Wallet, + ...args: any[] +) => { + const factory = await ethers.getContractFactory(name, provider); + const contract = await factory.deploy(...args); + await contract.waitForDeployment(); + return contract; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbde1be2d..ed71e7ad1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16844,7 +16844,7 @@ snapshots: '@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10))': dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) ethers: 6.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) lodash.isequal: 4.5.0 @@ -16865,7 +16865,7 @@ snapshots: '@nomicfoundation/ignition-core': 0.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@nomicfoundation/ignition-ui': 0.15.1 chalk: 4.1.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fs-extra: 10.1.0 hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) prompts: 2.4.2 @@ -16946,7 +16946,7 @@ snapshots: '@ethersproject/address': 5.7.0 cbor: 8.1.0 chalk: 2.4.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) lodash.clonedeep: 4.5.0 semver: 6.3.1 @@ -16961,7 +16961,7 @@ snapshots: '@ethersproject/address': 5.7.0 cbor: 8.1.0 chalk: 2.4.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) lodash.clonedeep: 4.5.0 semver: 6.3.1 @@ -16975,7 +16975,7 @@ snapshots: '@ethersproject/address': 5.6.1 '@nomicfoundation/solidity-analyzer': 0.1.1 cbor: 9.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) ethers: 6.13.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) fs-extra: 10.1.0 immer: 10.0.2 @@ -19890,7 +19890,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -23443,7 +23443,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) for-each@0.3.3: dependencies: @@ -24052,7 +24052,7 @@ snapshots: axios: 0.21.4(debug@4.3.4) chalk: 4.1.2 chokidar: 3.6.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) enquirer: 2.4.1 ethers: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) form-data: 4.0.0 @@ -24217,7 +24217,7 @@ snapshots: chalk: 2.4.2 chokidar: 3.6.0 ci-info: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) enquirer: 2.4.1 env-paths: 2.2.1 ethereum-cryptography: 1.2.0 @@ -24298,7 +24298,7 @@ snapshots: uuid: 8.3.2 ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@5.0.10) optionalDependencies: - ts-node: 10.9.2(@types/node@18.19.31)(typescript@5.4.5) + ts-node: 10.9.2(@types/node@20.11.20)(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: - bufferutil @@ -24513,7 +24513,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -29836,7 +29836,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 2.1.2 @@ -30544,7 +30544,7 @@ snapshots: typechain@8.3.2(typescript@5.4.5): dependencies: '@types/prettier': 2.7.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fs-extra: 7.0.1 glob: 7.1.7 js-sha3: 0.8.0 From 6a14f257682e168a9e2470e5706b8aa0bd5200a6 Mon Sep 17 00:00:00 2001 From: Julink Date: Tue, 23 Jul 2024 17:43:28 +0200 Subject: [PATCH 04/13] fix: linea-ccip-gateway TestL1 contract --- .../linea-ccip-gateway/contracts/TestL1.sol | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/linea-ccip-gateway/contracts/TestL1.sol b/packages/linea-ccip-gateway/contracts/TestL1.sol index 02b7aaddd..66705aabe 100644 --- a/packages/linea-ccip-gateway/contracts/TestL1.sol +++ b/packages/linea-ccip-gateway/contracts/TestL1.sol @@ -8,7 +8,7 @@ import {IEVMVerifier} from "linea-state-verifier/contracts/IEVMVerifier.sol"; contract TestL1 is EVMFetchTarget { using EVMFetcher for EVMFetcher.EVMFetchRequest; - uint256 constant L2_BLOCK_RANGE_ACCEPTED = 86400; + uint256 constant ACCEPTED_L2_BLOCK_RANGE_LENGTH = 86400; IEVMVerifier verifier; // Slot 0 address target; @@ -18,10 +18,22 @@ contract TestL1 is EVMFetchTarget { target = _target; } + /** + * @dev inherits from EVMFetchTarget + */ + function getAcceptedL2BlockRangeLength() + public + pure + override + returns (uint256) + { + return ACCEPTED_L2_BLOCK_RANGE_LENGTH; + } + function getLatest() public view returns (uint256) { EVMFetcher.newFetchRequest(verifier, target).getStatic(0).fetch( this.getLatestCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) + "" ); } @@ -35,7 +47,7 @@ contract TestL1 is EVMFetchTarget { function getName() public view returns (string memory) { EVMFetcher.newFetchRequest(verifier, target).getDynamic(1).fetch( this.getNameCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) + "" ); } @@ -51,10 +63,7 @@ contract TestL1 is EVMFetchTarget { .newFetchRequest(verifier, target) .getDynamic(3) .element(idx) - .fetch( - this.getHighscorerCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) - ); + .fetch(this.getHighscorerCallback.selector, ""); } function getHighscorerCallback( @@ -70,10 +79,7 @@ contract TestL1 is EVMFetchTarget { .getStatic(0) .getStatic(2) .ref(0) - .fetch( - this.getLatestHighscoreCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) - ); + .fetch(this.getLatestHighscoreCallback.selector, ""); } function getLatestHighscoreCallback( @@ -89,10 +95,7 @@ contract TestL1 is EVMFetchTarget { .getStatic(0) .getDynamic(3) .ref(0) - .fetch( - this.getLatestHighscorerCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) - ); + .fetch(this.getLatestHighscorerCallback.selector, ""); } function getLatestHighscorerCallback( @@ -109,10 +112,7 @@ contract TestL1 is EVMFetchTarget { .newFetchRequest(verifier, target) .getDynamic(4) .element(_name) - .fetch( - this.getNicknameCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) - ); + .fetch(this.getNicknameCallback.selector, ""); } function getNicknameCallback( @@ -128,10 +128,7 @@ contract TestL1 is EVMFetchTarget { .getDynamic(1) .getDynamic(4) .ref(0) - .fetch( - this.getPrimaryNicknameCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) - ); + .fetch(this.getPrimaryNicknameCallback.selector, ""); } function getPrimaryNicknameCallback( @@ -144,7 +141,7 @@ contract TestL1 is EVMFetchTarget { function getZero() public view returns (uint256) { EVMFetcher.newFetchRequest(verifier, target).getStatic(5).fetch( this.getZeroCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) + "" ); } @@ -161,10 +158,7 @@ contract TestL1 is EVMFetchTarget { .getStatic(5) .getStatic(2) .ref(0) - .fetch( - this.getZeroIndexCallback.selector, - abi.encode(L2_BLOCK_RANGE_ACCEPTED) - ); + .fetch(this.getZeroIndexCallback.selector, ""); } function getZeroIndexCallback( From 4e156e5a4a5a0c4ca5375b489577320a34a82212 Mon Sep 17 00:00:00 2001 From: Julink Date: Wed, 24 Jul 2024 09:15:40 +0200 Subject: [PATCH 05/13] fix: tags in wait functions + comments --- .../test/testL1ResolverLocal.spec.ts | 33 ++++++++++++++++--- packages/linea-ens-resolver/test/utils.ts | 12 +++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts index 736620357..395c6678a 100644 --- a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -31,7 +31,7 @@ const coinType = 0; // Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" const SIGNER_L1_PK = "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"; -// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" +// Account 1 on L2 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" const SIGNER_L2_PK = "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; @@ -270,7 +270,6 @@ describe("Crosschain Resolver Local", () => { .then((tx) => tx.wait()); const tx = await l2Resolver.setContenthash(subDomainNode, contenthash); - const txReceipt = await tx.wait(); lastSetupTxBlockNumber = txReceipt.blockNumber; @@ -291,6 +290,12 @@ describe("Crosschain Resolver Local", () => { }); it("should revert when querying L1Resolver and the currentL2BlockNumber is older than the L2 block number we are fetching the data from", async () => { + // This test needs at least one finalized L2 block + await waitForLatestL2BlockNumberFinalizedToChange( + rollup, + 2000, + "finalized" + ); const currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ blockTag: "finalized", }); @@ -377,7 +382,7 @@ describe("Crosschain Resolver Local", () => { // Wait for the latest L2 finalized block to more recent than the L2 block number we are fetching the data from await waitForL2BlockNumberFinalized(rollup, lastSetupTxBlockNumber, 5000); - let tx = await target + await target .setTarget(encodedname, l2ResolverAddress) .then((tx) => tx.wait()); const result = await l2Resolver["addr(bytes32)"](node); @@ -468,8 +473,26 @@ describe("Crosschain Resolver Local", () => { } }); - it("should not revert when querying L1Resolver right after a finalization has occured", async () => { - waitForLatestL2BlockNumberFinalizedToChange(rollup, 2000); + it("should not revert when querying L1Resolver right after a finalization has occured(based on latest L1 block)", async () => { + await waitForLatestL2BlockNumberFinalizedToChange(rollup, 2000); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(ethers.getAddress(decoded[0])).to.equal( + ethers.getAddress(REGISTRANT_ADDR) + ); + }); + + it("should not revert when querying L1Resolver right after a finalization has occured(based on lastest L1 finalized block)", async () => { + await waitForLatestL2BlockNumberFinalizedToChange( + rollup, + 2000, + "finalized" + ); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [subDomainNode]); diff --git a/packages/linea-ens-resolver/test/utils.ts b/packages/linea-ens-resolver/test/utils.ts index 9972b3dba..0bc436776 100644 --- a/packages/linea-ens-resolver/test/utils.ts +++ b/packages/linea-ens-resolver/test/utils.ts @@ -4,6 +4,7 @@ import { FeeData, TransactionRequest, Wallet, + BlockTag, } from "ethers"; import { ethers } from "hardhat"; import { setTimeout } from "timers/promises"; @@ -50,12 +51,17 @@ export async function waitForL2BlockNumberFinalized( export async function waitForLatestL2BlockNumberFinalizedToChange( rollup: Contract, - pollingInterval: number + pollingInterval: number, + blockTag: BlockTag = "latest" ) { - const currentL2BlockNumber = await rollup.currentL2BlockNumber(); + const currentL2BlockNumber = await rollup.currentL2BlockNumber({ + blockTag, + }); let newL2BlockNumber; do { - newL2BlockNumber = await rollup.currentL2BlockNumber(); + newL2BlockNumber = await rollup.currentL2BlockNumber({ + blockTag, + }); await setTimeout(pollingInterval); } while (currentL2BlockNumber === newL2BlockNumber); } From 0255e41039d440470e9a2dab8654521c9eb91df6 Mon Sep 17 00:00:00 2001 From: Julink Date: Wed, 24 Jul 2024 11:33:53 +0200 Subject: [PATCH 06/13] feat: added test to manipulate block number after ccip request --- .../test/testL1ResolverLocal.spec.ts | 34 ++++++++++++++++ packages/linea-ens-resolver/test/utils.ts | 39 +++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts index 395c6678a..883fbf944 100644 --- a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -10,8 +10,11 @@ import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/ import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; import { EthereumProvider } from "hardhat/types"; import { + changeBlockNumberInCCIPResponse, deployContract, + fetchCCIPGateway, getAndIncreaseFeeData, + getExtraData, sendTransactionsWithInterval, waitForL2BlockNumberFinalized, waitForLatestL2BlockNumberFinalizedToChange, @@ -505,6 +508,37 @@ describe("Crosschain Resolver Local", () => { ); }); + it("should revert when block sent by the gateway > currentL2BlockNumber", async () => { + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + try { + await target.resolve(encodedSubDomain, calldata); + } catch (e) { + const extraData: string = getExtraData(e); + const resultData: string = await fetchCCIPGateway(e); + + const currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ + blockTag: "finalized", + }); + const wrongL2BlockNumber = currentL2BlockNumberFinalized + BigInt(10); + + // Construct the new data string + const resultDataModified = changeBlockNumberInCCIPResponse( + resultData, + wrongL2BlockNumber + ); + + try { + await target.getStorageSlotsCallback(resultDataModified, extraData); + throw "Should have reverted"; + } catch (error) { + expect(error.reason).to.equal( + "LineaSparseProofVerifier: invalid state root" + ); + } + } + }); + after(async () => { clearInterval(sendTransactionsPromise); }); diff --git a/packages/linea-ens-resolver/test/utils.ts b/packages/linea-ens-resolver/test/utils.ts index 0bc436776..1b8860f03 100644 --- a/packages/linea-ens-resolver/test/utils.ts +++ b/packages/linea-ens-resolver/test/utils.ts @@ -5,6 +5,8 @@ import { TransactionRequest, Wallet, BlockTag, + FetchRequest, + AbiCoder, } from "ethers"; import { ethers } from "hardhat"; import { setTimeout } from "timers/promises"; @@ -63,16 +65,45 @@ export async function waitForLatestL2BlockNumberFinalizedToChange( blockTag, }); await setTimeout(pollingInterval); - } while (currentL2BlockNumber === newL2BlockNumber); + } while (currentL2BlockNumber >= newL2BlockNumber); } -export const deployContract = async ( +export async function deployContract( name: string, provider: Wallet, ...args: any[] -) => { +) { const factory = await ethers.getContractFactory(name, provider); const contract = await factory.deploy(...args); await contract.waitForDeployment(); return contract; -}; +} + +export async function fetchCCIPGateway(ccipCustomException) { + const sender = ccipCustomException.revert.args[0]; + const url = ccipCustomException.revert.args[1]; + const data = ccipCustomException.revert.args[2]; + const request = new FetchRequest(url); + request.body = { data, sender }; + + const resp = await request.send(); + return resp.bodyJson.data; +} + +export function getExtraData(ccipCustomException) { + return ccipCustomException.revert.args[4]; +} + +export function changeBlockNumberInCCIPResponse( + ccipRespData: string, + newBlockNumber: bigint +) { + const prefix = ccipRespData.slice(0, 2 + 64 * 2); + const suffix = ccipRespData.slice(2 + 64 * 3); + + const blockNoTestHex = AbiCoder.defaultAbiCoder() + .encode(["uint256"], [newBlockNumber]) + .slice(2); + + return prefix + blockNoTestHex + suffix; +} From cd40e58743be7f83e7bd0b8620a28cb92a4f9112 Mon Sep 17 00:00:00 2001 From: Julink Date: Wed, 24 Jul 2024 12:47:26 +0200 Subject: [PATCH 07/13] fix: lastSetupTxBlockNumber type --- .../linea-ens-resolver/test/testL1ResolverLocal.spec.ts | 8 +++++--- packages/linea-ens-resolver/test/utils.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts index 883fbf944..9a95fb8ce 100644 --- a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -87,7 +87,7 @@ describe("Crosschain Resolver Local", () => { signerL2Address, l2ResolverAddress, wrapperAddress; - let lastSetupTxBlockNumber: number; + let lastSetupTxBlockNumber: bigint; let sendTransactionsPromise: NodeJS.Timeout; before(async () => { @@ -274,7 +274,7 @@ describe("Crosschain Resolver Local", () => { const tx = await l2Resolver.setContenthash(subDomainNode, contenthash); const txReceipt = await tx.wait(); - lastSetupTxBlockNumber = txReceipt.blockNumber; + lastSetupTxBlockNumber = BigInt(txReceipt.blockNumber); // Generate activity on Linea to make finalization events happen const [maxPriorityFeePerGas, maxFeePerGas] = getAndIncreaseFeeData( @@ -302,7 +302,9 @@ describe("Crosschain Resolver Local", () => { const currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ blockTag: "finalized", }); - expect(currentL2BlockNumberFinalized < lastSetupTxBlockNumber); + expect(lastSetupTxBlockNumber).to.be.greaterThan( + currentL2BlockNumberFinalized + ); await target .setTarget(encodedname, l2ResolverAddress) .then((tx) => tx.wait()); diff --git a/packages/linea-ens-resolver/test/utils.ts b/packages/linea-ens-resolver/test/utils.ts index 1b8860f03..3da03b62f 100644 --- a/packages/linea-ens-resolver/test/utils.ts +++ b/packages/linea-ens-resolver/test/utils.ts @@ -39,7 +39,7 @@ export function getAndIncreaseFeeData( export async function waitForL2BlockNumberFinalized( rollup: Contract, - afterBlockNo: number, + afterBlockNo: bigint, pollingInterval: number ) { let currentL2BlockNumberFinalized; From 164a1b6df9d4426034b689a5b3ecaf1dcf529fc3 Mon Sep 17 00:00:00 2001 From: Julink Date: Wed, 24 Jul 2024 18:53:51 +0200 Subject: [PATCH 08/13] feat: added tests when shomei-frontend or l2-node shut and comes back up --- .../test/testL1ResolverLocal.spec.ts | 181 ++++++++++++++---- packages/linea-ens-resolver/test/utils.ts | 40 +++- 2 files changed, 179 insertions(+), 42 deletions(-) diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts index 9a95fb8ce..c5d8215fa 100644 --- a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -12,6 +12,7 @@ import { EthereumProvider } from "hardhat/types"; import { changeBlockNumberInCCIPResponse, deployContract, + execDockerCommand, fetchCCIPGateway, getAndIncreaseFeeData, getExtraData, @@ -19,6 +20,7 @@ import { waitForL2BlockNumberFinalized, waitForLatestL2BlockNumberFinalizedToChange, } from "./utils"; +import { setTimeout } from "timers/promises"; const labelhash = (label) => ethers.keccak256(ethers.toUtf8Bytes(label)); const encodeName = (name) => "0x" + packet.name.encode(name).toString("hex"); @@ -37,6 +39,9 @@ const SIGNER_L1_PK = // Account 1 on L2 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" const SIGNER_L2_PK = "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; +// Account 2 on L2 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" +const SIGNER_L2_2_PK = + "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"; const REGISTRANT_ADDR = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; @@ -44,6 +49,10 @@ const SUB_DOMAIN = "testpoh.linea-test.eth"; const subDomainNode = ethers.namehash(SUB_DOMAIN); const encodedSubDomain = encodeName(SUB_DOMAIN); +const SUB_SUB_DOMAIN = "foo.testpoh.linea-test.eth"; +const subSubNode = ethers.namehash(SUB_SUB_DOMAIN); +const encodedSubSubDomain = encodeName(SUB_SUB_DOMAIN); + const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; const EMPTY_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; @@ -81,13 +90,14 @@ describe("Crosschain Resolver Local", () => { let wrapper: Contract; let baseRegistrar: Contract; let rollup: Contract; + let l2factoryContract: Contract; let signerL1, signerL2, signerL1Address, signerL2Address, l2ResolverAddress, wrapperAddress; - let lastSetupTxBlockNumber: bigint; + let lastSetupTxBlockNumber = BigInt(0); let sendTransactionsPromise: NodeJS.Timeout; before(async () => { @@ -230,7 +240,7 @@ describe("Crosschain Resolver Local", () => { await verifier.waitForDeployment(); const implContract = await deployContract("DelegatableResolver", signerL2); - const l2factoryContract = await deployContract( + l2factoryContract = await deployContract( "DelegatableResolverFactory", signerL2, await implContract.getAddress() @@ -249,7 +259,7 @@ describe("Crosschain Resolver Local", () => { await ens.getAddress(), wrapperAddress, "", - 59141 + L2_CHAIN_ID ); const delegatableResolverImpl = await ethers.getContractFactory( @@ -272,8 +282,16 @@ describe("Crosschain Resolver Local", () => { .setText(subDomainNode, "name", "test.eth") .then((tx) => tx.wait()); - const tx = await l2Resolver.setContenthash(subDomainNode, contenthash); + await l2Resolver + .setContenthash(subDomainNode, contenthash) + .then((tx) => tx.wait()); + + const tx = await l2Resolver["setAddr(bytes32,address)"]( + subSubNode, + REGISTRANT_ADDR + ); const txReceipt = await tx.wait(); + // Kepp track of the last block tx's setup block number lastSetupTxBlockNumber = BigInt(txReceipt.blockNumber); // Generate activity on Linea to make finalization events happen @@ -293,18 +311,7 @@ describe("Crosschain Resolver Local", () => { }); it("should revert when querying L1Resolver and the currentL2BlockNumber is older than the L2 block number we are fetching the data from", async () => { - // This test needs at least one finalized L2 block - await waitForLatestL2BlockNumberFinalizedToChange( - rollup, - 2000, - "finalized" - ); - const currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ - blockTag: "finalized", - }); - expect(lastSetupTxBlockNumber).to.be.greaterThan( - currentL2BlockNumberFinalized - ); + await waitForL2BlockNumberFinalized(rollup, BigInt(1), 2000); await target .setTarget(encodedname, l2ResolverAddress) .then((tx) => tx.wait()); @@ -390,25 +397,23 @@ describe("Crosschain Resolver Local", () => { await target .setTarget(encodedname, l2ResolverAddress) .then((tx) => tx.wait()); - const result = await l2Resolver["addr(bytes32)"](node); - expect(result).to.equal(EMPTY_ADDRESS); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [node]); - const result2 = await target.resolve(encodedname, calldata, { + const result = await target.resolve(encodedname, calldata, { enableCcipRead: true, }); - const decoded = i.decodeFunctionResult("addr", result2); + const decoded = i.decodeFunctionResult("addr", result); expect(decoded[0]).to.equal(EMPTY_ADDRESS); }); it("should resolve ETH Address", async () => { const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [subDomainNode]); - const result2 = await target.resolve(encodedSubDomain, calldata, { + const result = await target.resolve(encodedSubDomain, calldata, { enableCcipRead: true, }); - const decoded = i.decodeFunctionResult("addr", result2); + const decoded = i.decodeFunctionResult("addr", result); expect(ethers.getAddress(decoded[0])).to.equal( ethers.getAddress(REGISTRANT_ADDR) ); @@ -419,10 +424,10 @@ describe("Crosschain Resolver Local", () => { "function addr(bytes32,uint256) returns(bytes)", ]); const calldata = i.encodeFunctionData("addr", [subDomainNode, coinType]); - const result2 = await target.resolve(encodedSubDomain, calldata, { + const result = await target.resolve(encodedSubDomain, calldata, { enableCcipRead: true, }); - const decoded = i.decodeFunctionResult("addr", result2); + const decoded = i.decodeFunctionResult("addr", result); expect(decoded[0]).to.equal(testAddr); }); @@ -431,22 +436,33 @@ describe("Crosschain Resolver Local", () => { "function text(bytes32,string) returns(string)", ]); const calldata = i.encodeFunctionData("text", [subDomainNode, "name"]); - const result2 = await target.resolve(encodedSubDomain, calldata, { + const result = await target.resolve(encodedSubDomain, calldata, { enableCcipRead: true, }); - const decoded = i.decodeFunctionResult("text", result2); + const decoded = i.decodeFunctionResult("text", result); expect(decoded[0]).to.equal("test.eth"); }); + it("should resolve ETH Address for sub sub domain", async () => { + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subSubNode]); + + const result = await target.resolve(encodedSubSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result); + expect(decoded[0]).to.equal(REGISTRANT_ADDR); + }); + it("should resolve contenthash", async () => { const i = new ethers.Interface([ "function contenthash(bytes32) returns(bytes)", ]); const calldata = i.encodeFunctionData("contenthash", [subDomainNode]); - const result2 = await target.resolve(encodedSubDomain, calldata, { + const result = await target.resolve(encodedSubDomain, calldata, { enableCcipRead: true, }); - const decoded = i.decodeFunctionResult("contenthash", result2); + const decoded = i.decodeFunctionResult("contenthash", result); expect(decoded[0]).to.equal(contenthash); }); @@ -483,10 +499,10 @@ describe("Crosschain Resolver Local", () => { const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [subDomainNode]); - const result2 = await target.resolve(encodedSubDomain, calldata, { + const result = await target.resolve(encodedSubDomain, calldata, { enableCcipRead: true, }); - const decoded = i.decodeFunctionResult("addr", result2); + const decoded = i.decodeFunctionResult("addr", result); expect(ethers.getAddress(decoded[0])).to.equal( ethers.getAddress(REGISTRANT_ADDR) ); @@ -501,10 +517,10 @@ describe("Crosschain Resolver Local", () => { const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [subDomainNode]); - const result2 = await target.resolve(encodedSubDomain, calldata, { + const result = await target.resolve(encodedSubDomain, calldata, { enableCcipRead: true, }); - const decoded = i.decodeFunctionResult("addr", result2); + const decoded = i.decodeFunctionResult("addr", result); expect(ethers.getAddress(decoded[0])).to.equal( ethers.getAddress(REGISTRANT_ADDR) ); @@ -524,7 +540,6 @@ describe("Crosschain Resolver Local", () => { }); const wrongL2BlockNumber = currentL2BlockNumberFinalized + BigInt(10); - // Construct the new data string const resultDataModified = changeBlockNumberInCCIPResponse( resultData, wrongL2BlockNumber @@ -541,6 +556,106 @@ describe("Crosschain Resolver Local", () => { } }); + it("should revert when block number has been altered and does not match the state root hash used by the gateway", async () => { + const previousL2BlockNumber = await rollup.currentL2BlockNumber({ + blockTag: "finalized", + }); + await waitForLatestL2BlockNumberFinalizedToChange( + rollup, + 2000, + "finalized" + ); + const currentL2BlockNumber = await rollup.currentL2BlockNumber({ + blockTag: "finalized", + }); + + expect(currentL2BlockNumber).to.be.greaterThan(previousL2BlockNumber); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + try { + await target.resolve(encodedSubDomain, calldata); + } catch (e) { + const extraData: string = getExtraData(e); + const resultData: string = await fetchCCIPGateway(e); + // Construct the new data string + const resultDataModified = changeBlockNumberInCCIPResponse( + resultData, + previousL2BlockNumber + ); + + try { + await target.getStorageSlotsCallback(resultDataModified, extraData); + throw "Should have reverted"; + } catch (error) { + expect(error.reason).to.equal( + "LineaProofHelper: invalid account proof" + ); + } + } + }); + + it("should revert if shomei node is down and keep resolving after it starts back up", async () => { + await execDockerCommand("stop", "shomei-frontend"); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + + try { + await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (error) { + expect(error.shortMessage).to.equal( + `error encountered during CCIP fetch: "Internal server error: Error: connect ECONNREFUSED ::1:8889"` + ); + } + + await execDockerCommand("start", "shomei-frontend"); + await setTimeout(5_000); + await waitForLatestL2BlockNumberFinalizedToChange( + rollup, + 2000, + "finalized" + ); + const result = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result); + expect(ethers.getAddress(decoded[0])).to.equal( + ethers.getAddress(REGISTRANT_ADDR) + ); + }); + + it("should revert if l2 node is down and keep resolving after it starts back up", async () => { + await execDockerCommand("stop", "l2-node"); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + + try { + await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (error) { + expect(error.shortMessage).to.equal( + `error encountered during CCIP fetch: "Internal server error: Error: connect ECONNREFUSED ::1:8845"` + ); + } + + await execDockerCommand("start", "l2-node"); + await setTimeout(5_000); + const result = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result); + expect(ethers.getAddress(decoded[0])).to.equal( + ethers.getAddress(REGISTRANT_ADDR) + ); + }); + after(async () => { clearInterval(sendTransactionsPromise); }); diff --git a/packages/linea-ens-resolver/test/utils.ts b/packages/linea-ens-resolver/test/utils.ts index 3da03b62f..40381744f 100644 --- a/packages/linea-ens-resolver/test/utils.ts +++ b/packages/linea-ens-resolver/test/utils.ts @@ -1,3 +1,4 @@ +import { exec } from "child_process"; import { BigNumberish, Contract, @@ -44,9 +45,10 @@ export async function waitForL2BlockNumberFinalized( ) { let currentL2BlockNumberFinalized; do { - currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ - blockTag: "finalized", - }); + currentL2BlockNumberFinalized = + (await rollup.currentL2BlockNumber({ + blockTag: "finalized", + })) || BigInt(0); await setTimeout(pollingInterval); } while (currentL2BlockNumberFinalized < afterBlockNo); } @@ -56,14 +58,16 @@ export async function waitForLatestL2BlockNumberFinalizedToChange( pollingInterval: number, blockTag: BlockTag = "latest" ) { - const currentL2BlockNumber = await rollup.currentL2BlockNumber({ - blockTag, - }); + const currentL2BlockNumber = + (await rollup.currentL2BlockNumber({ + blockTag, + })) || BigInt(0); let newL2BlockNumber; do { - newL2BlockNumber = await rollup.currentL2BlockNumber({ - blockTag, - }); + newL2BlockNumber = + (await rollup.currentL2BlockNumber({ + blockTag, + })) || BigInt(0); await setTimeout(pollingInterval); } while (currentL2BlockNumber >= newL2BlockNumber); } @@ -107,3 +111,21 @@ export function changeBlockNumberInCCIPResponse( return prefix + blockNoTestHex + suffix; } + +export async function execDockerCommand( + command: string, + containerName: string +): Promise { + const dockerCommand = `docker ${command} ${containerName}`; + console.log(`Executing: ${dockerCommand}...`); + return new Promise((resolve, reject) => { + exec(dockerCommand, (error, stdout, stderr) => { + if (error) { + console.error(`Error executing (${dockerCommand}): ${stderr}`); + reject(error); + } + console.log(`Execution success (${dockerCommand}): ${stdout}`); + resolve(stdout); + }); + }); +} From 7c26a7934275200dd0417472e9fcba866a09516e Mon Sep 17 00:00:00 2001 From: Julink Date: Thu, 25 Jul 2024 14:24:51 +0200 Subject: [PATCH 09/13] feat: return generic error message from ccip-gateway instead of specific ones --- .../src/evm-gateway/EVMGateway.ts | 2 +- .../test/testL1ResolverLocal.spec.ts | 63 +++++++++++-------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts b/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts index 2f53b554c..b31d76cfe 100644 --- a/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts +++ b/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts @@ -103,7 +103,7 @@ export class EVMGateway { return [proofs]; } catch (e) { logError(e, { addr, commands, constants }); - throw e; + throw "ccip-gateway error calling getStorageSlots"; } }, }, diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts index c5d8215fa..a1219ef16 100644 --- a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -80,7 +80,15 @@ declare module "hardhat/types/runtime" { } } -// These tests need to be run along the Linea local stack running +/** + * Requirements to run those test: + * - In the zkevm-monorepo: Change the config aggregation-proofs-limit in config/coordinator/coordinator-docker.config.toml + * to aggregation-proofs-limit=15 + * - Then run make fresh-start-all + * - In the linea-ens monorepo: Run cd packages/linea-ens-resolver/ + * - Then run pnpm i + * - pnpm test:local + */ describe("Crosschain Resolver Local", () => { let l1Provider: JsonRpcProvider; let l2Provider: JsonRpcProvider; @@ -326,8 +334,8 @@ describe("Crosschain Resolver Local", () => { }); throw "Should have reverted"; } catch (e) { - expect(e.shortMessage).contain( - `error encountered during CCIP fetch: "Internal server error: No storage proofs on contract ${l2ResolverAddress}` + expect(e.shortMessage).equal( + `error encountered during CCIP fetch: "Internal server error: ccip-gateway error calling getStorageSlots"` ); } }); @@ -595,7 +603,7 @@ describe("Crosschain Resolver Local", () => { } }); - it("should revert if shomei node is down and keep resolving after it starts back up", async () => { + it("should revert if shomei-frontend is down and ccip-gateway should keep responding after shomei-frontend starts back up", async () => { await execDockerCommand("stop", "shomei-frontend"); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); @@ -608,27 +616,26 @@ describe("Crosschain Resolver Local", () => { throw "Should have reverted"; } catch (error) { expect(error.shortMessage).to.equal( - `error encountered during CCIP fetch: "Internal server error: Error: connect ECONNREFUSED ::1:8889"` + `error encountered during CCIP fetch: "Internal server error: ccip-gateway error calling getStorageSlots"` ); } await execDockerCommand("start", "shomei-frontend"); await setTimeout(5_000); - await waitForLatestL2BlockNumberFinalizedToChange( - rollup, - 2000, - "finalized" - ); - const result = await target.resolve(encodedSubDomain, calldata, { - enableCcipRead: true, - }); - const decoded = i.decodeFunctionResult("addr", result); - expect(ethers.getAddress(decoded[0])).to.equal( - ethers.getAddress(REGISTRANT_ADDR) - ); + + try { + await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (error) { + expect(error.shortMessage).to.equal( + `error encountered during CCIP fetch: "Internal server error: ccip-gateway error calling getStorageSlots"` + ); + } }); - it("should revert if l2 node is down and keep resolving after it starts back up", async () => { + it("should revert if l2 node is down and ccip-gateway should keep responding after l2-node starts back up", async () => { await execDockerCommand("stop", "l2-node"); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); @@ -641,19 +648,23 @@ describe("Crosschain Resolver Local", () => { throw "Should have reverted"; } catch (error) { expect(error.shortMessage).to.equal( - `error encountered during CCIP fetch: "Internal server error: Error: connect ECONNREFUSED ::1:8845"` + `error encountered during CCIP fetch: "Internal server error: ccip-gateway error calling getStorageSlots"` ); } await execDockerCommand("start", "l2-node"); await setTimeout(5_000); - const result = await target.resolve(encodedSubDomain, calldata, { - enableCcipRead: true, - }); - const decoded = i.decodeFunctionResult("addr", result); - expect(ethers.getAddress(decoded[0])).to.equal( - ethers.getAddress(REGISTRANT_ADDR) - ); + + try { + await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (error) { + expect(error.shortMessage).to.equal( + `error encountered during CCIP fetch: "Internal server error: ccip-gateway error calling getStorageSlots"` + ); + } }); after(async () => { From b2b2a8d8996cf733ad526a7de504bc259477724f Mon Sep 17 00:00:00 2001 From: Julink Date: Fri, 26 Jul 2024 09:38:00 +0200 Subject: [PATCH 10/13] feat: added debug logs for ccip-gateway when NODE_ENV=='debug' --- packages/linea-ccip-gateway/src/L2ProofService.ts | 7 ++++++- .../linea-ccip-gateway/src/evm-gateway/EVMGateway.ts | 4 +++- .../src/evm-gateway/EVMProofHelper.ts | 5 +++++ packages/linea-ccip-gateway/src/utils.ts | 12 +++++++++++- pnpm-lock.yaml | 5 ++++- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/linea-ccip-gateway/src/L2ProofService.ts b/packages/linea-ccip-gateway/src/L2ProofService.ts index bc9103d6e..c248f11eb 100644 --- a/packages/linea-ccip-gateway/src/L2ProofService.ts +++ b/packages/linea-ccip-gateway/src/L2ProofService.ts @@ -6,7 +6,7 @@ import { ethers, } from "ethers"; import { EVMProofHelper, IProofService, StateProof } from "./evm-gateway"; -import { logError } from "./utils"; +import { logDebug, logError } from "./utils"; export type L2ProvableBlock = number; @@ -44,10 +44,15 @@ export class L2ProofService implements IProofService { */ async getProvableBlock(): Promise { try { + logDebug( + "Calling currentL2BlockNumber() on Rollup Contract", + await this.rollup.getAddress() + ); const lastBlockFinalized = await this.rollup.currentL2BlockNumber({ blockTag: "finalized", }); if (!lastBlockFinalized) throw new Error("No block found"); + logDebug("Provable block found", lastBlockFinalized); return lastBlockFinalized; } catch (e) { logError(e); diff --git a/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts b/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts index b31d76cfe..307292290 100644 --- a/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts +++ b/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts @@ -9,7 +9,7 @@ import { zeroPadValue, } from "ethers"; import { IProofService, ProvableBlock } from "./IProofService"; -import { logError } from "../utils"; +import { logDebug, logError } from "../utils"; const OP_CONSTANT = 0x00; const OP_BACKREF = 0x20; @@ -98,8 +98,10 @@ export class EVMGateway { type: "getStorageSlots", func: async (args) => { const [addr, commands, constants] = args; + logDebug("CCIP request started", addr, commands, constants); try { const proofs = await this.createProofs(addr, commands, constants); + logDebug("CCIP request finished with encoded proofs", proofs); return [proofs]; } catch (e) { logError(e, { addr, commands, constants }); diff --git a/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts b/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts index 1813867ed..17906a8f3 100644 --- a/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts +++ b/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts @@ -1,4 +1,5 @@ import { toBeHex, AddressLike, JsonRpcProvider, ethers } from "ethers"; +import { logDebug } from "../utils"; interface ProofStruct { key: string; @@ -68,6 +69,8 @@ export class EVMProofHelper { "0x" + blockNo.toString(16), ]; + logDebug("Starting getProofs", args); + // We have to reinitilize the provider L2 because of an issue when multiple // requests are sent at the same time, the provider becomes not aware of // the linea_getProof method @@ -80,7 +83,9 @@ export class EVMProofHelper { staticNetwork: true, } ); + logDebug("Calling linea_getProof with L2 provider", providerUrl); const proofs: StateProof = await providerL2.send("linea_getProof", args); + logDebug("Proof result", proofs); return proofs; } } diff --git a/packages/linea-ccip-gateway/src/utils.ts b/packages/linea-ccip-gateway/src/utils.ts index 89e26a509..4a9f99673 100644 --- a/packages/linea-ccip-gateway/src/utils.ts +++ b/packages/linea-ccip-gateway/src/utils.ts @@ -3,5 +3,15 @@ export function logError(error: any, ...objects: any[]) { error: error.message || error, details: objects, }; - console.log(logObject); + console.error(logObject); +} + +export function logDebug(message: string, ...objects: any[]) { + if (process.env.NODE_ENV === "debug") { + const logObject = { + message, + details: objects, + }; + console.dir(logObject, { depth: 6 }); + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed71e7ad1..5637cf8a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5019,6 +5019,7 @@ packages: are-we-there-yet@2.0.0: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} + deprecated: This package is no longer supported. arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -7664,6 +7665,7 @@ packages: gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} + deprecated: This package is no longer supported. gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} @@ -10240,6 +10242,7 @@ packages: npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -24298,7 +24301,7 @@ snapshots: uuid: 8.3.2 ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@5.0.10) optionalDependencies: - ts-node: 10.9.2(@types/node@20.11.20)(typescript@5.4.5) + ts-node: 10.9.2(@types/node@18.19.31)(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: - bufferutil From 1d23a8bfd1d0b8d707bda6aaefdb57a70d2b285a Mon Sep 17 00:00:00 2001 From: Julink Date: Fri, 26 Jul 2024 11:01:05 +0200 Subject: [PATCH 11/13] chore: remove unused constant --- packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts index a1219ef16..46be10253 100644 --- a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -39,9 +39,6 @@ const SIGNER_L1_PK = // Account 1 on L2 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" const SIGNER_L2_PK = "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; -// Account 2 on L2 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" -const SIGNER_L2_2_PK = - "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"; const REGISTRANT_ADDR = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; From c40ecbaae2bb2bdd37f9e4ae7cc21358f37466a6 Mon Sep 17 00:00:00 2001 From: Julink Date: Mon, 29 Jul 2024 08:51:50 +0200 Subject: [PATCH 12/13] feat: added function execute tx and wait to finish its execution --- .../test/testL1ResolverLocal.spec.ts | 114 ++++++++---------- packages/linea-ens-resolver/test/utils.ts | 5 + 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts index 46be10253..84e1903f7 100644 --- a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -13,6 +13,7 @@ import { changeBlockNumberInCCIPResponse, deployContract, execDockerCommand, + executeTransaction, fetchCCIPGateway, getAndIncreaseFeeData, getExtraData, @@ -170,7 +171,7 @@ describe("Crosschain Resolver Local", () => { ethers.namehash("eth") ); const baseRegistrarAddress = await baseRegistrar.getAddress(); - await baseRegistrar.addController(signerL1Address).then((tx) => tx.wait()); + await executeTransaction(baseRegistrar.addController(signerL1Address)); const metaDataservice = await deployContract( "StaticMetadataService", signerL1, @@ -183,22 +184,22 @@ describe("Crosschain Resolver Local", () => { ); const reverseRegistrarAddress = await reverseRegistrar.getAddress(); - await ens - .setSubnodeOwner(EMPTY_BYTES32, labelhash("reverse"), signerL1) - .then((tx) => tx.wait()); - await ens - .setSubnodeOwner( + await executeTransaction( + ens.setSubnodeOwner(EMPTY_BYTES32, labelhash("reverse"), signerL1) + ); + await executeTransaction( + ens.setSubnodeOwner( ethers.namehash("reverse"), labelhash("addr"), reverseRegistrarAddress ) - .then((tx) => tx.wait()); - await ens - .setSubnodeOwner(EMPTY_BYTES32, labelhash("eth"), baseRegistrarAddress) - .then((tx) => tx.wait()); - await baseRegistrar - .register(labelhash(domainName), signerL1Address, 100000000) - .then((tx) => tx.wait()); + ); + await executeTransaction( + ens.setSubnodeOwner(EMPTY_BYTES32, labelhash("eth"), baseRegistrarAddress) + ); + await executeTransaction( + baseRegistrar.register(labelhash(domainName), signerL1Address, 100000000) + ); const publicResolver = await deployContract( "PublicResolver", @@ -209,9 +210,9 @@ describe("Crosschain Resolver Local", () => { await reverseRegistrar.getAddress() ); const publicResolverAddress = await publicResolver.getAddress(); - await reverseRegistrar - .setDefaultResolver(publicResolverAddress) - .then((tx) => tx.wait()); + await executeTransaction( + reverseRegistrar.setDefaultResolver(publicResolverAddress) + ); wrapper = await deployContract( "NameWrapper", @@ -250,9 +251,9 @@ describe("Crosschain Resolver Local", () => { signerL2, await implContract.getAddress() ); - await l2factoryContract - .create(await signerL2.getAddress()) - .then((tx) => tx.wait()); + await executeTransaction( + l2factoryContract.create(await signerL2.getAddress()) + ); const logs = await l2factoryContract.queryFilter("NewDelegatableResolver"); //@ts-ignore l2ResolverAddress = logs[0].args[0]; @@ -272,24 +273,25 @@ describe("Crosschain Resolver Local", () => { signerL2 ); l2Resolver = delegatableResolverImpl.attach(l2ResolverAddress); - await l2Resolver["setAddr(bytes32,address)"]( - subDomainNode, - REGISTRANT_ADDR - ).then((tx) => tx.wait()); + await executeTransaction( + l2Resolver["setAddr(bytes32,address)"](subDomainNode, REGISTRANT_ADDR) + ); - await l2Resolver["setAddr(bytes32,uint256,bytes)"]( - subDomainNode, - coinType, - testAddr - ).then((tx) => tx.wait()); + await executeTransaction( + l2Resolver["setAddr(bytes32,uint256,bytes)"]( + subDomainNode, + coinType, + testAddr + ) + ); - await l2Resolver - .setText(subDomainNode, "name", "test.eth") - .then((tx) => tx.wait()); + await executeTransaction( + l2Resolver.setText(subDomainNode, "name", "test.eth") + ); - await l2Resolver - .setContenthash(subDomainNode, contenthash) - .then((tx) => tx.wait()); + await executeTransaction( + l2Resolver.setContenthash(subDomainNode, contenthash) + ); const tx = await l2Resolver["setAddr(bytes32,address)"]( subSubNode, @@ -317,9 +319,7 @@ describe("Crosschain Resolver Local", () => { it("should revert when querying L1Resolver and the currentL2BlockNumber is older than the L2 block number we are fetching the data from", async () => { await waitForL2BlockNumberFinalized(rollup, BigInt(1), 2000); - await target - .setTarget(encodedname, l2ResolverAddress) - .then((tx) => tx.wait()); + await executeTransaction(target.setTarget(encodedname, l2ResolverAddress)); const result = await l2Resolver["addr(bytes32)"](subDomainNode); expect(ethers.getAddress(result)).to.equal(REGISTRANT_ADDR); @@ -340,9 +340,9 @@ describe("Crosschain Resolver Local", () => { it("should not allow non owner to set target", async () => { const incorrectname = encodeName("notowned.eth"); try { - await target - .setTarget(incorrectname, l2ResolverAddress) - .then((tx) => tx.wait()); + await executeTransaction( + target.setTarget(incorrectname, l2ResolverAddress) + ); throw "Should have reverted"; } catch (e) { expect(e.reason).equal("Not authorized to set target for this node"); @@ -353,17 +353,13 @@ describe("Crosschain Resolver Local", () => { }); it("should allow owner to set target", async () => { - await target - .setTarget(encodedname, signerL1Address) - .then((tx) => tx.wait()); + await executeTransaction(target.setTarget(encodedname, signerL1Address)); const result = await target.getTarget(encodeName(baseDomain)); expect(result[1]).to.equal(signerL1Address); }); it("subname should get target of its parent", async () => { - await target - .setTarget(encodedname, signerL1Address) - .then((tx) => tx.wait()); + await executeTransaction(target.setTarget(encodedname, signerL1Address)); const result = await target.getTarget(encodedSubDomain); expect(result[0]).to.equal(subDomainNode); expect(result[1]).to.equal(signerL1Address); @@ -372,24 +368,22 @@ describe("Crosschain Resolver Local", () => { it("should allow wrapped owner to set target", async () => { const label = "wrapped"; const tokenId = labelhash(label); - await baseRegistrar - .setApprovalForAll(wrapperAddress, true) - .then((tx) => tx.wait()); - await baseRegistrar - .register(tokenId, signerL1Address, 100000000) - .then((tx) => tx.wait()); - await wrapper - .wrapETH2LD( + await executeTransaction( + baseRegistrar.setApprovalForAll(wrapperAddress, true) + ); + await executeTransaction( + baseRegistrar.register(tokenId, signerL1Address, 100000000) + ); + await executeTransaction( + wrapper.wrapETH2LD( label, signerL1Address, 0, // CAN_DO_EVERYTHING EMPTY_ADDRESS ) - .then((tx) => tx.wait()); + ); const wrappedtname = encodeName(`${label}.eth`); - await target - .setTarget(wrappedtname, l2ResolverAddress) - .then((tx) => tx.wait()); + await executeTransaction(target.setTarget(wrappedtname, l2ResolverAddress)); const encodedname = encodeName(`${label}.eth`); const result = await target.getTarget(encodedname); expect(result[1]).to.equal(l2ResolverAddress); @@ -399,9 +393,7 @@ describe("Crosschain Resolver Local", () => { // Wait for the latest L2 finalized block to more recent than the L2 block number we are fetching the data from await waitForL2BlockNumberFinalized(rollup, lastSetupTxBlockNumber, 5000); - await target - .setTarget(encodedname, l2ResolverAddress) - .then((tx) => tx.wait()); + await executeTransaction(target.setTarget(encodedname, l2ResolverAddress)); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [node]); diff --git a/packages/linea-ens-resolver/test/utils.ts b/packages/linea-ens-resolver/test/utils.ts index 40381744f..6e7c65f93 100644 --- a/packages/linea-ens-resolver/test/utils.ts +++ b/packages/linea-ens-resolver/test/utils.ts @@ -129,3 +129,8 @@ export async function execDockerCommand( }); }); } + +export async function executeTransaction(transactionPromise: Promise) { + const tx = await transactionPromise; + return tx.wait(); +} \ No newline at end of file From 40f6231ede4d40d28908bbe7909aaa8180370f6c Mon Sep 17 00:00:00 2001 From: Julink Date: Mon, 29 Jul 2024 09:14:48 +0200 Subject: [PATCH 13/13] feat: added info logs for CCIP gateway --- .../src/evm-gateway/EVMGateway.ts | 13 +++++++++++-- .../src/evm-gateway/EVMProofHelper.ts | 4 ++-- packages/linea-ccip-gateway/src/utils.ts | 8 ++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts b/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts index 307292290..ca19890e9 100644 --- a/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts +++ b/packages/linea-ccip-gateway/src/evm-gateway/EVMGateway.ts @@ -9,7 +9,7 @@ import { zeroPadValue, } from "ethers"; import { IProofService, ProvableBlock } from "./IProofService"; -import { logDebug, logError } from "../utils"; +import { logDebug, logError, logInfo } from "../utils"; const OP_CONSTANT = 0x00; const OP_BACKREF = 0x20; @@ -98,9 +98,18 @@ export class EVMGateway { type: "getStorageSlots", func: async (args) => { const [addr, commands, constants] = args; - logDebug("CCIP request started", addr, commands, constants); + logInfo(`CCIP request started for L2 target contract`, { + address: addr, + }); + logDebug("CCIP request started with args", addr, commands, constants); try { const proofs = await this.createProofs(addr, commands, constants); + logInfo( + `CCIP request successfully executed for L2 target contract`, + { + address: addr, + } + ); logDebug("CCIP request finished with encoded proofs", proofs); return [proofs]; } catch (e) { diff --git a/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts b/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts index 17906a8f3..d48cc2234 100644 --- a/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts +++ b/packages/linea-ccip-gateway/src/evm-gateway/EVMProofHelper.ts @@ -1,5 +1,5 @@ import { toBeHex, AddressLike, JsonRpcProvider, ethers } from "ethers"; -import { logDebug } from "../utils"; +import { logDebug, logInfo } from "../utils"; interface ProofStruct { key: string; @@ -69,7 +69,7 @@ export class EVMProofHelper { "0x" + blockNo.toString(16), ]; - logDebug("Starting getProofs", args); + logInfo("Calling linea_getProof with args", args); // We have to reinitilize the provider L2 because of an issue when multiple // requests are sent at the same time, the provider becomes not aware of diff --git a/packages/linea-ccip-gateway/src/utils.ts b/packages/linea-ccip-gateway/src/utils.ts index 4a9f99673..94d3e08b1 100644 --- a/packages/linea-ccip-gateway/src/utils.ts +++ b/packages/linea-ccip-gateway/src/utils.ts @@ -15,3 +15,11 @@ export function logDebug(message: string, ...objects: any[]) { console.dir(logObject, { depth: 6 }); } } + +export function logInfo(message: string, ...objects: any[]) { + const logObject = { + message, + details: objects, + }; + console.dir(logObject, { depth: 3 }); +}