From 3f310cf369e644e2489cf70d762e9a2e66ab73a5 Mon Sep 17 00:00:00 2001 From: Lohann Paterno Coutinho Ferreira Date: Mon, 25 Nov 2024 11:14:33 -0300 Subject: [PATCH] Update gateway events (#27) --- src/Gateway.sol | 402 ++++++++++++++++------------------- src/Primitives.sol | 123 ++++++++--- src/interfaces/IExecutor.sol | 23 +- src/storage/Routes.sol | 168 ++++++++------- src/storage/Shards.sol | 228 ++++++++++---------- src/utils/EnumerableSet.sol | 7 +- src/utils/GasUtils.sol | 34 ++- test/EnumerableSet.t.sol | 7 +- test/GasUtils.t.sol | 24 +-- test/Gateway.t.sol | 7 +- 10 files changed, 547 insertions(+), 476 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index df1b724..fd60cf6 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -8,6 +8,7 @@ import {BranchlessMath} from "./utils/BranchlessMath.sol"; import {GasUtils} from "./utils/GasUtils.sol"; import {ERC1967} from "./utils/ERC1967.sol"; import {UFloat9x56, UFloatMath} from "./utils/Float9x56.sol"; +import {RouteStore} from "./storage/Routes.sol"; import {ShardStore} from "./storage/Shards.sol"; import {IGateway} from "./interfaces/IGateway.sol"; import {IUpgradable} from "./interfaces/IUpgradable.sol"; @@ -20,12 +21,18 @@ import { UpdateNetworkInfo, Signature, Network, + Route, GmpStatus, GmpSender, - PrimitiveUtils + GmpCallback, + PrimitiveUtils, + MAX_PAYLOAD_SIZE } from "./Primitives.sol"; +import {NetworkID, NetworkIDHelpers} from "./NetworkID.sol"; abstract contract GatewayEIP712 { + using NetworkIDHelpers for NetworkID; + // EIP-712: Typed structured data hashing and signing // https://eips.ethereum.org/EIPS/eip-712 uint16 internal immutable NETWORK_ID; @@ -35,17 +42,17 @@ abstract contract GatewayEIP712 { constructor(uint16 networkId, address gateway) { NETWORK_ID = networkId; PROXY_ADDRESS = gateway; - DOMAIN_SEPARATOR = computeDomainSeparator(networkId, gateway); + DOMAIN_SEPARATOR = computeDomainSeparator(NetworkID.wrap(networkId), gateway); } // Computes the EIP-712 domain separador - function computeDomainSeparator(uint256 networkId, address addr) internal pure returns (bytes32) { + function computeDomainSeparator(NetworkID networkId, address addr) internal pure returns (bytes32) { return keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("Analog Gateway Contract"), keccak256("0.1.0"), - uint256(networkId), + uint256(networkId.asUint()), address(addr) ) ); @@ -60,18 +67,22 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { using BranchlessMath for uint256; using UFloatMath for UFloat9x56; using ShardStore for ShardStore.MainStorage; - - uint8 internal constant SHARD_ACTIVE = (1 << 0); // Shard active bitflag - uint8 internal constant SHARD_Y_PARITY = (1 << 1); // Pubkey y parity bitflag + using RouteStore for RouteStore.MainStorage; + using RouteStore for RouteStore.NetworkInfo; + using NetworkIDHelpers for NetworkID; /** - * @dev Maximum size of the GMP payload + * @dev Non-zero value used to initialize the `prevMessageHash` storage */ - uint256 internal constant MAX_PAYLOAD_SIZE = 0x6000; - - // Non-zero value used to initialize the `prevMessageHash` storage bytes32 internal constant FIRST_MESSAGE_PLACEHOLDER = bytes32(uint256(2 ** 256 - 1)); + /** + * @dev Selector of `GmpCreated` event. + * keccak256("GmpCreated(bytes32,bytes32,address,uint16,uint256,uint256,bytes)"); + */ + bytes32 private constant GMP_CREATED_EVENT_SELECTOR = + 0x0114885f90b5168242aa31b7afb9c2e9f88e90ce329c893d3e6c56021c4c03a5; + // GMP message status mapping(bytes32 => GmpInfo) private _messages; @@ -82,9 +93,6 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // messageHash => shardId mapping(bytes32 => bytes32) private _executedMessages; - // Network ID => Source network - mapping(uint16 => NetworkInfo) private _networkInfo; - /** * @dev GMP info stored in the Gateway Contract * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep @@ -131,7 +139,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { constructor(uint16 network, address proxy) payable GatewayEIP712(network, proxy) {} // EIP-712 typed hash - function initialize(address admin, TssKey[] memory keys, Network[] calldata networks) external { + function initialize(address admin, TssKey[] calldata keys, Network[] calldata networks) external { require(PROXY_ADDRESS == address(this), "only proxy can be initialize"); require(prevMessageHash == 0, "already initialized"); ERC1967.setAdmin(admin); @@ -141,11 +149,11 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { prevMessageHash = FIRST_MESSAGE_PLACEHOLDER; // Register networks - _updateNetworks(networks); + RouteStore.getMainStorage().initialize(networks, NetworkID.wrap(NETWORK_ID), computeDomainSeparator); + // _updateNetworks(networks); // Register keys - ShardStore.MainStorage storage shards = ShardStore.getMainStorage(); - shards.registerTssKeys(keys); + ShardStore.getMainStorage().registerTssKeys(keys); // emit event TssKey[] memory revoked = new TssKey[](0); @@ -156,7 +164,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { return _messages[id]; } - function keyInfo(bytes32 id) external view returns (ShardStore.KeyInfo memory) { + function keyInfo(bytes32 id) external view returns (ShardStore.ShardInfo memory) { ShardStore.MainStorage storage store = ShardStore.getMainStorage(); return store.get(ShardStore.ShardID.wrap(id)); } @@ -165,12 +173,8 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { return NETWORK_ID; } - function networkInfo(uint16 id) external view returns (NetworkInfo memory) { - return _networkInfo[id]; - } - - function listShards() external view returns (TssKey[] memory) { - return ShardStore.getMainStorage().listShards(); + function networkInfo(uint16 id) external view returns (RouteStore.NetworkInfo memory) { + return RouteStore.getMainStorage().get(NetworkID.wrap(id)); } /** @@ -178,19 +182,11 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { */ function _verifySignature(Signature calldata signature, bytes32 message) private view { // Load shard from storage - ShardStore.KeyInfo storage signer; - { - ShardStore.MainStorage storage store = ShardStore.getMainStorage(); - signer = store.get(signature); - } - - // Verify if shard is active - uint8 status = signer.status; - require((status & SHARD_ACTIVE) > 0, "shard key revoked or not exists"); + ShardStore.ShardInfo storage signer = ShardStore.getMainStorage().get(signature); // Load y parity bit, it must be 27 (even), or 28 (odd) // ref: https://ethereum.github.io/yellowpaper/paper.pdf - uint8 yParity = BranchlessMath.ternaryU8((status & SHARD_Y_PARITY) > 0, 28, 27); + uint8 yParity = BranchlessMath.ternaryU8(signer.yParity > 0, 28, 27); // Verify Signature require( @@ -199,63 +195,33 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { ); } - // Converts a `TssKey` into an `ShardStore.ShardID` unique identifier - function _tssKeyToShardId(TssKey memory tssKey) private pure returns (ShardStore.ShardID) { - // The tssKey coord x is already collision resistant - // if we are unsure about it, we can hash the coord and parity bit - return ShardStore.ShardID.wrap(bytes32(tssKey.xCoord)); - } - - // Initialize networks - function _updateNetworks(Network[] calldata networks) private { - for (uint256 i = 0; i < networks.length; i++) { - Network calldata network = networks[i]; - NetworkInfo storage info = _networkInfo[network.id]; - require(info.domainSeparator == bytes32(0), "network already initialized"); - require(network.id != NETWORK_ID || network.gateway == address(this), "wrong gateway address"); - info.domainSeparator = computeDomainSeparator(network.id, network.gateway); - info.gasLimit = 15_000_000; // Default to 15M gas - info.relativeGasPrice = UFloatMath.ONE; - info.baseFee = 0; - } - } - - // Register/Revoke TSS keys and emits [`KeySetChanged`] event - function _updateKeys(bytes32 messageHash, TssKey[] memory keysToRevoke, TssKey[] memory newKeys) private { - ShardStore.MainStorage storage shards = ShardStore.getMainStorage(); - - // Revoke tss keys (revoked keys can be registred again keeping the previous nonce) - shards.revokeKeys(keysToRevoke); - - // Register or activate revoked keys - shards.registerTssKeys(newKeys); - - // Emit event - emit KeySetChanged(messageHash, keysToRevoke, newKeys); - } - // Register/Revoke TSS keys using shard TSS signature - function updateKeys(Signature calldata signature, UpdateKeysMessage memory message) public { + function updateKeys(Signature calldata signature, UpdateKeysMessage calldata message) external { + // Check if the message was already executed to prevent replay attacks bytes32 messageHash = message.eip712TypedHash(DOMAIN_SEPARATOR); - - // Verify signature and if the message was already executed require(_executedMessages[messageHash] == bytes32(0), "message already executed"); - _verifySignature(signature, messageHash); - // Store the message hash to prevent replay attacks + // Verify the signature and store the message hash + _verifySignature(signature, messageHash); _executedMessages[messageHash] = bytes32(signature.xCoord); // Register/Revoke shards pubkeys - _updateKeys(messageHash, message.revoke, message.register); + ShardStore.MainStorage storage store = ShardStore.getMainStorage(); + + // Revoke tss keys (revoked keys can be registred again keeping the previous nonce) + store.revokeKeys(message.revoke); + + // Register or activate revoked keys + store.registerTssKeys(message.register); + + // Emit event + emit KeySetChanged(messageHash, message.revoke, message.register); } // Execute GMP message - function _execute(bytes32 payloadHash, GmpMessage calldata message, bytes memory data) - private - returns (GmpStatus status, bytes32 result) - { + function _execute(GmpCallback memory message) private returns (GmpStatus status, bytes32 result) { // Verify if this GMP message was already executed - GmpInfo storage gmp = _messages[payloadHash]; + GmpInfo storage gmp = _messages[message.eip712hash]; require(gmp.status == GmpStatus.NOT_FOUND, "message already executed"); // Update status to `pending` to prevent reentrancy attacks. @@ -279,13 +245,14 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { bool success; address dest = message.dest; + bytes memory callback = message.callback; /// @solidity memory-safe-assembly assembly { // Using low-level assembly because the GMP is considered executed // regardless if the call reverts or not. - let ptr := add(data, 32) - let size := mload(data) - mstore(data, 0) + let ptr := add(callback, 32) + let size := mload(callback) + mstore(callback, 0) // returns 1 if the call succeed, and 0 if it reverted success := @@ -295,13 +262,13 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { 0, // value in wei to transfer (always zero for GMP) ptr, // input memory pointer size, // input size - data, // output memory pointer + callback, // output memory pointer 32 // output size (fixed 32 bytes) ) // Get Result, reuse data to keep a predictable memory expansion - result := mload(data) - mstore(data, size) + result := mload(callback) + mstore(callback, size) } // Update GMP status @@ -311,7 +278,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { gmp.status = status; // Emit event - emit GmpExecuted(payloadHash, message.source, message.dest, status, result); + emit GmpExecuted(message.eip712hash, message.source, message.dest, status, result); } /** @@ -326,7 +293,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { uint256 initialGas = gasleft(); // Add the solidity selector overhead to the initial gas, this way we guarantee that // the `initialGas` represents the actual gas that was available to this contract. - initialGas = initialGas.saturatingAdd(453); + initialGas = initialGas.saturatingAdd(437); // Theoretically we could remove the destination network field // and fill it up with the network id of the contract, then the signature will fail. @@ -335,17 +302,20 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { // Check if the message data is too large require(message.data.length <= MAX_PAYLOAD_SIZE, "msg data too large"); - // Verify the signature - (bytes32 messageHash, bytes memory data) = message.encodeCallback(DOMAIN_SEPARATOR); - _verifySignature(signature, messageHash); + // Convert the `GmpMessage` into `GmpCallback`, which is a more efficient representation. + // see `src/Primitives.sol` for more details. + GmpCallback memory callback = message.intoCallback(DOMAIN_SEPARATOR); + + // Verify the TSS Schnorr Signature + _verifySignature(signature, callback.eip712hash); // Execute GMP message - (status, result) = _execute(messageHash, message, data); + (status, result) = _execute(callback); // Refund the chronicle gas unchecked { // Compute GMP gas used - uint256 gasUsed = 7214; + uint256 gasUsed = 7223; gasUsed = gasUsed.saturatingAdd(GasUtils.txBaseCost()); gasUsed = gasUsed.saturatingAdd(GasUtils.proxyOverheadGasCost(uint16(msg.data.length), 64)); gasUsed = gasUsed.saturatingAdd(initialGas - gasleft()); @@ -355,6 +325,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { /// @solidity memory-safe-assembly assembly { + // Refund the gas used pop(call(gas(), caller(), refund, 0, 0, 0, 0)) } } @@ -363,33 +334,22 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { /** * @dev Send message from this chain to another chain. * @param destinationAddress the target address on the destination chain - * @param destinationNetwork the target chain where the contract call will be made + * @param routeId the target chain where the contract call will be made * @param executionGasLimit the gas limit available for the contract call * @param data message data with no specified format */ - function submitMessage( - address destinationAddress, - uint16 destinationNetwork, - uint256 executionGasLimit, - bytes calldata data - ) external payable returns (bytes32) { + function submitMessage(address destinationAddress, uint16 routeId, uint256 executionGasLimit, bytes calldata data) + external + payable + returns (bytes32) + { // Check if the message data is too large require(data.length <= MAX_PAYLOAD_SIZE, "msg data too large"); - // Check if the destination network is supported - NetworkInfo storage info = _networkInfo[destinationNetwork]; - bytes32 domainSeparator = info.domainSeparator; - require(domainSeparator != bytes32(0), "unsupported network"); - - // Check if the sender has deposited enougth funds to execute the GMP message - { - uint256 nonZeros = GasUtils.countNonZerosCalldata(data); - uint256 zeros = data.length - nonZeros; - uint256 msgPrice = GasUtils.estimateWeiCost( - info.relativeGasPrice, info.baseFee, uint16(nonZeros), uint16(zeros), executionGasLimit - ); - require(msg.value >= msgPrice, "insufficient tx value"); - } + // Check if the provided parameters are valid + // See `RouteStorage.estimateWeiCost` at `storage/Routes.sol` for more details. + RouteStore.NetworkInfo memory route = RouteStore.getMainStorage().get(NetworkID.wrap(routeId)); + require(msg.value >= route.estimateWeiCost(data, executionGasLimit), "insufficient tx value"); // We use 20 bytes for represent the address and 1 bit for the contract flag GmpSender source = msg.sender.toSender(tx.origin != msg.sender); @@ -404,19 +364,34 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { bytes memory payload; { GmpMessage memory message = - GmpMessage(source, NETWORK_ID, destinationAddress, destinationNetwork, executionGasLimit, salt, data); - prevHash = message.eip712TypedHash(domainSeparator); + GmpMessage(source, NETWORK_ID, destinationAddress, routeId, executionGasLimit, salt, data); + prevHash = message.eip712TypedHash(route.domainSeparator); prevMessageHash = prevHash; payload = message.data; } + // Emit `GmpCreated` event without copy the data, to simplify the gas estimation. + _emitGmpCreated(prevHash, source, destinationAddress, routeId, executionGasLimit, salt, payload); + } + + /** + * @dev Emit `GmpCreated` event without copy the data, to simplify the gas estimation. + */ + function _emitGmpCreated( + bytes32 prevHash, + GmpSender source, + address destinationAddress, + uint16 destinationNetwork, + uint256 executionGasLimit, + uint256 salt, + bytes memory payload + ) private { // Emit `GmpCreated` event without copy the data, to simplify the gas estimation. // the assembly code below is equivalent to: // ```solidity // emit GmpCreated(prevHash, source, destinationAddress, destinationNetwork, executionGasLimit, salt, data); // return prevHash; // ``` - bytes32 eventSelector = GmpCreated.selector; assembly { let ptr := sub(payload, 0x80) mstore(ptr, destinationNetwork) // dest network @@ -425,7 +400,7 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { mstore(add(ptr, 0x60), 0x80) // data offset let size := and(add(mload(payload), 31), 0xffffffe0) size := add(size, 160) - log4(ptr, size, eventSelector, prevHash, source, destinationAddress) + log4(ptr, size, GMP_CREATED_EVENT_SELECTOR, prevHash, source, destinationAddress) mstore(0, prevHash) return(0, 32) } @@ -447,127 +422,130 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { view returns (uint256) { - NetworkInfo storage network = _networkInfo[networkid]; - uint256 baseFee = uint256(network.baseFee); - UFloat9x56 relativeGasPrice = network.relativeGasPrice; - - // Verify if the network exists - require(baseFee > 0 || UFloat9x56.unwrap(relativeGasPrice) > 0, "unsupported network"); - - // if the message data is too large, we use the maximum base fee. - baseFee = BranchlessMath.ternary(messageSize > MAX_PAYLOAD_SIZE, 2 ** 256 - 1, baseFee); + // NetworkInfo storage network = _networkInfo[networkid]; + RouteStore.NetworkInfo memory route = RouteStore.getMainStorage().get(NetworkID.wrap(networkid)); // Estimate the cost - return GasUtils.estimateWeiCost(relativeGasPrice, baseFee, uint16(messageSize), 0, gasLimit); + return route.estimateWeiCost(uint16(messageSize), gasLimit); } - function _setNetworkInfo(bytes32 executor, bytes32 messageHash, UpdateNetworkInfo calldata info) private { - require(info.mortality >= block.number, "message expired"); - require(executor != bytes32(0), "executor cannot be zero"); - - // Verify signature and if the message was already executed - require(_executedMessages[messageHash] == bytes32(0), "message already executed"); - - // Update network info - NetworkInfo memory stored = _networkInfo[info.networkId]; + /** + * Deposit funds to the gateway contract + * IMPORTANT: this function must be called only by the administrator!!!! + */ + function deposit() external payable {} - // Verify and update domain separator if it's not zero - stored.domainSeparator = - BranchlessMath.ternary(info.domainSeparator != bytes32(0), info.domainSeparator, stored.domainSeparator); - require(stored.domainSeparator != bytes32(0), "domain separator cannot be zero"); + /** + * Withdraw funds from the gateway contract + * @param amount The amount to withdraw + * @param recipient The recipient address + * @param data The data to send to the recipient (in case it is a contract) + */ + function withdraw(uint256 amount, address recipient, bytes calldata data) external returns (bytes memory output) { + require(msg.sender == _getAdmin(), "unauthorized"); + // Check if the recipient is a contract + if (recipient.code.length > 0) { + bool success; + (success, output) = recipient.call{value: amount, gas: gasleft()}(data); + if (!success) { + /// @solidity memory-safe-assembly + assembly { + revert(add(output, 32), mload(output)) + } + } + } else { + payable(recipient).transfer(amount); + output = ""; + } + } - // Update gas limit if it's not zero - stored.gasLimit = BranchlessMath.ternaryU64(info.gasLimit > 0, info.gasLimit, stored.gasLimit); + /*////////////////////////////////////////////////////////////// + SHARDS MANAGEMENT METHODS + //////////////////////////////////////////////////////////////*/ - // Update relative gas price and base fee if any of them are greater than zero - { - bool shouldUpdate = UFloat9x56.unwrap(info.relativeGasPrice) > 0 || info.baseFee > 0; - stored.relativeGasPrice = UFloat9x56.wrap( - BranchlessMath.ternaryU64( - shouldUpdate, UFloat9x56.unwrap(info.relativeGasPrice), UFloat9x56.unwrap(stored.relativeGasPrice) - ) - ); - stored.baseFee = BranchlessMath.ternaryU128(shouldUpdate, info.baseFee, stored.baseFee); - } + /** + * @dev List all shards. + */ + function shards() external view returns (TssKey[] memory) { + return ShardStore.getMainStorage().listShards(); + } - // Save the message hash to prevent replay attacks - _executedMessages[messageHash] = executor; + /** + * @dev Returns the number of active shards. + */ + function shardCount() external view returns (uint256) { + return ShardStore.getMainStorage().length(); + } - // Update network info - _networkInfo[info.networkId] = stored; + /** + * @dev Returns a shard by index. + * - Reverts with `IndexOutOfBounds` if the index is out of bounds. + */ + function shardAt(uint256 index) external view returns (TssKey memory) { + (ShardStore.ShardID xCoord, ShardStore.ShardInfo storage shard) = ShardStore.getMainStorage().at(index); + return TssKey({xCoord: uint256(ShardStore.ShardID.unwrap(xCoord)), yParity: shard.yParity}); + } - emit NetworkUpdated( - messageHash, - info.networkId, - stored.domainSeparator, - stored.relativeGasPrice, - stored.baseFee, - stored.gasLimit - ); + /** + * @dev Register a single Shards with provided TSS public key. + */ + function setShard(TssKey calldata publicKey) external { + require(msg.sender == _getAdmin(), "unauthorized"); + ShardStore.getMainStorage().register(publicKey); } /** - * @dev set network info using admin account + * @dev Register Shards in batch. */ - function setNetworkInfo(UpdateNetworkInfo calldata info) external { + function setShard(TssKey[] calldata publicKeys) external { require(msg.sender == _getAdmin(), "unauthorized"); - bytes32 messageHash = info.eip712TypedHash(DOMAIN_SEPARATOR); - _setNetworkInfo(bytes32(uint256(uint160(_getAdmin()))), messageHash, info); + ShardStore.getMainStorage().registerTssKeys(publicKeys); } /** - * @dev set network info using admin account + * @dev Revoke a single shard TSS Key. */ - function updateNetworks(UpdateNetworkInfo[] calldata networks) external { - require(networks.length > 0, "networks cannot be empty"); + function revokeShard(TssKey calldata publicKey) external { require(msg.sender == _getAdmin(), "unauthorized"); - for (uint256 i = 0; i < networks.length; i++) { - UpdateNetworkInfo calldata info = networks[i]; - bytes32 messageHash = info.eip712TypedHash(DOMAIN_SEPARATOR); - _setNetworkInfo(bytes32(uint256(uint160(_getAdmin()))), messageHash, info); - } + ShardStore.getMainStorage().revoke(publicKey); } /** - * @dev Update network information - * @param signature Schnorr signature - * @param info new network info + * @dev Revoke Shards in batch. */ - function setNetworkInfo(Signature calldata signature, UpdateNetworkInfo calldata info) external { - // Verify signature and check if the message was already executed - bytes32 messageHash = info.eip712TypedHash(DOMAIN_SEPARATOR); - _verifySignature(signature, messageHash); + function revokeShard(TssKey[] calldata publicKeys) external { + require(msg.sender == _getAdmin(), "unauthorized"); + ShardStore.getMainStorage().revokeKeys(publicKeys); + } + + /*////////////////////////////////////////////////////////////// + LISTING ROUTES AND SHARDS + //////////////////////////////////////////////////////////////*/ - // Update network info - _setNetworkInfo(bytes32(signature.xCoord), messageHash, info); + /** + * @dev List all routes. + */ + function routes() external view returns (Route[] memory) { + return RouteStore.getMainStorage().listRoutes(); } /** - * Deposit funds to the gateway contract + * @dev Create or update a single route */ - function deposit() external payable {} + function setRoute(Route calldata info) external { + require(msg.sender == _getAdmin(), "unauthorized"); + RouteStore.getMainStorage().createOrUpdateRoute(info); + } /** - * Withdraw funds from the gateway contract - * @param amount The amount to withdraw - * @param recipient The recipient address - * @param data The data to send to the recipient (in case it is a contract) + * @dev Create or update an array of routes */ - function withdraw(uint256 amount, address recipient, bytes calldata data) external returns (bytes memory output) { + function setRoute(Route[] calldata values) external { require(msg.sender == _getAdmin(), "unauthorized"); - // Check if the recipient is a contract - if (recipient.code.length > 0) { - bool success; - (success, output) = recipient.call{value: amount, gas: gasleft()}(data); - if (!success) { - /// @solidity memory-safe-assembly - assembly { - revert(add(output, 32), mload(output)) - } - } - } else { - payable(recipient).transfer(amount); - output = ""; + require(values.length > 0, "routes cannot be empty"); + RouteStore.MainStorage storage store = RouteStore.getMainStorage(); + for (uint256 i = 0; i < values.length; i++) { + store.createOrUpdateRoute(values[i]); } } @@ -587,17 +565,15 @@ contract Gateway is IGateway, IExecutor, IUpgradable, GatewayEIP712 { } // OBS: remove != revoke (when revoked, you cannot register again) - function sudoRemoveShards(TssKey[] memory revokedKeys) external payable { + function sudoRemoveShards(TssKey[] calldata revokedKeys) external payable { require(msg.sender == _getAdmin(), "unauthorized"); - ShardStore.MainStorage storage shards = ShardStore.getMainStorage(); - shards.revokeKeys(revokedKeys); + ShardStore.getMainStorage().revokeKeys(revokedKeys); emit KeySetChanged(bytes32(0), revokedKeys, new TssKey[](0)); } - function sudoAddShards(TssKey[] memory newKeys) external payable { + function sudoAddShards(TssKey[] calldata newKeys) external payable { require(msg.sender == _getAdmin(), "unauthorized"); - ShardStore.MainStorage storage shards = ShardStore.getMainStorage(); - shards.registerTssKeys(newKeys); + ShardStore.getMainStorage().registerTssKeys(newKeys); emit KeySetChanged(bytes32(0), new TssKey[](0), newKeys); } diff --git a/src/Primitives.sol b/src/Primitives.sol index 97ed938..3db6bf8 100644 --- a/src/Primitives.sol +++ b/src/Primitives.sol @@ -5,6 +5,12 @@ pragma solidity >=0.8.0; import {BranchlessMath} from "./utils/BranchlessMath.sol"; import {UFloatMath, UFloat9x56} from "./utils/Float9x56.sol"; +import {NetworkID} from "./NetworkID.sol"; + +/** + * @dev Maximum size of the GMP payload + */ +uint256 constant MAX_PAYLOAD_SIZE = 0x6000; /** * @dev GmpSender is the sender of a GMP message @@ -83,6 +89,23 @@ struct UpdateNetworkInfo { uint64 mortality; } +/** + * @dev A Route represents a communication channel between two networks. + * @param networkId The id of the provided network. + * @param gasLimit The maximum amount of gas we allow on this particular network. + * @param gateway Destination chain gateway address. + * @param relativeGasPriceNumerator Gas price numerator in terms of the source chain token. + * @param relativeGasPriceDenominator Gas price denominator in terms of the source chain token. + */ +struct Route { + NetworkID networkId; + uint64 gasLimit; + uint128 baseFee; + bytes32 gateway; + uint256 relativeGasPriceNumerator; + uint256 relativeGasPriceDenominator; +} + /** * @dev Message payload used to revoke or/and register new shards * @param revoke Shard's keys to revoke @@ -104,6 +127,28 @@ enum GmpStatus { PENDING } +/** + * @dev GmpMessage with EIP-712 GMP ID and callback function encoded. + * @param eip712hash EIP-712 hash of the `GmpMessage`, which is it's unique identifier + * @param source Pubkey/Address of who send the GMP message + * @param srcNetwork Source chain identifier (for ethereum networks it is the EIP-155 chain id) + * @param dest Destination/Recipient contract address + * @param destNetwork Destination chain identifier (it's the EIP-155 chain_id for ethereum networks) + * @param gasLimit gas limit of the GMP call + * @param salt Message salt, useful for sending two messages with same content + * @param callback encoded callback of `IGmpRecipient` interface, see `IGateway.sol` for more details. + */ +struct GmpCallback { + bytes32 eip712hash; + GmpSender source; + uint16 srcNetwork; + address dest; + uint16 destNetwork; + uint256 gasLimit; + uint256 salt; + bytes callback; +} + /** * @dev EIP-712 utility functions for primitives */ @@ -214,53 +259,73 @@ library PrimitiveUtils { } } - function encodeCallback(GmpMessage calldata message, bytes32 domainSeparator) + /** + * @dev Converts the `GmpMessage` into a `GmpCallback` struct, which contains all fields from + * `GmpMessage`, plus the EIP-712 id and `IGmpReceiver.onGmpReceived` callback encoded. + * + * This method also prevents copying the `message.data` to memory twice, which is expensive if + * the data is large, using solidity `abi.encode` does the following: + * 1. Copy the `message.data` to memory to compute the `GmpMessage` EIP-712 hash. + * 2. Copy again to encode the `IGmpReceiver.onGmpReceived` callback. + * Instead we copy it once and use the same memory location compute the EIP-712 hash and create + * the `IGmpReceiver.onGmpReceived` callback, unfortunately this requires inline assembly. + * + * @param message GmpMessage from calldata to be encoded + * @param domainSeparator EIP-712 domain separator + * @return callback `GmpCallback` struct + */ + function intoCallback(GmpMessage calldata message, bytes32 domainSeparator) internal pure - returns (bytes32 messageHash, bytes memory r) + returns (GmpCallback memory callback) { bytes calldata data = message.data; /// @solidity memory-safe-assembly assembly { - r := mload(0x40) + callback := mload(0x40) // GmpMessage Type Hash - mstore(add(r, 0x0004), GMP_MESSAGE_TYPE_HASH) - mstore(add(r, 0x0024), calldataload(add(message, 0x00))) // message.source - mstore(add(r, 0x0044), calldataload(add(message, 0x20))) // message.srcNetwork - mstore(add(r, 0x0064), calldataload(add(message, 0x40))) // message.dest - mstore(add(r, 0x0084), calldataload(add(message, 0x60))) // message.destNetwork - mstore(add(r, 0x00a4), calldataload(add(message, 0x80))) // message.gasLimit - mstore(add(r, 0x00c4), calldataload(add(message, 0xa0))) // message.salt + mstore(add(callback, 0x0000), GMP_MESSAGE_TYPE_HASH) + mstore(add(callback, 0x0020), calldataload(add(message, 0x00))) // message.source + mstore(add(callback, 0x0040), calldataload(add(message, 0x20))) // message.srcNetwork + mstore(add(callback, 0x0060), calldataload(add(message, 0x40))) // message.dest + mstore(add(callback, 0x0080), calldataload(add(message, 0x60))) // message.destNetwork + mstore(add(callback, 0x00a0), calldataload(add(message, 0x80))) // message.gasLimit + mstore(add(callback, 0x00c0), calldataload(add(message, 0xa0))) // message.salt // Copy message.data to memory let size := data.length - mstore(add(r, 0x0104), size) // message.data length - calldatacopy(add(r, 0x0124), data.offset, size) // message.data + mstore(add(callback, 0x01c4), size) // message.data.length + calldatacopy(add(callback, 0x01e4), data.offset, size) // message.data // Computed GMP Typed Hash - messageHash := keccak256(add(r, 0x0124), size) // keccak(message.data) - mstore(add(r, 0x00e4), messageHash) - messageHash := keccak256(add(r, 0x04), 0x0100) // GMP eip712 hash + let messageHash := keccak256(add(callback, 0x01e4), size) // keccak(message.data) + mstore(add(callback, 0x00e0), messageHash) + messageHash := keccak256(callback, 0x0100) // GMP eip712 hash mstore(0, 0x1901) mstore(0x20, domainSeparator) mstore(0x40, messageHash) // this will be restored at the end of this function messageHash := keccak256(0x1e, 0x42) // GMP Typed Hash - // onGmpReceived - size := and(add(size, 31), 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) - size := add(size, 0xa4) - mstore(add(r, 0x0064), 0x01900937) // selector - mstore(add(r, 0x0060), size) // length - mstore(add(r, 0x0084), messageHash) // GMP Typed Hash - mstore(add(r, 0x00a4), calldataload(add(message, 0x20))) // msg.network - mstore(add(r, 0x00c4), calldataload(add(message, 0x00))) // msg.source - mstore(add(r, 0x00e4), 0x80) // msg.data offset - - size := and(add(size, 31), 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) - size := add(size, 0x60) - mstore(0x40, add(add(r, size), 0x40)) - r := add(r, 0x60) + // Retore message.data.offset + mstore(add(callback, 0x00e0), add(callback, 0x0120)) + mstore(callback, messageHash) + + // selector + GMP_ID + network + source + data.offset + data.length + size := add(and(add(size, 31), 0xffffffe0), 0xa4) + + // onGmpReceived(bytes32 id, uint128 network, bytes32 source, bytes calldata payload) + mstore(add(callback, 0x0124), 0x01900937) // selector + mstore(add(callback, 0x0120), size) // length + mstore(add(callback, 0x0144), messageHash) // id + mstore(add(callback, 0x0164), calldataload(add(message, 0x20))) // network + mstore(add(callback, 0x0184), calldataload(add(message, 0x00))) // source + mstore(add(callback, 0x01a4), 0x80) // payload.offset + + // update free memory pointer + size := add(and(add(size, 31), 0xffffffe0), 0x0120) + size := and(add(add(callback, size), 31), 0xffffffe0) + mstore(0x40, add(size, 0x40)) } } diff --git a/src/interfaces/IExecutor.sol b/src/interfaces/IExecutor.sol index fe4d7c9..71b18d9 100644 --- a/src/interfaces/IExecutor.sol +++ b/src/interfaces/IExecutor.sol @@ -12,7 +12,7 @@ import { UpdateKeysMessage, UpdateNetworkInfo, GmpSender, - TssKey + Route } from "../Primitives.sol"; /** @@ -42,7 +42,12 @@ interface IExecutor { /** * @dev List all shards currently registered in the gateway. */ - function listShards() external returns (TssKey[] memory); + function shards() external returns (TssKey[] memory); + + /** + * @dev List all shards currently registered in the gateway. + */ + function routes() external returns (Route[] memory); /** * Execute GMP message @@ -53,20 +58,6 @@ interface IExecutor { external returns (GmpStatus status, bytes32 result); - /** - * Update TSS key set - * @param signature Schnorr signature - * @param message Shard's keys to register and revoke - */ - function updateKeys(Signature memory signature, UpdateKeysMessage memory message) external; - - /** - * Update or insert a new network info - * @param signature Schnorr signature - * @param info Network info - */ - function setNetworkInfo(Signature memory signature, UpdateNetworkInfo memory info) external; - /** * Deposit funds to the gateway contract */ diff --git a/src/storage/Routes.sol b/src/storage/Routes.sol index cd48497..7d8decf 100644 --- a/src/storage/Routes.sol +++ b/src/storage/Routes.sol @@ -2,12 +2,13 @@ // Analog's Contracts (last updated v0.1.0) (src/storage/Routes.sol) pragma solidity ^0.8.20; -import {UpdateNetworkInfo, Signature, Network} from "../Primitives.sol"; +import {UpdateNetworkInfo, Signature, Network, Route, MAX_PAYLOAD_SIZE} from "../Primitives.sol"; import {NetworkIDHelpers, NetworkID} from "../NetworkID.sol"; import {EnumerableSet, Pointer} from "../utils/EnumerableSet.sol"; import {BranchlessMath} from "../utils/BranchlessMath.sol"; import {UFloat9x56, UFloatMath} from "../utils/Float9x56.sol"; import {StoragePtr} from "../utils/Pointer.sol"; +import {GasUtils} from "../utils/GasUtils.sol"; /** * @dev EIP-7201 Route's Storage @@ -17,6 +18,7 @@ library RouteStore { using Pointer for uint256; using EnumerableSet for EnumerableSet.Map; using NetworkIDHelpers for NetworkID; + using UFloatMath for UFloat9x56; /** * @dev Namespace of the routes storage `analog.one.gateway.routes`. @@ -24,9 +26,6 @@ library RouteStore { */ bytes32 internal constant _EIP7201_NAMESPACE = 0xb184f2aad520cf7f1f1270909517c75ae33cdf2bd7d32b997a96577f11a48800; - uint8 internal constant SHARD_ACTIVE = (1 << 0); // Shard active bitflag - uint8 internal constant SHARD_Y_PARITY = (1 << 1); // Pubkey y parity bitflag - /** * @dev Network info stored in the Gateway Contract * @param domainSeparator Domain EIP-712 - Replay Protection Mechanism. @@ -42,16 +41,14 @@ library RouteStore { } /** - * @dev Network info stored in the Gateway Contract - * @param id Message unique id. + * @dev Emitted when a route is updated. * @param networkId Network identifier. * @param domainSeparator Domain EIP-712 - Replay Protection Mechanism. * @param relativeGasPrice Gas price of destination chain, in terms of the source chain token. * @param baseFee Base fee for cross-chain message approval on destination, in terms of source native gas token. * @param gasLimit The maximum amount of gas we allow on this particular network. */ - event NetworkUpdated( - bytes32 indexed id, + event RouteUpdated( uint16 indexed networkId, bytes32 indexed domainSeparator, UFloat9x56 relativeGasPrice, @@ -71,8 +68,7 @@ library RouteStore { EnumerableSet.Map routes; } - error ShardAlreadyRegistered(NetworkID id); - error ShardNotExists(NetworkID id); + error RouteNotExists(NetworkID id); error IndexOutOfBounds(uint256 index); function getMainStorage() internal pure returns (MainStorage storage $) { @@ -81,25 +77,15 @@ library RouteStore { } } - function asPtr(NetworkInfo storage keyInfo) internal pure returns (StoragePtr ptr) { - assembly { - ptr := keyInfo.slot - } - } - - function _ptrToRoute(StoragePtr ptr) private pure returns (NetworkInfo storage route) { + /** + * @dev Converts a `StoragePtr` into an `NetworkInfo`. + */ + function pointerToRoute(StoragePtr ptr) private pure returns (NetworkInfo storage route) { assembly { route.slot := ptr } } - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(MainStorage storage store, NetworkInfo storage keyInfo) internal view returns (bool) { - return store.routes.contains(asPtr(keyInfo)); - } - /** * @dev Returns true if the value is in the set. O(1). */ @@ -115,7 +101,7 @@ library RouteStore { */ function getOrAdd(MainStorage storage store, NetworkID id) private returns (bool, NetworkInfo storage) { (bool success, StoragePtr ptr) = store.routes.tryAdd(bytes32(uint256(id.asUint()))); - return (success, _ptrToRoute(ptr)); + return (success, pointerToRoute(ptr)); } /** @@ -149,12 +135,12 @@ library RouteStore { * * - `index` must be strictly less than {length}. */ - function at(MainStorage storage store, uint256 index) internal view returns (NetworkInfo storage) { - StoragePtr ptr = store.routes.at(index); - if (ptr.isNull()) { + function at(MainStorage storage store, uint256 index) internal view returns (NetworkID, NetworkInfo storage) { + (bytes32 key, StoragePtr value) = store.routes.at(index); + if (value.isNull()) { revert IndexOutOfBounds(index); } - return _ptrToRoute(ptr); + return (NetworkID.wrap(uint16(uint256(key))), pointerToRoute(value)); } /** @@ -166,9 +152,9 @@ library RouteStore { function get(MainStorage storage store, NetworkID id) internal view returns (NetworkInfo storage) { StoragePtr ptr = store.routes.get(bytes32(uint256(id.asUint()))); if (ptr.isNull()) { - revert ShardNotExists(id); + revert RouteNotExists(id); } - return _ptrToRoute(ptr); + return pointerToRoute(ptr); } /** @@ -176,66 +162,56 @@ library RouteStore { */ function tryGet(MainStorage storage store, NetworkID id) internal view returns (bool, NetworkInfo storage) { (bool exists, StoragePtr ptr) = store.routes.tryGet(bytes32(uint256(id.asUint()))); - return (exists, _ptrToRoute(ptr)); + return (exists, pointerToRoute(ptr)); } - function createOrUpdateNetworkInfo(MainStorage storage store, bytes32 messageHash, UpdateNetworkInfo calldata info) - private - { - require(info.mortality >= block.number, "message expired"); - - // Verify signature and if the message was already executed - // require(_executedMessages[messageHash] == bytes32(0), "message already executed"); - + function createOrUpdateRoute(MainStorage storage store, Route calldata route) internal { // Update network info - (bool created, NetworkInfo storage stored) = getOrAdd(store, NetworkID.wrap(info.networkId)); - require(!created || info.domainSeparator != bytes32(0), "domain separator cannot be zero"); + (bool created, NetworkInfo storage stored) = getOrAdd(store, route.networkId); + require(!created || stored.domainSeparator != bytes32(0), "domain separator cannot be zero"); // Verify and update domain separator if it's not zero - if (info.domainSeparator != bytes32(0)) { - stored.domainSeparator = info.domainSeparator; + if (route.gateway != bytes32(0)) { + stored.domainSeparator = route.gateway; } // Update gas limit if it's not zero - if (info.gasLimit > 0) { - stored.gasLimit = info.gasLimit; + if (route.gasLimit > 0) { + stored.gasLimit = route.gasLimit; } // Update relative gas price and base fee if any of them are greater than zero - if (UFloat9x56.unwrap(info.relativeGasPrice) > 0 || info.baseFee > 0) { - stored.relativeGasPrice = info.relativeGasPrice; - stored.baseFee = info.baseFee; + if (route.relativeGasPriceDenominator > 0) { + UFloat9x56 relativeGasPrice = + UFloatMath.fromRational(route.relativeGasPriceNumerator, route.relativeGasPriceDenominator); + stored.relativeGasPrice = relativeGasPrice; + stored.baseFee = route.baseFee; } - // Save the message hash to prevent replay attacks - // _executedMessages[messageHash] = executor; - - // Update network info - // _networkInfo[info.networkId] = stored; - - emit NetworkUpdated( - messageHash, - info.networkId, - stored.domainSeparator, - stored.relativeGasPrice, - stored.baseFee, - stored.gasLimit + emit RouteUpdated( + route.networkId.asUint(), stored.domainSeparator, stored.relativeGasPrice, stored.baseFee, stored.gasLimit ); } - // Initialize networks + /** + * @dev Storage initializer function, used to set up the initial storage of the contract. + * @param store Storage location. + * @param networks List of networks to initialize. + * @param networkdID The network id of this chain. + * @param computeDomainSeparator Function to compute the domain separator. + */ function initialize( MainStorage storage store, Network[] calldata networks, NetworkID networkdID, - function(Network calldata) internal pure returns(bytes32) computeDomainSeparator - ) private { + function(NetworkID, address) internal pure returns (bytes32) computeDomainSeparator + ) internal { for (uint256 i = 0; i < networks.length; i++) { Network calldata network = networks[i]; - (bool exists, NetworkInfo storage info) = tryGet(store, NetworkID.wrap(network.id)); - require(!exists && info.domainSeparator == bytes32(0), "network already initialized"); + (bool created, NetworkInfo storage info) = getOrAdd(store, NetworkID.wrap(network.id)); + require(created, "network already initialized"); require(network.id != networkdID.asUint() || network.gateway == address(this), "wrong gateway address"); - info.domainSeparator = computeDomainSeparator(network); + info.domainSeparator = computeDomainSeparator(NetworkID.wrap(network.id), network.gateway); info.gasLimit = 15_000_000; // Default to 15M gas info.relativeGasPrice = UFloatMath.ONE; info.baseFee = 0; @@ -250,12 +226,62 @@ library RouteStore { * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ - function listRoutes(MainStorage storage store) internal view returns (NetworkInfo[] memory) { + function listRoutes(MainStorage storage store) internal view returns (Route[] memory) { bytes32[] memory idx = store.routes.keys; - NetworkInfo[] memory routes = new NetworkInfo[](idx.length); + Route[] memory routes = new Route[](idx.length); for (uint256 i = 0; i < idx.length; i++) { - routes[i] = _ptrToRoute(store.routes.values[idx[i]]); + (bool success, NetworkInfo storage route) = tryGet(store, NetworkID.wrap(uint16(uint256(idx[i])))); + require(success, "route not found"); + (uint256 numerator, uint256 denominator) = route.relativeGasPrice.toRational(); + routes[i] = Route({ + networkId: NetworkID.wrap(uint16(uint256(idx[i]))), + gasLimit: route.gasLimit, + baseFee: route.baseFee, + gateway: route.domainSeparator, + relativeGasPriceNumerator: numerator, + relativeGasPriceDenominator: denominator + }); } return routes; } + + /** + * @dev Check a few preconditions before estimate the GMP wei cost. + */ + function _checkPreconditions(NetworkInfo memory route, uint256 messageSize, uint256 gasLimit) private pure { + // Verify if the network exists + require(route.domainSeparator != bytes32(0), "unsupported route"); + require(route.baseFee > 0 || UFloat9x56.unwrap(route.relativeGasPrice) > 0, "route is temporarily disabled"); + + // Verify if the gas limit and message size are within the limits + require(gasLimit <= route.gasLimit, "gas limit exceeded"); + require(messageSize <= MAX_PAYLOAD_SIZE, "maximum payload size exceeded"); + } + + /** + * @dev Utility function for measure the wei cost of a GMP message. + */ + function estimateWeiCost(NetworkInfo memory route, bytes calldata data, uint256 gasLimit) + internal + pure + returns (uint256) + { + _checkPreconditions(route, data.length, gasLimit); + uint256 nonZeros = GasUtils.countNonZerosCalldata(data); + uint256 zeros = data.length - nonZeros; + return + GasUtils.estimateWeiCost(route.relativeGasPrice, route.baseFee, uint16(nonZeros), uint16(zeros), gasLimit); + } + + /** + * @dev Utility function for measure the wei cost of a GMP message. + */ + function estimateWeiCost(NetworkInfo memory route, uint256 messageSize, uint256 gasLimit) + internal + pure + returns (uint256) + { + _checkPreconditions(route, messageSize, gasLimit); + return GasUtils.estimateWeiCost(route.relativeGasPrice, route.baseFee, uint16(messageSize), 0, gasLimit); + } } diff --git a/src/storage/Shards.sol b/src/storage/Shards.sol index 30dddcc..aa890a1 100644 --- a/src/storage/Shards.sol +++ b/src/storage/Shards.sol @@ -7,6 +7,30 @@ import {EnumerableSet, Pointer} from "../utils/EnumerableSet.sol"; import {BranchlessMath} from "../utils/BranchlessMath.sol"; import {StoragePtr} from "../utils/Pointer.sol"; +library _ShardStore { + function from(uint256 xCoord) internal pure returns (ShardStore.ShardID) { + return ShardStore.ShardID.wrap(bytes32(xCoord)); + } + + /** + * @dev Converts a `StoragePtr` into a `ShardInfo`. + */ + function asShardInfo(StoragePtr ptr) internal pure returns (ShardStore.ShardInfo storage info) { + assembly { + info.slot := ptr + } + } + + /** + * @dev Converts a `ShardInfo` into a `StoragePtr`. + */ + function asPtr(ShardStore.ShardInfo storage info) internal pure returns (StoragePtr ptr) { + assembly { + ptr := info.slot + } + } +} + /** * @dev EIP-7201 Shard's Storage */ @@ -14,6 +38,9 @@ library ShardStore { using Pointer for StoragePtr; using Pointer for uint256; using EnumerableSet for EnumerableSet.Map; + using _ShardStore for uint256; + using _ShardStore for StoragePtr; + using _ShardStore for ShardInfo; /** * @dev Namespace of the shards storage `analog.one.gateway.shards`. @@ -29,15 +56,6 @@ library ShardStore { */ type ShardID is bytes32; - /** - * @dev Current status of the shard - */ - enum ShardStatus { - Unregistered, - Active, - Revoked - } - /** * @dev Shard info stored in the Gateway Contract * OBS: the order of the attributes matters! ethereum storage is 256bit aligned, try to keep @@ -46,9 +64,10 @@ library ShardStore { * * @custom:storage-location erc7201:analog.one.gateway.shards */ - struct KeyInfo { - uint8 status; + struct ShardInfo { + uint8 yParity; uint32 nonce; + uint64 createdAtBlock; } /** @@ -73,25 +92,6 @@ library ShardStore { } } - function asPtr(KeyInfo storage keyInfo) internal pure returns (StoragePtr ptr) { - assembly { - ptr := keyInfo.slot - } - } - - function _getKeyInfo(StoragePtr ptr) private pure returns (KeyInfo storage keyInfo) { - assembly { - keyInfo.slot := ptr - } - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(MainStorage storage store, KeyInfo storage keyInfo) internal view returns (bool) { - return store.shards.contains(asPtr(keyInfo)); - } - /** * @dev Returns true if the value is in the set. O(1). */ @@ -105,25 +105,21 @@ library ShardStore { * Returns true if the value was added to the set, that is if it was not * already present. */ - function getOrAdd(MainStorage storage store, ShardID xCoord) private returns (bool, KeyInfo storage) { + function getOrAdd(MainStorage storage store, ShardID xCoord) private returns (bool, ShardInfo storage) { (bool success, StoragePtr ptr) = store.shards.tryAdd(ShardID.unwrap(xCoord)); - return (success, _getKeyInfo(ptr)); + return (success, ptr.asShardInfo()); } /** * @dev Removes a value from a set. O(1). * - * Returns true if the value was removed from the set, that is if it was - * present. + * Reverts if the value does not exist in the set. */ - function remove(MainStorage storage store, ShardID id) internal returns (bool) { + function remove(MainStorage storage store, ShardID id) internal { StoragePtr ptr = store.shards.remove(ShardID.unwrap(id)); if (ptr.isNull()) { - return false; + revert ShardNotExists(id); } - KeyInfo storage keyInfo = _getKeyInfo(ptr); - keyInfo.status &= ~SHARD_ACTIVE; - return true; } /** @@ -143,12 +139,12 @@ library ShardStore { * * - `index` must be strictly less than {length}. */ - function at(MainStorage storage store, uint256 index) internal view returns (KeyInfo storage) { - StoragePtr ptr = store.shards.at(index); + function at(MainStorage storage store, uint256 index) internal view returns (ShardID, ShardInfo storage) { + (bytes32 xCoord, StoragePtr ptr) = store.shards.at(index); if (ptr.isNull()) { revert IndexOutOfBounds(index); } - return _getKeyInfo(ptr); + return (ShardID.wrap(xCoord), ptr.asShardInfo()); } /** @@ -157,12 +153,12 @@ library ShardStore { * Requirements: * - `key` must be in the map. */ - function get(MainStorage storage store, ShardID key) internal view returns (KeyInfo storage) { + function get(MainStorage storage store, ShardID key) internal view returns (ShardInfo storage) { StoragePtr ptr = store.shards.get(ShardID.unwrap(key)); if (ptr.isNull()) { revert ShardNotExists(key); } - return _getKeyInfo(ptr); + return ptr.asShardInfo(); } /** @@ -171,7 +167,7 @@ library ShardStore { * Requirements: * - `key.xCoord` must be in the map. */ - function get(MainStorage storage store, TssKey calldata key) internal view returns (KeyInfo storage) { + function get(MainStorage storage store, TssKey calldata key) internal view returns (ShardInfo storage) { return get(store, ShardID.wrap(bytes32(key.xCoord))); } @@ -181,59 +177,65 @@ library ShardStore { * Requirements: * - `signature.xCoord` must be in the map. */ - function get(MainStorage storage store, Signature calldata signature) internal view returns (KeyInfo storage) { + function get(MainStorage storage store, Signature calldata signature) internal view returns (ShardInfo storage) { return get(store, ShardID.wrap(bytes32(signature.xCoord))); } /** * @dev Returns the value associated with `key`. O(1). */ - function tryGet(MainStorage storage store, ShardID key) internal view returns (bool, KeyInfo storage) { + function tryGet(MainStorage storage store, ShardID key) private view returns (bool, ShardInfo storage) { (bool exists, StoragePtr ptr) = store.shards.tryGet(ShardID.unwrap(key)); - return (exists, _getKeyInfo(ptr)); + return (exists, ptr.asShardInfo()); } /** - * @dev Register TSS keys. + * @dev Register a single TSS key. + * Requirements: + * - The `newKey` should not be already registered. + */ + function register(MainStorage storage store, TssKey calldata newKey) internal { + // Check y-parity + require(newKey.yParity == (newKey.yParity & 1), "y parity bit must be 0 or 1, cannot register shard"); + + // Read shard from storage + ShardID id = ShardID.wrap(bytes32(newKey.xCoord)); + (bool created, ShardInfo storage stored) = getOrAdd(store, id); + + // Check if the shard is already registered + if (!created) { + revert ShardAlreadyRegistered(id); + } + + // Get the current status and nonce + ShardInfo memory shard = stored; + + require( + shard.createdAtBlock == 0 || shard.yParity == newKey.yParity, + "the provided y-parity doesn't match the existing y-parity, cannot register shard" + ); + + // Update nonce + shard.nonce |= uint32(BranchlessMath.toUint(shard.nonce == 0)); + + // Save new status and nonce in the storage + stored.createdAtBlock = + BranchlessMath.ternaryU64(shard.createdAtBlock > 0, shard.createdAtBlock, uint64(block.number)); + stored.nonce = shard.nonce; + stored.yParity = newKey.yParity; + } + + /** + * @dev Register TSS keys in batch. * Requirements: * - The `keys` should not be already registered. */ - function registerTssKeys(MainStorage storage store, TssKey[] memory keys) internal { + function registerTssKeys(MainStorage storage store, TssKey[] calldata keys) internal { // We don't perform any arithmetic operation, except iterate a loop unchecked { // Register or activate tss key (revoked keys keep the previous nonce) for (uint256 i = 0; i < keys.length; i++) { - TssKey memory newKey = keys[i]; - uint8 yParity = newKey.yParity; - require(yParity == (yParity & 1), "y parity bit must be 0 or 1, cannot register shard"); - - // Read shard from storage - ShardID id = ShardID.wrap(bytes32(newKey.xCoord)); - (bool success, KeyInfo storage shard) = getOrAdd(store, id); - - // Check if the shard is already registered - if (!success) { - revert ShardAlreadyRegistered(id); - } - - uint32 nonce = shard.nonce; - uint8 status = shard.status; - { - uint8 actualYParity = uint8(BranchlessMath.toUint((status & SHARD_Y_PARITY) > 0)); - require( - nonce == 0 || actualYParity == yParity, - "the provided y-parity doesn't match the existing y-parity, cannot register shard" - ); - nonce += uint32(BranchlessMath.toUint(nonce == 0)); - } - - // enable/disable the y-parity flag - status = BranchlessMath.ternaryU8(yParity > 0, status | SHARD_Y_PARITY, status & ~SHARD_Y_PARITY); - status |= SHARD_ACTIVE; - - // Save new status and nonce in the storage - shard.status = status; - shard.nonce = nonce; + register(store, keys[i]); } } } @@ -243,37 +245,27 @@ library ShardStore { * Requirements: * - The `keys` must be registered. */ - function revokeKeys(MainStorage storage store, TssKey[] memory keys) internal { - // We don't perform any arithmetic operation, except iterate a loop - unchecked { - // Revoke tss keys - for (uint256 i = 0; i < keys.length; i++) { - TssKey memory revokedKey = keys[i]; - - // Read shard from storage - ShardID id = ShardID.wrap(bytes32(revokedKey.xCoord)); - KeyInfo storage shard; - { - bool shardExists; - (shardExists, shard) = tryGet(store, id); - - if (!shardExists || shard.nonce == 0) { - revert ShardNotExists(id); - } - } - - // Check y-parity - { - uint8 yParity = (shard.status & SHARD_Y_PARITY) > 0 ? 1 : 0; - require(yParity == revokedKey.yParity, "y parity bit mismatch, cannot revoke key"); - } - - // Disable SHARD_ACTIVE bitflag - shard.status = shard.status & (~SHARD_ACTIVE); // Disable active flag - - // Remove from the set - store.shards.remove(ShardID.unwrap(id)); - } + function revoke(MainStorage storage store, TssKey calldata publicKey) internal { + // Read shard from storage + ShardID id = ShardID.wrap(bytes32(publicKey.xCoord)); + ShardInfo memory shard = get(store, id); + + // Check y-parity + require(shard.yParity == shard.yParity, "y parity mismatch, cannot revoke key"); + + // Remove from the set + store.shards.remove(ShardID.unwrap(id)); + } + + /** + * @dev Revoke TSS keys im batch. + * Requirements: + * - The `publicKeys` must be registered. + */ + function revokeKeys(MainStorage storage store, TssKey[] calldata publicKeys) internal { + // Revoke tss keys + for (uint256 i = 0; i < publicKeys.length; i++) { + revoke(store, publicKeys[i]); } } @@ -287,11 +279,15 @@ library ShardStore { */ function listShards(MainStorage storage store) internal view returns (TssKey[] memory) { bytes32[] memory idx = store.shards.keys; - TssKey[] memory keys = new TssKey[](idx.length); + TssKey[] memory shards = new TssKey[](idx.length); for (uint256 i = 0; i < idx.length; i++) { - KeyInfo storage keyInfo = _getKeyInfo(store.shards.values[idx[i]]); - keys[i] = TssKey(keyInfo.status & SHARD_Y_PARITY, uint256(idx[i])); + ShardID id = ShardID.wrap(idx[i]); + (bool success, ShardInfo storage shard) = tryGet(store, id); + if (!success) { + revert ShardNotExists(id); + } + shards[i] = TssKey(shard.yParity, uint256(ShardID.unwrap(id))); } - return keys; + return shards; } } diff --git a/src/utils/EnumerableSet.sol b/src/utils/EnumerableSet.sol index cff2a74..3055eed 100644 --- a/src/utils/EnumerableSet.sol +++ b/src/utils/EnumerableSet.sol @@ -167,15 +167,14 @@ library EnumerableSet { * * - `index` must be strictly less than {length}. */ - function at(Map storage map, uint256 index) internal view returns (StoragePtr r) { + function at(Map storage map, uint256 index) internal view returns (bytes32 key, StoragePtr r) { assembly ("memory-safe") { mstore(0x00, map.slot) - let key := sload(add(keccak256(0x00, 0x20), index)) + key := sload(add(keccak256(0x00, 0x20), index)) mstore(0x00, key) mstore(0x20, add(map.slot, 1)) r := keccak256(0x00, 0x40) - key := not(sload(sub(r, 1))) - r := mul(r, and(lt(index, sload(map.slot)), eq(index, key))) + r := mul(r, and(lt(index, sload(map.slot)), eq(index, not(sload(sub(r, 1)))))) } } diff --git a/src/utils/GasUtils.sol b/src/utils/GasUtils.sol index 0d5f12c..ac618f8 100644 --- a/src/utils/GasUtils.sol +++ b/src/utils/GasUtils.sol @@ -13,15 +13,30 @@ library GasUtils { /** * @dev Base cost of the `IExecutor.execute` method. */ - uint256 internal constant EXECUTION_BASE_COST = 44469 + 2245 - 11; + uint256 internal constant EXECUTION_BASE_COST = 46658; + + /** + * @dev Initial amount of memory used by `IExecutor.execute` method. + */ + uint256 internal constant MEMORY_OFFSET = 0x3c0; /** * @dev Base cost of the `IGateway.submitMessage` method. */ - uint256 internal constant SUBMIT_BASE_COST = 23181; + uint256 internal constant SUBMIT_BASE_COST = 25802; using BranchlessMath for uint256; + /** + * @dev Compute the gas cost of memory expansion. + * @param words number of words, where a word is 32 bytes + */ + function memoryExpansionGasCost(uint256 words) internal pure returns (uint256) { + unchecked { + return (words.saturatingMul(words) >> 9).saturatingAdd(words.saturatingMul(3)); + } + } + /** * @dev Compute the amount of gas used by the `GatewayProxy`. * @param calldataLen The length of the calldata in bytes @@ -44,7 +59,7 @@ library GasUtils { // MEMORY EXPANSION uint256 words = BranchlessMath.max(calldataLen, returnLen); - gasCost = gasCost.saturatingAdd((words.saturatingMul(words) >> 9).saturatingAdd(words * 3)); + gasCost = gasCost.saturatingAdd(memoryExpansionGasCost(words)); return gasCost; } } @@ -71,7 +86,7 @@ library GasUtils { gasCost += words << 8; // Memory expansion cost - words += 13; + words += 13 + 4; gasCost += ((words * words) >> 9) + (words * 3); return gasCost; @@ -186,9 +201,10 @@ library GasUtils { // Memory expansion cost words = 0xa4 + (words << 5); // onGmpReceived encoded call size words = (words + 31) & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0; - words += 0x0200; // Memory size + words += MEMORY_OFFSET; // Memory size words = (words + 31) >> 5; // to words gas = gas.saturatingAdd(((words * words) >> 9) + (words * 3)); + return gas; } } @@ -197,7 +213,7 @@ library GasUtils { * @dev Compute the inverse of `N - floor(N / 64)` defined by EIP-150, used to * compute the gas needed for a transaction. */ - function _inverseOfAllButOne64th(uint256 x) private pure returns (uint256 inverse) { + function inverseOfAllButOne64th(uint256 x) internal pure returns (uint256 inverse) { unchecked { // inverse = (x * 64) / 63 inverse = x.saturatingShl(6).saturatingDiv(63); @@ -213,10 +229,10 @@ library GasUtils { */ function executionGasNeeded(uint256 messageSize, uint256 gasLimit) internal pure returns (uint256 gasNeeded) { unchecked { - gasNeeded = _inverseOfAllButOne64th(gasLimit); + gasNeeded = inverseOfAllButOne64th(gasLimit); gasNeeded = gasNeeded.saturatingAdd(_executionGasCost(messageSize, gasLimit)); - gasNeeded = gasNeeded.saturatingAdd(2114); - gasNeeded = _inverseOfAllButOne64th(gasNeeded); + gasNeeded = gasNeeded.saturatingAdd(2114 + 2); + gasNeeded = inverseOfAllButOne64th(gasNeeded); messageSize = (uint256(messageSize).saturatingAdd(31) >> 5) << 5; messageSize = messageSize.saturatingAdd(388); gasNeeded = gasNeeded.saturatingAdd(proxyOverheadGasCost(messageSize, 64)); diff --git a/test/EnumerableSet.t.sol b/test/EnumerableSet.t.sol index 3ad112f..ebdeb7d 100644 --- a/test/EnumerableSet.t.sol +++ b/test/EnumerableSet.t.sol @@ -45,7 +45,8 @@ contract EnumerableSetTest is Test { } function _at(uint256 index, bool success) private view returns (MyStruct storage r) { - bytes32 ptr = map.at(index).asBytes32(); + (, StoragePtr raw) = map.at(index); + bytes32 ptr = raw.asBytes32(); if (success) { assertNotEq(ptr, bytes32(0), "map.at failed"); assembly { @@ -189,8 +190,10 @@ contract EnumerableSetTest is Test { assertEq(index, 0, "unexpected index"); // Map.at works - store = map.at(0).getUint256Slot(); + (bytes32 atKey, StoragePtr raw) = map.at(0); + store = raw.getUint256Slot(); assertEq(store.value, value, "unexpected value when retrieving by index"); + assertEq(atKey, key, "unexpected key when retrieving by index"); // Map.contains works StoragePtr ptr = map.get(key); diff --git a/test/GasUtils.t.sol b/test/GasUtils.t.sol index 5c75d7d..08fdcd0 100644 --- a/test/GasUtils.t.sol +++ b/test/GasUtils.t.sol @@ -229,22 +229,22 @@ contract GasUtilsBase is Test { function test_gasUtils() external pure { uint256 baseCost = GasUtils.EXECUTION_BASE_COST; - assertEq(GasUtils.estimateGas(0, 0, 0), 31739 + baseCost); - assertEq(GasUtils.estimateGas(0, 33, 0), 32112 + baseCost); - assertEq(GasUtils.estimateGas(33, 0, 0), 32772 + baseCost); - assertEq(GasUtils.estimateGas(20, 13, 0), 32512 + baseCost); + assertEq(GasUtils.estimateGas(0, 0, 0), 31783 + baseCost); + assertEq(GasUtils.estimateGas(0, 33, 0), 32155 + baseCost); + assertEq(GasUtils.estimateGas(33, 0, 0), 32815 + baseCost); + assertEq(GasUtils.estimateGas(20, 13, 0), 32555 + baseCost); UFloat9x56 one = UFloatMath.ONE; - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 0, 0), 31739 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 32112 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32772 + baseCost); - assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32512 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 0, 0), 31783 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 0, 33, 0), 32155 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 33, 0, 0), 32815 + baseCost); + assertEq(GasUtils.estimateWeiCost(one, 0, 20, 13, 0), 32555 + baseCost); UFloat9x56 two = UFloat9x56.wrap(0x8080000000000000); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 0, 0), (31739 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (32112 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32772 + baseCost) * 2); - assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32512 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 0, 0), (31783 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 0, 33, 0), (32155 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 33, 0, 0), (32815 + baseCost) * 2); + assertEq(GasUtils.estimateWeiCost(two, 0, 20, 13, 0), (32555 + baseCost) * 2); } } diff --git a/test/Gateway.t.sol b/test/Gateway.t.sol index 79eb0e5..3c645e3 100644 --- a/test/Gateway.t.sol +++ b/test/Gateway.t.sol @@ -137,10 +137,9 @@ contract GatewayBase is Test { bytes32 private _srcDomainSeparator; bytes32 private _dstDomainSeparator; - uint256 private constant SUBMIT_GAS_COST = 15034; + // Netowrk ids uint16 private constant SRC_NETWORK_ID = 1234; uint16 internal constant DEST_NETWORK_ID = 1337; - uint8 private constant GMP_STATUS_SUCCESS = 1; constructor() { signer = new Signer(secret); @@ -216,7 +215,7 @@ contract GatewayBase is Test { function test_estimateMessageCost() external { vm.txGasPrice(1); uint256 cost = gateway.estimateMessageCost(DEST_NETWORK_ID, 96, 100000); - assertEq(cost, GasUtils.EXECUTION_BASE_COST + 134032); + assertEq(cost, GasUtils.EXECUTION_BASE_COST + 134075); } function test_checkPayloadSize() external { @@ -627,7 +626,7 @@ contract GatewayBase is Test { id, GmpSender.unwrap(gmp.source), gmp.dest, gmp.destNetwork, gmp.gasLimit, gmp.salt, gmp.data ); assertEq(ctx.submitMessage(gmp), id, "unexpected GMP id"); - assertEq(ctx.executionCost, expectedCost - 6800, "unexpected execution gas cost"); + assertEq(ctx.executionCost, expectedCost - 8800, "unexpected execution gas cost"); } }