diff --git a/packages/protocol/contracts/L1/TaikoErrors.sol b/packages/protocol/contracts/L1/TaikoErrors.sol index ae54b5a415..79d3b8c3be 100644 --- a/packages/protocol/contracts/L1/TaikoErrors.sol +++ b/packages/protocol/contracts/L1/TaikoErrors.sol @@ -29,6 +29,7 @@ abstract contract TaikoErrors { error L1_BLOB_NOT_REUSEABLE(); error L1_BLOB_NOT_USED(); error L1_BLOCK_MISMATCH(); + error L1_CHAIN_DATA_NOT_RELAYED(); error L1_INVALID_BLOCK_ID(); error L1_INVALID_CONFIG(); error L1_INVALID_ETH_DEPOSIT(); diff --git a/packages/protocol/contracts/L1/TaikoL1.sol b/packages/protocol/contracts/L1/TaikoL1.sol index 610e178aa2..a8a44c5245 100644 --- a/packages/protocol/contracts/L1/TaikoL1.sol +++ b/packages/protocol/contracts/L1/TaikoL1.sol @@ -175,7 +175,7 @@ contract TaikoL1 is override returns (ICrossChainSync.Snippet memory) { - return LibUtils.getSyncedSnippet(state, getConfig(), blockId); + return LibUtils.getSyncedSnippet(state, getConfig(), AddressResolver(this), blockId); } /// @notice Gets the state variables of the TaikoL1 contract. diff --git a/packages/protocol/contracts/L1/libs/LibUtils.sol b/packages/protocol/contracts/L1/libs/LibUtils.sol index 8568da953e..48873dcf9f 100644 --- a/packages/protocol/contracts/L1/libs/LibUtils.sol +++ b/packages/protocol/contracts/L1/libs/LibUtils.sol @@ -14,7 +14,10 @@ pragma solidity 0.8.24; +import "../../common/AddressResolver.sol"; import "../../common/ICrossChainSync.sol"; +import "../../signal/ISignalService.sol"; +import "../../signal/LibSignals.sol"; import "../TaikoData.sol"; /// @title LibUtils @@ -22,6 +25,7 @@ import "../TaikoData.sol"; library LibUtils { // Warning: Any errors defined here must also be defined in TaikoErrors.sol. error L1_BLOCK_MISMATCH(); + error L1_CHAIN_DATA_NOT_RELAYED(); error L1_INVALID_BLOCK_ID(); error L1_TRANSITION_NOT_FOUND(); error L1_UNEXPECTED_TRANSITION_ID(); @@ -56,6 +60,7 @@ library LibUtils { function getSyncedSnippet( TaikoData.State storage state, TaikoData.Config memory config, + AddressResolver resolver, uint64 blockId ) external @@ -70,14 +75,19 @@ library LibUtils { if (blk.blockId != _blockId) revert L1_BLOCK_MISMATCH(); if (blk.verifiedTransitionId == 0) revert L1_TRANSITION_NOT_FOUND(); - TaikoData.TransitionState storage transition = - state.transitions[slot][blk.verifiedTransitionId]; + TaikoData.TransitionState storage ts = state.transitions[slot][blk.verifiedTransitionId]; + + // bool relayed = ISignalService(resolver.resolve("signal_service", + // false)).isChainDataRelayed( + // config.chainId, LibSignals.STATE_ROOT, ts.stateRoot + // ); + // if (!relayed) revert L1_CHAIN_DATA_NOT_RELAYED(); return ICrossChainSync.Snippet({ syncedInBlock: blk.proposedIn, blockId: blockId, - blockHash: transition.blockHash, - stateRoot: transition.stateRoot + blockHash: ts.blockHash, + stateRoot: ts.stateRoot }); } diff --git a/packages/protocol/contracts/L1/libs/LibVerifying.sol b/packages/protocol/contracts/L1/libs/LibVerifying.sol index 99f582b55d..cb338f873e 100644 --- a/packages/protocol/contracts/L1/libs/LibVerifying.sol +++ b/packages/protocol/contracts/L1/libs/LibVerifying.sol @@ -18,6 +18,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../common/AddressResolver.sol"; import "../../libs/LibMath.sol"; import "../../signal/ISignalService.sol"; +import "../../signal/LibSignals.sol"; import "../tiers/ITierProvider.sol"; import "../TaikoData.sol"; import "./LibUtils.sol"; @@ -247,7 +248,9 @@ library LibVerifying { // This also means if we verified more than one block, only the last one's stateRoot // is sent as a signal and verifiable with merkle proofs, all other blocks' // stateRoot are not. - ISignalService(resolver.resolve("signal_service", false)).sendSignal(stateRoot); + ISignalService(resolver.resolve("signal_service", false)).relayChainData( + config.chainId, LibSignals.STATE_ROOT, stateRoot + ); emit CrossChainSynced( uint64(block.number), lastVerifiedBlockId, blockHash, stateRoot diff --git a/packages/protocol/contracts/L2/TaikoL2.sol b/packages/protocol/contracts/L2/TaikoL2.sol index 86e8a19941..7ee875b2cb 100644 --- a/packages/protocol/contracts/L2/TaikoL2.sol +++ b/packages/protocol/contracts/L2/TaikoL2.sol @@ -19,6 +19,7 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../common/ICrossChainSync.sol"; import "../signal/ISignalService.sol"; +import "../signal/LibSignals.sol"; import "../libs/LibAddress.sol"; import "../libs/LibMath.sol"; import "./Lib1559Math.sol"; @@ -143,7 +144,9 @@ contract TaikoL2 is CrossChainOwned, ICrossChainSync { // Store the L1's state root as a signal to the local signal service to // allow for multi-hop bridging. - ISignalService(resolve("signal_service", false)).sendSignal(l1StateRoot); + ISignalService(resolve("signal_service", false)).relayChainData( + ownerChainId, LibSignals.STATE_ROOT, l1StateRoot + ); emit CrossChainSynced(uint64(block.number), l1Height, l1BlockHash, l1StateRoot); @@ -157,7 +160,6 @@ contract TaikoL2 is CrossChainOwned, ICrossChainSync { }); publicInputHash = publicInputHashNew; latestSyncedL1Height = l1Height; - emit Anchored(blockhash(parentId), gasExcess); } diff --git a/packages/protocol/contracts/bridge/Bridge.sol b/packages/protocol/contracts/bridge/Bridge.sol index d1e819597d..575a1d7573 100644 --- a/packages/protocol/contracts/bridge/Bridge.sol +++ b/packages/protocol/contracts/bridge/Bridge.sol @@ -573,7 +573,7 @@ contract Bridge is EssentialContract, IBridge { /// @param signal The signal. /// @param chainId The ID of the chain the signal is stored on /// @param proof The merkle inclusion proof. - /// @return True if the message was received. + /// @return success True if the message was received. function _proveSignalReceived( address signalService, bytes32 signal, @@ -582,13 +582,12 @@ contract Bridge is EssentialContract, IBridge { ) private view - returns (bool) + returns (bool success) { bytes memory data = abi.encodeCall( ISignalService.proveSignalReceived, (chainId, resolve(chainId, "bridge", false), signal, proof) ); - (bool success, bytes memory ret) = signalService.staticcall(data); - return success ? abi.decode(ret, (bool)) : false; + (success,) = signalService.staticcall(data); } } diff --git a/packages/protocol/contracts/libs/LibTrieProof.sol b/packages/protocol/contracts/libs/LibTrieProof.sol index df4968ddf9..96dab809e5 100644 --- a/packages/protocol/contracts/libs/LibTrieProof.sol +++ b/packages/protocol/contracts/libs/LibTrieProof.sol @@ -20,37 +20,42 @@ library LibTrieProof { error LTP_INVALID_ACCOUNT_PROOF(); error LTP_INVALID_INCLUSION_PROOF(); - /** - * Verifies that the value of a slot in the storage of an account is value. - * - * @param stateRoot The merkle root of state tree. - * @param addr The address of contract. - * @param slot The slot in the contract. - * @param value The value to be verified. - * @param mkproof The proof obtained by encoding storage proof. - */ - function verifyFullMerkleProof( - bytes32 stateRoot, + /// @notice Verifies that the value of a slot in the storage of an account is value. + /// + /// @param rootHash The merkle root of state tree or the account tree. If accountProof's length + /// is zero, it is used as the account's storage root, otherwise it will be used as the state + /// root. + /// @param addr The address of contract. + /// @param slot The slot in the contract. + /// @param value The value to be verified. + /// @param accountProof The account proof + /// @param storageProof The storage proof + /// @return storageRoot The account's storage root + function verifyMerkleProof( + bytes32 rootHash, address addr, bytes32 slot, bytes memory value, - bytes memory mkproof + bytes[] memory accountProof, + bytes[] memory storageProof ) internal pure + returns (bytes32 storageRoot) { - (bytes[] memory accountProof, bytes[] memory storageProof) = - abi.decode(mkproof, (bytes[], bytes[])); + if (accountProof.length != 0) { + bytes memory rlpAccount = + SecureMerkleTrie.get(abi.encodePacked(addr), accountProof, rootHash); - bytes memory rlpAccount = - SecureMerkleTrie.get(abi.encodePacked(addr), accountProof, stateRoot); + if (rlpAccount.length == 0) revert LTP_INVALID_ACCOUNT_PROOF(); - if (rlpAccount.length == 0) revert LTP_INVALID_ACCOUNT_PROOF(); + RLPReader.RLPItem[] memory accountState = RLPReader.readList(rlpAccount); - RLPReader.RLPItem[] memory accountState = RLPReader.readList(rlpAccount); - - bytes memory storageRoot = - RLPReader.readBytes(accountState[ACCOUNT_FIELD_INDEX_STORAGE_HASH]); + storageRoot = + bytes32(RLPReader.readBytes(accountState[ACCOUNT_FIELD_INDEX_STORAGE_HASH])); + } else { + storageRoot = rootHash; + } bool verified = SecureMerkleTrie.verifyInclusionProof( bytes.concat(slot), value, storageProof, bytes32(storageRoot) diff --git a/packages/protocol/contracts/signal/HopRelayRegistry.sol b/packages/protocol/contracts/signal/HopRelayRegistry.sol deleted file mode 100644 index 95f5feaf2f..0000000000 --- a/packages/protocol/contracts/signal/HopRelayRegistry.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT -// _____ _ _ _ _ -// |_ _|_ _(_) |_____ | | __ _| |__ ___ -// | |/ _` | | / / _ \ | |__/ _` | '_ (_-< -// |_|\__,_|_|_\_\___/ |____\__,_|_.__/__/ -// -// Email: security@taiko.xyz -// Website: https://taiko.xyz -// GitHub: https://github.com/taikoxyz -// Discord: https://discord.gg/taikoxyz -// Twitter: https://twitter.com/taikoxyz -// Blog: https://mirror.xyz/labs.taiko.eth -// Youtube: https://www.youtube.com/@taikoxyz - -pragma solidity 0.8.24; - -import "../common/EssentialContract.sol"; -import "./IHopRelayRegistry.sol"; - -/// @title HopRelayRegistry -contract HopRelayRegistry is EssentialContract, IHopRelayRegistry { - mapping(uint64 => mapping(uint64 => mapping(address => bool))) internal registry; - uint256[49] private __gap; - - event RelayRegistered( - uint64 indexed srcChainId, - uint64 indexed hopChainId, - address indexed hopRelay, - bool registered - ); - - error MHG_INVALID_PARAMS(); - error MHG_INVALID_STATE(); - - function init() external initializer { - __Essential_init(); - } - - /// @dev Register a trusted hop relay. - /// @param srcChainId The source chain ID where state roots correspond to. - /// @param hopChainId The hop relay's local chain ID. - /// @param hopRelay The address of the relay. - function registerRelay( - uint64 srcChainId, - uint64 hopChainId, - address hopRelay - ) - external - onlyOwner - { - _registerRelay(srcChainId, hopChainId, hopRelay, true); - } - - /// @dev Deregister a trusted hop relay. - /// @param srcChainId The source chain ID where state roots correspond to. - /// @param hopChainId The hop relay's local chain ID. - /// @param hopRelay The address of the relay. - function deregisterRelay( - uint64 srcChainId, - uint64 hopChainId, - address hopRelay - ) - external - onlyOwner - { - _registerRelay(srcChainId, hopChainId, hopRelay, false); - } - - /// @inheritdoc IHopRelayRegistry - function isRelayRegistered( - uint64 srcChainId, - uint64 hopChainId, - address hopRelay - ) - public - view - returns (bool) - { - return registry[srcChainId][hopChainId][hopRelay]; - } - - function _registerRelay( - uint64 srcChainId, - uint64 hopChainId, - address hopRelay, - bool registered - ) - private - { - if ( - srcChainId == 0 || hopChainId == 0 || srcChainId == hopChainId || hopRelay == address(0) - ) { - revert MHG_INVALID_PARAMS(); - } - if (registry[srcChainId][hopChainId][hopRelay] == registered) { - revert MHG_INVALID_STATE(); - } - registry[srcChainId][hopChainId][hopRelay] = registered; - emit RelayRegistered(srcChainId, hopChainId, hopRelay, registered); - } -} diff --git a/packages/protocol/contracts/signal/IHopRelayRegistry.sol b/packages/protocol/contracts/signal/IHopRelayRegistry.sol deleted file mode 100644 index 659ffa10eb..0000000000 --- a/packages/protocol/contracts/signal/IHopRelayRegistry.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -// _____ _ _ _ _ -// |_ _|_ _(_) |_____ | | __ _| |__ ___ -// | |/ _` | | / / _ \ | |__/ _` | '_ (_-< -// |_|\__,_|_|_\_\___/ |____\__,_|_.__/__/ -// -// Email: security@taiko.xyz -// Website: https://taiko.xyz -// GitHub: https://github.com/taikoxyz -// Discord: https://discord.gg/taikoxyz -// Twitter: https://twitter.com/taikoxyz -// Blog: https://mirror.xyz/labs.taiko.eth -// Youtube: https://www.youtube.com/@taikoxyz - -pragma solidity 0.8.24; - -/// @title IHopRelayRegistry -/// @notice A registry of hop relays for multi-hop bridging. -// A hop relay is a contract that relays a corresponding chain's state roots to its loal signal -// service. -interface IHopRelayRegistry { - /// @dev Returns if a relay is trusted. - /// @param srcChainId The source chain ID where state roots correspond to. - /// @param hopChainId The hop relay's local chain ID. - /// @param hopRelay The address of the relay. - /// @return trusted True if the relay is a trusted one. - function isRelayRegistered( - uint64 srcChainId, - uint64 hopChainId, - address hopRelay - ) - external - view - returns (bool trusted); -} diff --git a/packages/protocol/contracts/signal/ISignalService.sol b/packages/protocol/contracts/signal/ISignalService.sol index ea4a63af0a..3bfd0acfe3 100644 --- a/packages/protocol/contracts/signal/ISignalService.sol +++ b/packages/protocol/contracts/signal/ISignalService.sol @@ -16,33 +16,58 @@ pragma solidity 0.8.24; /// a merkle proof. interface ISignalService { - /// @notice Send a signal (message) by setting the storage slot to a value - /// of 1. + /// @notice Send a signal (message) by setting the storage slot to a value of 1. /// @param signal The signal (message) to send. - /// @return storageSlot The location in storage where this signal is stored. - function sendSignal(bytes32 signal) external returns (bytes32 storageSlot); + /// @return slot The location in storage where this signal is stored. + function sendSignal(bytes32 signal) external returns (bytes32 slot); - /// @notice Verifies if a particular signal has already been sent. - /// @param app The address that initiated the signal. - /// @param signal The signal (message) that was sent. - /// @return True if the signal has been sent, otherwise false. - function isSignalSent(address app, bytes32 signal) external view returns (bool); + /// @notice Relay a data from a remote chain locally as a signal. The signal is calculated + /// uniquely from chainId, kind, and data. + /// @param chainId The remote chainId. + /// @param kind A value to mark the data type. + /// @param data The remote data. + /// @return slot The location in storage where this signal is stored. + function relayChainData( + uint64 chainId, + bytes32 kind, + bytes32 data + ) + external + returns (bytes32 slot); /// @notice Verifies if a signal has been received on the target chain. - /// @param srcChainId The identifier for the source chain from which the + /// @param chainId The identifier for the source chain from which the /// signal originated. /// @param app The address that initiated the signal. /// @param signal The signal (message) to send. /// @param proof Merkle proof that the signal was persisted on the /// source chain. - /// @return True if the signal has been received, otherwise false. function proveSignalReceived( - uint64 srcChainId, + uint64 chainId, address app, bytes32 signal, bytes calldata proof + ) + external; + + /// @notice Checks if a chain data has been relayed. + /// uniquely from chainId, kind, and data. + /// @param chainId The remote chainId. + /// @param kind A value to mark the data type. + /// @param data The remote data. + /// @return True if the data has been relayed, otherwise false. + function isChainDataRelayed( + uint64 chainId, + bytes32 kind, + bytes32 data ) external view returns (bool); + + /// @notice Verifies if a particular signal has already been sent. + /// @param app The address that initiated the signal. + /// @param signal The signal (message) that was sent. + /// @return True if the signal has been sent, otherwise false. + function isSignalSent(address app, bytes32 signal) external view returns (bool); } diff --git a/packages/protocol/contracts/signal/LibSignals.sol b/packages/protocol/contracts/signal/LibSignals.sol new file mode 100644 index 0000000000..5cf4ca6904 --- /dev/null +++ b/packages/protocol/contracts/signal/LibSignals.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// _____ _ _ _ _ +// |_ _|_ _(_) |_____ | | __ _| |__ ___ +// | |/ _` | | / / _ \ | |__/ _` | '_ (_-< +// |_|\__,_|_|_\_\___/ |____\__,_|_.__/__/ +// +// Email: security@taiko.xyz +// Website: https://taiko.xyz +// GitHub: https://github.com/taikoxyz +// Discord: https://discord.gg/taikoxyz +// Twitter: https://twitter.com/taikoxyz +// Blog: https://mirror.xyz/labs.taiko.eth +// Youtube: https://www.youtube.com/@taikoxyz + +pragma solidity 0.8.24; + +/// @title LibSignals +library LibSignals { + bytes32 public constant STATE_ROOT = keccak256("STATE_ROOT"); + bytes32 public constant SIGNAL_ROOT = keccak256("SIGNAL_ROOT"); +} diff --git a/packages/protocol/contracts/signal/SignalService.sol b/packages/protocol/contracts/signal/SignalService.sol index 808bc94b5d..a6685dd7f9 100644 --- a/packages/protocol/contracts/signal/SignalService.sol +++ b/packages/protocol/contracts/signal/SignalService.sol @@ -16,53 +16,42 @@ pragma solidity 0.8.24; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "../common/EssentialContract.sol"; -import "../common/ICrossChainSync.sol"; import "../libs/LibTrieProof.sol"; -import "../thirdparty/optimism/trie/SecureMerkleTrie.sol"; -import "../thirdparty/optimism/rlp/RLPReader.sol"; -import "./IHopRelayRegistry.sol"; import "./ISignalService.sol"; +import "./LibSignals.sol"; /// @title SignalService /// @dev Labeled in AddressResolver as "signal_service" /// @notice See the documentation in {ISignalService} for more details. -/// -/// @dev Authorization Guide for Multi-Hop Bridging: -/// For facilitating multi-hop bridging, authorize all deployed TaikoL1 and -/// TaikoL2 contracts involved in the bridging path. -/// Use the respective chain IDs as labels for authorization. -/// Note: SignalService should not authorize Bridges or other Bridgable -/// applications. contract SignalService is EssentialContract, ISignalService { - using SafeCast for uint256; - - // merkleProof represents ABI-encoded tuple of (key, value, and proof) - // returned from the eth_getProof() API. - struct Hop { - uint64 chainId; - address relay; - bytes32 stateRoot; - bytes merkleProof; + enum CacheOption { + CACHE_NOTHING, + CACHE_SIGNAL_ROOT, + CACHE_STATE_ROOT, + CACHE_BOTH } - struct Proof { - uint64 height; - bytes merkleProof; - // Ensure that hops are ordered such that those closer to the signal's source chain come - // before others. - Hop[] hops; + struct HopProof { + uint64 chainId; + CacheOption cacheOption; + bytes32 rootHash; + bytes[] accountProof; + bytes[] storageProof; } uint256[50] private __gap; - error SS_INVALID_PARAMS(); - error SS_INVALID_PROOF(); + event SnippetRelayed( + uint64 indexed chainid, bytes32 indexed kind, bytes32 data, bytes32 signal + ); + + error SS_EMPTY_PROOF(); error SS_INVALID_APP(); - error SS_INVALID_HOP_PROOF(); - error SS_INVALID_RELAY(); + error SS_INVALID_LAST_HOP_CHAINID(); + error SS_INVALID_MID_HOP_CHAINID(); + error SS_INVALID_PARAMS(); error SS_INVALID_SIGNAL(); - error SS_INVALID_STATE_ROOT(); - error SS_MULTIHOP_DISABLED(); + error SS_LOCAL_CHAIN_DATA_NOT_FOUND(); error SS_UNSUPPORTED(); /// @dev Initializer to be called after being deployed behind a proxy. @@ -71,113 +60,96 @@ contract SignalService is EssentialContract, ISignalService { } /// @inheritdoc ISignalService - function sendSignal(bytes32 signal) public returns (bytes32 slot) { - if (signal == 0) revert SS_INVALID_SIGNAL(); - slot = getSignalSlot(uint64(block.chainid), msg.sender, signal); - assembly { - sstore(slot, 1) - } + function relayChainData( + uint64 chainId, + bytes32 kind, + bytes32 data + ) + external + onlyFromNamed("taiko") + returns (bytes32 slot) + { + return _relayChainData(chainId, kind, data); } /// @inheritdoc ISignalService - function isSignalSent(address app, bytes32 signal) public view returns (bool) { - if (signal == 0) revert SS_INVALID_SIGNAL(); - if (app == address(0)) revert SS_INVALID_APP(); - bytes32 slot = getSignalSlot(uint64(block.chainid), app, signal); - uint256 value; - assembly { - value := sload(slot) - } - return value == 1; + function sendSignal(bytes32 signal) public returns (bytes32 slot) { + return _sendSignal(msg.sender, signal); } /// @inheritdoc ISignalService /// @dev This function may revert. function proveSignalReceived( - uint64 srcChainId, + uint64 chainId, address app, bytes32 signal, bytes calldata proof ) public - view virtual - returns (bool) { - if (app == address(0) || signal == 0 || srcChainId == 0 || srcChainId == block.chainid) { - revert SS_INVALID_PARAMS(); - } + if (app == address(0) || signal == 0) revert SS_INVALID_PARAMS(); - Proof memory p = abi.decode(proof, (Proof)); - if (!isMultiHopEnabled() && p.hops.length > 0) { - revert SS_MULTIHOP_DISABLED(); - } + HopProof[] memory _hopProofs = abi.decode(proof, (HopProof[])); + if (_hopProofs.length == 0) revert SS_EMPTY_PROOF(); - uint64 _srcChainId = srcChainId; - address _srcApp = app; - bytes32 _srcSignal = signal; + uint64 _chainId = chainId; + address _app = app; + bytes32 _signal = signal; + address _signalService = resolve(_chainId, "signal_service", false); - // Verify hop proofs - IHopRelayRegistry hrr; - if (p.hops.length > 0) { - hrr = IHopRelayRegistry(resolve("hop_relay_registry", false)); - } + for (uint256 i; i < _hopProofs.length; ++i) { + HopProof memory hop = _hopProofs[i]; + + bytes32 signalRoot = _verifyHopProof(_chainId, _app, _signal, hop, _signalService); - ICrossChainSync ccs = ICrossChainSync(resolve("taiko", false)); - bytes32 stateRoot = ccs.getSyncedSnippet(p.height).stateRoot; - - // If a signal is sent from chainA -> chainB -> chainC (this chain), we verify the proofs in - // the following order: - // 1. using chainC's latest parent's stateRoot to verify that chainB's TaikoL1/TaikoL2 - // contract has sent a given hop stateRoot on chainB using its own signal service. - // 2. using the verified hop stateRoot to verify that the source app on chainA has sent a - // signal using its own signal service. - // We always verify the proofs in the reversed order (top to bottom). - for (uint256 i; i < p.hops.length; ++i) { - Hop memory hop = p.hops[i]; - if (hop.stateRoot == stateRoot) revert SS_INVALID_HOP_PROOF(); - - if (!hrr.isRelayRegistered(_srcChainId, hop.chainId, hop.relay)) { - revert SS_INVALID_RELAY(); + bool isLastHop = i == _hopProofs.length - 1; + if (isLastHop) { + if (hop.chainId != block.chainid) revert SS_INVALID_LAST_HOP_CHAINID(); + _signalService = address(this); + } else { + if (hop.chainId == 0 || hop.chainId == block.chainid) { + revert SS_INVALID_MID_HOP_CHAINID(); + } + _signalService = resolve(hop.chainId, "signal_service", false); } - verifyMerkleProof(hop.stateRoot, _srcChainId, _srcApp, _srcSignal, hop.merkleProof); + bool isFullProof = hop.accountProof.length > 0; - _srcChainId = hop.chainId; - _srcApp = hop.relay; - _srcSignal = hop.stateRoot; + _cacheChainData(hop, _chainId, signalRoot, isFullProof, isLastHop); + + bytes32 kind = isFullProof ? LibSignals.STATE_ROOT : LibSignals.SIGNAL_ROOT; + _signal = signalForChainData(_chainId, kind, hop.rootHash); + _chainId = hop.chainId; + _app = _signalService; } - verifyMerkleProof(stateRoot, _srcChainId, _srcApp, _srcSignal, p.merkleProof); - return true; + if (!isSignalSent(address(this), _signal)) revert SS_LOCAL_CHAIN_DATA_NOT_FOUND(); } - function verifyMerkleProof( - bytes32 stateRoot, - uint64 srcChainId, - address srcApp, - bytes32 srcSignal, - bytes memory merkleProof + /// @inheritdoc ISignalService + function isChainDataRelayed( + uint64 chainId, + bytes32 kind, + bytes32 data ) public view - virtual + returns (bool) { - if (stateRoot == 0) revert SS_INVALID_STATE_ROOT(); - if (merkleProof.length == 0) revert SS_INVALID_PROOF(); - - address signalService = resolve(srcChainId, "signal_service", false); - - bytes32 slot = getSignalSlot(srcChainId, srcApp, srcSignal); - - // verifyFullMerkleProof() will revert in case if something is not valid - LibTrieProof.verifyFullMerkleProof(stateRoot, signalService, slot, hex"01", merkleProof); + return isSignalSent(address(this), signalForChainData(chainId, kind, data)); } - /// @notice Checks if multi-hop is enabled. - /// @return Returns true if multi-hop bridging is enabled. - function isMultiHopEnabled() public view virtual returns (bool) { - return false; + /// @inheritdoc ISignalService + function isSignalSent(address app, bytes32 signal) public view returns (bool) { + if (signal == 0) revert SS_INVALID_SIGNAL(); + if (app == address(0)) revert SS_INVALID_APP(); + bytes32 slot = getSignalSlot(uint64(block.chainid), app, signal); + uint256 value; + assembly { + value := sload(slot) + } + return value == 1; } /// @notice Get the storage slot of the signal. @@ -198,7 +170,87 @@ contract SignalService is EssentialContract, ISignalService { return keccak256(abi.encodePacked("SIGNAL", chainId, app, signal)); } + function signalForChainData( + uint64 chainId, + bytes32 kind, + bytes32 data + ) + public + pure + returns (bytes32) + { + return keccak256(abi.encode(chainId, kind, data)); + } + + function _relayChainData( + uint64 chainId, + bytes32 kind, + bytes32 data + ) + internal + returns (bytes32 slot) + { + bytes32 signal = signalForChainData(chainId, kind, data); + emit SnippetRelayed(chainId, kind, data, signal); + return _sendSignal(address(this), signal); + } + + function _sendSignal(address sender, bytes32 signal) internal returns (bytes32 slot) { + if (signal == 0) revert SS_INVALID_SIGNAL(); + slot = getSignalSlot(uint64(block.chainid), sender, signal); + assembly { + sstore(slot, 1) + } + } + + function _verifyHopProof( + uint64 chainId, + address app, + bytes32 signal, + HopProof memory hop, + address relay + ) + internal + virtual + returns (bytes32 signalRoot) + { + return LibTrieProof.verifyMerkleProof( + hop.rootHash, + relay, + getSignalSlot(chainId, app, signal), + hex"01", + hop.accountProof, + hop.storageProof + ); + } + function _authorizePause(address) internal pure override { revert SS_UNSUPPORTED(); } + + function _cacheChainData( + HopProof memory hop, + uint64 chainId, + bytes32 signalRoot, + bool isFullProof, + bool isLastHop + ) + private + { + // cache state root + bool cacheStateRoot = hop.cacheOption == CacheOption.CACHE_BOTH + || hop.cacheOption == CacheOption.CACHE_STATE_ROOT; + + if (cacheStateRoot && isFullProof && !isLastHop) { + _relayChainData(chainId, LibSignals.STATE_ROOT, hop.rootHash); + } + + // cache signal root + bool cacheSignalRoot = hop.cacheOption == CacheOption.CACHE_BOTH + || hop.cacheOption == CacheOption.CACHE_SIGNAL_ROOT; + + if (cacheSignalRoot && (!isLastHop || isFullProof)) { + _relayChainData(chainId, LibSignals.SIGNAL_ROOT, signalRoot); + } + } } diff --git a/packages/protocol/docs/multihop/L1_to_L2.png b/packages/protocol/docs/multihop/L1_to_L2.png deleted file mode 100644 index cacc2042a2..0000000000 Binary files a/packages/protocol/docs/multihop/L1_to_L2.png and /dev/null differ diff --git a/packages/protocol/docs/multihop/L2A_to_L3.png b/packages/protocol/docs/multihop/L2A_to_L3.png deleted file mode 100644 index 53fe72df69..0000000000 Binary files a/packages/protocol/docs/multihop/L2A_to_L3.png and /dev/null differ diff --git a/packages/protocol/docs/multihop/L2_to_L1.png b/packages/protocol/docs/multihop/L2_to_L1.png deleted file mode 100644 index 50e07b96cc..0000000000 Binary files a/packages/protocol/docs/multihop/L2_to_L1.png and /dev/null differ diff --git a/packages/protocol/docs/multihop/L2_to_L2.png b/packages/protocol/docs/multihop/L2_to_L2.png deleted file mode 100644 index b77cd790bf..0000000000 Binary files a/packages/protocol/docs/multihop/L2_to_L2.png and /dev/null differ diff --git a/packages/protocol/docs/multihop/bridge_1hop.png b/packages/protocol/docs/multihop/bridge_1hop.png new file mode 100644 index 0000000000..473d12e85e Binary files /dev/null and b/packages/protocol/docs/multihop/bridge_1hop.png differ diff --git a/packages/protocol/docs/multihop/bridge_2hop.png b/packages/protocol/docs/multihop/bridge_2hop.png new file mode 100644 index 0000000000..c904229d1d Binary files /dev/null and b/packages/protocol/docs/multihop/bridge_2hop.png differ diff --git a/packages/protocol/docs/multihop/cache_1.png b/packages/protocol/docs/multihop/cache_1.png new file mode 100644 index 0000000000..ef4890cffc Binary files /dev/null and b/packages/protocol/docs/multihop/cache_1.png differ diff --git a/packages/protocol/docs/multihop/cache_1_done.png b/packages/protocol/docs/multihop/cache_1_done.png new file mode 100644 index 0000000000..2b977b9b9a Binary files /dev/null and b/packages/protocol/docs/multihop/cache_1_done.png differ diff --git a/packages/protocol/docs/multihop/cache_1_use_1.png b/packages/protocol/docs/multihop/cache_1_use_1.png new file mode 100644 index 0000000000..01686def48 Binary files /dev/null and b/packages/protocol/docs/multihop/cache_1_use_1.png differ diff --git a/packages/protocol/docs/multihop/cache_1_use_2.png b/packages/protocol/docs/multihop/cache_1_use_2.png new file mode 100644 index 0000000000..50cb942135 Binary files /dev/null and b/packages/protocol/docs/multihop/cache_1_use_2.png differ diff --git a/packages/protocol/docs/multihop/l1_l2_sync.png b/packages/protocol/docs/multihop/l1_l2_sync.png new file mode 100644 index 0000000000..980411653e Binary files /dev/null and b/packages/protocol/docs/multihop/l1_l2_sync.png differ diff --git a/packages/protocol/docs/multihop/merkle_proof.png b/packages/protocol/docs/multihop/merkle_proof.png new file mode 100644 index 0000000000..ab1637891f Binary files /dev/null and b/packages/protocol/docs/multihop/merkle_proof.png differ diff --git a/packages/protocol/docs/multihop/state.png b/packages/protocol/docs/multihop/state.png new file mode 100644 index 0000000000..b7e7f694d2 Binary files /dev/null and b/packages/protocol/docs/multihop/state.png differ diff --git a/packages/protocol/docs/multihop/three_chains.png b/packages/protocol/docs/multihop/three_chains.png new file mode 100644 index 0000000000..c2aa5a6ec8 Binary files /dev/null and b/packages/protocol/docs/multihop/three_chains.png differ diff --git a/packages/protocol/docs/multihop_bridging_deployment.md b/packages/protocol/docs/multihop_bridging_deployment.md index 71f2014c69..faa86413c4 100644 --- a/packages/protocol/docs/multihop_bridging_deployment.md +++ b/packages/protocol/docs/multihop_bridging_deployment.md @@ -1,99 +1,90 @@ -# Deployment for Multi-Hop Briding +# Multi-hop cross-chain bridging -We expect that bridging acorss multiple layers are supported natively by Taiko. I'd like to explain how this is done. +This document explains how multi-hop cross-chain bridging works in Taiko. -First of all, we need to ensures some contracts are shared by multiple Taiko deployments. For example, if we deploy two layer 2s, L2A and L2B, if we would like users to deposit Ether to L2A, then bridge Ether from L2A directly to L2B, then withdraw the Ether on L1, then the contract that holds Ether must be shared by L2A and L2B. +## L1<->L2 data synchronization +We'll use this diagram to illustrate a blockchain's state. The large triangle represents the world state, while the smaller triangle represents the storage tree of a special contract named the "Signal Service," deployed on both L1 and L2. -## Shared contracts +![State Diagram](./multihop/state.png) -On L2 or any layer, then following contracts shall be deployed as sigletons shared by multiple TaikoL1 deployments. +When a signal is sent by the Signal Service, a unique slot in its storage is updated with a value of `1`, as shown in the Solidity code below: -- SignalService -- Bridge -- and all token vaults e.g., ERC20Vault -- An AddressManager used by the above contracts. - -There are some inter-dependency among these shared contracts. Specificly - -- Bridge.sol depends on SignalService; -- Token vaults depend on Bridge.sol; - -These 1-to-1 dependency relations are acheived by AddressResolver with a name-based address resolution (lookup). - -### SignalService +```solidity +function _sendSignal(address sender, bytes32 signal) internal returns (bytes32 slot) { + if (signal == 0) revert SS_INVALID_SIGNAL(); + slot = getSignalSlot(uint64(block.chainid), sender, signal); + assembly { + sstore(slot, 1) + } +} + +function getSignalSlot(uint64 chainId, address app, bytes32 signal) public pure returns (bytes32) { + return keccak256(abi.encodePacked("SIGNAL", chainId, app, signal)); +} +``` -SignalService also uses AuthorizableContract to authorize multiple TaikoL1 and TaikoL2 contracts deployed **on each chain** that is part of the path of multi-hop bridging. +Merkle proofs can verify signals sent by specific senders when the signal service's state root is known on another chain. A full merkle proof comprises an *account proof* and a *storage proof*. However, if the signal service's storage root (or the *signal root*) is known on another chain, only a storage proof is necessary to verify the signal's source. -For each TaikoL1/TaikoL2 contracts, we need to perform the following: +![Merkle Proof](./multihop/merkle_proof.png) -```solidity -// 1 is Ethereum's chainID -SignalService(sharedSignalServiceAddr).authorize(address(TaikoL1A), 1); -SignalService(sharedSignalServiceAddr).authorize(address(TaikoL1B), 1); +Taiko's core protocol code (TaikoL1.sol and TaikoL2.sol) automatically synchronizes or relays the state roots between L1 and L2. -// 10001 is the L2A's chainId -SignalService(sharedSignalServiceAddr).authorize(address(TaikoL2A), 10001); +When chainA's state root is relayed to chainB, a special signal is sent in chainB's signal service. This signal is calculated incorporating chainA's block ID. These special signals are always sent by the target chain's signal service. -// 10002 is the L2B's chainId -SignalService(sharedSignalServiceAddr).authorize(address(TaikoL2B), 10002); -... -``` +![L1-L2 Sync](./multihop/l1_l2_sync.png) -The label **must be** the id of the chain where the smart contract has been deployed to. +If you deploy more chains using Taiko protocol, you can create a chain of relayed state roots between them. -To guarantee this design works, each pre-deployed contract must have a unique address on L2 and L3 chains, incorporating the chain ID into the address (as a prefix). +![Three Chains](./multihop/three_chains.png) -### Bridge +## Verifying bridged messages -Bridge depends on a local SignalService .Therefore, we need to registered the service as: +### One-hop bridging +Consider the 1-hop example below. -```solidity -addManager.setAddress(block.chainId, "signal_service", localSignalService); -``` +To verify that "some app" has sent a custom message, we verify if the corresponding signal (associated with the message sender, "some app") has been set by the signal service (0x1000A) on L1. After L1's state root is relayed to L2, we need the following info on L2 to verify the message on L1: -Bridge also need to know each and every conterparty bridge deployed **on each chain** that is part of the multi-hop bridging. +1. Message's signal and its sender, to compute the storage slot now supposed to be 1. +2. A full merkle proof generated by an L1 node for the above slot. +3. L1 signal service's address associated with the merkle proof. +4. L2 signal service's address to verify that L1's state root has been relayed to L2 already. -```solidity -addManager.setAddress(remoteChainId1, "bridge", remoteBridge1); -addManager.setAddress(remoteChainId2, "bridge", remoteBridge2); -... -``` +![1-Hop Bridging](./multihop/bridge_1hop.png) -### ERC20Vault +### Multi-hop bridging +In the 2-hop example below, two merkle proofs are needed, and the signal service addresses for L1 and L2 need verification. L3's signal service address does not need verification as the bridging verification occurs in L3's signal service contract, with L3's signal service address being `address(this)`. -ERC20Vault (and other token vaults) depends on a local Bridge, you must have: +![2-Hop Bridging](./multihop/bridge_2hop.png) -```solidity -addressManager.setAddress(block.chainId, "bridge", localBridge) -``` +## Caching -Similiar with Bridge, ERC20Vault also needs to know their conterpart vaults **on each chain** that is part of the path of multi-hop bridging. Therefore, we must perform: +Caching is optional and is activated per hop when the transaction intends to reuse some state root or signal root for future bridging verification. -```solidity -addressManager.setAddress(remoteChainId1, "erc20_vault", remoteERC20Vault1); -addressManager.setAddress(remoteChainId2, "erc20_vault", remoteERC20Vault2); -... -``` +In the diagram below with 2 hops, L1's state root and L2's signal root can be cached to L3 if specified. -### Dedicated AddressManager +![Cache Example 1](./multihop/cache_1.png) -A dedicated AddressManager should be deployed on each chain to support only these shared contracts. This AddressManager shall not be used by the TaikoL1 deployments. +If both are cached, two more signals will be sent in L3's signal service. -## Bridging +![Cache Example 1 Done](./multihop/cache_1_done.png) -### L1 to L2 +Depending on the type of data (state root or signal root), the signal is generated differently. -![L1_to_L2](./multihop/L1_to_L2.png "L1 to L2") +```solidity +function signalForChainData(uint64 chainId, bytes32 kind, bytes32 data) public pure returns (bytes32) { + return keccak256(abi.encode(chainId, kind, data)); +} +``` -### L2 to L1 +Once cached on L3, one full merkle proof is sufficient to verify everything that happened on L1 before or when L1's state root becomes 0x1111. This allows skipping the middle-hop. -![L2_to_L1](./multihop/L2_to_L1.png "L2 to L1") +![Cache Use 1](./multihop/cache_1_use_1.png) -### L2 to L2 +If L1's state root is not cached on L3 but only L2's signal root is, then one full merkle proof for L1 and a storage proof for L2 are used to verify a bridged message. -![L2_to_L2](./multihop/L2_to_L2.png "L2 to L2") +![Cache Use 2](./multihop/cache_1_use_2.png) -### L2 to L3 on another L2 +Note that the last hop (L2)'s state root has already been auto-relayed to L3, so it cannot be recached. Therefore, only the last hop's signal root can be cached. -![L2A_to_L3](./multihop/L2A_to_L3.png "L2A to L3") +For all other non-last hops, if a full proof is used, the state root can be cached; if a storage proof is used, the signal root can be cached. But by default, caching is all disabled. \ No newline at end of file diff --git a/packages/protocol/foundry.toml b/packages/protocol/foundry.toml index c32dc2be27..0ccad3d340 100644 --- a/packages/protocol/foundry.toml +++ b/packages/protocol/foundry.toml @@ -25,7 +25,10 @@ fs_permissions = [ { access = "read", path = "./genesis" }, ] -fuzz = { runs = 256 } +# 2394: transient storage warning +ignored_error_codes = [2394] + +fuzz = { runs = 200 } # Workaround as a fixed fuzz seed. # Bug is confirmed, will be fixed soon: diff --git a/packages/protocol/genesis/GenerateGenesis.g.sol b/packages/protocol/genesis/GenerateGenesis.g.sol index 596694eb9b..a541054677 100644 --- a/packages/protocol/genesis/GenerateGenesis.g.sol +++ b/packages/protocol/genesis/GenerateGenesis.g.sol @@ -266,8 +266,7 @@ contract TestGenerateGenesis is Test, AddressResolver { addressManager.setAddress(1, "erc1155_vault", erc1155VaultProxyAddress); vm.stopPrank(); - address erc1155VaultAddress = getPredeployedContractAddress("ERC1155VaultImpl"); - + // address erc1155VaultAddress = getPredeployedContractAddress("ERC1155VaultImpl"); vm.startPrank(erc1155VaultProxy.owner()); @@ -286,8 +285,8 @@ contract TestGenerateGenesis is Test, AddressResolver { vm.startPrank(ownerSecurityCouncil); - SignalService signalService = - SignalService(payable(getPredeployedContractAddress("SignalServiceImpl"))); + // SignalService signalService = + // SignalService(payable(getPredeployedContractAddress("SignalServiceImpl"))); signalServiceProxy.upgradeTo(address(new SignalService())); @@ -301,7 +300,7 @@ contract TestGenerateGenesis is Test, AddressResolver { assertEq(regularERC20.symbol(), "RGL"); } - function getPredeployedContractAddress(string memory contractName) private returns (address) { + function getPredeployedContractAddress(string memory contractName) private view returns (address) { return configJSON.readAddress(string.concat(".contractAddresses.", contractName)); } @@ -330,7 +329,7 @@ contract TestGenerateGenesis is Test, AddressResolver { private { vm.startPrank(owner); - address contractAddress = getPredeployedContractAddress(contractName); + // address contractAddress = getPredeployedContractAddress(contractName); address proxyAddress = getPredeployedContractAddress(proxyName); OwnerUUPSUpgradable proxy = OwnerUUPSUpgradable(payable(proxyAddress)); diff --git a/packages/protocol/script/DeployERC20Airdrop.s.sol b/packages/protocol/script/DeployERC20Airdrop.s.sol index 99d5552d73..ef69e4bcdc 100644 --- a/packages/protocol/script/DeployERC20Airdrop.s.sol +++ b/packages/protocol/script/DeployERC20Airdrop.s.sol @@ -15,8 +15,6 @@ pragma solidity 0.8.24; import "../test/DeployCapability.sol"; -import "forge-std/src/console2.sol"; - import "../contracts/team/airdrop/ERC20Airdrop.sol"; // @KorbinianK , @2manslkh diff --git a/packages/protocol/script/DeployOnL1.s.sol b/packages/protocol/script/DeployOnL1.s.sol index 38d78a1f9b..748cc181fc 100644 --- a/packages/protocol/script/DeployOnL1.s.sol +++ b/packages/protocol/script/DeployOnL1.s.sol @@ -116,6 +116,7 @@ contract DeployOnL1 is DeployCapability { copyRegister(rollupAddressManager, sharedAddressManager, "taiko_token"); copyRegister(rollupAddressManager, sharedAddressManager, "signal_service"); copyRegister(rollupAddressManager, sharedAddressManager, "bridge"); + copyRegister(sharedAddressManager, rollupAddressManager, "taiko"); address proposer = vm.envAddress("PROPOSER"); if (proposer != address(0)) { diff --git a/packages/protocol/test/HelperContracts.sol b/packages/protocol/test/HelperContracts.sol index f3ad180196..0e16cf1ae4 100644 --- a/packages/protocol/test/HelperContracts.sol +++ b/packages/protocol/test/HelperContracts.sol @@ -46,21 +46,19 @@ contract SkipProofCheckSignal is SignalService { public pure override - returns (bool) - { - return true; - } + { } } contract DummyCrossChainSync is EssentialContract, ICrossChainSync { Snippet private _snippet; - function setSyncedData(bytes32 blockHash, bytes32 stateRoot) external { + function setSnippet(uint64 blockId, bytes32 blockHash, bytes32 stateRoot) external { + _snippet.blockId = blockId; _snippet.blockHash = blockHash; _snippet.stateRoot = stateRoot; } - function getSyncedSnippet(uint64) external view returns (Snippet memory) { + function getSyncedSnippet(uint64 /*blockId*/ ) public view returns (Snippet memory) { return _snippet; } } diff --git a/packages/protocol/test/L2/TaikoL2.t.sol b/packages/protocol/test/L2/TaikoL2.t.sol index acaf017baa..8f8dd5a9d2 100644 --- a/packages/protocol/test/L2/TaikoL2.t.sol +++ b/packages/protocol/test/L2/TaikoL2.t.sol @@ -44,9 +44,11 @@ contract TestTaikoL2 is TaikoTest { L2 = TaikoL2EIP1559Configurable( payable( deployProxy({ - name: "taiko_l2", + name: "taiko", impl: address(new TaikoL2EIP1559Configurable()), - data: abi.encodeCall(TaikoL2.init, (addressManager, l1ChainId, gasExcess)) + data: abi.encodeCall(TaikoL2.init, (addressManager, l1ChainId, gasExcess)), + registerTo: addressManager, + owner: address(0) }) ) ); @@ -54,17 +56,6 @@ contract TestTaikoL2 is TaikoTest { L2.setConfigAndExcess(TaikoL2.Config(gasTarget, quotient), gasExcess); gasExcess = 195_420_300_100; - L2skip = SkipBasefeeCheckL2( - payable( - deployProxy({ - name: "taiko_l2", - impl: address(new SkipBasefeeCheckL2()), - data: abi.encodeCall(TaikoL2.init, (addressManager, l1ChainId, gasExcess)) - }) - ) - ); - - L2skip.setConfigAndExcess(TaikoL2.Config(gasTarget, quotient), gasExcess); vm.roll(block.number + 1); vm.warp(block.timestamp + 30); @@ -107,106 +98,6 @@ contract TestTaikoL2 is TaikoTest { } } - function test_simulation_lower_traffic() external { - console2.log("LOW TRAFFIC STARTS"); // For parser - _simulation(100_000, 10_000_000, 1, 8); - console2.log("LOW TRAFFIC ENDS"); - } - - function test_simulation_higher_traffic() external { - console2.log("HIGH TRAFFIC STARTS"); // For parser - _simulation(100_000, 120_000_000, 1, 8); - console2.log("HIGH TRAFFIC ENDS"); - } - - function test_simulation_target_traffic() external { - console2.log("TARGET TRAFFIC STARTS"); // For parser - _simulation(60_000_000, 0, 12, 0); - console2.log("TARGET TRAFFIC ENDS"); - } - - function _simulation( - uint256 minGas, - uint256 maxDiffToMinGas, - uint8 quickest, - uint8 maxDiffToQuickest - ) - internal - { - // We need to randomize the: - // - parent gas used (We should sometimes exceed 150.000.000 gas / 12 - // seconds (to simulate congestion a bit) !!) - // - the time we fire away an L2 block (anchor transaction). - // The rest is baked in. - // initial gas excess issued: 49954623777 (from eip1559_util.py) if we - // want to stick to the params of 10x Ethereum gas, etc. - - // This variables counts if we reached the 12seconds (L1) height, if so - // then resets the accumulated parent gas used and increments the L1 - // height number - uint8 accumulated_seconds = 0; - uint256 accumulated_parent_gas_per_l1_block = 0; - uint64 l1Height = uint64(block.number); - uint64 l1BlockCounter = 0; - uint64 maxL2BlockCount = 180; - uint256 allBaseFee = 0; - uint256 allGasUsed = 0; - uint256 newRandomWithoutSalt; - // Simulate 200 L2 blocks - for (uint256 i; i < maxL2BlockCount; ++i) { - newRandomWithoutSalt = uint256( - keccak256( - abi.encodePacked( - block.prevrandao, msg.sender, block.timestamp, i, newRandomWithoutSalt, salt - ) - ) - ); - - uint32 currentGasUsed; - if (maxDiffToMinGas == 0) { - currentGasUsed = uint32(minGas); - } else { - currentGasUsed = - uint32(pickRandomNumber(newRandomWithoutSalt, minGas, maxDiffToMinGas)); - } - salt = uint256(keccak256(abi.encodePacked(currentGasUsed, salt))); - accumulated_parent_gas_per_l1_block += currentGasUsed; - allGasUsed += currentGasUsed; - - uint8 currentTimeAhead; - if (maxDiffToQuickest == 0) { - currentTimeAhead = uint8(quickest); - } else { - currentTimeAhead = - uint8(pickRandomNumber(newRandomWithoutSalt, quickest, maxDiffToQuickest)); - } - accumulated_seconds += currentTimeAhead; - - if (accumulated_seconds >= 12) { - console2.log( - "Gas used per L1 block:", l1Height, ":", accumulated_parent_gas_per_l1_block - ); - l1Height++; - l1BlockCounter++; - accumulated_parent_gas_per_l1_block = 0; - accumulated_seconds = 0; - } - - vm.prank(L2.GOLDEN_TOUCH_ADDRESS()); - _anchorSimulation(currentGasUsed, l1Height); - uint256 currentBaseFee = L2skip.getBasefee(l1Height, currentGasUsed); - allBaseFee += currentBaseFee; - console2.log("Actual gas in L2 block is:", currentGasUsed); - console2.log("L2block to baseFee is:", i, ":", currentBaseFee); - vm.roll(block.number + 1); - - vm.warp(block.timestamp + currentTimeAhead); - } - - console2.log("Average wei gas price per L2 block is:", (allBaseFee / maxL2BlockCount)); - console2.log("Average gasUsed per L1 block:", (allGasUsed / l1BlockCounter)); - } - // calling anchor in the same block more than once should fail function test_L2_AnchorTx_revert_in_same_block() external { vm.fee(1); @@ -247,24 +138,4 @@ contract TestTaikoL2 is TaikoTest { bytes32 l1StateRoot = randBytes32(); L2.anchor(l1Hash, l1StateRoot, 12_345, parentGasLimit); } - - function _anchorSimulation(uint32 parentGasLimit, uint64 l1Height) private { - bytes32 l1Hash = randBytes32(); - bytes32 l1StateRoot = randBytes32(); - L2skip.anchor(l1Hash, l1StateRoot, l1Height, parentGasLimit); - } - - // Semi-random number generator - function pickRandomNumber( - uint256 randomNum, - uint256 lowerLimit, - uint256 diffBtwLowerAndUpperLimit - ) - internal - view - returns (uint256) - { - randomNum = uint256(keccak256(abi.encodePacked(randomNum, salt))); - return (lowerLimit + (randomNum % diffBtwLowerAndUpperLimit)); - } } diff --git a/packages/protocol/test/L2/TaikoL2NoFeeCheck.t.sol b/packages/protocol/test/L2/TaikoL2NoFeeCheck.t.sol new file mode 100644 index 0000000000..062b34afe7 --- /dev/null +++ b/packages/protocol/test/L2/TaikoL2NoFeeCheck.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../TaikoTest.sol"; + +contract SkipBasefeeCheckL2 is TaikoL2EIP1559Configurable { + function skipFeeCheck() public pure override returns (bool) { + return true; + } +} + +contract TestTaikoL2NoFeeCheck is TaikoTest { + using SafeCast for uint256; + + // Initial salt for semi-random generation + uint256 salt = 2_195_684_615_435_261_315_311; + // same as `block_gas_limit` in foundry.toml + uint32 public constant BLOCK_GAS_LIMIT = 30_000_000; + + address public addressManager; + SkipBasefeeCheckL2 public L2; + + function setUp() public { + addressManager = deployProxy({ + name: "address_manager", + impl: address(new AddressManager()), + data: abi.encodeCall(AddressManager.init, ()) + }); + + deployProxy({ + name: "signal_service", + impl: address(new SignalService()), + data: abi.encodeCall(SignalService.init, (addressManager)), + registerTo: addressManager, + owner: address(0) + }); + + uint64 gasExcess = 0; + uint8 quotient = 8; + uint32 gasTarget = 60_000_000; + uint64 l1ChainId = 12_345; + + gasExcess = 195_420_300_100; + L2 = SkipBasefeeCheckL2( + payable( + deployProxy({ + name: "taiko", + impl: address(new SkipBasefeeCheckL2()), + data: abi.encodeCall(TaikoL2.init, (addressManager, l1ChainId, gasExcess)), + registerTo: addressManager, + owner: address(0) + }) + ) + ); + + L2.setConfigAndExcess(TaikoL2.Config(gasTarget, quotient), gasExcess); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 30); + } + + function test_L2_NoFeeCheck_simulation_lower_traffic() external { + console2.log("LOW TRAFFIC STARTS"); // For parser + _simulation(100_000, 10_000_000, 1, 8); + console2.log("LOW TRAFFIC ENDS"); + } + + function test_L2_NoFeeCheck_simulation_higher_traffic() external { + console2.log("HIGH TRAFFIC STARTS"); // For parser + _simulation(100_000, 120_000_000, 1, 8); + console2.log("HIGH TRAFFIC ENDS"); + } + + function test_L2_NoFeeCheck_simulation_target_traffic() external { + console2.log("TARGET TRAFFIC STARTS"); // For parser + _simulation(60_000_000, 0, 12, 0); + console2.log("TARGET TRAFFIC ENDS"); + } + + function _simulation( + uint256 minGas, + uint256 maxDiffToMinGas, + uint8 quickest, + uint8 maxDiffToQuickest + ) + internal + { + // We need to randomize the: + // - parent gas used (We should sometimes exceed 150.000.000 gas / 12 + // seconds (to simulate congestion a bit) !!) + // - the time we fire away an L2 block (anchor transaction). + // The rest is baked in. + // initial gas excess issued: 49954623777 (from eip1559_util.py) if we + // want to stick to the params of 10x Ethereum gas, etc. + + // This variables counts if we reached the 12seconds (L1) height, if so + // then resets the accumulated parent gas used and increments the L1 + // height number + uint8 accumulated_seconds = 0; + uint256 accumulated_parent_gas_per_l1_block = 0; + uint64 l1Height = uint64(block.number); + uint64 l1BlockCounter = 0; + uint64 maxL2BlockCount = 180; + uint256 allBaseFee = 0; + uint256 allGasUsed = 0; + uint256 newRandomWithoutSalt; + // Simulate 200 L2 blocks + for (uint256 i; i < maxL2BlockCount; ++i) { + newRandomWithoutSalt = uint256( + keccak256( + abi.encodePacked( + block.prevrandao, msg.sender, block.timestamp, i, newRandomWithoutSalt, salt + ) + ) + ); + + uint32 currentGasUsed; + if (maxDiffToMinGas == 0) { + currentGasUsed = uint32(minGas); + } else { + currentGasUsed = + uint32(pickRandomNumber(newRandomWithoutSalt, minGas, maxDiffToMinGas)); + } + salt = uint256(keccak256(abi.encodePacked(currentGasUsed, salt))); + accumulated_parent_gas_per_l1_block += currentGasUsed; + allGasUsed += currentGasUsed; + + uint8 currentTimeAhead; + if (maxDiffToQuickest == 0) { + currentTimeAhead = uint8(quickest); + } else { + currentTimeAhead = + uint8(pickRandomNumber(newRandomWithoutSalt, quickest, maxDiffToQuickest)); + } + accumulated_seconds += currentTimeAhead; + + if (accumulated_seconds >= 12) { + console2.log( + "Gas used per L1 block:", l1Height, ":", accumulated_parent_gas_per_l1_block + ); + l1Height++; + l1BlockCounter++; + accumulated_parent_gas_per_l1_block = 0; + accumulated_seconds = 0; + } + + vm.prank(L2.GOLDEN_TOUCH_ADDRESS()); + _anchorSimulation(currentGasUsed, l1Height); + uint256 currentBaseFee = L2.getBasefee(l1Height, currentGasUsed); + allBaseFee += currentBaseFee; + console2.log("Actual gas in L2 block is:", currentGasUsed); + console2.log("L2block to baseFee is:", i, ":", currentBaseFee); + vm.roll(block.number + 1); + + vm.warp(block.timestamp + currentTimeAhead); + } + + console2.log("Average wei gas price per L2 block is:", (allBaseFee / maxL2BlockCount)); + console2.log("Average gasUsed per L1 block:", (allGasUsed / l1BlockCounter)); + } + + function test_L2_NoFeeCheck_L2_AnchorTx_signing(bytes32 digest) external { + (uint8 v, uint256 r, uint256 s) = LibL2Signer.signAnchor(digest, uint8(1)); + address signer = ecrecover(digest, v + 27, bytes32(r), bytes32(s)); + assertEq(signer, L2.GOLDEN_TOUCH_ADDRESS()); + + (v, r, s) = LibL2Signer.signAnchor(digest, uint8(2)); + signer = ecrecover(digest, v + 27, bytes32(r), bytes32(s)); + assertEq(signer, L2.GOLDEN_TOUCH_ADDRESS()); + + vm.expectRevert(); + LibL2Signer.signAnchor(digest, uint8(0)); + + vm.expectRevert(); + LibL2Signer.signAnchor(digest, uint8(3)); + } + + // Semi-random number generator + function pickRandomNumber( + uint256 randomNum, + uint256 lowerLimit, + uint256 diffBtwLowerAndUpperLimit + ) + internal + view + returns (uint256) + { + randomNum = uint256(keccak256(abi.encodePacked(randomNum, salt))); + return (lowerLimit + (randomNum % diffBtwLowerAndUpperLimit)); + } + + function _anchorSimulation(uint32 parentGasLimit, uint64 l1Height) private { + bytes32 l1Hash = randBytes32(); + bytes32 l1StateRoot = randBytes32(); + L2.anchor(l1Hash, l1StateRoot, l1Height, parentGasLimit); + } +} diff --git a/packages/protocol/test/TaikoTest.sol b/packages/protocol/test/TaikoTest.sol index fd87bb1c3c..bbf9ac2a1d 100644 --- a/packages/protocol/test/TaikoTest.sol +++ b/packages/protocol/test/TaikoTest.sol @@ -4,13 +4,13 @@ pragma solidity 0.8.24; import "forge-std/src/Test.sol"; import "forge-std/src/console2.sol"; +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "../contracts/thirdparty/LibFixedPointMath.sol"; import "../contracts/bridge/Bridge.sol"; import "../contracts/signal/SignalService.sol"; -import "../contracts/signal/HopRelayRegistry.sol"; import "../contracts/tokenvault/BridgedERC20.sol"; import "../contracts/tokenvault/BridgedERC721.sol"; import "../contracts/tokenvault/BridgedERC1155.sol"; @@ -81,4 +81,12 @@ abstract contract TaikoTest is Test, DeployCapability { function randBytes32() internal returns (bytes32) { return keccak256(abi.encodePacked("bytes32", _seed++)); } + + function strToBytes32(string memory input) internal pure returns (bytes32 result) { + require(bytes(input).length <= 32, "String too long"); + // Copy the string's bytes directly into the bytes32 variable + assembly { + result := mload(add(input, 32)) + } + } } diff --git a/packages/protocol/test/bridge/Bridge.t.sol b/packages/protocol/test/bridge/Bridge.t.sol index 5a9c4895d1..5e3de09379 100644 --- a/packages/protocol/test/bridge/Bridge.t.sol +++ b/packages/protocol/test/bridge/Bridge.t.sol @@ -648,7 +648,8 @@ contract BridgeTest is TaikoTest { addressManager.setAddress(dest, "signal_service", address(mockProofSignalService)); - crossChainSync.setSyncedData( + crossChainSync.setSnippet( + 123, 0xd5f5d8ac6bc37139c97389b00e9cf53e89c153ad8a5fc765ffe9f44ea9f3d31e, 0x631b214fb030d82847224f0b3d3b906a6764dded176ad3c7262630204867ba85 ); diff --git a/packages/protocol/test/libs/LibTrieProof.t.sol b/packages/protocol/test/libs/LibTrieProof.t.sol index a497477a02..0306c9f92b 100644 --- a/packages/protocol/test/libs/LibTrieProof.t.sol +++ b/packages/protocol/test/libs/LibTrieProof.t.sol @@ -5,7 +5,7 @@ import "../TaikoTest.sol"; import "../../contracts/libs/LibTrieProof.sol"; contract TestLibTrieProof is TaikoTest { - function test_verifyFullMerkleProof() public pure { + function test_verifyMerkleProof() public { // Not needed for now, but leave it as is. //uint64 chainId = 11_155_111; // Created the proofs on a deployed Sepolia // contract, this is why this chainId. @@ -15,16 +15,15 @@ contract TestLibTrieProof is TaikoTest { // //Actually a messageHash // This one is the "sender app" aka the source bridge but i mocked it for now to be an EOA // (for slot calculation) - address contractWhichStoresValue1AtSlot = 0x17DF3c450D1dC61558ecA7B10e4bBC8ddcdB1f28; + address addr = 0x17DF3c450D1dC61558ecA7B10e4bBC8ddcdB1f28; // This is the slot i queried the eth_getProof on Sepolia for blockheight: 0x5000B5 - bytes32 slotStoredAtTheApp = - 0xfa2ef1bab164a0522c2c110bbea1a54ac6399d3ba24437480c29947143a5402e; - // This is the worldStateRoot at blockheight: 0x5000B5 (Sepolia!) - bytes32 worldStateRoot = 0x90c5f343ed98545ad5ad4e840492e1008218c0ea92f8fd74a826aaf4c477a3fe; - // This is the worldStateRoot just RLP encoded with + bytes32 slot = 0xfa2ef1bab164a0522c2c110bbea1a54ac6399d3ba24437480c29947143a5402e; + // This is the stateRoot at blockheight: 0x5000B5 (Sepolia!) + bytes32 stateRoot = 0x90c5f343ed98545ad5ad4e840492e1008218c0ea92f8fd74a826aaf4c477a3fe; + // This is the stateRoot just RLP encoded with // https://toolkit.abdk.consulting/ethereum#key-to-address,rlp // Not needed for now but leave it as is - //bytes memory worldStateRootRLPEncoded = + //bytes memory stateRootRLPEncoded = // hex"e1a090c5f343ed98545ad5ad4e840492e1008218c0ea92f8fd74a826aaf4c477a3fe"; bytes[] memory accountProof = new bytes[](8); @@ -49,14 +48,26 @@ contract TestLibTrieProof is TaikoTest { bytes[] memory storageProof = new bytes[](1); storageProof[0] = hex"e3a1209749684f52b5c0717a7ca78127fb56043d637d81763c04e9d30ba4d4746d56e901"; - bytes memory merkleProof = abi.encode(accountProof, storageProof); - - LibTrieProof.verifyFullMerkleProof( - worldStateRoot, - contractWhichStoresValue1AtSlot, - slotStoredAtTheApp, - hex"01", - merkleProof + + vm.expectRevert(); + LibTrieProof.verifyMerkleProof( + stateRoot, randAddress(), slot, hex"01", accountProof, storageProof + ); + + vm.expectRevert(); + LibTrieProof.verifyMerkleProof( + stateRoot, address(0), slot, hex"01", accountProof, storageProof + ); + + bytes32 storageRoot = LibTrieProof.verifyMerkleProof( + stateRoot, addr, slot, hex"01", accountProof, storageProof + ); + + accountProof = new bytes[](0); + bytes32 storageRoot2 = LibTrieProof.verifyMerkleProof( + storageRoot, addr, slot, hex"01", accountProof, storageProof ); + + assertEq(storageRoot2, storageRoot); } } diff --git a/packages/protocol/test/signal/SignalService.t.sol b/packages/protocol/test/signal/SignalService.t.sol index e21c939dbe..3a3983fb2e 100644 --- a/packages/protocol/test/signal/SignalService.t.sol +++ b/packages/protocol/test/signal/SignalService.t.sol @@ -3,44 +3,29 @@ pragma solidity 0.8.24; import "../TaikoTest.sol"; -contract SignalServiceForTest is SignalService { - bool private _skipVerifyMerkleProof; - bool private _multiHopEnabled; - - function setSkipMerkleProofCheck(bool skip) external { - _skipVerifyMerkleProof = skip; - } - - function setMultiHopEnabled(bool enabled) external { - _multiHopEnabled = enabled; - } - - function verifyMerkleProof( - bytes32, /*stateRoot*/ - uint64, /*srcChainId*/ - address, /*srcApp*/ - bytes32, /*srcSignal*/ - bytes memory /*merkleProof*/ +contract MockSignalService is SignalService { + function _verifyHopProof( + uint64, /*chainId*/ + address, /*app*/ + bytes32, /*signal*/ + HopProof memory, /*hop*/ + address /*relay*/ ) - public - view + internal + pure override + returns (bytes32) { - if (!_skipVerifyMerkleProof) revert("verifyMerkleProof failed"); - } - - function isMultiHopEnabled() public view override returns (bool) { - return _multiHopEnabled; + // Skip verifying the merkle proof entirely + return bytes32(uint256(789)); } } contract TestSignalService is TaikoTest { AddressManager addressManager; - SignalServiceForTest signalService; - SignalService destSignalService; - HopRelayRegistry hopRelayRegistry; - DummyCrossChainSync crossChainSync; + MockSignalService signalService; uint64 public destChainId = 7; + address taiko; function setUp() public { vm.startPrank(Alice); @@ -57,40 +42,16 @@ contract TestSignalService is TaikoTest { }) ); - signalService = SignalServiceForTest( + signalService = MockSignalService( deployProxy({ name: "signal_service", - impl: address(new SignalServiceForTest()), + impl: address(new MockSignalService()), data: abi.encodeCall(SignalService.init, (address(addressManager))) }) ); - hopRelayRegistry = HopRelayRegistry( - deployProxy({ - name: "hop_relay_registry", - impl: address(new HopRelayRegistry()), - data: abi.encodeCall(HopRelayRegistry.init, ()), - registerTo: address(addressManager), - owner: address(0) - }) - ); - - destSignalService = SignalService( - deployProxy({ - name: "signal_service", - impl: address(new SignalServiceForTest()), - data: abi.encodeCall(SignalService.init, (address(addressManager))) - }) - ); - - crossChainSync = DummyCrossChainSync( - deployProxy({ - name: "taiko", // must be named so - impl: address(new DummyCrossChainSync()), - data: "" - }) - ); - + taiko = randAddress(); + addressManager.setAddress(uint64(block.chainid), "taiko", taiko); vm.stopPrank(); } @@ -127,107 +88,459 @@ contract TestSignalService is TaikoTest { } } - function test_SignalService_proveSignalReceived_L1_L2() public { - signalService.setSkipMerkleProofCheck(true); - signalService.setMultiHopEnabled(false); + function test_SignalService_proveSignalReceived_revert_invalid_chainid_or_signal() public { + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](1); + + // app being address(0) will revert + vm.expectRevert(SignalService.SS_INVALID_PARAMS.selector); + signalService.proveSignalReceived({ + chainId: 1, + app: address(0), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + + // signal being 0 will revert + vm.expectRevert(SignalService.SS_INVALID_PARAMS.selector); + signalService.proveSignalReceived({ + chainId: uint64(block.chainid), + app: randAddress(), + signal: 0, + proof: abi.encode(proofs) + }); + } + + function test_SignalService_proveSignalReceived_revert_malformat_proof() public { + // "undecodable proof" is not decodeable into SignalService.HopProof[] memory + vm.expectRevert(); + signalService.proveSignalReceived({ + chainId: 0, + app: randAddress(), + signal: randBytes32(), + proof: "undecodable proof" + }); + } - bytes32 stateRoot = randBytes32(); - crossChainSync.setSyncedData("", stateRoot); + function test_SignalService_proveSignalReceived_revert_src_signal_service_not_registered() + public + { + uint64 srcChainId = uint64(block.chainid - 1); - uint64 thisChainId = uint64(block.chainid); + // Did not call the following, so revert with RESOLVER_ZERO_ADDR + // vm.prank(Alice); + // addressManager.setAddress(srcChainId, "signal_service", randAddress()); - uint64 srcChainId = thisChainId + 1; - address app = randAddress(); - bytes32 signal = randBytes32(); + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](1); - SignalService.Proof memory p; - p.height = 10; - // p.merkleProof = "doesn't matter"; + vm.expectRevert( + abi.encodeWithSelector( + AddressResolver.RESOLVER_ZERO_ADDR.selector, + srcChainId, + strToBytes32("signal_service") + ) + ); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + } - vm.expectRevert(); // cannot resolve "taiko" - signalService.proveSignalReceived(srcChainId, app, signal, abi.encode(p)); + function test_SignalService_proveSignalReceived_revert_zero_size_proof() public { + uint64 srcChainId = uint64(block.chainid - 1); - vm.startPrank(Alice); - register(address(addressManager), "taiko", address(crossChainSync), thisChainId); - assertEq(signalService.proveSignalReceived(srcChainId, app, signal, abi.encode(p)), true); + vm.prank(Alice); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); - signalService.setSkipMerkleProofCheck(false); + // proofs.length must > 0 in order not to revert + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](0); - vm.expectRevert(); // cannot decode the proof - signalService.proveSignalReceived(srcChainId, app, signal, abi.encode(p)); + vm.expectRevert(SignalService.SS_EMPTY_PROOF.selector); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); } - function test_SignalService_proveSignalReceived_multi_hop_L2_L2() public { - signalService.setSkipMerkleProofCheck(true); - signalService.setMultiHopEnabled(false); + function test_SignalService_proveSignalReceived_revert_last_hop_incorrect_chainid() public { + uint64 srcChainId = uint64(block.chainid - 1); - bytes32 stateRoot = randBytes32(); - crossChainSync.setSyncedData("", stateRoot); + vm.prank(Alice); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); - uint64 thisChainId = uint64(block.chainid); + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](1); - uint64 srcChainId = thisChainId + 1; + // proofs[0].chainId must be block.chainid in order not to revert + proofs[0].chainId = uint64(block.chainid + 1); - uint64 hop1ChainId = thisChainId + 2; - address hop1Relay = randAddress(); - bytes32 hop1StateRoot = randBytes32(); + vm.expectRevert(SignalService.SS_INVALID_LAST_HOP_CHAINID.selector); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + } - uint64 hop2ChainId = thisChainId + 3; - address hop2Relay = randAddress(); - bytes32 hop2StateRoot = randBytes32(); + function test_SignalService_proveSignalReceived_revert_mid_hop_incorrect_chainid() public { + uint64 srcChainId = uint64(block.chainid - 1); - address app = randAddress(); - bytes32 signal = randBytes32(); + vm.prank(Alice); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); - SignalService.Proof memory p; - p.height = 10; - p.hops = new SignalService.Hop[](2); + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](2); - p.hops[0] = SignalService.Hop({ - chainId: hop1ChainId, - relay: hop1Relay, - stateRoot: hop1StateRoot, - merkleProof: "dummy proof1" + // proofs[0].chainId must NOT be block.chainid in order not to revert + proofs[0].chainId = uint64(block.chainid); + + vm.expectRevert(SignalService.SS_INVALID_MID_HOP_CHAINID.selector); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) }); + } + + function test_SignalService_proveSignalReceived_revert_mid_hop_not_registered() public { + uint64 srcChainId = uint64(block.chainid + 1); + + vm.prank(Alice); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); - p.hops[1] = SignalService.Hop({ - chainId: hop2ChainId, - relay: hop2Relay, - stateRoot: hop2StateRoot, - merkleProof: "dummy proof2" + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](2); + + // proofs[0].chainId must NOT be block.chainid in order not to revert + proofs[0].chainId = srcChainId + 1; + + vm.expectRevert( + abi.encodeWithSelector( + AddressResolver.RESOLVER_ZERO_ADDR.selector, + proofs[0].chainId, + strToBytes32("signal_service") + ) + ); + + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) }); + } - vm.startPrank(Alice); - register(address(addressManager), "taiko", address(crossChainSync), thisChainId); + function test_SignalService_proveSignalReceived_local_chaindata_not_found() public { + uint64 srcChainId = uint64(block.chainid + 1); - // Multiple is disabled, shall revert - vm.expectRevert(SignalService.SS_MULTIHOP_DISABLED.selector); - signalService.proveSignalReceived(srcChainId, app, signal, abi.encode(p)); + vm.prank(Alice); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); - // Enable multi-hop - vm.startPrank(Alice); - signalService.setMultiHopEnabled(true); + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](1); + + proofs[0].chainId = uint64(block.chainid); - // Neither relay is registered - vm.expectRevert(SignalService.SS_INVALID_RELAY.selector); - signalService.proveSignalReceived(srcChainId, app, signal, abi.encode(p)); + // the proof is a storage proof + proofs[0].accountProof = new bytes[](0); + proofs[0].storageProof = new bytes[](10); - // Register both relays + vm.expectRevert(SignalService.SS_LOCAL_CHAIN_DATA_NOT_FOUND.selector); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + + // the proof is a full proof + proofs[0].accountProof = new bytes[](1); + + vm.expectRevert(SignalService.SS_LOCAL_CHAIN_DATA_NOT_FOUND.selector); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + } + + function test_SignalService_proveSignalReceived_one_hop_cache_signal_root() public { + uint64 srcChainId = uint64(block.chainid + 1); + + vm.prank(Alice); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); + + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](1); + + proofs[0].chainId = uint64(block.chainid); + proofs[0].rootHash = randBytes32(); + + // the proof is a storage proof + proofs[0].accountProof = new bytes[](0); + proofs[0].storageProof = new bytes[](10); + + vm.expectRevert(SignalService.SS_LOCAL_CHAIN_DATA_NOT_FOUND.selector); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + + // relay the signal root + vm.prank(taiko); + signalService.relayChainData(srcChainId, LibSignals.SIGNAL_ROOT, proofs[0].rootHash); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + } + + function test_SignalService_proveSignalReceived_one_hop_state_root() public { + uint64 srcChainId = uint64(block.chainid + 1); + + vm.prank(Alice); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); + + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](1); + + proofs[0].chainId = uint64(block.chainid); + proofs[0].rootHash = randBytes32(); + + // the proof is a full merkle proof + proofs[0].accountProof = new bytes[](1); + proofs[0].storageProof = new bytes[](10); + + vm.expectRevert(SignalService.SS_LOCAL_CHAIN_DATA_NOT_FOUND.selector); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + + // relay the state root + vm.prank(taiko); + signalService.relayChainData(srcChainId, LibSignals.STATE_ROOT, proofs[0].rootHash); + + // Should not revert + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + + assertEq( + signalService.isChainDataRelayed( + srcChainId, LibSignals.SIGNAL_ROOT, bytes32(uint256(789)) + ), + false + ); + } + + function test_SignalService_proveSignalReceived_multiple_hops() public { + uint64 srcChainId = uint64(block.chainid + 1); + + vm.prank(Alice); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); + + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](3); + + // first hop with full merkle proof + proofs[0].chainId = uint64(block.chainid + 2); + proofs[0].rootHash = randBytes32(); + proofs[0].accountProof = new bytes[](1); + proofs[0].storageProof = new bytes[](10); + + // second hop with storage merkle proof + proofs[1].chainId = uint64(block.chainid + 3); + proofs[1].rootHash = randBytes32(); + proofs[1].accountProof = new bytes[](0); + proofs[1].storageProof = new bytes[](10); + + // third/last hop with full merkle proof + proofs[2].chainId = uint64(block.chainid); + proofs[2].rootHash = randBytes32(); + proofs[2].accountProof = new bytes[](1); + proofs[2].storageProof = new bytes[](10); + + // expect RESOLVER_ZERO_ADDR + vm.expectRevert( + abi.encodeWithSelector( + AddressResolver.RESOLVER_ZERO_ADDR.selector, + proofs[0].chainId, + strToBytes32("signal_service") + ) + ); + + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + + // Add two trusted hop relayers vm.startPrank(Alice); - hopRelayRegistry.registerRelay(srcChainId, hop1ChainId, hop1Relay); - hopRelayRegistry.registerRelay(hop1ChainId, hop2ChainId, hop2Relay); + addressManager.setAddress(proofs[0].chainId, "signal_service", randAddress() /*relay1*/ ); + addressManager.setAddress(proofs[1].chainId, "signal_service", randAddress() /*relay2*/ ); vm.stopPrank(); - signalService.proveSignalReceived(srcChainId, app, signal, abi.encode(p)); + vm.expectRevert(SignalService.SS_LOCAL_CHAIN_DATA_NOT_FOUND.selector); + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + + vm.prank(taiko); + signalService.relayChainData(proofs[1].chainId, LibSignals.STATE_ROOT, proofs[2].rootHash); - // Deregister the first relay and register it again with incorrect chainIds + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + } + + function test_SignalService_proveSignalReceived_multiple_hops_caching() public { + uint64 srcChainId = uint64(block.chainid + 1); + uint64 nextChainId = srcChainId + 100; + + SignalService.HopProof[] memory proofs = new SignalService.HopProof[](9); + + // hop 1: full merkle proof, CACHE_NOTHING + proofs[0].chainId = nextChainId++; + proofs[0].rootHash = randBytes32(); + proofs[0].accountProof = new bytes[](1); + proofs[0].storageProof = new bytes[](10); + proofs[0].cacheOption = SignalService.CacheOption.CACHE_NOTHING; + + // hop 2: full merkle proof, CACHE_STATE_ROOT + proofs[1].chainId = nextChainId++; + proofs[1].rootHash = randBytes32(); + proofs[1].accountProof = new bytes[](1); + proofs[1].storageProof = new bytes[](10); + proofs[1].cacheOption = SignalService.CacheOption.CACHE_STATE_ROOT; + + // hop 3: full merkle proof, CACHE_SIGNAL_ROOT + proofs[2].chainId = nextChainId++; + proofs[2].rootHash = randBytes32(); + proofs[2].accountProof = new bytes[](1); + proofs[2].storageProof = new bytes[](10); + proofs[2].cacheOption = SignalService.CacheOption.CACHE_SIGNAL_ROOT; + + // hop 4: full merkle proof, CACHE_BOTH + proofs[3].chainId = nextChainId++; + proofs[3].rootHash = randBytes32(); + proofs[3].accountProof = new bytes[](1); + proofs[3].storageProof = new bytes[](10); + proofs[3].cacheOption = SignalService.CacheOption.CACHE_BOTH; + + // hop 5: storage merkle proof, CACHE_NOTHING + proofs[4].chainId = nextChainId++; + proofs[4].rootHash = randBytes32(); + proofs[4].accountProof = new bytes[](0); + proofs[4].storageProof = new bytes[](10); + proofs[4].cacheOption = SignalService.CacheOption.CACHE_NOTHING; + + // hop 6: storage merkle proof, CACHE_STATE_ROOT + proofs[5].chainId = nextChainId++; + proofs[5].rootHash = randBytes32(); + proofs[5].accountProof = new bytes[](0); + proofs[5].storageProof = new bytes[](10); + proofs[5].cacheOption = SignalService.CacheOption.CACHE_STATE_ROOT; + + // hop 7: storage merkle proof, CACHE_SIGNAL_ROOT + proofs[6].chainId = nextChainId++; + proofs[6].rootHash = randBytes32(); + proofs[6].accountProof = new bytes[](0); + proofs[6].storageProof = new bytes[](10); + proofs[6].cacheOption = SignalService.CacheOption.CACHE_SIGNAL_ROOT; + + // hop 8: storage merkle proof, CACHE_BOTH + proofs[7].chainId = nextChainId++; + proofs[7].rootHash = randBytes32(); + proofs[7].accountProof = new bytes[](0); + proofs[7].storageProof = new bytes[](10); + proofs[7].cacheOption = SignalService.CacheOption.CACHE_BOTH; + + // last hop, 9: full merkle proof, CACHE_BOTH + proofs[8].chainId = uint64(block.chainid); + proofs[8].rootHash = randBytes32(); + proofs[8].accountProof = new bytes[](1); + proofs[8].storageProof = new bytes[](10); + proofs[8].cacheOption = SignalService.CacheOption.CACHE_BOTH; + + // Add two trusted hop relayers vm.startPrank(Alice); - hopRelayRegistry.deregisterRelay(srcChainId, hop1ChainId, hop1Relay); - hopRelayRegistry.registerRelay(999, 888, hop1Relay); + addressManager.setAddress(srcChainId, "signal_service", randAddress()); + for (uint256 i; i < proofs.length; ++i) { + addressManager.setAddress( + proofs[i].chainId, "signal_service", randAddress() /*relay1*/ + ); + } vm.stopPrank(); - // Still revert - vm.expectRevert(SignalService.SS_INVALID_RELAY.selector); - signalService.proveSignalReceived(srcChainId, app, signal, abi.encode(p)); + vm.prank(taiko); + signalService.relayChainData(proofs[7].chainId, LibSignals.STATE_ROOT, proofs[8].rootHash); + + signalService.proveSignalReceived({ + chainId: srcChainId, + app: randAddress(), + signal: randBytes32(), + proof: abi.encode(proofs) + }); + + // hop 1: full merkle proof, CACHE_NOTHING + _verifyCache(srcChainId, proofs[0].rootHash, false, false); + // hop 2: full merkle proof, CACHE_STATE_ROOT + _verifyCache(proofs[0].chainId, proofs[1].rootHash, true, false); + // hop 3: full merkle proof, CACHE_SIGNAL_ROOT + _verifyCache(proofs[1].chainId, proofs[2].rootHash, false, true); + // hop 4: full merkle proof, CACHE_BOTH + _verifyCache(proofs[2].chainId, proofs[3].rootHash, true, true); + + // hop 5: storage merkle proof, CACHE_NOTHING + _verifyCache(proofs[3].chainId, proofs[4].rootHash, false, false); + + // hop 6: storage merkle proof, CACHE_STATE_ROOT + _verifyCache(proofs[4].chainId, proofs[5].rootHash, false, false); + + // hop 7: storage merkle proof, CACHE_SIGNAL_ROOT + _verifyCache(proofs[5].chainId, proofs[6].rootHash, false, true); + + // hop 8: storage merkle proof, CACHE_BOTH + _verifyCache(proofs[6].chainId, proofs[7].rootHash, false, true); + + // last hop, 9: full merkle proof, CACHE_BOTH + // last hop's state root is already cached even before the proveSignalReceived call. + _verifyCache(proofs[7].chainId, proofs[8].rootHash, true, true); + } + + function _verifyCache( + uint64 chainId, + bytes32 stateRoot, + bool stateRootCached, + bool signalRootCached + ) + private + { + assertEq( + signalService.isChainDataRelayed(chainId, LibSignals.STATE_ROOT, stateRoot), + stateRootCached + ); + + assertEq( + signalService.isChainDataRelayed(chainId, LibSignals.SIGNAL_ROOT, bytes32(uint256(789))), + signalRootCached + ); } } diff --git a/packages/protocol/utils/generate_genesis/taikoL2.ts b/packages/protocol/utils/generate_genesis/taikoL2.ts index dd4aa26aae..3e2854e242 100644 --- a/packages/protocol/utils/generate_genesis/taikoL2.ts +++ b/packages/protocol/utils/generate_genesis/taikoL2.ts @@ -242,6 +242,9 @@ async function generateContractConfigs( // AddressManager addresses: { [chainId]: { + [ethers.utils.hexlify( + ethers.utils.toUtf8Bytes("taiko"), + )]: addressMap.TaikoL2, [ethers.utils.hexlify( ethers.utils.toUtf8Bytes("bridge"), )]: addressMap.Bridge, @@ -459,6 +462,8 @@ async function generateContractConfigs( _paused: 1, // _FALSE // Ownable2Upgradeable _owner: ownerSecurityCouncil, + // AddressResolver + addressManager: addressMap.SharedAddressManager, }, slots: { [IMPLEMENTATION_SLOT]: addressMap.SignalServiceImpl,