diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e13782c..ccb2d090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `outputsEpochRootHash` field to `OutputValidityProof` + ### Changed -- Encode outputs with `abi.encodeWithSignature` +- Merged the tree of vouchers and the tree of notices into one tree of outputs +- Updated the encoding of outputs, to use `abi.encodeWithSignature` instead of `abi.encode`, in order to add a 4-byte header + +### Removed + +- `vouchersEpochRootHash` and `noticesEpochRootHash` fields from `OutputValidityProof` ## [1.0.0] 2023-08-22 diff --git a/onchain/rollups/contracts/library/LibOutputValidation.sol b/onchain/rollups/contracts/library/LibOutputValidation.sol index c9a48b70..9dbad7e6 100644 --- a/onchain/rollups/contracts/library/LibOutputValidation.sol +++ b/onchain/rollups/contracts/library/LibOutputValidation.sol @@ -7,26 +7,117 @@ import {CanonicalMachine} from "../common/CanonicalMachine.sol"; import {MerkleV2} from "@cartesi/util/contracts/MerkleV2.sol"; import {OutputEncoding} from "../common/OutputEncoding.sol"; -/// @param inputIndexWithinEpoch Which input, inside the epoch, the output belongs to -/// @param outputIndexWithinInput Index of output emitted by the input -/// @param outputHashesRootHash Merkle root of hashes of outputs emitted by the input -/// @param vouchersEpochRootHash Merkle root of all epoch's voucher metadata hashes -/// @param noticesEpochRootHash Merkle root of all epoch's notice metadata hashes -/// @param machineStateHash Hash of the machine state claimed this epoch -/// @param outputHashInOutputHashesSiblings Proof that this output metadata is in metadata memory range -/// @param outputHashesInEpochSiblings Proof that this output metadata is in epoch's output memory range +/// @param inputIndexWithinEpoch Which input, inside the epoch, the output belongs to (G) +/// @param outputIndexWithinInput Index of output emitted by the input (D) +/// @param outputHashesRootHash Merkle root of hashes of outputs emitted by the input (F) +/// @param outputsEpochRootHash Merkle root of all epoch's voucher metadata hashes (I) +/// @param machineStateHash Hash of the machine state claimed this epoch (J) +/// @param outputHashInOutputHashesSiblings Proof that this output metadata is in metadata memory range (E) +/// @param outputHashesInEpochSiblings Proof that this output metadata is in epoch's output memory range (H) struct OutputValidityProof { uint64 inputIndexWithinEpoch; uint64 outputIndexWithinInput; bytes32 outputHashesRootHash; - bytes32 vouchersEpochRootHash; - bytes32 noticesEpochRootHash; + bytes32 outputsEpochRootHash; bytes32 machineStateHash; bytes32[] outputHashInOutputHashesSiblings; bytes32[] outputHashesInEpochSiblings; } /// @title Output Validation Library +/// +/// @notice The diagram below aims to better illustrate the algorithm +/// behind calculating the epoch hash. Each component in the diagram is +/// labeled, so it can be more easily referenced in this documentation. +/// The diagram is laid out in a top-down fashion, with the epoch hash +/// being the top-most component, but the text will traverse the diagram +/// in a bottom-up fashion. +/// +/// The off-chain machine may, while processing an input, generate +/// zero or more outputs. For the scope of this algorithm, let us assume +/// that an output is an arbitrary array of bytes. +/// +/// Every output is hashed into a 256-bit word (A), which is then +/// divided into four 64-bit words. From these words, a Merkle tree +/// is constructed from the bottom up (B). The result is a Merkle +/// root hash (C) that fully represents the contents of the output. +/// +/// Now, this process is repeated for every output generated by an input. +/// The Merkle root hashes (C) derived from these outputs are then ordered +/// from oldest to newest (D), and used to construct yet another Merkle tree +/// (E). The result is a Merkle root hash (F) that fully represents the +/// contents of every output generated by an input. +/// +/// We once again repeat this process, but now for every input accepted in a +/// certain epoch. Then, we organize the Merkle root hashes (F) calculated +/// in the previous step and order them from oldest to newest (G). From these +/// Merkle root hashes (F), we build a final Merkle tree (H). The result is +/// a Merkle root hash (I) that fully represents the contents of every output +/// generated by every input in a certain epoch. +/// +/// Finally, this Merkle root hash (I) is combined with the machine state +/// hash (J) to obtain the epoch hash (K). +/// +/// ``` +/// ┌──────────────┐ +/// ┌─────────┤Epoch Hash (K)├────────┐ +/// │ └──────────────┘ │ +/// │ │ +/// │ │ +/// │ ┌──────────▼───────────┐ +/// │ │Machine State Hash (J)│ +/// │ └──────────────────────┘ +/// ┌─────▼─────┐ +/// │Merkle Root│ ───> Epoch's output hashes root hash (I) +/// └───────────┘ +/// x +/// xxx │ +/// xxxxx │ +/// xxxxxxx │ +/// xxxxxxxxx ├──> Epoch's outputs Merkle tree (H) +/// xxxxxxxxxxx │ +/// xxxxxxxxxxxxx │ +/// xxxxxxxxxxxxxxx │ +/// xxxxxxxxxxxxxxxxx +/// ┌────────┬─┬────────┐ +/// │ ... │┼│ ... │ ───> For each input in the epoch (G) +/// └────────┴┼┴────────┘ +/// │ +/// │ +/// ┌─────▼─────┐ +/// │Merkle Root│ ───> Input's output hashes Merkle root hash (F) +/// └───────────┘ +/// x +/// xxx │ +/// xxxxx │ +/// xxxxxxx │ +/// xxxxxxxxx ├──> Input's outputs Merkle tree (E) +/// xxxxxxxxxxx │ +/// xxxxxxxxxxxxx │ +/// xxxxxxxxxxxxxxx │ +/// xxxxxxxxxxxxxxxxx +/// ┌────────┬─┬────────┐ +/// │ ... │┼│ ... │ ───> For each output from the input (D) +/// └────────┴┼┴────────┘ +/// │ +/// │ +/// ┌─────▼─────┐ +/// │Merkle Root│ ───> Output hash Merkle root hash (C) +/// └───────────┘ +/// x +/// x x │ +/// x x │ +/// x x │ +/// x x ├──> Output hash Merkle tree (B) +/// x x │ +/// x x x x │ +/// x x x x │ +/// x x x x +/// ┌────┬────┬────┬────┐ +/// │ │ │ │ │ ───> Output hash (A) +/// └────┴────┴────┴────┘ +/// ``` +/// library LibOutputValidation { using CanonicalMachine for CanonicalMachine.Log2Size; @@ -47,25 +138,19 @@ library LibOutputValidation { error InputIndexOutOfClaimBounds(); /// @notice Make sure the output proof is valid, otherwise revert. - /// @param v The output validity proof - /// @param encodedOutput The encoded output - /// @param epochHash The hash of the epoch in which the output was generated - /// @param outputsEpochRootHash Either `v.vouchersEpochRootHash` (for vouchers) - /// or `v.noticesEpochRootHash` (for notices) - function validateEncodedOutput( + /// @param v The output validity proof (D..J) + /// @param output The output (which, when ABI-encoded and Keccak256-hashed, becomes A, + /// and, when Merkelized, generates tree B and root hash C) + /// @param epochHash The hash of the epoch in which the output was generated (K) + function validateOutput( OutputValidityProof calldata v, - bytes memory encodedOutput, - bytes32 epochHash, - bytes32 outputsEpochRootHash + bytes memory output, + bytes32 epochHash ) internal pure { // prove that outputs hash is represented in a finalized epoch if ( keccak256( - abi.encodePacked( - v.vouchersEpochRootHash, - v.noticesEpochRootHash, - v.machineStateHash - ) + abi.encodePacked(v.outputsEpochRootHash, v.machineStateHash) ) != epochHash ) { revert IncorrectEpochHash(); @@ -82,7 +167,7 @@ library LibOutputValidation { CanonicalMachine.EPOCH_OUTPUT_LOG2_SIZE.uint64OfSize(), v.outputHashesRootHash, v.outputHashesInEpochSiblings - ) != outputsEpochRootHash + ) != v.outputsEpochRootHash ) { revert IncorrectOutputsEpochRootHash(); } @@ -105,7 +190,7 @@ library LibOutputValidation { // is contained in it. We can't simply use hashOfOutput because the // log2size of the leaf is three (8 bytes) not five (32 bytes) bytes32 merkleRootOfHashOfOutput = MerkleV2.getMerkleRootFromBytes( - abi.encodePacked(keccak256(encodedOutput)), + abi.encodePacked(keccak256(abi.encode(output))), CanonicalMachine.KECCAK_LOG2_SIZE.uint64OfSize() ); @@ -128,44 +213,33 @@ library LibOutputValidation { } /// @notice Make sure the output proof is valid, otherwise revert. - /// @param v The output validity proof + /// @param v The output validity proof (D..J) /// @param destination The address that will receive the payload through a message call /// @param payload The payload, which—in the case of Solidity contracts—encodes a function call - /// @param epochHash The hash of the epoch in which the output was generated + /// @param epochHash The hash of the epoch in which the output was generated (K) function validateVoucher( OutputValidityProof calldata v, address destination, bytes calldata payload, bytes32 epochHash ) internal pure { - bytes memory encodedVoucher = OutputEncoding.encodeVoucher( - destination, - payload - ); - validateEncodedOutput( + validateOutput( v, - encodedVoucher, - epochHash, - v.vouchersEpochRootHash + OutputEncoding.encodeVoucher(destination, payload), + epochHash ); } /// @notice Make sure the output proof is valid, otherwise revert. - /// @param v The output validity proof + /// @param v The output validity proof (D..J) /// @param notice The notice - /// @param epochHash The hash of the epoch in which the output was generated + /// @param epochHash The hash of the epoch in which the output was generated (K) function validateNotice( OutputValidityProof calldata v, bytes calldata notice, bytes32 epochHash ) internal pure { - bytes memory encodedNotice = OutputEncoding.encodeNotice(notice); - validateEncodedOutput( - v, - encodedNotice, - epochHash, - v.noticesEpochRootHash - ); + validateOutput(v, OutputEncoding.encodeNotice(notice), epochHash); } /// @notice Get the position of a voucher on the bit mask.