Skip to content

Commit

Permalink
feat!: unify output trees
Browse files Browse the repository at this point in the history
* The epoch hash used to be composed by three Merkle trees: one for the
  vouchers generated in the epoch, another for the notices generated in
  the epoch, and a third one for the machine address space as a whole.
  This commit unifies vouchers and notices in the same Merkle tree,
  making the epoch hash composed now of only two Merkle trees: one for
  the outputs generated in the epoch, and another for the machine.

* The leaves of this new tree represent any type of output: voucher,
  notice, or any other output that we might come up with in the future.
  Their binary representations are disjoint because of a 4-byte header.

* This unification greatly simplifies the output validation algorithm,
  and removes a 256-bit field from the `OutputValidityProof` struct.

* Add text to `LibOutputValidation` that explains how the epoch hash
  is calculated, with the help of an ASCII diagram
  • Loading branch information
guidanoli committed Aug 28, 2023
1 parent b85edc5 commit 9048099
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 46 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
164 changes: 119 additions & 45 deletions onchain/rollups/contracts/library/LibOutputValidation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand All @@ -82,7 +167,7 @@ library LibOutputValidation {
CanonicalMachine.EPOCH_OUTPUT_LOG2_SIZE.uint64OfSize(),
v.outputHashesRootHash,
v.outputHashesInEpochSiblings
) != outputsEpochRootHash
) != v.outputsEpochRootHash
) {
revert IncorrectOutputsEpochRootHash();
}
Expand All @@ -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()
);

Expand All @@ -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.
Expand Down

0 comments on commit 9048099

Please sign in to comment.