@openzeppelin/miden-multisig-client 0.18.0-rc.1 → 0.18.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -14
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/multisig/authArgErrors.d.ts +15 -1
- package/dist/multisig/authArgErrors.d.ts.map +1 -1
- package/dist/multisig/authArgErrors.js +20 -0
- package/dist/multisig/authArgErrors.js.map +1 -1
- package/dist/multisig.d.ts +36 -16
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +109 -58
- package/dist/multisig.js.map +1 -1
- package/dist/prover/workflow.d.ts +7 -3
- package/dist/prover/workflow.d.ts.map +1 -1
- package/dist/prover/workflow.js +7 -5
- package/dist/prover/workflow.js.map +1 -1
- package/dist/transaction/summary.d.ts +92 -14
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +93 -3
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction.d.ts +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/dist/types/proposal.d.ts +9 -8
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +3 -0
- package/src/multisig/authArgErrors.ts +28 -1
- package/src/multisig.ts +127 -65
- package/src/prover/workflow.ts +7 -10
- package/src/transaction/summary.ts +161 -16
- package/src/transaction.ts +6 -0
- package/src/types/proposal.ts +9 -8
package/src/multisig.ts
CHANGED
|
@@ -44,7 +44,10 @@ import {
|
|
|
44
44
|
chainAnchorFromBase64,
|
|
45
45
|
chainAnchorToBase64,
|
|
46
46
|
executeForSummary,
|
|
47
|
-
|
|
47
|
+
executeForSummaryAtTip,
|
|
48
|
+
isStaleChainError,
|
|
49
|
+
prepareTipExecution,
|
|
50
|
+
syncToBoundBlock,
|
|
48
51
|
summaryApprovalExpirationBlockNum,
|
|
49
52
|
summarySalt,
|
|
50
53
|
buildUpdateSignersTransactionRequest,
|
|
@@ -157,7 +160,7 @@ export interface CreateProposalOptions {
|
|
|
157
160
|
/** Proposal nonce; defaults to `Date.now()`. */
|
|
158
161
|
nonce?: number;
|
|
159
162
|
/**
|
|
160
|
-
* Blocks after the proposal
|
|
163
|
+
* Blocks after the block the proposal binds by which the transaction must be
|
|
161
164
|
* included; past that the approvers' signatures no longer authorize it. The
|
|
162
165
|
* summary binds it, so the executing party can neither shorten nor extend it.
|
|
163
166
|
* Omitted, the approval never expires (the upstream default).
|
|
@@ -808,11 +811,12 @@ export class Multisig {
|
|
|
808
811
|
* Every synced proposal's metadata is checked against its signed summary
|
|
809
812
|
* and the outcome is recorded in {@link Proposal.verification}. One that
|
|
810
813
|
* fails is still cached and returned, so a single stale or corrupt
|
|
811
|
-
* proposal cannot hide the others (issue #462
|
|
812
|
-
* proposal
|
|
813
|
-
* with `retryable: true` means a transient node error
|
|
814
|
-
*
|
|
815
|
-
*
|
|
814
|
+
* proposal cannot hide the others (issue #462). Verification re-executes
|
|
815
|
+
* each proposal at the chain tip, so the Miden client is synced once first.
|
|
816
|
+
* `failed` with `retryable: true` means a transient node error or chain
|
|
817
|
+
* state this client had not caught up with, worth syncing again;
|
|
818
|
+
* `retryable: false` means the proposal cannot be reproduced and must be
|
|
819
|
+
* re-proposed. `signProposal` and `executeProposal` re-verify and
|
|
816
820
|
* refuse a failed proposal. A failed proposal still counts as reported, so
|
|
817
821
|
* it is pruned like any other once GUARDIAN stops listing it.
|
|
818
822
|
*
|
|
@@ -852,6 +856,14 @@ export class Multisig {
|
|
|
852
856
|
const factory = this.proposalFactory();
|
|
853
857
|
|
|
854
858
|
const reported = new Map<string, { delta: (typeof deltas)[number]; verified: Proposal }>();
|
|
859
|
+
if (deltas.length > 0) {
|
|
860
|
+
// Verification re-executes at the store's sync height; bring it to the
|
|
861
|
+
// tip once for the whole listing. Best effort: a node outage then shows
|
|
862
|
+
// up on each proposal as a retryable failure instead of failing the sync.
|
|
863
|
+
await this.syncChain().catch((error) =>
|
|
864
|
+
console.warn('Could not sync the Miden client before verifying proposals', error),
|
|
865
|
+
);
|
|
866
|
+
}
|
|
855
867
|
for (const delta of deltas) {
|
|
856
868
|
const proposalId = normalizeHexWord(
|
|
857
869
|
computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data)
|
|
@@ -1972,6 +1984,8 @@ export class Multisig {
|
|
|
1972
1984
|
|
|
1973
1985
|
async signProposal(proposalId: string): Promise<Proposal> {
|
|
1974
1986
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1987
|
+
// Verification re-executes the proposal at the store's sync height.
|
|
1988
|
+
await this.syncChain();
|
|
1975
1989
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
1976
1990
|
if (!existingProposal) {
|
|
1977
1991
|
throw new Error(`Proposal not found: ${proposalId}`);
|
|
@@ -2019,9 +2033,14 @@ export class Multisig {
|
|
|
2019
2033
|
return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
2020
2034
|
}
|
|
2021
2035
|
|
|
2036
|
+
/**
|
|
2037
|
+
* Builds the final, fully signed request for a ready proposal, for a caller
|
|
2038
|
+
* that proves and submits it with its own pipeline. The request declares the
|
|
2039
|
+
* block its summary binds, so execute it at the chain tip, without an
|
|
2040
|
+
* anchor, on a client synced to at least that block.
|
|
2041
|
+
*/
|
|
2022
2042
|
async createTransactionProposalRequest(proposalId: string): Promise<TransactionRequest> {
|
|
2023
|
-
const { finalRequest
|
|
2024
|
-
anchor.free();
|
|
2043
|
+
const { finalRequest } = await this.prepareProposalExecution(proposalId);
|
|
2025
2044
|
return finalRequest;
|
|
2026
2045
|
}
|
|
2027
2046
|
|
|
@@ -2031,25 +2050,19 @@ export class Multisig {
|
|
|
2031
2050
|
* @param proposalId - The proposal commitment/ID
|
|
2032
2051
|
*/
|
|
2033
2052
|
async executeProposal(proposalId: string): Promise<void> {
|
|
2034
|
-
const { metadata, finalRequest, proposal
|
|
2035
|
-
await this.prepareProposalExecution(proposalId);
|
|
2036
|
-
|
|
2037
|
-
try {
|
|
2038
|
-
if (metadata.proposalType === 'switch_guardian') {
|
|
2039
|
-
// #417: import notes embedded in pending proposals from the old
|
|
2040
|
-
// GUARDIAN. Must run before the switch executes and repoints;
|
|
2041
|
-
// best-effort and bounded — see preservePreSwitchProposalNotes.
|
|
2042
|
-
await this.preservePreSwitchProposalNotes();
|
|
2043
|
-
}
|
|
2053
|
+
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
2044
2054
|
|
|
2045
|
-
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
anchor.free();
|
|
2055
|
+
if (metadata.proposalType === 'switch_guardian') {
|
|
2056
|
+
// #417: import notes embedded in pending proposals from the old
|
|
2057
|
+
// GUARDIAN. Must run before the switch executes and repoints;
|
|
2058
|
+
// best-effort and bounded — see preservePreSwitchProposalNotes.
|
|
2059
|
+
await this.preservePreSwitchProposalNotes();
|
|
2051
2060
|
}
|
|
2052
2061
|
|
|
2062
|
+
// Execute at the chain tip. The request declares the block the signed
|
|
2063
|
+
// summary binds, so the summary the cosigners signed reproduces there.
|
|
2064
|
+
await this.submitAtTip(finalRequest);
|
|
2065
|
+
|
|
2053
2066
|
if (metadata.proposalType === 'switch_guardian') {
|
|
2054
2067
|
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
2055
2068
|
throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
|
|
@@ -2109,11 +2122,12 @@ export class Multisig {
|
|
|
2109
2122
|
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
2110
2123
|
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
2111
2124
|
* after `prepareCustomExecution` rebuilds its request with the returned advice.
|
|
2112
|
-
* The transaction is executed at the
|
|
2113
|
-
*
|
|
2125
|
+
* The transaction is executed at the chain tip, so the request has to declare
|
|
2126
|
+
* the block its auth args bind (`feeAwareTransactionRequestBuilder` does).
|
|
2114
2127
|
*/
|
|
2115
2128
|
async submitTransaction(proposalId: string, request: TransactionRequest): Promise<void> {
|
|
2116
2129
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
2130
|
+
await this.syncChain();
|
|
2117
2131
|
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
2118
2132
|
const existing = this.getLocalProposal(proposalId);
|
|
2119
2133
|
const proposal = this.proposalFactory().fromDelta(
|
|
@@ -2135,11 +2149,40 @@ export class Multisig {
|
|
|
2135
2149
|
`Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`,
|
|
2136
2150
|
);
|
|
2137
2151
|
}
|
|
2138
|
-
|
|
2139
|
-
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
|
|
2140
2152
|
} finally {
|
|
2141
2153
|
anchor.free();
|
|
2142
2154
|
}
|
|
2155
|
+
await this.submitAtTip(request);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/**
|
|
2159
|
+
* Executes `request` at the chain tip, then proves, submits and applies it,
|
|
2160
|
+
* syncing first when this client is still below the block the request binds.
|
|
2161
|
+
*/
|
|
2162
|
+
private async submitAtTip(request: TransactionRequest): Promise<void> {
|
|
2163
|
+
const webClient = await this.getRawClient();
|
|
2164
|
+
await prepareTipExecution(webClient, request, () =>
|
|
2165
|
+
retryRpcRead(() => webClient.syncState(), this.rpcConfig),
|
|
2166
|
+
);
|
|
2167
|
+
await this.proverWorkflow.submit(AccountId.fromHex(this._accountId), request);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* Brings the Miden client to the chain tip before a proposal is re-executed:
|
|
2172
|
+
* an execution loads foreign accounts, the fee faucet among them, at the
|
|
2173
|
+
* store's sync height, which a node prunes about 50 blocks later.
|
|
2174
|
+
*/
|
|
2175
|
+
private async syncChain(): Promise<void> {
|
|
2176
|
+
const webClient = await this.getRawClient();
|
|
2177
|
+
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
/** {@link syncToBoundBlock} with this client's RPC retry policy. */
|
|
2181
|
+
private async syncToBoundBlock(boundBlockNum: number): Promise<void> {
|
|
2182
|
+
const webClient = await this.getRawClient();
|
|
2183
|
+
await syncToBoundBlock(webClient, boundBlockNum, () =>
|
|
2184
|
+
retryRpcRead(() => webClient.syncState(), this.rpcConfig),
|
|
2185
|
+
);
|
|
2143
2186
|
}
|
|
2144
2187
|
|
|
2145
2188
|
/**
|
|
@@ -2203,6 +2246,8 @@ export class Multisig {
|
|
|
2203
2246
|
transactionRequestBytes: Uint8Array,
|
|
2204
2247
|
): Promise<AdviceMap> {
|
|
2205
2248
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
2249
|
+
// The binding probe re-executes the producer's request at the store's sync height.
|
|
2250
|
+
await this.syncChain();
|
|
2206
2251
|
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
2207
2252
|
const existing = this.getLocalProposal(proposalId);
|
|
2208
2253
|
const proposal = this.proposalFactory().fromDelta(
|
|
@@ -2237,13 +2282,11 @@ export class Multisig {
|
|
|
2237
2282
|
|
|
2238
2283
|
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
2239
2284
|
|
|
2240
|
-
//
|
|
2241
|
-
//
|
|
2242
|
-
//
|
|
2243
|
-
//
|
|
2244
|
-
// before executing against it.
|
|
2285
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so its block
|
|
2286
|
+
// commitment is checked against the signed summary: it has to name the
|
|
2287
|
+
// block the summary binds. The probe itself runs at the chain tip, where
|
|
2288
|
+
// the request's declared bound block reproduces the signed summary.
|
|
2245
2289
|
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
2246
|
-
let derivedCommitmentHex: string;
|
|
2247
2290
|
try {
|
|
2248
2291
|
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
2249
2292
|
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
@@ -2252,12 +2295,12 @@ export class Multisig {
|
|
|
2252
2295
|
`Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`,
|
|
2253
2296
|
);
|
|
2254
2297
|
}
|
|
2255
|
-
const webClient = await this.getRawClient();
|
|
2256
|
-
const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
|
|
2257
|
-
derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
2258
2298
|
} finally {
|
|
2259
2299
|
anchor.free();
|
|
2260
2300
|
}
|
|
2301
|
+
const webClient = await this.getRawClient();
|
|
2302
|
+
const derived = await executeForSummaryAtTip(webClient, this._accountId, bindingRequest);
|
|
2303
|
+
const derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
2261
2304
|
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
2262
2305
|
throw new Error(
|
|
2263
2306
|
`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`,
|
|
@@ -2396,14 +2439,13 @@ export class Multisig {
|
|
|
2396
2439
|
}
|
|
2397
2440
|
|
|
2398
2441
|
/**
|
|
2399
|
-
* The returned
|
|
2400
|
-
*
|
|
2442
|
+
* The returned request declares the block the signed summary binds, and
|
|
2443
|
+
* executes at the chain tip.
|
|
2401
2444
|
*/
|
|
2402
2445
|
private async prepareProposalExecution(proposalId: string): Promise<{
|
|
2403
2446
|
finalRequest: TransactionRequest;
|
|
2404
2447
|
metadata: ProposalMetadata;
|
|
2405
2448
|
proposal: Proposal;
|
|
2406
|
-
anchor: ChainAnchor;
|
|
2407
2449
|
}> {
|
|
2408
2450
|
const proposal = this.getLocalProposal(proposalId);
|
|
2409
2451
|
if (!proposal) {
|
|
@@ -2411,6 +2453,8 @@ export class Multisig {
|
|
|
2411
2453
|
}
|
|
2412
2454
|
|
|
2413
2455
|
this.proposalFactory().assertAccountId(proposal.accountId);
|
|
2456
|
+
// Verification and execution run at the store's sync height.
|
|
2457
|
+
await this.syncChain();
|
|
2414
2458
|
await this.verifyProposalMetadataBinding(proposal);
|
|
2415
2459
|
|
|
2416
2460
|
const metadata = proposal.metadata;
|
|
@@ -2560,17 +2604,21 @@ export class Multisig {
|
|
|
2560
2604
|
}
|
|
2561
2605
|
|
|
2562
2606
|
const anchor = this.requireProposalAnchor(proposalId, metadata);
|
|
2607
|
+
let binding: ProposalRequestBinding;
|
|
2563
2608
|
try {
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
proposalRequestBinding(txSummary, anchor, saltHex),
|
|
2567
|
-
adviceMap,
|
|
2568
|
-
);
|
|
2569
|
-
return { finalRequest, metadata, proposal, anchor };
|
|
2570
|
-
} catch (error) {
|
|
2609
|
+
binding = proposalRequestBinding(txSummary, anchor, saltHex);
|
|
2610
|
+
} finally {
|
|
2571
2611
|
anchor.free();
|
|
2572
|
-
throw error;
|
|
2573
2612
|
}
|
|
2613
|
+
// A switch_guardian proposal is verified without a rebuild, so this may be
|
|
2614
|
+
// the first time this client needs the chain at the bound block.
|
|
2615
|
+
await this.syncToBoundBlock(binding.boundBlockNum);
|
|
2616
|
+
const finalRequest = await this.buildTransactionRequestFromMetadata(
|
|
2617
|
+
metadata,
|
|
2618
|
+
binding,
|
|
2619
|
+
adviceMap,
|
|
2620
|
+
);
|
|
2621
|
+
return { finalRequest, metadata, proposal };
|
|
2574
2622
|
}
|
|
2575
2623
|
|
|
2576
2624
|
/**
|
|
@@ -2655,6 +2703,10 @@ export class Multisig {
|
|
|
2655
2703
|
|
|
2656
2704
|
const proposal = this.proposalFactory().fromExported(exported);
|
|
2657
2705
|
|
|
2706
|
+
// Verification re-executes every built-in type at the store's sync height.
|
|
2707
|
+
if (proposal.metadata.proposalType !== 'custom') {
|
|
2708
|
+
await this.syncChain();
|
|
2709
|
+
}
|
|
2658
2710
|
await this.verifyProposalMetadataBinding(proposal);
|
|
2659
2711
|
this.proposals.set(proposal.id, proposal);
|
|
2660
2712
|
|
|
@@ -2695,6 +2747,10 @@ export class Multisig {
|
|
|
2695
2747
|
throw new Error('You have already signed this proposal');
|
|
2696
2748
|
}
|
|
2697
2749
|
|
|
2750
|
+
// Verification re-executes every built-in type at the store's sync height.
|
|
2751
|
+
if (proposal.metadata?.proposalType !== 'custom') {
|
|
2752
|
+
await this.syncChain();
|
|
2753
|
+
}
|
|
2698
2754
|
const commitmentToSign = await this.verifyProposalMetadataBinding(proposal);
|
|
2699
2755
|
|
|
2700
2756
|
// Sign the commitment
|
|
@@ -2758,7 +2814,7 @@ export class Multisig {
|
|
|
2758
2814
|
} catch (error) {
|
|
2759
2815
|
proposal.verification = {
|
|
2760
2816
|
status: 'failed',
|
|
2761
|
-
retryable: isTransientRpcError(error),
|
|
2817
|
+
retryable: isTransientRpcError(error) || isStaleChainError(error),
|
|
2762
2818
|
message: error instanceof Error ? error.message : String(error),
|
|
2763
2819
|
};
|
|
2764
2820
|
throw error;
|
|
@@ -2770,10 +2826,11 @@ export class Multisig {
|
|
|
2770
2826
|
|
|
2771
2827
|
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
2772
2828
|
|
|
2773
|
-
// The anchor arrives from an untrusted party via GUARDIAN, so check
|
|
2774
|
-
//
|
|
2775
|
-
//
|
|
2776
|
-
//
|
|
2829
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so check that it
|
|
2830
|
+
// names the block the signed summary binds: the rebuild below binds the
|
|
2831
|
+
// block it names. Nothing executes against it; the rebuild runs at the
|
|
2832
|
+
// chain tip. `ChainAnchor.deserialize` already enforced internal
|
|
2833
|
+
// header/chain consistency.
|
|
2777
2834
|
const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
|
|
2778
2835
|
try {
|
|
2779
2836
|
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
@@ -2812,6 +2869,13 @@ export class Multisig {
|
|
|
2812
2869
|
return txSummaryCommitment;
|
|
2813
2870
|
}
|
|
2814
2871
|
|
|
2872
|
+
// The rebuild reads the chain's fee faucet from the synced protocol
|
|
2873
|
+
// configuration and executes at the tip, so the store has to have synced
|
|
2874
|
+
// to the block the summary binds; a cosigner that has only just loaded
|
|
2875
|
+
// the account has not.
|
|
2876
|
+
await this.syncToBoundBlock(binding.boundBlockNum);
|
|
2877
|
+
const webClient = await this.getRawClient();
|
|
2878
|
+
|
|
2815
2879
|
// A consume-notes summary commits to *authenticated* consumption (see
|
|
2816
2880
|
// ensureNotesAuthenticated), which miden-client decides from this store
|
|
2817
2881
|
// alone. Put the store in that mode before the rebuild, or a cosigner
|
|
@@ -2824,8 +2888,7 @@ export class Multisig {
|
|
|
2824
2888
|
}
|
|
2825
2889
|
|
|
2826
2890
|
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, binding);
|
|
2827
|
-
const
|
|
2828
|
-
const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
|
|
2891
|
+
const reconstructed = await executeForSummaryAtTip(webClient, this._accountId, request);
|
|
2829
2892
|
const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
|
|
2830
2893
|
|
|
2831
2894
|
if (reconstructedCommitment !== txSummaryCommitment) {
|
|
@@ -2838,12 +2901,6 @@ export class Multisig {
|
|
|
2838
2901
|
}
|
|
2839
2902
|
}
|
|
2840
2903
|
|
|
2841
|
-
/**
|
|
2842
|
-
* Decodes a proposal's chain anchor. Throws when absent: a proposal without
|
|
2843
|
-
* an anchor was created at an unknown reference block, so its signed summary
|
|
2844
|
-
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
2845
|
-
* anchor and must `free()` it once done.
|
|
2846
|
-
*/
|
|
2847
2904
|
/**
|
|
2848
2905
|
* Reads a proposal's salt. Throws when absent, because there is nothing to fall
|
|
2849
2906
|
* back to.
|
|
@@ -2892,12 +2949,17 @@ export class Multisig {
|
|
|
2892
2949
|
return saltHex;
|
|
2893
2950
|
}
|
|
2894
2951
|
|
|
2952
|
+
/**
|
|
2953
|
+
* Decodes a proposal's chain anchor, which names the block its signed summary
|
|
2954
|
+
* binds. Throws when absent: every proposal carries one, so a proposal
|
|
2955
|
+
* without it is malformed and is neither verified nor executed. The caller
|
|
2956
|
+
* owns the returned anchor and must `free()` it once done.
|
|
2957
|
+
*/
|
|
2895
2958
|
private requireProposalAnchor(proposalId: string, metadata: ProposalMetadata): ChainAnchor {
|
|
2896
2959
|
if (!metadata.chainAnchor) {
|
|
2897
2960
|
throw new Error(
|
|
2898
|
-
`Proposal ${proposalId} has no chain anchor
|
|
2899
|
-
'
|
|
2900
|
-
'at the original reference block',
|
|
2961
|
+
`Proposal ${proposalId} has no chain anchor, which names the block its signed ` +
|
|
2962
|
+
'summary binds; it cannot be verified or executed',
|
|
2901
2963
|
);
|
|
2902
2964
|
}
|
|
2903
2965
|
return chainAnchorFromBase64(metadata.chainAnchor);
|
package/src/prover/workflow.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AccountId,
|
|
3
|
-
ChainAnchor,
|
|
4
3
|
MidenClient,
|
|
5
4
|
TransactionRequest,
|
|
6
5
|
} from '@miden-sdk/miden-sdk';
|
|
@@ -15,15 +14,13 @@ export class ProverWorkflow {
|
|
|
15
14
|
private readonly runtime?: RetryRuntime,
|
|
16
15
|
) {}
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
const execution = await this.client.transactions.executeRequest(accountId, request
|
|
25
|
-
anchor,
|
|
26
|
-
});
|
|
17
|
+
/**
|
|
18
|
+
* Executes a request at the chain tip, then proves, submits, and applies it.
|
|
19
|
+
* A multisig proposal's request declares the block its summary binds, so the
|
|
20
|
+
* signed summary reproduces at the tip.
|
|
21
|
+
*/
|
|
22
|
+
async submit(accountId: AccountId, request: TransactionRequest): Promise<void> {
|
|
23
|
+
const execution = await this.client.transactions.executeRequest(accountId, request);
|
|
27
24
|
const proof = await proveWithRetry(execution, this.config, this.runtime);
|
|
28
25
|
const submission = await proof.submit();
|
|
29
26
|
await submission.apply();
|
|
@@ -5,8 +5,10 @@ import type {
|
|
|
5
5
|
WasmWebClient,
|
|
6
6
|
} from '@miden-sdk/miden-sdk';
|
|
7
7
|
import { AccountId, ChainAnchor, Word } from '@miden-sdk/miden-sdk';
|
|
8
|
+
import { BoundBlockNotDeclaredError } from '../multisig/authArgErrors.js';
|
|
8
9
|
import { getRawMidenClient } from '../raw-client.js';
|
|
9
10
|
import { base64ToUint8Array, normalizeHexWord, uint8ArrayToBase64 } from '../utils/encoding.js';
|
|
11
|
+
import { requestBoundBlockNum } from './authArgs.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Layout of the six user params a multisig auth component binds into the
|
|
@@ -37,17 +39,51 @@ export class SummaryAnchorMismatchError extends Error {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
* The Miden client synced and its node still has not produced the block a
|
|
43
|
+
* proposal binds, so the proposal cannot execute at this client's tip yet.
|
|
44
|
+
* Worth retrying once the node catches up.
|
|
45
|
+
*/
|
|
46
|
+
export class ChainBehindBoundBlockError extends Error {
|
|
47
|
+
readonly retryable = true;
|
|
48
|
+
readonly syncHeight: number;
|
|
49
|
+
readonly boundBlockNum: number;
|
|
50
|
+
|
|
51
|
+
constructor(details: { syncHeight: number; boundBlockNum: number }) {
|
|
52
|
+
super(
|
|
53
|
+
`the Miden client synced to block ${details.syncHeight}, below block ` +
|
|
54
|
+
`${details.boundBlockNum} the proposal binds; its node has not reached that block yet`,
|
|
55
|
+
);
|
|
56
|
+
this.name = 'ChainBehindBoundBlockError';
|
|
57
|
+
this.syncHeight = details.syncHeight;
|
|
58
|
+
this.boundBlockNum = details.boundBlockNum;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a failed re-execution came from chain state this client can catch up
|
|
64
|
+
* with rather than from the proposal itself: a node that has not reached the
|
|
65
|
+
* bound block yet, or account state the node pruned because this client had
|
|
66
|
+
* not synced recently. Either clears on a later attempt, which syncs first.
|
|
67
|
+
*/
|
|
68
|
+
export function isStaleChainError(error: unknown): boolean {
|
|
69
|
+
if (error instanceof ChainBehindBoundBlockError) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
return message.includes('has been pruned');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Derives the summary awaiting authorization for a proposal the caller is
|
|
78
|
+
* creating now, and captures a `ChainAnchor` at the current sync height to ship
|
|
79
|
+
* with it.
|
|
46
80
|
*
|
|
47
|
-
* The
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
81
|
+
* The summary is derived at the chain tip, like every other execution of a
|
|
82
|
+
* multisig proposal (see {@link executeForSummaryAtTip}). The anchor still
|
|
83
|
+
* travels in the proposal: it names the block the request's auth args bind,
|
|
84
|
+
* which is how a rebuild learns that block, and 0.18.0-rc.1 clients re-execute
|
|
85
|
+
* at it. A proposer builds at the sync height the anchor is captured at, and
|
|
86
|
+
* the check below is what makes that hold.
|
|
51
87
|
*/
|
|
52
88
|
export function executeForSummary(
|
|
53
89
|
client: MidenClient,
|
|
@@ -67,12 +103,11 @@ export async function executeForSummary(
|
|
|
67
103
|
txRequest: TransactionRequest,
|
|
68
104
|
midenRpcEndpoint?: string,
|
|
69
105
|
): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }> {
|
|
70
|
-
const acc = AccountId.fromHex(accountId);
|
|
71
106
|
const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
|
|
72
107
|
const anchor = await rawClient.chainAnchorForRequest(txRequest);
|
|
73
108
|
let summary: TransactionSummary;
|
|
74
109
|
try {
|
|
75
|
-
summary = await rawClient
|
|
110
|
+
summary = await executeForSummaryAtTip(rawClient, accountId, txRequest);
|
|
76
111
|
} catch (error) {
|
|
77
112
|
anchor.free();
|
|
78
113
|
throw error;
|
|
@@ -91,11 +126,120 @@ export async function executeForSummary(
|
|
|
91
126
|
return { summary, anchor };
|
|
92
127
|
}
|
|
93
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Executes a multisig request at the chain tip to obtain the summary awaiting
|
|
131
|
+
* authorization. This is how cosigners and the executor reproduce a proposal's
|
|
132
|
+
* summary, whatever block they have synced to.
|
|
133
|
+
*
|
|
134
|
+
* Since protocol 0.17 a multisig summary binds the block its auth args name
|
|
135
|
+
* (the bound block), not the block the transaction executes against, so it
|
|
136
|
+
* reproduces at any later tip once the bound block is in the transaction's
|
|
137
|
+
* partial blockchain. The request declares it through `withBlockNumbers`, and
|
|
138
|
+
* foreign accounts, the fee faucet among them, load at the tip. Re-executing at
|
|
139
|
+
* the proposal's anchor instead loads them at the bound block, which a node
|
|
140
|
+
* prunes about 50 blocks later (issue #462).
|
|
141
|
+
*
|
|
142
|
+
* The client has to have synced to at least the bound block. When it has not,
|
|
143
|
+
* this syncs once before executing.
|
|
144
|
+
*
|
|
145
|
+
* @throws BoundBlockNotDeclaredError when the request binds a block in its
|
|
146
|
+
* multisig auth args without declaring it.
|
|
147
|
+
*/
|
|
148
|
+
export function executeForSummaryAtTip(
|
|
149
|
+
client: MidenClient,
|
|
150
|
+
accountId: string,
|
|
151
|
+
txRequest: TransactionRequest,
|
|
152
|
+
midenRpcEndpoint: string,
|
|
153
|
+
): Promise<TransactionSummary>;
|
|
154
|
+
export function executeForSummaryAtTip(
|
|
155
|
+
client: WasmWebClient,
|
|
156
|
+
accountId: string,
|
|
157
|
+
txRequest: TransactionRequest,
|
|
158
|
+
midenRpcEndpoint?: string,
|
|
159
|
+
): Promise<TransactionSummary>;
|
|
160
|
+
export async function executeForSummaryAtTip(
|
|
161
|
+
client: MidenClient | WasmWebClient,
|
|
162
|
+
accountId: string,
|
|
163
|
+
txRequest: TransactionRequest,
|
|
164
|
+
midenRpcEndpoint?: string,
|
|
165
|
+
): Promise<TransactionSummary> {
|
|
166
|
+
const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
|
|
167
|
+
await prepareTipExecution(rawClient, txRequest);
|
|
168
|
+
return rawClient.executeForSummary(AccountId.fromHex(accountId), txRequest);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Gets `client` ready to execute `request` at the chain tip: checks the
|
|
173
|
+
* request declares the block its multisig auth args bind, and syncs to that
|
|
174
|
+
* block (see {@link syncToBoundBlock}).
|
|
175
|
+
*
|
|
176
|
+
* @throws BoundBlockNotDeclaredError when the request binds a block in its
|
|
177
|
+
* multisig auth args without declaring it.
|
|
178
|
+
*/
|
|
179
|
+
export async function prepareTipExecution(
|
|
180
|
+
client: WasmWebClient,
|
|
181
|
+
request: TransactionRequest,
|
|
182
|
+
syncState?: () => Promise<unknown>,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
const boundBlockNum = requireDeclaredBoundBlock(request);
|
|
185
|
+
if (boundBlockNum !== undefined) {
|
|
186
|
+
await syncToBoundBlock(client, boundBlockNum, syncState);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The block `request`'s multisig auth args bind, after checking the request
|
|
192
|
+
* declares it. `undefined` for a request without multisig auth args, which
|
|
193
|
+
* has no bound block to declare.
|
|
194
|
+
*
|
|
195
|
+
* @throws BoundBlockNotDeclaredError when the block is bound but not declared.
|
|
196
|
+
*/
|
|
197
|
+
export function requireDeclaredBoundBlock(request: TransactionRequest): number | undefined {
|
|
198
|
+
const boundBlockNum = requestBoundBlockNum(request);
|
|
199
|
+
if (boundBlockNum !== undefined && !request.blockNumbers().includes(boundBlockNum)) {
|
|
200
|
+
throw new BoundBlockNotDeclaredError(boundBlockNum);
|
|
201
|
+
}
|
|
202
|
+
return boundBlockNum;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Syncs `client` once when its sync height is below `blockNum`, the block a
|
|
207
|
+
* proposal binds. Execution at a tip below it fails with "requested block N is
|
|
208
|
+
* after transaction reference block M", and a store that has never synced (a
|
|
209
|
+
* cosigner that has only just loaded the account) holds no header to rebuild
|
|
210
|
+
* the request from. `syncState` lets a caller wrap the sync in its own retry
|
|
211
|
+
* policy.
|
|
212
|
+
*
|
|
213
|
+
* This does not make a store that is already past the bound block current. An
|
|
214
|
+
* execution loads foreign accounts, the fee faucet among them, at the store's
|
|
215
|
+
* sync height, which a node prunes about 50 blocks later, so the multisig
|
|
216
|
+
* entry points that re-execute a proposal sync the chain first.
|
|
217
|
+
*
|
|
218
|
+
* @throws ChainBehindBoundBlockError when the node has not reached the block.
|
|
219
|
+
*/
|
|
220
|
+
export async function syncToBoundBlock(
|
|
221
|
+
client: WasmWebClient,
|
|
222
|
+
blockNum: number,
|
|
223
|
+
syncState: () => Promise<unknown> = () => client.syncState(),
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
if ((await client.getSyncHeight()) >= blockNum) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await syncState();
|
|
229
|
+
const syncHeight = await client.getSyncHeight();
|
|
230
|
+
if (syncHeight < blockNum) {
|
|
231
|
+
throw new ChainBehindBoundBlockError({ syncHeight, boundBlockNum: blockNum });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
94
235
|
/**
|
|
95
236
|
* Executes a transaction at the given `ChainAnchor`'s reference block to
|
|
96
|
-
* obtain the summary awaiting authorization
|
|
97
|
-
*
|
|
98
|
-
*
|
|
237
|
+
* obtain the summary awaiting authorization.
|
|
238
|
+
*
|
|
239
|
+
* For a summary that binds the reference block, such as a single-signature
|
|
240
|
+
* one. A multisig proposal's summary binds its bound block instead and is
|
|
241
|
+
* reproduced with {@link executeForSummaryAtTip}: re-executing it at an anchor
|
|
242
|
+
* fails once the node prunes the anchor block's account state.
|
|
99
243
|
*/
|
|
100
244
|
export function executeForSummaryAt(
|
|
101
245
|
client: MidenClient,
|
|
@@ -134,7 +278,8 @@ export function chainAnchorToBase64(anchor: ChainAnchor): string {
|
|
|
134
278
|
* Deserializes a `ChainAnchor` from its base64 wire form. `ChainAnchor`
|
|
135
279
|
* deserialization validates the header/chain consistency internally, so a
|
|
136
280
|
* decoded anchor only needs its block commitment checked against the signed
|
|
137
|
-
* transaction summary before it is
|
|
281
|
+
* transaction summary before the block it names is taken as the one the
|
|
282
|
+
* summary binds.
|
|
138
283
|
*/
|
|
139
284
|
export function chainAnchorFromBase64(anchorBase64: string): ChainAnchor {
|
|
140
285
|
return ChainAnchor.deserialize(base64ToUint8Array(anchorBase64));
|
package/src/transaction.ts
CHANGED
|
@@ -10,11 +10,17 @@ export {
|
|
|
10
10
|
buildConsumeNotesTransactionRequestFromNotes,
|
|
11
11
|
} from './transaction/consumeNotes.js';
|
|
12
12
|
export {
|
|
13
|
+
ChainBehindBoundBlockError,
|
|
13
14
|
chainAnchorBlockNum,
|
|
14
15
|
chainAnchorFromBase64,
|
|
15
16
|
chainAnchorToBase64,
|
|
16
17
|
executeForSummary,
|
|
17
18
|
executeForSummaryAt,
|
|
19
|
+
executeForSummaryAtTip,
|
|
20
|
+
prepareTipExecution,
|
|
21
|
+
isStaleChainError,
|
|
22
|
+
requireDeclaredBoundBlock,
|
|
23
|
+
syncToBoundBlock,
|
|
18
24
|
summaryApprovalExpirationBlockNum,
|
|
19
25
|
summarySalt,
|
|
20
26
|
SummaryAnchorMismatchError,
|