From 1cadafeb62ed49162d19ef10d50930004e3f6df6 Mon Sep 17 00:00:00 2001 From: Mitch Date: Mon, 30 Sep 2024 09:04:32 -0400 Subject: [PATCH 1/7] Introduces an escrow contract that is able to store test tokens from provers. The implementation allows the rollup contract to stake deposited bonds, and unstake them. Provers may initiate withdraws which are not executable until 3 epochs after their initiation. Proposers will use the minBalanceAtTime function to ensure that a prover has sufficient funds at the slot they would "cash in" the quote. Additionally, the signatures used on the epoch proof quotes have been updated to use EIP 712 style signing and verification. --- .vscode/settings.json | 4 +- l1-contracts/.solhint.json | 47 +-- .../src/core/ProofCommitmentEscrow.sol | 153 +++++++++ l1-contracts/src/core/Rollup.sol | 10 +- .../interfaces/IProofCommitmentEscrow.sol | 11 +- l1-contracts/src/core/interfaces/IRollup.sol | 7 +- .../src/core/libraries/DataStructures.sol | 32 +- .../src/core/libraries/EpochProofQuoteLib.sol | 64 ++++ l1-contracts/src/core/libraries/Errors.sol | 5 + .../src/core/libraries/crypto/EIP712Lib.sol | 27 ++ .../src/mock/MockProofCommitmentEscrow.sol | 12 +- l1-contracts/test/Rollup.t.sol | 9 +- .../test/prover-coordination/EscrowERC20.sol | 15 + .../ProofCommitmentEscrow.t.sol | 318 ++++++++++++++++++ .../test/prover-coordination/Signatures.t.sol | 55 +++ .../circuit-types/src/domain_separator.ts | 10 + .../circuit-types/src/p2p/signature_utils.ts | 8 + .../epoch_proof_quote.test.ts | 26 +- .../prover_coordination/epoch_proof_quote.ts | 6 +- .../epoch_proof_quote_payload.ts | 13 +- .../foundation/src/crypto/keccak/index.ts | 7 +- 21 files changed, 748 insertions(+), 91 deletions(-) create mode 100644 l1-contracts/src/core/ProofCommitmentEscrow.sol create mode 100644 l1-contracts/src/core/libraries/EpochProofQuoteLib.sol create mode 100644 l1-contracts/src/core/libraries/crypto/EIP712Lib.sol create mode 100644 l1-contracts/test/prover-coordination/EscrowERC20.sol create mode 100644 l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol create mode 100644 l1-contracts/test/prover-coordination/Signatures.t.sol create mode 100644 yarn-project/circuit-types/src/domain_separator.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 3790d618e42..2e0e5227291 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -107,7 +107,7 @@ "*.macros": "cpp", "*.tpp": "cpp" }, - "solidity.compileUsingRemoteVersion": "v0.8.18", + "solidity.compileUsingRemoteVersion": "v0.8.27", "solidity.formatter": "forge", "search.exclude": { "**/.yarn": true, @@ -171,5 +171,5 @@ }, "files.trimTrailingWhitespace": true, "cmake.sourceDirectory": "${workspaceFolder}/barretenberg/cpp", - "typescript.tsserver.maxTsServerMemory": 4096, + "typescript.tsserver.maxTsServerMemory": 4096 } diff --git a/l1-contracts/.solhint.json b/l1-contracts/.solhint.json index d4a30c78520..2acdeeb2a60 100644 --- a/l1-contracts/.solhint.json +++ b/l1-contracts/.solhint.json @@ -1,10 +1,7 @@ { "extends": "solhint:recommended", "rules": { - "compiler-version": [ - "error", - ">=0.8.27" - ], + "compiler-version": ["error", ">=0.8.27"], "no-inline-assembly": "off", "gas-custom-errors": "off", "func-visibility": [ @@ -14,12 +11,8 @@ } ], "no-empty-blocks": "off", - "no-unused-vars": [ - "error" - ], - "state-visibility": [ - "error" - ], + "no-unused-vars": ["error"], + "state-visibility": ["error"], "not-rely-on-time": "off", "const-name-snakecase": [ "error", @@ -39,29 +32,13 @@ "allowPrefix": true } ], - "private-func-leading-underscore": [ - "error" - ], - "private-vars-no-leading-underscore": [ - "error" - ], - "func-param-name-leading-underscore": [ - "error" - ], - "func-param-name-mixedcase": [ - "error" - ], - "strict-override": [ - "error" - ], - "strict-import": [ - "error" - ], - "ordering": [ - "error" - ], - "comprehensive-interface": [ - "error" - ] + "private-func-leading-underscore": ["error"], + "private-vars-no-leading-underscore": ["error"], + "func-param-name-leading-underscore": ["error"], + "func-param-name-mixedcase": ["error"], + "strict-override": ["error"], + "strict-import": ["error"], + "ordering": ["error"], + "comprehensive-interface": ["error"] } -} \ No newline at end of file +} diff --git a/l1-contracts/src/core/ProofCommitmentEscrow.sol b/l1-contracts/src/core/ProofCommitmentEscrow.sol new file mode 100644 index 00000000000..147cd2c69db --- /dev/null +++ b/l1-contracts/src/core/ProofCommitmentEscrow.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; + +import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; + +contract ProofCommitmentEscrow is IProofCommitmentEscrow { + using SafeERC20 for IERC20; + + struct Stake { + uint256 amount; + address prover; + } + + struct WithdrawRequest { + uint256 amount; + uint256 executableAt; + } + + address public immutable OWNER; + uint256 public constant WITHDRAW_DELAY = + Constants.ETHEREUM_SLOT_DURATION * Constants.AZTEC_EPOCH_DURATION * 3; + mapping(address => uint256) public deposits; + mapping(address => WithdrawRequest) public withdrawRequests; + IERC20 public token; + Stake public stake; + + modifier onlyOwner() { + if (msg.sender != OWNER) { + revert Errors.ProofCommitmentEscrow__NotOwner(msg.sender); + } + _; + } + + modifier hasBalance(address _prover, uint256 _amount) { + if (deposits[_prover] < _amount) { + revert Errors.ProofCommitmentEscrow__InsufficientBalance(deposits[_prover], _amount); + } + _; + } + + constructor(IERC20 _token, address _owner) { + token = _token; + OWNER = _owner; + } + + /** + * @notice Deposit tokens into the escrow + * + * @dev The caller must have approved the token transfer + * + * @param _amount The amount of tokens to deposit + */ + function deposit(uint256 _amount) external override { + token.safeTransferFrom(msg.sender, address(this), _amount); + + deposits[msg.sender] += _amount; + } + + /** + * @notice Start a withdrawal request + * + * @dev The caller must have sufficient balance + * The withdrawal request will be executable after a delay + * Subsequent calls to this function will overwrite the previous request + * + * @param _amount - The amount of tokens to withdraw + */ + function startWithdraw(uint256 _amount) external override hasBalance(msg.sender, _amount) { + withdrawRequests[msg.sender] = + WithdrawRequest({amount: _amount, executableAt: block.timestamp + WITHDRAW_DELAY}); + } + + /** + * @notice Execute a mature withdrawal request + */ + function executeWithdraw() external override { + WithdrawRequest storage request = withdrawRequests[msg.sender]; + if (request.executableAt > block.timestamp) { + revert Errors.ProofCommitmentEscrow__WithdrawRequestNotReady( + block.timestamp, request.executableAt + ); + } + + uint256 amount = request.amount; + + delete withdrawRequests[msg.sender]; + deposits[msg.sender] -= amount; + token.safeTransfer(msg.sender, amount); + } + + /** + * @notice Stake an amount of previously deposited tokens + * + * @dev Only callable by the owner + * The prover must have sufficient balance + * The prover's balance will be reduced by the bond amount + */ + function stakeBond(uint256 _amount, address _prover) + external + override + onlyOwner + hasBalance(_prover, _amount) + { + deposits[_prover] -= _amount; + stake = Stake({amount: _amount, prover: _prover}); + } + + /** + * @notice Unstake the bonded tokens, returning them to the prover + * + * @dev Only callable by the owner + */ + function unstakeBond() external override onlyOwner { + deposits[stake.prover] += stake.amount; + delete stake; + } + + /** + * @notice Get the minimum balance of a prover at a given timestamp. + * + * @dev Returns 0 if the timestamp is beyond the WITHDRAW_DELAY from the current block timestamp + * + * @param _timestamp The timestamp at which to check the balance + * @param _prover The address of the prover + * + * @return The balance of the prover at the given timestamp, compensating for withdrawal requests that have matured by that time + */ + function minBalanceAtTime(uint256 _timestamp, address _prover) + external + view + override + returns (uint256) + { + // If the timestamp is beyond the WITHDRAW_DELAY, the minimum possible balance is 0; + // the prover could issue a withdraw request in this block for the full amount, + // and execute it exactly WITHDRAW_DELAY later. + if (_timestamp >= block.timestamp + WITHDRAW_DELAY) { + return 0; + } + + uint256 balance = deposits[_prover]; + if (withdrawRequests[_prover].executableAt <= _timestamp) { + balance -= withdrawRequests[_prover].amount; + } + return balance; + } +} diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 497e8f7fcc1..e6c99cb856e 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2023 Aztec Labs. +// Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; @@ -11,6 +11,7 @@ import {IVerifier} from "@aztec/core/interfaces/IVerifier.sol"; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {EpochProofQuoteLib} from "@aztec/core/libraries/EpochProofQuoteLib.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {HeaderLib} from "@aztec/core/libraries/HeaderLib.sol"; import {TxsDecoder} from "@aztec/core/libraries/TxsDecoder.sol"; @@ -34,7 +35,6 @@ import {Timestamp, Slot, Epoch, SlotLib, EpochLib} from "@aztec/core/libraries/T */ contract Rollup is Leonidas, IRollup, ITestRollup { using SafeCast for uint256; - using SlotLib for Slot; using EpochLib for Epoch; @@ -169,7 +169,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { bytes32[] memory _txHashes, SignatureLib.Signature[] memory _signatures, bytes calldata _body, - DataStructures.SignedEpochProofQuote calldata _quote + EpochProofQuoteLib.SignedEpochProofQuote calldata _quote ) external override(IRollup) { propose(_header, _archive, _blockHash, _txHashes, _signatures, _body); claimEpochProofRight(_quote); @@ -324,7 +324,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { return TxsDecoder.decode(_body); } - function claimEpochProofRight(DataStructures.SignedEpochProofQuote calldata _quote) + function claimEpochProofRight(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) public override(IRollup) { @@ -559,7 +559,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { return publicInputs; } - function validateEpochProofRightClaim(DataStructures.SignedEpochProofQuote calldata _quote) + function validateEpochProofRightClaim(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) public view override(IRollup) diff --git a/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol b/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol index f6c0d55f7f8..0a2b722ba20 100644 --- a/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol +++ b/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol @@ -4,7 +4,14 @@ pragma solidity >=0.8.27; interface IProofCommitmentEscrow { function deposit(uint256 _amount) external; - function withdraw(uint256 _amount) external; + + function startWithdraw(uint256 _amount) external; + + function executeWithdraw() external; + function stakeBond(uint256 _bondAmount, address _prover) external; - function unstakeBond(uint256 _bondAmount, address _prover) external; + + function unstakeBond() external; + + function minBalanceAtTime(uint256 _timestamp, address _prover) external view returns (uint256); } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 62974893e69..3dd7956dfa3 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -7,6 +7,7 @@ import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; +import {EpochProofQuoteLib} from "@aztec/core/libraries/EpochProofQuoteLib.sol"; import {Timestamp, Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; @@ -30,7 +31,7 @@ interface IRollup { function prune() external; - function claimEpochProofRight(DataStructures.SignedEpochProofQuote calldata _quote) external; + function claimEpochProofRight(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) external; function propose( bytes calldata _header, @@ -48,7 +49,7 @@ interface IRollup { bytes32[] memory _txHashes, SignatureLib.Signature[] memory _signatures, bytes calldata _body, - DataStructures.SignedEpochProofQuote calldata _quote + EpochProofQuoteLib.SignedEpochProofQuote calldata _quote ) external; function submitEpochRootProof( @@ -98,7 +99,7 @@ interface IRollup { function getEpochToProve() external view returns (Epoch); function nextEpochToClaim() external view returns (Epoch); function getEpochForBlock(uint256 blockNumber) external view returns (Epoch); - function validateEpochProofRightClaim(DataStructures.SignedEpochProofQuote calldata _quote) + function validateEpochProofRightClaim(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) external view; function getEpochProofPublicInputs( diff --git a/l1-contracts/src/core/libraries/DataStructures.sol b/l1-contracts/src/core/libraries/DataStructures.sol index 8827cededee..537f3d6e7b2 100644 --- a/l1-contracts/src/core/libraries/DataStructures.sol +++ b/l1-contracts/src/core/libraries/DataStructures.sol @@ -1,10 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2023 Aztec Labs. +// Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; -import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; - -import {Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; +import {Epoch} from "@aztec/core/libraries/TimeMath.sol"; /** * @title Data Structures Library @@ -77,32 +75,6 @@ library DataStructures { bool ignoreSignatures; } - /** - * @notice Struct encompassing an epoch proof quote - * @param epochToProve - The epoch number to prove - * @param validUntilSlot - The deadline of the quote, denoted in L2 slots - * @param bondAmount - The size of the bond - * @param prover - The address of the prover - * @param basisPointFee - The fee measured in basis points - */ - struct EpochProofQuote { - Epoch epochToProve; - Slot validUntilSlot; - uint256 bondAmount; - address prover; - uint32 basisPointFee; - } - - /** - * @notice A signed quote for the epoch proof - * @param quote - The Epoch Proof Quote - * @param signature - A signature on the quote - */ - struct SignedEpochProofQuote { - EpochProofQuote quote; - SignatureLib.Signature signature; - } - /** * @notice Struct containing the Epoch Proof Claim * @param epochToProve - the epoch that the bond provider is claiming to prove diff --git a/l1-contracts/src/core/libraries/EpochProofQuoteLib.sol b/l1-contracts/src/core/libraries/EpochProofQuoteLib.sol new file mode 100644 index 00000000000..50a9658371c --- /dev/null +++ b/l1-contracts/src/core/libraries/EpochProofQuoteLib.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; +import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; + +library EpochProofQuoteLib { + /** + * @notice Struct encompassing an epoch proof quote + * @param epochToProve - The epoch number to prove + * @param validUntilSlot - The deadline of the quote, denoted in L2 slots + * @param bondAmount - The size of the bond + * @param prover - The address of the prover + * @param basisPointFee - The fee measured in basis points + */ + struct EpochProofQuote { + Epoch epochToProve; + Slot validUntilSlot; + uint256 bondAmount; + address prover; + uint32 basisPointFee; + } + + /** + * @notice A signed quote for the epoch proof + * @param quote - The Epoch Proof Quote + * @param signature - A signature on the quote + */ + struct SignedEpochProofQuote { + EpochProofQuote quote; + SignatureLib.Signature signature; + } + + bytes32 public constant EPOCH_PROOF_QUOTE_TYPEHASH = keccak256( + "EpochProofQuote(uint256 epochToProve,uint256 validUntilSlot,uint256 bondAmount,address prover,uint32 basisPointFee)" + ); + + function hash(EpochProofQuote memory quote) internal pure returns (bytes32) { + return keccak256( + abi.encode( + EPOCH_PROOF_QUOTE_TYPEHASH, + quote.epochToProve, + quote.validUntilSlot, + quote.bondAmount, + quote.prover, + quote.basisPointFee + ) + ); + } + + function toDigest(EpochProofQuote memory quote, bytes32 domainSeparator) + internal + pure + returns (bytes32) + { + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, hash(quote))); + } + + function verify(SignedEpochProofQuote memory quote, bytes32 domainSeparator) internal pure { + bytes32 digest = toDigest(quote.quote, domainSeparator); + SignatureLib.verify(quote.signature, quote.quote.prover, digest); + } +} diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index a6b3d8a68af..4e21c192b3a 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -100,4 +100,9 @@ library Errors { error FeeJuicePortal__AlreadyInitialized(); // 0xc7a172fe error FeeJuicePortal__InvalidInitialization(); // 0xfd9b3208 error FeeJuicePortal__Unauthorized(); // 0x67e3691e + + // Proof Commitment Escrow + error ProofCommitmentEscrow__InsufficientBalance(uint256 balance, uint256 requested); + error ProofCommitmentEscrow__NotOwner(address caller); + error ProofCommitmentEscrow__WithdrawRequestNotReady(uint256 current, uint256 readyAt); } diff --git a/l1-contracts/src/core/libraries/crypto/EIP712Lib.sol b/l1-contracts/src/core/libraries/crypto/EIP712Lib.sol new file mode 100644 index 00000000000..89128c9d7a9 --- /dev/null +++ b/l1-contracts/src/core/libraries/crypto/EIP712Lib.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +library EIP712Lib { + struct EIP712Domain { + string name; + string version; + } + + bytes32 public constant EIP712DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version)"); + + bytes32 public constant DOMAIN_SEPARATOR = + 0xa5a70ffb22bda94bb24c78bd9ec602157f08294910e13b891f0f17910c9ebe1f; + // hash(EIP712Lib.EIP712Domain({name: "Aztec Rollup", version: "1"})); + + function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) { + return keccak256( + abi.encode( + EIP712DOMAIN_TYPEHASH, + keccak256(bytes(eip712Domain.name)), + keccak256(bytes(eip712Domain.version)) + ) + ); + } +} diff --git a/l1-contracts/src/mock/MockProofCommitmentEscrow.sol b/l1-contracts/src/mock/MockProofCommitmentEscrow.sol index 4fdd0ca1306..42934a0e98b 100644 --- a/l1-contracts/src/mock/MockProofCommitmentEscrow.sol +++ b/l1-contracts/src/mock/MockProofCommitmentEscrow.sol @@ -9,15 +9,23 @@ contract MockProofCommitmentEscrow is IProofCommitmentEscrow { // do nothing } - function withdraw(uint256 _amount) external override { + function startWithdraw(uint256 _amount) external override { // do nothing } - function unstakeBond(uint256 _amount, address _prover) external override { + function executeWithdraw() external override { + // do nothing + } + + function unstakeBond() external override { // do nothing } function stakeBond(uint256 _amount, address _prover) external override { // do nothing } + + function minBalanceAtTime(uint256, address) external pure override returns (uint256) { + return 0; + } } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index bd581a883b2..a58403e5abc 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2023 Aztec Labs. +// Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; import {DecoderBase} from "./decoders/Base.sol"; @@ -7,6 +7,7 @@ import {DecoderBase} from "./decoders/Base.sol"; import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; +import {EpochProofQuoteLib} from "@aztec/core/libraries/EpochProofQuoteLib.sol"; import {Registry} from "@aztec/governance/Registry.sol"; import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; @@ -46,7 +47,7 @@ contract RollupTest is DecoderBase { SignatureLib.Signature[] internal signatures; - DataStructures.SignedEpochProofQuote internal quote; + EpochProofQuoteLib.SignedEpochProofQuote internal quote; /** * @notice Set up the contracts needed for the tests with time aligned to the provided block name @@ -77,8 +78,8 @@ contract RollupTest is DecoderBase { merkleTestUtil = new MerkleTestUtil(); txsHelper = new TxsDecoderHelper(); - quote = DataStructures.SignedEpochProofQuote({ - quote: DataStructures.EpochProofQuote({ + quote = EpochProofQuoteLib.SignedEpochProofQuote({ + quote: EpochProofQuoteLib.EpochProofQuote({ epochToProve: Epoch.wrap(0), validUntilSlot: Slot.wrap(1), bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), diff --git a/l1-contracts/test/prover-coordination/EscrowERC20.sol b/l1-contracts/test/prover-coordination/EscrowERC20.sol new file mode 100644 index 00000000000..48b743e825c --- /dev/null +++ b/l1-contracts/test/prover-coordination/EscrowERC20.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity ^0.8.27; + +import {ERC20} from "@oz/token/ERC20/ERC20.sol"; + +// solhint-disable comprehensive-interface + +contract EscrowERC20 is ERC20 { + constructor() ERC20("TestToken", "TST") {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol new file mode 100644 index 00000000000..aacc1f6ce4d --- /dev/null +++ b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {Test} from "forge-std/Test.sol"; + +import {ProofCommitmentEscrow} from "@aztec/core/ProofCommitmentEscrow.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; + +import {EscrowERC20} from "./EscrowERC20.sol"; + +// solhint-disable comprehensive-interface + +contract TestProofCommitmentEscrow is Test { + ProofCommitmentEscrow internal _escrow; + EscrowERC20 internal _token; + + modifier setup() { + _token = new EscrowERC20(); + _escrow = new ProofCommitmentEscrow(_token, address(this)); + _; + } + + function testDeposit() public setup { + address prover = address(42); + uint256 depositAmount = 100; + _mintAndDeposit(prover, depositAmount); + + assertEq( + _token.balanceOf(address(_escrow)), + depositAmount, + "Escrow balance should match deposit amount" + ); + assertEq(_token.balanceOf(prover), 0, "Prover balance should be 0 after deposit"); + } + + function testCannotWithdrawWithoutMatureRequest() public setup { + address prover = address(42); + uint256 depositAmount = 100; + uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); + + _mintAndDeposit(prover, depositAmount); + + vm.prank(prover); + _escrow.startWithdraw(depositAmount); + + vm.prank(prover); + vm.expectRevert( + abi.encodeWithSelector( + Errors.ProofCommitmentEscrow__WithdrawRequestNotReady.selector, + block.timestamp, + withdrawReadyAt + ) + ); + _escrow.executeWithdraw(); + + vm.warp(block.timestamp + _escrow.WITHDRAW_DELAY() - 1); + vm.prank(prover); + vm.expectRevert( + abi.encodeWithSelector( + Errors.ProofCommitmentEscrow__WithdrawRequestNotReady.selector, + block.timestamp, + withdrawReadyAt + ) + ); + _escrow.executeWithdraw(); + } + + function testWithdrawAfterDelay() public setup { + address prover = address(42); + uint256 depositAmount = 100; + uint256 withdrawAmount = 50; + uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); + + _mintAndDeposit(prover, depositAmount); + + vm.prank(prover); + _escrow.startWithdraw(withdrawAmount); + + vm.warp(withdrawReadyAt); + + vm.prank(prover); + _escrow.executeWithdraw(); + + assertEq( + _token.balanceOf(address(_escrow)), + depositAmount - withdrawAmount, + "Escrow balance should be reduced after withdrawal" + ); + assertEq(_token.balanceOf(prover), withdrawAmount, "Prover balance should match deposit amount"); + } + + function testCannotReplayWithdrawRequest() public setup { + address prover = address(42); + uint256 depositAmount = 100; + uint256 withdrawAmount = 50; + uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); + + _mintAndDeposit(prover, depositAmount); + + vm.prank(prover); + _escrow.startWithdraw(withdrawAmount); + vm.warp(withdrawReadyAt); + + vm.prank(prover); + _escrow.executeWithdraw(); + + vm.prank(prover); + _escrow.executeWithdraw(); + + assertEq( + _token.balanceOf(address(_escrow)), + depositAmount - withdrawAmount, + "Escrow balance should be reduced after withdrawal" + ); + } + + function testOnlyOwnerCanStake() public setup { + address prover = address(42); + vm.prank(prover); + vm.expectRevert(abi.encodeWithSelector(Errors.ProofCommitmentEscrow__NotOwner.selector, prover)); + _escrow.stakeBond(0, address(0)); + } + + function testCannotStakeMoreThanProverBalance() public setup { + address prover = address(42); + uint256 depositAmount = 100; + uint256 stakeAmount = depositAmount + 1; + + _mintAndDeposit(prover, depositAmount); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.ProofCommitmentEscrow__InsufficientBalance.selector, depositAmount, stakeAmount + ) + ); + _escrow.stakeBond(stakeAmount, prover); + + assertEq( + _token.balanceOf(address(_escrow)), + depositAmount, + "Escrow balance should match deposit amount" + ); + assertEq(_escrow.deposits(prover), depositAmount, "Prover balance should match deposit amount"); + } + + function testOnlyOwnerCanUnstake() public setup { + address prover = address(42); + vm.prank(prover); + vm.expectRevert(abi.encodeWithSelector(Errors.ProofCommitmentEscrow__NotOwner.selector, prover)); + _escrow.unstakeBond(); + } + + function testStakeAndUnstake() public setup { + address prover = address(42); + uint256 depositAmount = 100; + uint256 stakeAmount = 50; + + _mintAndDeposit(prover, depositAmount); + + _escrow.stakeBond(stakeAmount, prover); + + assertEq( + _escrow.deposits(prover), depositAmount - stakeAmount, "Prover balance should be reduced" + ); + + _escrow.unstakeBond(); + + assertEq( + _escrow.deposits(prover), depositAmount, "Prover balance should be restored after unstake" + ); + } + + function testOverwritingStakeSlashesPreviousProver() public setup { + // Arrange + address proverA = address(42); + address proverB = address(43); + uint256 depositAmountA = 100; + uint256 stakeAmountA = 50; + uint256 depositAmountB = 200; + uint256 stakeAmountB = 100; + + // Prover A deposits and is staked + _mintAndDeposit(proverA, depositAmountA); + _escrow.stakeBond(stakeAmountA, proverA); + + // Prover B deposits and owner overwrites the stake + _mintAndDeposit(proverB, depositAmountB); + _escrow.stakeBond(stakeAmountB, proverB); + + // Prover A cannot recover the staked amount + uint256 expectedDepositA = depositAmountA - stakeAmountA; + assertEq( + _escrow.deposits(proverA), + expectedDepositA, + "Prover A's deposit should reflect the slashed stake" + ); + + // Owner cannot unstake Prover A's stake anymore + _escrow.unstakeBond(); + assertEq( + _escrow.deposits(proverB), + depositAmountB, + "Prover B's deposit should be restored after unstake" + ); + assertEq( + _escrow.deposits(proverA), + expectedDepositA, + "Prover A's deposit remains slashed after unstake" + ); + } + + function testWithdrawRequestOverwriting() public setup { + // Arrange + address prover = address(42); + uint256 depositAmount = 100; + uint256 withdrawAmountA = 40; + uint256 withdrawAmountB = 60; + uint256 withdrawReadyAtA = block.timestamp + _escrow.WITHDRAW_DELAY(); + uint256 withdrawReadyAtB = block.timestamp + 2 * _escrow.WITHDRAW_DELAY(); + + _mintAndDeposit(prover, depositAmount); + + // Prover starts first withdraw request + vm.prank(prover); + _escrow.startWithdraw(withdrawAmountA); + + // Prover starts second withdraw request before executing first + vm.warp(withdrawReadyAtA); + + vm.prank(prover); + _escrow.startWithdraw(withdrawAmountB); + + // Attempt to execute first withdraw request after its delay + vm.prank(prover); + vm.expectRevert( + abi.encodeWithSelector( + Errors.ProofCommitmentEscrow__WithdrawRequestNotReady.selector, + withdrawReadyAtA, + withdrawReadyAtB + ) + ); + _escrow.executeWithdraw(); + + // Execute second withdraw request after its delay + vm.warp(withdrawReadyAtB); + vm.prank(prover); + _escrow.executeWithdraw(); + + // Assert + assertEq( + _escrow.deposits(prover), + depositAmount - withdrawAmountB, + "Prover's deposit should be reduced by the withdrawn amount" + ); + } + + function testMinBalanceAtSlot() public setup { + // Arrange + address prover = address(42); + uint256 depositAmount = 100; + uint256 withdrawAmount = 25; + uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); + + _mintAndDeposit(prover, depositAmount); + + assertEq( + _escrow.minBalanceAtTime(block.timestamp, prover), + depositAmount, + "Min balance should match deposit amount before any withdraw request" + ); + + assertEq( + _escrow.minBalanceAtTime(withdrawReadyAt - 1, prover), + depositAmount, + "Min balance should match deposit amount before withdraw request matures" + ); + + vm.prank(prover); + _escrow.startWithdraw(withdrawAmount); + + assertEq( + _escrow.minBalanceAtTime(block.timestamp, prover), + depositAmount, + "Min balance should be unaffected by pending withdraw request before maturity" + ); + + assertEq( + _escrow.minBalanceAtTime(block.timestamp + _escrow.WITHDRAW_DELAY(), prover), + 0, + "Min balance should be 0 at or beyond the delay window" + ); + + vm.warp(block.timestamp + 1); + + assertEq( + _escrow.minBalanceAtTime(withdrawReadyAt, prover), + depositAmount - withdrawAmount, + "Min balance should be 75 after withdraw request matures" + ); + + assertEq( + _escrow.minBalanceAtTime(withdrawReadyAt + 1, prover), + 0, + "Min balance should be 0 at or beyond the delay window" + ); + } + + function _mintAndDeposit(address _prover, uint256 _amount) internal { + _token.mint(_prover, _amount); + + vm.prank(_prover); + _token.approve(address(_escrow), _amount); + + vm.prank(_prover); + _escrow.deposit(_amount); + } +} diff --git a/l1-contracts/test/prover-coordination/Signatures.t.sol b/l1-contracts/test/prover-coordination/Signatures.t.sol new file mode 100644 index 00000000000..1b3fd1fbbb6 --- /dev/null +++ b/l1-contracts/test/prover-coordination/Signatures.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.27; + +import {Test} from "forge-std/Test.sol"; + +import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; +import {EIP712Lib} from "@aztec/core/libraries/crypto/EIP712Lib.sol"; +import {EpochProofQuoteLib} from "@aztec/core/libraries/EpochProofQuoteLib.sol"; +import {Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; + +// solhint-disable comprehensive-interface + +contract TestSignatures is Test { + function testQuoteSignatures() public { + bytes32 separator = EIP712Lib.DOMAIN_SEPARATOR; + EpochProofQuoteLib.EpochProofQuote memory _quote = EpochProofQuoteLib.EpochProofQuote({ + epochToProve: Epoch.wrap(42), + validUntilSlot: Slot.wrap(100), + bondAmount: 1000000000000000000, + prover: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826, + basisPointFee: 5000 + }); + assertEq( + EpochProofQuoteLib.hash(_quote), + 0x4ed9cc91360dbd11218c8fcf4e8402f7a85d298dd4b4fb3d1fbcbb1a8ae72cc9, + "Invalid quote hash" + ); + + bytes32 digest = EpochProofQuoteLib.toDigest(_quote, separator); + + assertEq( + digest, 0x6927eba5b70276d7a9ccedac195c01844c8e71b3fdf9f8a5972914e8a2d5911d, "Invalid digest" + ); + + uint8 v = 27; + bytes32 r = 0x63e9cf4ee71dcfeae09da434f3e7c2d5c336b7d83b78fbbbe6d1639b206b5b69; + bytes32 s = 0x57b1e46efa6b388c4333c8dd40da6ec25297b6c277fba4f1b42e3b6f1d0ddfde; + EpochProofQuoteLib.SignedEpochProofQuote memory _signedQuote = EpochProofQuoteLib + .SignedEpochProofQuote({ + quote: _quote, + signature: SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}) + }); + + EpochProofQuoteLib.verify(_signedQuote, separator); + + _signedQuote = EpochProofQuoteLib.SignedEpochProofQuote({ + quote: _quote, + signature: SignatureLib.Signature({isEmpty: true, v: v, r: r, s: s}) + }); + vm.expectRevert(abi.encodeWithSelector(Errors.SignatureLib__CannotVerifyEmpty.selector)); + EpochProofQuoteLib.verify(_signedQuote, separator); + } +} diff --git a/yarn-project/circuit-types/src/domain_separator.ts b/yarn-project/circuit-types/src/domain_separator.ts new file mode 100644 index 00000000000..6b1e0b87edd --- /dev/null +++ b/yarn-project/circuit-types/src/domain_separator.ts @@ -0,0 +1,10 @@ +import { Buffer32 } from '@aztec/foundation/buffer'; + +import { type TypedDataDomain, domainSeparator } from 'viem'; + +export const DOMAIN: TypedDataDomain = { + name: 'Aztec Rollup', + version: '1', +}; + +export const DOMAIN_SEPARATOR = Buffer32.fromString(domainSeparator({ domain: DOMAIN })); diff --git a/yarn-project/circuit-types/src/p2p/signature_utils.ts b/yarn-project/circuit-types/src/p2p/signature_utils.ts index b06cbdf5f92..f0a8fc32731 100644 --- a/yarn-project/circuit-types/src/p2p/signature_utils.ts +++ b/yarn-project/circuit-types/src/p2p/signature_utils.ts @@ -1,6 +1,8 @@ import { Buffer32 } from '@aztec/foundation/buffer'; import { keccak256, makeEthSignDigest } from '@aztec/foundation/crypto'; +import { DOMAIN_SEPARATOR } from '../domain_separator.js'; + export interface Signable { getPayloadToSign(): Buffer; } @@ -23,3 +25,9 @@ export function getHashedSignaturePayloadEthSignedMessage(s: Signable): Buffer32 const payload = getHashedSignaturePayload(s); return makeEthSignDigest(payload); } + +export function get712StructuredDigest(s: Signable): Buffer32 { + return Buffer32.fromBuffer( + keccak256(Buffer.concat([Buffer.from('\x19\x01'), DOMAIN_SEPARATOR.buffer, keccak256(s.getPayloadToSign())])), + ); +} diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts index 049845921e1..a8b27ab1291 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts @@ -1,6 +1,8 @@ import { EthAddress } from '@aztec/circuits.js'; -import { Secp256k1Signer } from '@aztec/foundation/crypto'; +import { Buffer32 } from '@aztec/foundation/buffer'; +import { Secp256k1Signer, keccak256 } from '@aztec/foundation/crypto'; +import { get712StructuredDigest, getHashedSignaturePayload } from '../p2p/signature_utils.js'; import { EpochProofQuote } from './epoch_proof_quote.js'; import { EpochProofQuotePayload } from './epoch_proof_quote_payload.js'; @@ -16,9 +18,27 @@ describe('epoch proof quote', () => { }); const quote = EpochProofQuote.new(payload, signer); - expect(EpochProofQuote.fromBuffer(quote.toBuffer())).toEqual(quote); - expect(quote.senderAddress).toEqual(signer.address); }); + + it('should be able to use eip 712', () => { + const payload = EpochProofQuotePayload.fromFields({ + epochToProve: 42n, + validUntilSlot: 100n, + bondAmount: 1000000000000000000n, + prover: EthAddress.fromString('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'), + basisPointFee: 5000, + }); + + const hash = getHashedSignaturePayload(payload).to0xString(); + expect(hash).toEqual('0x4ed9cc91360dbd11218c8fcf4e8402f7a85d298dd4b4fb3d1fbcbb1a8ae72cc9'); + + const digest = get712StructuredDigest(payload).to0xString(); + expect(digest).toEqual('0x6927eba5b70276d7a9ccedac195c01844c8e71b3fdf9f8a5972914e8a2d5911d'); + + const signer = new Secp256k1Signer(Buffer32.fromBuffer(keccak256(Buffer.from('cow')))); + const quote = EpochProofQuote.new(payload, signer); + expect(quote.senderAddress.toString().toLowerCase()).toEqual('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826'); + }); }); diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts index 8839b257ff7..96fc396dfeb 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts @@ -6,7 +6,7 @@ import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; import { Gossipable } from '../p2p/gossipable.js'; -import { getHashedSignaturePayloadEthSignedMessage } from '../p2p/signature_utils.js'; +import { get712StructuredDigest } from '../p2p/signature_utils.js'; import { TopicType, createTopicString } from '../p2p/topic_type.js'; import { EpochProofQuotePayload } from './epoch_proof_quote_payload.js'; @@ -37,14 +37,14 @@ export class EpochProofQuote extends Gossipable { } static new(payload: EpochProofQuotePayload, signer: Secp256k1Signer): EpochProofQuote { - const digest = getHashedSignaturePayloadEthSignedMessage(payload); + const digest = get712StructuredDigest(payload); const signature = signer.sign(digest); return new EpochProofQuote(payload, signature); } get senderAddress(): EthAddress { if (!this.sender) { - const hashed = getHashedSignaturePayloadEthSignedMessage(this.payload); + const hashed = get712StructuredDigest(this.payload); // Cache the sender for later use this.sender = recoverAddress(hashed, this.signature); diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts index be70356f7d8..b586791fdc4 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts @@ -1,4 +1,6 @@ import { EthAddress } from '@aztec/circuits.js'; +import { Buffer32 } from '@aztec/foundation/buffer'; +import { keccak256 } from '@aztec/foundation/crypto'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; @@ -50,9 +52,18 @@ export class EpochProofQuotePayload implements Signable { ); } + static TYPE_HASH = Buffer32.fromBuffer( + keccak256( + Buffer.from( + 'EpochProofQuote(uint256 epochToProve,uint256 validUntilSlot,uint256 bondAmount,address prover,uint32 basisPointFee)', + ), + ), + ); + getPayloadToSign(): Buffer { - const abi = parseAbiParameters('uint256, uint256, uint256, address, uint32'); + const abi = parseAbiParameters('bytes32, uint256, uint256, uint256, address, uint32'); const encodedData = encodeAbiParameters(abi, [ + EpochProofQuotePayload.TYPE_HASH.to0xString(), this.epochToProve, this.validUntilSlot, this.bondAmount, diff --git a/yarn-project/foundation/src/crypto/keccak/index.ts b/yarn-project/foundation/src/crypto/keccak/index.ts index 940e9d26ff2..4e8ed8d3473 100644 --- a/yarn-project/foundation/src/crypto/keccak/index.ts +++ b/yarn-project/foundation/src/crypto/keccak/index.ts @@ -1,12 +1,17 @@ import { Keccak } from 'sha3'; +import { Buffer32 } from '../../buffer/buffer32.js'; + /** * Computes the Keccak-256 hash of the given input buffer. * * @param input - The input buffer to be hashed. * @returns The computed Keccak-256 hash as a Buffer. */ -export function keccak256(input: Buffer) { +export function keccak256(input: Buffer | Buffer32) { + if (input instanceof Buffer32) { + input = input.buffer; + } const hash = new Keccak(256); return hash.update(input).digest(); } From e56bf530b9d2bb457b6ebd3994681d6161e2eb6d Mon Sep 17 00:00:00 2001 From: Mitch Date: Mon, 30 Sep 2024 13:02:40 -0400 Subject: [PATCH 2/7] refactor the 712 flow on the contracts --- .../src/core/ProofCommitmentEscrow.sol | 73 +++++++++---------- l1-contracts/src/core/Rollup.sol | 24 +++++- .../interfaces/IProofCommitmentEscrow.sol | 15 ++-- l1-contracts/src/core/interfaces/IRollup.sol | 8 ++ l1-contracts/src/core/libraries/Errors.sol | 2 +- .../src/core/libraries/crypto/EIP712Lib.sol | 27 ------- .../core/libraries/crypto/SignatureLib.sol | 4 + .../src/mock/MockProofCommitmentEscrow.sol | 3 +- l1-contracts/test/Rollup.t.sol | 6 +- .../PortalERC20.sol => TestERC20.sol} | 2 +- .../PortalERC20.t.sol => TestERC20.t.sol} | 8 +- l1-contracts/test/portals/TokenPortal.t.sol | 12 +-- l1-contracts/test/portals/UniswapPortal.t.sol | 8 +- .../ProofCommitmentEscrow.t.sol | 19 ++--- .../test/prover-coordination/Signatures.t.sol | 71 +++++++++++++++--- l1-contracts/test/sparta/Sparta.t.sol | 6 +- 16 files changed, 173 insertions(+), 115 deletions(-) delete mode 100644 l1-contracts/src/core/libraries/crypto/EIP712Lib.sol rename l1-contracts/test/{portals/PortalERC20.sol => TestERC20.sol} (89%) rename l1-contracts/test/{portals/PortalERC20.t.sol => TestERC20.t.sol} (60%) diff --git a/l1-contracts/src/core/ProofCommitmentEscrow.sol b/l1-contracts/src/core/ProofCommitmentEscrow.sol index 147cd2c69db..7f63cd17aa3 100644 --- a/l1-contracts/src/core/ProofCommitmentEscrow.sol +++ b/l1-contracts/src/core/ProofCommitmentEscrow.sol @@ -8,6 +8,7 @@ import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; +import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; contract ProofCommitmentEscrow is IProofCommitmentEscrow { using SafeERC20 for IERC20; @@ -19,34 +20,25 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { struct WithdrawRequest { uint256 amount; - uint256 executableAt; + Timestamp executableAt; } - address public immutable OWNER; + address public immutable ROLLUP; uint256 public constant WITHDRAW_DELAY = Constants.ETHEREUM_SLOT_DURATION * Constants.AZTEC_EPOCH_DURATION * 3; mapping(address => uint256) public deposits; mapping(address => WithdrawRequest) public withdrawRequests; - IERC20 public token; + IERC20 public immutable token; Stake public stake; - modifier onlyOwner() { - if (msg.sender != OWNER) { - revert Errors.ProofCommitmentEscrow__NotOwner(msg.sender); - } - _; - } - - modifier hasBalance(address _prover, uint256 _amount) { - if (deposits[_prover] < _amount) { - revert Errors.ProofCommitmentEscrow__InsufficientBalance(deposits[_prover], _amount); - } + modifier onlyRollup() { + require(msg.sender == ROLLUP, Errors.ProofCommitmentEscrow__NotOwner(msg.sender)); _; } constructor(IERC20 _token, address _owner) { token = _token; - OWNER = _owner; + ROLLUP = _owner; } /** @@ -60,6 +52,8 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { token.safeTransferFrom(msg.sender, address(this), _amount); deposits[msg.sender] += _amount; + + emit Deposit(msg.sender, _amount); } /** @@ -71,27 +65,35 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { * * @param _amount - The amount of tokens to withdraw */ - function startWithdraw(uint256 _amount) external override hasBalance(msg.sender, _amount) { - withdrawRequests[msg.sender] = - WithdrawRequest({amount: _amount, executableAt: block.timestamp + WITHDRAW_DELAY}); + function startWithdraw(uint256 _amount) external override { + require( + deposits[msg.sender] >= _amount, + Errors.ProofCommitmentEscrow__InsufficientBalance(deposits[msg.sender], _amount) + ); + + withdrawRequests[msg.sender] = WithdrawRequest({ + amount: _amount, + executableAt: Timestamp.wrap(block.timestamp + WITHDRAW_DELAY) + }); + + emit StartWithdraw(msg.sender, _amount, withdrawRequests[msg.sender].executableAt); } /** * @notice Execute a mature withdrawal request */ function executeWithdraw() external override { - WithdrawRequest storage request = withdrawRequests[msg.sender]; - if (request.executableAt > block.timestamp) { - revert Errors.ProofCommitmentEscrow__WithdrawRequestNotReady( - block.timestamp, request.executableAt - ); - } - - uint256 amount = request.amount; + WithdrawRequest memory request = withdrawRequests[msg.sender]; + require( + request.executableAt <= Timestamp.wrap(block.timestamp), + Errors.ProofCommitmentEscrow__WithdrawRequestNotReady(block.timestamp, request.executableAt) + ); delete withdrawRequests[msg.sender]; - deposits[msg.sender] -= amount; - token.safeTransfer(msg.sender, amount); + deposits[msg.sender] -= request.amount; + token.safeTransfer(msg.sender, request.amount); + + emit ExecuteWithdraw(msg.sender, request.amount); } /** @@ -101,14 +103,11 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { * The prover must have sufficient balance * The prover's balance will be reduced by the bond amount */ - function stakeBond(uint256 _amount, address _prover) - external - override - onlyOwner - hasBalance(_prover, _amount) - { + function stakeBond(uint256 _amount, address _prover) external override onlyRollup { deposits[_prover] -= _amount; stake = Stake({amount: _amount, prover: _prover}); + + emit StakeBond(_prover, _amount); } /** @@ -116,7 +115,7 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { * * @dev Only callable by the owner */ - function unstakeBond() external override onlyOwner { + function unstakeBond() external override onlyRollup { deposits[stake.prover] += stake.amount; delete stake; } @@ -131,7 +130,7 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { * * @return The balance of the prover at the given timestamp, compensating for withdrawal requests that have matured by that time */ - function minBalanceAtTime(uint256 _timestamp, address _prover) + function minBalanceAtTime(Timestamp _timestamp, address _prover) external view override @@ -140,7 +139,7 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { // If the timestamp is beyond the WITHDRAW_DELAY, the minimum possible balance is 0; // the prover could issue a withdraw request in this block for the full amount, // and execute it exactly WITHDRAW_DELAY later. - if (_timestamp >= block.timestamp + WITHDRAW_DELAY) { + if (_timestamp >= Timestamp.wrap(block.timestamp + WITHDRAW_DELAY)) { return 0; } diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index e6c99cb856e..85fee888076 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -2,6 +2,9 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; +import {EIP712} from "@oz/utils/cryptography/EIP712.sol"; +import {ECDSA} from "@oz/utils/cryptography/ECDSA.sol"; + import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol"; import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol"; @@ -33,7 +36,7 @@ import {Timestamp, Slot, Epoch, SlotLib, EpochLib} from "@aztec/core/libraries/T * @notice Rollup contract that is concerned about readability and velocity of development * not giving a damn about gas costs. */ -contract Rollup is Leonidas, IRollup, ITestRollup { +contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { using SafeCast for uint256; using SlotLib for Slot; using EpochLib for Epoch; @@ -105,6 +108,25 @@ contract Rollup is Leonidas, IRollup, ITestRollup { setupEpoch(); } + function quoteToDigest(EpochProofQuoteLib.EpochProofQuote memory quote) + public + view + override(IRollup) + returns (bytes32) + { + return _hashTypedDataV4(EpochProofQuoteLib.hash(quote)); + } + + function verifySignedQuote(EpochProofQuoteLib.SignedEpochProofQuote memory signedQuote) + public + view + override(IRollup) + { + bytes32 digest = quoteToDigest(signedQuote.quote); + address recoveredSigner = ECDSA.recover(digest, SignatureLib.toBytes(signedQuote.signature)); + require(recoveredSigner == signedQuote.quote.prover); + } + /** * @notice Prune the pending chain up to the last proven block * diff --git a/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol b/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol index 0a2b722ba20..e49547dd9fc 100644 --- a/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol +++ b/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol @@ -2,16 +2,19 @@ // Copyright 2024 Aztec Labs. pragma solidity >=0.8.27; +import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; + interface IProofCommitmentEscrow { - function deposit(uint256 _amount) external; + event Deposit(address indexed depositor, uint256 amount); + event StartWithdraw(address indexed withdrawer, uint256 amount, Timestamp executableAt); + event ExecuteWithdraw(address indexed withdrawer, uint256 amount); + event StakeBond(address indexed prover, uint256 amount); + event UnstakeBond(address indexed prover); + function deposit(uint256 _amount) external; function startWithdraw(uint256 _amount) external; - function executeWithdraw() external; - function stakeBond(uint256 _bondAmount, address _prover) external; - function unstakeBond() external; - - function minBalanceAtTime(uint256 _timestamp, address _prover) external view returns (uint256); + function minBalanceAtTime(Timestamp _timestamp, address _prover) external view returns (uint256); } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 3dd7956dfa3..5b224bd522b 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -92,6 +92,14 @@ interface IRollup { Epoch provenEpochNumber ); + function quoteToDigest(EpochProofQuoteLib.EpochProofQuote memory quote) + external + view + returns (bytes32); + function verifySignedQuote(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) + external + view; + function archive() external view returns (bytes32); function archiveAt(uint256 _blockNumber) external view returns (bytes32); function getProvenBlockNumber() external view returns (uint256); diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 4e21c192b3a..c0756bd9a20 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -104,5 +104,5 @@ library Errors { // Proof Commitment Escrow error ProofCommitmentEscrow__InsufficientBalance(uint256 balance, uint256 requested); error ProofCommitmentEscrow__NotOwner(address caller); - error ProofCommitmentEscrow__WithdrawRequestNotReady(uint256 current, uint256 readyAt); + error ProofCommitmentEscrow__WithdrawRequestNotReady(uint256 current, Timestamp readyAt); } diff --git a/l1-contracts/src/core/libraries/crypto/EIP712Lib.sol b/l1-contracts/src/core/libraries/crypto/EIP712Lib.sol deleted file mode 100644 index 89128c9d7a9..00000000000 --- a/l1-contracts/src/core/libraries/crypto/EIP712Lib.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.27; - -library EIP712Lib { - struct EIP712Domain { - string name; - string version; - } - - bytes32 public constant EIP712DOMAIN_TYPEHASH = - keccak256("EIP712Domain(string name,string version)"); - - bytes32 public constant DOMAIN_SEPARATOR = - 0xa5a70ffb22bda94bb24c78bd9ec602157f08294910e13b891f0f17910c9ebe1f; - // hash(EIP712Lib.EIP712Domain({name: "Aztec Rollup", version: "1"})); - - function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) { - return keccak256( - abi.encode( - EIP712DOMAIN_TYPEHASH, - keccak256(bytes(eip712Domain.name)), - keccak256(bytes(eip712Domain.version)) - ) - ); - } -} diff --git a/l1-contracts/src/core/libraries/crypto/SignatureLib.sol b/l1-contracts/src/core/libraries/crypto/SignatureLib.sol index 98f37d0cc1c..c38f9c661b4 100644 --- a/l1-contracts/src/core/libraries/crypto/SignatureLib.sol +++ b/l1-contracts/src/core/libraries/crypto/SignatureLib.sol @@ -24,4 +24,8 @@ library SignatureLib { address recovered = ecrecover(_digest, _signature.v, _signature.r, _signature.s); require(_signer == recovered, Errors.SignatureLib__InvalidSignature(_signer, recovered)); } + + function toBytes(Signature memory _signature) internal pure returns (bytes memory) { + return abi.encodePacked(_signature.v, _signature.r, _signature.s); + } } diff --git a/l1-contracts/src/mock/MockProofCommitmentEscrow.sol b/l1-contracts/src/mock/MockProofCommitmentEscrow.sol index 42934a0e98b..d4f1db360aa 100644 --- a/l1-contracts/src/mock/MockProofCommitmentEscrow.sol +++ b/l1-contracts/src/mock/MockProofCommitmentEscrow.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.27; import {IProofCommitmentEscrow} from "@aztec/core/interfaces/IProofCommitmentEscrow.sol"; +import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; contract MockProofCommitmentEscrow is IProofCommitmentEscrow { function deposit(uint256 _amount) external override { @@ -25,7 +26,7 @@ contract MockProofCommitmentEscrow is IProofCommitmentEscrow { // do nothing } - function minBalanceAtTime(uint256, address) external pure override returns (uint256) { + function minBalanceAtTime(Timestamp, address) external pure override returns (uint256) { return 0; } } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index a58403e5abc..b5498ac9477 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -19,7 +19,7 @@ import {FeeJuicePortal} from "@aztec/core/FeeJuicePortal.sol"; import {Leonidas} from "@aztec/core/Leonidas.sol"; import {NaiveMerkle} from "./merkle/Naive.sol"; import {MerkleTestUtil} from "./merkle/TestUtil.sol"; -import {PortalERC20} from "./portals/PortalERC20.sol"; +import {TestERC20} from "./TestERC20.sol"; import {TxsDecoderHelper} from "./decoders/helpers/TxsDecoderHelper.sol"; import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol"; @@ -42,7 +42,7 @@ contract RollupTest is DecoderBase { Rollup internal rollup; MerkleTestUtil internal merkleTestUtil; TxsDecoderHelper internal txsHelper; - PortalERC20 internal portalERC20; + TestERC20 internal portalERC20; FeeJuicePortal internal feeJuicePortal; SignatureLib.Signature[] internal signatures; @@ -63,7 +63,7 @@ contract RollupTest is DecoderBase { } registry = new Registry(address(this)); - portalERC20 = new PortalERC20(); + portalERC20 = new TestERC20(); feeJuicePortal = new FeeJuicePortal(address(this)); portalERC20.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT); feeJuicePortal.initialize( diff --git a/l1-contracts/test/portals/PortalERC20.sol b/l1-contracts/test/TestERC20.sol similarity index 89% rename from l1-contracts/test/portals/PortalERC20.sol rename to l1-contracts/test/TestERC20.sol index 4c1dc5372be..3f0e54dc5f6 100644 --- a/l1-contracts/test/portals/PortalERC20.sol +++ b/l1-contracts/test/TestERC20.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "@oz/token/ERC20/ERC20.sol"; -contract PortalERC20 is ERC20 { +contract TestERC20 is ERC20 { constructor() ERC20("Portal", "PORTAL") {} function mint(address to, uint256 amount) external { diff --git a/l1-contracts/test/portals/PortalERC20.t.sol b/l1-contracts/test/TestERC20.t.sol similarity index 60% rename from l1-contracts/test/portals/PortalERC20.t.sol rename to l1-contracts/test/TestERC20.t.sol index cf69d9a6882..2b17e1a0eb6 100644 --- a/l1-contracts/test/portals/PortalERC20.t.sol +++ b/l1-contracts/test/TestERC20.t.sol @@ -1,13 +1,13 @@ pragma solidity ^0.8.18; import "forge-std/Test.sol"; -import {PortalERC20} from "./PortalERC20.sol"; +import {TestERC20} from "./TestERC20.sol"; -contract PortalERC20Test is Test { - PortalERC20 portalERC20; +contract TestERC20Test is Test { + TestERC20 portalERC20; function setUp() public { - portalERC20 = new PortalERC20(); + portalERC20 = new TestERC20(); } function test_mint() public { diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index d6c47232cff..5715fafd00b 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -17,7 +17,7 @@ import {IFeeJuicePortal} from "@aztec/core/interfaces/IFeeJuicePortal.sol"; // Portal tokens import {TokenPortal} from "./TokenPortal.sol"; -import {PortalERC20} from "./PortalERC20.sol"; +import {TestERC20} from "../TestERC20.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; @@ -37,7 +37,7 @@ contract TokenPortalTest is Test { bytes32 internal l2TokenAddress = bytes32(uint256(0x42)); TokenPortal internal tokenPortal; - PortalERC20 internal portalERC20; + TestERC20 internal portalERC20; // input params uint32 internal deadline = uint32(block.timestamp + 1 days); @@ -59,7 +59,7 @@ contract TokenPortalTest is Test { function setUp() public { registry = new Registry(address(this)); - portalERC20 = new PortalERC20(); + portalERC20 = new TestERC20(); rollup = new Rollup(IFeeJuicePortal(address(0)), bytes32(0), address(this), new address[](0)); inbox = rollup.INBOX(); outbox = rollup.OUTBOX(); @@ -70,7 +70,7 @@ contract TokenPortalTest is Test { tokenPortal.initialize(address(registry), address(portalERC20), l2TokenAddress); // Modify the proven block count - vm.store(address(rollup), bytes32(uint256(7)), bytes32(l2BlockNumber)); + vm.store(address(rollup), bytes32(uint256(9)), bytes32(l2BlockNumber)); assertEq(rollup.getProvenBlockNumber(), l2BlockNumber); vm.deal(address(this), 100 ether); @@ -88,7 +88,7 @@ contract TokenPortalTest is Test { abi.encodeWithSignature( "mint_private(bytes32,uint256)", secretHashForRedeemingMintedNotes, amount ) - ), + ), secretHash: secretHashForL2MessageConsumption }); } @@ -166,7 +166,7 @@ contract TokenPortalTest is Test { abi.encodeWithSignature( "withdraw(address,uint256,address)", recipient, withdrawAmount, _designatedCaller ) - ) + ) }) ); diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index aa3ee982ad0..9c7044eae9f 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -65,7 +65,7 @@ contract UniswapPortalTest is Test { uniswapPortal.initialize(address(registry), l2UniswapAddress); // Modify the proven block count - vm.store(address(rollup), bytes32(uint256(7)), bytes32(l2BlockNumber + 1)); + vm.store(address(rollup), bytes32(uint256(9)), bytes32(l2BlockNumber + 1)); assertEq(rollup.getProvenBlockNumber(), l2BlockNumber + 1); // have DAI locked in portal that can be moved when funds are withdrawn @@ -90,7 +90,7 @@ contract UniswapPortalTest is Test { recipient: DataStructures.L1Actor(address(daiTokenPortal), block.chainid), content: Hash.sha256ToField( abi.encodeWithSignature("withdraw(address,uint256,address)", _recipient, amount, _caller) - ) + ) }); return message.sha256ToField(); @@ -122,7 +122,7 @@ contract UniswapPortalTest is Test { secretHash, _caller ) - ) + ) }); return message.sha256ToField(); @@ -153,7 +153,7 @@ contract UniswapPortalTest is Test { secretHash, _caller ) - ) + ) }); return message.sha256ToField(); diff --git a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol index aacc1f6ce4d..1c4c2d1cc6c 100644 --- a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol +++ b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol @@ -6,6 +6,7 @@ import {Test} from "forge-std/Test.sol"; import {ProofCommitmentEscrow} from "@aztec/core/ProofCommitmentEscrow.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; import {EscrowERC20} from "./EscrowERC20.sol"; @@ -129,11 +130,7 @@ contract TestProofCommitmentEscrow is Test { _mintAndDeposit(prover, depositAmount); - vm.expectRevert( - abi.encodeWithSelector( - Errors.ProofCommitmentEscrow__InsufficientBalance.selector, depositAmount, stakeAmount - ) - ); + vm.expectRevert(); _escrow.stakeBond(stakeAmount, prover); assertEq( @@ -260,18 +257,18 @@ contract TestProofCommitmentEscrow is Test { address prover = address(42); uint256 depositAmount = 100; uint256 withdrawAmount = 25; - uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); + Timestamp withdrawReadyAt = Timestamp.wrap(block.timestamp + _escrow.WITHDRAW_DELAY()); _mintAndDeposit(prover, depositAmount); assertEq( - _escrow.minBalanceAtTime(block.timestamp, prover), + _escrow.minBalanceAtTime(Timestamp.wrap(block.timestamp), prover), depositAmount, "Min balance should match deposit amount before any withdraw request" ); assertEq( - _escrow.minBalanceAtTime(withdrawReadyAt - 1, prover), + _escrow.minBalanceAtTime(withdrawReadyAt - Timestamp.wrap(1), prover), depositAmount, "Min balance should match deposit amount before withdraw request matures" ); @@ -280,13 +277,13 @@ contract TestProofCommitmentEscrow is Test { _escrow.startWithdraw(withdrawAmount); assertEq( - _escrow.minBalanceAtTime(block.timestamp, prover), + _escrow.minBalanceAtTime(Timestamp.wrap(block.timestamp), prover), depositAmount, "Min balance should be unaffected by pending withdraw request before maturity" ); assertEq( - _escrow.minBalanceAtTime(block.timestamp + _escrow.WITHDRAW_DELAY(), prover), + _escrow.minBalanceAtTime(Timestamp.wrap(block.timestamp + _escrow.WITHDRAW_DELAY()), prover), 0, "Min balance should be 0 at or beyond the delay window" ); @@ -300,7 +297,7 @@ contract TestProofCommitmentEscrow is Test { ); assertEq( - _escrow.minBalanceAtTime(withdrawReadyAt + 1, prover), + _escrow.minBalanceAtTime(withdrawReadyAt + Timestamp.wrap(1), prover), 0, "Min balance should be 0 at or beyond the delay window" ); diff --git a/l1-contracts/test/prover-coordination/Signatures.t.sol b/l1-contracts/test/prover-coordination/Signatures.t.sol index 1b3fd1fbbb6..c9a1513e411 100644 --- a/l1-contracts/test/prover-coordination/Signatures.t.sol +++ b/l1-contracts/test/prover-coordination/Signatures.t.sol @@ -4,21 +4,53 @@ pragma solidity >=0.8.27; import {Test} from "forge-std/Test.sol"; +import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; + +import {Registry} from "@aztec/governance/Registry.sol"; +import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; +import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; +import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {Rollup} from "@aztec/core/Rollup.sol"; +import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; +import {FeeJuicePortal} from "@aztec/core/FeeJuicePortal.sol"; +import {Leonidas} from "@aztec/core/Leonidas.sol"; + import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; -import {EIP712Lib} from "@aztec/core/libraries/crypto/EIP712Lib.sol"; import {EpochProofQuoteLib} from "@aztec/core/libraries/EpochProofQuoteLib.sol"; import {Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; +import {TestERC20} from "../TestERC20.sol"; + +import {SignatureChecker} from "@oz/utils/cryptography/SignatureChecker.sol"; + // solhint-disable comprehensive-interface contract TestSignatures is Test { - function testQuoteSignatures() public { - bytes32 separator = EIP712Lib.DOMAIN_SEPARATOR; + Registry internal registry; + Inbox internal inbox; + Outbox internal outbox; + Rollup internal rollup; + TestERC20 internal testERC20; + FeeJuicePortal internal feeJuicePortal; + + modifier setup() { + registry = new Registry(address(this)); + testERC20 = new TestERC20(); + feeJuicePortal = new FeeJuicePortal(address(this)); + testERC20.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT); + feeJuicePortal.initialize( + address(registry), address(testERC20), bytes32(Constants.FEE_JUICE_ADDRESS) + ); + rollup = new Rollup(feeJuicePortal, bytes32(0), address(this), new address[](0)); + _; + } + + function testQuoteSignatures() public setup { EpochProofQuoteLib.EpochProofQuote memory _quote = EpochProofQuoteLib.EpochProofQuote({ epochToProve: Epoch.wrap(42), validUntilSlot: Slot.wrap(100), - bondAmount: 1000000000000000000, + bondAmount: 1e18, prover: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826, basisPointFee: 5000 }); @@ -28,7 +60,7 @@ contract TestSignatures is Test { "Invalid quote hash" ); - bytes32 digest = EpochProofQuoteLib.toDigest(_quote, separator); + bytes32 digest = rollup.quoteToDigest(_quote); assertEq( digest, 0x6927eba5b70276d7a9ccedac195c01844c8e71b3fdf9f8a5972914e8a2d5911d, "Invalid digest" @@ -43,13 +75,32 @@ contract TestSignatures is Test { signature: SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}) }); - EpochProofQuoteLib.verify(_signedQuote, separator); + SignatureChecker.isValidSignatureNow( + _quote.prover, digest, SignatureLib.toBytes(_signedQuote.signature) + ); + } - _signedQuote = EpochProofQuoteLib.SignedEpochProofQuote({ + function testEmptySignatures() public { + EpochProofQuoteLib.EpochProofQuote memory _quote = EpochProofQuoteLib.EpochProofQuote({ + epochToProve: Epoch.wrap(42), + validUntilSlot: Slot.wrap(100), + bondAmount: 1000000000000000000, + prover: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826, + basisPointFee: 5000 + }); + uint8 v = 27; + bytes32 r = 0x63e9cf4ee71dcfeae09da434f3e7c2d5c336b7d83b78fbbbe6d1639b206b5b69; + bytes32 s = 0x57b1e46efa6b388c4333c8dd40da6ec25297b6c277fba4f1b42e3b6f1d0ddfde; + EpochProofQuoteLib.SignedEpochProofQuote memory _signedQuote = EpochProofQuoteLib + .SignedEpochProofQuote({ quote: _quote, - signature: SignatureLib.Signature({isEmpty: true, v: v, r: r, s: s}) + signature: SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}) }); - vm.expectRevert(abi.encodeWithSelector(Errors.SignatureLib__CannotVerifyEmpty.selector)); - EpochProofQuoteLib.verify(_signedQuote, separator); + bytes32 digest = rollup.quoteToDigest(_quote); + + vm.expectRevert(); + SignatureChecker.isValidSignatureNow( + _quote.prover, digest, SignatureLib.toBytes(_signedQuote.signature) + ); } } diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index 0eb15f5772f..ca9000498cb 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -15,7 +15,7 @@ import {Rollup} from "@aztec/core/Rollup.sol"; import {Leonidas} from "@aztec/core/Leonidas.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; -import {PortalERC20} from "../portals/PortalERC20.sol"; +import {TestERC20} from "../TestERC20.sol"; import {TxsDecoderHelper} from "../decoders/helpers/TxsDecoderHelper.sol"; import {IFeeJuicePortal} from "@aztec/core/interfaces/IFeeJuicePortal.sol"; import {MessageHashUtils} from "@oz/utils/cryptography/MessageHashUtils.sol"; @@ -44,7 +44,7 @@ contract SpartaTest is DecoderBase { Rollup internal rollup; MerkleTestUtil internal merkleTestUtil; TxsDecoderHelper internal txsHelper; - PortalERC20 internal portalERC20; + TestERC20 internal portalERC20; SignatureLib.Signature internal emptySignature; mapping(address validator => uint256 privateKey) internal privateKeys; @@ -73,7 +73,7 @@ contract SpartaTest is DecoderBase { initialValidators[i - 1] = validator; } - portalERC20 = new PortalERC20(); + portalERC20 = new TestERC20(); rollup = new Rollup(IFeeJuicePortal(address(0)), bytes32(0), address(this), initialValidators); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); From c962569166cce3a128686b826102565f980a4bb6 Mon Sep 17 00:00:00 2001 From: Mitch Date: Mon, 30 Sep 2024 14:39:58 -0400 Subject: [PATCH 3/7] rename the erc20 portal contract --- .../token_bridge/1_depositing_to_aztec.md | 4 +-- l1-contracts/test/Rollup.t.sol | 22 ++++++++-------- l1-contracts/test/TestERC20.t.sol | 8 +++--- l1-contracts/test/portals/TokenPortal.t.sol | 26 +++++++++---------- l1-contracts/test/sparta/Sparta.t.sol | 4 +-- yarn-project/aztec-faucet/src/bin/index.ts | 4 +-- yarn-project/aztec/src/sandbox.ts | 8 +++--- .../circuit-types/src/domain_separator.ts | 10 ------- yarn-project/circuit-types/src/mocks.ts | 3 +++ .../circuit-types/src/p2p/signature_utils.ts | 10 ++++--- .../epoch_proof_quote.test.ts | 2 ++ .../epoch_proof_quote_payload.ts | 8 ++++-- .../cli/src/cmds/devnet/bootstrap_network.ts | 8 +++--- .../cli/src/cmds/l1/get_l1_balance.ts | 4 +-- yarn-project/cli/src/utils/aztec.ts | 8 +++--- yarn-project/cli/src/utils/portal_manager.ts | 6 ++--- .../cross_chain_messaging_test.ts | 4 +-- .../end-to-end/src/e2e_fees/fees_test.ts | 4 +-- .../src/fixtures/setup_l1_contracts.ts | 8 +++--- yarn-project/end-to-end/src/fixtures/utils.ts | 8 +++--- .../src/shared/cross_chain_test_harness.ts | 15 +++++------ .../src/shared/gas_portal_test_harness.ts | 6 ++--- .../scripts/generate-artifacts.sh | 2 +- .../src/epoch_proof_quote_pool/test_utils.ts | 2 ++ 24 files changed, 91 insertions(+), 93 deletions(-) delete mode 100644 yarn-project/circuit-types/src/domain_separator.ts diff --git a/docs/docs/tutorials/codealong/contract_tutorials/advanced/token_bridge/1_depositing_to_aztec.md b/docs/docs/tutorials/codealong/contract_tutorials/advanced/token_bridge/1_depositing_to_aztec.md index 0b05d3e92a6..ba35449ba13 100644 --- a/docs/docs/tutorials/codealong/contract_tutorials/advanced/token_bridge/1_depositing_to_aztec.md +++ b/docs/docs/tutorials/codealong/contract_tutorials/advanced/token_bridge/1_depositing_to_aztec.md @@ -32,9 +32,9 @@ This imports relevant files including the interfaces used by the Aztec rollup. A Create a basic ERC20 contract that can mint tokens to anyone. We will use this to test. -Create a file `PortalERC20.sol` in the same folder and add: +Create a file `TestERC20.sol` in the same folder and add: -#include_code contract /l1-contracts/test/portals/PortalERC20.sol solidity +#include_code contract /l1-contracts/test/TestERC20.sol solidity Replace the openzeppelin import with this: diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index b5498ac9477..9a9e9e7e4fe 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -42,7 +42,7 @@ contract RollupTest is DecoderBase { Rollup internal rollup; MerkleTestUtil internal merkleTestUtil; TxsDecoderHelper internal txsHelper; - TestERC20 internal portalERC20; + TestERC20 internal testERC20; FeeJuicePortal internal feeJuicePortal; SignatureLib.Signature[] internal signatures; @@ -63,11 +63,11 @@ contract RollupTest is DecoderBase { } registry = new Registry(address(this)); - portalERC20 = new TestERC20(); + testERC20 = new TestERC20(); feeJuicePortal = new FeeJuicePortal(address(this)); - portalERC20.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT); + testERC20.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT); feeJuicePortal.initialize( - address(registry), address(portalERC20), bytes32(Constants.FEE_JUICE_ADDRESS) + address(registry), address(testERC20), bytes32(Constants.FEE_JUICE_ADDRESS) ); rollup = new Rollup(feeJuicePortal, bytes32(0), address(this), new address[](0)); inbox = Inbox(address(rollup.INBOX())); @@ -425,7 +425,7 @@ contract RollupTest is DecoderBase { DecoderBase.Data memory data = load("mixed_block_1").block; bytes32[] memory txHashes = new bytes32[](0); - uint256 portalBalance = portalERC20.balanceOf(address(feeJuicePortal)); + uint256 portalBalance = testERC20.balanceOf(address(feeJuicePortal)); address coinbase = data.decodedHeader.globalVariables.coinbase; // Progress time as necessary @@ -437,17 +437,17 @@ contract RollupTest is DecoderBase { mstore(add(header, add(0x20, 0x0248)), feeAmount) } - assertEq(portalERC20.balanceOf(address(rollup)), 0, "invalid rollup balance"); + assertEq(testERC20.balanceOf(address(rollup)), 0, "invalid rollup balance"); // We jump to the time of the block. (unless it is in the past) vm.warp(max(block.timestamp, data.decodedHeader.globalVariables.timestamp)); - uint256 coinbaseBalance = portalERC20.balanceOf(coinbase); + uint256 coinbaseBalance = testERC20.balanceOf(coinbase); assertEq(coinbaseBalance, 0, "invalid initial coinbase balance"); // Assert that balance have NOT been increased by proposing the block rollup.propose(header, data.archive, data.blockHash, txHashes, signatures, data.body); - assertEq(portalERC20.balanceOf(coinbase), 0, "invalid coinbase balance"); + assertEq(testERC20.balanceOf(coinbase), 0, "invalid coinbase balance"); } (bytes32 preArchive, bytes32 preBlockHash,) = rollup.blocks(0); @@ -472,11 +472,11 @@ contract RollupTest is DecoderBase { coinbase, feeAmount ); - assertEq(portalERC20.balanceOf(coinbase), 0, "invalid coinbase balance"); + assertEq(testERC20.balanceOf(coinbase), 0, "invalid coinbase balance"); } { - portalERC20.mint(address(feeJuicePortal), feeAmount - portalBalance); + testERC20.mint(address(feeJuicePortal), feeAmount - portalBalance); // When the block is proven we should have received the funds _submitEpochProofWithFee( @@ -490,7 +490,7 @@ contract RollupTest is DecoderBase { coinbase, feeAmount ); - assertEq(portalERC20.balanceOf(coinbase), feeAmount, "invalid coinbase balance"); + assertEq(testERC20.balanceOf(coinbase), feeAmount, "invalid coinbase balance"); } } diff --git a/l1-contracts/test/TestERC20.t.sol b/l1-contracts/test/TestERC20.t.sol index 2b17e1a0eb6..4f50cb73f21 100644 --- a/l1-contracts/test/TestERC20.t.sol +++ b/l1-contracts/test/TestERC20.t.sol @@ -4,14 +4,14 @@ import "forge-std/Test.sol"; import {TestERC20} from "./TestERC20.sol"; contract TestERC20Test is Test { - TestERC20 portalERC20; + TestERC20 testERC20; function setUp() public { - portalERC20 = new TestERC20(); + testERC20 = new TestERC20(); } function test_mint() public { - portalERC20.mint(address(this), 100); - assertEq(portalERC20.balanceOf(address(this)), 100); + testERC20.mint(address(this), 100); + assertEq(testERC20.balanceOf(address(this)), 100); } } diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index 5715fafd00b..dff8eca55b1 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -37,7 +37,7 @@ contract TokenPortalTest is Test { bytes32 internal l2TokenAddress = bytes32(uint256(0x42)); TokenPortal internal tokenPortal; - TestERC20 internal portalERC20; + TestERC20 internal testERC20; // input params uint32 internal deadline = uint32(block.timestamp + 1 days); @@ -59,7 +59,7 @@ contract TokenPortalTest is Test { function setUp() public { registry = new Registry(address(this)); - portalERC20 = new TestERC20(); + testERC20 = new TestERC20(); rollup = new Rollup(IFeeJuicePortal(address(0)), bytes32(0), address(this), new address[](0)); inbox = rollup.INBOX(); outbox = rollup.OUTBOX(); @@ -67,7 +67,7 @@ contract TokenPortalTest is Test { registry.upgrade(address(rollup)); tokenPortal = new TokenPortal(); - tokenPortal.initialize(address(registry), address(portalERC20), l2TokenAddress); + tokenPortal.initialize(address(registry), address(testERC20), l2TokenAddress); // Modify the proven block count vm.store(address(rollup), bytes32(uint256(9)), bytes32(l2BlockNumber)); @@ -88,7 +88,7 @@ contract TokenPortalTest is Test { abi.encodeWithSignature( "mint_private(bytes32,uint256)", secretHashForRedeemingMintedNotes, amount ) - ), + ), secretHash: secretHashForL2MessageConsumption }); } @@ -108,8 +108,8 @@ contract TokenPortalTest is Test { function testDepositPrivate() public returns (bytes32) { // mint token and approve to the portal - portalERC20.mint(address(this), mintAmount); - portalERC20.approve(address(tokenPortal), mintAmount); + testERC20.mint(address(this), mintAmount); + testERC20.approve(address(tokenPortal), mintAmount); // Check for the expected message DataStructures.L1ToL2Msg memory expectedMessage = _createExpectedMintPrivateL1ToL2Message(); @@ -134,8 +134,8 @@ contract TokenPortalTest is Test { function testDepositPublic() public returns (bytes32) { // mint token and approve to the portal - portalERC20.mint(address(this), mintAmount); - portalERC20.approve(address(tokenPortal), mintAmount); + testERC20.mint(address(this), mintAmount); + testERC20.approve(address(tokenPortal), mintAmount); // Check for the expected message DataStructures.L1ToL2Msg memory expectedMessage = _createExpectedMintPublicL1ToL2Message(); @@ -166,7 +166,7 @@ contract TokenPortalTest is Test { abi.encodeWithSignature( "withdraw(address,uint256,address)", recipient, withdrawAmount, _designatedCaller ) - ) + ) }) ); @@ -183,7 +183,7 @@ contract TokenPortalTest is Test { returns (bytes32, bytes32[] memory, bytes32) { // send assets to the portal - portalERC20.mint(address(tokenPortal), withdrawAmount); + testERC20.mint(address(tokenPortal), withdrawAmount); // Create the message (bytes32 l2ToL1Message,) = _createWithdrawMessageForOutbox(_designatedCaller); @@ -208,7 +208,7 @@ contract TokenPortalTest is Test { // add message with caller as this address (bytes32 l2ToL1Message, bytes32[] memory siblingPath, bytes32 treeRoot) = _addWithdrawMessageInOutbox(address(0), l2BlockNumber); - assertEq(portalERC20.balanceOf(recipient), 0); + assertEq(testERC20.balanceOf(recipient), 0); vm.startPrank(_caller); vm.expectEmit(true, true, true, true); @@ -216,7 +216,7 @@ contract TokenPortalTest is Test { tokenPortal.withdraw(recipient, withdrawAmount, false, l2BlockNumber, 0, siblingPath); // Should have received 654 RNA tokens - assertEq(portalERC20.balanceOf(recipient), withdrawAmount); + assertEq(testERC20.balanceOf(recipient), withdrawAmount); // Should not be able to withdraw again vm.expectRevert( @@ -261,6 +261,6 @@ contract TokenPortalTest is Test { tokenPortal.withdraw(recipient, withdrawAmount, true, l2BlockNumber, 0, siblingPath); // Should have received 654 RNA tokens - assertEq(portalERC20.balanceOf(recipient), withdrawAmount); + assertEq(testERC20.balanceOf(recipient), withdrawAmount); } } diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index ca9000498cb..a6e290c2fc1 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -44,7 +44,7 @@ contract SpartaTest is DecoderBase { Rollup internal rollup; MerkleTestUtil internal merkleTestUtil; TxsDecoderHelper internal txsHelper; - TestERC20 internal portalERC20; + TestERC20 internal testERC20; SignatureLib.Signature internal emptySignature; mapping(address validator => uint256 privateKey) internal privateKeys; @@ -73,7 +73,7 @@ contract SpartaTest is DecoderBase { initialValidators[i - 1] = validator; } - portalERC20 = new TestERC20(); + testERC20 = new TestERC20(); rollup = new Rollup(IFeeJuicePortal(address(0)), bytes32(0), address(this), initialValidators); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); diff --git a/yarn-project/aztec-faucet/src/bin/index.ts b/yarn-project/aztec-faucet/src/bin/index.ts index 15058725657..456c067e4ed 100644 --- a/yarn-project/aztec-faucet/src/bin/index.ts +++ b/yarn-project/aztec-faucet/src/bin/index.ts @@ -2,7 +2,7 @@ import { NULL_KEY, createEthereumChain } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; import { createDebugLogger } from '@aztec/foundation/log'; -import { PortalERC20Abi } from '@aztec/l1-artifacts'; +import { TestERC20Abi } from '@aztec/l1-artifacts'; import http from 'http'; import Koa from 'koa'; @@ -182,7 +182,7 @@ async function transferAsset(assetName: AssetName, address: string) { try { const contract = getContract({ - abi: PortalERC20Abi, + abi: TestERC20Abi, address: assetAddress, client: walletClient, }); diff --git a/yarn-project/aztec/src/sandbox.ts b/yarn-project/aztec/src/sandbox.ts index 0a9f6158c72..9e508647a4b 100644 --- a/yarn-project/aztec/src/sandbox.ts +++ b/yarn-project/aztec/src/sandbox.ts @@ -20,12 +20,12 @@ import { InboxBytecode, OutboxAbi, OutboxBytecode, - PortalERC20Abi, - PortalERC20Bytecode, RegistryAbi, RegistryBytecode, RollupAbi, RollupBytecode, + TestERC20Abi, + TestERC20Bytecode, } from '@aztec/l1-artifacts'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types'; import { FeeJuiceAddress } from '@aztec/protocol-contracts/fee-juice'; @@ -108,8 +108,8 @@ export async function deployContractsToL1( contractBytecode: RollupBytecode, }, feeJuice: { - contractAbi: PortalERC20Abi, - contractBytecode: PortalERC20Bytecode, + contractAbi: TestERC20Abi, + contractBytecode: TestERC20Bytecode, }, feeJuicePortal: { contractAbi: FeeJuicePortalAbi, diff --git a/yarn-project/circuit-types/src/domain_separator.ts b/yarn-project/circuit-types/src/domain_separator.ts deleted file mode 100644 index 6b1e0b87edd..00000000000 --- a/yarn-project/circuit-types/src/domain_separator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Buffer32 } from '@aztec/foundation/buffer'; - -import { type TypedDataDomain, domainSeparator } from 'viem'; - -export const DOMAIN: TypedDataDomain = { - name: 'Aztec Rollup', - version: '1', -}; - -export const DOMAIN_SEPARATOR = Buffer32.fromString(domainSeparator({ domain: DOMAIN })); diff --git a/yarn-project/circuit-types/src/mocks.ts b/yarn-project/circuit-types/src/mocks.ts index 5c86312a170..4351dae7543 100644 --- a/yarn-project/circuit-types/src/mocks.ts +++ b/yarn-project/circuit-types/src/mocks.ts @@ -27,6 +27,7 @@ import { } from '@aztec/circuits.js/testing'; import { type ContractArtifact, NoteSelector } from '@aztec/foundation/abi'; import { makeTuple } from '@aztec/foundation/array'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { padArrayEnd, times } from '@aztec/foundation/collection'; import { randomBigInt, randomBytes, randomInt } from '@aztec/foundation/crypto'; import { Signature } from '@aztec/foundation/eth-signature'; @@ -233,6 +234,7 @@ export const mockEpochProofQuote = ( bondAmount?: bigint, proverAddress?: EthAddress, basisPointFee?: number, + domainSeparator?: Buffer32, ) => { const quotePayload: EpochProofQuotePayload = new EpochProofQuotePayload( epochToProve, @@ -240,6 +242,7 @@ export const mockEpochProofQuote = ( bondAmount ?? randomBigInt(10000n) + 1000n, proverAddress ?? EthAddress.random(), basisPointFee ?? randomInt(100), + domainSeparator ?? Buffer32.random(), ); const sig: Signature = Signature.empty(); return new EpochProofQuote(quotePayload, sig); diff --git a/yarn-project/circuit-types/src/p2p/signature_utils.ts b/yarn-project/circuit-types/src/p2p/signature_utils.ts index f0a8fc32731..80c1d53b933 100644 --- a/yarn-project/circuit-types/src/p2p/signature_utils.ts +++ b/yarn-project/circuit-types/src/p2p/signature_utils.ts @@ -1,12 +1,14 @@ import { Buffer32 } from '@aztec/foundation/buffer'; import { keccak256, makeEthSignDigest } from '@aztec/foundation/crypto'; -import { DOMAIN_SEPARATOR } from '../domain_separator.js'; - export interface Signable { getPayloadToSign(): Buffer; } +export interface ScopedToDomain { + domainSeparator: Buffer32; +} + /** * Get the hashed payload for the signature of the `Signable` * @param s - The `Signable` to sign @@ -26,8 +28,8 @@ export function getHashedSignaturePayloadEthSignedMessage(s: Signable): Buffer32 return makeEthSignDigest(payload); } -export function get712StructuredDigest(s: Signable): Buffer32 { +export function get712StructuredDigest(s: Signable & ScopedToDomain): Buffer32 { return Buffer32.fromBuffer( - keccak256(Buffer.concat([Buffer.from('\x19\x01'), DOMAIN_SEPARATOR.buffer, keccak256(s.getPayloadToSign())])), + keccak256(Buffer.concat([Buffer.from('\x19\x01'), s.domainSeparator.buffer, keccak256(s.getPayloadToSign())])), ); } diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts index a8b27ab1291..a3df7b1a5c1 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts @@ -15,6 +15,7 @@ describe('epoch proof quote', () => { epochToProve: 42n, prover: EthAddress.random(), validUntilSlot: 100n, + domainSeparator: Buffer32.random(), }); const quote = EpochProofQuote.new(payload, signer); @@ -29,6 +30,7 @@ describe('epoch proof quote', () => { bondAmount: 1000000000000000000n, prover: EthAddress.fromString('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'), basisPointFee: 5000, + domainSeparator: Buffer32.random(), }); const hash = getHashedSignaturePayload(payload).to0xString(); diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts index b586791fdc4..06f05a0f070 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts @@ -6,15 +6,16 @@ import { type FieldsOf } from '@aztec/foundation/types'; import { encodeAbiParameters, parseAbiParameters } from 'viem'; -import { type Signable } from '../p2p/signature_utils.js'; +import { type ScopedToDomain, type Signable } from '../p2p/signature_utils.js'; -export class EpochProofQuotePayload implements Signable { +export class EpochProofQuotePayload implements Signable, ScopedToDomain { constructor( public readonly epochToProve: bigint, public readonly validUntilSlot: bigint, public readonly bondAmount: bigint, public readonly prover: EthAddress, public readonly basisPointFee: number, + public readonly domainSeparator: Buffer32, ) {} static getFields(fields: FieldsOf) { @@ -24,6 +25,7 @@ export class EpochProofQuotePayload implements Signable { fields.bondAmount, fields.prover, fields.basisPointFee, + fields.domainSeparator, ] as const; } @@ -39,6 +41,7 @@ export class EpochProofQuotePayload implements Signable { reader.readUInt256(), reader.readObject(EthAddress), reader.readNumber(), + reader.readObject(Buffer32), ); } @@ -49,6 +52,7 @@ export class EpochProofQuotePayload implements Signable { fields.bondAmount, fields.prover, fields.basisPointFee, + fields.domainSeparator, ); } diff --git a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts index 5d5ef4eff6b..55b3d945513 100644 --- a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts +++ b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts @@ -100,13 +100,11 @@ export async function bootstrapNetwork( * Step 1. Deploy the L1 contracts, but don't initialize */ async function deployERC20({ walletClient, publicClient }: L1Clients) { - const { PortalERC20Abi, PortalERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } = await import( - '@aztec/l1-artifacts' - ); + const { TestERC20Abi, TestERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } = await import('@aztec/l1-artifacts'); const erc20: ContractArtifacts = { - contractAbi: PortalERC20Abi, - contractBytecode: PortalERC20Bytecode, + contractAbi: TestERC20Abi, + contractBytecode: TestERC20Bytecode, }; const portal: ContractArtifacts = { contractAbi: TokenPortalAbi, diff --git a/yarn-project/cli/src/cmds/l1/get_l1_balance.ts b/yarn-project/cli/src/cmds/l1/get_l1_balance.ts index 43f75ae5cde..c89cbb95954 100644 --- a/yarn-project/cli/src/cmds/l1/get_l1_balance.ts +++ b/yarn-project/cli/src/cmds/l1/get_l1_balance.ts @@ -1,7 +1,7 @@ import { type EthAddress } from '@aztec/circuits.js'; import { createEthereumChain } from '@aztec/ethereum'; import { type LogFn } from '@aztec/foundation/log'; -import { PortalERC20Abi } from '@aztec/l1-artifacts'; +import { TestERC20Abi } from '@aztec/l1-artifacts'; import { createPublicClient, getContract, http } from 'viem'; @@ -22,7 +22,7 @@ export async function getL1Balance( if (token) { const gasL1 = getContract({ address: token.toString(), - abi: PortalERC20Abi, + abi: TestERC20Abi, client: publicClient, }); diff --git a/yarn-project/cli/src/utils/aztec.ts b/yarn-project/cli/src/utils/aztec.ts index 71919cd8ec7..2c6f4316ee2 100644 --- a/yarn-project/cli/src/utils/aztec.ts +++ b/yarn-project/cli/src/utils/aztec.ts @@ -72,8 +72,8 @@ export async function deployAztecContracts( FeeJuicePortalAbi, FeeJuicePortalBytecode, - PortalERC20Abi, - PortalERC20Bytecode, + TestERC20Abi, + TestERC20Bytecode, } = await import('@aztec/l1-artifacts'); const { createEthereumChain, deployL1Contracts } = await import('@aztec/ethereum'); const { mnemonicToAccount, privateKeyToAccount } = await import('viem/accounts'); @@ -100,8 +100,8 @@ export async function deployAztecContracts( contractBytecode: RollupBytecode, }, feeJuice: { - contractAbi: PortalERC20Abi, - contractBytecode: PortalERC20Bytecode, + contractAbi: TestERC20Abi, + contractBytecode: TestERC20Bytecode, }, feeJuicePortal: { contractAbi: FeeJuicePortalAbi, diff --git a/yarn-project/cli/src/utils/portal_manager.ts b/yarn-project/cli/src/utils/portal_manager.ts index a30040f42c9..265ca2f5f95 100644 --- a/yarn-project/cli/src/utils/portal_manager.ts +++ b/yarn-project/cli/src/utils/portal_manager.ts @@ -1,7 +1,7 @@ // REFACTOR: This file has been shamelessly copied from yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts // We should make this a shared utility in the aztec.js package. import { type AztecAddress, type DebugLogger, type EthAddress, Fr, type PXE, computeSecretHash } from '@aztec/aztec.js'; -import { FeeJuicePortalAbi, PortalERC20Abi, TokenPortalAbi } from '@aztec/l1-artifacts'; +import { FeeJuicePortalAbi, TestERC20Abi, TokenPortalAbi } from '@aztec/l1-artifacts'; import { type Account, @@ -31,7 +31,7 @@ function generateClaimSecret(): [Fr, Fr] { } class L1TokenManager { - private contract: GetContractReturnType>; + private contract: GetContractReturnType>; public constructor( public readonly address: EthAddress, @@ -41,7 +41,7 @@ class L1TokenManager { ) { this.contract = getContract({ address: this.address.toString(), - abi: PortalERC20Abi, + abi: TestERC20Abi, client: this.walletClient, }); } diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts index 6f0f889e4eb..168aa7fe72c 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts @@ -11,7 +11,7 @@ import { createDebugLogger, } from '@aztec/aztec.js'; import { createL1Clients } from '@aztec/ethereum'; -import { InboxAbi, OutboxAbi, PortalERC20Abi, RollupAbi, TokenPortalAbi } from '@aztec/l1-artifacts'; +import { InboxAbi, OutboxAbi, RollupAbi, TestERC20Abi, TokenPortalAbi } from '@aztec/l1-artifacts'; import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts.js'; import { type Chain, type HttpTransport, type PublicClient, getContract } from 'viem'; @@ -158,7 +158,7 @@ export class CrossChainMessagingTest { }); const underlyingERC20 = getContract({ address: crossChainContext.underlying.toString(), - abi: PortalERC20Abi, + abi: TestERC20Abi, client: walletClient, }); diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index aa5761aa80f..1157a4f3c7b 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -17,7 +17,7 @@ import { import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; import { EthAddress, GasSettings, computePartialAddress } from '@aztec/circuits.js'; import { createL1Clients } from '@aztec/ethereum'; -import { PortalERC20Abi } from '@aztec/l1-artifacts'; +import { TestERC20Abi } from '@aztec/l1-artifacts'; import { AppSubscriptionContract, TokenContract as BananaCoin, @@ -340,7 +340,7 @@ export class FeesTest { const { walletClient } = createL1Clients(context.aztecNodeConfig.l1RpcUrl, MNEMONIC); const gasL1 = getContract({ address: data.l1FeeJuiceAddress.toString(), - abi: PortalERC20Abi, + abi: TestERC20Abi, client: walletClient, }); return await gasL1.read.balanceOf([this.coinbase.toString()]); diff --git a/yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts b/yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts index 6f622272b8f..c62950d85cb 100644 --- a/yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts +++ b/yarn-project/end-to-end/src/fixtures/setup_l1_contracts.ts @@ -7,12 +7,12 @@ import { InboxBytecode, OutboxAbi, OutboxBytecode, - PortalERC20Abi, - PortalERC20Bytecode, RegistryAbi, RegistryBytecode, RollupAbi, RollupBytecode, + TestERC20Abi, + TestERC20Bytecode, } from '@aztec/l1-artifacts'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types'; import { FeeJuiceAddress } from '@aztec/protocol-contracts/fee-juice'; @@ -46,8 +46,8 @@ export const setupL1Contracts = async ( contractBytecode: RollupBytecode, }, feeJuice: { - contractAbi: PortalERC20Abi, - contractBytecode: PortalERC20Bytecode, + contractAbi: TestERC20Abi, + contractBytecode: TestERC20Bytecode, }, feeJuicePortal: { contractAbi: FeeJuicePortalAbi, diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 7d456587d34..5534791b2ed 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -49,12 +49,12 @@ import { InboxBytecode, OutboxAbi, OutboxBytecode, - PortalERC20Abi, - PortalERC20Bytecode, RegistryAbi, RegistryBytecode, RollupAbi, RollupBytecode, + TestERC20Abi, + TestERC20Bytecode, } from '@aztec/l1-artifacts'; import { AuthRegistryContract, RouterContract } from '@aztec/noir-contracts.js'; import { FeeJuiceContract } from '@aztec/noir-contracts.js/FeeJuice'; @@ -136,8 +136,8 @@ export const setupL1Contracts = async ( contractBytecode: RollupBytecode, }, feeJuice: { - contractAbi: PortalERC20Abi, - contractBytecode: PortalERC20Bytecode, + contractAbi: TestERC20Abi, + contractBytecode: TestERC20Bytecode, }, feeJuicePortal: { contractAbi: FeeJuicePortalAbi, diff --git a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts index 3f14d4b34c5..bbb2b206b2d 100644 --- a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts @@ -22,8 +22,8 @@ import { sha256ToField } from '@aztec/foundation/crypto'; import { InboxAbi, OutboxAbi, - PortalERC20Abi, - PortalERC20Bytecode, + TestERC20Abi, + TestERC20Bytecode, TokenPortalAbi, TokenPortalBytecode, } from '@aztec/l1-artifacts'; @@ -82,16 +82,13 @@ export async function deployAndInitializeTokenAndBridgeContracts( underlyingERC20: any; }> { if (!underlyingERC20Address) { - underlyingERC20Address = await deployL1Contract( - walletClient, - publicClient, - PortalERC20Abi, - PortalERC20Bytecode, - ).then(({ address }) => address); + underlyingERC20Address = await deployL1Contract(walletClient, publicClient, TestERC20Abi, TestERC20Bytecode).then( + ({ address }) => address, + ); } const underlyingERC20 = getContract({ address: underlyingERC20Address!.toString(), - abi: PortalERC20Abi, + abi: TestERC20Abi, client: walletClient, }); diff --git a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts index df75a856c5e..0d61fefecfe 100644 --- a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts @@ -8,7 +8,7 @@ import { type Wallet, computeSecretHash, } from '@aztec/aztec.js'; -import { FeeJuicePortalAbi, OutboxAbi, PortalERC20Abi } from '@aztec/l1-artifacts'; +import { FeeJuicePortalAbi, OutboxAbi, TestERC20Abi } from '@aztec/l1-artifacts'; import { FeeJuiceContract } from '@aztec/noir-contracts.js'; import { FeeJuiceAddress } from '@aztec/protocol-contracts/fee-juice'; @@ -68,7 +68,7 @@ export class FeeJuicePortalTestingHarnessFactory { const gasL1 = getContract({ address: feeJuiceAddress.toString(), - abi: PortalERC20Abi, + abi: TestERC20Abi, client: walletClient, }); @@ -125,7 +125,7 @@ export class GasBridgingTestHarness implements IGasBridgingTestHarness { /** Token portal instance. */ public tokenPortal: GetContractReturnType>, /** Underlying token for portal tests. */ - public underlyingERC20: GetContractReturnType>, + public underlyingERC20: GetContractReturnType>, /** Message Bridge Outbox. */ public outbox: GetContractReturnType>, /** Viem Public client instance. */ diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index ef9c640c9fa..2903b4ca429 100755 --- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh +++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh @@ -15,7 +15,7 @@ CONTRACTS=( "l1-contracts:Outbox" "l1-contracts:Rollup" "l1-contracts:TokenPortal" - "l1-contracts:PortalERC20" + "l1-contracts:TestERC20" "l1-contracts:UniswapPortal" "l1-contracts:IERC20" "l1-contracts:FeeJuicePortal" diff --git a/yarn-project/p2p/src/epoch_proof_quote_pool/test_utils.ts b/yarn-project/p2p/src/epoch_proof_quote_pool/test_utils.ts index 0847e254014..a6b9aceb3de 100644 --- a/yarn-project/p2p/src/epoch_proof_quote_pool/test_utils.ts +++ b/yarn-project/p2p/src/epoch_proof_quote_pool/test_utils.ts @@ -1,5 +1,6 @@ import { EpochProofQuote, EpochProofQuotePayload } from '@aztec/circuit-types'; import { EthAddress } from '@aztec/circuits.js'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { Secp256k1Signer, randomBigInt, randomInt } from '@aztec/foundation/crypto'; export function makeRandomEpochProofQuotePayload(): EpochProofQuotePayload { @@ -9,6 +10,7 @@ export function makeRandomEpochProofQuotePayload(): EpochProofQuotePayload { epochToProve: randomBigInt(1000000n), prover: EthAddress.random(), validUntilSlot: randomBigInt(1000000n), + domainSeparator: Buffer32.random(), }); } From c851a38fcb46ae848cec7258d31614c045ed929e Mon Sep 17 00:00:00 2001 From: Mitch Date: Mon, 30 Sep 2024 20:38:43 -0400 Subject: [PATCH 4/7] enable proof quote signature verification on l1 update tests --- l1-contracts/src/core/Rollup.sol | 2 + .../src/core/libraries/SignatureLib.sol | 27 --- .../core/libraries/crypto/SignatureLib.sol | 2 +- l1-contracts/test/Rollup.t.sol | 117 +++++++------ .../test/prover-coordination/Signatures.t.sol | 106 ------------ yarn-project/aztec.js/src/index.ts | 1 + yarn-project/circuit-types/src/mocks.ts | 3 - .../epoch_proof_quote.test.ts | 31 +--- .../prover_coordination/epoch_proof_quote.ts | 43 ++--- .../epoch_proof_quote_payload.ts | 29 +++- yarn-project/end-to-end/Earthfile | 4 + .../e2e_json_coordination.test.ts | 155 +++++++++++++----- .../e2e_quote_signing.test.ts | 66 ++++++++ .../src/epoch_proof_quote_pool/test_utils.ts | 3 +- .../src/publisher/l1-publisher.ts | 51 +----- .../src/sequencer/sequencer.ts | 7 + 16 files changed, 307 insertions(+), 340 deletions(-) delete mode 100644 l1-contracts/src/core/libraries/SignatureLib.sol delete mode 100644 l1-contracts/test/prover-coordination/Signatures.t.sol create mode 100644 yarn-project/end-to-end/src/prover-coordination/e2e_quote_signing.test.ts diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 85fee888076..e856da11b2e 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -586,6 +586,8 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { view override(IRollup) { + verifySignedQuote(_quote); + Slot currentSlot = getCurrentSlot(); address currentProposer = getCurrentProposer(); Epoch epochToProve = getEpochToProve(); diff --git a/l1-contracts/src/core/libraries/SignatureLib.sol b/l1-contracts/src/core/libraries/SignatureLib.sol deleted file mode 100644 index 8d28e2f2796..00000000000 --- a/l1-contracts/src/core/libraries/SignatureLib.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.27; - -import {Errors} from "@aztec/core/libraries/Errors.sol"; - -library SignatureLib { - struct Signature { - bool isEmpty; - uint8 v; - bytes32 r; - bytes32 s; - } - - /** - * @notice Verified a signature, throws if the signature is invalid or empty - * - * @param _signature - The signature to verify - * @param _signer - The expected signer of the signature - * @param _digest - The digest that was signed - */ - function verify(Signature memory _signature, address _signer, bytes32 _digest) internal pure { - require(!_signature.isEmpty, Errors.SignatureLib__CannotVerifyEmpty()); - address recovered = ecrecover(_digest, _signature.v, _signature.r, _signature.s); - require(_signer == recovered, Errors.SignatureLib__InvalidSignature(_signer, recovered)); - } -} diff --git a/l1-contracts/src/core/libraries/crypto/SignatureLib.sol b/l1-contracts/src/core/libraries/crypto/SignatureLib.sol index c38f9c661b4..bcae500df8c 100644 --- a/l1-contracts/src/core/libraries/crypto/SignatureLib.sol +++ b/l1-contracts/src/core/libraries/crypto/SignatureLib.sol @@ -26,6 +26,6 @@ library SignatureLib { } function toBytes(Signature memory _signature) internal pure returns (bytes memory) { - return abi.encodePacked(_signature.v, _signature.r, _signature.s); + return abi.encodePacked(_signature.r, _signature.s, _signature.v); } } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 9a9e9e7e4fe..cc481648bfa 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -47,7 +47,8 @@ contract RollupTest is DecoderBase { SignatureLib.Signature[] internal signatures; - EpochProofQuoteLib.SignedEpochProofQuote internal quote; + EpochProofQuoteLib.EpochProofQuote internal quote; + EpochProofQuoteLib.SignedEpochProofQuote internal signedQuote; /** * @notice Set up the contracts needed for the tests with time aligned to the provided block name @@ -78,16 +79,16 @@ contract RollupTest is DecoderBase { merkleTestUtil = new MerkleTestUtil(); txsHelper = new TxsDecoderHelper(); - quote = EpochProofQuoteLib.SignedEpochProofQuote({ - quote: EpochProofQuoteLib.EpochProofQuote({ - epochToProve: Epoch.wrap(0), - validUntilSlot: Slot.wrap(1), - bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), - prover: address(0), - basisPointFee: 0 - }), - signature: SignatureLib.Signature({isEmpty: false, v: 27, r: bytes32(0), s: bytes32(0)}) + uint256 privateKey = 0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234; + address signer = vm.addr(privateKey); + quote = EpochProofQuoteLib.EpochProofQuote({ + epochToProve: Epoch.wrap(0), + validUntilSlot: Slot.wrap(1), + bondAmount: rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), + prover: signer, + basisPointFee: 0 }); + signedQuote = _quoteToSignedQuote(quote); _; } @@ -100,7 +101,7 @@ contract RollupTest is DecoderBase { // sanity check that proven/pending tip are at genesis vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NoEpochToProve.selector)); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); warpToL2Slot(1); assertEq(rollup.getCurrentSlot(), 1, "warp to slot 1 failed"); @@ -108,46 +109,50 @@ contract RollupTest is DecoderBase { // empty slots do not move pending chain vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NoEpochToProve.selector)); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); } function testClaimWithWrongEpoch() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); - - quote.quote.epochToProve = Epoch.wrap(1); + quote.epochToProve = Epoch.wrap(1); + signedQuote = _quoteToSignedQuote(quote); vm.expectRevert( abi.encodeWithSelector( - Errors.Rollup__NotClaimingCorrectEpoch.selector, 0, quote.quote.epochToProve + Errors.Rollup__NotClaimingCorrectEpoch.selector, 0, signedQuote.quote.epochToProve ) ); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); } function testClaimWithInsufficientBond() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); - quote.quote.bondAmount = 0; + quote.bondAmount = 0; + signedQuote = _quoteToSignedQuote(quote); vm.expectRevert( abi.encodeWithSelector( Errors.Rollup__InsufficientBondAmount.selector, rollup.PROOF_COMMITMENT_MIN_BOND_AMOUNT_IN_TST(), - quote.quote.bondAmount + signedQuote.quote.bondAmount ) ); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); } function testClaimPastValidUntil() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); - quote.quote.validUntilSlot = Slot.wrap(0); + quote.validUntilSlot = Slot.wrap(0); + signedQuote = _quoteToSignedQuote(quote); vm.expectRevert( - abi.encodeWithSelector(Errors.Rollup__QuoteExpired.selector, 1, quote.quote.validUntilSlot) + abi.encodeWithSelector( + Errors.Rollup__QuoteExpired.selector, 1, signedQuote.quote.validUntilSlot + ) ); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); } function testClaimSimple() public setUpFor("mixed_block_1") { @@ -155,9 +160,9 @@ contract RollupTest is DecoderBase { vm.expectEmit(true, true, true, true); emit IRollup.ProofRightClaimed( - quote.quote.epochToProve, address(0), address(this), quote.quote.bondAmount, Slot.wrap(1) + quote.epochToProve, quote.prover, address(this), quote.bondAmount, Slot.wrap(1) ); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); ( Epoch epochToProve, @@ -166,26 +171,26 @@ contract RollupTest is DecoderBase { address bondProvider, address proposerClaimant ) = rollup.proofClaim(); - assertEq(epochToProve, quote.quote.epochToProve, "Invalid epoch to prove"); - assertEq(basisPointFee, quote.quote.basisPointFee, "Invalid basis point fee"); - assertEq(bondAmount, quote.quote.bondAmount, "Invalid bond amount"); + assertEq(epochToProve, signedQuote.quote.epochToProve, "Invalid epoch to prove"); + assertEq(basisPointFee, signedQuote.quote.basisPointFee, "Invalid basis point fee"); + assertEq(bondAmount, signedQuote.quote.bondAmount, "Invalid bond amount"); // TODO #8573 // This will be fixed with proper escrow - assertEq(bondProvider, address(0), "Invalid bond provider"); + assertEq(bondProvider, quote.prover, "Invalid bond provider"); assertEq(proposerClaimant, address(this), "Invalid proposer claimant"); } function testClaimTwice() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector)); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); warpToL2Slot(2); vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector)); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); // warp to epoch 1 warpToL2Slot(Constants.AZTEC_EPOCH_DURATION); @@ -193,7 +198,7 @@ contract RollupTest is DecoderBase { // We should still be trying to prove epoch 0 in epoch 1 vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector)); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); // still nothing to prune vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NothingToPrune.selector)); @@ -212,17 +217,18 @@ contract RollupTest is DecoderBase { rollup.CLAIM_DURATION_IN_L2_SLOTS() ) ); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); } function testNoPruneWhenClaimExists() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); - quote.quote.validUntilSlot = Epoch.wrap(2).toSlots(); + quote.validUntilSlot = Epoch.wrap(2).toSlots(); + signedQuote = _quoteToSignedQuote(quote); warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS() - 1); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS()); @@ -233,50 +239,49 @@ contract RollupTest is DecoderBase { function testPruneWhenClaimExpires() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); - quote.quote.validUntilSlot = Epoch.wrap(2).toSlots(); + quote.validUntilSlot = Epoch.wrap(2).toSlots(); + signedQuote = _quoteToSignedQuote(quote); warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS() - 1); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); warpToL2Slot(Constants.AZTEC_EPOCH_DURATION * 2); // We should still be trying to prove epoch 0 in epoch 2 vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__ProofRightAlreadyClaimed.selector)); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); rollup.prune(); vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NoEpochToProve.selector)); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); } function testClaimAfterPrune() public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", false, 1); - quote.quote.validUntilSlot = Epoch.wrap(3).toSlots(); - quote.quote.prover = address(this); + quote.validUntilSlot = Epoch.wrap(3).toSlots(); + signedQuote = _quoteToSignedQuote(quote); warpToL2Slot(Constants.AZTEC_EPOCH_DURATION + rollup.CLAIM_DURATION_IN_L2_SLOTS() - 1); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); warpToL2Slot(Constants.AZTEC_EPOCH_DURATION * 3); rollup.prune(); _testBlock("mixed_block_1", false, Epoch.wrap(3).toSlots().unwrap()); - quote.quote.epochToProve = Epoch.wrap(3); + + quote.epochToProve = Epoch.wrap(3); + signedQuote = _quoteToSignedQuote(quote); vm.expectEmit(true, true, true, true); emit IRollup.ProofRightClaimed( - quote.quote.epochToProve, - address(this), - address(this), - quote.quote.bondAmount, - Epoch.wrap(3).toSlots() + quote.epochToProve, quote.prover, address(this), quote.bondAmount, Epoch.wrap(3).toSlots() ); - rollup.claimEpochProofRight(quote); + rollup.claimEpochProofRight(signedQuote); } function testPruneWhenNoProofClaim() public setUpFor("mixed_block_1") { @@ -701,6 +706,20 @@ contract RollupTest is DecoderBase { _submitEpochProof(rollup, 1, preArchive, data.archive, preBlockHash, wrongBlockHash, bytes32(0)); } + function _quoteToSignedQuote(EpochProofQuoteLib.EpochProofQuote memory _quote) + internal + view + returns (EpochProofQuoteLib.SignedEpochProofQuote memory) + { + bytes32 digest = rollup.quoteToDigest(_quote); + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234, digest); + return EpochProofQuoteLib.SignedEpochProofQuote({ + quote: _quote, + signature: SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}) + }); + } + function _testBlock(string memory name, bool _submitProof) public { _testBlock(name, _submitProof, 0); } diff --git a/l1-contracts/test/prover-coordination/Signatures.t.sol b/l1-contracts/test/prover-coordination/Signatures.t.sol deleted file mode 100644 index c9a1513e411..00000000000 --- a/l1-contracts/test/prover-coordination/Signatures.t.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity >=0.8.27; - -import {Test} from "forge-std/Test.sol"; - -import {Constants} from "@aztec/core/libraries/ConstantsGen.sol"; - -import {Registry} from "@aztec/governance/Registry.sol"; -import {Inbox} from "@aztec/core/messagebridge/Inbox.sol"; -import {Outbox} from "@aztec/core/messagebridge/Outbox.sol"; -import {Errors} from "@aztec/core/libraries/Errors.sol"; -import {Rollup} from "@aztec/core/Rollup.sol"; -import {IRollup} from "@aztec/core/interfaces/IRollup.sol"; -import {FeeJuicePortal} from "@aztec/core/FeeJuicePortal.sol"; -import {Leonidas} from "@aztec/core/Leonidas.sol"; - -import {SignatureLib} from "@aztec/core/libraries/crypto/SignatureLib.sol"; -import {EpochProofQuoteLib} from "@aztec/core/libraries/EpochProofQuoteLib.sol"; -import {Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; -import {Errors} from "@aztec/core/libraries/Errors.sol"; - -import {TestERC20} from "../TestERC20.sol"; - -import {SignatureChecker} from "@oz/utils/cryptography/SignatureChecker.sol"; - -// solhint-disable comprehensive-interface - -contract TestSignatures is Test { - Registry internal registry; - Inbox internal inbox; - Outbox internal outbox; - Rollup internal rollup; - TestERC20 internal testERC20; - FeeJuicePortal internal feeJuicePortal; - - modifier setup() { - registry = new Registry(address(this)); - testERC20 = new TestERC20(); - feeJuicePortal = new FeeJuicePortal(address(this)); - testERC20.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT); - feeJuicePortal.initialize( - address(registry), address(testERC20), bytes32(Constants.FEE_JUICE_ADDRESS) - ); - rollup = new Rollup(feeJuicePortal, bytes32(0), address(this), new address[](0)); - _; - } - - function testQuoteSignatures() public setup { - EpochProofQuoteLib.EpochProofQuote memory _quote = EpochProofQuoteLib.EpochProofQuote({ - epochToProve: Epoch.wrap(42), - validUntilSlot: Slot.wrap(100), - bondAmount: 1e18, - prover: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826, - basisPointFee: 5000 - }); - assertEq( - EpochProofQuoteLib.hash(_quote), - 0x4ed9cc91360dbd11218c8fcf4e8402f7a85d298dd4b4fb3d1fbcbb1a8ae72cc9, - "Invalid quote hash" - ); - - bytes32 digest = rollup.quoteToDigest(_quote); - - assertEq( - digest, 0x6927eba5b70276d7a9ccedac195c01844c8e71b3fdf9f8a5972914e8a2d5911d, "Invalid digest" - ); - - uint8 v = 27; - bytes32 r = 0x63e9cf4ee71dcfeae09da434f3e7c2d5c336b7d83b78fbbbe6d1639b206b5b69; - bytes32 s = 0x57b1e46efa6b388c4333c8dd40da6ec25297b6c277fba4f1b42e3b6f1d0ddfde; - EpochProofQuoteLib.SignedEpochProofQuote memory _signedQuote = EpochProofQuoteLib - .SignedEpochProofQuote({ - quote: _quote, - signature: SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}) - }); - - SignatureChecker.isValidSignatureNow( - _quote.prover, digest, SignatureLib.toBytes(_signedQuote.signature) - ); - } - - function testEmptySignatures() public { - EpochProofQuoteLib.EpochProofQuote memory _quote = EpochProofQuoteLib.EpochProofQuote({ - epochToProve: Epoch.wrap(42), - validUntilSlot: Slot.wrap(100), - bondAmount: 1000000000000000000, - prover: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826, - basisPointFee: 5000 - }); - uint8 v = 27; - bytes32 r = 0x63e9cf4ee71dcfeae09da434f3e7c2d5c336b7d83b78fbbbe6d1639b206b5b69; - bytes32 s = 0x57b1e46efa6b388c4333c8dd40da6ec25297b6c277fba4f1b42e3b6f1d0ddfde; - EpochProofQuoteLib.SignedEpochProofQuote memory _signedQuote = EpochProofQuoteLib - .SignedEpochProofQuote({ - quote: _quote, - signature: SignatureLib.Signature({isEmpty: false, v: v, r: r, s: s}) - }); - bytes32 digest = rollup.quoteToDigest(_quote); - - vm.expectRevert(); - SignatureChecker.isValidSignatureNow( - _quote.prover, digest, SignatureLib.toBytes(_signedQuote.signature) - ); - } -} diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index d1eec6cabc4..b7505e7058a 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -135,6 +135,7 @@ export { L1NotePayload, L1EventPayload, EpochProofQuote, + EpochProofQuotePayload, } from '@aztec/circuit-types'; export { NodeInfo } from '@aztec/types/interfaces'; diff --git a/yarn-project/circuit-types/src/mocks.ts b/yarn-project/circuit-types/src/mocks.ts index 4351dae7543..5c86312a170 100644 --- a/yarn-project/circuit-types/src/mocks.ts +++ b/yarn-project/circuit-types/src/mocks.ts @@ -27,7 +27,6 @@ import { } from '@aztec/circuits.js/testing'; import { type ContractArtifact, NoteSelector } from '@aztec/foundation/abi'; import { makeTuple } from '@aztec/foundation/array'; -import { Buffer32 } from '@aztec/foundation/buffer'; import { padArrayEnd, times } from '@aztec/foundation/collection'; import { randomBigInt, randomBytes, randomInt } from '@aztec/foundation/crypto'; import { Signature } from '@aztec/foundation/eth-signature'; @@ -234,7 +233,6 @@ export const mockEpochProofQuote = ( bondAmount?: bigint, proverAddress?: EthAddress, basisPointFee?: number, - domainSeparator?: Buffer32, ) => { const quotePayload: EpochProofQuotePayload = new EpochProofQuotePayload( epochToProve, @@ -242,7 +240,6 @@ export const mockEpochProofQuote = ( bondAmount ?? randomBigInt(10000n) + 1000n, proverAddress ?? EthAddress.random(), basisPointFee ?? randomInt(100), - domainSeparator ?? Buffer32.random(), ); const sig: Signature = Signature.empty(); return new EpochProofQuote(quotePayload, sig); diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts index a3df7b1a5c1..9ab6d7172c4 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.test.ts @@ -1,46 +1,17 @@ import { EthAddress } from '@aztec/circuits.js'; -import { Buffer32 } from '@aztec/foundation/buffer'; -import { Secp256k1Signer, keccak256 } from '@aztec/foundation/crypto'; -import { get712StructuredDigest, getHashedSignaturePayload } from '../p2p/signature_utils.js'; -import { EpochProofQuote } from './epoch_proof_quote.js'; import { EpochProofQuotePayload } from './epoch_proof_quote_payload.js'; describe('epoch proof quote', () => { it('should serialize / deserialize', () => { - const signer = Secp256k1Signer.random(); const payload = EpochProofQuotePayload.fromFields({ basisPointFee: 5000, bondAmount: 1000000000000000000n, epochToProve: 42n, prover: EthAddress.random(), validUntilSlot: 100n, - domainSeparator: Buffer32.random(), }); - const quote = EpochProofQuote.new(payload, signer); - expect(EpochProofQuote.fromBuffer(quote.toBuffer())).toEqual(quote); - expect(quote.senderAddress).toEqual(signer.address); - }); - - it('should be able to use eip 712', () => { - const payload = EpochProofQuotePayload.fromFields({ - epochToProve: 42n, - validUntilSlot: 100n, - bondAmount: 1000000000000000000n, - prover: EthAddress.fromString('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'), - basisPointFee: 5000, - domainSeparator: Buffer32.random(), - }); - - const hash = getHashedSignaturePayload(payload).to0xString(); - expect(hash).toEqual('0x4ed9cc91360dbd11218c8fcf4e8402f7a85d298dd4b4fb3d1fbcbb1a8ae72cc9'); - - const digest = get712StructuredDigest(payload).to0xString(); - expect(digest).toEqual('0x6927eba5b70276d7a9ccedac195c01844c8e71b3fdf9f8a5972914e8a2d5911d'); - - const signer = new Secp256k1Signer(Buffer32.fromBuffer(keccak256(Buffer.from('cow')))); - const quote = EpochProofQuote.new(payload, signer); - expect(quote.senderAddress.toString().toLowerCase()).toEqual('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826'); + expect(EpochProofQuotePayload.fromBuffer(payload.toBuffer())).toEqual(payload); }); }); diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts index 96fc396dfeb..43824aaf105 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts @@ -1,20 +1,16 @@ -import { type EthAddress } from '@aztec/circuits.js'; import { Buffer32 } from '@aztec/foundation/buffer'; -import { type Secp256k1Signer, recoverAddress } from '@aztec/foundation/crypto'; +import { type Secp256k1Signer } from '@aztec/foundation/crypto'; import { Signature } from '@aztec/foundation/eth-signature'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; import { Gossipable } from '../p2p/gossipable.js'; -import { get712StructuredDigest } from '../p2p/signature_utils.js'; import { TopicType, createTopicString } from '../p2p/topic_type.js'; import { EpochProofQuotePayload } from './epoch_proof_quote_payload.js'; export class EpochProofQuote extends Gossipable { static override p2pTopic: string = createTopicString(TopicType.epoch_proof_quote); - private sender: EthAddress | undefined; - constructor(public readonly payload: EpochProofQuotePayload, public readonly signature: Signature) { super(); } @@ -36,32 +32,27 @@ export class EpochProofQuote extends Gossipable { return new EpochProofQuote(reader.readObject(EpochProofQuotePayload), reader.readObject(Signature)); } - static new(payload: EpochProofQuotePayload, signer: Secp256k1Signer): EpochProofQuote { - const digest = get712StructuredDigest(payload); - const signature = signer.sign(digest); - return new EpochProofQuote(payload, signature); - } - - get senderAddress(): EthAddress { - if (!this.sender) { - const hashed = get712StructuredDigest(this.payload); - - // Cache the sender for later use - this.sender = recoverAddress(hashed, this.signature); + /** + * Creates a new quote with a signature. + * The digest provided must match what the rollup contract will produce i.e. `_hashTypedDataV4(EpochProofQuoteLib.hash(quote))` + * + * @param digest the digest of the payload that should be signed + * @param payload the actual quote + * @param signer the signer + * @returns a quote with an accompanying signature + */ + static new(digest: Buffer32, payload: EpochProofQuotePayload, signer: Secp256k1Signer): EpochProofQuote { + if (!payload.prover.equals(signer.address)) { + throw new Error(`Quote prover does not match signer. Prover [${payload.prover}], Signer [${signer.address}]`); } - - return this.sender; + const signature = signer.sign(digest); + const quote = new EpochProofQuote(payload, signature); + return quote; } toViemArgs() { return { - quote: { - epochToProve: this.payload.epochToProve, - validUntilSlot: this.payload.validUntilSlot, - bondAmount: this.payload.bondAmount, - prover: this.payload.prover.toString(), - basisPointFee: this.payload.basisPointFee, - }, + quote: this.payload.toViemArgs(), signature: this.signature.toViemSignature(), }; } diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts index 06f05a0f070..ca84ee02fe3 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts @@ -4,18 +4,18 @@ import { keccak256 } from '@aztec/foundation/crypto'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; +import { inspect } from 'util'; import { encodeAbiParameters, parseAbiParameters } from 'viem'; -import { type ScopedToDomain, type Signable } from '../p2p/signature_utils.js'; +import { type Signable } from '../p2p/signature_utils.js'; -export class EpochProofQuotePayload implements Signable, ScopedToDomain { +export class EpochProofQuotePayload implements Signable { constructor( public readonly epochToProve: bigint, public readonly validUntilSlot: bigint, public readonly bondAmount: bigint, public readonly prover: EthAddress, public readonly basisPointFee: number, - public readonly domainSeparator: Buffer32, ) {} static getFields(fields: FieldsOf) { @@ -25,7 +25,6 @@ export class EpochProofQuotePayload implements Signable, ScopedToDomain { fields.bondAmount, fields.prover, fields.basisPointFee, - fields.domainSeparator, ] as const; } @@ -41,7 +40,6 @@ export class EpochProofQuotePayload implements Signable, ScopedToDomain { reader.readUInt256(), reader.readObject(EthAddress), reader.readNumber(), - reader.readObject(Buffer32), ); } @@ -52,7 +50,6 @@ export class EpochProofQuotePayload implements Signable, ScopedToDomain { fields.bondAmount, fields.prover, fields.basisPointFee, - fields.domainSeparator, ); } @@ -78,4 +75,24 @@ export class EpochProofQuotePayload implements Signable, ScopedToDomain { // NOTE: trim the first two bytes to get rid of the `0x` prefix return Buffer.from(encodedData.slice(2), 'hex'); } + + toViemArgs(): { + epochToProve: bigint; + validUntilSlot: bigint; + bondAmount: bigint; + prover: `0x${string}`; + basisPointFee: number; + } { + return { + epochToProve: this.epochToProve, + validUntilSlot: this.validUntilSlot, + bondAmount: this.bondAmount, + prover: this.prover.toString(), + basisPointFee: this.basisPointFee, + }; + } + + [inspect.custom](): string { + return `EpochProofQuotePayload { epochToProve: ${this.epochToProve}, validUntilSlot: ${this.validUntilSlot}, bondAmount: ${this.bondAmount}, prover: ${this.prover}, basisPointFee: ${this.basisPointFee} }`; + } } diff --git a/yarn-project/end-to-end/Earthfile b/yarn-project/end-to-end/Earthfile index c349b153efb..f11e262c86e 100644 --- a/yarn-project/end-to-end/Earthfile +++ b/yarn-project/end-to-end/Earthfile @@ -172,6 +172,10 @@ e2e-token-contract: LOCALLY RUN ./scripts/e2e_test.sh ./src/e2e_token_contract +e2e-prover-coordination: + LOCALLY + RUN ./scripts/e2e_test.sh ./src/prover-coordination/e2e_json_coordination.test.ts + e2e-public-testnet: ARG L1_CHAIN_ID="31337" # automatically exported as ENV variables for below diff --git a/yarn-project/end-to-end/src/prover-coordination/e2e_json_coordination.test.ts b/yarn-project/end-to-end/src/prover-coordination/e2e_json_coordination.test.ts index 8ebe827b0bc..1a3daa2a931 100644 --- a/yarn-project/end-to-end/src/prover-coordination/e2e_json_coordination.test.ts +++ b/yarn-project/end-to-end/src/prover-coordination/e2e_json_coordination.test.ts @@ -2,18 +2,29 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AccountWalletWithSecretKey, type DebugLogger, - type EpochProofQuote, + EpochProofQuote, + EpochProofQuotePayload, EthCheatCodes, createDebugLogger, - mockEpochProofQuote, } from '@aztec/aztec.js'; import { AZTEC_EPOCH_DURATION, AZTEC_SLOT_DURATION, type AztecAddress, EthAddress } from '@aztec/circuits.js'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { times } from '@aztec/foundation/collection'; +import { Secp256k1Signer, keccak256, randomBigInt, randomInt } from '@aztec/foundation/crypto'; import { RollupAbi } from '@aztec/l1-artifacts'; import { StatefulTestContract } from '@aztec/noir-contracts.js'; import { beforeAll } from '@jest/globals'; -import { type PublicClient, getAddress, getContract } from 'viem'; +import { + type Account, + type Chain, + type GetContractReturnType, + type HttpTransport, + type PublicClient, + type WalletClient, + getAddress, + getContract, +} from 'viem'; import { type ISnapshotManager, @@ -22,16 +33,12 @@ import { createSnapshotManager, } from '../fixtures/snapshot_manager.js'; -// Tests simple block building with a sequencer that does not upload proofs to L1, -// and then follows with a prover node run (with real proofs disabled, but -// still simulating all circuits via a prover-client), in order to test -// the coordination through L1 between the sequencer and the prover node. -describe('e2e_prover_node', () => { +describe('e2e_json_coordination', () => { let ctx: SubsystemsContext; let wallet: AccountWalletWithSecretKey; let recipient: AztecAddress; let contract: StatefulTestContract; - let rollupContract: any; + let rollupContract: GetContractReturnType>; let publicClient: PublicClient; let cc: EthCheatCodes; let publisherAddress: EthAddress; @@ -41,7 +48,14 @@ describe('e2e_prover_node', () => { beforeAll(async () => { logger = createDebugLogger('aztec:prover_coordination:e2e_json_coordination'); - snapshotManager = createSnapshotManager(`prover_coordination/e2e_json_coordination`, process.env.E2E_DATA_PATH); + snapshotManager = createSnapshotManager( + `prover_coordination/e2e_json_coordination`, + process.env.E2E_DATA_PATH, + {}, + { + assumeProvenThrough: undefined, + }, + ); await snapshotManager.snapshot('setup', addAccounts(2, logger), async ({ accountKeys }, ctx) => { const accountManagers = accountKeys.map(ak => getSchnorrAccount(ctx.pxe, ak[0], ak[1], 1)); @@ -79,12 +93,19 @@ describe('e2e_prover_node', () => { }); }); - const expectProofClaimOnL1 = async (quote: EpochProofQuote, proposerAddress: EthAddress) => { - const claimFromContract = await rollupContract.read.proofClaim(); - expect(claimFromContract[0]).toEqual(quote.payload.epochToProve); - expect(claimFromContract[1]).toEqual(BigInt(quote.payload.basisPointFee)); - expect(claimFromContract[2]).toEqual(quote.payload.bondAmount); - expect(claimFromContract[4]).toEqual(proposerAddress.toChecksumString()); + const expectProofClaimOnL1 = async (expected: { + epochToProve: bigint; + basisPointFee: number; + bondAmount: bigint; + proposer: EthAddress; + prover: EthAddress; + }) => { + const [epochToProve, basisPointFee, bondAmount, prover, proposer] = await rollupContract.read.proofClaim(); + expect(epochToProve).toEqual(expected.epochToProve); + expect(basisPointFee).toEqual(BigInt(expected.basisPointFee)); + expect(bondAmount).toEqual(expected.bondAmount); + expect(prover).toEqual(expected.prover.toChecksumString()); + expect(proposer).toEqual(expected.proposer.toChecksumString()); }; const getL1Timestamp = async () => { @@ -110,7 +131,11 @@ describe('e2e_prover_node', () => { }; const getEpochToProve = async () => { - return await rollupContract.read.getEpochToProve(); + return await rollupContract.read.getEpochToProve().catch(e => { + if (e instanceof Error && e.message.includes('NoEpochToProve')) { + return undefined; + } + }); }; const logState = async () => { @@ -130,18 +155,44 @@ describe('e2e_prover_node', () => { await logState(); }; + const makeEpochProofQuote = async ({ + epochToProve, + validUntilSlot, + bondAmount, + prover, + basisPointFee, + signer, + }: { + epochToProve: bigint; + validUntilSlot?: bigint; + bondAmount?: bigint; + prover?: EthAddress; + basisPointFee?: number; + signer?: Secp256k1Signer; + }) => { + signer ??= new Secp256k1Signer(Buffer32.fromBuffer(keccak256(Buffer.from('cow')))); + const quotePayload: EpochProofQuotePayload = new EpochProofQuotePayload( + epochToProve, + validUntilSlot ?? randomBigInt(10000n), + bondAmount ?? randomBigInt(10000n) + 1000n, + prover ?? EthAddress.fromString('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'), + basisPointFee ?? randomInt(100), + ); + const digest = await rollupContract.read.quoteToDigest([quotePayload.toViemArgs()]); + return EpochProofQuote.new(Buffer32.fromString(digest), quotePayload, signer); + }; + it('Sequencer selects best valid proving quote for each block', async () => { // We want to create a set of proving quotes, some valid and some invalid // The sequencer should select the cheapest valid quote when it proposes the block // Here we are creating a proof quote for epoch 0, this will NOT get used yet - const quoteForEpoch0 = mockEpochProofQuote( - 0n, // epoch 0 - BigInt(AZTEC_EPOCH_DURATION + 10), // valid until slot 10 into epoch 1 - 10000n, - EthAddress.random(), - 1, - ); + const quoteForEpoch0 = await makeEpochProofQuote({ + epochToProve: 0n, + validUntilSlot: BigInt(AZTEC_EPOCH_DURATION + 10), + bondAmount: 10000n, + basisPointFee: 1, + }); // Send in the quote await ctx.proverNode.sendEpochProofQuote(quoteForEpoch0); @@ -154,16 +205,14 @@ describe('e2e_prover_node', () => { const epoch0BlockNumber = await getPendingBlockNumber(); // Verify that the claim state on L1 is unitialised - const uninitialisedProofClaim = mockEpochProofQuote( - 0n, // epoch 0 - BigInt(0), - 0n, - EthAddress.random(), - 0, - ); - // The rollup contract should have an uninitialised proof claim struct - await expectProofClaimOnL1(uninitialisedProofClaim, EthAddress.ZERO); + await expectProofClaimOnL1({ + epochToProve: 0n, + basisPointFee: 0, + bondAmount: 0n, + prover: EthAddress.ZERO, + proposer: EthAddress.ZERO, + }); // Now go to epoch 1 await advanceToNextEpoch(); @@ -176,7 +225,7 @@ describe('e2e_prover_node', () => { const epoch1BlockNumber = await getPendingBlockNumber(); // Check it was published - await expectProofClaimOnL1(quoteForEpoch0, publisherAddress); + await expectProofClaimOnL1({ ...quoteForEpoch0.payload, proposer: publisherAddress }); // now 'prove' epoch 0 await rollupContract.write.setAssumeProvenThroughBlockNumber([BigInt(epoch0BlockNumber)]); @@ -189,15 +238,37 @@ describe('e2e_prover_node', () => { const currentSlot = await getSlot(); // Now create a number of quotes, some valid some invalid for epoch 1, the lowest priced valid quote should be chosen - const validQuotes = times(3, (i: number) => - mockEpochProofQuote(1n, currentSlot + 2n, 10000n, EthAddress.random(), 10 + i), + const validQuotes = await Promise.all( + times(3, (i: number) => + makeEpochProofQuote({ + epochToProve: 1n, + validUntilSlot: currentSlot + 2n, + bondAmount: 10000n, + basisPointFee: 10 + i, + }), + ), ); - const proofQuoteInvalidSlot = mockEpochProofQuote(1n, 3n, 10000n, EthAddress.random(), 1); + const proofQuoteInvalidSlot = await makeEpochProofQuote({ + epochToProve: 1n, + validUntilSlot: 3n, + bondAmount: 10000n, + basisPointFee: 1, + }); - const proofQuoteInvalidEpoch = mockEpochProofQuote(2n, currentSlot + 4n, 10000n, EthAddress.random(), 2); + const proofQuoteInvalidEpoch = await makeEpochProofQuote({ + epochToProve: 2n, + validUntilSlot: currentSlot + 4n, + bondAmount: 10000n, + basisPointFee: 2, + }); - const proofQuoteInsufficientBond = mockEpochProofQuote(1n, currentSlot + 4n, 0n, EthAddress.random(), 3); + const proofQuoteInsufficientBond = await makeEpochProofQuote({ + epochToProve: 1n, + validUntilSlot: currentSlot + 4n, + bondAmount: 0n, + basisPointFee: 3, + }); const allQuotes = [proofQuoteInvalidSlot, proofQuoteInvalidEpoch, ...validQuotes, proofQuoteInsufficientBond]; @@ -208,12 +279,12 @@ describe('e2e_prover_node', () => { const expectedQuote = validQuotes[0]; - await expectProofClaimOnL1(expectedQuote, publisherAddress); + await expectProofClaimOnL1({ ...expectedQuote.payload, proposer: publisherAddress }); // building another block should succeed, we should not try and submit another quote await contract.methods.create_note(recipient, recipient, 10).send().wait(); - await expectProofClaimOnL1(expectedQuote, publisherAddress); + await expectProofClaimOnL1({ ...expectedQuote.payload, proposer: publisherAddress }); // now 'prove' epoch 1 await rollupContract.write.setAssumeProvenThroughBlockNumber([BigInt(epoch1BlockNumber)]); @@ -225,6 +296,6 @@ describe('e2e_prover_node', () => { await contract.methods.create_note(recipient, recipient, 10).send().wait(); // The quote state on L1 is the same as before - await expectProofClaimOnL1(expectedQuote, publisherAddress); + await expectProofClaimOnL1({ ...expectedQuote.payload, proposer: publisherAddress }); }); }); diff --git a/yarn-project/end-to-end/src/prover-coordination/e2e_quote_signing.test.ts b/yarn-project/end-to-end/src/prover-coordination/e2e_quote_signing.test.ts new file mode 100644 index 00000000000..5dbdffcf3d7 --- /dev/null +++ b/yarn-project/end-to-end/src/prover-coordination/e2e_quote_signing.test.ts @@ -0,0 +1,66 @@ +import { EpochProofQuote, EpochProofQuotePayload } from '@aztec/aztec.js'; +import { EthAddress } from '@aztec/circuits.js'; +import { Buffer32 } from '@aztec/foundation/buffer'; +import { Secp256k1Signer, keccak256 } from '@aztec/foundation/crypto'; +import { RollupAbi } from '@aztec/l1-artifacts'; + +import { beforeAll } from '@jest/globals'; +import { + type Chain, + type GetContractReturnType, + type HttpTransport, + type PublicClient, + getAddress, + getContract, +} from 'viem'; + +import { type ISnapshotManager, type SubsystemsContext, createSnapshotManager } from '../fixtures/snapshot_manager.js'; + +/** + * Tests the creation of epoch proof quotes and their validation on L1 + */ +describe('e2e_quote_signature_validation', () => { + let ctx: SubsystemsContext; + let rollupContract: GetContractReturnType>; + + // let logger: DebugLogger; + let snapshotManager: ISnapshotManager; + + beforeAll(async () => { + snapshotManager = createSnapshotManager(`prover_coordination/e2e_quote_signing`, process.env.E2E_DATA_PATH); + + ctx = await snapshotManager.setup(); + + await ctx.proverNode.stop(); + + rollupContract = getContract({ + address: getAddress(ctx.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString()), + abi: RollupAbi, + client: ctx.deployL1ContractsValues.walletClient, + }); + }); + + it('can get domain information from the rollup', async () => { + const [, name, version, chainId, address] = await rollupContract.read.eip712Domain(); + expect(name).toBe('Aztec Rollup'); + expect(version).toBe('1'); + expect(chainId).toBe(31337n); + expect(address).toBe(rollupContract.address); + }); + + it('can verify a signed quote on L1', async () => { + const payload = EpochProofQuotePayload.fromFields({ + epochToProve: 42n, + validUntilSlot: 100n, + bondAmount: 1000000000000000000n, + prover: EthAddress.fromString('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'), + basisPointFee: 5000, + }); + + const signer = new Secp256k1Signer(Buffer32.fromBuffer(keccak256(Buffer.from('cow')))); + const digest = await rollupContract.read.quoteToDigest([payload.toViemArgs()]); + const quote = EpochProofQuote.new(Buffer32.fromString(digest), payload, signer); + + await rollupContract.read.verifySignedQuote([quote.toViemArgs()]); + }); +}); diff --git a/yarn-project/p2p/src/epoch_proof_quote_pool/test_utils.ts b/yarn-project/p2p/src/epoch_proof_quote_pool/test_utils.ts index a6b9aceb3de..baa5f9e2425 100644 --- a/yarn-project/p2p/src/epoch_proof_quote_pool/test_utils.ts +++ b/yarn-project/p2p/src/epoch_proof_quote_pool/test_utils.ts @@ -10,7 +10,6 @@ export function makeRandomEpochProofQuotePayload(): EpochProofQuotePayload { epochToProve: randomBigInt(1000000n), prover: EthAddress.random(), validUntilSlot: randomBigInt(1000000n), - domainSeparator: Buffer32.random(), }); } @@ -21,7 +20,7 @@ export function makeRandomEpochProofQuote(payload?: EpochProofQuotePayload): { const signer = Secp256k1Signer.random(); return { - quote: EpochProofQuote.new(payload ?? makeRandomEpochProofQuotePayload(), signer), + quote: EpochProofQuote.new(Buffer32.random(), payload ?? makeRandomEpochProofQuotePayload(), signer), signer, }; } diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index c074423b057..99df20363d1 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -28,6 +28,7 @@ import { RollupAbi } from '@aztec/l1-artifacts'; import { type TelemetryClient } from '@aztec/telemetry-client'; import pick from 'lodash.pick'; +import { inspect } from 'util'; import { ContractFunctionRevertedError, type GetContractReturnType, @@ -294,54 +295,6 @@ export class L1Publisher { return false; } - { - const timer = new Timer(); - - // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available - // This means that we can avoid the simulation issues in later checks. - // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which - // make time consistency checks break. - await this.validateBlockForSubmission(block.header, { - digest: digest.toBuffer(), - signatures: attestations ?? [], - }); - - const txHash = await this.sendProposeTx(proposeTxArgs); - - if (!txHash) { - this.log.info(`Failed to publish block ${block.number} to L1`, ctx); - return false; - } - - const receipt = await this.getTransactionReceipt(txHash); - if (!receipt) { - this.log.info(`Failed to get receipt for tx ${txHash}`, ctx); - return false; - } - - // Tx was mined successfully - if (receipt.status) { - const tx = await this.getTransactionStats(txHash); - const stats: L1PublishBlockStats = { - ...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'), - ...pick(tx!, 'calldataGas', 'calldataSize'), - ...block.getStats(), - eventName: 'rollup-published-to-l1', - }; - this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx }); - this.metrics.recordProcessBlockTx(timer.ms(), stats); - - return true; - } - - this.metrics.recordFailedTx('process'); - this.log.error(`Rollup.process tx status failed ${receipt.transactionHash}`, { - ...ctx, - ...receipt, - }); - await this.sleepOrInterrupted(); - } - const timer = new Timer(); // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available @@ -617,6 +570,8 @@ export class L1Publisher { encodedData, L1Publisher.PROPOSE_AND_CLAIM_GAS_GUESS, ); + this.log.info(`ProposeAndClaim`); + this.log.info(inspect(quote.payload)); return await this.rollupContract.write.proposeAndClaim([...args, quote.toViemArgs()], { account: this.account, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 0195d1afc3a..3e83d8a40c0 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -32,6 +32,8 @@ import { type PublicProcessorFactory } from '@aztec/simulator'; import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec/telemetry-client'; import { type ValidatorClient } from '@aztec/validator-client'; +import { inspect } from 'util'; + import { type BlockBuilderFactory } from '../block_builder/index.js'; import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js'; import { type L1Publisher } from '../publisher/l1-publisher.js'; @@ -491,6 +493,8 @@ export class Sequencer { const proofQuote = await proofQuotePromise; + this.log.verbose(proofQuote ? `Using proof quote ${inspect(proofQuote.payload)}` : 'No proof quote available'); + try { await this.publishL2Block(block, attestations, txHashes, proofQuote); this.metrics.recordPublishedBlock(workDuration); @@ -562,6 +566,9 @@ export class Sequencer { // Get quotes for the epoch to be proven const quotes = await this.p2pClient.getEpochProofQuotes(epochToProve); this.log.verbose(`Retrieved ${quotes.length} quotes, slot: ${slotNumber}, epoch to prove: ${epochToProve}`); + for (const quote of quotes) { + this.log.verbose(inspect(quote.payload)); + } // ensure these quotes are still valid for the slot and have the contract validate them const validQuotesPromise = Promise.all( quotes.filter(x => x.payload.validUntilSlot >= slotNumber).map(x => this.publisher.validateProofQuote(x)), From 361db07bb33ccaff63857c0e77fc2de449891398 Mon Sep 17 00:00:00 2001 From: Mitch Date: Mon, 30 Sep 2024 20:52:14 -0400 Subject: [PATCH 5/7] remove dead code --- .../src/core/libraries/EpochProofQuoteLib.sol | 13 -------- l1-contracts/src/core/libraries/Errors.sol | 6 ++-- .../test/prover-coordination/EscrowERC20.sol | 15 ---------- .../ProofCommitmentEscrow.t.sol | 6 ++-- .../circuit-types/src/p2p/signature_utils.ts | 10 ------- .../epoch_proof_quote_payload.ts | 30 +------------------ 6 files changed, 7 insertions(+), 73 deletions(-) delete mode 100644 l1-contracts/test/prover-coordination/EscrowERC20.sol diff --git a/l1-contracts/src/core/libraries/EpochProofQuoteLib.sol b/l1-contracts/src/core/libraries/EpochProofQuoteLib.sol index 50a9658371c..1aba031a64d 100644 --- a/l1-contracts/src/core/libraries/EpochProofQuoteLib.sol +++ b/l1-contracts/src/core/libraries/EpochProofQuoteLib.sol @@ -48,17 +48,4 @@ library EpochProofQuoteLib { ) ); } - - function toDigest(EpochProofQuote memory quote, bytes32 domainSeparator) - internal - pure - returns (bytes32) - { - return keccak256(abi.encodePacked("\x19\x01", domainSeparator, hash(quote))); - } - - function verify(SignedEpochProofQuote memory quote, bytes32 domainSeparator) internal pure { - bytes32 digest = toDigest(quote.quote, domainSeparator); - SignatureLib.verify(quote.signature, quote.quote.prover, digest); - } } diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index c0756bd9a20..8df16131fd1 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -102,7 +102,7 @@ library Errors { error FeeJuicePortal__Unauthorized(); // 0x67e3691e // Proof Commitment Escrow - error ProofCommitmentEscrow__InsufficientBalance(uint256 balance, uint256 requested); - error ProofCommitmentEscrow__NotOwner(address caller); - error ProofCommitmentEscrow__WithdrawRequestNotReady(uint256 current, Timestamp readyAt); + error ProofCommitmentEscrow__InsufficientBalance(uint256 balance, uint256 requested); // 0x09b8b789 + error ProofCommitmentEscrow__NotOwner(address caller); // 0x2ac332c1 + error ProofCommitmentEscrow__WithdrawRequestNotReady(uint256 current, Timestamp readyAt); // 0xb32ab8a7 } diff --git a/l1-contracts/test/prover-coordination/EscrowERC20.sol b/l1-contracts/test/prover-coordination/EscrowERC20.sol deleted file mode 100644 index 48b743e825c..00000000000 --- a/l1-contracts/test/prover-coordination/EscrowERC20.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2024 Aztec Labs. -pragma solidity ^0.8.27; - -import {ERC20} from "@oz/token/ERC20/ERC20.sol"; - -// solhint-disable comprehensive-interface - -contract EscrowERC20 is ERC20 { - constructor() ERC20("TestToken", "TST") {} - - function mint(address to, uint256 amount) external { - _mint(to, amount); - } -} diff --git a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol index 1c4c2d1cc6c..cb3501ad842 100644 --- a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol +++ b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol @@ -8,16 +8,16 @@ import {ProofCommitmentEscrow} from "@aztec/core/ProofCommitmentEscrow.sol"; import {Errors} from "@aztec/core/libraries/Errors.sol"; import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; -import {EscrowERC20} from "./EscrowERC20.sol"; +import {TestERC20} from "../TestERC20.sol"; // solhint-disable comprehensive-interface contract TestProofCommitmentEscrow is Test { ProofCommitmentEscrow internal _escrow; - EscrowERC20 internal _token; + TestERC20 internal _token; modifier setup() { - _token = new EscrowERC20(); + _token = new TestERC20(); _escrow = new ProofCommitmentEscrow(_token, address(this)); _; } diff --git a/yarn-project/circuit-types/src/p2p/signature_utils.ts b/yarn-project/circuit-types/src/p2p/signature_utils.ts index 80c1d53b933..b06cbdf5f92 100644 --- a/yarn-project/circuit-types/src/p2p/signature_utils.ts +++ b/yarn-project/circuit-types/src/p2p/signature_utils.ts @@ -5,10 +5,6 @@ export interface Signable { getPayloadToSign(): Buffer; } -export interface ScopedToDomain { - domainSeparator: Buffer32; -} - /** * Get the hashed payload for the signature of the `Signable` * @param s - The `Signable` to sign @@ -27,9 +23,3 @@ export function getHashedSignaturePayloadEthSignedMessage(s: Signable): Buffer32 const payload = getHashedSignaturePayload(s); return makeEthSignDigest(payload); } - -export function get712StructuredDigest(s: Signable & ScopedToDomain): Buffer32 { - return Buffer32.fromBuffer( - keccak256(Buffer.concat([Buffer.from('\x19\x01'), s.domainSeparator.buffer, keccak256(s.getPayloadToSign())])), - ); -} diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts index ca84ee02fe3..e7b62c707f9 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote_payload.ts @@ -1,15 +1,10 @@ import { EthAddress } from '@aztec/circuits.js'; -import { Buffer32 } from '@aztec/foundation/buffer'; -import { keccak256 } from '@aztec/foundation/crypto'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; import { inspect } from 'util'; -import { encodeAbiParameters, parseAbiParameters } from 'viem'; -import { type Signable } from '../p2p/signature_utils.js'; - -export class EpochProofQuotePayload implements Signable { +export class EpochProofQuotePayload { constructor( public readonly epochToProve: bigint, public readonly validUntilSlot: bigint, @@ -53,29 +48,6 @@ export class EpochProofQuotePayload implements Signable { ); } - static TYPE_HASH = Buffer32.fromBuffer( - keccak256( - Buffer.from( - 'EpochProofQuote(uint256 epochToProve,uint256 validUntilSlot,uint256 bondAmount,address prover,uint32 basisPointFee)', - ), - ), - ); - - getPayloadToSign(): Buffer { - const abi = parseAbiParameters('bytes32, uint256, uint256, uint256, address, uint32'); - const encodedData = encodeAbiParameters(abi, [ - EpochProofQuotePayload.TYPE_HASH.to0xString(), - this.epochToProve, - this.validUntilSlot, - this.bondAmount, - this.prover.toString(), - this.basisPointFee, - ] as const); - - // NOTE: trim the first two bytes to get rid of the `0x` prefix - return Buffer.from(encodedData.slice(2), 'hex'); - } - toViemArgs(): { epochToProve: bigint; validUntilSlot: bigint; From 9a93591154e63adfde84b5d1e75ecc7e9519abbc Mon Sep 17 00:00:00 2001 From: Mitch Date: Mon, 30 Sep 2024 21:23:56 -0400 Subject: [PATCH 6/7] make updates to test fix formatting --- l1-contracts/test/portals/UniswapPortal.t.sol | 6 +- .../ProofCommitmentEscrow.t.sol | 113 ++++++++++-------- 2 files changed, 67 insertions(+), 52 deletions(-) diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index 9c7044eae9f..afb986f9046 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -90,7 +90,7 @@ contract UniswapPortalTest is Test { recipient: DataStructures.L1Actor(address(daiTokenPortal), block.chainid), content: Hash.sha256ToField( abi.encodeWithSignature("withdraw(address,uint256,address)", _recipient, amount, _caller) - ) + ) }); return message.sha256ToField(); @@ -122,7 +122,7 @@ contract UniswapPortalTest is Test { secretHash, _caller ) - ) + ) }); return message.sha256ToField(); @@ -153,7 +153,7 @@ contract UniswapPortalTest is Test { secretHash, _caller ) - ) + ) }); return message.sha256ToField(); diff --git a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol index cb3501ad842..e0fcea53610 100644 --- a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol +++ b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol @@ -15,6 +15,8 @@ import {TestERC20} from "../TestERC20.sol"; contract TestProofCommitmentEscrow is Test { ProofCommitmentEscrow internal _escrow; TestERC20 internal _token; + address internal prover; + uint256 internal depositAmount; modifier setup() { _token = new TestERC20(); @@ -22,10 +24,22 @@ contract TestProofCommitmentEscrow is Test { _; } - function testDeposit() public setup { - address prover = address(42); - uint256 depositAmount = 100; - _mintAndDeposit(prover, depositAmount); + modifier setupWithApproval(address _prover, uint256 _depositAmount) { + _token = new TestERC20(); + _escrow = new ProofCommitmentEscrow(_token, address(this)); + + _token.mint(_prover, _depositAmount); + vm.prank(_prover); + _token.approve(address(_escrow), _depositAmount); + + prover = _prover; + depositAmount = _depositAmount; + _; + } + + function testDeposit() public setupWithApproval(address(42), 100) { + vm.prank(prover); + _escrow.deposit(depositAmount); assertEq( _token.balanceOf(address(_escrow)), @@ -35,9 +49,9 @@ contract TestProofCommitmentEscrow is Test { assertEq(_token.balanceOf(prover), 0, "Prover balance should be 0 after deposit"); } - function testCannotWithdrawWithoutMatureRequest() public setup { - address prover = address(42); - uint256 depositAmount = 100; + function testCannotWithdrawWithoutMatureRequest() public setupWithApproval(address(42), 100) { + vm.prank(prover); + _escrow.deposit(depositAmount); uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); _mintAndDeposit(prover, depositAmount); @@ -67,9 +81,9 @@ contract TestProofCommitmentEscrow is Test { _escrow.executeWithdraw(); } - function testWithdrawAfterDelay() public setup { - address prover = address(42); - uint256 depositAmount = 100; + function testWithdrawAfterDelay() public setupWithApproval(address(42), 100) { + vm.prank(prover); + _escrow.deposit(depositAmount); uint256 withdrawAmount = 50; uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); @@ -91,9 +105,9 @@ contract TestProofCommitmentEscrow is Test { assertEq(_token.balanceOf(prover), withdrawAmount, "Prover balance should match deposit amount"); } - function testCannotReplayWithdrawRequest() public setup { - address prover = address(42); - uint256 depositAmount = 100; + function testCannotReplayWithdrawRequest() public setupWithApproval(address(42), 100) { + vm.prank(prover); + _escrow.deposit(depositAmount); uint256 withdrawAmount = 50; uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); @@ -116,16 +130,18 @@ contract TestProofCommitmentEscrow is Test { ); } - function testOnlyOwnerCanStake() public setup { - address prover = address(42); - vm.prank(prover); - vm.expectRevert(abi.encodeWithSelector(Errors.ProofCommitmentEscrow__NotOwner.selector, prover)); + function testOnlyOwnerCanStake(address nonOwner) public setup { + vm.assume(nonOwner != address(this)); + vm.prank(nonOwner); + vm.expectRevert( + abi.encodeWithSelector(Errors.ProofCommitmentEscrow__NotOwner.selector, nonOwner) + ); _escrow.stakeBond(0, address(0)); } - function testCannotStakeMoreThanProverBalance() public setup { - address prover = address(42); - uint256 depositAmount = 100; + function testCannotStakeMoreThanProverBalance() public setupWithApproval(address(42), 100) { + vm.prank(prover); + _escrow.deposit(depositAmount); uint256 stakeAmount = depositAmount + 1; _mintAndDeposit(prover, depositAmount); @@ -141,16 +157,18 @@ contract TestProofCommitmentEscrow is Test { assertEq(_escrow.deposits(prover), depositAmount, "Prover balance should match deposit amount"); } - function testOnlyOwnerCanUnstake() public setup { - address prover = address(42); - vm.prank(prover); - vm.expectRevert(abi.encodeWithSelector(Errors.ProofCommitmentEscrow__NotOwner.selector, prover)); + function testOnlyOwnerCanUnstake(address nonOwner) public setup { + vm.assume(nonOwner != address(this)); + vm.prank(nonOwner); + vm.expectRevert( + abi.encodeWithSelector(Errors.ProofCommitmentEscrow__NotOwner.selector, nonOwner) + ); _escrow.unstakeBond(); } - function testStakeAndUnstake() public setup { - address prover = address(42); - uint256 depositAmount = 100; + function testStakeAndUnstake() public setupWithApproval(address(42), 100) { + vm.prank(prover); + _escrow.deposit(depositAmount); uint256 stakeAmount = 50; _mintAndDeposit(prover, depositAmount); @@ -169,7 +187,6 @@ contract TestProofCommitmentEscrow is Test { } function testOverwritingStakeSlashesPreviousProver() public setup { - // Arrange address proverA = address(42); address proverB = address(43); uint256 depositAmountA = 100; @@ -177,12 +194,22 @@ contract TestProofCommitmentEscrow is Test { uint256 depositAmountB = 200; uint256 stakeAmountB = 100; + _token.mint(proverA, depositAmountA); + vm.prank(proverA); + _token.approve(address(_escrow), depositAmountA); + vm.prank(proverA); + _escrow.deposit(depositAmountA); + + _token.mint(proverB, depositAmountB); + vm.prank(proverB); + _token.approve(address(_escrow), depositAmountB); + vm.prank(proverB); + _escrow.deposit(depositAmountB); + // Prover A deposits and is staked - _mintAndDeposit(proverA, depositAmountA); _escrow.stakeBond(stakeAmountA, proverA); // Prover B deposits and owner overwrites the stake - _mintAndDeposit(proverB, depositAmountB); _escrow.stakeBond(stakeAmountB, proverB); // Prover A cannot recover the staked amount @@ -207,16 +234,14 @@ contract TestProofCommitmentEscrow is Test { ); } - function testWithdrawRequestOverwriting() public setup { - // Arrange - address prover = address(42); - uint256 depositAmount = 100; + function testWithdrawRequestOverwriting() public setupWithApproval(address(42), 100) { uint256 withdrawAmountA = 40; uint256 withdrawAmountB = 60; uint256 withdrawReadyAtA = block.timestamp + _escrow.WITHDRAW_DELAY(); uint256 withdrawReadyAtB = block.timestamp + 2 * _escrow.WITHDRAW_DELAY(); - _mintAndDeposit(prover, depositAmount); + vm.prank(prover); + _escrow.deposit(depositAmount); // Prover starts first withdraw request vm.prank(prover); @@ -252,14 +277,12 @@ contract TestProofCommitmentEscrow is Test { ); } - function testMinBalanceAtSlot() public setup { - // Arrange - address prover = address(42); - uint256 depositAmount = 100; + function testMinBalanceAtSlot() public setupWithApproval(address(42), 100) { uint256 withdrawAmount = 25; Timestamp withdrawReadyAt = Timestamp.wrap(block.timestamp + _escrow.WITHDRAW_DELAY()); - _mintAndDeposit(prover, depositAmount); + vm.prank(prover); + _escrow.deposit(depositAmount); assertEq( _escrow.minBalanceAtTime(Timestamp.wrap(block.timestamp), prover), @@ -303,13 +326,5 @@ contract TestProofCommitmentEscrow is Test { ); } - function _mintAndDeposit(address _prover, uint256 _amount) internal { - _token.mint(_prover, _amount); - - vm.prank(_prover); - _token.approve(address(_escrow), _amount); - - vm.prank(_prover); - _escrow.deposit(_amount); - } + function _mintAndDeposit(address _prover, uint256 _amount) internal {} } From a4d67efc5ead2d8232cf68f29f745fdc174c8be6 Mon Sep 17 00:00:00 2001 From: Mitch Date: Tue, 1 Oct 2024 10:57:51 -0400 Subject: [PATCH 7/7] cleanup --- .../src/core/ProofCommitmentEscrow.sol | 16 +- l1-contracts/src/core/Rollup.sol | 14 +- .../interfaces/IProofCommitmentEscrow.sol | 6 +- l1-contracts/src/core/interfaces/IRollup.sol | 3 - .../src/mock/MockProofCommitmentEscrow.sol | 4 +- .../ProofCommitmentEscrow.t.sol | 187 ++++++++---------- .../prover_coordination/epoch_proof_quote.ts | 1 + .../e2e_quote_signing.test.ts | 66 ------- 8 files changed, 98 insertions(+), 199 deletions(-) delete mode 100644 yarn-project/end-to-end/src/prover-coordination/e2e_quote_signing.test.ts diff --git a/l1-contracts/src/core/ProofCommitmentEscrow.sol b/l1-contracts/src/core/ProofCommitmentEscrow.sol index 7f63cd17aa3..a29f92ac0c4 100644 --- a/l1-contracts/src/core/ProofCommitmentEscrow.sol +++ b/l1-contracts/src/core/ProofCommitmentEscrow.sol @@ -13,11 +13,6 @@ import {Timestamp} from "@aztec/core/libraries/TimeMath.sol"; contract ProofCommitmentEscrow is IProofCommitmentEscrow { using SafeERC20 for IERC20; - struct Stake { - uint256 amount; - address prover; - } - struct WithdrawRequest { uint256 amount; Timestamp executableAt; @@ -29,7 +24,6 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { mapping(address => uint256) public deposits; mapping(address => WithdrawRequest) public withdrawRequests; IERC20 public immutable token; - Stake public stake; modifier onlyRollup() { require(msg.sender == ROLLUP, Errors.ProofCommitmentEscrow__NotOwner(msg.sender)); @@ -103,9 +97,8 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { * The prover must have sufficient balance * The prover's balance will be reduced by the bond amount */ - function stakeBond(uint256 _amount, address _prover) external override onlyRollup { + function stakeBond(address _prover, uint256 _amount) external override onlyRollup { deposits[_prover] -= _amount; - stake = Stake({amount: _amount, prover: _prover}); emit StakeBond(_prover, _amount); } @@ -115,9 +108,10 @@ contract ProofCommitmentEscrow is IProofCommitmentEscrow { * * @dev Only callable by the owner */ - function unstakeBond() external override onlyRollup { - deposits[stake.prover] += stake.amount; - delete stake; + function unstakeBond(address _prover, uint256 _amount) external override onlyRollup { + deposits[_prover] += _amount; + + emit UnstakeBond(_prover, _amount); } /** diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index e856da11b2e..de9e5c88ea7 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -117,16 +117,6 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { return _hashTypedDataV4(EpochProofQuoteLib.hash(quote)); } - function verifySignedQuote(EpochProofQuoteLib.SignedEpochProofQuote memory signedQuote) - public - view - override(IRollup) - { - bytes32 digest = quoteToDigest(signedQuote.quote); - address recoveredSigner = ECDSA.recover(digest, SignatureLib.toBytes(signedQuote.signature)); - require(recoveredSigner == signedQuote.quote.prover); - } - /** * @notice Prune the pending chain up to the last proven block * @@ -358,7 +348,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { // We don't currently unstake, // but we will as part of https://github.com/AztecProtocol/aztec-packages/issues/8652. // Blocked on submitting epoch proofs to this contract. - PROOF_COMMITMENT_ESCROW.stakeBond(_quote.quote.bondAmount, _quote.quote.prover); + PROOF_COMMITMENT_ESCROW.stakeBond(_quote.quote.prover, _quote.quote.bondAmount); proofClaim = DataStructures.EpochProofClaim({ epochToProve: epochToProve, @@ -586,7 +576,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup { view override(IRollup) { - verifySignedQuote(_quote); + SignatureLib.verify(_quote.signature, _quote.quote.prover, quoteToDigest(_quote.quote)); Slot currentSlot = getCurrentSlot(); address currentProposer = getCurrentProposer(); diff --git a/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol b/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol index e49547dd9fc..e844400fba5 100644 --- a/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol +++ b/l1-contracts/src/core/interfaces/IProofCommitmentEscrow.sol @@ -9,12 +9,12 @@ interface IProofCommitmentEscrow { event StartWithdraw(address indexed withdrawer, uint256 amount, Timestamp executableAt); event ExecuteWithdraw(address indexed withdrawer, uint256 amount); event StakeBond(address indexed prover, uint256 amount); - event UnstakeBond(address indexed prover); + event UnstakeBond(address indexed prover, uint256 amount); function deposit(uint256 _amount) external; function startWithdraw(uint256 _amount) external; function executeWithdraw() external; - function stakeBond(uint256 _bondAmount, address _prover) external; - function unstakeBond() external; + function stakeBond(address _prover, uint256 _amount) external; + function unstakeBond(address _prover, uint256 _amount) external; function minBalanceAtTime(Timestamp _timestamp, address _prover) external view returns (uint256); } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 5b224bd522b..6aa9f616938 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -96,9 +96,6 @@ interface IRollup { external view returns (bytes32); - function verifySignedQuote(EpochProofQuoteLib.SignedEpochProofQuote calldata _quote) - external - view; function archive() external view returns (bytes32); function archiveAt(uint256 _blockNumber) external view returns (bytes32); diff --git a/l1-contracts/src/mock/MockProofCommitmentEscrow.sol b/l1-contracts/src/mock/MockProofCommitmentEscrow.sol index d4f1db360aa..4568f3392eb 100644 --- a/l1-contracts/src/mock/MockProofCommitmentEscrow.sol +++ b/l1-contracts/src/mock/MockProofCommitmentEscrow.sol @@ -18,11 +18,11 @@ contract MockProofCommitmentEscrow is IProofCommitmentEscrow { // do nothing } - function unstakeBond() external override { + function unstakeBond(address _prover, uint256 _amount) external override { // do nothing } - function stakeBond(uint256 _amount, address _prover) external override { + function stakeBond(address _prover, uint256 _amount) external override { // do nothing } diff --git a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol index e0fcea53610..401fd67a2bc 100644 --- a/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol +++ b/l1-contracts/test/prover-coordination/ProofCommitmentEscrow.t.sol @@ -13,51 +13,45 @@ import {TestERC20} from "../TestERC20.sol"; // solhint-disable comprehensive-interface contract TestProofCommitmentEscrow is Test { - ProofCommitmentEscrow internal _escrow; - TestERC20 internal _token; + // solhint-disable-next-line var-name-mixedcase + ProofCommitmentEscrow internal ESCROW; + // solhint-disable-next-line var-name-mixedcase + TestERC20 internal TOKEN; address internal prover; uint256 internal depositAmount; - modifier setup() { - _token = new TestERC20(); - _escrow = new ProofCommitmentEscrow(_token, address(this)); - _; - } - modifier setupWithApproval(address _prover, uint256 _depositAmount) { - _token = new TestERC20(); - _escrow = new ProofCommitmentEscrow(_token, address(this)); - - _token.mint(_prover, _depositAmount); + TOKEN.mint(_prover, _depositAmount); vm.prank(_prover); - _token.approve(address(_escrow), _depositAmount); + TOKEN.approve(address(ESCROW), _depositAmount); prover = _prover; depositAmount = _depositAmount; _; } + function setUp() public { + TOKEN = new TestERC20(); + ESCROW = new ProofCommitmentEscrow(TOKEN, address(this)); + } + function testDeposit() public setupWithApproval(address(42), 100) { vm.prank(prover); - _escrow.deposit(depositAmount); + ESCROW.deposit(depositAmount); assertEq( - _token.balanceOf(address(_escrow)), - depositAmount, - "Escrow balance should match deposit amount" + TOKEN.balanceOf(address(ESCROW)), depositAmount, "Escrow balance should match deposit amount" ); - assertEq(_token.balanceOf(prover), 0, "Prover balance should be 0 after deposit"); + assertEq(TOKEN.balanceOf(prover), 0, "Prover balance should be 0 after deposit"); } function testCannotWithdrawWithoutMatureRequest() public setupWithApproval(address(42), 100) { vm.prank(prover); - _escrow.deposit(depositAmount); - uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); - - _mintAndDeposit(prover, depositAmount); + ESCROW.deposit(depositAmount); + uint256 withdrawReadyAt = block.timestamp + ESCROW.WITHDRAW_DELAY(); vm.prank(prover); - _escrow.startWithdraw(depositAmount); + ESCROW.startWithdraw(depositAmount); vm.prank(prover); vm.expectRevert( @@ -67,9 +61,9 @@ contract TestProofCommitmentEscrow is Test { withdrawReadyAt ) ); - _escrow.executeWithdraw(); + ESCROW.executeWithdraw(); - vm.warp(block.timestamp + _escrow.WITHDRAW_DELAY() - 1); + vm.warp(block.timestamp + ESCROW.WITHDRAW_DELAY() - 1); vm.prank(prover); vm.expectRevert( abi.encodeWithSelector( @@ -78,115 +72,108 @@ contract TestProofCommitmentEscrow is Test { withdrawReadyAt ) ); - _escrow.executeWithdraw(); + ESCROW.executeWithdraw(); } function testWithdrawAfterDelay() public setupWithApproval(address(42), 100) { vm.prank(prover); - _escrow.deposit(depositAmount); + ESCROW.deposit(depositAmount); uint256 withdrawAmount = 50; - uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); - - _mintAndDeposit(prover, depositAmount); + uint256 withdrawReadyAt = block.timestamp + ESCROW.WITHDRAW_DELAY(); vm.prank(prover); - _escrow.startWithdraw(withdrawAmount); + ESCROW.startWithdraw(withdrawAmount); vm.warp(withdrawReadyAt); vm.prank(prover); - _escrow.executeWithdraw(); + ESCROW.executeWithdraw(); assertEq( - _token.balanceOf(address(_escrow)), + TOKEN.balanceOf(address(ESCROW)), depositAmount - withdrawAmount, "Escrow balance should be reduced after withdrawal" ); - assertEq(_token.balanceOf(prover), withdrawAmount, "Prover balance should match deposit amount"); + assertEq(TOKEN.balanceOf(prover), withdrawAmount, "Prover balance should match deposit amount"); } - function testCannotReplayWithdrawRequest() public setupWithApproval(address(42), 100) { + function testCannotReplayWithdrawRequest(uint256 _withdrawAmount) + public + setupWithApproval(address(42), 100) + { vm.prank(prover); - _escrow.deposit(depositAmount); - uint256 withdrawAmount = 50; - uint256 withdrawReadyAt = block.timestamp + _escrow.WITHDRAW_DELAY(); - - _mintAndDeposit(prover, depositAmount); + ESCROW.deposit(depositAmount); + uint256 withdrawAmount = bound(_withdrawAmount, 1, depositAmount); + uint256 withdrawReadyAt = block.timestamp + ESCROW.WITHDRAW_DELAY(); vm.prank(prover); - _escrow.startWithdraw(withdrawAmount); + ESCROW.startWithdraw(withdrawAmount); vm.warp(withdrawReadyAt); vm.prank(prover); - _escrow.executeWithdraw(); + ESCROW.executeWithdraw(); vm.prank(prover); - _escrow.executeWithdraw(); + ESCROW.executeWithdraw(); assertEq( - _token.balanceOf(address(_escrow)), + TOKEN.balanceOf(address(ESCROW)), depositAmount - withdrawAmount, "Escrow balance should be reduced after withdrawal" ); } - function testOnlyOwnerCanStake(address nonOwner) public setup { + function testOnlyOwnerCanStake(address nonOwner) public { vm.assume(nonOwner != address(this)); vm.prank(nonOwner); vm.expectRevert( abi.encodeWithSelector(Errors.ProofCommitmentEscrow__NotOwner.selector, nonOwner) ); - _escrow.stakeBond(0, address(0)); + ESCROW.stakeBond(address(0), 0); } function testCannotStakeMoreThanProverBalance() public setupWithApproval(address(42), 100) { vm.prank(prover); - _escrow.deposit(depositAmount); + ESCROW.deposit(depositAmount); uint256 stakeAmount = depositAmount + 1; - _mintAndDeposit(prover, depositAmount); - vm.expectRevert(); - _escrow.stakeBond(stakeAmount, prover); + ESCROW.stakeBond(prover, stakeAmount); assertEq( - _token.balanceOf(address(_escrow)), - depositAmount, - "Escrow balance should match deposit amount" + TOKEN.balanceOf(address(ESCROW)), depositAmount, "Escrow balance should match deposit amount" ); - assertEq(_escrow.deposits(prover), depositAmount, "Prover balance should match deposit amount"); + assertEq(ESCROW.deposits(prover), depositAmount, "Prover balance should match deposit amount"); } - function testOnlyOwnerCanUnstake(address nonOwner) public setup { + function testOnlyOwnerCanUnstake(address nonOwner) public { vm.assume(nonOwner != address(this)); vm.prank(nonOwner); vm.expectRevert( abi.encodeWithSelector(Errors.ProofCommitmentEscrow__NotOwner.selector, nonOwner) ); - _escrow.unstakeBond(); + ESCROW.unstakeBond(address(0), 0); } function testStakeAndUnstake() public setupWithApproval(address(42), 100) { vm.prank(prover); - _escrow.deposit(depositAmount); + ESCROW.deposit(depositAmount); uint256 stakeAmount = 50; - _mintAndDeposit(prover, depositAmount); - - _escrow.stakeBond(stakeAmount, prover); + ESCROW.stakeBond(prover, stakeAmount); assertEq( - _escrow.deposits(prover), depositAmount - stakeAmount, "Prover balance should be reduced" + ESCROW.deposits(prover), depositAmount - stakeAmount, "Prover balance should be reduced" ); - _escrow.unstakeBond(); + ESCROW.unstakeBond(prover, stakeAmount); assertEq( - _escrow.deposits(prover), depositAmount, "Prover balance should be restored after unstake" + ESCROW.deposits(prover), depositAmount, "Prover balance should be restored after unstake" ); } - function testOverwritingStakeSlashesPreviousProver() public setup { + function testOverwritingStakeSlashesPreviousProver() public { address proverA = address(42); address proverB = address(43); uint256 depositAmountA = 100; @@ -194,64 +181,62 @@ contract TestProofCommitmentEscrow is Test { uint256 depositAmountB = 200; uint256 stakeAmountB = 100; - _token.mint(proverA, depositAmountA); + TOKEN.mint(proverA, depositAmountA); vm.prank(proverA); - _token.approve(address(_escrow), depositAmountA); + TOKEN.approve(address(ESCROW), depositAmountA); vm.prank(proverA); - _escrow.deposit(depositAmountA); + ESCROW.deposit(depositAmountA); - _token.mint(proverB, depositAmountB); + TOKEN.mint(proverB, depositAmountB); vm.prank(proverB); - _token.approve(address(_escrow), depositAmountB); + TOKEN.approve(address(ESCROW), depositAmountB); vm.prank(proverB); - _escrow.deposit(depositAmountB); + ESCROW.deposit(depositAmountB); - // Prover A deposits and is staked - _escrow.stakeBond(stakeAmountA, proverA); + // Prover A is staked + ESCROW.stakeBond(proverA, stakeAmountA); - // Prover B deposits and owner overwrites the stake - _escrow.stakeBond(stakeAmountB, proverB); + // Prover B is staked + ESCROW.stakeBond(proverB, stakeAmountB); - // Prover A cannot recover the staked amount + // Prover A is missing the stake uint256 expectedDepositA = depositAmountA - stakeAmountA; assertEq( - _escrow.deposits(proverA), + ESCROW.deposits(proverA), expectedDepositA, "Prover A's deposit should reflect the slashed stake" ); - // Owner cannot unstake Prover A's stake anymore - _escrow.unstakeBond(); + // Prover B gets unstaked + ESCROW.unstakeBond(proverB, stakeAmountB); assertEq( - _escrow.deposits(proverB), + ESCROW.deposits(proverB), depositAmountB, "Prover B's deposit should be restored after unstake" ); assertEq( - _escrow.deposits(proverA), - expectedDepositA, - "Prover A's deposit remains slashed after unstake" + ESCROW.deposits(proverA), expectedDepositA, "Prover A's deposit remains slashed after unstake" ); } function testWithdrawRequestOverwriting() public setupWithApproval(address(42), 100) { uint256 withdrawAmountA = 40; uint256 withdrawAmountB = 60; - uint256 withdrawReadyAtA = block.timestamp + _escrow.WITHDRAW_DELAY(); - uint256 withdrawReadyAtB = block.timestamp + 2 * _escrow.WITHDRAW_DELAY(); + uint256 withdrawReadyAtA = block.timestamp + ESCROW.WITHDRAW_DELAY(); + uint256 withdrawReadyAtB = block.timestamp + 2 * ESCROW.WITHDRAW_DELAY(); vm.prank(prover); - _escrow.deposit(depositAmount); + ESCROW.deposit(depositAmount); // Prover starts first withdraw request vm.prank(prover); - _escrow.startWithdraw(withdrawAmountA); + ESCROW.startWithdraw(withdrawAmountA); // Prover starts second withdraw request before executing first vm.warp(withdrawReadyAtA); vm.prank(prover); - _escrow.startWithdraw(withdrawAmountB); + ESCROW.startWithdraw(withdrawAmountB); // Attempt to execute first withdraw request after its delay vm.prank(prover); @@ -262,51 +247,51 @@ contract TestProofCommitmentEscrow is Test { withdrawReadyAtB ) ); - _escrow.executeWithdraw(); + ESCROW.executeWithdraw(); // Execute second withdraw request after its delay vm.warp(withdrawReadyAtB); vm.prank(prover); - _escrow.executeWithdraw(); + ESCROW.executeWithdraw(); // Assert assertEq( - _escrow.deposits(prover), + ESCROW.deposits(prover), depositAmount - withdrawAmountB, "Prover's deposit should be reduced by the withdrawn amount" ); } - function testMinBalanceAtSlot() public setupWithApproval(address(42), 100) { + function testMinBalanceAtTime() public setupWithApproval(address(42), 100) { uint256 withdrawAmount = 25; - Timestamp withdrawReadyAt = Timestamp.wrap(block.timestamp + _escrow.WITHDRAW_DELAY()); + Timestamp withdrawReadyAt = Timestamp.wrap(block.timestamp + ESCROW.WITHDRAW_DELAY()); vm.prank(prover); - _escrow.deposit(depositAmount); + ESCROW.deposit(depositAmount); assertEq( - _escrow.minBalanceAtTime(Timestamp.wrap(block.timestamp), prover), + ESCROW.minBalanceAtTime(Timestamp.wrap(block.timestamp), prover), depositAmount, "Min balance should match deposit amount before any withdraw request" ); assertEq( - _escrow.minBalanceAtTime(withdrawReadyAt - Timestamp.wrap(1), prover), + ESCROW.minBalanceAtTime(withdrawReadyAt - Timestamp.wrap(1), prover), depositAmount, "Min balance should match deposit amount before withdraw request matures" ); vm.prank(prover); - _escrow.startWithdraw(withdrawAmount); + ESCROW.startWithdraw(withdrawAmount); assertEq( - _escrow.minBalanceAtTime(Timestamp.wrap(block.timestamp), prover), + ESCROW.minBalanceAtTime(Timestamp.wrap(block.timestamp), prover), depositAmount, "Min balance should be unaffected by pending withdraw request before maturity" ); assertEq( - _escrow.minBalanceAtTime(Timestamp.wrap(block.timestamp + _escrow.WITHDRAW_DELAY()), prover), + ESCROW.minBalanceAtTime(Timestamp.wrap(block.timestamp + ESCROW.WITHDRAW_DELAY()), prover), 0, "Min balance should be 0 at or beyond the delay window" ); @@ -314,17 +299,15 @@ contract TestProofCommitmentEscrow is Test { vm.warp(block.timestamp + 1); assertEq( - _escrow.minBalanceAtTime(withdrawReadyAt, prover), + ESCROW.minBalanceAtTime(withdrawReadyAt, prover), depositAmount - withdrawAmount, "Min balance should be 75 after withdraw request matures" ); assertEq( - _escrow.minBalanceAtTime(withdrawReadyAt + Timestamp.wrap(1), prover), + ESCROW.minBalanceAtTime(withdrawReadyAt + Timestamp.wrap(1), prover), 0, "Min balance should be 0 at or beyond the delay window" ); } - - function _mintAndDeposit(address _prover, uint256 _amount) internal {} } diff --git a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts index 43824aaf105..7bfd6070bd2 100644 --- a/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts +++ b/yarn-project/circuit-types/src/prover_coordination/epoch_proof_quote.ts @@ -32,6 +32,7 @@ export class EpochProofQuote extends Gossipable { return new EpochProofQuote(reader.readObject(EpochProofQuotePayload), reader.readObject(Signature)); } + // TODO: https://github.com/AztecProtocol/aztec-packages/issues/8911 /** * Creates a new quote with a signature. * The digest provided must match what the rollup contract will produce i.e. `_hashTypedDataV4(EpochProofQuoteLib.hash(quote))` diff --git a/yarn-project/end-to-end/src/prover-coordination/e2e_quote_signing.test.ts b/yarn-project/end-to-end/src/prover-coordination/e2e_quote_signing.test.ts deleted file mode 100644 index 5dbdffcf3d7..00000000000 --- a/yarn-project/end-to-end/src/prover-coordination/e2e_quote_signing.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { EpochProofQuote, EpochProofQuotePayload } from '@aztec/aztec.js'; -import { EthAddress } from '@aztec/circuits.js'; -import { Buffer32 } from '@aztec/foundation/buffer'; -import { Secp256k1Signer, keccak256 } from '@aztec/foundation/crypto'; -import { RollupAbi } from '@aztec/l1-artifacts'; - -import { beforeAll } from '@jest/globals'; -import { - type Chain, - type GetContractReturnType, - type HttpTransport, - type PublicClient, - getAddress, - getContract, -} from 'viem'; - -import { type ISnapshotManager, type SubsystemsContext, createSnapshotManager } from '../fixtures/snapshot_manager.js'; - -/** - * Tests the creation of epoch proof quotes and their validation on L1 - */ -describe('e2e_quote_signature_validation', () => { - let ctx: SubsystemsContext; - let rollupContract: GetContractReturnType>; - - // let logger: DebugLogger; - let snapshotManager: ISnapshotManager; - - beforeAll(async () => { - snapshotManager = createSnapshotManager(`prover_coordination/e2e_quote_signing`, process.env.E2E_DATA_PATH); - - ctx = await snapshotManager.setup(); - - await ctx.proverNode.stop(); - - rollupContract = getContract({ - address: getAddress(ctx.deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString()), - abi: RollupAbi, - client: ctx.deployL1ContractsValues.walletClient, - }); - }); - - it('can get domain information from the rollup', async () => { - const [, name, version, chainId, address] = await rollupContract.read.eip712Domain(); - expect(name).toBe('Aztec Rollup'); - expect(version).toBe('1'); - expect(chainId).toBe(31337n); - expect(address).toBe(rollupContract.address); - }); - - it('can verify a signed quote on L1', async () => { - const payload = EpochProofQuotePayload.fromFields({ - epochToProve: 42n, - validUntilSlot: 100n, - bondAmount: 1000000000000000000n, - prover: EthAddress.fromString('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'), - basisPointFee: 5000, - }); - - const signer = new Secp256k1Signer(Buffer32.fromBuffer(keccak256(Buffer.from('cow')))); - const digest = await rollupContract.read.quoteToDigest([payload.toViemArgs()]); - const quote = EpochProofQuote.new(Buffer32.fromString(digest), payload, signer); - - await rollupContract.read.verifySignedQuote([quote.toViemArgs()]); - }); -});