Dedupe hardhat and core deploy (#449)

* Dedupe TestCoreDeploy and CoreDeploy
* Simplify TestCoreApp
* Remove checkpointing logic from test core
pull/503/head
Yorke Rhodes 3 years ago committed by GitHub
parent 4cd549b7fd
commit b0475af057
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      solidity/app/hardhat.config.ts
  2. 36
      typescript/hardhat/index.ts
  3. 1
      typescript/hardhat/package.json
  4. 213
      typescript/hardhat/src/TestAbacusDeploy.ts
  5. 90
      typescript/hardhat/src/TestCoreApp.ts
  6. 61
      typescript/hardhat/src/TestCoreDeploy.ts
  7. 19
      typescript/hardhat/src/TestDeploy.ts
  8. 30
      typescript/hardhat/src/TestRouterDeploy.ts
  9. 89
      typescript/hardhat/test/testAbacusDeploy.test.ts
  10. 2
      typescript/sdk/src/app.ts
  11. 1
      typescript/sdk/src/index.ts
  12. 1
      yarn.lock

@ -1,4 +1,3 @@
import '@abacus-network/hardhat';
import '@nomiclabs/hardhat-waffle'; import '@nomiclabs/hardhat-waffle';
import '@typechain/hardhat'; import '@typechain/hardhat';
import 'hardhat-gas-reporter'; import 'hardhat-gas-reporter';

@ -1,20 +1,40 @@
import { MultiProvider, TestChainNames } from '@abacus-network/sdk';
import '@nomiclabs/hardhat-waffle'; import '@nomiclabs/hardhat-waffle';
import { ethers } from 'ethers';
import { extendEnvironment } from 'hardhat/config'; import { extendEnvironment } from 'hardhat/config';
import { lazyObject } from "hardhat/plugins"; import { lazyObject } from "hardhat/plugins";
import { TestAbacusDeploy } from './src/TestAbacusDeploy' import 'hardhat/types/runtime';
export { TestAbacusDeploy } from './src/TestAbacusDeploy' import { TestCoreDeploy } from './src/TestCoreDeploy';
export { TestRouterDeploy } from './src/TestRouterDeploy'
import "hardhat/types/runtime";
declare module 'hardhat/types/runtime' { declare module 'hardhat/types/runtime' {
interface HardhatRuntimeEnvironment { interface HardhatRuntimeEnvironment {
abacus: TestAbacusDeploy; abacus: TestCoreDeploy;
} }
} }
export function hardhatMultiProvider(
provider: ethers.providers.Provider,
signer?: ethers.Signer,
): MultiProvider<TestChainNames> {
return new MultiProvider<TestChainNames>({
test1: {
provider,
signer,
},
test2: {
provider,
signer,
},
test3: {
provider,
signer,
},
});
}
// HardhatRuntimeEnvironment // HardhatRuntimeEnvironment
extendEnvironment((hre) => { extendEnvironment((hre) => {
hre.abacus = lazyObject(() => new TestAbacusDeploy({ signer: {} })); hre.abacus = lazyObject(
() => new TestCoreDeploy(hardhatMultiProvider(hre.ethers.provider)),
);
}); });

@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@abacus-network/core": "^0.1.1", "@abacus-network/core": "^0.1.1",
"@abacus-network/sdk": "^0.1.1",
"@abacus-network/utils": "^0.1.1", "@abacus-network/utils": "^0.1.1",
"@nomiclabs/hardhat-ethers": "^2.0.5", "@nomiclabs/hardhat-ethers": "^2.0.5",
"@nomiclabs/hardhat-waffle": "^2.0.2", "@nomiclabs/hardhat-waffle": "^2.0.2",

@ -1,213 +0,0 @@
import { TestDeploy } from './TestDeploy';
import {
AbacusConnectionManager,
AbacusConnectionManager__factory,
InterchainGasPaymaster,
InterchainGasPaymaster__factory,
Outbox,
Outbox__factory,
TestInbox,
TestInbox__factory,
UpgradeBeaconController,
UpgradeBeaconController__factory,
} from '@abacus-network/core';
import { types } from '@abacus-network/utils';
import { addressToBytes32 } from '@abacus-network/utils/dist/src/utils';
import { ethers } from 'ethers';
export type TestAbacusConfig = {
signer: Record<types.Domain, ethers.Signer>;
};
// Outbox & inbox validator managers are not required for testing and are therefore omitted.
export type TestAbacusInstance = {
outbox: Outbox;
abacusConnectionManager: AbacusConnectionManager;
upgradeBeaconController: UpgradeBeaconController;
inboxes: Record<types.Domain, TestInbox>;
interchainGasPaymaster: InterchainGasPaymaster;
};
export class TestAbacusDeploy extends TestDeploy<
TestAbacusInstance,
TestAbacusConfig
> {
async deploy(domains: types.Domain[], signer: ethers.Signer) {
// Clear previous deploy to support multiple tests.
for (const domain of this.domains) {
delete this.config.signer[domain];
delete this.instances[domain];
}
for (const domain of domains) {
this.config.signer[domain] = signer;
}
for (const domain of domains) {
this.instances[domain] = await this.deployInstance(domain);
}
}
async deployInstance(domain: types.Domain): Promise<TestAbacusInstance> {
const signer = this.config.signer[domain];
const upgradeBeaconControllerFactory = new UpgradeBeaconController__factory(
signer,
);
const upgradeBeaconController =
await upgradeBeaconControllerFactory.deploy();
const outboxFactory = new Outbox__factory(signer);
const outbox = await outboxFactory.deploy(domain);
// Outbox will require the validator manager to be a contract. We don't
// actually make use of the validator manager, so just we pass in the
// upgradeBeaconController as the validator manager to satisfy the contract
// requirement and avoid deploying a new validator manager.
await outbox.initialize(upgradeBeaconController.address);
const abacusConnectionManagerFactory = new AbacusConnectionManager__factory(
signer,
);
const abacusConnectionManager =
await abacusConnectionManagerFactory.deploy();
await abacusConnectionManager.setOutbox(outbox.address);
const interchainGasPaymasterFactory = new InterchainGasPaymaster__factory(
signer,
);
const interchainGasPaymaster = await interchainGasPaymasterFactory.deploy();
await abacusConnectionManager.setInterchainGasPaymaster(
interchainGasPaymaster.address,
);
const inboxFactory = new TestInbox__factory(signer);
const inboxes: Record<types.Domain, TestInbox> = {};
// this.remotes reads this.instances which has not yet been set.
const remotes = Object.keys(this.config.signer).map((d) => parseInt(d));
const deploys = remotes.map(async (remote) => {
const inbox = await inboxFactory.deploy(domain);
// Inbox will require the validator manager to be a contract. We don't
// actually make use of the validator manager, so we just pass in the
// upgradeBeaconController as the validator manager to satisfy the contract
// requirement and avoid deploying a new validator manager.
await inbox.initialize(
remote,
upgradeBeaconController.address,
ethers.constants.HashZero,
0,
);
await abacusConnectionManager.enrollInbox(remote, inbox.address);
inboxes[remote] = inbox;
});
await Promise.all(deploys);
// dispatch a dummy event to allow a consumer to checkpoint/process a single message
await outbox.dispatch(
remotes.find((_) => _ !== domain)!,
addressToBytes32(ethers.constants.AddressZero),
'0x',
);
return {
outbox,
abacusConnectionManager,
interchainGasPaymaster,
inboxes,
upgradeBeaconController,
};
}
async transferOwnership(domain: types.Domain, address: types.Address) {
await this.outbox(domain).transferOwnership(address);
await this.upgradeBeaconController(domain).transferOwnership(address);
await this.abacusConnectionManager(domain).transferOwnership(address);
for (const remote of this.remotes(domain)) {
await this.inbox(domain, remote).transferOwnership(address);
}
}
outbox(domain: types.Domain): Outbox {
return this.instances[domain].outbox;
}
upgradeBeaconController(domain: types.Domain): UpgradeBeaconController {
return this.instances[domain].upgradeBeaconController;
}
inbox(origin: types.Domain, destination: types.Domain): TestInbox {
return this.instances[destination].inboxes[origin];
}
interchainGasPaymaster(domain: types.Domain): InterchainGasPaymaster {
return this.instances[domain].interchainGasPaymaster;
}
abacusConnectionManager(domain: types.Domain): AbacusConnectionManager {
return this.instances[domain].abacusConnectionManager;
}
async processMessages(): Promise<
Map<types.Domain, Map<types.Domain, ethers.providers.TransactionResponse[]>>
> {
const responses: Map<
types.Domain,
Map<types.Domain, ethers.providers.TransactionResponse[]>
> = new Map();
for (const origin of this.domains) {
const outbound = await this.processOutboundMessages(origin);
responses.set(origin, new Map());
this.domains.forEach((destination) => {
responses
.get(origin)!
.set(destination, outbound.get(destination) ?? []);
});
}
return responses;
}
async processOutboundMessages(
origin: types.Domain,
): Promise<Map<types.Domain, ethers.providers.TransactionResponse[]>> {
const responses: Map<types.Domain, ethers.providers.TransactionResponse[]> =
new Map();
const outbox = this.outbox(origin);
const [root, index] = await outbox.latestCheckpoint();
// Find all unprocessed messages dispatched on the outbox since the previous checkpoint.
const dispatchFilter = outbox.filters.Dispatch();
const dispatches = await outbox.queryFilter(dispatchFilter);
for (const dispatch of dispatches) {
if (dispatch.args.leafIndex > index) {
// Message has not been checkpointed on the outbox
break;
}
const destination = dispatch.args.destination;
if (destination === origin)
throw new Error('Dispatched message to local domain');
const inbox = this.inbox(origin, destination);
const status = await inbox.messages(dispatch.args.messageHash);
if (status !== types.MessageStatus.PROCESSED) {
if (dispatch.args.leafIndex.toNumber() == 0) {
// disregard the dummy message
continue;
}
const [, inboxCheckpointIndex] = await inbox.latestCheckpoint();
if (
inboxCheckpointIndex < dispatch.args.leafIndex &&
inboxCheckpointIndex < index
) {
await inbox.setCheckpoint(root, index);
}
const response = await inbox.testProcess(
dispatch.args.message,
dispatch.args.leafIndex.toNumber(),
);
let destinationResponses = responses.get(destination) || [];
destinationResponses.push(response);
responses.set(destination, destinationResponses);
}
}
return responses;
}
}

@ -0,0 +1,90 @@
import {
TestInbox,
TestInbox__factory,
TestOutbox__factory,
} from '@abacus-network/core';
import {
AbacusCore,
chainMetadata,
DomainIdToChainName,
objMap,
TestChainNames,
} from '@abacus-network/sdk';
import { types } from '@abacus-network/utils';
import { ethers } from 'ethers';
export class TestCoreApp extends AbacusCore<TestChainNames> {
getContracts<Local extends TestChainNames>(chain: Local) {
const contracts = super.getContracts(chain);
return {
...contracts,
outbox: {
...contracts.outbox,
outbox: TestOutbox__factory.connect(
contracts.outbox.outbox.address,
contracts.outbox.outbox.signer,
),
},
inboxes: objMap(contracts.inboxes, (_, inbox) => ({
...inbox,
inbox: TestInbox__factory.connect(
inbox.inbox.address,
inbox.inbox.signer,
),
})),
};
}
async processMessages(): Promise<
Map<
TestChainNames,
Map<TestChainNames, ethers.providers.TransactionResponse[]>
>
> {
const responses = new Map();
for (const origin of this.chains()) {
const outbound = await this.processOutboundMessages(origin);
const originResponses = new Map();
this.remoteChains(origin).forEach((destination) =>
originResponses.set(destination, outbound.get(destination)),
);
responses.set(origin, originResponses);
}
return responses;
}
async processOutboundMessages<Local extends TestChainNames>(origin: Local) {
const responses = new Map();
const contracts = this.getContracts(origin);
const outbox = contracts.outbox.outbox;
const dispatchFilter = outbox.filters.Dispatch();
const dispatches = await outbox.queryFilter(dispatchFilter);
for (const dispatch of dispatches) {
const destination = dispatch.args.destination;
if (destination === chainMetadata[origin].id) {
throw new Error('Dispatched message to local domain');
}
const destinationChain = DomainIdToChainName[destination];
const inbox: TestInbox =
// @ts-ignore
this.getContracts(destinationChain).inboxes[origin].inbox;
const status = await inbox.messages(dispatch.args.messageHash);
if (status !== types.MessageStatus.PROCESSED) {
if (dispatch.args.leafIndex.toNumber() == 0) {
// disregard the dummy message
continue;
}
const response = await inbox.testProcess(
dispatch.args.message,
dispatch.args.leafIndex.toNumber(),
);
let destinationResponses = responses.get(destinationChain) || [];
destinationResponses.push(response);
responses.set(destinationChain, destinationResponses);
}
}
return responses;
}
}

@ -0,0 +1,61 @@
import { TestCoreApp } from './TestCoreApp';
import { TestInbox__factory, TestOutbox__factory } from '@abacus-network/core';
import { AbacusCoreDeployer, CoreConfig } from '@abacus-network/deploy';
import {
chainMetadata,
CoreContractAddresses,
MultiProvider,
TestChainNames,
} from '@abacus-network/sdk';
import { utils } from '@abacus-network/utils';
import { ethers } from 'ethers';
// dummy config as TestInbox and TestOutbox do not use deployed ValidatorManager
const testValidatorManagerConfig: CoreConfig = {
validatorManager: {
validators: [ethers.constants.AddressZero],
threshold: 1,
},
};
export class TestCoreDeploy extends AbacusCoreDeployer<TestChainNames> {
constructor(public readonly multiProvider: MultiProvider<TestChainNames>) {
super(multiProvider, {
test1: testValidatorManagerConfig,
test2: testValidatorManagerConfig,
test3: testValidatorManagerConfig,
});
}
inboxFactoryBuilder = (signer: ethers.Signer) =>
new TestInbox__factory(signer);
outboxFactoryBuilder = (signer: ethers.Signer) =>
new TestOutbox__factory(signer);
async deployContracts<LocalChain extends TestChainNames>(
local: LocalChain,
config: CoreConfig,
): Promise<CoreContractAddresses<TestChainNames, LocalChain>> {
const addresses = await super.deployContracts(local, config);
const signer = this.multiProvider.getChainConnection(local).signer!;
const outbox = this.outboxFactoryBuilder(signer).attach(
addresses.outbox.proxy,
);
const remote = this.multiProvider.remoteChains(local)[0];
// dispatch a dummy event to allow a consumer to checkpoint/process a single message
await outbox.dispatch(
chainMetadata[remote].id,
utils.addressToBytes32(ethers.constants.AddressZero),
'0x',
);
return addresses;
}
async deployCore() {
const result = await super.deploy();
return new TestCoreApp(result, this.multiProvider);
}
}

@ -1,19 +0,0 @@
import { types } from '@abacus-network/utils';
export class TestDeploy<T, V> {
public readonly config: V;
public readonly instances: Record<types.Domain, T>;
constructor(config: V) {
this.config = config;
this.instances = {};
}
get domains(): types.Domain[] {
return Object.keys(this.instances).map((d) => parseInt(d));
}
remotes(domain: types.Domain): types.Domain[] {
return this.domains.filter((d) => d !== domain);
}
}

@ -1,30 +0,0 @@
import { TestAbacusDeploy } from './TestAbacusDeploy';
import { TestDeploy } from './TestDeploy';
import { types, utils } from '@abacus-network/utils';
export interface Router {
address: types.Address;
enrollRemoteRouter(domain: types.Domain, router: types.Address): Promise<any>;
}
export abstract class TestRouterDeploy<T, V> extends TestDeploy<T, V> {
async deploy(abacus: TestAbacusDeploy) {
for (const domain of abacus.domains) {
this.instances[domain] = await this.deployInstance(domain, abacus);
}
for (const local of this.domains) {
for (const remote of this.remotes(local)) {
await this.router(local).enrollRemoteRouter(
remote,
utils.addressToBytes32(this.router(remote).address),
);
}
}
}
abstract deployInstance(
domain: types.Domain,
abacus: TestAbacusDeploy,
): Promise<T>;
abstract router(domain: types.Domain): Router;
}

@ -1,24 +1,29 @@
import { TestAbacusDeploy } from '..'; import { hardhatMultiProvider } from '../index';
import { TestRecipient__factory } from '@abacus-network/core'; import { TestCoreApp } from '../src/TestCoreApp';
import { TestCoreDeploy } from '../src/TestCoreDeploy';
import { TestOutbox, TestRecipient__factory } from '@abacus-network/core';
import { chainMetadata } from '@abacus-network/sdk';
import { utils } from '@abacus-network/utils'; import { utils } from '@abacus-network/utils';
import { expect } from 'chai'; import { expect } from 'chai';
import { ethers } from 'hardhat'; import { ethers } from 'hardhat';
const localDomain = 1000; const localChain = 'test1';
const remoteDomain = 2000; const localDomain = chainMetadata[localChain].id;
const domains = [localDomain, remoteDomain]; const remoteChain = 'test2';
const remoteDomain = chainMetadata[remoteChain].id;
const message = '0xdeadbeef'; const message = '0xdeadbeef';
describe('TestAbacusDeploy', async () => { describe('TestCoreDeploy', async () => {
let abacus: TestAbacusDeploy; let abacus: TestCoreApp, localOutbox: TestOutbox, remoteOutbox: TestOutbox;
beforeEach(async () => { beforeEach(async () => {
abacus = new TestAbacusDeploy({ signer: {} });
const [signer] = await ethers.getSigners(); const [signer] = await ethers.getSigners();
await abacus.deploy(domains, signer); const multiProvider = hardhatMultiProvider(ethers.provider, signer);
const deployer = new TestCoreDeploy(multiProvider);
abacus = await deployer.deployCore();
const recipient = await new TestRecipient__factory(signer).deploy(); const recipient = await new TestRecipient__factory(signer).deploy();
const localOutbox = abacus.outbox(localDomain); localOutbox = abacus.getContracts(localChain).outbox.outbox;
await expect( await expect(
localOutbox.dispatch( localOutbox.dispatch(
remoteDomain, remoteDomain,
@ -26,7 +31,7 @@ describe('TestAbacusDeploy', async () => {
message, message,
), ),
).to.emit(localOutbox, 'Dispatch'); ).to.emit(localOutbox, 'Dispatch');
const remoteOutbox = abacus.outbox(remoteDomain); remoteOutbox = abacus.getContracts(remoteChain).outbox.outbox;
await expect( await expect(
remoteOutbox.dispatch( remoteOutbox.dispatch(
localDomain, localDomain,
@ -36,57 +41,21 @@ describe('TestAbacusDeploy', async () => {
).to.emit(remoteOutbox, 'Dispatch'); ).to.emit(remoteOutbox, 'Dispatch');
}); });
describe('without a created checkpoint', () => { it('processes outbound messages for a single domain', async () => {
it('does not process outbound messages', async () => { const responses = await abacus.processOutboundMessages(localChain);
const responses = await abacus.processOutboundMessages(localDomain); expect(responses.get(remoteChain)!.length).to.equal(1);
expect(responses.get(remoteDomain)).to.be.undefined;
});
}); });
describe('with a checkpoint', () => { it('processes outbound messages for two domains', async () => {
beforeEach(async () => { const localResponses = await abacus.processOutboundMessages(localChain);
const localOutbox = abacus.outbox(localDomain); expect(localResponses.get(remoteChain)!.length).to.equal(1);
const remoteOutbox = abacus.outbox(remoteDomain); const remoteResponses = await abacus.processOutboundMessages(remoteChain);
await localOutbox.checkpoint(); expect(remoteResponses.get(localChain)!.length).to.equal(1);
await remoteOutbox.checkpoint(); });
});
it('processes outbound messages for a single domain', async () => {
const responses = await abacus.processOutboundMessages(localDomain);
expect(responses.get(remoteDomain)!.length).to.equal(1);
const [_, index] = await abacus.outbox(localDomain).latestCheckpoint();
expect(index).to.equal(1);
});
it('processes outbound messages for two domains', async () => {
const localResponses = await abacus.processOutboundMessages(localDomain);
expect(localResponses.get(remoteDomain)!.length).to.equal(1);
const [, localIndex] = await abacus
.outbox(localDomain)
.latestCheckpoint();
expect(localIndex).to.equal(1);
const remoteResponses = await abacus.processOutboundMessages(
remoteDomain,
);
expect(remoteResponses.get(localDomain)!.length).to.equal(1);
const [, remoteIndex] = await abacus
.outbox(remoteDomain)
.latestCheckpoint();
expect(remoteIndex).to.equal(1);
});
it('processes all messages', async () => { it('processes all messages', async () => {
const responses = await abacus.processMessages(); const responses = await abacus.processMessages();
expect(responses.get(localDomain)!.get(remoteDomain)!.length).to.equal(1); expect(responses.get(localChain)!.get(remoteChain)!.length).to.equal(1);
expect(responses.get(remoteDomain)!.get(localDomain)!.length).to.equal(1); expect(responses.get(remoteChain)!.get(localChain)!.length).to.equal(1);
const [, localIndex] = await abacus
.outbox(localDomain)
.latestCheckpoint();
expect(localIndex).to.equal(1);
const [, remoteIndex] = await abacus
.outbox(remoteDomain)
.latestCheckpoint();
expect(remoteIndex).to.equal(1);
});
}); });
}); });

@ -10,7 +10,7 @@ export class AbacusApp<
constructor( constructor(
builder: ContractsBuilder<any, Contracts>, builder: ContractsBuilder<any, Contracts>,
contractAddresses: ChainMap<Chain, any>, contractAddresses: ChainMap<Chain, any>,
multiProvider: MultiProvider<Chain>, readonly multiProvider: MultiProvider<Chain>,
) { ) {
super( super(
objMap( objMap(

@ -52,5 +52,6 @@ export {
ProxiedAddress, ProxiedAddress,
RemoteChainMap, RemoteChainMap,
Remotes, Remotes,
TestChainNames,
} from './types'; } from './types';
export { objMap, objMapEntries, promiseObjAll, utils } from './utils'; export { objMap, objMapEntries, promiseObjAll, utils } from './utils';

@ -90,6 +90,7 @@ __metadata:
resolution: "@abacus-network/hardhat@workspace:typescript/hardhat" resolution: "@abacus-network/hardhat@workspace:typescript/hardhat"
dependencies: dependencies:
"@abacus-network/core": ^0.1.1 "@abacus-network/core": ^0.1.1
"@abacus-network/sdk": ^0.1.1
"@abacus-network/utils": ^0.1.1 "@abacus-network/utils": ^0.1.1
"@nomiclabs/hardhat-ethers": ^2.0.5 "@nomiclabs/hardhat-ethers": ^2.0.5
"@nomiclabs/hardhat-waffle": ^2.0.2 "@nomiclabs/hardhat-waffle": ^2.0.2

Loading…
Cancel
Save