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. 296
      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
* 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 ============

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

@ -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
*/

@ -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,

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

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

@ -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
}

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

@ -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;
}

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

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

@ -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 { 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,
const badProof = await dispatchMessageAndReturnProof(
helperOutbox,
localDomain,
'0x1234567890123456789012345678901234567890', // non-existent contract address
body,
utils.addressToBytes32('0x1234567890123456789012345678901234567890'), // non-existent contract address
'hello world',
);
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,
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);
await expect(
validatorManager.process(
inbox.address,
badProof.root,
badProof.index,
badProof.message,
badProof.proof,
badProof.index,
),
).to.be.reverted;
});
});

@ -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 () => {
const { message, destDomain, abacusMessage, hash, leafIndex } =
const { message, destDomain, abacusMessage, leafIndex, hash } =
await testMessageValues();
// Send message with signer address as msg.sender

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

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

@ -28,15 +28,11 @@ const chainSummary = async <Chain extends ChainName>(
const remoteContracts = core.getContracts(remote);
const inbox =
remoteContracts.inboxes[chain as Exclude<Chain, Chain>].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(),
};
};

@ -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<DispatchEvent>;
export type AnnotatedCheckpoint = Annotated<CheckpointCachedEvent>;
export type AnnotatedProcess = Annotated<ProcessEvent>;
export type AnnotatedLifecycleEvent =
| AnnotatedDispatch
| AnnotatedCheckpoint
| AnnotatedProcess;
export type AnnotatedLifecycleEvent = AnnotatedDispatch | AnnotatedProcess;

@ -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<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)
*
* @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,
// 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<AbacusStatus> {
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

Loading…
Cancel
Save