@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/dist/multisig.js
CHANGED
|
@@ -6,23 +6,25 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { GuardianHttpClient } from '@openzeppelin/guardian-client';
|
|
8
8
|
import { Account, AccountId, AdviceMap, Endpoint, FeltArray, Note, NoteExportFormat, NoteFile, RpcClient, Signature, TransactionRequest, TransactionSummary, Word, } from '@miden-sdk/miden-sdk';
|
|
9
|
-
import { executeForSummary, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
|
|
9
|
+
import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary, executeForSummaryAt, summarySalt, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
|
|
10
10
|
import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
|
|
11
11
|
import { CONSUME_NOTES_METADATA_VERSION_V2, MAX_CONSUME_NOTES_METADATA_BYTES, } from './types/proposal.js';
|
|
12
12
|
import { LEGACY_CONSUME_NOTES_ENABLED } from './multisig/config.js';
|
|
13
13
|
import { ConsumeNotesMetadataOversizeError, LegacyConsumeNotesNoteMissingError, NoteBindingMismatchError, UnsupportedMetadataVersionError, } from './multisig/consumeNotesErrors.js';
|
|
14
14
|
import { noteFromBase64, noteToBase64 } from './utils/encoding.js';
|
|
15
15
|
import { base64ToUint8Array, uint8ArrayToBase64, normalizeHexWord, } from './utils/encoding.js';
|
|
16
|
-
import { buildSignatureAdviceEntry, normalizeSignerCommitment, signatureHexToBytes, tryComputeEcdsaCommitmentHex, } from './utils/signature.js';
|
|
16
|
+
import { assertEcdsaSignatureRecoverable, buildSignatureAdviceEntry, normalizeSignerCommitment, signatureHexToBytes, tryComputeEcdsaCommitmentHex, } from './utils/signature.js';
|
|
17
17
|
import { computeCommitmentFromTxSummary, accountIdToHex } from './multisig/helpers.js';
|
|
18
18
|
import { buildGuardianSignatureFromSigner } from './multisig/signing.js';
|
|
19
|
-
import { AccountInspector } from './inspector.js';
|
|
19
|
+
import { AccountInspector, assertCompleteDetectedConfig } from './inspector.js';
|
|
20
20
|
import { ProposalFactory } from './proposal/factory.js';
|
|
21
21
|
import { ProposalMetadataCodec } from './proposal/metadata.js';
|
|
22
22
|
import { ProposalSignatures } from './proposal/signatures.js';
|
|
23
23
|
import { getRawMidenClient, getTransactionProver, requireMidenRpcEndpoint, } from './raw-client.js';
|
|
24
24
|
import { resolveProverConfig, } from './prover/config.js';
|
|
25
25
|
import { ProverWorkflow } from './prover/workflow.js';
|
|
26
|
+
import { resolveRpcConfig, } from './rpc/config.js';
|
|
27
|
+
import { retryRpcRead } from './rpc/retry.js';
|
|
26
28
|
/**
|
|
27
29
|
* Represents a multisig account with GUARDIAN integration.
|
|
28
30
|
*/
|
|
@@ -51,6 +53,20 @@ function deserializeTransactionRequest(bytes) {
|
|
|
51
53
|
throw new Error(`failed to decode transaction request: ${detail}`);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Single home for the proposal-nonce default, plus a runtime guard for
|
|
58
|
+
* pre-#387 positional callers. Untyped JS passing the old `nonce` number (or
|
|
59
|
+
* a legacy trailing argument) would otherwise bind it as the options bag and
|
|
60
|
+
* silently fall back to every default — a public note instead of a private
|
|
61
|
+
* one, or the current threshold instead of the requested one — so it must
|
|
62
|
+
* fail loudly instead.
|
|
63
|
+
*/
|
|
64
|
+
function resolveProposalNonce(method, options, legacyArgs = []) {
|
|
65
|
+
if (typeof options !== 'object' || options === null || legacyArgs.length > 0) {
|
|
66
|
+
throw new Error(`${method}: positional optional parameters were replaced by a trailing options object (issue #387); pass { nonce, ... } instead`);
|
|
67
|
+
}
|
|
68
|
+
return options.nonce ?? Date.now();
|
|
69
|
+
}
|
|
54
70
|
export class Multisig {
|
|
55
71
|
account;
|
|
56
72
|
threshold;
|
|
@@ -63,10 +79,11 @@ export class Multisig {
|
|
|
63
79
|
midenClient;
|
|
64
80
|
rawClientPromise;
|
|
65
81
|
proverWorkflow;
|
|
82
|
+
rpcConfig;
|
|
66
83
|
_accountId;
|
|
67
84
|
midenRpcEndpoint;
|
|
68
85
|
proposals = new Map();
|
|
69
|
-
constructor(account, config, guardian, signer, midenClient, accountId, midenRpcEndpoint, proverConfig) {
|
|
86
|
+
constructor(account, config, guardian, signer, midenClient, accountId, midenRpcEndpoint, proverConfig, rpcConfig) {
|
|
70
87
|
this.account = account;
|
|
71
88
|
this.threshold = config.threshold;
|
|
72
89
|
this.signerCommitments = config.signerCommitments;
|
|
@@ -80,6 +97,7 @@ export class Multisig {
|
|
|
80
97
|
this.midenRpcEndpoint = requireMidenRpcEndpoint(midenRpcEndpoint);
|
|
81
98
|
this.rawClientPromise = getRawMidenClient(midenClient, this.midenRpcEndpoint);
|
|
82
99
|
this.proverWorkflow = new ProverWorkflow(this.midenClient, proverConfig ?? resolveProverConfig(undefined, getTransactionProver(midenClient)));
|
|
100
|
+
this.rpcConfig = rpcConfig ?? resolveRpcConfig(undefined);
|
|
83
101
|
}
|
|
84
102
|
getMidenRpcEndpoint() {
|
|
85
103
|
return this.midenRpcEndpoint;
|
|
@@ -124,7 +142,32 @@ export class Multisig {
|
|
|
124
142
|
*/
|
|
125
143
|
async getStoreAccount() {
|
|
126
144
|
const webClient = await this.getRawClient();
|
|
127
|
-
|
|
145
|
+
const stored = await retryRpcRead(() => webClient.getAccount(AccountId.fromHex(this._accountId)), this.rpcConfig);
|
|
146
|
+
return stored ?? this.account;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Read the current ordered signer public-key commitments from account
|
|
150
|
+
* storage (store-backed state, falling back to the snapshot).
|
|
151
|
+
*
|
|
152
|
+
* Commitments are ordered by signer index as currently stored; indices
|
|
153
|
+
* re-pack when signers are removed, so index 0 is the creation-time first
|
|
154
|
+
* key only until the first membership change. Unlike the
|
|
155
|
+
* `signerCommitments` field, which reflects the config detected at
|
|
156
|
+
* construction / last sync, this reads the account state directly.
|
|
157
|
+
* See `AccountInspector.getSignerPublicKeyCommitments` (issue #306).
|
|
158
|
+
*/
|
|
159
|
+
async getSignerPublicKeyCommitments() {
|
|
160
|
+
const account = await this.getStoreAccount();
|
|
161
|
+
return AccountInspector.getSignerPublicKeyCommitments(account);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read the current guardian public-key commitment from account storage.
|
|
165
|
+
* The guarded-multisig always includes a guardian, so this throws (rather
|
|
166
|
+
* than returning null) when the entry is missing.
|
|
167
|
+
*/
|
|
168
|
+
async getGuardianPublicKeyCommitment() {
|
|
169
|
+
const account = await this.getStoreAccount();
|
|
170
|
+
return AccountInspector.getGuardianPublicKeyCommitment(account);
|
|
128
171
|
}
|
|
129
172
|
/**
|
|
130
173
|
* Maps a proposal type to the procedure that determines its threshold.
|
|
@@ -164,6 +207,37 @@ export class Multisig {
|
|
|
164
207
|
}
|
|
165
208
|
return this.procedureThresholds.get(procedure) ?? this.threshold;
|
|
166
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Per-procedure threshold overrides whose effective signing ratio is diluted
|
|
212
|
+
* by growing the signer set to `newNumSigners`.
|
|
213
|
+
*
|
|
214
|
+
* Overrides are absolute signature counts, not ratios, and the on-chain
|
|
215
|
+
* `update_signers_and_threshold` procedure does not re-scale them: growing
|
|
216
|
+
* the approver set silently lowers every override's effective signing ratio
|
|
217
|
+
* (a 2-of-2 override becomes 2-of-n). Callers creating a proposal that grows
|
|
218
|
+
* the signer set should surface these overrides and suggest raising them via
|
|
219
|
+
* an update-procedure-threshold proposal alongside the growth.
|
|
220
|
+
*
|
|
221
|
+
* @param newNumSigners - Signer-set size the proposal produces
|
|
222
|
+
* @returns The configured overrides, or an empty list when the set does not grow
|
|
223
|
+
*/
|
|
224
|
+
overridesDilutedBySignerGrowth(newNumSigners) {
|
|
225
|
+
if (newNumSigners <= this.signerCommitments.length) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
return Array.from(this.procedureThresholds.entries()).map(([procedure, threshold]) => ({
|
|
229
|
+
procedure,
|
|
230
|
+
threshold,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
warnOnOverrideDilution(newNumSigners) {
|
|
234
|
+
const current = this.signerCommitments.length;
|
|
235
|
+
for (const { procedure, threshold } of this.overridesDilutedBySignerGrowth(newNumSigners)) {
|
|
236
|
+
console.warn(`growing the signer set dilutes the ${procedure} threshold override ` +
|
|
237
|
+
`(${threshold}-of-${current} becomes ${threshold}-of-${newNumSigners}); consider raising it ` +
|
|
238
|
+
`via an update-procedure-threshold proposal alongside the signer update`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
167
241
|
/**
|
|
168
242
|
* Update the GUARDIAN client used by this Multisig instance.
|
|
169
243
|
*
|
|
@@ -204,7 +278,7 @@ export class Multisig {
|
|
|
204
278
|
const state = await this.fetchState();
|
|
205
279
|
const accountId = AccountId.fromHex(this._accountId);
|
|
206
280
|
const webClient = await this.getRawClient();
|
|
207
|
-
const localAccount = await webClient.getAccount(accountId);
|
|
281
|
+
const localAccount = await retryRpcRead(() => webClient.getAccount(accountId), this.rpcConfig);
|
|
208
282
|
let accountForConfigRefresh = localAccount ?? null;
|
|
209
283
|
const guardianCommitment = normalizeHexWord(state.commitment);
|
|
210
284
|
const localCommitment = localAccount
|
|
@@ -224,7 +298,7 @@ export class Multisig {
|
|
|
224
298
|
async verifyStateCommitment() {
|
|
225
299
|
const accountId = AccountId.fromHex(this._accountId);
|
|
226
300
|
const webClient = await this.getRawClient();
|
|
227
|
-
const localAccount = await webClient.getAccount(accountId);
|
|
301
|
+
const localAccount = await retryRpcRead(() => webClient.getAccount(accountId), this.rpcConfig);
|
|
228
302
|
if (!localAccount) {
|
|
229
303
|
throw new Error(`Local account state not found for account ${this._accountId}. Sync the account before verifying.`);
|
|
230
304
|
}
|
|
@@ -283,7 +357,7 @@ export class Multisig {
|
|
|
283
357
|
async getOnChainCommitment(accountId) {
|
|
284
358
|
const rpcClient = new RpcClient(new Endpoint(this.getMidenRpcEndpoint()));
|
|
285
359
|
try {
|
|
286
|
-
const accountDetails = await rpcClient.getAccountDetails(accountId);
|
|
360
|
+
const accountDetails = await retryRpcRead(() => rpcClient.getAccountDetails(accountId), this.rpcConfig);
|
|
287
361
|
// If the account is not found or its commitment is zero, means that the account is not deployed yet
|
|
288
362
|
if (!accountDetails) {
|
|
289
363
|
return null;
|
|
@@ -311,12 +385,14 @@ export class Multisig {
|
|
|
311
385
|
}
|
|
312
386
|
try {
|
|
313
387
|
const detected = AccountInspector.fromAccount(account);
|
|
388
|
+
// Fail closed on a partial read: adopting a truncated signer set would
|
|
389
|
+
// let membership proposals rewrite the account without the omitted
|
|
390
|
+
// keys. The catch below keeps the previously validated config instead.
|
|
391
|
+
assertCompleteDetectedConfig(detected);
|
|
314
392
|
this.account = account;
|
|
315
393
|
this.threshold = detected.threshold;
|
|
316
394
|
this.signerCommitments = detected.signerCommitments;
|
|
317
|
-
|
|
318
|
-
this.guardianCommitment = detected.guardianCommitment;
|
|
319
|
-
}
|
|
395
|
+
this.guardianCommitment = detected.guardianCommitment;
|
|
320
396
|
this.procedureThresholds = new Map(detected.procedureThresholds);
|
|
321
397
|
}
|
|
322
398
|
catch (error) {
|
|
@@ -402,18 +478,22 @@ export class Multisig {
|
|
|
402
478
|
* Create an "add signer" proposal.
|
|
403
479
|
*
|
|
404
480
|
* @param newCommitment - Commitment of the new signer (hex)
|
|
405
|
-
* @param
|
|
406
|
-
*
|
|
481
|
+
* @param options - Optional settings: `nonce`, `newThreshold` (defaults to
|
|
482
|
+
* current threshold)
|
|
407
483
|
*/
|
|
408
|
-
async createAddSignerProposal(newCommitment,
|
|
484
|
+
async createAddSignerProposal(newCommitment, options = {}, ...legacyArgs) {
|
|
485
|
+
const proposalNonce = resolveProposalNonce('createAddSignerProposal', options, legacyArgs);
|
|
409
486
|
const webClient = await this.getRawClient();
|
|
410
|
-
const targetThreshold = newThreshold ?? this.threshold;
|
|
487
|
+
const targetThreshold = options.newThreshold ?? this.threshold;
|
|
411
488
|
const targetSignerCommitments = [...this.signerCommitments, newCommitment];
|
|
489
|
+
this.warnOnOverrideDilution(targetSignerCommitments.length);
|
|
412
490
|
const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, targetThreshold, targetSignerCommitments, { signatureScheme: this.signer.scheme });
|
|
413
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
491
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
492
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
493
|
+
anchor.free();
|
|
414
494
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
415
|
-
const proposalNonce = nonce ?? Date.now();
|
|
416
495
|
const metadata = {
|
|
496
|
+
chainAnchor,
|
|
417
497
|
proposalType: 'add_signer',
|
|
418
498
|
targetThreshold,
|
|
419
499
|
targetSignerCommitments,
|
|
@@ -427,10 +507,11 @@ export class Multisig {
|
|
|
427
507
|
* Create a "remove signer" proposal by executing the update_signers script to summary.
|
|
428
508
|
*
|
|
429
509
|
* @param signerToRemove - Commitment of the signer to remove (hex)
|
|
430
|
-
* @param
|
|
431
|
-
*
|
|
510
|
+
* @param options - Optional settings: `nonce`, `newThreshold` (defaults to
|
|
511
|
+
* min of current threshold and new signer count)
|
|
432
512
|
*/
|
|
433
|
-
async createRemoveSignerProposal(signerToRemove,
|
|
513
|
+
async createRemoveSignerProposal(signerToRemove, options = {}, ...legacyArgs) {
|
|
514
|
+
const proposalNonce = resolveProposalNonce('createRemoveSignerProposal', options, legacyArgs);
|
|
434
515
|
const webClient = await this.getRawClient();
|
|
435
516
|
const normalizedRemove = signerToRemove.toLowerCase();
|
|
436
517
|
const targetSignerCommitments = this.signerCommitments.filter((c) => c.toLowerCase() !== normalizedRemove);
|
|
@@ -440,15 +521,17 @@ export class Multisig {
|
|
|
440
521
|
if (targetSignerCommitments.length === 0) {
|
|
441
522
|
throw new Error('Cannot remove the last signer');
|
|
442
523
|
}
|
|
443
|
-
const targetThreshold = newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
524
|
+
const targetThreshold = options.newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
444
525
|
if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
|
|
445
526
|
throw new Error(`Invalid threshold ${targetThreshold}. Must be between 1 and ${targetSignerCommitments.length}`);
|
|
446
527
|
}
|
|
447
528
|
const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, targetThreshold, targetSignerCommitments, { signatureScheme: this.signer.scheme });
|
|
448
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
529
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
530
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
531
|
+
anchor.free();
|
|
449
532
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
450
|
-
const proposalNonce = nonce ?? Date.now();
|
|
451
533
|
const metadata = {
|
|
534
|
+
chainAnchor,
|
|
452
535
|
proposalType: 'remove_signer',
|
|
453
536
|
targetThreshold,
|
|
454
537
|
targetSignerCommitments,
|
|
@@ -462,9 +545,10 @@ export class Multisig {
|
|
|
462
545
|
* Create a "change threshold" proposal.
|
|
463
546
|
*
|
|
464
547
|
* @param newThreshold - The new threshold value
|
|
465
|
-
* @param
|
|
548
|
+
* @param options - Optional settings: `nonce`
|
|
466
549
|
*/
|
|
467
|
-
async createChangeThresholdProposal(newThreshold,
|
|
550
|
+
async createChangeThresholdProposal(newThreshold, options = {}) {
|
|
551
|
+
const proposalNonce = resolveProposalNonce('createChangeThresholdProposal', options);
|
|
468
552
|
const webClient = await this.getRawClient();
|
|
469
553
|
if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
|
|
470
554
|
throw new Error(`Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`);
|
|
@@ -473,10 +557,12 @@ export class Multisig {
|
|
|
473
557
|
throw new Error('New threshold is the same as current threshold');
|
|
474
558
|
}
|
|
475
559
|
const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, newThreshold, this.signerCommitments, { signatureScheme: this.signer.scheme });
|
|
476
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
560
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
561
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
562
|
+
anchor.free();
|
|
477
563
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
478
|
-
const proposalNonce = nonce ?? Date.now();
|
|
479
564
|
const metadata = {
|
|
565
|
+
chainAnchor,
|
|
480
566
|
proposalType: 'change_threshold',
|
|
481
567
|
targetThreshold: newThreshold,
|
|
482
568
|
targetSignerCommitments: this.signerCommitments,
|
|
@@ -486,7 +572,8 @@ export class Multisig {
|
|
|
486
572
|
};
|
|
487
573
|
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
488
574
|
}
|
|
489
|
-
async createUpdateProcedureThresholdProposal(targetProcedure, targetThreshold,
|
|
575
|
+
async createUpdateProcedureThresholdProposal(targetProcedure, targetThreshold, options = {}) {
|
|
576
|
+
const proposalNonce = resolveProposalNonce('createUpdateProcedureThresholdProposal', options);
|
|
490
577
|
const webClient = await this.getRawClient();
|
|
491
578
|
if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
|
|
492
579
|
throw new Error(`Invalid threshold ${targetThreshold}. Must be between 0 and ${this.signerCommitments.length}`);
|
|
@@ -499,13 +586,15 @@ export class Multisig {
|
|
|
499
586
|
throw new Error(`Procedure ${targetProcedure} already has threshold override ${targetThreshold}`);
|
|
500
587
|
}
|
|
501
588
|
const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(webClient, targetProcedure, targetThreshold, { signatureScheme: this.signer.scheme });
|
|
502
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
589
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
590
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
591
|
+
anchor.free();
|
|
503
592
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
504
|
-
const proposalNonce = nonce ?? Date.now();
|
|
505
593
|
const action = targetThreshold === 0
|
|
506
594
|
? `Clear threshold override for ${targetProcedure}`
|
|
507
595
|
: `Set ${targetProcedure} threshold override to ${targetThreshold}`;
|
|
508
596
|
const metadata = {
|
|
597
|
+
chainAnchor,
|
|
509
598
|
proposalType: 'update_procedure_threshold',
|
|
510
599
|
targetProcedure,
|
|
511
600
|
targetThreshold,
|
|
@@ -520,16 +609,19 @@ export class Multisig {
|
|
|
520
609
|
*
|
|
521
610
|
* @param newGuardianEndpoint - The new GUARDIAN server endpoint URL
|
|
522
611
|
* @param newGuardianPubkey - The new GUARDIAN server's public key commitment (hex)
|
|
523
|
-
* @param
|
|
612
|
+
* @param options - Optional settings: `nonce`
|
|
524
613
|
*/
|
|
525
|
-
async createSwitchGuardianProposal(newGuardianEndpoint, newGuardianPubkey,
|
|
614
|
+
async createSwitchGuardianProposal(newGuardianEndpoint, newGuardianPubkey, options = {}) {
|
|
615
|
+
const proposalNonce = resolveProposalNonce('createSwitchGuardianProposal', options);
|
|
526
616
|
const webClient = await this.getRawClient();
|
|
527
617
|
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
528
618
|
const { request, salt } = await buildUpdateGuardianTransactionRequest(webClient, newGuardianPubkey, { signatureScheme: this.signer.scheme });
|
|
529
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
619
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
620
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
621
|
+
anchor.free();
|
|
530
622
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
531
|
-
const proposalNonce = nonce ?? Date.now();
|
|
532
623
|
const metadata = {
|
|
624
|
+
chainAnchor,
|
|
533
625
|
proposalType: 'switch_guardian',
|
|
534
626
|
saltHex: salt.toHex(),
|
|
535
627
|
requiredSignatures: this.getEffectiveThreshold('switch_guardian'),
|
|
@@ -545,9 +637,10 @@ export class Multisig {
|
|
|
545
637
|
* Create a "consume notes" proposal to consume notes sent to the multisig account.
|
|
546
638
|
*
|
|
547
639
|
* @param noteIds - IDs of the notes to consume (hex strings)
|
|
548
|
-
* @param
|
|
640
|
+
* @param options - Optional settings: `nonce`
|
|
549
641
|
*/
|
|
550
|
-
async createConsumeNotesProposal(noteIds,
|
|
642
|
+
async createConsumeNotesProposal(noteIds, options = {}) {
|
|
643
|
+
const proposalNonce = resolveProposalNonce('createConsumeNotesProposal', options);
|
|
551
644
|
const webClient = await this.getRawClient();
|
|
552
645
|
if (noteIds.length === 0) {
|
|
553
646
|
throw new Error('At least one note ID is required');
|
|
@@ -564,10 +657,12 @@ export class Multisig {
|
|
|
564
657
|
}
|
|
565
658
|
const embeddedNotes = fetchedNotes.map((n) => noteToBase64(n));
|
|
566
659
|
const { request, salt } = buildConsumeNotesTransactionRequestFromNotes(fetchedNotes);
|
|
567
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
660
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
661
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
662
|
+
anchor.free();
|
|
568
663
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
569
|
-
const proposalNonce = nonce ?? Date.now();
|
|
570
664
|
const metadata = {
|
|
665
|
+
chainAnchor,
|
|
571
666
|
proposalType: 'consume_notes',
|
|
572
667
|
noteIds,
|
|
573
668
|
metadataVersion: CONSUME_NOTES_METADATA_VERSION_V2,
|
|
@@ -593,21 +688,26 @@ export class Multisig {
|
|
|
593
688
|
* @param recipientId - Account ID of the recipient (hex string)
|
|
594
689
|
* @param faucetId - Faucet/token account ID (hex string)
|
|
595
690
|
* @param amount - Amount to send
|
|
596
|
-
* @param
|
|
597
|
-
*
|
|
598
|
-
*
|
|
691
|
+
* @param options - Optional settings: `nonce`; `noteType` selects the created
|
|
692
|
+
* note's visibility (defaults to `NoteType.Public`, issue #322);
|
|
693
|
+
* `reclaimHeight`/`timelockHeight` build a P2IDE note (issue #366)
|
|
599
694
|
*/
|
|
600
|
-
async createP2idProposal(recipientId, faucetId, amount,
|
|
695
|
+
async createP2idProposal(recipientId, faucetId, amount, options = {}, ...legacyArgs) {
|
|
696
|
+
const proposalNonce = resolveProposalNonce('createP2idProposal', options, legacyArgs);
|
|
601
697
|
const webClient = await this.getRawClient();
|
|
602
698
|
if (amount <= 0n) {
|
|
603
699
|
throw new Error('Amount must be greater than 0');
|
|
604
700
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
701
|
+
// Forward everything but the nonce, so a note option added to
|
|
702
|
+
// CreateP2idProposalOptions can't be silently dropped before the builder.
|
|
703
|
+
const { nonce: _nonce, ...noteOptions } = options;
|
|
704
|
+
const { request, salt } = buildP2idTransactionRequest(this._accountId, recipientId, faucetId, amount, noteOptions);
|
|
705
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
706
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
707
|
+
anchor.free();
|
|
608
708
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
609
|
-
const proposalNonce = nonce ?? Date.now();
|
|
610
709
|
const metadata = {
|
|
710
|
+
chainAnchor,
|
|
611
711
|
proposalType: 'p2id',
|
|
612
712
|
saltHex: salt.toHex(),
|
|
613
713
|
requiredSignatures: this.getEffectiveThreshold('p2id'),
|
|
@@ -616,6 +716,9 @@ export class Multisig {
|
|
|
616
716
|
amount: amount.toString(),
|
|
617
717
|
// Omitted for public notes so the wire shape matches pre-#322 proposals.
|
|
618
718
|
noteType: p2idNoteTypeToMetadata(options.noteType),
|
|
719
|
+
// Omitted when absent so plain-P2ID payloads keep the pre-#366 wire shape.
|
|
720
|
+
reclaimHeight: options.reclaimHeight,
|
|
721
|
+
timelockHeight: options.timelockHeight,
|
|
619
722
|
description: `Send ${amount} of asset ${faucetId.slice(0, 10)}... to ${recipientId.slice(0, 10)}...`,
|
|
620
723
|
};
|
|
621
724
|
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
@@ -663,7 +766,7 @@ export class Multisig {
|
|
|
663
766
|
}
|
|
664
767
|
/**
|
|
665
768
|
* Export a note created by this multisig account as serialized note-file
|
|
666
|
-
* bytes for out-of-band delivery
|
|
769
|
+
* bytes for out-of-band delivery.
|
|
667
770
|
*
|
|
668
771
|
* A private note publishes only its commitment on chain, so the recipient
|
|
669
772
|
* can never learn its contents via sync; the sender must hand them the
|
|
@@ -700,7 +803,7 @@ export class Multisig {
|
|
|
700
803
|
}
|
|
701
804
|
/**
|
|
702
805
|
* Export a note created by this multisig account as a note file downloaded
|
|
703
|
-
* by the browser
|
|
806
|
+
* by the browser. Browser-only convenience over
|
|
704
807
|
* {@link exportNoteToBytes}; use that method directly in non-DOM
|
|
705
808
|
* environments.
|
|
706
809
|
*
|
|
@@ -726,7 +829,7 @@ export class Multisig {
|
|
|
726
829
|
}
|
|
727
830
|
}
|
|
728
831
|
/**
|
|
729
|
-
* Import a note file received out-of-band
|
|
832
|
+
* Import a note file received out-of-band so the note can be
|
|
730
833
|
* consumed by this multisig account.
|
|
731
834
|
*
|
|
732
835
|
* Sync the Miden client with the network afterwards so the note's on-chain
|
|
@@ -751,7 +854,7 @@ export class Multisig {
|
|
|
751
854
|
return webClient.importNoteFile(noteFile);
|
|
752
855
|
}
|
|
753
856
|
/**
|
|
754
|
-
* Import a note file received out-of-band
|
|
857
|
+
* Import a note file received out-of-band from a browser
|
|
755
858
|
* `File`/`Blob` (e.g. a file-input selection). See
|
|
756
859
|
* {@link importNoteFromBytes} for the returned identifier semantics.
|
|
757
860
|
*/
|
|
@@ -765,10 +868,9 @@ export class Multisig {
|
|
|
765
868
|
* The P2ID note is rebuilt deterministically from the proposal salt, so the
|
|
766
869
|
* ID is known ahead of execution. For a private P2ID this is the ID to pass
|
|
767
870
|
* to {@link exportNoteToBytes} after executing, so the note file can be delivered
|
|
768
|
-
* to the recipient out-of-band
|
|
871
|
+
* to the recipient out-of-band.
|
|
769
872
|
*
|
|
770
|
-
*
|
|
771
|
-
* current vault state, which execution itself changes.
|
|
873
|
+
* The note ID remains deterministic from the proposal metadata and salt.
|
|
772
874
|
*/
|
|
773
875
|
async getP2idNoteId(proposal) {
|
|
774
876
|
const metadata = proposal.metadata;
|
|
@@ -779,8 +881,7 @@ export class Multisig {
|
|
|
779
881
|
!metadata.saltHex) {
|
|
780
882
|
throw new Error('getP2idNoteId requires a P2ID proposal with recipient, faucet, amount, and salt metadata');
|
|
781
883
|
}
|
|
782
|
-
const
|
|
783
|
-
const note = buildP2idNoteFromMetadata(this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), account, parseP2idNoteType(metadata.noteType), metadata.saltHex);
|
|
884
|
+
const note = buildP2idNoteFromMetadata(this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), parseP2idNoteType(metadata.noteType), metadata.saltHex, { reclaimHeight: metadata.reclaimHeight, timelockHeight: metadata.timelockHeight });
|
|
784
885
|
return note.id().toString();
|
|
785
886
|
}
|
|
786
887
|
/**
|
|
@@ -821,6 +922,18 @@ export class Multisig {
|
|
|
821
922
|
async abandonStatus(nonce) {
|
|
822
923
|
return this.guardian.abandonStatus(this._accountId, nonce);
|
|
823
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Fetch one page of this account's canonical delta history
|
|
927
|
+
* from GUARDIAN (issue #413), newest-first by nonce, with decoded
|
|
928
|
+
* input/output note summaries. Pass `options.cursor` from a previous
|
|
929
|
+
* page's `nextCursor` to resume; an absent `nextCursor` means the
|
|
930
|
+
* feed is exhausted. Served while the account is paused. Only
|
|
931
|
+
* transactions pushed through GUARDIAN appear — history of
|
|
932
|
+
* transactions executed elsewhere is not visible to it.
|
|
933
|
+
*/
|
|
934
|
+
async deltaHistory(options = {}) {
|
|
935
|
+
return this.guardian.getDeltaHistory(this._accountId, options);
|
|
936
|
+
}
|
|
824
937
|
async signProposal(proposalId) {
|
|
825
938
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
826
939
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
@@ -861,8 +974,17 @@ export class Multisig {
|
|
|
861
974
|
*/
|
|
862
975
|
async executeProposal(proposalId) {
|
|
863
976
|
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
977
|
+
// Execute at the proposal's anchored reference block, so the summary the
|
|
978
|
+
// cosigners signed reproduces exactly. The anchor was already checked
|
|
979
|
+
// against the summary's block commitment during binding verification.
|
|
864
980
|
const accountId = AccountId.fromHex(this._accountId);
|
|
865
|
-
|
|
981
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
982
|
+
try {
|
|
983
|
+
await this.proverWorkflow.submitAt(accountId, finalRequest, anchor);
|
|
984
|
+
}
|
|
985
|
+
finally {
|
|
986
|
+
anchor.free();
|
|
987
|
+
}
|
|
866
988
|
if (metadata.proposalType === 'switch_guardian') {
|
|
867
989
|
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
868
990
|
throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
|
|
@@ -879,13 +1001,17 @@ export class Multisig {
|
|
|
879
1001
|
deltaPayload: switchDelta.deltaPayload.txSummary,
|
|
880
1002
|
});
|
|
881
1003
|
}
|
|
882
|
-
catch {
|
|
883
|
-
//
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
// Best-effort — see above — but the failure must be visible: a
|
|
1006
|
+
// silently lost push leaves the pre-switch GUARDIAN serving this
|
|
1007
|
+
// account (split-brain, issue #305) with nothing to diagnose by.
|
|
1008
|
+
console.warn('SwitchGuardian delta push to the pre-switch GUARDIAN failed; it ' +
|
|
1009
|
+
'will keep serving this account until reconciliation', error);
|
|
884
1010
|
}
|
|
885
1011
|
try {
|
|
886
1012
|
const webClient = await this.getRawClient();
|
|
887
|
-
await webClient.syncState();
|
|
888
|
-
const updatedAccount = await webClient.getAccount(accountId);
|
|
1013
|
+
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
1014
|
+
const updatedAccount = await retryRpcRead(() => webClient.getAccount(accountId), this.rpcConfig);
|
|
889
1015
|
if (!updatedAccount) {
|
|
890
1016
|
throw new Error(`Updated account ${this._accountId} is missing from local client`);
|
|
891
1017
|
}
|
|
@@ -906,18 +1032,37 @@ export class Multisig {
|
|
|
906
1032
|
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
907
1033
|
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
908
1034
|
* after `prepareCustomExecution` rebuilds its request with the returned advice.
|
|
1035
|
+
* The transaction is executed at the proposal's anchored reference block,
|
|
1036
|
+
* since the collected signatures only authorize the summary produced there.
|
|
909
1037
|
*/
|
|
910
|
-
async submitTransaction(request) {
|
|
911
|
-
|
|
1038
|
+
async submitTransaction(proposalId, request) {
|
|
1039
|
+
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1040
|
+
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
1041
|
+
const existing = this.getLocalProposal(proposalId);
|
|
1042
|
+
const proposal = this.proposalFactory().fromDelta(delta, normalizedProposalId, existing?.metadata, existing?.signatures ?? []);
|
|
1043
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1044
|
+
try {
|
|
1045
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1046
|
+
const txSummary = TransactionSummary.deserialize(base64ToUint8Array(delta.deltaPayload.txSummary.data));
|
|
1047
|
+
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
1048
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1049
|
+
throw new Error(`Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
|
|
1050
|
+
}
|
|
1051
|
+
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
|
|
1052
|
+
}
|
|
1053
|
+
finally {
|
|
1054
|
+
anchor.free();
|
|
1055
|
+
}
|
|
912
1056
|
}
|
|
913
1057
|
/**
|
|
914
|
-
* Create a proposal from a producer-built transaction the SDK does not model
|
|
915
|
-
*
|
|
1058
|
+
* Create a proposal from a producer-built transaction the SDK does not model.
|
|
1059
|
+
* `transactionRequestBytes` is a serialized TransactionRequest;
|
|
916
1060
|
* `proposalType` is a free-form, non-empty label that must not collide with a
|
|
917
1061
|
* built-in type. The integration keeps its own recipe to execute later via
|
|
918
1062
|
* `prepareCustomExecution`.
|
|
919
1063
|
*/
|
|
920
|
-
async createCustomProposal(transactionRequestBytes, proposalType,
|
|
1064
|
+
async createCustomProposal(transactionRequestBytes, proposalType, options = {}) {
|
|
1065
|
+
const proposalNonce = resolveProposalNonce('createCustomProposal', options);
|
|
921
1066
|
const label = proposalType.trim().toLowerCase();
|
|
922
1067
|
if (label.length === 0) {
|
|
923
1068
|
throw new Error('proposalType must not be empty');
|
|
@@ -930,10 +1075,12 @@ export class Multisig {
|
|
|
930
1075
|
}
|
|
931
1076
|
const webClient = await this.getRawClient();
|
|
932
1077
|
const request = deserializeTransactionRequest(transactionRequestBytes);
|
|
933
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
1078
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
1079
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1080
|
+
anchor.free();
|
|
934
1081
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
935
|
-
const proposalNonce = nonce ?? Date.now();
|
|
936
1082
|
const metadata = {
|
|
1083
|
+
chainAnchor,
|
|
937
1084
|
proposalType: 'custom',
|
|
938
1085
|
description: '',
|
|
939
1086
|
rawProposalType: label,
|
|
@@ -944,7 +1091,7 @@ export class Multisig {
|
|
|
944
1091
|
/**
|
|
945
1092
|
* Assemble the validated execution advice (cosigner signatures + GUARDIAN
|
|
946
1093
|
* acknowledgment) for a ready custom proposal, so an integration can rebuild
|
|
947
|
-
* its transaction with its own recipe and submit
|
|
1094
|
+
* its transaction with its own recipe and submit.
|
|
948
1095
|
*
|
|
949
1096
|
* `transactionRequestBytes` is the serialized transaction request; it is used only to verify
|
|
950
1097
|
* (binding check) that it reproduces the signed commitment, before the
|
|
@@ -967,9 +1114,26 @@ export class Multisig {
|
|
|
967
1114
|
const txSummary = TransactionSummary.deserialize(base64ToUint8Array(delta.deltaPayload.txSummary.data));
|
|
968
1115
|
const signedCommitmentHex = normalizeHexWord(txSummary.toCommitment().toHex());
|
|
969
1116
|
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1117
|
+
// Probe at the proposal's anchored reference block: the signed summary
|
|
1118
|
+
// binds that block's commitment, so probing at the local sync height would
|
|
1119
|
+
// never reproduce it. The anchor arrives from an untrusted party via
|
|
1120
|
+
// GUARDIAN, so its block commitment is checked against the signed summary
|
|
1121
|
+
// before executing against it.
|
|
1122
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1123
|
+
let derivedCommitmentHex;
|
|
1124
|
+
try {
|
|
1125
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1126
|
+
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
1127
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1128
|
+
throw new Error(`Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
|
|
1129
|
+
}
|
|
1130
|
+
const webClient = await this.getRawClient();
|
|
1131
|
+
const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
|
|
1132
|
+
derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
1133
|
+
}
|
|
1134
|
+
finally {
|
|
1135
|
+
anchor.free();
|
|
1136
|
+
}
|
|
973
1137
|
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
974
1138
|
throw new Error(`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`);
|
|
975
1139
|
}
|
|
@@ -998,7 +1162,10 @@ export class Multisig {
|
|
|
998
1162
|
const signerCommitment = Word.fromHex(signerCommitmentHex);
|
|
999
1163
|
const sigBytes = signatureHexToBytes(cosignerSig.signature.signature, cosignerSig.signature.scheme);
|
|
1000
1164
|
const signature = Signature.deserialize(sigBytes);
|
|
1001
|
-
|
|
1165
|
+
if (cosignerSig.signature.scheme === 'ecdsa' && ecdsaPublicKey) {
|
|
1166
|
+
assertEcdsaSignatureRecoverable(cosignerSig.signature.signature, normalizedTxCommitmentHex, ecdsaPublicKey);
|
|
1167
|
+
}
|
|
1168
|
+
const { key, values } = buildSignatureAdviceEntry(signerCommitment, createTxCommitmentWord(), signature);
|
|
1002
1169
|
const keyHex = normalizeHexWord(key.toHex());
|
|
1003
1170
|
if (adviceMapKeys.has(keyHex)) {
|
|
1004
1171
|
throw new Error(`Duplicate advice-map key detected for proposal ${proposalId}`);
|
|
@@ -1026,7 +1193,10 @@ export class Multisig {
|
|
|
1026
1193
|
}
|
|
1027
1194
|
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1028
1195
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1029
|
-
|
|
1196
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1197
|
+
assertEcdsaSignatureRecoverable(ackSigHex, normalizedTxCommitmentHex, ackPubkey);
|
|
1198
|
+
}
|
|
1199
|
+
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(guardianCommitment, createTxCommitmentWord(), ackSignature);
|
|
1030
1200
|
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1031
1201
|
if (adviceMapKeys.has(ackKeyHex)) {
|
|
1032
1202
|
throw new Error(`Duplicate advice-map key detected for GUARDIAN acknowledgment in proposal ${proposalId}`);
|
|
@@ -1073,7 +1243,7 @@ export class Multisig {
|
|
|
1073
1243
|
}
|
|
1074
1244
|
const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
|
|
1075
1245
|
const txSummary = TransactionSummary.deserialize(txSummaryBytes);
|
|
1076
|
-
const saltHex = txSummary
|
|
1246
|
+
const saltHex = summarySalt(txSummary).toHex();
|
|
1077
1247
|
const txCommitmentHex = txSummary.toCommitment().toHex();
|
|
1078
1248
|
const normalizedTxCommitmentHex = normalizeHexWord(txCommitmentHex);
|
|
1079
1249
|
const normalizedSignerCommitments = new Set(this.signerCommitments.map((commitment) => normalizeHexWord(commitment)));
|
|
@@ -1100,9 +1270,10 @@ export class Multisig {
|
|
|
1100
1270
|
const signerCommitment = Word.fromHex(signerCommitmentHex);
|
|
1101
1271
|
const sigBytes = signatureHexToBytes(cosignerSig.signature.signature, cosignerSig.signature.scheme);
|
|
1102
1272
|
const signature = Signature.deserialize(sigBytes);
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1273
|
+
if (cosignerSig.signature.scheme === 'ecdsa' && ecdsaPublicKey) {
|
|
1274
|
+
assertEcdsaSignatureRecoverable(cosignerSig.signature.signature, normalizedTxCommitmentHex, ecdsaPublicKey);
|
|
1275
|
+
}
|
|
1276
|
+
const { key, values } = buildSignatureAdviceEntry(signerCommitment, createTxCommitmentWord(), signature);
|
|
1106
1277
|
const keyHex = normalizeHexWord(key.toHex());
|
|
1107
1278
|
if (adviceMapKeys.has(keyHex)) {
|
|
1108
1279
|
throw new Error(`Duplicate advice-map key detected for proposal ${proposalId}`);
|
|
@@ -1134,7 +1305,10 @@ export class Multisig {
|
|
|
1134
1305
|
}
|
|
1135
1306
|
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1136
1307
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1137
|
-
|
|
1308
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1309
|
+
assertEcdsaSignatureRecoverable(ackSigHex, normalizedTxCommitmentHex, ackPubkey);
|
|
1310
|
+
}
|
|
1311
|
+
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(guardianCommitment, createTxCommitmentWord(), ackSignature);
|
|
1138
1312
|
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1139
1313
|
if (adviceMapKeys.has(ackKeyHex)) {
|
|
1140
1314
|
throw new Error(`Duplicate advice-map key detected for GUARDIAN acknowledgment in proposal ${proposalId}`);
|
|
@@ -1278,34 +1452,58 @@ export class Multisig {
|
|
|
1278
1452
|
}
|
|
1279
1453
|
async verifyProposalMetadataBinding(proposal) {
|
|
1280
1454
|
const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1455
|
+
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
1456
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so check its
|
|
1457
|
+
// block commitment against the one bound into the signed summary before
|
|
1458
|
+
// anything executes against it. `ChainAnchor.deserialize` already enforced
|
|
1459
|
+
// internal header/chain consistency.
|
|
1460
|
+
const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
|
|
1461
|
+
try {
|
|
1462
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1463
|
+
const summaryBlockCommitment = normalizeHexWord(summary.blockCommitment().toHex());
|
|
1464
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1465
|
+
throw new Error(`Invalid proposal: chain anchor does not match the block commitment bound into the tx_summary for ${proposal.id}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (proposal.metadata.proposalType === 'custom') {
|
|
1468
|
+
// Custom proposals have no per-type reconstruction recipe;
|
|
1469
|
+
// the id ↔ tx_summary commitment match above is the only available
|
|
1470
|
+
// integrity guarantee for an opaque proposal.
|
|
1471
|
+
return txSummaryCommitment;
|
|
1472
|
+
}
|
|
1473
|
+
if (proposal.metadata.proposalType === 'switch_guardian') {
|
|
1474
|
+
// Re-execution would mutate the WASM account twice. The proposal ID and
|
|
1475
|
+
// guardian endpoint commitment provide the binding checks for this type.
|
|
1476
|
+
return txSummaryCommitment;
|
|
1477
|
+
}
|
|
1478
|
+
const salt = proposal.metadata.saltHex
|
|
1479
|
+
? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
|
|
1480
|
+
: summarySalt(summary);
|
|
1481
|
+
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, salt);
|
|
1482
|
+
const webClient = await this.getRawClient();
|
|
1483
|
+
const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
|
|
1484
|
+
const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
|
|
1485
|
+
if (reconstructedCommitment !== txSummaryCommitment) {
|
|
1486
|
+
throw new Error(`Invalid proposal: metadata does not match tx_summary for ${proposal.id}`);
|
|
1487
|
+
}
|
|
1285
1488
|
return txSummaryCommitment;
|
|
1286
1489
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
// The WASM `executeForSummary` leaves the guardian-disabling side effect
|
|
1290
|
-
// applied to the in-session account, so re-execution reconstructs a smaller
|
|
1291
|
-
// delta and falsely rejects with "metadata does not match tx_summary". The
|
|
1292
|
-
// native Rust client does not mutate, so this is an intentional divergence.
|
|
1293
|
-
// The id ↔ tx_summary match above plus `verifyGuardianEndpointCommitment`
|
|
1294
|
-
// at propose/execute time still bind the proposal.
|
|
1295
|
-
return txSummaryCommitment;
|
|
1490
|
+
finally {
|
|
1491
|
+
anchor.free();
|
|
1296
1492
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
if (
|
|
1306
|
-
throw new Error(`
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Decodes a proposal's chain anchor. Throws when absent: a proposal without
|
|
1496
|
+
* an anchor was created at an unknown reference block, so its signed summary
|
|
1497
|
+
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
1498
|
+
* anchor and must `free()` it once done.
|
|
1499
|
+
*/
|
|
1500
|
+
requireProposalAnchor(proposalId, metadata) {
|
|
1501
|
+
if (!metadata.chainAnchor) {
|
|
1502
|
+
throw new Error(`Proposal ${proposalId} has no chain anchor; it was created without ` +
|
|
1503
|
+
'chain-anchored execution and its signed summary cannot be reproduced ' +
|
|
1504
|
+
'at the original reference block');
|
|
1307
1505
|
}
|
|
1308
|
-
return
|
|
1506
|
+
return chainAnchorFromBase64(metadata.chainAnchor);
|
|
1309
1507
|
}
|
|
1310
1508
|
async buildTransactionRequestFromMetadata(metadata, salt, signatureAdviceMap) {
|
|
1311
1509
|
const webClient = await this.getRawClient();
|
|
@@ -1361,8 +1559,13 @@ export class Multisig {
|
|
|
1361
1559
|
throw new UnsupportedMetadataVersionError(version);
|
|
1362
1560
|
}
|
|
1363
1561
|
case 'p2id': {
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1562
|
+
const { request } = buildP2idTransactionRequest(this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), {
|
|
1563
|
+
salt,
|
|
1564
|
+
signatureAdviceMap,
|
|
1565
|
+
noteType: parseP2idNoteType(metadata.noteType),
|
|
1566
|
+
reclaimHeight: metadata.reclaimHeight,
|
|
1567
|
+
timelockHeight: metadata.timelockHeight,
|
|
1568
|
+
});
|
|
1366
1569
|
return request;
|
|
1367
1570
|
}
|
|
1368
1571
|
case 'custom':
|