-
Notifications
You must be signed in to change notification settings - Fork 244
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
base: master
Are you sure you want to change the base?
Conversation
Deploying agoric-sdk with
|
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 |
- 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)
716ef26
to
10a27ec
Compare
- 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
10a27ec
to
1ce6478
Compare
|
||
/** | ||
* 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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
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; | ||
}; |
There was a problem hiding this comment.
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,
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why include?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
love to see it
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[]
andM.arrayOf(PendingTxShape)
, but is now sorted descending bytx.amount
dequeueStatus
was renamed tomatchAndDequeueSettlement
, and now returns an array ofPendingTx
s instead of a singlePendingTx
or undefined. If no matches are found, an empty array is returnedmatchAndDequeueSettlement
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 viaMAX_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
andmatchAndDequeueSettlement
with a wide range of scenarios.Upgrade Considerations
Includes
migratePendingSettleStore
which will handle theMapStore
migration the next time the contract starts.