diff --git a/solidity/core/contracts/Inbox.sol b/solidity/core/contracts/Inbox.sol index 93796b74b..d46c840ac 100644 --- a/solidity/core/contracts/Inbox.sol +++ b/solidity/core/contracts/Inbox.sol @@ -55,14 +55,8 @@ contract Inbox is IInbox, ReentrancyGuardUpgradeable, Version0, Mailbox { * @dev This event allows watchers to observe the merkle proof they need * to prove fraud on the Outbox. * @param messageHash Hash of message that was processed. - * @param leafIndex The leaf index of the message that was processed. - * @param proof A merkle proof of inclusion of `messageHash` at `leafIndex`. */ - event Process( - bytes32 indexed messageHash, - uint256 indexed leafIndex, - bytes32[32] proof - ); + event Process(bytes32 indexed messageHash); // ============ Constructor ============ @@ -82,41 +76,27 @@ contract Inbox is IInbox, ReentrancyGuardUpgradeable, Version0, Mailbox { // ============ External Functions ============ - /** - * @notice Caches the provided merkle root and index. - * @dev Called by the validator manager, which is responsible for verifying a - * quorum of validator signatures on the checkpoint. - * @dev Reverts if the checkpoint's index is not greater than the index of the latest checkpoint in the cache. - * @param _root Checkpoint's merkle root. - * @param _index Checkpoint's index. - */ - function cacheCheckpoint(bytes32 _root, uint256 _index) - external - override - onlyValidatorManager - { - // Ensure that the checkpoint is newer than the latest we've cached. - require(_index > cachedCheckpoints[latestCachedRoot], "!newer"); - _cacheCheckpoint(_root, _index); - } - /** * @notice Attempts to process the provided formatted `message`. Performs * verification against root of the proof + * @dev Called by the validator manager, which is responsible for verifying a + * quorum of validator signatures on the checkpoint. * @dev Reverts if verification of the message fails. - * @dev Includes the eventual function signature for Sovereign Consensus, - * but comments out the name to suppress compiler warning + * @param _root The merkle root of the checkpoint used to prove message inclusion. + * @param _index The index of the checkpoint used to prove message inclusion. * @param _message Formatted message (refer to Mailbox.sol Message library) * @param _proof Merkle proof of inclusion for message's leaf - * @param _index Index of leaf in outbox's merkle tree + * @param _leafIndex Index of leaf in outbox's merkle tree */ function process( + bytes32 _root, + uint256 _index, bytes calldata _message, bytes32[32] calldata _proof, - uint256 _index, - bytes calldata /* _sovereignData */ - ) external override nonReentrant { - bytes32 _messageHash = _message.leaf(_index); + uint256 _leafIndex + ) external override nonReentrant onlyValidatorManager { + require(_index >= _leafIndex, "!index"); + bytes32 _messageHash = _message.leaf(_leafIndex); // ensure that message has not been processed require( messages[_messageHash] == MessageStatus.None, @@ -126,12 +106,12 @@ contract Inbox is IInbox, ReentrancyGuardUpgradeable, Version0, Mailbox { bytes32 _calculatedRoot = MerkleLib.branchRoot( _messageHash, _proof, - _index + _leafIndex ); - // ensure that the root has been cached - require(cachedCheckpoints[_calculatedRoot] >= _index, "!cache"); + // verify the merkle proof + require(_calculatedRoot == _root, "!proof"); _process(_message, _messageHash); - emit Process(_messageHash, _index, _proof); + emit Process(_messageHash); } // ============ Internal Functions ============ diff --git a/solidity/core/contracts/Mailbox.sol b/solidity/core/contracts/Mailbox.sol index 83d146651..cc513dbe8 100644 --- a/solidity/core/contracts/Mailbox.sol +++ b/solidity/core/contracts/Mailbox.sol @@ -20,29 +20,16 @@ abstract contract Mailbox is IMailbox, OwnableUpgradeable { // ============ Public Variables ============ - // Cached checkpoints, mapping root => leaf index. - // Cached checkpoints must have index > 0 as the presence of such - // a checkpoint cannot be distinguished from its absence. - mapping(bytes32 => uint256) public cachedCheckpoints; - // The latest cached root - bytes32 public latestCachedRoot; // Address of the validator manager contract. address public validatorManager; // ============ Upgrade Gap ============ // gap for upgrade safety - uint256[47] private __GAP; + uint256[49] private __GAP; // ============ Events ============ - /** - * @notice Emitted when a checkpoint is cached. - * @param root Merkle root - * @param index Leaf index - */ - event CheckpointCached(bytes32 indexed root, uint256 indexed index); - /** * @notice Emitted when the validator manager contract is changed * @param validatorManager The address of the new validatorManager @@ -89,21 +76,6 @@ abstract contract Mailbox is IMailbox, OwnableUpgradeable { _setValidatorManager(_validatorManager); } - /** - * @notice Returns the latest entry in the checkpoint cache. - * @return root Latest cached root - * @return index Latest cached index - */ - function latestCachedCheckpoint() - external - view - override - returns (bytes32 root, uint256 index) - { - root = latestCachedRoot; - index = cachedCheckpoints[root]; - } - // ============ Internal Functions ============ /** @@ -118,17 +90,4 @@ abstract contract Mailbox is IMailbox, OwnableUpgradeable { validatorManager = _validatorManager; emit NewValidatorManager(_validatorManager); } - - /** - * @notice Caches the provided checkpoint. - * Caching checkpoints with index == 0 are disallowed. - * @param _root The merkle root to cache. - * @param _index The leaf index of the latest message in the merkle tree. - */ - function _cacheCheckpoint(bytes32 _root, uint256 _index) internal { - require(_index > 0, "!index"); - cachedCheckpoints[_root] = _index; - latestCachedRoot = _root; - emit CheckpointCached(_root, _index); - } } diff --git a/solidity/core/contracts/Outbox.sol b/solidity/core/contracts/Outbox.sol index 9184c5450..9d033c4cf 100644 --- a/solidity/core/contracts/Outbox.sol +++ b/solidity/core/contracts/Outbox.sol @@ -48,16 +48,29 @@ contract Outbox is IOutbox, Version0, MerkleTreeManager, Mailbox { // ============ Public Storage Variables ============ + // Cached checkpoints, mapping root => leaf index. + // Cached checkpoints must have index > 0 as the presence of such + // a checkpoint cannot be distinguished from its absence. + mapping(bytes32 => uint256) public cachedCheckpoints; + // The latest cached root + bytes32 public latestCachedRoot; // Current state of contract States public state; // ============ Upgrade Gap ============ // gap for upgrade safety - uint256[49] private __GAP; + uint256[47] private __GAP; // ============ Events ============ + /** + * @notice Emitted when a checkpoint is cached. + * @param root Merkle root + * @param index Leaf index + */ + event CheckpointCached(bytes32 indexed root, uint256 indexed index); + /** * @notice Emitted when a new message is dispatched via Abacus * @param messageHash Hash of message; the leaf inserted to the Merkle tree for the message @@ -134,11 +147,14 @@ contract Outbox is IOutbox, Version0, MerkleTreeManager, Mailbox { /** * @notice Caches the current merkle root and index. - * @dev emits Checkpoint event + * @dev emits CheckpointCached event */ function cacheCheckpoint() external override notFailed { - (bytes32 root, uint256 index) = latestCheckpoint(); - _cacheCheckpoint(root, index); + (bytes32 _root, uint256 _index) = latestCheckpoint(); + require(_index > 0, "!index"); + cachedCheckpoints[_root] = _index; + latestCachedRoot = _root; + emit CheckpointCached(_root, _index); } /** @@ -151,6 +167,20 @@ contract Outbox is IOutbox, Version0, MerkleTreeManager, Mailbox { emit Fail(); } + /** + * @notice Returns the latest entry in the checkpoint cache. + * @return root Latest cached root + * @return index Latest cached index + */ + function latestCachedCheckpoint() + external + view + returns (bytes32 root, uint256 index) + { + root = latestCachedRoot; + index = cachedCheckpoints[root]; + } + /** * @notice Returns the number of inserted leaves in the tree */ diff --git a/solidity/core/contracts/test/TestInbox.sol b/solidity/core/contracts/test/TestInbox.sol index 950716f44..e7107c872 100644 --- a/solidity/core/contracts/test/TestInbox.sol +++ b/solidity/core/contracts/test/TestInbox.sol @@ -8,10 +8,6 @@ contract TestInbox is Inbox { constructor(uint32 _localDomain) Inbox(_localDomain) {} // solhint-disable-line no-empty-blocks - function setCachedCheckpoint(bytes32 _root, uint256 _index) external { - cachedCheckpoints[_root] = _index; - } - function testBranchRoot( bytes32 leaf, bytes32[32] calldata proof, diff --git a/solidity/core/contracts/test/TestMailbox.sol b/solidity/core/contracts/test/TestMailbox.sol index 952e50c02..8df12c63d 100644 --- a/solidity/core/contracts/test/TestMailbox.sol +++ b/solidity/core/contracts/test/TestMailbox.sol @@ -9,8 +9,4 @@ contract TestMailbox is Mailbox { function initialize(address _validatorManager) external initializer { __Mailbox_initialize(_validatorManager); } - - function cacheCheckpoint(bytes32 _root, uint256 _index) external { - _cacheCheckpoint(_root, _index); - } } diff --git a/solidity/core/contracts/test/TestValidatorManager.sol b/solidity/core/contracts/test/TestValidatorManager.sol index 8810173ac..6c2e3f76a 100644 --- a/solidity/core/contracts/test/TestValidatorManager.sol +++ b/solidity/core/contracts/test/TestValidatorManager.sol @@ -8,11 +8,14 @@ import {IInbox} from "../../interfaces/IInbox.sol"; * to be a contract. */ contract TestValidatorManager { - function cacheCheckpoint( + function process( IInbox _inbox, bytes32 _root, - uint256 _index + uint256 _index, + bytes calldata _message, + bytes32[32] calldata _proof, + uint256 _leafIndex ) external { - _inbox.cacheCheckpoint(_root, _index); + _inbox.process(_root, _index, _message, _proof, _leafIndex); } } diff --git a/solidity/core/contracts/test/bad-recipient/BadRecipientHandle.sol b/solidity/core/contracts/test/bad-recipient/BadRecipient2.sol similarity index 84% rename from solidity/core/contracts/test/bad-recipient/BadRecipientHandle.sol rename to solidity/core/contracts/test/bad-recipient/BadRecipient2.sol index 0d2d1a525..41760bb4b 100644 --- a/solidity/core/contracts/test/bad-recipient/BadRecipientHandle.sol +++ b/solidity/core/contracts/test/bad-recipient/BadRecipient2.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; -contract BadRecipientHandle { +contract BadRecipient2 { function handle(uint32, bytes32) external pure {} // solhint-disable-line no-empty-blocks } diff --git a/solidity/core/contracts/validator-manager/InboxValidatorManager.sol b/solidity/core/contracts/validator-manager/InboxValidatorManager.sol index 672d2d8d4..85b7630e1 100644 --- a/solidity/core/contracts/validator-manager/InboxValidatorManager.sol +++ b/solidity/core/contracts/validator-manager/InboxValidatorManager.sol @@ -12,18 +12,6 @@ import {MultisigValidatorManager} from "./MultisigValidatorManager.sol"; * them to an Inbox. */ contract InboxValidatorManager is MultisigValidatorManager { - // ============ Events ============ - - /** - * @notice Emitted when a checkpoint has been signed by a quorum - * of validators and cached on an Inbox. - * @dev This event allows watchers to observe the signatures they need - * to prove fraud on the Outbox. - * @param signatures The signatures by a quorum of validators on the - * checkpoint. - */ - event Quorum(bytes[] signatures); - // ============ Constructor ============ /** @@ -43,24 +31,29 @@ contract InboxValidatorManager is MultisigValidatorManager { // ============ External Functions ============ /** - * @notice Submits a checkpoint signed by a quorum of validators to be cached by an Inbox. + * @notice Verifies a signed checkpoint and submits a message for processing. * @dev Reverts if `_signatures` is not a quorum of validator signatures. * @dev Reverts if `_signatures` is not sorted in ascending order by the signer * address, which is required for duplicate detection. - * @param _inbox The inbox to submit the checkpoint to. - * @param _root The merkle root of the checkpoint. - * @param _index The index of the checkpoint. + * @param _inbox The inbox to submit the message to. + * @param _root The merkle root of the signed checkpoint. + * @param _index The index of the signed checkpoint. * @param _signatures Signatures over the checkpoint to be checked for a validator * quorum. Must be sorted in ascending order by signer address. + * @param _message The message to process. + * @param _proof Merkle proof of inclusion for message's leaf + * @param _leafIndex Index of leaf in outbox's merkle tree */ - function cacheCheckpoint( + function process( IInbox _inbox, bytes32 _root, uint256 _index, - bytes[] calldata _signatures + bytes[] calldata _signatures, + bytes calldata _message, + bytes32[32] calldata _proof, + uint256 _leafIndex ) external { require(isQuorum(_root, _index, _signatures), "!quorum"); - emit Quorum(_signatures); - _inbox.cacheCheckpoint(_root, _index); + _inbox.process(_root, _index, _message, _proof, _leafIndex); } } diff --git a/solidity/core/interfaces/IInbox.sol b/solidity/core/interfaces/IInbox.sol index 365b6eb44..f55679271 100644 --- a/solidity/core/interfaces/IInbox.sol +++ b/solidity/core/interfaces/IInbox.sol @@ -4,14 +4,13 @@ pragma solidity >=0.6.11; import {IMailbox} from "./IMailbox.sol"; interface IInbox is IMailbox { - function cacheCheckpoint(bytes32 _root, uint256 _index) external; - function remoteDomain() external returns (uint32); function process( + bytes32 _root, + uint256 _index, bytes calldata _message, bytes32[32] calldata _proof, - uint256 _index, - bytes calldata _sovereignData + uint256 _leafIndex ) external; } diff --git a/solidity/core/interfaces/IMailbox.sol b/solidity/core/interfaces/IMailbox.sol index 8cfb0845f..9b505b92e 100644 --- a/solidity/core/interfaces/IMailbox.sol +++ b/solidity/core/interfaces/IMailbox.sol @@ -3,11 +3,4 @@ pragma solidity >=0.6.11; interface IMailbox { function localDomain() external view returns (uint32); - - function cachedCheckpoints(bytes32) external view returns (uint256); - - function latestCachedCheckpoint() - external - view - returns (bytes32 root, uint256 index); } diff --git a/solidity/core/interfaces/IOutbox.sol b/solidity/core/interfaces/IOutbox.sol index 52524fd73..194a0d38a 100644 --- a/solidity/core/interfaces/IOutbox.sol +++ b/solidity/core/interfaces/IOutbox.sol @@ -17,4 +17,11 @@ interface IOutbox is IMailbox { function count() external returns (uint256); function fail() external; + + function cachedCheckpoints(bytes32) external view returns (uint256); + + function latestCachedCheckpoint() + external + view + returns (bytes32 root, uint256 index); } diff --git a/solidity/core/test/common.test.ts b/solidity/core/test/common.test.ts deleted file mode 100644 index 1e1f72807..000000000 --- a/solidity/core/test/common.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { ethers } from 'hardhat'; - -import { TestMailbox, TestMailbox__factory } from '../types'; - -const localDomain = 1000; -const ONLY_OWNER_REVERT_MSG = 'Ownable: caller is not the owner'; - -describe('Mailbox', async () => { - let owner: SignerWithAddress, - nonowner: SignerWithAddress, - mailbox: TestMailbox; - - before(async () => { - [owner, nonowner] = await ethers.getSigners(); - }); - - beforeEach(async () => { - const mailboxFactory = new TestMailbox__factory(owner); - mailbox = await mailboxFactory.deploy(localDomain); - // The ValidatorManager is unused in these tests *but* needs to be a - // contract. - await mailbox.initialize(mailbox.address); - expect(await mailbox.validatorManager()).to.equal(mailbox.address); - }); - - it('Cannot be initialized twice', async () => { - await expect(mailbox.initialize(mailbox.address)).to.be.revertedWith( - 'Initializable: contract is already initialized', - ); - }); - - it('Allows owner to update the ValidatorManager', async () => { - const mailboxFactory = new TestMailbox__factory(owner); - const newValidatorManager = await mailboxFactory.deploy(localDomain); - await mailbox.setValidatorManager(newValidatorManager.address); - expect(await mailbox.validatorManager()).to.equal( - newValidatorManager.address, - ); - }); - - it('Does not allow nonowner to update the ValidatorManager', async () => { - await expect( - mailbox.connect(nonowner).setValidatorManager(mailbox.address), - ).to.be.revertedWith(ONLY_OWNER_REVERT_MSG); - }); - - it('Caches a checkpoint', async () => { - const root = - '0x9c7a007113f829cfd019a91e4ca5e7f6760589fd6bc7925c877f6971ffee1647'; - const index = 1; - await mailbox.cacheCheckpoint(root, index); - expect(await mailbox.latestCachedRoot()).to.equal(root); - expect(await mailbox.cachedCheckpoints(root)).to.equal(index); - const [actualRoot, actualIndex] = await mailbox.latestCachedCheckpoint(); - expect(actualRoot).to.equal(root); - expect(actualIndex).to.equal(index); - }); - - it('Reverts when caching a checkpoint with index zero', async () => { - const root = - '0x9c7a007113f829cfd019a91e4ca5e7f6760589fd6bc7925c877f6971ffee1647'; - const index = 0; - await expect(mailbox.cacheCheckpoint(root, index)).to.be.revertedWith( - '!index', - ); - }); -}); diff --git a/solidity/core/test/inbox.test.ts b/solidity/core/test/inbox.test.ts index 8d6218759..db6ad7c1c 100644 --- a/solidity/core/test/inbox.test.ts +++ b/solidity/core/test/inbox.test.ts @@ -4,29 +4,31 @@ import { expect } from 'chai'; import { ethers } from 'hardhat'; import { types, utils } from '@abacus-network/utils'; -import { MessageStatus } from '@abacus-network/utils/dist/src/types'; -import messageWithProof from '../../../vectors/messageWithProof.json'; -import proveAndProcessTestCases from '../../../vectors/proveAndProcess.json'; import { BadRecipient1__factory, + BadRecipient2__factory, BadRecipient3__factory, BadRecipient5__factory, BadRecipient6__factory, - BadRecipientHandle__factory, TestInbox, TestInbox__factory, + TestOutbox, + TestOutbox__factory, TestRecipient__factory, TestValidatorManager, TestValidatorManager__factory, } from '../types'; +import { MerkleProof, dispatchMessageAndReturnProof } from './lib/mailboxes'; + const localDomain = 3000; const remoteDomain = 1000; describe('Inbox', async () => { const badRecipientFactories = [ BadRecipient1__factory, + BadRecipient2__factory, BadRecipient3__factory, BadRecipient5__factory, BadRecipient6__factory, @@ -34,235 +36,173 @@ describe('Inbox', async () => { let inbox: TestInbox, signer: SignerWithAddress, - abacusMessageSender: SignerWithAddress, - validatorManager: TestValidatorManager; + validatorManager: TestValidatorManager, + helperOutbox: TestOutbox, + recipient: string, + proof: MerkleProof; before(async () => { - [signer, abacusMessageSender] = await ethers.getSigners(); + [signer] = await ethers.getSigners(); // Inbox.initialize will ensure the validator manager is a contract. - // TestValidatorManager doesn't have any special logic, it just submits - // checkpoints without any signature verification. + // TestValidatorManager doesn't have any special logic, it just forwards + // calls to Inbox.process. const testValidatorManagerFactory = new TestValidatorManager__factory( signer, ); validatorManager = await testValidatorManagerFactory.deploy(); + const recipientF = new TestRecipient__factory(signer); + recipient = utils.addressToBytes32((await recipientF.deploy()).address); + + // Deploy a helper outbox contract so that we can easily construct merkle + // proofs. + const outboxFactory = new TestOutbox__factory(signer); + helperOutbox = await outboxFactory.deploy(localDomain); + await helperOutbox.initialize(validatorManager.address); + + proof = await dispatchMessageAndReturnProof( + helperOutbox, + remoteDomain, + recipient, + 'hello world', + ); }); beforeEach(async () => { const inboxFactory = new TestInbox__factory(signer); - inbox = await inboxFactory.deploy(localDomain); - await inbox.initialize(remoteDomain, validatorManager.address); + inbox = await inboxFactory.deploy(remoteDomain); + await inbox.initialize(localDomain, validatorManager.address); }); it('Cannot be initialized twice', async () => { await expect( - inbox.initialize(remoteDomain, validatorManager.address), + inbox.initialize(localDomain, validatorManager.address), ).to.be.revertedWith('Initializable: contract is already initialized'); }); - it('Caches checkpoint from validator manager', async () => { - const root = ethers.utils.formatBytes32String('first new root'); - const index = 1; - await validatorManager.cacheCheckpoint(inbox.address, root, index); - const [croot, cindex] = await inbox.latestCachedCheckpoint(); - expect(croot).to.equal(root); - expect(cindex).to.equal(index); - }); - - it('Rejects checkpoint from non-validator manager', async () => { - const root = ethers.utils.formatBytes32String('first new root'); - const index = 1; - await expect(inbox.cacheCheckpoint(root, index)).to.be.revertedWith( - '!validatorManager', + it('processes a message', async () => { + await validatorManager.process( + inbox.address, + proof.root, + proof.index, + proof.message, + proof.proof, + proof.index, + ); + expect(await inbox.messages(proof.leaf)).to.eql( + types.MessageStatus.PROCESSED, ); - }); - - it('Rejects old checkpoint from validator manager', async () => { - let root = ethers.utils.formatBytes32String('first new root'); - let index = 10; - await validatorManager.cacheCheckpoint(inbox.address, root, index); - const [croot, cindex] = await inbox.latestCachedCheckpoint(); - expect(croot).to.equal(root); - expect(cindex).to.equal(index); - - root = ethers.utils.formatBytes32String('second new root'); - index = 9; - await expect( - validatorManager.cacheCheckpoint(inbox.address, root, index), - ).to.be.revertedWith('!newer'); - }); - - it('Processes a valid message', async () => { - const signers = await ethers.getSigners(); - const recipientF = new TestRecipient__factory(signers[signers.length - 1]); - const recipient = await recipientF.deploy(); - await recipient.deployTransaction.wait(); - - const { index, proof, root, message } = messageWithProof; - await inbox.setCachedCheckpoint(root, 1); - - await inbox.process(message, proof, index, '0x'); - const hash = utils.messageHash(message, index); - expect(await inbox.messages(hash)).to.eql(MessageStatus.PROCESSED); }); it('Rejects an already-processed message', async () => { - const { leaf, index, proof, root, message } = messageWithProof; - - await inbox.setCachedCheckpoint(root, 1); - // Set message status as MessageStatus.Processed - await inbox.setMessageStatus(leaf, MessageStatus.PROCESSED); + await inbox.setMessageStatus(proof.leaf, types.MessageStatus.PROCESSED); // Try to process message again - await expect(inbox.process(message, proof, index, '0x')).to.be.revertedWith( - '!MessageStatus.None', - ); + await expect( + validatorManager.process( + inbox.address, + proof.root, + proof.index, + proof.message, + proof.proof, + proof.index, + ), + ).to.be.revertedWith('!MessageStatus.None'); }); it('Rejects invalid message proof', async () => { - const { leaf, index, proof, root, message } = messageWithProof; - // Switch ordering of proof hashes // NB: We copy 'path' here to avoid mutating the test cases for // other tests. - const newProof = [...proof]; - newProof[0] = proof[1]; - newProof[1] = proof[0]; - - await inbox.setCachedCheckpoint(root, 1); + const newProof = proof.proof.slice().reverse(); + + expect( + validatorManager.process( + inbox.address, + proof.root, + proof.index, + proof.message, + newProof, + proof.index, + ), + ).to.be.revertedWith('!proof'); + expect(await inbox.messages(proof.leaf)).to.equal(types.MessageStatus.NONE); + }); - expect(inbox.process(message, newProof, index, '0x')).to.be.revertedWith( - '!cache', - ); - expect(await inbox.messages(leaf)).to.equal(types.MessageStatus.NONE); + it('Fails to process message when not called by validator manager', async () => { + await expect( + inbox.process( + proof.root, + proof.index, + proof.message, + proof.proof, + proof.index, + ), + ).to.be.revertedWith('!validatorManager'); }); for (let i = 0; i < badRecipientFactories.length; i++) { it(`Fails to process a message for a badly implemented recipient (${ i + 1 })`, async () => { - const sender = abacusMessageSender; const factory = new badRecipientFactories[i](signer); const badRecipient = await factory.deploy(); - const leafIndex = 0; - const abacusMessage = utils.formatMessage( - remoteDomain, - sender.address, - + const badProof = await dispatchMessageAndReturnProof( + helperOutbox, localDomain, - badRecipient.address, - '0x', + utils.addressToBytes32(badRecipient.address), + 'hello world', ); - await expect(inbox.testProcess(abacusMessage, leafIndex)).to.be.reverted; + await expect( + validatorManager.process( + inbox.address, + badProof.root, + badProof.index, + badProof.message, + badProof.proof, + badProof.index, + ), + ).to.be.reverted; }); } it('Fails to process message with wrong destination Domain', async () => { - const [sender, recipient] = await ethers.getSigners(); - const body = ethers.utils.formatBytes32String('message'); - - const leafIndex = 0; - const abacusMessage = utils.formatMessage( - remoteDomain, - sender.address, - // Wrong destination Domain - localDomain + 5, - recipient.address, - body, + const badProof = await dispatchMessageAndReturnProof( + helperOutbox, + localDomain + 1, + recipient, + 'hello world', ); await expect( - inbox.testProcess(abacusMessage, leafIndex), + validatorManager.process( + inbox.address, + badProof.root, + badProof.index, + badProof.message, + badProof.proof, + badProof.index, + ), ).to.be.revertedWith('!destination'); }); it('Fails to process message sent to a non-existent contract address', async () => { - const body = ethers.utils.formatBytes32String('message'); - - const leafIndex = 0; - const abacusMessage = utils.formatMessage( - remoteDomain, - abacusMessageSender.address, - localDomain, - '0x1234567890123456789012345678901234567890', // non-existent contract address - body, - ); - - await expect(inbox.testProcess(abacusMessage, leafIndex)).to.be.reverted; - }); - - it('Fails to process a message for bad handler function', async () => { - const sender = abacusMessageSender; - const [recipient] = await ethers.getSigners(); - const factory = new BadRecipientHandle__factory(recipient); - const testRecipient = await factory.deploy(); - - const leafIndex = 0; - const abacusMessage = utils.formatMessage( - remoteDomain, - sender.address, - localDomain, - testRecipient.address, - '0x', - ); - - // Ensure bad handler function causes process to fail - await expect(inbox.testProcess(abacusMessage, leafIndex)).to.be.reverted; - }); - - it('Processes a message directly', async () => { - const sender = abacusMessageSender; - const [recipient] = await ethers.getSigners(); - const factory = new TestRecipient__factory(recipient); - const testRecipient = await factory.deploy(); - - const leafIndex = 0; - const abacusMessage = utils.formatMessage( - remoteDomain, - sender.address, - localDomain, - testRecipient.address, - '0x', - ); - - await inbox.testProcess(abacusMessage, leafIndex); - - const hash = utils.messageHash(abacusMessage, leafIndex); - expect(await inbox.messages(hash)).to.eql(MessageStatus.PROCESSED); - }); - - it('Proves and processes a message', async () => { - const sender = abacusMessageSender; - const testRecipientFactory = new TestRecipient__factory(signer); - const testRecipient = await testRecipientFactory.deploy(); - - const leafIndex = 0; - // Note that hash of this message specifically matches leaf of 1st - // proveAndProcess test case - const abacusMessage = utils.formatMessage( - remoteDomain, - sender.address, + const badProof = await dispatchMessageAndReturnProof( + helperOutbox, localDomain, - testRecipient.address, - '0x', + utils.addressToBytes32('0x1234567890123456789012345678901234567890'), // non-existent contract address + 'hello world', ); - - // Assert above message and test case have matching leaves - const { path, index } = proveAndProcessTestCases[0]; - const hash = utils.messageHash(abacusMessage, leafIndex); - - // Set inbox's current root to match newly computed root that includes - // the new leaf (normally root will have already been computed and path - // simply verifies leaf is in tree but because it is cryptographically - // impossible to find the inputs that create a pre-determined root, we - // simply recalculate root with the leaf using branchRoot) - const proofRoot = await inbox.testBranchRoot(hash, path, index); - await inbox.setCachedCheckpoint(proofRoot, 1); - - await inbox.process(abacusMessage, path, index, '0x'); - - expect(await inbox.messages(hash)).to.equal(types.MessageStatus.PROCESSED); + await expect( + validatorManager.process( + inbox.address, + badProof.root, + badProof.index, + badProof.message, + badProof.proof, + badProof.index, + ), + ).to.be.reverted; }); }); diff --git a/solidity/core/test/lib/mailboxes.ts b/solidity/core/test/lib/mailboxes.ts new file mode 100644 index 000000000..32b6065f6 --- /dev/null +++ b/solidity/core/test/lib/mailboxes.ts @@ -0,0 +1,56 @@ +import { expect } from 'chai'; +import { BigNumber, ethers } from 'ethers'; + +import { utils } from '@abacus-network/utils'; + +import { TestOutbox } from '../../types'; +import { DispatchEvent } from '../../types/contracts/Outbox'; + +export const dispatchMessage = async ( + outbox: TestOutbox, + destination: number, + recipient: string, + messageStr: string, +) => { + const tx = await outbox.dispatch( + destination, + recipient, + ethers.utils.toUtf8Bytes(messageStr), + ); + const receipt = await tx.wait(); + const dispatch = receipt.events![0] as DispatchEvent; + expect(dispatch.event).to.equal('Dispatch'); + return dispatch.args!; +}; + +export const dispatchMessageAndReturnProof = async ( + outbox: TestOutbox, + destination: number, + recipient: string, + messageStr: string, +): Promise => { + const { leafIndex, message } = await dispatchMessage( + outbox, + destination, + recipient, + messageStr, + ); + const messageHash = utils.messageHash(message, leafIndex.toNumber()); + const root = await outbox.root(); + const proof = await outbox.proof(); + return { + root, + proof: proof, + leaf: messageHash, + index: leafIndex, + message, + }; +}; + +export interface MerkleProof { + root: string; + proof: string[]; + leaf: string; + index: BigNumber; + message: string; +} diff --git a/solidity/core/test/outbox.test.ts b/solidity/core/test/outbox.test.ts index 7d04c5c20..52aa9dd99 100644 --- a/solidity/core/test/outbox.test.ts +++ b/solidity/core/test/outbox.test.ts @@ -89,7 +89,7 @@ describe('Outbox', async () => { }); it('Dispatches a message', async () => { - const { message, destDomain, abacusMessage, hash, leafIndex } = + const { message, destDomain, abacusMessage, leafIndex, hash } = await testMessageValues(); // Send message with signer address as msg.sender diff --git a/solidity/core/test/validator-manager/inboxValidatorManager.test.ts b/solidity/core/test/validator-manager/inboxValidatorManager.test.ts index 2ac65a610..e0127a63b 100644 --- a/solidity/core/test/validator-manager/inboxValidatorManager.test.ts +++ b/solidity/core/test/validator-manager/inboxValidatorManager.test.ts @@ -2,14 +2,17 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { ethers } from 'hardhat'; -import { Validator } from '@abacus-network/utils'; +import { Validator, types, utils } from '@abacus-network/utils'; import { Inbox, InboxValidatorManager, InboxValidatorManager__factory, Inbox__factory, + TestOutbox__factory, + TestRecipient__factory, } from '../../types'; +import { MerkleProof, dispatchMessageAndReturnProof } from '../lib/mailboxes'; import { signCheckpoint } from './utils'; @@ -21,6 +24,7 @@ describe('InboxValidatorManager', () => { let validatorManager: InboxValidatorManager, inbox: Inbox, signer: SignerWithAddress, + proof: MerkleProof, validator0: Validator, validator1: Validator; @@ -42,42 +46,60 @@ describe('InboxValidatorManager', () => { const inboxFactory = new Inbox__factory(signer); inbox = await inboxFactory.deploy(INBOX_DOMAIN); await inbox.initialize(OUTBOX_DOMAIN, validatorManager.address); - }); - describe('#checkpoint', () => { - const root = ethers.utils.formatBytes32String('test root'); - const index = 1; + // Deploy a helper outbox contract so that we can easily construct merkle + // proofs. + const outboxFactory = new TestOutbox__factory(signer); + const helperOutbox = await outboxFactory.deploy(OUTBOX_DOMAIN); + await helperOutbox.initialize(validatorManager.address); + const recipientF = await new TestRecipient__factory(signer).deploy(); + const recipient = utils.addressToBytes32(recipientF.address); + proof = await dispatchMessageAndReturnProof( + helperOutbox, + INBOX_DOMAIN, + recipient, + 'hello world', + ); + }); - it('submits a checkpoint to the Inbox if there is a quorum', async () => { + describe('#process', () => { + it('processes a message on the Inbox if there is a quorum', async () => { const signatures = await signCheckpoint( - root, - index, + proof.root, + proof.index, [validator0, validator1], // 2/2 signers, making a quorum ); - await validatorManager.cacheCheckpoint( + await validatorManager.process( inbox.address, - root, - index, + proof.root, + proof.index, signatures, + proof.message, + proof.proof, + proof.index, + ); + expect(await inbox.messages(proof.leaf)).to.eql( + types.MessageStatus.PROCESSED, ); - - expect(await inbox.cachedCheckpoints(root)).to.equal(index); }); it('reverts if there is not a quorum', async () => { const signatures = await signCheckpoint( - root, - index, + proof.root, + proof.index, [validator0], // 1/2 signers is not a quorum ); await expect( - validatorManager.cacheCheckpoint( + validatorManager.process( inbox.address, - root, - index, + proof.root, + proof.index, signatures, + proof.message, + proof.proof, + proof.index, ), ).to.be.revertedWith('!quorum'); }); diff --git a/solidity/core/test/validator-manager/outboxValidatorManager.test.ts b/solidity/core/test/validator-manager/outboxValidatorManager.test.ts index 670cded12..8e73f4af1 100644 --- a/solidity/core/test/validator-manager/outboxValidatorManager.test.ts +++ b/solidity/core/test/validator-manager/outboxValidatorManager.test.ts @@ -1,6 +1,5 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { BigNumber } from 'ethers'; import { ethers } from 'hardhat'; import { Validator, types, utils } from '@abacus-network/utils'; @@ -11,7 +10,10 @@ import { TestOutbox, TestOutbox__factory, } from '../../types'; -import { DispatchEvent } from '../../types/contracts/Outbox'; +import { + MerkleProof, + dispatchMessageAndReturnProof as _dispatchMessageAndReturnProof, +} from '../lib/mailboxes'; import { signCheckpoint } from './utils'; @@ -19,13 +21,6 @@ const OUTBOX_DOMAIN = 1234; const INBOX_DOMAIN = 4321; const QUORUM_THRESHOLD = 2; -interface MerkleProof { - root: string; - proof: string[]; - leaf: string; - index: BigNumber; -} - describe('OutboxValidatorManager', () => { let validatorManager: OutboxValidatorManager, outbox: TestOutbox, @@ -34,38 +29,6 @@ describe('OutboxValidatorManager', () => { validator0: Validator, validator1: Validator; - const dispatchMessage = async (outbox: TestOutbox, message: string) => { - const recipient = utils.addressToBytes32(validator0.address); - const destination = INBOX_DOMAIN; - const tx = await outbox.dispatch( - destination, - recipient, - ethers.utils.formatBytes32String(message), - ); - const receipt = await tx.wait(); - const dispatch = receipt.events![0] as DispatchEvent; - expect(dispatch.event).to.equal('Dispatch'); - return dispatch.args!; - }; - - const dispatchMessageAndReturnProof = async ( - outbox: TestOutbox, - messageStr: string, - ): Promise => { - const { messageHash, leafIndex } = await dispatchMessage( - outbox, - messageStr, - ); - const root = await outbox.root(); - const proof = await outbox.proof(); - return { - root, - proof, - leaf: messageHash, - index: leafIndex, - }; - }; - before(async () => { const signers = await ethers.getSigners(); signer = signers[0]; @@ -73,6 +36,18 @@ describe('OutboxValidatorManager', () => { validator1 = await Validator.fromSigner(signers[2], OUTBOX_DOMAIN); }); + const dispatchMessageAndReturnProof = async ( + outbox: TestOutbox, + message: string, + ) => { + return _dispatchMessageAndReturnProof( + outbox, + INBOX_DOMAIN, + utils.addressToBytes32(validator0.address), + message, + ); + }; + beforeEach(async () => { const validatorManagerFactory = new OutboxValidatorManager__factory(signer); validatorManager = await validatorManagerFactory.deploy( @@ -99,7 +74,7 @@ describe('OutboxValidatorManager', () => { const root = ethers.utils.formatBytes32String('test root'); beforeEach(async () => { - await dispatchMessage(outbox, 'message'); + await dispatchMessageAndReturnProof(outbox, 'message'); }); it('accepts a premature checkpoint if it has been signed by a quorum of validators', async () => { @@ -176,8 +151,8 @@ describe('OutboxValidatorManager', () => { const helperMessage = (j: number) => j === differingIndex ? fraudulentMessage : actualMessage; for (; index < proofIndex; index++) { - await dispatchMessage(outbox, actualMessage); - await dispatchMessage(helperOutbox, helperMessage(index)); + await dispatchMessageAndReturnProof(outbox, actualMessage); + await dispatchMessageAndReturnProof(helperOutbox, helperMessage(index)); } const proofA = await dispatchMessageAndReturnProof(outbox, actualMessage); const proofB = await dispatchMessageAndReturnProof( @@ -185,8 +160,8 @@ describe('OutboxValidatorManager', () => { helperMessage(proofIndex), ); for (index = proofIndex + 1; index < messageCount; index++) { - await dispatchMessage(outbox, actualMessage); - await dispatchMessage(helperOutbox, helperMessage(index)); + await dispatchMessageAndReturnProof(outbox, actualMessage); + await dispatchMessageAndReturnProof(helperOutbox, helperMessage(index)); } return { proofA: proofA, proofB: proofB }; diff --git a/typescript/infra/hardhat.config.ts b/typescript/infra/hardhat.config.ts index d9a784b13..a5c718d91 100644 --- a/typescript/infra/hardhat.config.ts +++ b/typescript/infra/hardhat.config.ts @@ -28,15 +28,11 @@ const chainSummary = async ( const remoteContracts = core.getContracts(remote); const inbox = remoteContracts.inboxes[chain as Exclude].inbox.contract; - const [inboxCheckpointRoot, inboxCheckpointIndex] = - await inbox.latestCachedCheckpoint(); const processFilter = inbox.filters.Process(); const processes = await inbox.queryFilter(processFilter); return { chain: remote, processed: processes.length, - root: inboxCheckpointRoot, - index: inboxCheckpointIndex.toNumber(), }; }; diff --git a/typescript/sdk/src/core/events.ts b/typescript/sdk/src/core/events.ts index f8d2d0862..bc9854732 100644 --- a/typescript/sdk/src/core/events.ts +++ b/typescript/sdk/src/core/events.ts @@ -1,23 +1,13 @@ -import type { - CheckpointCachedEvent, - ProcessEvent, -} from '@abacus-network/core/types/contracts/Inbox'; +import type { ProcessEvent } from '@abacus-network/core/types/contracts/Inbox'; import type { DispatchEvent } from '@abacus-network/core/types/contracts/Outbox'; import { Annotated } from '../events'; -export { DispatchEvent, CheckpointCachedEvent, ProcessEvent }; +export { DispatchEvent, ProcessEvent }; -export type AbacusLifecyleEvent = - | ProcessEvent - | CheckpointCachedEvent - | DispatchEvent; +export type AbacusLifecyleEvent = ProcessEvent | DispatchEvent; export type AnnotatedDispatch = Annotated; -export type AnnotatedCheckpoint = Annotated; export type AnnotatedProcess = Annotated; -export type AnnotatedLifecycleEvent = - | AnnotatedDispatch - | AnnotatedCheckpoint - | AnnotatedProcess; +export type AnnotatedLifecycleEvent = AnnotatedDispatch | AnnotatedProcess; diff --git a/typescript/sdk/src/core/message.ts b/typescript/sdk/src/core/message.ts index 6ae5147fb..1797013f4 100644 --- a/typescript/sdk/src/core/message.ts +++ b/typescript/sdk/src/core/message.ts @@ -17,11 +17,9 @@ import { import { delay } from '../utils'; import { - AnnotatedCheckpoint, AnnotatedDispatch, AnnotatedLifecycleEvent, AnnotatedProcess, - CheckpointCachedEvent, DispatchEvent, ProcessEvent, } from './events'; @@ -73,7 +71,6 @@ export enum InboxMessageStatus { } export type EventCache = { - inboxCheckpoint?: AnnotatedCheckpoint; process?: AnnotatedProcess; }; @@ -272,55 +269,12 @@ export class AbacusMessage { ); } - /** - * Get the Inbox `Checkpoint` event associated with this message (if any) - * - * @returns An {@link AnnotatedCheckpoint} (if any) - */ - async getInboxCheckpoint(): Promise { - // if we have already gotten the event, - // return it without re-querying - if (this.cache.inboxCheckpoint) { - return this.cache.inboxCheckpoint; - } - - const leafIndex = this.dispatch.event.args.leafIndex; - const [checkpointRoot, checkpointIndex] = - await this.inbox.latestCachedCheckpoint(); - // The checkpoint index needs to be at least leafIndex + 1 to include - // the message. - if (checkpointIndex.lte(leafIndex)) { - return undefined; - } - - // if not, attempt to query the event - const checkpointFilter = this.inbox.filters.CheckpointCached( - checkpointRoot, - checkpointIndex, - ); - const checkpointLogs: AnnotatedCheckpoint[] = - await findAnnotatedSingleEvent( - this.multiProvider, - this.destinationName, - this.inbox, - checkpointFilter, - ); - if (checkpointLogs.length === 1) { - // if event is returned, store it to the object - this.cache.inboxCheckpoint = checkpointLogs[0]; - } else if (checkpointLogs.length > 1) { - throw new Error('multiple inbox checkpoints for same root'); - } - // return the event or undefined if it wasn't found - return this.cache.inboxCheckpoint; - } - /** * Get the Inbox `Process` event associated with this message (if any) * * @returns An {@link AnnotatedProcess} (if any) */ - async getProcess(startBlock?: number): Promise { + async getProcess(): Promise { // if we have already gotten the event, // return it without re-querying if (this.cache.process) { @@ -333,7 +287,6 @@ export class AbacusMessage { this.destinationName, this.inbox, processFilter, - startBlock, ); if (processLogs.length === 1) { // if event is returned, store it to the object @@ -352,17 +305,8 @@ export class AbacusMessage { */ async events(): Promise { const events: AnnotatedLifecycleEvent[] = [this.dispatch]; - // attempt to get Inbox checkpoint - const inboxCheckpoint = await this.getInboxCheckpoint(); - if (!inboxCheckpoint) { - return { - status: MessageStatus.Included, // the message was sent, then included in an Checkpoint on Outbox - events, - }; - } - events.push(inboxCheckpoint); // attempt to get Inbox process - const process = await this.getProcess(inboxCheckpoint.blockNumber); + const process = await this.getProcess(); if (!process) { // NOTE: when this is the status, you may way to // query confirmAt() to check if challenge period