Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve(BundleDataClient,SpokePoolClient): Log about duplicate events and delete getLatestProposedBundleData #884

Merged
merged 40 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b0a7a23
improve(BundleDataClient): Log about duplicate destination chain events
nicholaspai Feb 10, 2025
55783b7
Log duplicate deposits in spoke pool client
nicholaspai Feb 10, 2025
9850552
getLatestProposedBundleData should never load data from scratch
nicholaspai Feb 10, 2025
1ab05cc
lint
nicholaspai Feb 10, 2025
7c30e1a
Update BundleDataClient.ts
nicholaspai Feb 10, 2025
8c066af
Update BundleDataClient.ts
nicholaspai Feb 10, 2025
ce0be1d
Update SpokePoolClient.ts
nicholaspai Feb 10, 2025
bf3bae6
Update BundleDataClient.ts
nicholaspai Feb 10, 2025
faebdb8
exit after n == 4
nicholaspai Feb 10, 2025
42fb574
Update SpokePoolClient.ts
nicholaspai Feb 10, 2025
04d3592
Update BundleDataClient.ts
nicholaspai Feb 10, 2025
7545cf2
reduce noisiness of logs
nicholaspai Feb 11, 2025
2df77cf
Update BundleDataClient.ts
nicholaspai Feb 11, 2025
3f802dc
Update SpokePoolClient.ts
nicholaspai Feb 11, 2025
f2429b8
Clean up logs
nicholaspai Feb 11, 2025
2b6a8dd
Update BundleDataClient.ts
nicholaspai Feb 11, 2025
34ce1e0
Update BundleDataClient.ts
nicholaspai Feb 11, 2025
ea48db5
Update BundleDataClient.ts
nicholaspai Feb 11, 2025
331dd34
Update BundleDataClient.ts
nicholaspai Feb 11, 2025
fafbbb3
Update BundleDataClient.ts
nicholaspai Feb 12, 2025
9e3090b
refactor
nicholaspai Feb 12, 2025
e1c8599
Merge branch 'master' into log-duplicates
nicholaspai Feb 12, 2025
29b9464
duplicate event util
nicholaspai Feb 12, 2025
30a1abb
Add getPendingPoolRebalanceLeavesFromArweave
nicholaspai Feb 12, 2025
c90cb2f
Update BundleDataClient.ts
nicholaspai Feb 12, 2025
d2e23df
fix
nicholaspai Feb 12, 2025
00e1d7b
Update package.json
nicholaspai Feb 12, 2025
6e3ca4d
Update BundleDataClient.ts
nicholaspai Feb 12, 2025
d6b2a36
merge
nicholaspai Feb 13, 2025
54741d6
Update BundleDataClient.ts
nicholaspai Feb 13, 2025
7264854
Update BundleDataClient.ts
nicholaspai Feb 13, 2025
412fa8f
Revert "Update BundleDataClient.ts"
nicholaspai Feb 13, 2025
337adc8
Update BundleDataClient.ts
nicholaspai Feb 13, 2025
988356a
Update BundleDataClient.ts
nicholaspai Feb 13, 2025
34b6010
Update BundleDataClient.ts
nicholaspai Feb 13, 2025
aab61e9
Update src/clients/HubPoolClient.ts
nicholaspai Feb 13, 2025
329d44c
Update HubPoolClient.ts
nicholaspai Feb 13, 2025
4333390
Update SpokePoolClient.ts
nicholaspai Feb 13, 2025
70773e2
Update HubPoolClient.ts
nicholaspai Feb 13, 2025
502ed7f
Update package.json
nicholaspai Feb 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 111 additions & 133 deletions src/clients/BundleDataClient/BundleDataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
Deposit,
DepositWithBlock,
} from "../../interfaces";
import { AcrossConfigStoreClient, SpokePoolClient } from "..";
import { SpokePoolClient } from "..";
import {
BigNumber,
bnZero,
Expand All @@ -41,18 +41,17 @@ import {
isZeroValueFillOrSlowFillRequest,
chainIsEvm,
isValidEvmAddress,
duplicateEvent,
} from "../../utils";
import winston from "winston";
import {
_buildPoolRebalanceRoot,
BundleData,
BundleDataSS,
getEndBlockBuffers,
getRefundInformationFromFill,
getRefundsFromBundle,
getWidestPossibleExpectedBlockRange,
isChainDisabled,
PoolRebalanceRoot,
prettyPrintV3SpokePoolEvents,
V3DepositWithBlock,
V3FillWithBlock,
Expand Down Expand Up @@ -240,13 +239,7 @@ export class BundleDataClient {
);
}

private async loadPersistedDataFromArweave(
blockRangesForChains: number[][]
): Promise<LoadDataReturnValue | undefined> {
if (!isDefined(this.clients?.arweaveClient)) {
return undefined;
}
const start = performance.now();
private async getBundleDataFromArweave(blockRangesForChains: number[][]) {
const persistedData = await this.clients.arweaveClient.getByTopic(
this.getArweaveBundleDataClientKey(blockRangesForChains),
BundleDataSS
Expand All @@ -256,6 +249,20 @@ export class BundleDataClient {
if (!isDefined(persistedData) || persistedData.length < 1) {
return undefined;
}
return persistedData;
}

private async loadPersistedDataFromArweave(
blockRangesForChains: number[][]
): Promise<LoadDataReturnValue | undefined> {
if (!isDefined(this.clients?.arweaveClient)) {
return undefined;
}
const start = performance.now();
const persistedData = await this.getBundleDataFromArweave(blockRangesForChains);
if (!isDefined(persistedData)) {
return undefined;
}

// A converter function to account for the fact that our SuperStruct schema does not support numeric
// keys in records. Fundamentally, this is a limitation of superstruct itself.
Expand Down Expand Up @@ -431,52 +438,6 @@ export class BundleDataClient {
}, toBN(0));
}

private async getLatestProposedBundleData(): Promise<{ bundleData: LoadDataReturnValue; blockRanges: number[][] }> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need these functions in across-protocol/relayer#2085

const hubPoolClient = this.clients.hubPoolClient;
// Determine which bundle we should fetch from arweave, either the pending bundle or the latest
// executed one. Both should have arweave data but if for some reason the arweave data is missing,
// this function will have to compute the bundle data from scratch which will be slow. We have to fallback
// to computing the bundle from scratch since this function needs to return the full bundle data so that
// it can be used to get the running balance proposed using its data.
const bundleBlockRanges = getImpliedBundleBlockRanges(
hubPoolClient,
this.clients.configStoreClient,
hubPoolClient.hasPendingProposal()
? hubPoolClient.getLatestProposedRootBundle()
: hubPoolClient.getLatestFullyExecutedRootBundle(hubPoolClient.latestBlockSearched)! // ! because we know there is a bundle
);
return {
blockRanges: bundleBlockRanges,
bundleData: await this.loadData(
bundleBlockRanges,
this.spokePoolClients,
true // this bundle data should have been published to arweave
),
};
}

async getLatestPoolRebalanceRoot(): Promise<{ root: PoolRebalanceRoot; blockRanges: number[][] }> {
const { bundleData, blockRanges } = await this.getLatestProposedBundleData();
const hubPoolClient = this.clients.hubPoolClient;
const root = _buildPoolRebalanceRoot(
hubPoolClient.latestBlockSearched,
blockRanges[0][1],
bundleData.bundleDepositsV3,
bundleData.bundleFillsV3,
bundleData.bundleSlowFillsV3,
bundleData.unexecutableSlowFills,
bundleData.expiredDepositsToRefundV3,
{
hubPoolClient,
configStoreClient: hubPoolClient.configStoreClient as AcrossConfigStoreClient,
}
);
return {
root,
blockRanges,
};
}

// @dev This function should probably be moved to the InventoryClient since it bypasses loadData completely now.
// Return refunds from the next valid bundle. This will contain any refunds that have been sent but are not included
// in a valid bundle with all of its leaves executed. This contains refunds from:
Expand Down Expand Up @@ -863,6 +824,14 @@ export class BundleDataClient {
"Not using correct bundle deposit hash key"
);
if (deposit.blockNumber >= originChainBlockRange[0]) {
if (bundleDepositsV3?.[originChainId]?.[deposit.inputToken]?.find((d) => duplicateEvent(deposit, d))) {
this.logger.debug({
at: "BundleDataClient#loadData",
message: "Duplicate deposit detected",
deposit,
});
throw new Error("Duplicate deposit detected");
}
bundleDepositHashes.push(newBundleDepositHash);
updateBundleDepositsV3(bundleDepositsV3, deposit);
} else if (deposit.blockNumber < originChainBlockRange[0]) {
Expand Down Expand Up @@ -920,64 +889,69 @@ export class BundleDataClient {
fillCounter++;
const relayDataHash = getRelayEventKey(fill);
if (v3RelayHashes[relayDataHash]) {
if (!v3RelayHashes[relayDataHash].fill) {
assert(
isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
"Deposit should exist in relay hash dictionary."
if (v3RelayHashes[relayDataHash].fill) {
this.logger.debug({
at: "BundleDataClient#loadData",
message: "Duplicate fill detected",
fill,
});
Comment on lines +942 to +946
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the subsequent Duplicate slow fill request detected log message are pretty far away from the if statement that can trigger them; it's hard to see the logic in github at least. wdyt about relocating them to the top?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creates a big diff but good point: 9e3090b

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pxrl I've decided to revert back to this implementation becaues the diff is so big and hard to verify.

throw new Error("Duplicate fill detected");
}
assert(
isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
"Deposit should exist in relay hash dictionary."
);
v3RelayHashes[relayDataHash].fill = fill;
if (fill.blockNumber >= destinationChainBlockRange[0]) {
const fillToRefund = await verifyFillRepayment(
fill,
destinationClient.spokePool.provider,
v3RelayHashes[relayDataHash].deposits![0],
this.clients.hubPoolClient
);
v3RelayHashes[relayDataHash].fill = fill;
if (fill.blockNumber >= destinationChainBlockRange[0]) {
const fillToRefund = await verifyFillRepayment(
fill,
destinationClient.spokePool.provider,
v3RelayHashes[relayDataHash].deposits![0],
this.clients.hubPoolClient
);
if (!isDefined(fillToRefund)) {
bundleUnrepayableFillsV3.push(fill);
// We don't return here yet because we still need to mark unexecutable slow fill leaves
// or duplicate deposits. However, we won't issue a fast fill refund.
} else {
v3RelayHashes[relayDataHash].fill = fillToRefund;
validatedBundleV3Fills.push({
...fillToRefund,
quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp,
});

// Now that we know this deposit has been filled on-chain, identify any duplicate deposits
// sent for this fill and refund them to the filler, because this value would not be paid out
// otherwise. These deposits can no longer expire and get refunded as an expired deposit,
// and they won't trigger a pre-fill refund because the fill is in this bundle.
// Pre-fill refunds only happen when deposits are sent in this bundle and the
// fill is from a prior bundle. Paying out the filler keeps the behavior consistent for how
// we deal with duplicate deposits regardless if the deposit is matched with a pre-fill or
// a current bundle fill.
const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1);
duplicateDeposits.forEach((duplicateDeposit) => {
if (isSlowFill(fill)) {
updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit);
} else {
validatedBundleV3Fills.push({
...fillToRefund,
quoteTimestamp: duplicateDeposit.quoteTimestamp,
});
}
});
}

// If fill replaced a slow fill request, then mark it as one that might have created an
// unexecutable slow fill. We can't know for sure until we check the slow fill request
// events.
if (
fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill &&
_canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposits![0])
) {
fastFillsReplacingSlowFills.push(relayDataHash);
}
if (!isDefined(fillToRefund)) {
bundleUnrepayableFillsV3.push(fill);
// We don't return here yet because we still need to mark unexecutable slow fill leaves
// or duplicate deposits. However, we won't issue a fast fill refund.
} else {
v3RelayHashes[relayDataHash].fill = fillToRefund;
validatedBundleV3Fills.push({
...fillToRefund,
quoteTimestamp: v3RelayHashes[relayDataHash].deposits![0].quoteTimestamp,
});

// Now that we know this deposit has been filled on-chain, identify any duplicate deposits
// sent for this fill and refund them to the filler, because this value would not be paid out
// otherwise. These deposits can no longer expire and get refunded as an expired deposit,
// and they won't trigger a pre-fill refund because the fill is in this bundle.
// Pre-fill refunds only happen when deposits are sent in this bundle and the
// fill is from a prior bundle. Paying out the filler keeps the behavior consistent for how
// we deal with duplicate deposits regardless if the deposit is matched with a pre-fill or
// a current bundle fill.
const duplicateDeposits = v3RelayHashes[relayDataHash].deposits!.slice(1);
duplicateDeposits.forEach((duplicateDeposit) => {
if (isSlowFill(fill)) {
updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit);
} else {
validatedBundleV3Fills.push({
...fillToRefund,
quoteTimestamp: duplicateDeposit.quoteTimestamp,
});
}
});
}

// If fill replaced a slow fill request, then mark it as one that might have created an
// unexecutable slow fill. We can't know for sure until we check the slow fill request
// events.
if (
fill.relayExecutionInfo.fillType === FillType.ReplacedSlowFill &&
_canCreateSlowFillLeaf(v3RelayHashes[relayDataHash].deposits![0])
) {
fastFillsReplacingSlowFills.push(relayDataHash);
}
} else {
throw new Error("Duplicate fill detected");
}

return;
}

Expand Down Expand Up @@ -1077,30 +1051,34 @@ export class BundleDataClient {
const relayDataHash = getRelayEventKey(slowFillRequest);

if (v3RelayHashes[relayDataHash]) {
if (!v3RelayHashes[relayDataHash].slowFillRequest) {
v3RelayHashes[relayDataHash].slowFillRequest = slowFillRequest;
if (v3RelayHashes[relayDataHash].fill) {
// Exiting here assumes that slow fill requests must precede fills, so if there was a fill
// following this slow fill request, then we would have already seen it. We don't need to check
// for a fill older than this slow fill request.
return;
}
assert(
isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
"Deposit should exist in relay hash dictionary."
);
const matchedDeposit = v3RelayHashes[relayDataHash].deposits![0];

if (
slowFillRequest.blockNumber >= destinationChainBlockRange[0] &&
_canCreateSlowFillLeaf(matchedDeposit) &&
!_depositIsExpired(matchedDeposit)
) {
validatedBundleSlowFills.push(matchedDeposit);
}
} else {
if (v3RelayHashes[relayDataHash].slowFillRequest) {
this.logger.debug({
at: "BundleDataClient#loadData",
message: "Duplicate slow fill request detected",
slowFillRequest,
});
throw new Error("Duplicate slow fill request detected.");
}
v3RelayHashes[relayDataHash].slowFillRequest = slowFillRequest;
if (v3RelayHashes[relayDataHash].fill) {
// Exiting here assumes that slow fill requests must precede fills, so if there was a fill
// following this slow fill request, then we would have already seen it. We don't need to check
// for a fill older than this slow fill request.
return;
}
assert(
isDefined(v3RelayHashes[relayDataHash].deposits) && v3RelayHashes[relayDataHash].deposits!.length > 0,
"Deposit should exist in relay hash dictionary."
);
const matchedDeposit = v3RelayHashes[relayDataHash].deposits![0];

if (
slowFillRequest.blockNumber >= destinationChainBlockRange[0] &&
_canCreateSlowFillLeaf(matchedDeposit) &&
!_depositIsExpired(matchedDeposit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: For a future optimisation, I wonder if this could also exclude slow fills that will be unfillable based on the fillDeadline and the challenge window required at the time of the proposal. Not sure if there are chicken and egg problems there though, since the challenge window could be modified up or down whilst the proposer is building a proposal. It would however be a way of returning deposit refunds to users faster, because the slow-fill that is ultimately unfillable is both an unfortunate use of capital as well as bad UX because the user has to wait for 2 bundles to get their repayment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can do this because of this non-determinism:

since the challenge window could be modified up or down whilst the proposer is building a proposal

We just don't know which block number this bundle will be proposed. Theoretically the mainnet bundle end block could be hours before the proposal lands

) {
validatedBundleSlowFills.push(matchedDeposit);
}
return;
}

Expand Down
22 changes: 12 additions & 10 deletions src/clients/HubPoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,17 +776,19 @@ export class HubPoolClient extends BaseAbstractClient {
return endBlock > 0 ? endBlock + 1 : 0;
}

getRunningBalanceBeforeBlockForChain(block: number, chain: number, l1Token: string): TokenRunningBalance {
getLatestExecutedRootBundleContainingL1Token(block: number, chain: number, l1Token: string): ExecutedRootBundle {
// Search ExecutedRootBundles in descending block order to find the most recent event before the target block.
const executedRootBundle = sortEventsDescending(this.executedRootBundles).find(
(executedLeaf: ExecutedRootBundle) => {
return (
executedLeaf.blockNumber <= block &&
executedLeaf.chainId === chain &&
executedLeaf.l1Tokens.map((l1Token) => l1Token.toLowerCase()).includes(l1Token.toLowerCase())
);
}
) as ExecutedRootBundle;
return sortEventsDescending(this.executedRootBundles).find((executedLeaf: ExecutedRootBundle) => {
return (
executedLeaf.blockNumber <= block &&
executedLeaf.chainId === chain &&
executedLeaf.l1Tokens.map((l1Token) => l1Token.toLowerCase()).includes(l1Token.toLowerCase())
Copy link
Contributor

@pxrl pxrl Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparison sticks out to me. l1Token as passed into the function is overridden within the Array.call() closure. Additionally, l1Token is a string in either case, and String.includes() reverts to checking for substrings, rather than a complete match.

Suggested change
executedLeaf.l1Tokens.map((l1Token) => l1Token.toLowerCase()).includes(l1Token.toLowerCase())
executedLeaf.l1Tokens.some((token) => token.toLowerCase() === l1Token.toLowerCase()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats a good point, though I don't think the .map result matters since the map returns the modified array right? (which is then thrown away)

);
}) as ExecutedRootBundle;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this to ward of an ...is potentially undefined warning? Is it still necessary if the above change is made?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, this is an improvement: 329d44c

}

getRunningBalanceBeforeBlockForChain(block: number, chain: number, l1Token: string): TokenRunningBalance {
const executedRootBundle = this.getLatestExecutedRootBundleContainingL1Token(block, chain, l1Token);

return this.getRunningBalanceForToken(l1Token, executedRootBundle);
}
Expand Down
Loading