Skip to content

Commit b443f99

Browse files
authored
Merge branch 'master' into bz/hlExecutor
2 parents 1bc2ca7 + 47ede61 commit b443f99

File tree

4 files changed

+374
-13
lines changed

4 files changed

+374
-13
lines changed

index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
stringifyThrownValue,
1212
} from "./src/utils";
1313
import { runRelayer, runRebalancer } from "./src/relayer";
14-
import { runDataworker } from "./src/dataworker";
14+
import { runDataworker, runDisputerWatchdog } from "./src/dataworker";
1515
import { runMonitor } from "./src/monitor";
1616
import { runFinalizer } from "./src/finalizer";
1717
import { version } from "./package.json";
@@ -23,6 +23,7 @@ let cmd: string;
2323

2424
const CMDS = {
2525
dataworker: runDataworker,
26+
"disputer-watchdog": runDisputerWatchdog,
2627
finalizer: runFinalizer,
2728
help: help,
2829
monitor: runMonitor,

src/dataworker/Disputer.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { Contract, Signer } from "ethers";
2+
import { WETH9__factory as WETH9 } from "@across-protocol/contracts";
3+
import { AugmentedTransaction, TransactionClient } from "../clients";
4+
import {
5+
BigNumber,
6+
bnUint256Max,
7+
bnZero,
8+
formatEther,
9+
getNetworkName,
10+
isDefined,
11+
Provider,
12+
TransactionReceipt,
13+
winston,
14+
} from "../utils";
15+
16+
export class Disputer {
17+
protected bondToken: Contract;
18+
protected bondAmount: BigNumber;
19+
protected bondMultiplier: { min: number; target: number };
20+
protected provider: Provider;
21+
protected txnClient: TransactionClient;
22+
protected chain: string;
23+
private initPromise: Promise<void>;
24+
25+
constructor(
26+
protected readonly chainId: number,
27+
protected readonly logger: winston.Logger,
28+
protected readonly hubPool: Contract,
29+
readonly signer: Signer,
30+
protected readonly simulate = true
31+
) {
32+
this.chain = getNetworkName(chainId);
33+
this.provider = hubPool.provider;
34+
// signer.connect() is unsupported in test.
35+
this.signer = signer.provider ? signer : signer.connect(hubPool.provider);
36+
this.bondMultiplier = {
37+
min: 4,
38+
target: 8,
39+
};
40+
this.txnClient = new TransactionClient(this.logger);
41+
42+
const initPromise = async () => {
43+
// @todo: Optimise all calls here by using Multicall3 to query:
44+
// - bondToken
45+
// - bondAmount
46+
// - native balance
47+
const [bondToken, bondAmount] = await Promise.all([this.hubPool.bondToken(), this.hubPool.bondAmount()]);
48+
this.bondToken = WETH9.connect(bondToken, this.signer);
49+
this.bondAmount = bondAmount;
50+
};
51+
this.initPromise = initPromise();
52+
}
53+
54+
async validate(): Promise<void> {
55+
await this.initPromise;
56+
57+
const { bondAmount, logger } = this;
58+
const minBondAmount = bondAmount.mul(this.bondMultiplier.min);
59+
60+
// Balance checks.
61+
const balance = await this.balance();
62+
const mintAmount = minBondAmount.sub(balance);
63+
if (mintAmount.gt(bnZero)) {
64+
const nativeBalance = await this.provider.getBalance(await this.signer.getAddress());
65+
if (nativeBalance.gt(mintAmount)) {
66+
await this.mintBond(mintAmount);
67+
} else {
68+
const fmtAmount = formatEther(mintAmount);
69+
const message = `Insufficient native token balance to mint ${fmtAmount} bond tokens.`;
70+
if (!this.simulate && balance.lt(bondAmount)) {
71+
throw new Error(message);
72+
}
73+
logger.warn({ at: "Disputer::validate", message, nativeBalance, mintAmount });
74+
}
75+
}
76+
77+
// Ensure allowances are in place.
78+
const allowance = await this.allowance();
79+
const minAllowance = bondAmount.mul(this.bondMultiplier.target);
80+
if (allowance.lt(minAllowance)) {
81+
await this.approve();
82+
}
83+
}
84+
85+
async balance(): Promise<BigNumber> {
86+
const disputer = await this.signer.getAddress();
87+
return this.bondToken.balanceOf(disputer);
88+
}
89+
90+
async allowance(): Promise<BigNumber> {
91+
const signer = await this.signer.getAddress();
92+
return this.bondToken.allowance(signer, this.hubPool.address);
93+
}
94+
95+
async approve(amount = bnUint256Max): Promise<TransactionReceipt | undefined> {
96+
const { chainId, bondToken, hubPool } = this;
97+
const txn = {
98+
chainId,
99+
contract: bondToken,
100+
method: "approve",
101+
args: [hubPool.address, amount],
102+
message: "Approved HubPool to spend bondToken.",
103+
amount,
104+
unpermissioned: false,
105+
canFailInSimulation: false,
106+
nonMulticall: true,
107+
};
108+
109+
return this.submit(txn);
110+
}
111+
112+
async mintBond(amount: BigNumber): Promise<TransactionReceipt | undefined> {
113+
const { chainId, bondToken } = this;
114+
const txn = {
115+
chainId,
116+
contract: bondToken,
117+
method: "deposit",
118+
args: [],
119+
message: `Minted ${amount} HubPool bondToken.`,
120+
value: amount,
121+
unpermissioned: false,
122+
canFailInSimulation: false,
123+
nonMulticall: true,
124+
};
125+
126+
return this.submit(txn);
127+
}
128+
129+
dispute(): Promise<TransactionReceipt | undefined> {
130+
const { chainId, hubPool } = this;
131+
const txn = {
132+
chainId,
133+
contract: hubPool.connect(this.signer),
134+
method: "disputeRootBundle",
135+
args: [],
136+
message: "Disputed HubPool root bundle proposal.",
137+
unpermissioned: false,
138+
canFailInSimulation: false,
139+
nonMulticall: true,
140+
};
141+
142+
try {
143+
return this.submit(txn);
144+
} catch (err) {
145+
this.logger.error({ at: "Disputer::dispute", message: "Failed to submit HubPool dispute.", err });
146+
}
147+
148+
return Promise.resolve(undefined);
149+
}
150+
151+
protected async submit(txn: AugmentedTransaction, maxTries = 3): Promise<TransactionReceipt | undefined> {
152+
const { chainId, logger, txnClient } = this;
153+
154+
if (this.simulate) {
155+
logger.warn({ at: "Disputer::submit", message: `Suppressing ${txn.method} transaction.` });
156+
return Promise.resolve(undefined);
157+
}
158+
159+
let txnReceipt: TransactionReceipt;
160+
let cause: unknown;
161+
let tries = 0;
162+
163+
do {
164+
try {
165+
const [txnResponse] = await txnClient.submit(chainId, [txn]);
166+
txnReceipt = await txnResponse.wait();
167+
return txnReceipt;
168+
} catch (err: unknown) {
169+
cause = err;
170+
}
171+
} while (!isDefined(txnReceipt) && ++tries < maxTries);
172+
173+
throw new Error(`Unable to submit transaction on ${this.chain}`, { cause });
174+
}
175+
}

src/dataworker/index.ts

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { TokenClient } from "../clients";
22
import {
3+
blockExplorerLink,
4+
CHAIN_IDs,
35
EvmAddress,
46
SvmAddress,
57
winston,
@@ -13,19 +15,23 @@ import {
1315
getSvmSignerFromEvmSigner,
1416
waitForPubSub,
1517
averageBlockTime,
18+
getRedisCache,
1619
getRedisPubSub,
20+
Provider,
1721
} from "../utils";
1822
import { spokePoolClientsToProviders } from "../common";
1923
import { Dataworker } from "./Dataworker";
2024
import { DataworkerConfig } from "./DataworkerConfig";
25+
import { generateValidationKey } from "./DataworkerUtils";
2126
import {
2227
constructDataworkerClients,
2328
constructSpokePoolClientsForFastDataworker,
2429
getSpokePoolClientEventSearchConfigsForFastDataworker,
2530
DataworkerClients,
2631
} from "./DataworkerClientHelper";
2732
import { BalanceAllocator } from "../clients/BalanceAllocator";
28-
import { PendingRootBundle, BundleData } from "../interfaces";
33+
import { PendingRootBundle, ProposedRootBundle, BundleData } from "../interfaces";
34+
import { Disputer } from "./Disputer";
2935

3036
config();
3137
let logger: winston.Logger;
@@ -80,23 +86,47 @@ function resolvePersonality(config: DataworkerConfig): string {
8086
return "Dataworker"; // unknown
8187
}
8288

89+
/**
90+
* Query the details of a proposal at a given block/tag.
91+
* @param provider Ethers provider instance.
92+
* @param chainId HubPool chain ID.
93+
* @param blockTag Block/tag to query at.
94+
*/
95+
async function getProposal(
96+
provider: Provider,
97+
chainId = CHAIN_IDs.MAINNET,
98+
blockTag: number | "latest" = "latest"
99+
): Promise<
100+
Pick<
101+
ProposedRootBundle,
102+
"poolRebalanceRoot" | "relayerRefundRoot" | "slowRelayRoot" | "challengePeriodEndTimestamp" | "proposer"
103+
> & { currentTime: number; currentBlock: number }
104+
> {
105+
const { number: currentBlock, timestamp: currentTime } = await provider.getBlock(blockTag);
106+
const hubPool = getDeployedContract("HubPool", chainId).connect(provider);
107+
108+
const { poolRebalanceRoot, relayerRefundRoot, slowRelayRoot, proposer, challengePeriodEndTimestamp } =
109+
await hubPool.rootBundleProposal({ blockTag: currentBlock });
110+
111+
return {
112+
currentTime,
113+
currentBlock,
114+
poolRebalanceRoot,
115+
relayerRefundRoot,
116+
slowRelayRoot,
117+
challengePeriodEndTimestamp,
118+
proposer: EvmAddress.from(proposer),
119+
};
120+
}
121+
83122
async function getChallengeRemaining(
84123
chainId: number,
85124
challengeBuffer: number,
86125
logger: winston.Logger
87126
): Promise<number> {
88127
const provider = await getProvider(chainId);
89-
const latestBlock = await provider.getBlockNumber();
90-
const hubPool = getDeployedContract("HubPool", chainId).connect(provider);
128+
const { currentBlock, currentTime, ...proposal } = await getProposal(provider, chainId);
91129

92-
const [proposal, currentTime] = await Promise.all([
93-
hubPool.rootBundleProposal({
94-
blockTag: latestBlock,
95-
}),
96-
hubPool.getCurrentTime({
97-
blockTag: latestBlock,
98-
}),
99-
]);
100130
const { challengePeriodEndTimestamp } = proposal;
101131
const challengeRemaining = Math.max(challengePeriodEndTimestamp + challengeBuffer - currentTime, 0);
102132
logger.debug({
@@ -105,8 +135,8 @@ async function getChallengeRemaining(
105135
challengeRemaining,
106136
challengeBuffer,
107137
challengePeriodEndTimestamp,
138+
currentBlock,
108139
currentTime,
109-
blockTag: latestBlock,
110140
});
111141

112142
return challengeRemaining;
@@ -375,3 +405,60 @@ export async function runDataworker(_logger: winston.Logger, baseSigner: Signer)
375405
await disconnectRedisClients(logger);
376406
}
377407
}
408+
409+
export async function runDisputerWatchdog(logger: winston.Logger, signer: Signer): Promise<void> {
410+
const personality = "Disputer Watchdog";
411+
const at = "runDisputerWatchDog";
412+
const config = new DataworkerConfig(process.env);
413+
const { hubPoolChainId: hubChainId, sendingTransactionsEnabled: enabled } = config;
414+
415+
const { DISPUTER_WATCHDOG_MIN_ATTESTATIONS = "3", DISPUTER_WATCHDOG_CHALLENGE_LIMIT = "600" } = process.env; // @todo Watchdog config.
416+
const minValidations = Number(DISPUTER_WATCHDOG_MIN_ATTESTATIONS);
417+
const challengeLimit = Number(DISPUTER_WATCHDOG_CHALLENGE_LIMIT);
418+
419+
const provider = await getProvider(hubChainId, logger);
420+
const hubPool = getDeployedContract("HubPool", hubChainId).connect(provider);
421+
const disputer = new Disputer(hubChainId, logger, hubPool, signer, !enabled);
422+
423+
logger.debug({ at, message: "Starting Disputer Watchdog." });
424+
425+
try {
426+
await disputer.validate();
427+
const redis = await getRedisCache(logger);
428+
429+
const { currentTime, currentBlock, ...proposal } = await getProposal(provider, hubChainId);
430+
431+
const getValidations = async () => {
432+
const key = generateValidationKey(proposal);
433+
const result = await redis.get<string>(key);
434+
return Number(result) || 0; // Revert to 0 on isNaN(result)
435+
};
436+
437+
// @todo Validate that currentTime is not too different from host time.
438+
const challengeRemaining = proposal.challengePeriodEndTimestamp - currentTime;
439+
if (challengeRemaining <= 0) {
440+
logger.debug({ at, message: "Proposal challenge window has elapsed, nothing to do..." });
441+
return;
442+
}
443+
444+
const validations = await getValidations();
445+
if (challengeRemaining <= challengeLimit && validations < minValidations) {
446+
const dispute = await disputer.dispute();
447+
const message = enabled
448+
? "Submitted HubPool root bundle dispute."
449+
: "Suppressed HubPool root bundle dispute due to configuration.";
450+
const txn = isDefined(dispute) ? blockExplorerLink(dispute.transactionHash, hubChainId) : undefined;
451+
logger.error({ at, message, proposal, txn });
452+
} else {
453+
const waiting = challengeRemaining - challengeLimit;
454+
const message =
455+
waiting > 0
456+
? `Must wait an additional ${waiting} seconds before evaluating validator attestations.`
457+
: "Current proposal has sufficient validator attestations.";
458+
logger.debug({ at, message, challengeLimit, validations, minValidations });
459+
}
460+
} finally {
461+
await disconnectRedisClients(logger);
462+
logger.debug({ at, message: `Completed ${personality} run.` });
463+
}
464+
}

0 commit comments

Comments
 (0)