@openzeppelin/miden-multisig-client 0.16.1 → 0.17.0-rc.1
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 +192 -11
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +32 -25
- package/dist/account/builder.js.map +1 -1
- package/dist/account/builder.test.js +73 -21
- package/dist/account/builder.test.js.map +1 -1
- package/dist/account/layout.d.ts +31 -0
- package/dist/account/layout.d.ts.map +1 -0
- package/dist/account/layout.js +31 -0
- package/dist/account/layout.js.map +1 -0
- package/dist/account/masm/account-components/auth.d.ts +1 -4
- package/dist/account/masm/account-components/auth.d.ts.map +1 -1
- package/dist/account/masm/account-components/auth.js +36 -53
- package/dist/account/masm/account-components/auth.js.map +1 -1
- package/dist/account/masm/index.d.ts +0 -1
- package/dist/account/masm/index.d.ts.map +1 -1
- package/dist/account/masm/index.js +0 -1
- package/dist/account/masm/index.js.map +1 -1
- package/dist/account/storage.d.ts +4 -0
- package/dist/account/storage.d.ts.map +1 -1
- package/dist/account/storage.js +9 -20
- package/dist/account/storage.js.map +1 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -5
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +74 -16
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/inspector.d.ts +59 -1
- package/dist/inspector.d.ts.map +1 -1
- package/dist/inspector.js +157 -29
- package/dist/inspector.js.map +1 -1
- package/dist/inspector.test.js +246 -37
- package/dist/inspector.test.js.map +1 -1
- package/dist/multisig.d.ts +109 -33
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +307 -104
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +582 -58
- package/dist/multisig.test.js.map +1 -1
- package/dist/procedures.d.ts +6 -7
- package/dist/procedures.d.ts.map +1 -1
- package/dist/procedures.js +6 -7
- package/dist/procedures.js.map +1 -1
- package/dist/proposal/metadata.d.ts.map +1 -1
- package/dist/proposal/metadata.js +13 -2
- package/dist/proposal/metadata.js.map +1 -1
- package/dist/proposal/metadata.test.js +57 -0
- package/dist/proposal/metadata.test.js.map +1 -1
- package/dist/prover/errors.d.ts +5 -0
- package/dist/prover/errors.d.ts.map +1 -1
- package/dist/prover/errors.js +7 -155
- package/dist/prover/errors.js.map +1 -1
- package/dist/prover/errors.test.js +5 -0
- package/dist/prover/errors.test.js.map +1 -1
- package/dist/prover/retry.d.ts +1 -6
- package/dist/prover/retry.d.ts.map +1 -1
- package/dist/prover/retry.js +5 -28
- package/dist/prover/retry.js.map +1 -1
- package/dist/prover/retry.test.js +1 -1
- package/dist/prover/retry.test.js.map +1 -1
- package/dist/prover/workflow.d.ts +4 -3
- package/dist/prover/workflow.d.ts.map +1 -1
- package/dist/prover/workflow.js +5 -2
- package/dist/prover/workflow.js.map +1 -1
- package/dist/prover/workflow.test.js +28 -3
- package/dist/prover/workflow.test.js.map +1 -1
- package/dist/retry/classify.d.ts +16 -0
- package/dist/retry/classify.d.ts.map +1 -0
- package/dist/retry/classify.js +187 -0
- package/dist/retry/classify.js.map +1 -0
- package/dist/retry/runtime.d.ts +13 -0
- package/dist/retry/runtime.d.ts.map +1 -0
- package/dist/retry/runtime.js +33 -0
- package/dist/retry/runtime.js.map +1 -0
- package/dist/rpc/config.d.ts +11 -0
- package/dist/rpc/config.d.ts.map +1 -0
- package/dist/rpc/config.js +17 -0
- package/dist/rpc/config.js.map +1 -0
- package/dist/rpc/config.test.d.ts +2 -0
- package/dist/rpc/config.test.d.ts.map +1 -0
- package/dist/rpc/config.test.js +24 -0
- package/dist/rpc/config.test.js.map +1 -0
- package/dist/rpc/errors.d.ts +2 -0
- package/dist/rpc/errors.d.ts.map +1 -0
- package/dist/rpc/errors.js +11 -0
- package/dist/rpc/errors.js.map +1 -0
- package/dist/rpc/errors.test.d.ts +2 -0
- package/dist/rpc/errors.test.d.ts.map +1 -0
- package/dist/rpc/errors.test.js +34 -0
- package/dist/rpc/errors.test.js.map +1 -0
- package/dist/rpc/retry.d.ts +4 -0
- package/dist/rpc/retry.d.ts.map +1 -0
- package/dist/rpc/retry.js +6 -0
- package/dist/rpc/retry.js.map +1 -0
- package/dist/rpc/retry.test.d.ts +2 -0
- package/dist/rpc/retry.test.d.ts.map +1 -0
- package/dist/rpc/retry.test.js +98 -0
- package/dist/rpc/retry.test.js.map +1 -0
- package/dist/transaction/index.d.ts +1 -1
- package/dist/transaction/index.d.ts.map +1 -1
- package/dist/transaction/index.js +1 -1
- package/dist/transaction/index.js.map +1 -1
- package/dist/transaction/p2id.d.ts +17 -6
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +22 -31
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/p2id.test.js +62 -24
- package/dist/transaction/p2id.test.js.map +1 -1
- package/dist/transaction/summary.d.ts +45 -2
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +42 -2
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/summary.test.d.ts +2 -0
- package/dist/transaction/summary.test.d.ts.map +1 -0
- package/dist/transaction/summary.test.js +26 -0
- package/dist/transaction/summary.test.js.map +1 -0
- package/dist/transaction/updateGuardian.d.ts.map +1 -1
- package/dist/transaction/updateGuardian.js +16 -18
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js +15 -17
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.d.ts +6 -1
- package/dist/transaction/updateSigners.d.ts.map +1 -1
- package/dist/transaction/updateSigners.js +20 -17
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts +2 -2
- 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 +28 -4
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js +18 -0
- package/dist/types/proposal.js.map +1 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/signature.d.ts +11 -7
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +24 -58
- package/dist/utils/signature.js.map +1 -1
- package/dist/utils/word.d.ts +7 -0
- package/dist/utils/word.d.ts.map +1 -1
- package/dist/utils/word.js +15 -0
- package/dist/utils/word.js.map +1 -1
- package/masm/account_components/auth/guarded_multisig.masm +42 -0
- package/package.json +7 -4
- package/src/account/builder.test.ts +111 -45
- package/src/account/builder.ts +45 -33
- package/src/account/layout.ts +33 -0
- package/src/account/masm/account-components/auth.ts +36 -56
- package/src/account/masm/index.ts +0 -1
- package/src/account/storage.ts +9 -22
- package/src/client.test.ts +87 -16
- package/src/client.ts +16 -3
- package/src/index.ts +27 -1
- package/src/inspector.test.ts +330 -38
- package/src/inspector.ts +196 -33
- package/src/multisig.test.ts +680 -63
- package/src/multisig.ts +400 -115
- package/src/procedures.ts +6 -7
- package/src/proposal/metadata.test.ts +76 -0
- package/src/proposal/metadata.ts +13 -2
- package/src/prover/errors.test.ts +8 -0
- package/src/prover/errors.ts +7 -175
- package/src/prover/retry.test.ts +1 -1
- package/src/prover/retry.ts +10 -35
- package/src/prover/workflow.test.ts +40 -4
- package/src/prover/workflow.ts +16 -4
- package/src/retry/classify.ts +220 -0
- package/src/retry/runtime.ts +45 -0
- package/src/rpc/config.test.ts +45 -0
- package/src/rpc/config.ts +30 -0
- package/src/rpc/errors.test.ts +69 -0
- package/src/rpc/errors.ts +12 -0
- package/src/rpc/retry.test.ts +144 -0
- package/src/rpc/retry.ts +12 -0
- package/src/transaction/index.ts +7 -1
- package/src/transaction/p2id.test.ts +112 -31
- package/src/transaction/p2id.ts +39 -38
- package/src/transaction/summary.test.ts +32 -0
- package/src/transaction/summary.ts +83 -4
- package/src/transaction/updateGuardian.ts +15 -25
- package/src/transaction/updateProcedureThreshold.ts +13 -29
- package/src/transaction/updateSigners.ts +38 -30
- package/src/transaction.ts +8 -1
- package/src/types/proposal.ts +43 -4
- package/src/types.ts +0 -1
- package/src/utils/signature.ts +32 -65
- package/src/utils/word.ts +17 -0
- package/dist/account/masm/auth.d.ts +0 -5
- package/dist/account/masm/auth.d.ts.map +0 -1
- package/dist/account/masm/auth.js +0 -1509
- package/dist/account/masm/auth.js.map +0 -1
- package/masm/account_components/auth/multisig.masm +0 -12
- package/masm/account_components/auth/multisig_ecdsa.masm +0 -12
- package/masm/account_components/auth/multisig_guardian.masm +0 -16
- package/masm/account_components/auth/multisig_guardian_ecdsa.masm +0 -16
- package/masm/auth/guardian.masm +0 -199
- package/masm/auth/guardian_ecdsa.masm +0 -195
- package/masm/auth/multisig.masm +0 -554
- package/masm/auth/multisig_ecdsa.masm +0 -554
- package/src/account/masm/auth.ts +0 -1512
package/src/procedures.ts
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* human-readable encoding and should not be copied into this table.
|
|
12
12
|
*/
|
|
13
13
|
export const PROCEDURE_ROOTS = {
|
|
14
|
-
update_signers: '
|
|
15
|
-
update_procedure_threshold: '
|
|
16
|
-
auth_tx: '
|
|
17
|
-
update_guardian: '
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
receive_asset: '0x6170fd6d682d91777b551fd866258f43cc657f1291f8f071500f4e56e9c153da',
|
|
14
|
+
update_signers: '0xa261cfd3c8791ac5abe1e78e14eade2f20789d73ab1c23c430418de59bc3380e',
|
|
15
|
+
update_procedure_threshold: '0x97587c61d49313b1d5a3c8b7437e0080e67ed9bd9d3e7206bcae562f934ccd03',
|
|
16
|
+
auth_tx: '0xa6aa6f69d9358535272ba433cd48d20628a5c69598e00c6dd01a22e83a5f15df',
|
|
17
|
+
update_guardian: '0x0a614ff7c81a561cbd2a4c2d9482031a7a841ca5de33349daed23a9d871b3675',
|
|
18
|
+
send_asset: '0x595bc83258726a66bd904912cfd5186c07cbd902dfbc115b7d6bc8105efc57e3',
|
|
19
|
+
receive_asset: '0x34a56dd18f6fe5aab63198b9dcfc6467e793ebabb37d56b994b902504635da13',
|
|
21
20
|
} as const;
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -157,3 +157,79 @@ describe('ProposalMetadataCodec p2id noteType (issue #322)', () => {
|
|
|
157
157
|
expect(() => ProposalMetadataCodec.validate(md)).toThrow(/unsupported noteType/);
|
|
158
158
|
});
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
describe('ProposalMetadataCodec p2id P2IDE heights (issue #366)', () => {
|
|
162
|
+
const baseWire: GuardianProposalMetadata = {
|
|
163
|
+
proposalType: 'p2id',
|
|
164
|
+
recipientId: '0xrecipient',
|
|
165
|
+
faucetId: '0xfaucet',
|
|
166
|
+
amount: '1000',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
it('round-trips reclaim and timelock heights through the codec', () => {
|
|
170
|
+
const md = ProposalMetadataCodec.fromGuardian({
|
|
171
|
+
...baseWire,
|
|
172
|
+
reclaimHeight: 12345,
|
|
173
|
+
timelockHeight: 700,
|
|
174
|
+
}) as P2IdProposalMetadata;
|
|
175
|
+
expect(md.reclaimHeight).toBe(12345);
|
|
176
|
+
expect(md.timelockHeight).toBe(700);
|
|
177
|
+
|
|
178
|
+
const wire = ProposalMetadataCodec.toGuardian(md);
|
|
179
|
+
expect(wire.reclaimHeight).toBe(12345);
|
|
180
|
+
expect(wire.timelockHeight).toBe(700);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('leaves heights absent for plain P2ID proposals (pre-#366 wire shape)', () => {
|
|
184
|
+
const md = ProposalMetadataCodec.fromGuardian(baseWire) as P2IdProposalMetadata;
|
|
185
|
+
expect(md.reclaimHeight).toBeUndefined();
|
|
186
|
+
expect(md.timelockHeight).toBeUndefined();
|
|
187
|
+
|
|
188
|
+
const wire = ProposalMetadataCodec.toGuardian(md);
|
|
189
|
+
expect(wire.reclaimHeight).toBeUndefined();
|
|
190
|
+
expect(wire.timelockHeight).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('fromGuardian rejects a zero height (encodes "no constraint" on-chain)', () => {
|
|
194
|
+
expect(() =>
|
|
195
|
+
ProposalMetadataCodec.fromGuardian({ ...baseWire, reclaimHeight: 0 }),
|
|
196
|
+
).toThrow(/unsupported reclaimHeight/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('fromGuardian rejects a non-integer or out-of-range height', () => {
|
|
200
|
+
expect(() =>
|
|
201
|
+
ProposalMetadataCodec.fromGuardian({ ...baseWire, timelockHeight: 1.5 }),
|
|
202
|
+
).toThrow(/unsupported timelockHeight/);
|
|
203
|
+
expect(() =>
|
|
204
|
+
ProposalMetadataCodec.fromGuardian({ ...baseWire, reclaimHeight: 0x1_0000_0000 }),
|
|
205
|
+
).toThrow(/unsupported reclaimHeight/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('validate rejects an invalid height', () => {
|
|
209
|
+
const md = {
|
|
210
|
+
proposalType: 'p2id',
|
|
211
|
+
description: '',
|
|
212
|
+
recipientId: '0xrecipient',
|
|
213
|
+
faucetId: '0xfaucet',
|
|
214
|
+
amount: '1000',
|
|
215
|
+
reclaimHeight: -1,
|
|
216
|
+
} as unknown as P2IdProposalMetadata;
|
|
217
|
+
expect(() => ProposalMetadataCodec.validate(md)).toThrow(/unsupported reclaimHeight/);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('toGuardian rejects an invalid height instead of emitting it on the wire', () => {
|
|
221
|
+
const base = {
|
|
222
|
+
proposalType: 'p2id',
|
|
223
|
+
description: '',
|
|
224
|
+
recipientId: '0xrecipient',
|
|
225
|
+
faucetId: '0xfaucet',
|
|
226
|
+
amount: '1000',
|
|
227
|
+
};
|
|
228
|
+
expect(() =>
|
|
229
|
+
ProposalMetadataCodec.toGuardian({ ...base, reclaimHeight: 0 } as P2IdProposalMetadata),
|
|
230
|
+
).toThrow(/unsupported reclaimHeight/);
|
|
231
|
+
expect(() =>
|
|
232
|
+
ProposalMetadataCodec.toGuardian({ ...base, timelockHeight: 1.5 } as P2IdProposalMetadata),
|
|
233
|
+
).toThrow(/unsupported timelockHeight/);
|
|
234
|
+
});
|
|
235
|
+
});
|
package/src/proposal/metadata.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ProposalMetadata as GuardianProposalMetadata } from '@openzeppelin/guardian-client';
|
|
2
2
|
import type { ProposalMetadata } from '../types.js';
|
|
3
3
|
import { isProcedureName } from '../procedures.js';
|
|
4
|
-
import { isP2idNoteVisibility } from '../types/proposal.js';
|
|
4
|
+
import { isP2idNoteVisibility, parseP2ideHeight } from '../types/proposal.js';
|
|
5
5
|
|
|
6
6
|
export class ProposalMetadataCodec {
|
|
7
7
|
static toGuardian(metadata: ProposalMetadata): GuardianProposalMetadata {
|
|
@@ -10,6 +10,7 @@ export class ProposalMetadataCodec {
|
|
|
10
10
|
description: metadata.description,
|
|
11
11
|
salt: metadata.saltHex,
|
|
12
12
|
requiredSignatures: metadata.requiredSignatures,
|
|
13
|
+
chainAnchor: metadata.chainAnchor,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
switch (metadata.proposalType) {
|
|
@@ -30,6 +31,11 @@ export class ProposalMetadataCodec {
|
|
|
30
31
|
// keeps the pre-#322 wire shape and matches the Rust encoder (which
|
|
31
32
|
// round-trips through the NoteType enum). Absent => public.
|
|
32
33
|
noteType: metadata.noteType === 'private' ? 'private' : undefined,
|
|
34
|
+
// Validated on encode too: the create path already rejects bad
|
|
35
|
+
// heights when building the note, but metadata handed directly to
|
|
36
|
+
// the codec must not reach the wire with a value cosigners reject.
|
|
37
|
+
reclaimHeight: parseP2ideHeight('reclaimHeight', metadata.reclaimHeight),
|
|
38
|
+
timelockHeight: parseP2ideHeight('timelockHeight', metadata.timelockHeight),
|
|
33
39
|
};
|
|
34
40
|
case 'switch_guardian':
|
|
35
41
|
return {
|
|
@@ -68,6 +74,7 @@ export class ProposalMetadataCodec {
|
|
|
68
74
|
description: guardian.description ?? '',
|
|
69
75
|
saltHex: guardian.salt,
|
|
70
76
|
requiredSignatures: guardian.requiredSignatures,
|
|
77
|
+
chainAnchor: guardian.chainAnchor,
|
|
71
78
|
};
|
|
72
79
|
|
|
73
80
|
switch (guardian.proposalType) {
|
|
@@ -87,6 +94,8 @@ export class ProposalMetadataCodec {
|
|
|
87
94
|
faucetId: guardian.faucetId,
|
|
88
95
|
amount: guardian.amount,
|
|
89
96
|
noteType: guardian.noteType,
|
|
97
|
+
reclaimHeight: parseP2ideHeight('reclaimHeight', guardian.reclaimHeight),
|
|
98
|
+
timelockHeight: parseP2ideHeight('timelockHeight', guardian.timelockHeight),
|
|
90
99
|
};
|
|
91
100
|
case 'consume_notes':
|
|
92
101
|
if (!guardian.noteIds || guardian.noteIds.length === 0) {
|
|
@@ -138,7 +147,7 @@ export class ProposalMetadataCodec {
|
|
|
138
147
|
};
|
|
139
148
|
default:
|
|
140
149
|
// Any proposal type the SDK does not model collapses to the 'custom'
|
|
141
|
-
// bucket while preserving the original label
|
|
150
|
+
// bucket while preserving the original label.
|
|
142
151
|
return {
|
|
143
152
|
...base,
|
|
144
153
|
proposalType: 'custom',
|
|
@@ -182,6 +191,8 @@ export class ProposalMetadataCodec {
|
|
|
182
191
|
if (metadata.noteType !== undefined && !isP2idNoteVisibility(metadata.noteType)) {
|
|
183
192
|
throw new Error(`p2id proposal has unsupported noteType '${metadata.noteType}'`);
|
|
184
193
|
}
|
|
194
|
+
parseP2ideHeight('reclaimHeight', metadata.reclaimHeight);
|
|
195
|
+
parseP2ideHeight('timelockHeight', metadata.timelockHeight);
|
|
185
196
|
return metadata;
|
|
186
197
|
case 'custom':
|
|
187
198
|
// Custom proposals are opaque to the SDK; nothing to validate beyond
|
|
@@ -50,4 +50,12 @@ describe('isTransientProverError', () => {
|
|
|
50
50
|
expect(isTransientProverError({ code: 14, message: 'unavailable' })).toBe(true);
|
|
51
51
|
expect(isTransientProverError({ code: 3, message: 'timeout text' })).toBe(false);
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
it('reads grpc code wording out of plain message text', () => {
|
|
55
|
+
expect(isTransientProverError(new Error('grpc code: NotFound'))).toBe(false);
|
|
56
|
+
expect(
|
|
57
|
+
isTransientProverError(new Error('grpc code: Internal; nested timeout')),
|
|
58
|
+
).toBe(false);
|
|
59
|
+
expect(isTransientProverError(new Error('grpc code: Unavailable'))).toBe(true);
|
|
60
|
+
});
|
|
53
61
|
});
|
package/src/prover/errors.ts
CHANGED
|
@@ -1,178 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
cause?: unknown;
|
|
3
|
-
code?: unknown;
|
|
4
|
-
status?: unknown;
|
|
5
|
-
statusCode?: unknown;
|
|
6
|
-
message?: unknown;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
type StructuredEvidence = 'transient' | 'permanent' | 'indeterminate';
|
|
10
|
-
|
|
11
|
-
const TRANSIENT_GRPC = new Set([
|
|
12
|
-
'cancelled',
|
|
13
|
-
'canceled',
|
|
14
|
-
'deadlineexceeded',
|
|
15
|
-
'unavailable',
|
|
16
|
-
'resourceexhausted',
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
const PERMANENT_GRPC = new Set([
|
|
20
|
-
'invalidargument',
|
|
21
|
-
'failedprecondition',
|
|
22
|
-
'permissiondenied',
|
|
23
|
-
'unauthenticated',
|
|
24
|
-
'notfound',
|
|
25
|
-
'alreadyexists',
|
|
26
|
-
'outofrange',
|
|
27
|
-
'unimplemented',
|
|
28
|
-
'aborted',
|
|
29
|
-
'internal',
|
|
30
|
-
'dataloss',
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
const TRANSIENT_HTTP = new Set([408, 429, 502, 503, 504]);
|
|
34
|
-
const NUMERIC_GRPC_CODES = new Map<number, string>([
|
|
35
|
-
[0, 'ok'],
|
|
36
|
-
[1, 'cancelled'],
|
|
37
|
-
[2, 'unknown'],
|
|
38
|
-
[3, 'invalidargument'],
|
|
39
|
-
[4, 'deadlineexceeded'],
|
|
40
|
-
[5, 'notfound'],
|
|
41
|
-
[6, 'alreadyexists'],
|
|
42
|
-
[7, 'permissiondenied'],
|
|
43
|
-
[8, 'resourceexhausted'],
|
|
44
|
-
[9, 'failedprecondition'],
|
|
45
|
-
[10, 'aborted'],
|
|
46
|
-
[11, 'outofrange'],
|
|
47
|
-
[12, 'unimplemented'],
|
|
48
|
-
[13, 'internal'],
|
|
49
|
-
[14, 'unavailable'],
|
|
50
|
-
[15, 'dataloss'],
|
|
51
|
-
[16, 'unauthenticated'],
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
function normalizeCode(value: unknown): string | undefined {
|
|
55
|
-
if (typeof value === 'number') {
|
|
56
|
-
return NUMERIC_GRPC_CODES.get(value);
|
|
57
|
-
}
|
|
58
|
-
return typeof value === 'string' ? value.replaceAll(/[\s_-]/g, '').toLowerCase() : undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function grpcEvidence(value: unknown): StructuredEvidence | undefined {
|
|
62
|
-
const code = normalizeCode(value);
|
|
63
|
-
if (code === undefined) {
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
if (TRANSIENT_GRPC.has(code)) {
|
|
67
|
-
return 'transient';
|
|
68
|
-
}
|
|
69
|
-
if (PERMANENT_GRPC.has(code)) {
|
|
70
|
-
return 'permanent';
|
|
71
|
-
}
|
|
72
|
-
if (code === 'unknown' || code === 'ok') {
|
|
73
|
-
return 'indeterminate';
|
|
74
|
-
}
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function httpEvidence(value: unknown): StructuredEvidence | undefined {
|
|
79
|
-
if (typeof value !== 'number' || !Number.isInteger(value) || value < 400 || value > 599) {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
return TRANSIENT_HTTP.has(value) ? 'transient' : 'permanent';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function httpMessageEvidence(message: string): StructuredEvidence | undefined {
|
|
86
|
-
let hasTransient = false;
|
|
87
|
-
for (const match of message.matchAll(
|
|
88
|
-
/(?:\bhttp(?:\s+status)?|\bstatus\s*:?)\s*(\d{3})\b/g,
|
|
89
|
-
)) {
|
|
90
|
-
const evidence = httpEvidence(Number(match[1]));
|
|
91
|
-
if (evidence === 'permanent') {
|
|
92
|
-
return 'permanent';
|
|
93
|
-
}
|
|
94
|
-
hasTransient ||= evidence === 'transient';
|
|
95
|
-
}
|
|
96
|
-
return hasTransient ? 'transient' : undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function flattenedTransient(message: string): boolean {
|
|
100
|
-
return [
|
|
101
|
-
'cancelled',
|
|
102
|
-
'canceled',
|
|
103
|
-
'deadline exceeded',
|
|
104
|
-
'timeout',
|
|
105
|
-
'unavailable',
|
|
106
|
-
'resource exhausted',
|
|
107
|
-
'request timeout',
|
|
108
|
-
'too many requests',
|
|
109
|
-
'rate limited',
|
|
110
|
-
'rate limit',
|
|
111
|
-
'bad gateway',
|
|
112
|
-
'service unavailable',
|
|
113
|
-
'gateway timeout',
|
|
114
|
-
'i/o timeout',
|
|
115
|
-
'io timeout',
|
|
116
|
-
'connection reset',
|
|
117
|
-
'broken pipe',
|
|
118
|
-
].some((signal) => message.includes(signal));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function asRecord(value: unknown): ErrorRecord | undefined {
|
|
122
|
-
return typeof value === 'object' && value !== null ? value as ErrorRecord : undefined;
|
|
123
|
-
}
|
|
1
|
+
import { isTransientError } from '../retry/classify.js';
|
|
124
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Prover-policy classification: the shared classifier with no transport-text
|
|
5
|
+
* extras — a bare "connection error" from a prover is treated as its
|
|
6
|
+
* considered answer.
|
|
7
|
+
*/
|
|
125
8
|
export function isTransientProverError(error: unknown): boolean {
|
|
126
|
-
|
|
127
|
-
const messages: string[] = [];
|
|
128
|
-
let hasTransient = false;
|
|
129
|
-
let hasPermanent = false;
|
|
130
|
-
let current: unknown = error;
|
|
131
|
-
|
|
132
|
-
while (current !== undefined && current !== null) {
|
|
133
|
-
const record = asRecord(current);
|
|
134
|
-
if (record !== undefined) {
|
|
135
|
-
if (seen.has(record)) {
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
seen.add(record);
|
|
139
|
-
for (const evidence of [
|
|
140
|
-
grpcEvidence(record.code),
|
|
141
|
-
httpEvidence(record.status),
|
|
142
|
-
httpEvidence(record.statusCode),
|
|
143
|
-
]) {
|
|
144
|
-
hasTransient ||= evidence === 'transient';
|
|
145
|
-
hasPermanent ||= evidence === 'permanent';
|
|
146
|
-
}
|
|
147
|
-
if (typeof record.message === 'string') {
|
|
148
|
-
const message = record.message.toLowerCase();
|
|
149
|
-
messages.push(message);
|
|
150
|
-
const evidence = httpMessageEvidence(message);
|
|
151
|
-
hasTransient ||= evidence === 'transient';
|
|
152
|
-
hasPermanent ||= evidence === 'permanent';
|
|
153
|
-
} else {
|
|
154
|
-
const message = String(current).toLowerCase();
|
|
155
|
-
messages.push(message);
|
|
156
|
-
const evidence = httpMessageEvidence(message);
|
|
157
|
-
hasTransient ||= evidence === 'transient';
|
|
158
|
-
hasPermanent ||= evidence === 'permanent';
|
|
159
|
-
}
|
|
160
|
-
current = record.cause;
|
|
161
|
-
} else {
|
|
162
|
-
const message = String(current).toLowerCase();
|
|
163
|
-
messages.push(message);
|
|
164
|
-
const evidence = httpMessageEvidence(message);
|
|
165
|
-
hasTransient ||= evidence === 'transient';
|
|
166
|
-
hasPermanent ||= evidence === 'permanent';
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (hasPermanent) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
if (hasTransient) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
return messages.some(flattenedTransient);
|
|
9
|
+
return isTransientError(error);
|
|
178
10
|
}
|
package/src/prover/retry.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { retryDelay } from '
|
|
3
|
+
import { retryDelay } from '../retry/runtime.js';
|
|
4
4
|
|
|
5
5
|
interface Fixtures {
|
|
6
6
|
delays: Array<{ retryIndex: number; unitRandom: number; delayMs: number }>;
|
package/src/prover/retry.ts
CHANGED
|
@@ -1,46 +1,21 @@
|
|
|
1
1
|
import type { TransactionExecution, TransactionProof } from '@miden-sdk/miden-sdk';
|
|
2
2
|
import type { ResolvedProverConfig } from './config.js';
|
|
3
3
|
import { isTransientProverError } from './errors.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const MAX_DELAY_MS = 8_000;
|
|
7
|
-
|
|
8
|
-
export interface RetryRuntime {
|
|
9
|
-
sleep(delayMs: number): Promise<void>;
|
|
10
|
-
unitRandom(): number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const productionRetryRuntime: RetryRuntime = {
|
|
14
|
-
sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
|
|
15
|
-
unitRandom: () => Math.random(),
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function retryDelay(retryIndex: number, unitRandom: number): number {
|
|
19
|
-
const exponent = Math.min(retryIndex, 1023);
|
|
20
|
-
const raw = BASE_DELAY_MS * 2 ** exponent;
|
|
21
|
-
const finiteRandom = Number.isFinite(unitRandom) ? unitRandom : 0.5;
|
|
22
|
-
const boundedRandom = Math.min(Math.max(finiteRandom, 0), 1 - Number.EPSILON);
|
|
23
|
-
return Math.min(Math.floor(raw * (0.75 + boundedRandom * 0.5)), MAX_DELAY_MS);
|
|
24
|
-
}
|
|
4
|
+
import type { RetryRuntime } from '../retry/runtime.js';
|
|
5
|
+
import { productionRetryRuntime, retryTransient } from '../retry/runtime.js';
|
|
25
6
|
|
|
26
7
|
export async function proveWithRetry(
|
|
27
8
|
execution: TransactionExecution,
|
|
28
9
|
config: ResolvedProverConfig,
|
|
29
10
|
runtime: RetryRuntime = productionRetryRuntime,
|
|
30
11
|
): Promise<TransactionProof> {
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
return retryTransient(
|
|
13
|
+
async () => {
|
|
33
14
|
const prover = config.createProver();
|
|
34
|
-
return prover === undefined
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
await runtime.sleep(retryDelay(attempt, runtime.unitRandom()));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
throw new Error('unreachable prover retry state');
|
|
15
|
+
return prover === undefined ? await execution.prove() : await execution.prove({ prover });
|
|
16
|
+
},
|
|
17
|
+
config.maxAttempts,
|
|
18
|
+
isTransientProverError,
|
|
19
|
+
runtime,
|
|
20
|
+
);
|
|
46
21
|
}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AccountId,
|
|
3
|
+
ChainAnchor,
|
|
3
4
|
MidenClient,
|
|
4
5
|
TransactionProver,
|
|
5
6
|
TransactionRequest,
|
|
6
7
|
} from '@miden-sdk/miden-sdk';
|
|
7
8
|
import { describe, expect, it, vi } from 'vitest';
|
|
8
9
|
import type { ResolvedProverConfig } from './config.js';
|
|
9
|
-
import type { RetryRuntime } from '
|
|
10
|
+
import type { RetryRuntime } from '../retry/runtime.js';
|
|
10
11
|
import { ProverWorkflow } from './workflow.js';
|
|
11
12
|
|
|
12
13
|
function asType<T>(value: unknown): T {
|
|
13
14
|
return value as T;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
const anchor = asType<ChainAnchor>({ marker: 'anchor' });
|
|
18
|
+
|
|
16
19
|
describe('ProverWorkflow', () => {
|
|
17
20
|
it('executes once, retries proof with fresh provers, submits once, and applies once', async () => {
|
|
18
21
|
const transient = Object.assign(new Error('temporarily unavailable'), {
|
|
@@ -47,12 +50,14 @@ describe('ProverWorkflow', () => {
|
|
|
47
50
|
runtime,
|
|
48
51
|
);
|
|
49
52
|
|
|
50
|
-
await workflow.
|
|
53
|
+
await workflow.submitAt(
|
|
51
54
|
asType<AccountId>({}),
|
|
52
55
|
asType<TransactionRequest>({}),
|
|
56
|
+
anchor,
|
|
53
57
|
);
|
|
54
58
|
|
|
55
59
|
expect(executeRequest).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(executeRequest.mock.calls[0][2]).toEqual({ anchor });
|
|
56
61
|
expect(prove).toHaveBeenCalledTimes(2);
|
|
57
62
|
expect(provers).toHaveLength(2);
|
|
58
63
|
expect(provers[0]).not.toBe(provers[1]);
|
|
@@ -83,12 +88,43 @@ describe('ProverWorkflow', () => {
|
|
|
83
88
|
);
|
|
84
89
|
|
|
85
90
|
await expect(
|
|
86
|
-
workflow.
|
|
91
|
+
workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor),
|
|
87
92
|
).rejects.toBe(final);
|
|
88
93
|
expect(prove).toHaveBeenCalledTimes(2);
|
|
89
94
|
expect(runtime.sleep).toHaveBeenCalledTimes(1);
|
|
90
95
|
});
|
|
91
96
|
|
|
97
|
+
it('never re-submits when submission fails with transient-looking wording', async () => {
|
|
98
|
+
const rateLimited = Object.assign(new Error('Too Many Requests!'), {
|
|
99
|
+
code: 'ResourceExhausted',
|
|
100
|
+
});
|
|
101
|
+
const submit = vi.fn().mockRejectedValue(rateLimited);
|
|
102
|
+
const prove = vi.fn().mockResolvedValue({ submit });
|
|
103
|
+
const client = {
|
|
104
|
+
transactions: { executeRequest: vi.fn().mockResolvedValue({ prove }) },
|
|
105
|
+
};
|
|
106
|
+
const runtime: RetryRuntime = {
|
|
107
|
+
sleep: vi.fn().mockResolvedValue(undefined),
|
|
108
|
+
unitRandom: () => 0.5,
|
|
109
|
+
};
|
|
110
|
+
const workflow = new ProverWorkflow(
|
|
111
|
+
asType<MidenClient>(client),
|
|
112
|
+
{
|
|
113
|
+
kind: 'remote',
|
|
114
|
+
maxAttempts: 5,
|
|
115
|
+
createProver: () => asType<TransactionProver>({}),
|
|
116
|
+
},
|
|
117
|
+
runtime,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor),
|
|
122
|
+
).rejects.toBe(rateLimited);
|
|
123
|
+
expect(prove).toHaveBeenCalledTimes(1);
|
|
124
|
+
expect(submit).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(runtime.sleep).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
92
128
|
it('uses the injected prover directly when no cloneable remote override exists', async () => {
|
|
93
129
|
const apply = vi.fn().mockResolvedValue({});
|
|
94
130
|
const submit = vi.fn().mockResolvedValue({ apply });
|
|
@@ -102,7 +138,7 @@ describe('ProverWorkflow', () => {
|
|
|
102
138
|
createProver: () => undefined,
|
|
103
139
|
});
|
|
104
140
|
|
|
105
|
-
await workflow.
|
|
141
|
+
await workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor);
|
|
106
142
|
|
|
107
143
|
expect(prove).toHaveBeenCalledWith();
|
|
108
144
|
});
|
package/src/prover/workflow.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AccountId,
|
|
3
|
+
ChainAnchor,
|
|
4
|
+
MidenClient,
|
|
5
|
+
TransactionRequest,
|
|
6
|
+
} from '@miden-sdk/miden-sdk';
|
|
2
7
|
import type { ResolvedProverConfig } from './config.js';
|
|
3
|
-
import type { RetryRuntime } from '
|
|
8
|
+
import type { RetryRuntime } from '../retry/runtime.js';
|
|
4
9
|
import { proveWithRetry } from './retry.js';
|
|
5
10
|
|
|
6
11
|
export class ProverWorkflow {
|
|
@@ -10,8 +15,15 @@ export class ProverWorkflow {
|
|
|
10
15
|
private readonly runtime?: RetryRuntime,
|
|
11
16
|
) {}
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
/** Proves, submits, and applies a request at its signed chain anchor. */
|
|
19
|
+
async submitAt(
|
|
20
|
+
accountId: AccountId,
|
|
21
|
+
request: TransactionRequest,
|
|
22
|
+
anchor: ChainAnchor,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const execution = await this.client.transactions.executeRequest(accountId, request, {
|
|
25
|
+
anchor,
|
|
26
|
+
});
|
|
15
27
|
const proof = await proveWithRetry(execution, this.config, this.runtime);
|
|
16
28
|
const submission = await proof.submit();
|
|
17
29
|
await submission.apply();
|