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 error handling #11070

Merged
merged 7 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 9 additions & 6 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ export const prepareAdvancerKit = (
poolAccount,
harden({ USDC: advanceAmount }),
);
// WARNING: this must never reject, see handler @throws {never} below
// void not enforced by linter until #10627 no-floating-vows
void watch(depositV, this.facets.depositHandler, {
advanceAmount,
destination,
Expand All @@ -233,6 +235,7 @@ export const prepareAdvancerKit = (
/**
* @param {undefined} result
* @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
* @throws {never} WARNING: this function must not throw, because user funds are at risk
*/
onFulfilled(result, ctx) {
const { poolAccount, intermediateRecipient, settlementAddress } =
Expand Down Expand Up @@ -265,6 +268,7 @@ export const prepareAdvancerKit = (
*
* @param {Error} error
* @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
* @throws {never} WARNING: this function must not throw, because user funds are at risk
*/
onRejected(error, { tmpSeat, advanceAmount, ...restCtx }) {
log(
Expand All @@ -285,17 +289,12 @@ export const prepareAdvancerKit = (
/**
* @param {undefined} result
* @param {AdvancerVowCtx} ctx
* @throws {never} WARNING: this function must not throw, because user funds are at risk
*/
onFulfilled(result, ctx) {
const { notifier } = this.state;
const { advanceAmount, destination, ...detail } = ctx;
log('Advance succeeded', { advanceAmount, destination });
// During development, due to a bug, this call threw.
// The failure was silent (no diagnostics) due to:
// - #10576 Vows do not report unhandled rejections
// For now, the advancer kit relies on consistency between
// notify, statusManager, and callers of handleTransactionEvent().
// TODO: revisit #10576 during #10510
notifier.notifyAdvancingResult({ destination, ...detail }, true);
},
/**
Expand All @@ -314,6 +313,8 @@ export const prepareAdvancerKit = (
tmpReturnSeat,
harden({ USDC: advanceAmount }),
);
// WARNING: this must never reject, see handler @throws {never} below
// void not enforced by linter until #10627 no-floating-vows
void watch(withdrawV, this.facets.withdrawHandler, {
advanceAmount,
tmpReturnSeat,
Expand All @@ -325,6 +326,7 @@ export const prepareAdvancerKit = (
*
* @param {undefined} result
* @param {{ advanceAmount: Amount<'nat'>; tmpReturnSeat: ZCFSeat; }} ctx
* @throws {never} WARNING: this function must not throw, because user funds are at risk
*/
onFulfilled(result, { advanceAmount, tmpReturnSeat }) {
const { borrower } = this.state;
Expand All @@ -342,6 +344,7 @@ export const prepareAdvancerKit = (
/**
* @param {Error} error
* @param {{ advanceAmount: Amount<'nat'>; tmpReturnSeat: ZCFSeat; }} ctx
* @throws {never} WARNING: this function must not throw, because user funds are at risk
*/
onRejected(error, { advanceAmount, tmpReturnSeat }) {
log(
Expand Down
30 changes: 12 additions & 18 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,8 @@ export const prepareStatusManager = (
/**
* @param {EvmHash} txId
* @param {TransactionRecord} record
* @returns {Promise<void>}
*/
const publishTxnRecord = async (txId, record) => {
const publishTxnRecord = (txId, record) => {
const txNode = E(txnsNode).makeChildNode(txId, {
sequence: true, // avoid overwriting other output in the block
});
Expand All @@ -133,9 +132,10 @@ export const prepareStatusManager = (
storedCompletedTxs.add(txId);
}

const capData = await E(marshaller).toCapData(record);

await E(txNode).setValue(JSON.stringify(capData));
// Don't await, just writing to vstorage.
void E.when(E(marshaller).toCapData(record), capData =>
E(txNode).setValue(JSON.stringify(capData)),
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should be using...

Suggested change
E(txNode).setValue(JSON.stringify(capData)),
E.sendOnly(txNode).setValue(JSON.stringify(capData)),

I don't see any other use of it in agoric-sdk production code, though.

);
};

/**
Expand All @@ -144,10 +144,7 @@ export const prepareStatusManager = (
*/
const publishEvidence = (hash, evidence) => {
// Don't await, just writing to vstorage.
void publishTxnRecord(
hash,
harden({ evidence, status: TxStatus.Observed }),
);
publishTxnRecord(hash, harden({ evidence, status: TxStatus.Observed }));
};

/**
Expand All @@ -174,10 +171,10 @@ export const prepareStatusManager = (
);
publishEvidence(txHash, evidence);
if (status === PendingTxStatus.AdvanceSkipped) {
void publishTxnRecord(txHash, harden({ status, risksIdentified }));
publishTxnRecord(txHash, harden({ status, risksIdentified }));
} else if (status !== PendingTxStatus.Observed) {
// publishEvidence publishes Observed
void publishTxnRecord(txHash, harden({ status }));
publishTxnRecord(txHash, harden({ status }));
}
};

Expand All @@ -200,7 +197,7 @@ export const prepareStatusManager = (
];
const txpost = { ...tx, status };
pendingSettleTxs.set(key, harden([...prefix, txpost, ...suffix]));
void publishTxnRecord(tx.txHash, harden({ status }));
publishTxnRecord(tx.txHash, harden({ status }));
}

return zone.exo(
Expand Down Expand Up @@ -288,7 +285,7 @@ export const prepareStatusManager = (
* @param {boolean} success whether the Transfer succeeded
*/
advanceOutcomeForMintedEarly(txHash, success) {
void publishTxnRecord(
publishTxnRecord(
txHash,
harden({
status: success
Expand Down Expand Up @@ -381,10 +378,7 @@ export const prepareStatusManager = (
* @param {import('./liquidity-pool.js').RepayAmountKWR} split
*/
disbursed(txHash, split) {
void publishTxnRecord(
txHash,
harden({ split, status: TxStatus.Disbursed }),
);
publishTxnRecord(txHash, harden({ split, status: TxStatus.Disbursed }));
},

/**
Expand All @@ -394,7 +388,7 @@ export const prepareStatusManager = (
* @param {boolean} success
*/
forwarded(txHash, success) {
void publishTxnRecord(
publishTxnRecord(
txHash,
harden({
status: success ? TxStatus.Forwarded : TxStatus.ForwardFailed,
Expand Down
95 changes: 56 additions & 39 deletions packages/fast-usdc/src/exos/transaction-feed.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/** @file Exo for @see {prepareTransactionFeedKit} */
import { makeTracer } from '@agoric/internal';
import { prepareDurablePublishKit } from '@agoric/notifier';
import { keyEQ, M } from '@endo/patterns';
import { Fail, quote } from '@endo/errors';
import { keyEQ, M } from '@endo/patterns';
import { CctpTxEvidenceShape, RiskAssessmentShape } from '../type-guards.js';
import { defineInertInvitation } from '../utils/zoe.js';
import { prepareOperatorKit } from './operator-kit.js';
Expand Down Expand Up @@ -64,8 +65,14 @@ export const stateShape = {
pending: M.remotable(),
risks: M.remotable(),
};
harden(stateShape);

/**
* A TransactionFeed is responsible for finding quorum among oracles.
*
* It receives attestations, records their evidence, and when enough oracles
* agree, publishes the results for the advancer to act on.
*
* @param {Zone} zone
* @param {ZCF} zcf
*/
Expand Down Expand Up @@ -148,18 +155,20 @@ export const prepareTransactionFeedKit = (zone, zcf) => {

/** @param {string} operatorId */
removeOperator(operatorId) {
const { operators } = this.state;
const { operators, pending, risks } = this.state;
trace('removeOperator', operatorId);
const operatorKit = operators.get(operatorId);
operatorKit.admin.disable();
operators.delete(operatorId);
pending.delete(operatorId);
risks.delete(operatorId);
Comment on lines +163 to +164
Copy link
Member

Choose a reason for hiding this comment

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

I suppose this change is covered by the revised remove operator test, though I haven't studied it well enough to be certain.

},
},
operatorPowers: {
/**
* Add evidence from an operator.
*
* NB: the operatorKit is responsible for
* NB: the operatorKit is responsible for revoking access.
*
* @param {CctpTxEvidence} evidence
* @param {RiskAssessment} riskAssessment
Expand All @@ -169,10 +178,6 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
const { operators, pending, risks } = this.state;
trace('attest', operatorId, evidence);

// TODO https://github.com/Agoric/agoric-sdk/pull/10720
// TODO validate that it's a valid for Fast USDC before accepting
// E.g. that the `recipientAddress` is the FU settlement account and that
// the EUD is a chain supported by FU.
const { txHash } = evidence;

// accept the evidence
Expand All @@ -192,6 +197,29 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
const found = [...pending.values()].filter(store =>
store.has(txHash),
);

{
let lastEvidence;
for (const store of found) {
const next = store.get(txHash);
if (lastEvidence && !keyEQ(lastEvidence, next)) {
// Ignore conflicting evidence, but treat it as an error
// because it should never happen and needs to be prevented
// from happening again.
trace(
'🚨 conflicting evidence for',
txHash,
':',
lastEvidence,
'!=',
next,
);
Fail`conflicting evidence for ${quote(txHash)}`;
}
lastEvidence = next;
}
}

const minAttestations = Math.ceil(operators.getSize() / 2);
trace(
'transaction',
Expand All @@ -202,48 +230,37 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
minAttestations,
'necessary attestations',
);

if (found.length < minAttestations) {
// wait for more
return;
}

let lastEvidence;
for (const store of found) {
const next = store.get(txHash);
if (lastEvidence && !keyEQ(lastEvidence, next)) {
// Ignore conflicting evidence, but treat it as an error
// because it should never happen and needs to be prevented
// from happening again.
trace(
'🚨 conflicting evidence for',
txHash,
':',
lastEvidence,
'!=',
next,
);
Fail`conflicting evidence for ${quote(txHash)}`;
}
lastEvidence = next;
}

const riskStores = [...risks.values()].filter(store =>
store.has(txHash),
);
// take the union of risks identified from all operators
const risksIdentified = allRisksIdentified(riskStores, txHash);

// sufficient agreement, so remove from pending risks, then publish
for (const store of found) {
store.delete(txHash);
// Publish at the threshold of agreement
if (found.length === minAttestations) {
// take the union of risks identified from all operators
const risksIdentified = allRisksIdentified(riskStores, txHash);
trace('publishing evidence', evidence, risksIdentified);
publisher.publish({
evidence,
risk: { risksIdentified },
});
return;
}
for (const store of riskStores) {
store.delete(txHash);

if (found.length === pending.getSize()) {
// all have reported so clean up
Copy link
Member

Choose a reason for hiding this comment

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

so clean-up depends on availability of all operators.

doesn't seem ideal, but seems workable for the coming release.

for (const store of found) {
store.delete(txHash);
}
for (const store of riskStores) {
store.delete(txHash);
}
}
trace('publishing evidence', evidence, risksIdentified);
publisher.publish({
evidence,
risk: { risksIdentified },
});
},
},
public: {
Expand Down
9 changes: 5 additions & 4 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ harden(meta);
* @param {ERef<Marshaller>} marshaller
* @param {FeeConfig} feeConfig
*/
const publishFeeConfig = async (node, marshaller, feeConfig) => {
const publishFeeConfig = (node, marshaller, feeConfig) => {
const feeNode = E(node).makeChildNode(FEE_NODE);
const value = await E(marshaller).toCapData(feeConfig);
return E(feeNode).setValue(JSON.stringify(value));
void E.when(E(marshaller).toCapData(feeConfig), value =>
E(feeNode).setValue(JSON.stringify(value)),
);
};

/**
Expand Down Expand Up @@ -247,7 +248,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
// So we use zone.exoClassKit above to define the liquidity pool kind
// and pass the shareMint into the maker / init function.

void publishFeeConfig(storageNode, marshaller, feeConfig);
publishFeeConfig(storageNode, marshaller, feeConfig);

const shareMint = await provideSingleton(
zone.mapStore('mint'),
Expand Down
Loading
Loading