From 537214cf3a6adac0fc21095e1d89917dacb0ddb1 Mon Sep 17 00:00:00 2001 From: preethamr Date: Wed, 25 Oct 2023 16:27:49 -0700 Subject: [PATCH] fix: resolve checks need in lh propose --- packages/adapters/database/src/client.ts | 20 +++++- packages/adapters/database/src/index.ts | 4 ++ packages/adapters/database/test/mock.ts | 1 + .../txservice/src/shared/contracts.ts | 18 ++++++ packages/agents/lighthouse/src/config.ts | 11 ++++ .../lighthouse/src/tasks/propose/errors.ts | 33 +++++++++- .../src/tasks/propose/operations/propose.ts | 61 +++++++++++++++++-- .../tasks/propose/operations/proposeSpoke.ts | 19 +++--- 8 files changed, 150 insertions(+), 17 deletions(-) diff --git a/packages/adapters/database/src/client.ts b/packages/adapters/database/src/client.ts index 02adc4d3f1..7f27e3558c 100644 --- a/packages/adapters/database/src/client.ts +++ b/packages/adapters/database/src/client.ts @@ -1070,6 +1070,17 @@ export const getCurrentProposedSnapshot = async ( return snapshot ? convertFromDbSnapshot(snapshot) : undefined; }; +export const getCurrentPropagatedSnapshot = async ( + _pool?: Pool | db.TxnClientForRepeatableRead, +): Promise => { + const poolToUse = _pool ?? pool; + + const snapshot = await db + .selectOne("snapshots", { status: "Propagated" }, { limit: 1, order: { by: "id", direction: "DESC" } }) + .run(poolToUse); + return snapshot ? convertFromDbSnapshot(snapshot) : undefined; +}; + export const getCurrentProposedOptimisticRoot = async ( domain: string, _pool?: Pool | db.TxnClientForRepeatableRead, @@ -1106,10 +1117,17 @@ export const saveFinalizedRoots = async ( await Promise.all( roots.map(async (root) => { - await db.update("snapshots", { status: "Finalized" }, { aggregate_root: root.aggregateRoot }).run(poolToUse); + await db + .update( + "snapshots", + { status: "Finalized" }, + { aggregate_root: root.aggregateRoot, propagate_timestamp: root.timestamp }, + ) + .run(poolToUse); }), ); }; + export const saveProposedSpokeRoots = async ( _roots: SpokeOptimisticRoot[], _pool?: Pool | db.TxnClientForRepeatableRead, diff --git a/packages/adapters/database/src/index.ts b/packages/adapters/database/src/index.ts index c9b2ab37c7..26c80e97b8 100644 --- a/packages/adapters/database/src/index.ts +++ b/packages/adapters/database/src/index.ts @@ -66,6 +66,7 @@ import { getLatestAggregateRoots, getPendingAggregateRoot, getCurrentProposedSnapshot, + getCurrentPropagatedSnapshot, getLatestPendingSnapshotRootByDomain, getMessageRootAggregatedFromIndex, getMessageRootsFromIndex, @@ -216,6 +217,7 @@ export type Database = { getAggregateRoots: (count: number, _pool?: Pool | TxnClientForRepeatableRead) => Promise; getBaseAggregateRoot: (_pool?: Pool | TxnClientForRepeatableRead) => Promise; getCurrentProposedSnapshot: (_pool?: Pool | TxnClientForRepeatableRead) => Promise; + getCurrentPropagatedSnapshot: (_pool?: Pool | TxnClientForRepeatableRead) => Promise; getMessageRootIndex: ( domain: string, messageRoot: string, @@ -409,6 +411,7 @@ export const getDatabase = async (databaseUrl: string, logger: Logger): Promise< getLatestAggregateRoots, getPendingAggregateRoot, getCurrentProposedSnapshot, + getCurrentPropagatedSnapshot, getLatestPendingSnapshotRootByDomain, getMessageRootAggregatedFromIndex, getMessageRootsFromIndex, @@ -501,6 +504,7 @@ export const getDatabaseAndPool = async ( getLatestAggregateRoots, getPendingAggregateRoot, getCurrentProposedSnapshot, + getCurrentPropagatedSnapshot, getLatestPendingSnapshotRootByDomain, getMessageRootAggregatedFromIndex, getMessageRootsFromIndex, diff --git a/packages/adapters/database/test/mock.ts b/packages/adapters/database/test/mock.ts index 55f126e953..a14f698d63 100644 --- a/packages/adapters/database/test/mock.ts +++ b/packages/adapters/database/test/mock.ts @@ -80,6 +80,7 @@ export const mockDatabase = (): Database => { savePropagatedOptimisticRoots: stub().resolves(), saveSnapshotRoots: stub().resolves(), getCurrentProposedSnapshot: stub().resolves(), + getCurrentPropagatedSnapshot: stub().resolves(), getBaseAggregateRoot: stub().resolves(), getAggregateRoots: stub().resolves(), getPendingAggregateRoot: stub().resolves(), diff --git a/packages/adapters/txservice/src/shared/contracts.ts b/packages/adapters/txservice/src/shared/contracts.ts index 7298231b23..f5223727d1 100644 --- a/packages/adapters/txservice/src/shared/contracts.ts +++ b/packages/adapters/txservice/src/shared/contracts.ts @@ -115,6 +115,17 @@ export const getDeployedMerkleRootManagerContract = ( return contract ? { address: contract.address, abi: contract.abi } : undefined; }; +export const getDeployedMerkleTreeManagerRootContract = ( + chainId: number, + postfix: ContractPostfix = "", +): { address: string; abi: any } | undefined => { + const record = _getContractDeployments()[chainId.toString()] ?? {}; + const contract = record[0]?.contracts + ? record[0]?.contracts[`MerkleTreeManagerRootUpgradeBeaconProxy${postfix}`] + : undefined; + return contract ? { address: contract.address, abi: contract.abi } : undefined; +}; + export const getDeployedHubConnecterContract = ( chainId: number, prefix: string, @@ -212,6 +223,11 @@ export type MerkleTreeManagerDeploymentGetter = ( postfix?: ContractPostfix, ) => { address: string; abi: any } | undefined; +export type RootMerkleTreeManagerDeploymentGetter = ( + chainId: number, + postfix?: ContractPostfix, +) => { address: string; abi: any } | undefined; + export type AmbDeploymentGetter = ( chainId: number, prefix: string, @@ -239,6 +255,7 @@ export type ConnextContractDeployments = { stableSwap: ConnextContractDeploymentGetter; spokeConnector: SpokeConnectorDeploymentGetter; spokeMerkleTreeManager: MerkleTreeManagerDeploymentGetter; + rootMerkleTreeManager: RootMerkleTreeManagerDeploymentGetter; hubConnector: HubConnectorDeploymentGetter; multisend: MultisendContractDeploymentGetter; unwrapper: UnwrapperContractDeploymentGetter; @@ -252,6 +269,7 @@ export const contractDeployments: ConnextContractDeployments = { stableSwap: getDeployedStableSwapContract, spokeConnector: getDeployedSpokeConnecterContract, spokeMerkleTreeManager: getDeployedMerkleRootManagerContract, + rootMerkleTreeManager: getDeployedMerkleTreeManagerRootContract, hubConnector: getDeployedHubConnecterContract, multisend: getDeployedMultisendContract, unwrapper: getDeployedUnwrapperContract, diff --git a/packages/agents/lighthouse/src/config.ts b/packages/agents/lighthouse/src/config.ts index 10d58a1a25..aed653a882 100644 --- a/packages/agents/lighthouse/src/config.ts +++ b/packages/agents/lighthouse/src/config.ts @@ -20,6 +20,7 @@ export const TChainConfig = Type.Object({ providers: Type.Array(Type.String()), deployments: Type.Object({ spokeMerkleTree: TAddress, + hubMerkleTree: TAddress, spokeConnector: TAddress, relayerProxy: TAddress, rootManager: TAddress, @@ -289,6 +290,16 @@ export const getEnvConfig = ( return res.address; })(), + hubMerkleTree: + chainConfig.deployments?.hubMerkleTree ?? + (() => { + const res = chainDataForChain ? deployments.rootMerkleTreeManager(hubChain, contractPostfix) : undefined; + + if (!res) { + throw new Error(`No hub MerkleTreeManager contract address for domain ${hubChain}`); + } + return res.address; + })(), relayerProxy: chainConfig.deployments?.relayerProxy ?? (() => { diff --git a/packages/agents/lighthouse/src/tasks/propose/errors.ts b/packages/agents/lighthouse/src/tasks/propose/errors.ts index e38ae8f1dc..a31d0eb816 100644 --- a/packages/agents/lighthouse/src/tasks/propose/errors.ts +++ b/packages/agents/lighthouse/src/tasks/propose/errors.ts @@ -29,6 +29,20 @@ export class NoSpokeConnector extends NxtpError { }); } } +export class NoMerkleTreeAddress extends NxtpError { + constructor( + public readonly hubDomain: string, + public readonly requestContext: RequestContext, + public readonly methodContext: MethodContext, + public readonly context: any = {}, + ) { + super(`No spokeconnector for domain ${hubDomain}`, { + ...context, + requestContext, + methodContext, + }); + } +} export class MissingRequiredDomain extends NxtpError { constructor( @@ -60,14 +74,27 @@ export class NoSnapshotRoot extends NxtpError { } } -export class NoSpokeOptimisticRoot extends NxtpError { +export class LatestPropagatedSnapshot extends NxtpError { constructor( - public readonly domain: string, public readonly requestContext: RequestContext, public readonly methodContext: MethodContext, public readonly context: any = {}, ) { - super(`SpokeOptimisticRoot not available for domain ${domain}`, { + super(`Latest propagated snapshot not available for hub domain`, { + ...context, + requestContext, + methodContext, + }); + } +} +export class NoRootTimestamp extends NxtpError { + constructor( + public readonly aggregateRoot: string, + public readonly requestContext: RequestContext, + public readonly methodContext: MethodContext, + public readonly context: any = {}, + ) { + super(`No propagated_timestamp available for snapshot with aggregate root ${aggregateRoot}`, { ...context, requestContext, methodContext, diff --git a/packages/agents/lighthouse/src/tasks/propose/operations/propose.ts b/packages/agents/lighthouse/src/tasks/propose/operations/propose.ts index b2a82b1a1b..568006489e 100644 --- a/packages/agents/lighthouse/src/tasks/propose/operations/propose.ts +++ b/packages/agents/lighthouse/src/tasks/propose/operations/propose.ts @@ -11,7 +11,13 @@ import { BigNumber } from "ethers"; import { NoBaseAggregateRootCount, NoBaseAggregateRoot } from "../../../errors"; import { sendWithRelayerWithBackup } from "../../../mockable"; -import { NoChainIdForDomain, MissingRequiredDomain, NoSnapshotRoot, NoSpokeConnector } from "../errors"; +import { + NoChainIdForDomain, + MissingRequiredDomain, + NoSnapshotRoot, + NoSpokeConnector, + NoMerkleTreeAddress, +} from "../errors"; import { getContext } from "../propose"; import { OptimisticHubDBHelper } from "../adapters"; @@ -20,6 +26,7 @@ export type ExtraPropagateParam = { _fee: string; _encodedData: string; }; +const EMPTY_ROOT = "0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757"; export const proposeHub = async () => { const { @@ -127,14 +134,60 @@ export const proposeSnapshot = async ( const hubChainId = domainToChainId(+config.hubDomain); // const _totalFee = constants.Zero; - const baseAggregateRoot = await database.getBaseAggregateRoot(); + let baseAggregateRoot = await database.getBaseAggregateRoot(); + + // If this is the first snapshot, there will be no base aggregate root. + let baseAggregateRootCount: number | undefined; + if (!baseAggregateRoot) { + const rootMerkleTreeAddress = config.chains[config.hubDomain]?.deployments.hubMerkleTree; + if (!rootMerkleTreeAddress) { + throw new NoMerkleTreeAddress(config.hubDomain, requestContext, methodContext); + } + + let root: string; + let count: BigNumber; + const encodedData = contracts.merkleTreeManager.encodeFunctionData("rootAndCount"); + try { + const idResultData = await chainreader.readTx({ + domain: +config.hubDomain, + to: rootMerkleTreeAddress, + data: encodedData, + }); + + [root, count] = contracts.merkleTreeManager.decodeFunctionResult("rootAndCount", idResultData); + } catch (err: unknown) { + logger.error( + "Failed to read the latest aggregate root and count from onchain", + requestContext, + methodContext, + jsonifyError(err as NxtpError), + ); + // Cannot proceed without the latest snapshot ID. + return; + } + logger.info("Got the latest aggregate root and count from onchain", requestContext, methodContext, { + root, + count, + }); + + if (root === EMPTY_ROOT && count.isZero()) { + baseAggregateRoot = root; + baseAggregateRootCount = 0; + logger.info("Found EMPTY_ROOT from onchain", requestContext, methodContext, { + baseAggregateRoot, + baseAggregateRootCount, + }); + } + } if (baseAggregateRoot === undefined) { throw new NoBaseAggregateRoot(); } - const baseAggregateRootCount = await database.getBaseAggregateRootCount(baseAggregateRoot); - if (!baseAggregateRootCount) { + if (baseAggregateRootCount === undefined) { + baseAggregateRootCount = await database.getBaseAggregateRootCount(baseAggregateRoot); + } + if (baseAggregateRootCount === undefined) { throw new NoBaseAggregateRootCount(baseAggregateRoot); } const baseAggregateRoots: string[] = await database.getAggregateRoots(baseAggregateRootCount); diff --git a/packages/agents/lighthouse/src/tasks/propose/operations/proposeSpoke.ts b/packages/agents/lighthouse/src/tasks/propose/operations/proposeSpoke.ts index cc716b7389..5912a929af 100644 --- a/packages/agents/lighthouse/src/tasks/propose/operations/proposeSpoke.ts +++ b/packages/agents/lighthouse/src/tasks/propose/operations/proposeSpoke.ts @@ -2,7 +2,7 @@ import { createLoggingContext, NxtpError, RequestContext, jsonifyError, domainTo import { BigNumber } from "ethers"; import { sendWithRelayerWithBackup } from "../../../mockable"; -import { NoChainIdForDomain, NoSpokeOptimisticRoot, NoSpokeConnector } from "../errors"; +import { NoChainIdForDomain, LatestPropagatedSnapshot, NoSpokeConnector, NoRootTimestamp } from "../errors"; import { getContext } from "../propose"; export type ExtraPropagateParam = { @@ -21,11 +21,17 @@ export const proposeSpoke = async (spokeDomain: string) => { adapters: { database, contracts, chainreader }, } = getContext(); const { requestContext, methodContext } = createLoggingContext(proposeSpoke.name); + const latestPropagatedSnapshot = await database.getCurrentPropagatedSnapshot(); + if (!latestPropagatedSnapshot) { + throw new LatestPropagatedSnapshot(requestContext, methodContext); + } + if (!latestPropagatedSnapshot.propagateTimestamp) { + throw new NoRootTimestamp(latestPropagatedSnapshot.aggregateRoot, requestContext, methodContext); + } //TODO: V1.1 special handling for when spoke domain is hub domain if (spokeDomain === config.hubDomain) { logger.info("Starting propose operation for hub", requestContext, methodContext, spokeDomain); - //TODO: V1.1 check if proposed aggregate root is alraadly saved await sendRootToHubSpoke(requestContext); return; } @@ -72,14 +78,9 @@ export const proposeSpoke = async (spokeDomain: string) => { }); try { - const latestOptimisticRoot = await database.getLatestPendingSpokeOptimisticRootByDomain(spokeDomain); - if (!latestOptimisticRoot) { - throw new NoSpokeOptimisticRoot(spokeDomain, requestContext, methodContext); - } - await proposeOptimisticRoot( - latestOptimisticRoot.aggregateRoot, - latestOptimisticRoot.rootTimestamp, + latestPropagatedSnapshot.aggregateRoot, + latestPropagatedSnapshot.propagateTimestamp, spokeDomain, requestContext, );