Remove caching of checkpoints from the Inbox (#523)

pull/527/head
Asa Oines 3 years ago committed by GitHub
parent 099cd93cec
commit 81b66b58aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 52
      solidity/core/contracts/Inbox.sol
  2. 43
      solidity/core/contracts/Mailbox.sol
  3. 38
      solidity/core/contracts/Outbox.sol
  4. 4
      solidity/core/contracts/test/TestInbox.sol
  5. 4
      solidity/core/contracts/test/TestMailbox.sol
  6. 9
      solidity/core/contracts/test/TestValidatorManager.sol
  7. 2
      solidity/core/contracts/test/bad-recipient/BadRecipient2.sol
  8. 33
      solidity/core/contracts/validator-manager/InboxValidatorManager.sol
  9. 7
      solidity/core/interfaces/IInbox.sol
  10. 7
      solidity/core/interfaces/IMailbox.sol
  11. 7
      solidity/core/interfaces/IOutbox.sol
  12. 69
      solidity/core/test/common.test.ts
  13. 298
      solidity/core/test/inbox.test.ts
  14. 56
      solidity/core/test/lib/mailboxes.ts
  15. 2
      solidity/core/test/outbox.test.ts
  16. 58
      solidity/core/test/validator-manager/inboxValidatorManager.test.ts
  17. 67
      solidity/core/test/validator-manager/outboxValidatorManager.test.ts
  18. 4
      typescript/infra/hardhat.config.ts
  19. 18
      typescript/sdk/src/core/events.ts
  20. 60
      typescript/sdk/src/core/message.ts

@ -55,14 +55,8 @@ contract Inbox is IInbox, ReentrancyGuardUpgradeable, Version0, Mailbox {
* @dev This event allows watchers to observe the merkle proof they need * @dev This event allows watchers to observe the merkle proof they need
* to prove fraud on the Outbox. * to prove fraud on the Outbox.
* @param messageHash Hash of message that was processed. * @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( event Process(bytes32 indexed messageHash);
bytes32 indexed messageHash,
uint256 indexed leafIndex,
bytes32[32] proof
);
// ============ Constructor ============ // ============ Constructor ============
@ -82,41 +76,27 @@ contract Inbox is IInbox, ReentrancyGuardUpgradeable, Version0, Mailbox {
// ============ External Functions ============ // ============ 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 * @notice Attempts to process the provided formatted `message`. Performs
* verification against root of the proof * 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 Reverts if verification of the message fails.
* @dev Includes the eventual function signature for Sovereign Consensus, * @param _root The merkle root of the checkpoint used to prove message inclusion.
* but comments out the name to suppress compiler warning * @param _index The index of the checkpoint used to prove message inclusion.
* @param _message Formatted message (refer to Mailbox.sol Message library) * @param _message Formatted message (refer to Mailbox.sol Message library)
* @param _proof Merkle proof of inclusion for message's leaf * @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( function process(
bytes32 _root,
uint256 _index,
bytes calldata _message, bytes calldata _message,
bytes32[32] calldata _proof, bytes32[32] calldata _proof,
uint256 _index, uint256 _leafIndex
bytes calldata /* _sovereignData */ ) external override nonReentrant onlyValidatorManager {
) external override nonReentrant { require(_index >= _leafIndex, "!index");
bytes32 _messageHash = _message.leaf(_index); bytes32 _messageHash = _message.leaf(_leafIndex);
// ensure that message has not been processed // ensure that message has not been processed
require( require(
messages[_messageHash] == MessageStatus.None, messages[_messageHash] == MessageStatus.None,
@ -126,12 +106,12 @@ contract Inbox is IInbox, ReentrancyGuardUpgradeable, Version0, Mailbox {
bytes32 _calculatedRoot = MerkleLib.branchRoot( bytes32 _calculatedRoot = MerkleLib.branchRoot(
_messageHash, _messageHash,
_proof, _proof,
_index _leafIndex
); );
// ensure that the root has been cached // verify the merkle proof
require(cachedCheckpoints[_calculatedRoot] >= _index, "!cache"); require(_calculatedRoot == _root, "!proof");
_process(_message, _messageHash); _process(_message, _messageHash);
emit Process(_messageHash, _index, _proof); emit Process(_messageHash);
} }
// ============ Internal Functions ============ // ============ Internal Functions ============

@ -20,29 +20,16 @@ abstract contract Mailbox is IMailbox, OwnableUpgradeable {
// ============ Public Variables ============ // ============ 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 of the validator manager contract.
address public validatorManager; address public validatorManager;
// ============ Upgrade Gap ============ // ============ Upgrade Gap ============
// gap for upgrade safety // gap for upgrade safety
uint256[47] private __GAP; uint256[49] private __GAP;
// ============ Events ============ // ============ 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 * @notice Emitted when the validator manager contract is changed
* @param validatorManager The address of the new validatorManager * @param validatorManager The address of the new validatorManager
@ -89,21 +76,6 @@ abstract contract Mailbox is IMailbox, OwnableUpgradeable {
_setValidatorManager(_validatorManager); _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 ============ // ============ Internal Functions ============
/** /**
@ -118,17 +90,4 @@ abstract contract Mailbox is IMailbox, OwnableUpgradeable {
validatorManager = _validatorManager; validatorManager = _validatorManager;
emit NewValidatorManager(_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);
}
} }

@ -48,16 +48,29 @@ contract Outbox is IOutbox, Version0, MerkleTreeManager, Mailbox {
// ============ Public Storage Variables ============ // ============ 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 // Current state of contract
States public state; States public state;
// ============ Upgrade Gap ============ // ============ Upgrade Gap ============
// gap for upgrade safety // gap for upgrade safety
uint256[49] private __GAP; uint256[47] private __GAP;
// ============ Events ============ // ============ 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 * @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 * @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. * @notice Caches the current merkle root and index.
* @dev emits Checkpoint event * @dev emits CheckpointCached event
*/ */
function cacheCheckpoint() external override notFailed { function cacheCheckpoint() external override notFailed {
(bytes32 root, uint256 index) = latestCheckpoint(); (bytes32 _root, uint256 _index) = latestCheckpoint();
_cacheCheckpoint(root, index); 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(); 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 * @notice Returns the number of inserted leaves in the tree
*/ */

@ -8,10 +8,6 @@ contract TestInbox is Inbox {
constructor(uint32 _localDomain) Inbox(_localDomain) {} // solhint-disable-line no-empty-blocks constructor(uint32 _localDomain) Inbox(_localDomain) {} // solhint-disable-line no-empty-blocks
function setCachedCheckpoint(bytes32 _root, uint256 _index) external {
cachedCheckpoints[_root] = _index;
}
function testBranchRoot( function testBranchRoot(
bytes32 leaf, bytes32 leaf,
bytes32[32] calldata proof, bytes32[32] calldata proof,

@ -9,8 +9,4 @@ contract TestMailbox is Mailbox {
function initialize(address _validatorManager) external initializer { function initialize(address _validatorManager) external initializer {
__Mailbox_initialize(_validatorManager); __Mailbox_initialize(_validatorManager);
} }
function cacheCheckpoint(bytes32 _root, uint256 _index) external {
_cacheCheckpoint(_root, _index);
}
} }

@ -8,11 +8,14 @@ import {IInbox} from "../../interfaces/IInbox.sol";
* to be a contract. * to be a contract.
*/ */
contract TestValidatorManager { contract TestValidatorManager {
function cacheCheckpoint( function process(
IInbox _inbox, IInbox _inbox,
bytes32 _root, bytes32 _root,
uint256 _index uint256 _index,
bytes calldata _message,
bytes32[32] calldata _proof,
uint256 _leafIndex
) external { ) external {
_inbox.cacheCheckpoint(_root, _index); _inbox.process(_root, _index, _message, _proof, _leafIndex);
} }
} }

@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0; pragma solidity >=0.8.0;
contract BadRecipientHandle { contract BadRecipient2 {
function handle(uint32, bytes32) external pure {} // solhint-disable-line no-empty-blocks function handle(uint32, bytes32) external pure {} // solhint-disable-line no-empty-blocks
} }

@ -12,18 +12,6 @@ import {MultisigValidatorManager} from "./MultisigValidatorManager.sol";
* them to an Inbox. * them to an Inbox.
*/ */
contract InboxValidatorManager is MultisigValidatorManager { 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 ============ // ============ Constructor ============
/** /**
@ -43,24 +31,29 @@ contract InboxValidatorManager is MultisigValidatorManager {
// ============ External Functions ============ // ============ 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 a quorum of validator signatures.
* @dev Reverts if `_signatures` is not sorted in ascending order by the signer * @dev Reverts if `_signatures` is not sorted in ascending order by the signer
* address, which is required for duplicate detection. * address, which is required for duplicate detection.
* @param _inbox The inbox to submit the checkpoint to. * @param _inbox The inbox to submit the message to.
* @param _root The merkle root of the checkpoint. * @param _root The merkle root of the signed checkpoint.
* @param _index The index of the checkpoint. * @param _index The index of the signed checkpoint.
* @param _signatures Signatures over the checkpoint to be checked for a validator * @param _signatures Signatures over the checkpoint to be checked for a validator
* quorum. Must be sorted in ascending order by signer address. * 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, IInbox _inbox,
bytes32 _root, bytes32 _root,
uint256 _index, uint256 _index,
bytes[] calldata _signatures bytes[] calldata _signatures,
bytes calldata _message,
bytes32[32] calldata _proof,
uint256 _leafIndex
) external { ) external {
require(isQuorum(_root, _index, _signatures), "!quorum"); require(isQuorum(_root, _index, _signatures), "!quorum");
emit Quorum(_signatures); _inbox.process(_root, _index, _message, _proof, _leafIndex);
_inbox.cacheCheckpoint(_root, _index);
} }
} }

@ -4,14 +4,13 @@ pragma solidity >=0.6.11;
import {IMailbox} from "./IMailbox.sol"; import {IMailbox} from "./IMailbox.sol";
interface IInbox is IMailbox { interface IInbox is IMailbox {
function cacheCheckpoint(bytes32 _root, uint256 _index) external;
function remoteDomain() external returns (uint32); function remoteDomain() external returns (uint32);
function process( function process(
bytes32 _root,
uint256 _index,
bytes calldata _message, bytes calldata _message,
bytes32[32] calldata _proof, bytes32[32] calldata _proof,
uint256 _index, uint256 _leafIndex
bytes calldata _sovereignData
) external; ) external;
} }

@ -3,11 +3,4 @@ pragma solidity >=0.6.11;
interface IMailbox { interface IMailbox {
function localDomain() external view returns (uint32); function localDomain() external view returns (uint32);
function cachedCheckpoints(bytes32) external view returns (uint256);
function latestCachedCheckpoint()
external
view
returns (bytes32 root, uint256 index);
} }

@ -17,4 +17,11 @@ interface IOutbox is IMailbox {
function count() external returns (uint256); function count() external returns (uint256);
function fail() external; function fail() external;
function cachedCheckpoints(bytes32) external view returns (uint256);
function latestCachedCheckpoint()
external
view
returns (bytes32 root, uint256 index);
} }

@ -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',
);
});
});

@ -4,29 +4,31 @@ import { expect } from 'chai';
import { ethers } from 'hardhat'; import { ethers } from 'hardhat';
import { types, utils } from '@abacus-network/utils'; 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 { import {
BadRecipient1__factory, BadRecipient1__factory,
BadRecipient2__factory,
BadRecipient3__factory, BadRecipient3__factory,
BadRecipient5__factory, BadRecipient5__factory,
BadRecipient6__factory, BadRecipient6__factory,
BadRecipientHandle__factory,
TestInbox, TestInbox,
TestInbox__factory, TestInbox__factory,
TestOutbox,
TestOutbox__factory,
TestRecipient__factory, TestRecipient__factory,
TestValidatorManager, TestValidatorManager,
TestValidatorManager__factory, TestValidatorManager__factory,
} from '../types'; } from '../types';
import { MerkleProof, dispatchMessageAndReturnProof } from './lib/mailboxes';
const localDomain = 3000; const localDomain = 3000;
const remoteDomain = 1000; const remoteDomain = 1000;
describe('Inbox', async () => { describe('Inbox', async () => {
const badRecipientFactories = [ const badRecipientFactories = [
BadRecipient1__factory, BadRecipient1__factory,
BadRecipient2__factory,
BadRecipient3__factory, BadRecipient3__factory,
BadRecipient5__factory, BadRecipient5__factory,
BadRecipient6__factory, BadRecipient6__factory,
@ -34,235 +36,173 @@ describe('Inbox', async () => {
let inbox: TestInbox, let inbox: TestInbox,
signer: SignerWithAddress, signer: SignerWithAddress,
abacusMessageSender: SignerWithAddress, validatorManager: TestValidatorManager,
validatorManager: TestValidatorManager; helperOutbox: TestOutbox,
recipient: string,
proof: MerkleProof;
before(async () => { before(async () => {
[signer, abacusMessageSender] = await ethers.getSigners(); [signer] = await ethers.getSigners();
// Inbox.initialize will ensure the validator manager is a contract. // Inbox.initialize will ensure the validator manager is a contract.
// TestValidatorManager doesn't have any special logic, it just submits // TestValidatorManager doesn't have any special logic, it just forwards
// checkpoints without any signature verification. // calls to Inbox.process.
const testValidatorManagerFactory = new TestValidatorManager__factory( const testValidatorManagerFactory = new TestValidatorManager__factory(
signer, signer,
); );
validatorManager = await testValidatorManagerFactory.deploy(); 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 () => { beforeEach(async () => {
const inboxFactory = new TestInbox__factory(signer); const inboxFactory = new TestInbox__factory(signer);
inbox = await inboxFactory.deploy(localDomain); inbox = await inboxFactory.deploy(remoteDomain);
await inbox.initialize(remoteDomain, validatorManager.address); await inbox.initialize(localDomain, validatorManager.address);
}); });
it('Cannot be initialized twice', async () => { it('Cannot be initialized twice', async () => {
await expect( await expect(
inbox.initialize(remoteDomain, validatorManager.address), inbox.initialize(localDomain, validatorManager.address),
).to.be.revertedWith('Initializable: contract is already initialized'); ).to.be.revertedWith('Initializable: contract is already initialized');
}); });
it('Caches checkpoint from validator manager', async () => { it('processes a message', async () => {
const root = ethers.utils.formatBytes32String('first new root'); await validatorManager.process(
const index = 1; inbox.address,
await validatorManager.cacheCheckpoint(inbox.address, root, index); proof.root,
const [croot, cindex] = await inbox.latestCachedCheckpoint(); proof.index,
expect(croot).to.equal(root); proof.message,
expect(cindex).to.equal(index); proof.proof,
}); proof.index,
);
it('Rejects checkpoint from non-validator manager', async () => { expect(await inbox.messages(proof.leaf)).to.eql(
const root = ethers.utils.formatBytes32String('first new root'); types.MessageStatus.PROCESSED,
const index = 1;
await expect(inbox.cacheCheckpoint(root, index)).to.be.revertedWith(
'!validatorManager',
); );
});
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 () => { it('Rejects an already-processed message', async () => {
const { leaf, index, proof, root, message } = messageWithProof; await inbox.setMessageStatus(proof.leaf, types.MessageStatus.PROCESSED);
await inbox.setCachedCheckpoint(root, 1);
// Set message status as MessageStatus.Processed
await inbox.setMessageStatus(leaf, MessageStatus.PROCESSED);
// Try to process message again // Try to process message again
await expect(inbox.process(message, proof, index, '0x')).to.be.revertedWith( await expect(
'!MessageStatus.None', 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 () => { it('Rejects invalid message proof', async () => {
const { leaf, index, proof, root, message } = messageWithProof;
// Switch ordering of proof hashes // Switch ordering of proof hashes
// NB: We copy 'path' here to avoid mutating the test cases for // NB: We copy 'path' here to avoid mutating the test cases for
// other tests. // other tests.
const newProof = [...proof]; const newProof = proof.proof.slice().reverse();
newProof[0] = proof[1];
newProof[1] = proof[0]; expect(
validatorManager.process(
await inbox.setCachedCheckpoint(root, 1); inbox.address,
proof.root,
expect(inbox.process(message, newProof, index, '0x')).to.be.revertedWith( proof.index,
'!cache', proof.message,
); newProof,
expect(await inbox.messages(leaf)).to.equal(types.MessageStatus.NONE); proof.index,
),
).to.be.revertedWith('!proof');
expect(await inbox.messages(proof.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++) { for (let i = 0; i < badRecipientFactories.length; i++) {
it(`Fails to process a message for a badly implemented recipient (${ it(`Fails to process a message for a badly implemented recipient (${
i + 1 i + 1
})`, async () => { })`, async () => {
const sender = abacusMessageSender;
const factory = new badRecipientFactories[i](signer); const factory = new badRecipientFactories[i](signer);
const badRecipient = await factory.deploy(); const badRecipient = await factory.deploy();
const leafIndex = 0; const badProof = await dispatchMessageAndReturnProof(
const abacusMessage = utils.formatMessage( helperOutbox,
remoteDomain,
sender.address,
localDomain, localDomain,
badRecipient.address, utils.addressToBytes32(badRecipient.address),
'0x', '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 () => { it('Fails to process message with wrong destination Domain', async () => {
const [sender, recipient] = await ethers.getSigners(); const badProof = await dispatchMessageAndReturnProof(
const body = ethers.utils.formatBytes32String('message'); helperOutbox,
localDomain + 1,
const leafIndex = 0; recipient,
const abacusMessage = utils.formatMessage( 'hello world',
remoteDomain,
sender.address,
// Wrong destination Domain
localDomain + 5,
recipient.address,
body,
); );
await expect( await expect(
inbox.testProcess(abacusMessage, leafIndex), validatorManager.process(
inbox.address,
badProof.root,
badProof.index,
badProof.message,
badProof.proof,
badProof.index,
),
).to.be.revertedWith('!destination'); ).to.be.revertedWith('!destination');
}); });
it('Fails to process message sent to a non-existent contract address', async () => { it('Fails to process message sent to a non-existent contract address', async () => {
const body = ethers.utils.formatBytes32String('message'); const badProof = await dispatchMessageAndReturnProof(
helperOutbox,
const leafIndex = 0;
const abacusMessage = utils.formatMessage(
remoteDomain,
abacusMessageSender.address,
localDomain, localDomain,
'0x1234567890123456789012345678901234567890', // non-existent contract address utils.addressToBytes32('0x1234567890123456789012345678901234567890'), // non-existent contract address
body, 'hello world',
); );
await expect(
await expect(inbox.testProcess(abacusMessage, leafIndex)).to.be.reverted; validatorManager.process(
}); inbox.address,
badProof.root,
it('Fails to process a message for bad handler function', async () => { badProof.index,
const sender = abacusMessageSender; badProof.message,
const [recipient] = await ethers.getSigners(); badProof.proof,
const factory = new BadRecipientHandle__factory(recipient); badProof.index,
const testRecipient = await factory.deploy(); ),
).to.be.reverted;
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,
localDomain,
testRecipient.address,
'0x',
);
// 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);
}); });
}); });

@ -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<MerkleProof> => {
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;
}

@ -89,7 +89,7 @@ describe('Outbox', async () => {
}); });
it('Dispatches a message', async () => { it('Dispatches a message', async () => {
const { message, destDomain, abacusMessage, hash, leafIndex } = const { message, destDomain, abacusMessage, leafIndex, hash } =
await testMessageValues(); await testMessageValues();
// Send message with signer address as msg.sender // Send message with signer address as msg.sender

@ -2,14 +2,17 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { expect } from 'chai'; import { expect } from 'chai';
import { ethers } from 'hardhat'; import { ethers } from 'hardhat';
import { Validator } from '@abacus-network/utils'; import { Validator, types, utils } from '@abacus-network/utils';
import { import {
Inbox, Inbox,
InboxValidatorManager, InboxValidatorManager,
InboxValidatorManager__factory, InboxValidatorManager__factory,
Inbox__factory, Inbox__factory,
TestOutbox__factory,
TestRecipient__factory,
} from '../../types'; } from '../../types';
import { MerkleProof, dispatchMessageAndReturnProof } from '../lib/mailboxes';
import { signCheckpoint } from './utils'; import { signCheckpoint } from './utils';
@ -21,6 +24,7 @@ describe('InboxValidatorManager', () => {
let validatorManager: InboxValidatorManager, let validatorManager: InboxValidatorManager,
inbox: Inbox, inbox: Inbox,
signer: SignerWithAddress, signer: SignerWithAddress,
proof: MerkleProof,
validator0: Validator, validator0: Validator,
validator1: Validator; validator1: Validator;
@ -42,42 +46,60 @@ describe('InboxValidatorManager', () => {
const inboxFactory = new Inbox__factory(signer); const inboxFactory = new Inbox__factory(signer);
inbox = await inboxFactory.deploy(INBOX_DOMAIN); inbox = await inboxFactory.deploy(INBOX_DOMAIN);
await inbox.initialize(OUTBOX_DOMAIN, validatorManager.address); await inbox.initialize(OUTBOX_DOMAIN, validatorManager.address);
});
describe('#checkpoint', () => { // Deploy a helper outbox contract so that we can easily construct merkle
const root = ethers.utils.formatBytes32String('test root'); // proofs.
const index = 1; 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( const signatures = await signCheckpoint(
root, proof.root,
index, proof.index,
[validator0, validator1], // 2/2 signers, making a quorum [validator0, validator1], // 2/2 signers, making a quorum
); );
await validatorManager.cacheCheckpoint( await validatorManager.process(
inbox.address, inbox.address,
root, proof.root,
index, proof.index,
signatures, 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 () => { it('reverts if there is not a quorum', async () => {
const signatures = await signCheckpoint( const signatures = await signCheckpoint(
root, proof.root,
index, proof.index,
[validator0], // 1/2 signers is not a quorum [validator0], // 1/2 signers is not a quorum
); );
await expect( await expect(
validatorManager.cacheCheckpoint( validatorManager.process(
inbox.address, inbox.address,
root, proof.root,
index, proof.index,
signatures, signatures,
proof.message,
proof.proof,
proof.index,
), ),
).to.be.revertedWith('!quorum'); ).to.be.revertedWith('!quorum');
}); });

@ -1,6 +1,5 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { expect } from 'chai'; import { expect } from 'chai';
import { BigNumber } from 'ethers';
import { ethers } from 'hardhat'; import { ethers } from 'hardhat';
import { Validator, types, utils } from '@abacus-network/utils'; import { Validator, types, utils } from '@abacus-network/utils';
@ -11,7 +10,10 @@ import {
TestOutbox, TestOutbox,
TestOutbox__factory, TestOutbox__factory,
} from '../../types'; } from '../../types';
import { DispatchEvent } from '../../types/contracts/Outbox'; import {
MerkleProof,
dispatchMessageAndReturnProof as _dispatchMessageAndReturnProof,
} from '../lib/mailboxes';
import { signCheckpoint } from './utils'; import { signCheckpoint } from './utils';
@ -19,13 +21,6 @@ const OUTBOX_DOMAIN = 1234;
const INBOX_DOMAIN = 4321; const INBOX_DOMAIN = 4321;
const QUORUM_THRESHOLD = 2; const QUORUM_THRESHOLD = 2;
interface MerkleProof {
root: string;
proof: string[];
leaf: string;
index: BigNumber;
}
describe('OutboxValidatorManager', () => { describe('OutboxValidatorManager', () => {
let validatorManager: OutboxValidatorManager, let validatorManager: OutboxValidatorManager,
outbox: TestOutbox, outbox: TestOutbox,
@ -34,38 +29,6 @@ describe('OutboxValidatorManager', () => {
validator0: Validator, validator0: Validator,
validator1: 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<MerkleProof> => {
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 () => { before(async () => {
const signers = await ethers.getSigners(); const signers = await ethers.getSigners();
signer = signers[0]; signer = signers[0];
@ -73,6 +36,18 @@ describe('OutboxValidatorManager', () => {
validator1 = await Validator.fromSigner(signers[2], OUTBOX_DOMAIN); 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 () => { beforeEach(async () => {
const validatorManagerFactory = new OutboxValidatorManager__factory(signer); const validatorManagerFactory = new OutboxValidatorManager__factory(signer);
validatorManager = await validatorManagerFactory.deploy( validatorManager = await validatorManagerFactory.deploy(
@ -99,7 +74,7 @@ describe('OutboxValidatorManager', () => {
const root = ethers.utils.formatBytes32String('test root'); const root = ethers.utils.formatBytes32String('test root');
beforeEach(async () => { 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 () => { 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) => const helperMessage = (j: number) =>
j === differingIndex ? fraudulentMessage : actualMessage; j === differingIndex ? fraudulentMessage : actualMessage;
for (; index < proofIndex; index++) { for (; index < proofIndex; index++) {
await dispatchMessage(outbox, actualMessage); await dispatchMessageAndReturnProof(outbox, actualMessage);
await dispatchMessage(helperOutbox, helperMessage(index)); await dispatchMessageAndReturnProof(helperOutbox, helperMessage(index));
} }
const proofA = await dispatchMessageAndReturnProof(outbox, actualMessage); const proofA = await dispatchMessageAndReturnProof(outbox, actualMessage);
const proofB = await dispatchMessageAndReturnProof( const proofB = await dispatchMessageAndReturnProof(
@ -185,8 +160,8 @@ describe('OutboxValidatorManager', () => {
helperMessage(proofIndex), helperMessage(proofIndex),
); );
for (index = proofIndex + 1; index < messageCount; index++) { for (index = proofIndex + 1; index < messageCount; index++) {
await dispatchMessage(outbox, actualMessage); await dispatchMessageAndReturnProof(outbox, actualMessage);
await dispatchMessage(helperOutbox, helperMessage(index)); await dispatchMessageAndReturnProof(helperOutbox, helperMessage(index));
} }
return { proofA: proofA, proofB: proofB }; return { proofA: proofA, proofB: proofB };

@ -28,15 +28,11 @@ const chainSummary = async <Chain extends ChainName>(
const remoteContracts = core.getContracts(remote); const remoteContracts = core.getContracts(remote);
const inbox = const inbox =
remoteContracts.inboxes[chain as Exclude<Chain, Chain>].inbox.contract; remoteContracts.inboxes[chain as Exclude<Chain, Chain>].inbox.contract;
const [inboxCheckpointRoot, inboxCheckpointIndex] =
await inbox.latestCachedCheckpoint();
const processFilter = inbox.filters.Process(); const processFilter = inbox.filters.Process();
const processes = await inbox.queryFilter(processFilter); const processes = await inbox.queryFilter(processFilter);
return { return {
chain: remote, chain: remote,
processed: processes.length, processed: processes.length,
root: inboxCheckpointRoot,
index: inboxCheckpointIndex.toNumber(),
}; };
}; };

@ -1,23 +1,13 @@
import type { import type { ProcessEvent } from '@abacus-network/core/types/contracts/Inbox';
CheckpointCachedEvent,
ProcessEvent,
} from '@abacus-network/core/types/contracts/Inbox';
import type { DispatchEvent } from '@abacus-network/core/types/contracts/Outbox'; import type { DispatchEvent } from '@abacus-network/core/types/contracts/Outbox';
import { Annotated } from '../events'; import { Annotated } from '../events';
export { DispatchEvent, CheckpointCachedEvent, ProcessEvent }; export { DispatchEvent, ProcessEvent };
export type AbacusLifecyleEvent = export type AbacusLifecyleEvent = ProcessEvent | DispatchEvent;
| ProcessEvent
| CheckpointCachedEvent
| DispatchEvent;
export type AnnotatedDispatch = Annotated<DispatchEvent>; export type AnnotatedDispatch = Annotated<DispatchEvent>;
export type AnnotatedCheckpoint = Annotated<CheckpointCachedEvent>;
export type AnnotatedProcess = Annotated<ProcessEvent>; export type AnnotatedProcess = Annotated<ProcessEvent>;
export type AnnotatedLifecycleEvent = export type AnnotatedLifecycleEvent = AnnotatedDispatch | AnnotatedProcess;
| AnnotatedDispatch
| AnnotatedCheckpoint
| AnnotatedProcess;

@ -17,11 +17,9 @@ import {
import { delay } from '../utils'; import { delay } from '../utils';
import { import {
AnnotatedCheckpoint,
AnnotatedDispatch, AnnotatedDispatch,
AnnotatedLifecycleEvent, AnnotatedLifecycleEvent,
AnnotatedProcess, AnnotatedProcess,
CheckpointCachedEvent,
DispatchEvent, DispatchEvent,
ProcessEvent, ProcessEvent,
} from './events'; } from './events';
@ -73,7 +71,6 @@ export enum InboxMessageStatus {
} }
export type EventCache = { export type EventCache = {
inboxCheckpoint?: AnnotatedCheckpoint;
process?: AnnotatedProcess; 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<AnnotatedCheckpoint | undefined> {
// 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<CheckpointCachedEvent>(
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) * Get the Inbox `Process` event associated with this message (if any)
* *
* @returns An {@link AnnotatedProcess} (if any) * @returns An {@link AnnotatedProcess} (if any)
*/ */
async getProcess(startBlock?: number): Promise<AnnotatedProcess | undefined> { async getProcess(): Promise<AnnotatedProcess | undefined> {
// if we have already gotten the event, // if we have already gotten the event,
// return it without re-querying // return it without re-querying
if (this.cache.process) { if (this.cache.process) {
@ -333,7 +287,6 @@ export class AbacusMessage {
this.destinationName, this.destinationName,
this.inbox, this.inbox,
processFilter, processFilter,
startBlock,
); );
if (processLogs.length === 1) { if (processLogs.length === 1) {
// if event is returned, store it to the object // if event is returned, store it to the object
@ -352,17 +305,8 @@ export class AbacusMessage {
*/ */
async events(): Promise<AbacusStatus> { async events(): Promise<AbacusStatus> {
const events: AnnotatedLifecycleEvent[] = [this.dispatch]; 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 // attempt to get Inbox process
const process = await this.getProcess(inboxCheckpoint.blockNumber); const process = await this.getProcess();
if (!process) { if (!process) {
// NOTE: when this is the status, you may way to // NOTE: when this is the status, you may way to
// query confirmAt() to check if challenge period // query confirmAt() to check if challenge period

Loading…
Cancel
Save