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

Internal JSDoc for fastUSDC and state transition diagram #11061

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
54 changes: 25 additions & 29 deletions packages/fast-usdc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,43 +59,39 @@ sequenceDiagram

# Status Manager

### Pending Advance State Diagram
### State Diagram

*Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.*
*Transactions are qualified by the OCW and TransactionFeed before being
delivered to the Advancer.*

```mermaid
stateDiagram-v2
[*] --> Observed: observe()
[*] --> Advancing: advancing()

Advancing --> Advanced: advanceOutcome(...true)
Advancing --> AdvanceFailed: advanceOutcome(...false)

Observed --> [*]: dequeueStatus()
Advanced --> [*]: dequeueStatus()
AdvanceFailed --> [*]: dequeueStatus()

note right of [*]
After dequeueStatus():
Transaction is removed
from pendingTxs store.
Settler will .disburse()
or .forward()
end note
```
The transactionFeed receives attestations from Oracles, records their
evidence, and when enough oracles agree, (if no risks are identified)
it publishes the results for the advancer to act on.

The Advancer subscribes (via `handleTransactionEvent`) to events published by
the transactionFeed. When notified of an appropriate opportunity, it is
responsible for advancing funds to fastUSDC payees.

### Complete state diagram (starting from Transaction Feed into Advancer)
The Settler is responsible for monitoring (via `receiveUpcall`) deposits to the
settlementAccount. It either `disburse`s funds to the Pool (if funds were
Copy link
Member

Choose a reason for hiding this comment

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

The Settler is responsible for monitoring (via receiveUpcall) deposits to the
settlementAccount.

Is it worth mentioning how receiveUpcall works? The Settler also needs to register a handler for the settlementAccount: OrchestrationAccount - that's what sets up the events:

E(settlementAccount).monitorTransfers(settler.tap); // "tap" is any durable exo with a `receiveUpcall` handler

Copy link
Contributor 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 that goes in the state diagram, but I can put it in the class comment.

`advance`d to the payee), or `forwards` funds to the payee (if pool funds
were not `advance`d).

```mermaid
stateDiagram-v2
state Forwarding <<choice>>
state AdvancingChoice <<choice>>

Observed --> AdvanceSkipped : Risks identified
Observed --> Advancing : No risks, can advance
Observed --> Forwarding : No risks, Mint deposited before advance
Copy link
Member

@0xpatrickdev 0xpatrickdev Feb 28, 2025

Choose a reason for hiding this comment

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

Observed --> Forwarding : No risks, Mint deposited before advance

We should expand this more because it's no longer accurate. An invariant is that the contract never Forwards without seeing evidence. When Settler.receiveUpcall fires and there are no matches in StatusManager's pendingTxs, or the found match is Advancing, it goes into the mintedEarly store. Each time Advancer.handleTransactionEvent is invoked, it will call Settler.notifier.checkMintedEarly() and early return if true. checkMintedEarly will clear the item from mintedEarly and initiate a .forward(). Settler.notifier.notifyAdvancingResult, which is called at the end of the Advancing state transition, also checks mintedEarly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that all that happens, and it complicates our process, but if it complicates the state transitions, it's not obvious to me how it should be represented. AFAICT, the state transitions are unaffected by all of this.

Should we represent those possibilities more explicitly in the states?

Perhaps I could add a note on the choice node listing the conditions that the code pays attention to to decide whether and when to attempt to forward.

Forwarding --> Forwarded
Advancing --> Advanced
Advanced --> Disbursed

Forwarding --> Forwarded : settler.forward() succeeds
Advancing --> AdvancingChoice
AdvancingChoice --> Advanced : advancer's transferHandler detects success
Advanced --> Disbursed : settler.disburse()
AdvanceSkipped --> Forwarding : Mint deposited
AdvanceFailed --> Forwarding : Mint deposited
Advancing --> AdvanceFailed
Forwarding --> ForwardFailed
```
AdvancingChoice --> AdvanceFailed : advancer's transferHandler detects failure
Forwarding --> ForwardFailed : settler.forward() fails
Copy link
Member

Choose a reason for hiding this comment

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

Should we see Forwarding? Not seeing it in the rendered diagram for some reason.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

[possible duplicate, I thought I wrote this already.]

The old Forwarding node didn't represent a recorded state, so I was looking for a way to hide it. it's now hidden behind an unlabelled choice node. The code mostly doesn't check the prior state before deciding on a new state, so three states can lead to these transitions.

Copy link
Member

Choose a reason for hiding this comment

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

choice nodes render just as the diamond, not with their label

```
4 changes: 2 additions & 2 deletions packages/fast-usdc/src/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Status values for FastUSDC.
* Status values for FastUSDC. Includes states for advancing and settling.
*
* @enum {(typeof TxStatus)[keyof typeof TxStatus]}
*/
Expand Down Expand Up @@ -31,7 +31,7 @@ export const TerminalTxStatus = {
};

/**
* Status values for the StatusManager.
* Status values for the StatusManager while an advance is being processed.
*
* @enum {(typeof PendingTxStatus)[keyof typeof PendingTxStatus]}
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** @file main export: @see {prepareAdvancerKit} */

import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
import { AmountMath } from '@agoric/ertp';
import { assertAllDefined, makeTracer } from '@agoric/internal';
Expand Down Expand Up @@ -29,6 +31,7 @@ import { makeFeeTools } from '../utils/fees.js';
* @import {AddressHook, EvmHash, FeeConfig, LogFn, NobleAddress, EvidenceWithRisk} from '../types.js';
* @import {StatusManager} from './status-manager.js';
* @import {LiquidityPoolKit} from './liquidity-pool.js';
* @import { TransactionFeedKit } from './transaction-feed.js';
*/

/**
Expand Down Expand Up @@ -103,6 +106,10 @@ export const stateShape = harden({
});

/**
* Advancer subscribes (using handleTransactionEvent) to events published by the
* {@link TransactionFeedKit}. When notified of an appropriate opportunity, it
* is responsible for advancing funds to EUD.
*
* @param {Zone} zone
* @param {AdvancerKitPowers} caps
*/
Expand Down
8 changes: 4 additions & 4 deletions packages/fast-usdc/src/exos/liquidity-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,23 +269,23 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
const post = depositCalc(shareWorth, proposal);

// COMMIT POINT
const mint = shareMint.mintGains(post.payouts);
const sharePayout = shareMint.mintGains(post.payouts);
try {
this.state.shareWorth = post.shareWorth;
zcf.atomicRearrange(
harden([
// zoe guarantees lp has proposal.give allocated
[lp, poolSeat, proposal.give],
// mintGains() above establishes that mint has post.payouts
[mint, lp, post.payouts],
// mintGains() above establishes that sharePayout has post.payouts
[sharePayout, lp, post.payouts],
]),
);
} catch (cause) {
// UNTIL #10684: ability to terminate an incarnation w/o terminating the contract
throw new Error('🚨 cannot commit deposit', { cause });
} finally {
lp.exit();
mint.exit();
sharePayout.exit();
}
external.publishPoolMetrics();
},
Expand Down
25 changes: 22 additions & 3 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** @file main export: @see {prepareSettler} */

import { AmountMath } from '@agoric/ertp';
import { assertAllDefined, makeTracer } from '@agoric/internal';
import { CosmosChainAddressShape } from '@agoric/orchestration';
Expand Down Expand Up @@ -105,6 +107,17 @@ export const stateShape = harden({
});

/**
* The Settler is responsible for monitoring (using receiveUpcall) deposits to
* the settlementAccount. It either "disburses" funds to the Pool (if funds were
* "advance"d to the payee), or "forwards" funds to the payee (if pool funds
* were not advanced).
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 it's worth mentioning:

  • it needs to register it's tap facet (receiveUpcall) via the settlementAccount so it gets events
  • something about the mintedEarly flow - we don't .forward() until notifier.checkMinted is called by the Advancer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mentioned the registration.

In Settler, self.forward() is called in

  • receiveUpcall in Observed, AdvanceSkipped, and AdvanceFailed
  • notifyAdvancingResult() if mintedEarly and no success, or
  • checkMintedEarly() if mintedEarly has the tx.

The first of these doesn't seem to match what the second bullet says. Would you rephrase?

*
* Funds are forwarded
*
* `receivUpcall` is configured to receive notifications in
* `monitorMintingDeposits()`, with a call to
* `settlementAccount.monitorTransfers()`.
*
* @param {Zone} zone
* @param {object} caps
* @param {StatusManager} caps.statusManager
Expand Down Expand Up @@ -227,7 +240,6 @@ export const prepareSettler = (
self.addMintedEarly(nfa, amount);
return;

case PendingTxStatus.Observed:
case PendingTxStatus.AdvanceSkipped:
case PendingTxStatus.AdvanceFailed:
return self.forward(found.txHash, amount, EUD);
Expand Down Expand Up @@ -275,10 +287,12 @@ export const prepareSettler = (
}
},
/**
* If the EUD received minted funds without an advance, forward the
* funds to the pool.
*
* @param {CctpTxEvidence} evidence
* @param {CosmosChainAddress} destination
* @returns {boolean}
* @throws {Error} if minted early, so advancer doesn't advance
* @returns {boolean} whether the EUD received funds without an advance
*/
checkMintedEarly(evidence, destination) {
const {
Expand Down Expand Up @@ -313,6 +327,9 @@ export const prepareSettler = (
asMultiset(mintedEarly).add(key);
},
/**
* The intended payee received an advance from the pool. When the funds
* are minted, disburse them to the pool and fee seats.
*
* @param {EvmHash} txHash
* @param {NatValue} fullValue
*/
Expand Down Expand Up @@ -344,6 +361,8 @@ export const prepareSettler = (
statusManager.disbursed(txHash, split);
},
/**
* Funds were not advanced. Forward proceeds to the payee directly.
*
* @param {EvmHash} txHash
* @param {NatValue} fullValue
* @param {string} EUD
Expand Down
9 changes: 0 additions & 9 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ export const prepareStatusManager = (
skipAdvance: M.call(CctpTxEvidenceShape, M.arrayOf(M.string())).returns(),
advanceOutcomeForMintedEarly: M.call(EvmHashShape, M.boolean()).returns(),
advanceOutcomeForUnknownMint: M.call(CctpTxEvidenceShape).returns(),
observe: M.call(CctpTxEvidenceShape).returns(),
hasBeenObserved: M.call(CctpTxEvidenceShape).returns(M.boolean()),
deleteCompletedTxs: M.call().returns(M.undefined()),
dequeueStatus: M.call(M.string(), M.bigint()).returns(
Expand Down Expand Up @@ -311,14 +310,6 @@ export const prepareStatusManager = (
publishEvidence(txHash, evidence);
},

/**
* Add a new transaction with OBSERVED status
* @param {CctpTxEvidence} evidence
*/
observe(evidence) {
initPendingTx(evidence, PendingTxStatus.Observed);
},

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. I think we also need some cleanup in status-manager.test.ts. Some tests can be removed, and others should be updated to use .advanceSkipped.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks.

/**
* Note: ADVANCING state implies tx has been OBSERVED
*
Expand Down
1 change: 1 addition & 0 deletions packages/fast-usdc/src/exos/transaction-feed.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/** @file Exo for @see {prepareTransactionFeedKit} */

import { makeTracer } from '@agoric/internal';
import { prepareDurablePublishKit } from '@agoric/notifier';
import { Fail, quote } from '@endo/errors';
Expand Down
73 changes: 21 additions & 52 deletions packages/fast-usdc/test/exos/settler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,6 @@ const makeTestContext = async t => {

return cctpTxEvidence;
},
/**
* slow path - e.g. insufficient pool funds
* @param evidence
*/
observe: (evidence?: CctpTxEvidence) => {
const cctpTxEvidence = makeEvidence(evidence);
t.log('Mock CCTP Evidence:', cctpTxEvidence);
t.log('Pretend we `OBSERVED` (did not advance)');
statusManager.observe(cctpTxEvidence);

return cctpTxEvidence;
},
/**
* mint early path. caller must simulate tap before calling
* @param evidence
Expand Down Expand Up @@ -338,15 +326,7 @@ test('slow path: forward to EUD; remove pending tx', async t => {
...defaultSettlerParams,
});
const simulate = makeSimulate(settler.notifier);
const cctpTxEvidence = simulate.observe();
t.deepEqual(
statusManager.lookupPending(
cctpTxEvidence.tx.forwardingAddress,
cctpTxEvidence.tx.amount,
),
[{ ...cctpTxEvidence, status: PendingTxStatus.Observed }],
'statusManager shows this tx is only observed',
);
const cctpTxEvidence = simulate.skipAdvance(['TOO_LARGE_AMOUNT']);

t.log('Simulate incoming IBC settlement');
void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO());
Expand Down Expand Up @@ -384,6 +364,7 @@ test('slow path: forward to EUD; remove pending tx', async t => {
await eventLoopIteration();
t.deepEqual(storage.getDeserialized(`fun.txns.${cctpTxEvidence.txHash}`), [
{ evidence: cctpTxEvidence, status: 'OBSERVED' },
{ risksIdentified: ['TOO_LARGE_AMOUNT'], status: 'ADVANCE_SKIPPED' },
{ status: 'FORWARDED' },
]);

Expand Down Expand Up @@ -628,7 +609,7 @@ test('Multiple minted early transactions with same address and amount', async t
]);

// Simulate a third transaction and verify no more are tracked as minted early
simulate.observe({
simulate.observeLate({
...MockCctpTxEvidences.AGORIC_PLUS_OSMO(),
txHash:
'0x0000000000000000000000000000000000000000000000000000000000000001',
Expand Down Expand Up @@ -775,12 +756,11 @@ test('slow path, and forward fails (terminal state)', async t => {
const {
common,
makeSettler,
statusManager,
defaultSettlerParams,
repayer,
makeSimulate,
inspectLogs,
accounts,
peekCalls,
storage,
} = t.context;
const { usdc } = common.brands;
Expand All @@ -790,43 +770,32 @@ test('slow path, and forward fails (terminal state)', async t => {
settlementAccount: accounts.settlement.account,
...defaultSettlerParams,
});
const simulate = makeSimulate(settler.notifier);
const cctpTxEvidence = simulate.observe();
t.deepEqual(
statusManager.lookupPending(
cctpTxEvidence.tx.forwardingAddress,
cctpTxEvidence.tx.amount,
),
[{ ...cctpTxEvidence, status: PendingTxStatus.Observed }],
'statusManager shows this tx is only observed',
);

t.log('simulating forward failure (e.g. unknown route)');
const mockE = Error('no connection info found');
accounts.settlement.transferVResolver.reject(mockE);
await eventLoopIteration();

t.log('Simulate incoming IBC settlement');
void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO());
await eventLoopIteration();
const simulate = makeSimulate(settler.notifier);

t.log('funds are forwarded; no interaction with LP');
t.like(accounts.settlement.callLog, [
const evidence = simulate.observeLate();

const rawLogs = inspectLogs();
const penultimateLog = rawLogs.slice(rawLogs.length - 2, rawLogs.length - 1);
await eventLoopIteration();
t.deepEqual(penultimateLog, [
[
'transfer',
'cosmos:osmosis-1:osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
usdc.units(150),
{
forwardOpts: {
intermediateRecipient: {
value: 'noble1test',
},
},
},
'⚠️ tap: minted before observed',
'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd',
150000000n,
],
]);

t.log('simulating forward failure (e.g. unknown route)');
const mockE = Error('no connection info found');
accounts.settlement.transferVResolver.reject(mockE);
await eventLoopIteration();
t.deepEqual(storage.getDeserialized(`fun.txns.${cctpTxEvidence.txHash}`), [
{ evidence: cctpTxEvidence, status: 'OBSERVED' },
t.deepEqual(storage.getDeserialized(`fun.txns.${evidence.txHash}`), [
{ evidence, status: 'OBSERVED' },
{ status: 'FORWARD_FAILED' },
]);
});
Expand Down
Loading
Loading