Skip to content

feat: settle against batched mints #11369

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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

Conversation

0xpatrickdev
Copy link
Member

@0xpatrickdev 0xpatrickdev commented May 9, 2025

closes: https://github.com/Agoric/agoric-private/issues/312

Description

These changes allow FUSDC to settle multiple advances against a single CCTP mint. To facilitate this change:

  • pendingSettleTxs is no longer keyed by NFA + amount, just NFA, to facilitate the new matching algorithm. The value type and guard remain the same, PendingTx[] and M.arrayOf(PendingTxShape), but is now sorted descending by tx.amount
  • dequeueStatus was renamed to matchAndDequeueSettlement, and now returns an array of PendingTxs instead of a single PendingTx or undefined. If no matches are found, an empty array is returned
  • matchAndDequeueSettlement attempts to find an exact match (hot path) and will fall back to a greedy depth-first search that matches the largest settlements first.

See design discussion/decisions.

Security Considerations

The greedy‐match algorithm used in matchAndDequeueSettlement limits recursion via MAX_MATCH_DEPTH to avoid denial-of-service risks from extremely large pending sets.

Scaling Considerations

appendToSortedStoredArray uses a linear scan for insertion; this is efficient for the expected small lists (dozens of elements at most).

The settlement‐matching algorithm employs memoization and a depth cap (MAX_MATCH_DEPTH = 25) to prevent exponential blowup. Typical match depth is 2–3.

Documentation Considerations

None

Testing Considerations

Includes and fixes a test.failing case where one mint should settle multiple advances.

Includes unit tests for appendToSortedStoredArray and matchAndDequeueSettlement with a wide range of scenarios.

Upgrade Considerations

Includes migratePendingSettleStore which will handle the MapStore migration the next time the contract starts.

Copy link

cloudflare-workers-and-pages bot commented May 9, 2025

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: c9a4ecd
Status: ✅  Deploy successful!
Preview URL: https://c12d3892.agoric-sdk.pages.dev
Branch Preview URL: https://312-batched-settle.agoric-sdk.pages.dev

View logs

turadg and others added 4 commits May 11, 2025 13:49
- like `appendToStoredArray`, but keep it sorted
Implement a new module that provides efficient transaction matching for USDC
settlements. The system maintains a sorted queue of pending transactions and
uses a two-phase matching strategy:
1. Exact-amount lookup for direct matches
2. Greedy combination algorithm for multi-transaction settlements (largest to smallest)
- migrate store to shape expected by `matchAndDequeueSettlement` in `packages/fast-usdc-contract/src/utils/settlement-matching.ts`
- replaces `dequeueStatus` in favor of `matchAndDequeueSettlement` which is able to match multiple
  pending settlement transactions with a single mint
@0xpatrickdev 0xpatrickdev marked this pull request as ready for review May 12, 2025 14:23
@0xpatrickdev 0xpatrickdev requested a review from a team as a code owner May 12, 2025 14:23

/**
* Append `item` to the array held at `key`, keeping it **sorted** according
* to `compare`. Uses a simple linear scan – clearer than a binary search and
Copy link
Member

Choose a reason for hiding this comment

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

agreed, thanks for capturing

mapStore: MapStore<K, V[]>,
key: K,
item: V,
compare: (a: V, b: V) => number = defaultCompareDesc as any,
Copy link
Member

Choose a reason for hiding this comment

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

I think the any is hiding that V requires this same constraint, bigint | number | string, as defaultCompareDesc has or the default isn't valid.

I see the one use of this is to compare transactions, which takes a comparator. So please remove this default one to make it sound.

Comment on lines 29 to 33
const comparePendingTxDesc = (a: PendingTx, b: PendingTx): number => {
if (a.tx.amount < b.tx.amount) return 1;
if (a.tx.amount > b.tx.amount) return -1;
return 0;
};
Copy link
Member

Choose a reason for hiding this comment

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

consider this idiom for scalars,

Suggested change
const comparePendingTxDesc = (a: PendingTx, b: PendingTx): number => {
if (a.tx.amount < b.tx.amount) return 1;
if (a.tx.amount > b.tx.amount) return -1;
return 0;
};
const comparePendingTxDesc = (a: PendingTx, b: PendingTx): number => Number(b.tx.amount - a.tx.amount));


const dfs = (index: number, sum: bigint): boolean => {
if (sum === target) return true;
if (sum > target) return false; // unreachable with current continue‑on‑overshoot logic
Copy link
Member

Choose a reason for hiding this comment

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

why include?

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 was on the fence, but leaned towards defensiveness (against infinite recursion) and clear documentation. Happy to remove, but the cost seems low.

const dfs = (index: number, sum: bigint): boolean => {
if (sum === target) return true;
if (sum > target) return false; // unreachable with current continue‑on‑overshoot logic
if (path.length >= MAX_MATCH_DEPTH) return false;
Copy link
Member

Choose a reason for hiding this comment

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

very unexpected to reach this depth. throw instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, much better idea.

@@ -127,7 +127,7 @@ export const makeTestLogger = (logger: LogFn) => {
logger(args);
};
const inspectLogs = (index?: number) =>
Copy link
Member

Choose a reason for hiding this comment

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

consider typing the return type, which is conditioned on index being provided.

* Uses `zone.makeOnce` as a cheap abstraction for "only do this once".
*/
zone.makeOnce('migratePendingSettleStore', () => {
const current = [...pendingSettleTxs.values()]; // XXX copy necessary?
Copy link
Member

Choose a reason for hiding this comment

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

Oh I see, the iterator might have nothing after .clear() is called. I think that's correct. Please do verify and then document decisively

*/
const pendingSettleTxs: MapStore<PendingTxKey, PendingTx[]> = zone.mapStore(
const pendingSettleTxs: MapStore<NobleAddress, PendingTx[]> = zone.mapStore(
Copy link
Member

Choose a reason for hiding this comment

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

I see that until migratePendingSettleStore the key is still PendingTxKey but statically the code should assume NobleAddress.

if (!pendingSettleTxs.has(nfa)) return undefined;
const allPending = pendingSettleTxs.get(nfa);
const idx = allPending.findIndex(tx => tx.tx.amount === amount);
if (idx < 0) {
return undefined;
}
// extract first item
Copy link
Member

Choose a reason for hiding this comment

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

this is extracting the leading range now, yes?

Copy link
Member Author

@0xpatrickdev 0xpatrickdev May 12, 2025

Choose a reason for hiding this comment

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

Yes, the changes to dequeueStatus had a short shelf life in this PR. "extract first item" is stale

@@ -40,7 +40,7 @@ test.before(async t => {
sync.lp.resolve({ lp500 });
});

test.serial.failing('multiple advance amounts settled by one mint', async t => {
Copy link
Member

Choose a reason for hiding this comment

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

love to see it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants