You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
309 lines
9.9 KiB
309 lines
9.9 KiB
const { waffle, ethers } = require('hardhat');
|
|
const { provider, deployMockContract } = waffle;
|
|
const { expect } = require('chai');
|
|
|
|
const testUtils = require('./utils');
|
|
const { testCases } = require('../../vectors/merkleTestCases.json');
|
|
const MockRecipient = require('../artifacts/contracts/test/MockRecipient.sol/MockRecipient.json');
|
|
|
|
const originSLIP44 = 1000;
|
|
const ownSLIP44 = 2000;
|
|
const optimisticSeconds = 3;
|
|
const initialCurrentRoot = ethers.utils.formatBytes32String('current');
|
|
const initialLastProcessed = 0;
|
|
const mockRecipientMessageString = 'message received';
|
|
|
|
describe('Replica', async () => {
|
|
let replica, signer, fakeSigner, updater, fakeUpdater, processor;
|
|
|
|
const enqueueValidUpdate = async (newRoot) => {
|
|
let oldRoot;
|
|
if ((await replica.queueLength()) == 0) {
|
|
oldRoot = await replica.current();
|
|
} else {
|
|
const lastEnqueued = await replica.queueEnd();
|
|
oldRoot = lastEnqueued;
|
|
}
|
|
|
|
const { signature } = await updater.signUpdate(oldRoot, newRoot);
|
|
await replica.update(oldRoot, newRoot, signature);
|
|
};
|
|
|
|
before(async () => {
|
|
[signer, fakeSigner, processor] = provider.getWallets();
|
|
updater = await optics.Updater.fromSigner(signer, originSLIP44);
|
|
fakeUpdater = await optics.Updater.fromSigner(fakeSigner, originSLIP44);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
const Replica = await ethers.getContractFactory('TestReplica');
|
|
replica = await Replica.deploy(
|
|
originSLIP44,
|
|
ownSLIP44,
|
|
updater.signer.address,
|
|
optimisticSeconds,
|
|
initialCurrentRoot,
|
|
initialLastProcessed,
|
|
);
|
|
await replica.deployed();
|
|
});
|
|
|
|
it('Halts on fail', async () => {
|
|
await replica.setFailed();
|
|
expect(await replica.state()).to.equal(optics.State.FAILED);
|
|
|
|
const newRoot = ethers.utils.formatBytes32String('new root');
|
|
await expect(enqueueValidUpdate(newRoot)).to.be.revertedWith(
|
|
'failed state',
|
|
);
|
|
});
|
|
|
|
it('Enqueues pending updates', async () => {
|
|
const firstNewRoot = ethers.utils.formatBytes32String('first new root');
|
|
await enqueueValidUpdate(firstNewRoot);
|
|
expect(await replica.queueEnd()).to.equal(firstNewRoot);
|
|
|
|
const secondNewRoot = ethers.utils.formatBytes32String('second next root');
|
|
await enqueueValidUpdate(secondNewRoot);
|
|
expect(await replica.queueEnd()).to.equal(secondNewRoot);
|
|
});
|
|
|
|
it('Returns the earliest pending update', async () => {
|
|
const firstNewRoot = ethers.utils.formatBytes32String('first new root');
|
|
await enqueueValidUpdate(firstNewRoot);
|
|
|
|
const beforeTimestamp = await replica.timestamp();
|
|
const secondNewRoot = ethers.utils.formatBytes32String('second next root');
|
|
await enqueueValidUpdate(secondNewRoot);
|
|
|
|
const [pending, confirmAt] = await replica.nextPending();
|
|
expect(pending).to.equal(firstNewRoot);
|
|
expect(confirmAt).to.equal(beforeTimestamp.add(optimisticSeconds));
|
|
});
|
|
|
|
it('Rejects update with invalid signature', async () => {
|
|
const firstNewRoot = ethers.utils.formatBytes32String('first new root');
|
|
await enqueueValidUpdate(firstNewRoot);
|
|
|
|
const secondNewRoot = ethers.utils.formatBytes32String('second new root');
|
|
const { signature: fakeSignature } = await fakeUpdater.signUpdate(
|
|
firstNewRoot,
|
|
secondNewRoot,
|
|
);
|
|
|
|
await expect(
|
|
replica.update(firstNewRoot, secondNewRoot, fakeSignature),
|
|
).to.be.revertedWith('bad sig');
|
|
});
|
|
|
|
it('Rejects initial update not building off initial root', async () => {
|
|
const fakeInitialRoot = ethers.utils.formatBytes32String('fake root');
|
|
const newRoot = ethers.utils.formatBytes32String('new root');
|
|
const { signature } = await updater.signUpdate(fakeInitialRoot, newRoot);
|
|
|
|
await expect(
|
|
replica.update(fakeInitialRoot, newRoot, signature),
|
|
).to.be.revertedWith('not current update');
|
|
});
|
|
|
|
it('Rejects updates not building off latest enqueued root', async () => {
|
|
const firstNewRoot = ethers.utils.formatBytes32String('first new root');
|
|
await enqueueValidUpdate(firstNewRoot);
|
|
|
|
const fakeLatestRoot = ethers.utils.formatBytes32String('fake root');
|
|
const secondNewRoot = ethers.utils.formatBytes32String('second new root');
|
|
const { signature } = await updater.signUpdate(
|
|
fakeLatestRoot,
|
|
secondNewRoot,
|
|
);
|
|
|
|
await expect(
|
|
replica.update(fakeLatestRoot, secondNewRoot, signature),
|
|
).to.be.revertedWith('not end of queue');
|
|
});
|
|
|
|
it('Accepts a double update proof', async () => {
|
|
const firstRoot = await replica.current();
|
|
const secondRoot = ethers.utils.formatBytes32String('second root');
|
|
const thirdRoot = ethers.utils.formatBytes32String('third root');
|
|
|
|
const { signature } = await updater.signUpdate(firstRoot, secondRoot);
|
|
const { signature: signature2 } = await updater.signUpdate(
|
|
firstRoot,
|
|
thirdRoot,
|
|
);
|
|
|
|
await expect(
|
|
replica.doubleUpdate(
|
|
firstRoot,
|
|
[secondRoot, thirdRoot],
|
|
signature,
|
|
signature2,
|
|
),
|
|
).to.emit(replica, 'DoubleUpdate');
|
|
|
|
expect(await replica.state()).to.equal(optics.State.FAILED);
|
|
});
|
|
|
|
it('Confirms a ready update', async () => {
|
|
const newRoot = ethers.utils.formatBytes32String('new root');
|
|
await enqueueValidUpdate(newRoot);
|
|
|
|
await testUtils.increaseTimestampBy(provider, optimisticSeconds);
|
|
|
|
await replica.confirm();
|
|
expect(await replica.current()).to.equal(newRoot);
|
|
});
|
|
|
|
it('Batch-confirms several ready updates', async () => {
|
|
const firstNewRoot = ethers.utils.formatBytes32String('first new root');
|
|
await enqueueValidUpdate(firstNewRoot);
|
|
|
|
const secondNewRoot = ethers.utils.formatBytes32String('second next root');
|
|
await enqueueValidUpdate(secondNewRoot);
|
|
|
|
// Increase time enough for both updates to be confirmable
|
|
await testUtils.increaseTimestampBy(provider, optimisticSeconds * 2);
|
|
|
|
await replica.confirm();
|
|
expect(await replica.current()).to.equal(secondNewRoot);
|
|
});
|
|
|
|
it('Rejects an early confirmation attempt', async () => {
|
|
const firstNewRoot = ethers.utils.formatBytes32String('first new root');
|
|
await enqueueValidUpdate(firstNewRoot);
|
|
|
|
// Don't increase time enough for update to be confirmable.
|
|
// Note that we use optimisticSeconds - 2 because the call to enqueue
|
|
// the valid root has already increased the timestamp by 1.
|
|
await testUtils.increaseTimestampBy(provider, optimisticSeconds - 2);
|
|
|
|
await expect(replica.confirm()).to.be.revertedWith('not time');
|
|
});
|
|
|
|
it('Proves a valid message', async () => {
|
|
// Use 1st proof of 1st merkle vector test case
|
|
const testCase = testCases[0];
|
|
let { leaf, index, path } = testCase.proofs[0];
|
|
|
|
await replica.setCurrentRoot(testCase.expectedRoot);
|
|
|
|
// Ensure proper static call return value
|
|
expect(await replica.callStatic.prove(leaf, path, index)).to.be.true;
|
|
|
|
await replica.prove(leaf, path, index);
|
|
expect(await replica.messages(leaf)).to.equal(optics.MessageStatus.PENDING);
|
|
});
|
|
|
|
it('Rejects invalid message proof', async () => {
|
|
// Use 1st proof of 1st merkle vector test case
|
|
const testCase = testCases[0];
|
|
let { leaf, index, path } = testCase.proofs[0];
|
|
|
|
// Switch ordering of proof hashes
|
|
const firstHash = path[0];
|
|
path[0] = path[1];
|
|
path[1] = firstHash;
|
|
|
|
await replica.setCurrentRoot(testCase.expectedRoot);
|
|
|
|
expect(await replica.callStatic.prove(leaf, path, index)).to.be.false;
|
|
|
|
await replica.prove(leaf, path, index);
|
|
expect(await replica.messages(leaf)).to.equal(optics.MessageStatus.NONE);
|
|
});
|
|
|
|
it('Processes a proved message', async () => {
|
|
const [sender, recipient] = provider.getWallets();
|
|
const mockRecipient = await deployMockContract(
|
|
recipient,
|
|
MockRecipient.abi,
|
|
);
|
|
|
|
const mockVal = '0x1234abcd';
|
|
|
|
await mockRecipient.mock.handle.returns(mockVal);
|
|
|
|
const sequence = (await replica.lastProcessed()).add(1);
|
|
|
|
const formattedMessage = optics.formatMessage(
|
|
originSLIP44,
|
|
sender.address,
|
|
sequence,
|
|
ownSLIP44,
|
|
mockRecipient.address,
|
|
'0x',
|
|
);
|
|
|
|
// Set message status to Message.Pending
|
|
await replica.setMessagePending(formattedMessage);
|
|
|
|
// Ensure proper static call return value
|
|
let [success, ret] = await replica.callStatic.process(formattedMessage);
|
|
expect(success).to.be.true;
|
|
expect(ret).to.equal(mockVal);
|
|
|
|
await replica.process(formattedMessage);
|
|
expect(await replica.lastProcessed()).to.equal(sequence);
|
|
});
|
|
|
|
it('Fails to process an unproved message', async () => {
|
|
const [sender, recipient] = provider.getWallets();
|
|
const sequence = (await replica.lastProcessed()).add(1);
|
|
const body = ethers.utils.formatBytes32String('message');
|
|
|
|
const formattedMessage = optics.formatMessage(
|
|
originSLIP44,
|
|
sender.address,
|
|
sequence,
|
|
ownSLIP44,
|
|
recipient.address,
|
|
body,
|
|
);
|
|
|
|
await expect(replica.process(formattedMessage)).to.be.revertedWith(
|
|
'not pending',
|
|
);
|
|
});
|
|
|
|
it('Fails to process out-of-order message', async () => {
|
|
const [sender, recipient] = provider.getWallets();
|
|
|
|
// Skip sequence ordering by adding 2 to lastProcessed
|
|
const sequence = (await replica.lastProcessed()).add(2);
|
|
const body = ethers.utils.formatBytes32String('message');
|
|
|
|
const formattedMessage = optics.formatMessage(
|
|
originSLIP44,
|
|
sender.address,
|
|
sequence,
|
|
ownSLIP44,
|
|
recipient.address,
|
|
body,
|
|
);
|
|
|
|
await expect(replica.process(formattedMessage)).to.be.revertedWith(
|
|
'!sequence',
|
|
);
|
|
});
|
|
|
|
it('Fails to process message with wrong destination slip44', async () => {
|
|
const [sender, recipient] = provider.getWallets();
|
|
const sequence = (await replica.lastProcessed()).add(1);
|
|
const body = ethers.utils.formatBytes32String('message');
|
|
|
|
const formattedMessage = optics.formatMessage(
|
|
originSLIP44,
|
|
sender.address,
|
|
sequence,
|
|
// Wrong destination slip44
|
|
ownSLIP44 + 5,
|
|
recipient.address,
|
|
body,
|
|
);
|
|
|
|
await expect(replica.process(formattedMessage)).to.be.revertedWith(
|
|
'!destination',
|
|
);
|
|
});
|
|
});
|
|
|