diff --git a/onchain/rollups/.changeset/mean-news-float.md b/onchain/rollups/.changeset/mean-news-float.md new file mode 100644 index 00000000..9745b757 --- /dev/null +++ b/onchain/rollups/.changeset/mean-news-float.md @@ -0,0 +1,7 @@ +--- +"@cartesi/rollups": major +--- + +Inputs are now blockchain-agnostic and self-contained blobs. For example, inputs added by EVM contracts like `InputBox` contain EVM-specific metadata like `msg.sender` and `block.timestamp`. + +Library `LibInput` is removed. diff --git a/onchain/rollups/contracts/common/Inputs.sol b/onchain/rollups/contracts/common/Inputs.sol new file mode 100644 index 00000000..6f125c58 --- /dev/null +++ b/onchain/rollups/contracts/common/Inputs.sol @@ -0,0 +1,22 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +/// @title Inputs +/// @notice Defines the signatures of inputs. +interface Inputs { + /// @notice An EVM input. + /// @param sender `msg.sender` + /// @param blockNumber `block.number` + /// @param blockTimestamp `block.timestamp` + /// @param inputIndex The index of the input in the input box + /// @param input The input payload + function EvmInput( + address sender, + uint256 blockNumber, + uint256 blockTimestamp, + uint256 inputIndex, + bytes calldata input + ) external; +} diff --git a/onchain/rollups/contracts/inputs/IInputBox.sol b/onchain/rollups/contracts/inputs/IInputBox.sol index 521de8e5..c6a38469 100644 --- a/onchain/rollups/contracts/inputs/IInputBox.sol +++ b/onchain/rollups/contracts/inputs/IInputBox.sol @@ -9,7 +9,7 @@ interface IInputBox { /// @param dapp The address of the DApp /// @param inputIndex The index of the input in the input box /// @param sender The address that sent the input - /// @param input The contents of the input + /// @param input The input payload /// @dev MUST be triggered on a successful call to `addInput`. event InputAdded( address indexed dapp, @@ -20,7 +20,7 @@ interface IInputBox { /// @notice Add an input to a DApp's input box. /// @param _dapp The address of the DApp - /// @param _input The contents of the input + /// @param _input The input payload /// @return The hash of the input plus some extra metadata /// @dev MUST fire an `InputAdded` event accordingly. /// Input larger than machine limit will raise `InputSizeExceedsLimit` error. diff --git a/onchain/rollups/contracts/inputs/InputBox.sol b/onchain/rollups/contracts/inputs/InputBox.sol index 6999a195..3ae74004 100644 --- a/onchain/rollups/contracts/inputs/InputBox.sol +++ b/onchain/rollups/contracts/inputs/InputBox.sol @@ -4,16 +4,17 @@ pragma solidity ^0.8.8; import {IInputBox} from "./IInputBox.sol"; -import {LibInput} from "../library/LibInput.sol"; +import {CanonicalMachine} from "../common/CanonicalMachine.sol"; +import {Inputs} from "../common/Inputs.sol"; /// @title Input Box /// -/// @notice Trustless and permissionless contract that receives arbitrary blobs -/// (called "inputs") from anyone and adds a compound hash to an append-only list +/// @notice Trustless and permissionless contract that receives arbitrary +/// data from anyone and adds a compound hash to an append-only list /// (called "input box"). Each DApp has its own input box. /// -/// The hash that is stored on-chain is composed by the hash of the input blob, -/// the block number and timestamp, the input sender address, and the input index. +/// The input hash is composed by the input payload, the block number and timestamp, +/// the address of the input sender, and the index of the input. /// /// Data availability is guaranteed by the emission of `InputAdded` events /// on every successful call to `addInput`. This ensures that inputs can be @@ -23,6 +24,11 @@ import {LibInput} from "../library/LibInput.sol"; /// From the perspective of this contract, inputs are encoding-agnostic byte /// arrays. It is up to the DApp to interpret, validate and act upon inputs. contract InputBox is IInputBox { + using CanonicalMachine for CanonicalMachine.Log2Size; + + /// @notice Raised when input is larger than the machine limit. + error InputSizeExceedsLimit(); + /// @notice Mapping from DApp address to list of input hashes. /// @dev See the `getNumberOfInputs`, `getInputHash` and `addInput` functions. mapping(address => bytes32[]) internal inputBoxes; @@ -34,14 +40,17 @@ contract InputBox is IInputBox { bytes32[] storage inputBox = inputBoxes[_dapp]; uint256 inputIndex = inputBox.length; - bytes32 inputHash = LibInput.computeInputHash( - msg.sender, - block.number, - block.timestamp, - _input, - inputIndex + bytes memory input = abi.encodeCall( + Inputs.EvmInput, + (msg.sender, block.number, block.timestamp, inputIndex, _input) ); + if (input.length > CanonicalMachine.INPUT_MAX_SIZE) { + revert InputSizeExceedsLimit(); + } + + bytes32 inputHash = keccak256(input); + // add input to the input box inputBox.push(inputHash); diff --git a/onchain/rollups/contracts/library/LibInput.sol b/onchain/rollups/contracts/library/LibInput.sol deleted file mode 100644 index 1cae79cb..00000000 --- a/onchain/rollups/contracts/library/LibInput.sol +++ /dev/null @@ -1,47 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.8; - -import {CanonicalMachine} from "../common/CanonicalMachine.sol"; - -/// @title Input Library -library LibInput { - using CanonicalMachine for CanonicalMachine.Log2Size; - - /// @notice Raised when input is larger than the machine limit. - error InputSizeExceedsLimit(); - - /// @notice Summarize input data in a single hash. - /// @param sender `msg.sender` - /// @param blockNumber `block.number` - /// @param blockTimestamp `block.timestamp` - /// @param input The input blob - /// @param inputIndex The index of the input in the input box - /// @return The input hash - function computeInputHash( - address sender, - uint256 blockNumber, - uint256 blockTimestamp, - bytes calldata input, - uint256 inputIndex - ) internal pure returns (bytes32) { - if (input.length > CanonicalMachine.INPUT_MAX_SIZE) { - revert InputSizeExceedsLimit(); - } - - bytes32 keccakMetadata = keccak256( - abi.encode( - sender, - blockNumber, - blockTimestamp, - 0, //TODO decide how to deal with epoch index - inputIndex // input index in the input box - ) - ); - - bytes32 keccakInput = keccak256(input); - - return keccak256(abi.encode(keccakMetadata, keccakInput)); - } -} diff --git a/onchain/rollups/test/foundry/inputs/InputBox.t.sol b/onchain/rollups/test/foundry/inputs/InputBox.t.sol index d8902bee..d54700b3 100644 --- a/onchain/rollups/test/foundry/inputs/InputBox.t.sol +++ b/onchain/rollups/test/foundry/inputs/InputBox.t.sol @@ -8,7 +8,7 @@ import {Test} from "forge-std/Test.sol"; import {InputBox} from "contracts/inputs/InputBox.sol"; import {IInputBox} from "contracts/inputs/IInputBox.sol"; import {CanonicalMachine} from "contracts/common/CanonicalMachine.sol"; -import {LibInput} from "contracts/library/LibInput.sol"; +import {Inputs} from "contracts/common/Inputs.sol"; contract InputBoxHandler is Test { IInputBox immutable inputBox; @@ -98,12 +98,11 @@ contract InputBoxHandler is Test { ); // Compute the input hash from the arguments passed to `addInput` - bytes32 computedInputHash = LibInput.computeInputHash( - msg.sender, - block.number, - block.timestamp, - _input, - index + bytes32 computedInputHash = keccak256( + abi.encodeCall( + Inputs.EvmInput, + (msg.sender, block.number, block.timestamp, index, _input) + ) ); // Check if the input hash matches the computed one @@ -161,10 +160,12 @@ contract InputBoxTest is Test { function testAddLargeInput() public { address dapp = vm.addr(1); - inputBox.addInput(dapp, new bytes(CanonicalMachine.INPUT_MAX_SIZE)); + uint256 maxLength = getMaxInputPayloadLength(); - vm.expectRevert(LibInput.InputSizeExceedsLimit.selector); - inputBox.addInput(dapp, new bytes(CanonicalMachine.INPUT_MAX_SIZE + 1)); + inputBox.addInput(dapp, new bytes(maxLength)); + + vm.expectRevert(InputBox.InputSizeExceedsLimit.selector); + inputBox.addInput(dapp, new bytes(maxLength + 1)); } // fuzz testing with multiple inputs @@ -175,7 +176,7 @@ contract InputBoxTest is Test { // assume #bytes for each input is within bounds for (uint256 i; i < numInputs; ++i) { - vm.assume(_inputs[i].length <= CanonicalMachine.INPUT_MAX_SIZE); + vm.assume(_inputs[i].length <= getMaxInputPayloadLength()); } // adding inputs @@ -199,12 +200,17 @@ contract InputBoxTest is Test { // testing added inputs for (uint256 i; i < numInputs; ++i) { // compute input hash for each input - bytes32 inputHash = LibInput.computeInputHash( - address(this), - i, // block.number - i + year2022, // block.timestamp - _inputs[i], - i // inputBox.length + bytes32 inputHash = keccak256( + abi.encodeCall( + Inputs.EvmInput, + ( + address(this), + i, // block.number + i + year2022, // block.timestamp + i, // inputBox.length + _inputs[i] + ) + ) ); // test if input hash is the same as in InputBox assertEq(inputHash, inputBox.getInputHash(_dapp, i)); @@ -253,4 +259,15 @@ contract InputBoxTest is Test { } assertEq(sum, totalNumOfInputs, "total number of inputs"); } + + function getMaxInputPayloadLength() internal pure returns (uint256) { + bytes memory blob = abi.encodeCall( + Inputs.EvmInput, + (address(0), 0, 0, 0, new bytes(32)) + ); + // number of bytes in input blob excluding input payload + uint256 extraBytes = blob.length - 32; + // because it's abi encoded, input payloads are stored as multiples of 32 bytes + return ((CanonicalMachine.INPUT_MAX_SIZE - extraBytes) / 32) * 32; + } }