@openzeppelin/miden-multisig-client 0.16.2 → 0.17.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 +144 -7
- 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.map +1 -1
- package/dist/client.js +5 -3
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +68 -15
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +4 -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 +106 -32
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +289 -95
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +581 -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/workflow.d.ts +3 -2
- 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 +6 -4
- package/dist/prover/workflow.test.js.map +1 -1
- 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 +80 -15
- package/src/client.ts +5 -3
- package/src/index.ts +26 -1
- package/src/inspector.test.ts +330 -38
- package/src/inspector.ts +196 -33
- package/src/multisig.test.ts +679 -63
- package/src/multisig.ts +361 -107
- package/src/procedures.ts +6 -7
- package/src/proposal/metadata.test.ts +76 -0
- package/src/proposal/metadata.ts +13 -2
- package/src/prover/workflow.test.ts +9 -4
- package/src/prover/workflow.ts +15 -3
- 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
|
@@ -4,28 +4,107 @@ import type {
|
|
|
4
4
|
TransactionSummary,
|
|
5
5
|
WasmWebClient,
|
|
6
6
|
} from '@miden-sdk/miden-sdk';
|
|
7
|
-
import { AccountId } from '@miden-sdk/miden-sdk';
|
|
7
|
+
import { AccountId, ChainAnchor, Word } from '@miden-sdk/miden-sdk';
|
|
8
8
|
import { getRawMidenClient } from '../raw-client.js';
|
|
9
|
+
import { base64ToUint8Array, uint8ArrayToBase64 } from '../utils/encoding.js';
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Index of the first user param carrying the auth-arg salt. The guarded-multisig
|
|
13
|
+
* auth component zeroes user params 0-2 and fills 3-6 with the auth args, matching
|
|
14
|
+
* `push.0.0.0` ahead of `multisig::auth_tx` in `guarded_multisig.masm`.
|
|
15
|
+
*/
|
|
16
|
+
const SALT_USER_PARAM_OFFSET = 3;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Captures a `ChainAnchor` for the request at the current sync height and
|
|
20
|
+
* executes the transaction against it to obtain the summary awaiting
|
|
21
|
+
* authorization. The anchor is returned alongside the summary so the proposer
|
|
22
|
+
* can ship it with the signed data; cosigners and the executor then reproduce
|
|
23
|
+
* the summary — which binds the reference block commitment since protocol
|
|
24
|
+
* 0.16 — with {@link executeForSummaryAt} regardless of their own sync height.
|
|
25
|
+
*/
|
|
10
26
|
export function executeForSummary(
|
|
11
27
|
client: MidenClient,
|
|
12
28
|
accountId: string,
|
|
13
29
|
txRequest: TransactionRequest,
|
|
14
30
|
midenRpcEndpoint: string,
|
|
15
|
-
): Promise<TransactionSummary>;
|
|
31
|
+
): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }>;
|
|
16
32
|
export function executeForSummary(
|
|
17
33
|
client: WasmWebClient,
|
|
18
34
|
accountId: string,
|
|
19
35
|
txRequest: TransactionRequest,
|
|
20
36
|
midenRpcEndpoint?: string,
|
|
21
|
-
): Promise<TransactionSummary>;
|
|
37
|
+
): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }>;
|
|
22
38
|
export async function executeForSummary(
|
|
23
39
|
client: MidenClient | WasmWebClient,
|
|
24
40
|
accountId: string,
|
|
25
41
|
txRequest: TransactionRequest,
|
|
26
42
|
midenRpcEndpoint?: string,
|
|
43
|
+
): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }> {
|
|
44
|
+
const acc = AccountId.fromHex(accountId);
|
|
45
|
+
const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
|
|
46
|
+
const anchor = await rawClient.chainAnchorForRequest(txRequest);
|
|
47
|
+
const summary = await rawClient.executeForSummaryAt(acc, txRequest, anchor);
|
|
48
|
+
return { summary, anchor };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Executes a transaction at the given `ChainAnchor`'s reference block to
|
|
53
|
+
* obtain the summary awaiting authorization — the anchored counterpart of
|
|
54
|
+
* {@link executeForSummary} for cosigners and executors holding a proposal's
|
|
55
|
+
* anchor.
|
|
56
|
+
*/
|
|
57
|
+
export function executeForSummaryAt(
|
|
58
|
+
client: MidenClient,
|
|
59
|
+
accountId: string,
|
|
60
|
+
txRequest: TransactionRequest,
|
|
61
|
+
anchor: ChainAnchor,
|
|
62
|
+
midenRpcEndpoint: string,
|
|
63
|
+
): Promise<TransactionSummary>;
|
|
64
|
+
export function executeForSummaryAt(
|
|
65
|
+
client: WasmWebClient,
|
|
66
|
+
accountId: string,
|
|
67
|
+
txRequest: TransactionRequest,
|
|
68
|
+
anchor: ChainAnchor,
|
|
69
|
+
midenRpcEndpoint?: string,
|
|
70
|
+
): Promise<TransactionSummary>;
|
|
71
|
+
export async function executeForSummaryAt(
|
|
72
|
+
client: MidenClient | WasmWebClient,
|
|
73
|
+
accountId: string,
|
|
74
|
+
txRequest: TransactionRequest,
|
|
75
|
+
anchor: ChainAnchor,
|
|
76
|
+
midenRpcEndpoint?: string,
|
|
27
77
|
): Promise<TransactionSummary> {
|
|
28
78
|
const acc = AccountId.fromHex(accountId);
|
|
29
79
|
const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
|
|
30
|
-
return rawClient.
|
|
80
|
+
return rawClient.executeForSummaryAt(acc, txRequest, anchor);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Serializes a `ChainAnchor` to base64 for the proposal wire payload.
|
|
85
|
+
*/
|
|
86
|
+
export function chainAnchorToBase64(anchor: ChainAnchor): string {
|
|
87
|
+
return uint8ArrayToBase64(anchor.serialize());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Deserializes a `ChainAnchor` from its base64 wire form. `ChainAnchor`
|
|
92
|
+
* deserialization validates the header/chain consistency internally, so a
|
|
93
|
+
* decoded anchor only needs its block commitment checked against the signed
|
|
94
|
+
* transaction summary before it is safe to execute against.
|
|
95
|
+
*/
|
|
96
|
+
export function chainAnchorFromBase64(anchorBase64: string): ChainAnchor {
|
|
97
|
+
return ChainAnchor.deserialize(base64ToUint8Array(anchorBase64));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reads the auth-arg salt back out of a transaction summary.
|
|
102
|
+
*
|
|
103
|
+
* Since miden-protocol 0.16-rc the summary binds seven user-defined elements
|
|
104
|
+
* instead of a dedicated salt word. The guarded-multisig auth component zeroes
|
|
105
|
+
* the leading three and passes the auth args as the trailing four, so the salt
|
|
106
|
+
* is the tail of `userParams()`.
|
|
107
|
+
*/
|
|
108
|
+
export function summarySalt(summary: TransactionSummary): Word {
|
|
109
|
+
return Word.newFromFelts(summary.userParams().slice(SALT_USER_PARAM_OFFSET));
|
|
31
110
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AdviceMap,
|
|
3
|
-
FeltArray,
|
|
4
2
|
type MidenClient,
|
|
5
3
|
TransactionRequest,
|
|
6
4
|
TransactionRequestBuilder,
|
|
@@ -9,37 +7,38 @@ import {
|
|
|
9
7
|
Word,
|
|
10
8
|
Word as WordType,
|
|
11
9
|
} from '@miden-sdk/miden-sdk';
|
|
12
|
-
import { GUARDIAN_ECDSA_MASM, GUARDIAN_MASM } from '../account/masm/auth.js';
|
|
13
10
|
import { compileTxScript } from '../raw-client.js';
|
|
14
11
|
import { normalizeHexWord } from '../utils/encoding.js';
|
|
15
12
|
import { randomWord } from '../utils/random.js';
|
|
13
|
+
import { authSchemeId } from '../utils/signature.js';
|
|
16
14
|
import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
|
|
17
15
|
import type { SignatureScheme } from '../types.js';
|
|
18
16
|
|
|
19
17
|
async function buildUpdateGuardianScript(
|
|
20
18
|
client: MidenClient | WasmWebClient,
|
|
19
|
+
newGuardianPubkey: string,
|
|
21
20
|
signatureScheme: SignatureScheme,
|
|
22
21
|
midenRpcEndpoint?: string,
|
|
23
22
|
): Promise<TransactionScript> {
|
|
24
|
-
|
|
25
|
-
const
|
|
23
|
+
// A word literal preserves the key's element order on the operand stack.
|
|
24
|
+
const keyLiteral = normalizeHexWord(newGuardianPubkey);
|
|
25
|
+
const schemeId = authSchemeId(signatureScheme);
|
|
26
26
|
|
|
27
|
+
// Calling the origin procedure yields the same MAST root as its component re-export.
|
|
27
28
|
const scriptSource = `
|
|
28
|
-
use
|
|
29
|
+
use miden::standards::auth::guardian
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
@transaction_script
|
|
32
|
+
pub proc main
|
|
33
|
+
push.${keyLiteral}
|
|
34
|
+
push.${schemeId}
|
|
33
35
|
call.guardian::update_guardian_public_key
|
|
36
|
+
drop
|
|
37
|
+
dropw
|
|
34
38
|
end
|
|
35
39
|
`;
|
|
36
40
|
|
|
37
|
-
return compileTxScript(
|
|
38
|
-
client,
|
|
39
|
-
scriptSource,
|
|
40
|
-
[{ namespace: guardianLibraryPath, code: guardianMasm }],
|
|
41
|
-
midenRpcEndpoint,
|
|
42
|
-
);
|
|
41
|
+
return compileTxScript(client, scriptSource, [], midenRpcEndpoint);
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
export function buildUpdateGuardianTransactionRequest(
|
|
@@ -60,25 +59,16 @@ export async function buildUpdateGuardianTransactionRequest(
|
|
|
60
59
|
const signatureScheme = options.signatureScheme ?? 'falcon';
|
|
61
60
|
const script = await buildUpdateGuardianScript(
|
|
62
61
|
client,
|
|
62
|
+
newGuardianPubkey,
|
|
63
63
|
signatureScheme,
|
|
64
64
|
options.midenRpcEndpoint,
|
|
65
65
|
);
|
|
66
66
|
|
|
67
67
|
const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
|
|
68
|
-
|
|
69
|
-
const pubkeyWordForAdvice = WordType.fromHex(normalizeHexWord(newGuardianPubkey));
|
|
70
|
-
const pubkeyWordForFelts = WordType.fromHex(normalizeHexWord(newGuardianPubkey));
|
|
71
|
-
const pubkeyWordForScript = WordType.fromHex(normalizeHexWord(newGuardianPubkey));
|
|
72
|
-
|
|
73
|
-
const advice = new AdviceMap();
|
|
74
|
-
advice.insert(pubkeyWordForAdvice, new FeltArray(pubkeyWordForFelts.toFelts()));
|
|
75
|
-
|
|
76
68
|
const authSaltForBuilder = WordType.fromHex(normalizeHexWord(authSaltHex));
|
|
77
69
|
|
|
78
70
|
let txBuilder = new TransactionRequestBuilder();
|
|
79
71
|
txBuilder = txBuilder.withCustomScript(script);
|
|
80
|
-
txBuilder = txBuilder.withScriptArg(pubkeyWordForScript);
|
|
81
|
-
txBuilder = txBuilder.extendAdviceMap(advice);
|
|
82
72
|
txBuilder = txBuilder.withAuthArg(authSaltForBuilder);
|
|
83
73
|
|
|
84
74
|
if (options.signatureAdviceMap) {
|
|
@@ -10,16 +10,11 @@ import {
|
|
|
10
10
|
Word,
|
|
11
11
|
Word as WordType,
|
|
12
12
|
} from '@miden-sdk/miden-sdk';
|
|
13
|
-
import {
|
|
14
|
-
MULTISIG_ECDSA_MASM,
|
|
15
|
-
MULTISIG_MASM,
|
|
16
|
-
} from '../account/masm/auth.js';
|
|
17
13
|
import { getProcedureRoot, type ProcedureName } from '../procedures.js';
|
|
18
14
|
import { compileTxScript } from '../raw-client.js';
|
|
19
15
|
import { normalizeHexWord } from '../utils/encoding.js';
|
|
20
16
|
import { randomWord } from '../utils/random.js';
|
|
21
17
|
import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
|
|
22
|
-
import type { SignatureScheme } from '../types.js';
|
|
23
18
|
|
|
24
19
|
function buildProcedureThresholdFelts(procedure: ProcedureName, threshold: number): Felt[] {
|
|
25
20
|
const procedureRoot = WordType.fromHex(normalizeHexWord(getProcedureRoot(procedure)));
|
|
@@ -32,48 +27,39 @@ function buildProcedureThresholdFelts(procedure: ProcedureName, threshold: numbe
|
|
|
32
27
|
];
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const configHash = Poseidon2.hashElements(
|
|
30
|
+
/**
|
|
31
|
+
* `set_procedure_threshold` reads its `[proc_threshold, PROC_ROOT]` inputs from the operand stack
|
|
32
|
+
* (pushed by the script), so no advice-map entry is attached; this hash is returned only for
|
|
33
|
+
* caller bookkeeping.
|
|
34
|
+
*/
|
|
35
|
+
function buildProcedureThresholdConfigHash(procedure: ProcedureName, threshold: number): Word {
|
|
36
|
+
return Poseidon2.hashElements(
|
|
43
37
|
new FeltArray(buildProcedureThresholdFelts(procedure, threshold)),
|
|
44
38
|
);
|
|
45
|
-
const payload = new FeltArray(buildProcedureThresholdFelts(procedure, threshold));
|
|
46
|
-
return { configHash, payload };
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
async function buildUpdateProcedureThresholdScript(
|
|
50
42
|
client: MidenClient | WasmWebClient,
|
|
51
43
|
procedure: ProcedureName,
|
|
52
44
|
threshold: number,
|
|
53
|
-
signatureScheme: SignatureScheme,
|
|
54
45
|
midenRpcEndpoint?: string,
|
|
55
46
|
): Promise<TransactionScript> {
|
|
56
|
-
const multisigMasm = signatureScheme === 'ecdsa' ? MULTISIG_ECDSA_MASM : MULTISIG_MASM;
|
|
57
47
|
const procedureRoot = normalizeHexWord(getProcedureRoot(procedure));
|
|
58
48
|
|
|
59
49
|
const scriptSource = `
|
|
60
|
-
use
|
|
50
|
+
use miden::standards::auth::multisig
|
|
61
51
|
|
|
62
|
-
|
|
52
|
+
@transaction_script
|
|
53
|
+
pub proc main
|
|
63
54
|
push.${procedureRoot}
|
|
64
55
|
push.${threshold}
|
|
65
|
-
call.multisig::
|
|
56
|
+
call.multisig::set_procedure_threshold
|
|
66
57
|
dropw
|
|
67
58
|
drop
|
|
68
59
|
end
|
|
69
60
|
`;
|
|
70
61
|
|
|
71
|
-
return compileTxScript(
|
|
72
|
-
client,
|
|
73
|
-
scriptSource,
|
|
74
|
-
[{ namespace: 'oz_multisig::multisig', code: multisigMasm }],
|
|
75
|
-
midenRpcEndpoint,
|
|
76
|
-
);
|
|
62
|
+
return compileTxScript(client, scriptSource, [], midenRpcEndpoint);
|
|
77
63
|
}
|
|
78
64
|
|
|
79
65
|
export function buildUpdateProcedureThresholdTransactionRequest(
|
|
@@ -94,14 +80,12 @@ export async function buildUpdateProcedureThresholdTransactionRequest(
|
|
|
94
80
|
threshold: number,
|
|
95
81
|
options: SignatureOptions = {},
|
|
96
82
|
): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
|
|
97
|
-
const
|
|
98
|
-
const { configHash } = buildProcedureThresholdAdvice(procedure, threshold);
|
|
83
|
+
const configHash = buildProcedureThresholdConfigHash(procedure, threshold);
|
|
99
84
|
|
|
100
85
|
const script = await buildUpdateProcedureThresholdScript(
|
|
101
86
|
client,
|
|
102
87
|
procedure,
|
|
103
88
|
threshold,
|
|
104
|
-
signatureScheme,
|
|
105
89
|
options.midenRpcEndpoint,
|
|
106
90
|
);
|
|
107
91
|
const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
|
|
@@ -11,66 +11,66 @@ import {
|
|
|
11
11
|
Word,
|
|
12
12
|
Word as WordType,
|
|
13
13
|
} from '@miden-sdk/miden-sdk';
|
|
14
|
-
import {
|
|
15
|
-
MULTISIG_ECDSA_MASM,
|
|
16
|
-
MULTISIG_MASM,
|
|
17
|
-
} from '../account/masm/auth.js';
|
|
18
14
|
import { compileTxScript } from '../raw-client.js';
|
|
19
15
|
import { normalizeHexWord } from '../utils/encoding.js';
|
|
20
16
|
import { randomWord } from '../utils/random.js';
|
|
17
|
+
import { authSchemeId } from '../utils/signature.js';
|
|
21
18
|
import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
|
|
22
19
|
import type { SignatureScheme } from '../types.js';
|
|
23
20
|
|
|
24
|
-
function buildMultisigConfigFelts(
|
|
21
|
+
function buildMultisigConfigFelts(
|
|
22
|
+
threshold: number,
|
|
23
|
+
signerCommitments: string[],
|
|
24
|
+
signatureScheme: SignatureScheme,
|
|
25
|
+
): Felt[] {
|
|
25
26
|
const numApprovers = signerCommitments.length;
|
|
27
|
+
const schemeId = authSchemeId(signatureScheme);
|
|
26
28
|
const felts: Felt[] = [
|
|
27
29
|
new Felt(BigInt(threshold)),
|
|
28
30
|
new Felt(BigInt(numApprovers)),
|
|
29
31
|
new Felt(0n),
|
|
30
32
|
new Felt(0n),
|
|
31
33
|
];
|
|
34
|
+
// Interleave [PUB_KEY, SCHEME_ID] per approver, in reverse index order.
|
|
32
35
|
for (const commitment of [...signerCommitments].reverse()) {
|
|
33
36
|
const word = WordType.fromHex(normalizeHexWord(commitment));
|
|
34
37
|
felts.push(...word.toFelts());
|
|
38
|
+
felts.push(new Felt(BigInt(schemeId)), new Felt(0n), new Felt(0n), new Felt(0n));
|
|
35
39
|
}
|
|
36
40
|
return felts;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
function buildMultisigConfigAdvice(
|
|
43
|
+
export function buildMultisigConfigAdvice(
|
|
40
44
|
threshold: number,
|
|
41
45
|
signerCommitments: string[],
|
|
46
|
+
signatureScheme: SignatureScheme,
|
|
42
47
|
): { configHash: Word; payload: FeltArray } {
|
|
43
|
-
// `Poseidon2.hashElements` consumes (frees) its `FeltArray` by value, so the
|
|
44
|
-
//
|
|
45
|
-
//
|
|
48
|
+
// `Poseidon2.hashElements` consumes (frees) its `FeltArray` by value, so the advice payload
|
|
49
|
+
// must be a separately built array — reusing the hashed array surfaces as "null pointer
|
|
50
|
+
// passed to rust" at the later `advice.insert`.
|
|
46
51
|
const configHash = Poseidon2.hashElements(
|
|
47
|
-
new FeltArray(buildMultisigConfigFelts(threshold, signerCommitments)),
|
|
52
|
+
new FeltArray(buildMultisigConfigFelts(threshold, signerCommitments, signatureScheme)),
|
|
53
|
+
);
|
|
54
|
+
const payload = new FeltArray(
|
|
55
|
+
buildMultisigConfigFelts(threshold, signerCommitments, signatureScheme),
|
|
48
56
|
);
|
|
49
|
-
const payload = new FeltArray(buildMultisigConfigFelts(threshold, signerCommitments));
|
|
50
57
|
return { configHash, payload };
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
async function buildUpdateSignersScript(
|
|
54
61
|
client: MidenClient | WasmWebClient,
|
|
55
|
-
signatureScheme: SignatureScheme,
|
|
56
62
|
midenRpcEndpoint?: string,
|
|
57
63
|
): Promise<TransactionScript> {
|
|
58
|
-
const multisigMasm = signatureScheme === 'ecdsa' ? MULTISIG_ECDSA_MASM : MULTISIG_MASM;
|
|
59
|
-
|
|
60
64
|
const scriptSource = `
|
|
61
|
-
use
|
|
65
|
+
use miden::standards::auth::multisig
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
@transaction_script
|
|
68
|
+
pub proc main
|
|
64
69
|
call.multisig::update_signers_and_threshold
|
|
65
70
|
end
|
|
66
71
|
`;
|
|
67
72
|
|
|
68
|
-
return compileTxScript(
|
|
69
|
-
client,
|
|
70
|
-
scriptSource,
|
|
71
|
-
[{ namespace: 'oz_multisig::multisig', code: multisigMasm }],
|
|
72
|
-
midenRpcEndpoint,
|
|
73
|
-
);
|
|
73
|
+
return compileTxScript(client, scriptSource, [], midenRpcEndpoint);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function buildUpdateSignersTransactionRequest(
|
|
@@ -92,20 +92,28 @@ export async function buildUpdateSignersTransactionRequest(
|
|
|
92
92
|
options: SignatureOptions = {},
|
|
93
93
|
): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
|
|
94
94
|
const signatureScheme = options.signatureScheme ?? 'falcon';
|
|
95
|
-
const { configHash: configHashForAdvice, payload } = buildMultisigConfigAdvice(
|
|
95
|
+
const { configHash: configHashForAdvice, payload } = buildMultisigConfigAdvice(
|
|
96
|
+
threshold,
|
|
97
|
+
signerCommitments,
|
|
98
|
+
signatureScheme,
|
|
99
|
+
);
|
|
96
100
|
|
|
97
|
-
const { configHash: configHashForScript } = buildMultisigConfigAdvice(
|
|
101
|
+
const { configHash: configHashForScript } = buildMultisigConfigAdvice(
|
|
102
|
+
threshold,
|
|
103
|
+
signerCommitments,
|
|
104
|
+
signatureScheme,
|
|
105
|
+
);
|
|
98
106
|
|
|
99
|
-
const { configHash: configHashForReturn } = buildMultisigConfigAdvice(
|
|
107
|
+
const { configHash: configHashForReturn } = buildMultisigConfigAdvice(
|
|
108
|
+
threshold,
|
|
109
|
+
signerCommitments,
|
|
110
|
+
signatureScheme,
|
|
111
|
+
);
|
|
100
112
|
|
|
101
113
|
const advice = new AdviceMap();
|
|
102
114
|
advice.insert(configHashForAdvice, payload);
|
|
103
115
|
|
|
104
|
-
const script = await buildUpdateSignersScript(
|
|
105
|
-
client,
|
|
106
|
-
signatureScheme,
|
|
107
|
-
options.midenRpcEndpoint,
|
|
108
|
-
);
|
|
116
|
+
const script = await buildUpdateSignersScript(client, options.midenRpcEndpoint);
|
|
109
117
|
|
|
110
118
|
const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
|
|
111
119
|
|
package/src/transaction.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
export {
|
|
2
2
|
buildConsumeNotesTransactionRequest,
|
|
3
3
|
} from './transaction/consumeNotes.js';
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
chainAnchorFromBase64,
|
|
6
|
+
chainAnchorToBase64,
|
|
7
|
+
executeForSummary,
|
|
8
|
+
executeForSummaryAt,
|
|
9
|
+
summarySalt,
|
|
10
|
+
} from './transaction/summary.js';
|
|
5
11
|
export {
|
|
6
12
|
buildP2idNoteFromMetadata,
|
|
7
13
|
buildP2idTransactionRequest,
|
|
8
14
|
parseP2idNoteType,
|
|
9
15
|
p2idNoteTypeToMetadata,
|
|
10
16
|
type P2idTransactionOptions,
|
|
17
|
+
type P2ideHeightOptions,
|
|
11
18
|
} from './transaction/p2id.js';
|
|
12
19
|
export {
|
|
13
20
|
buildUpdateGuardianTransactionRequest,
|
package/src/types/proposal.ts
CHANGED
|
@@ -37,6 +37,14 @@ interface BaseProposalMetadata {
|
|
|
37
37
|
description: string;
|
|
38
38
|
saltHex?: string;
|
|
39
39
|
requiredSignatures?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Base64-serialized Miden `ChainAnchor` pinning the reference block the
|
|
42
|
+
* proposal's transaction summary was built at. Required to verify or
|
|
43
|
+
* execute the proposal: since protocol 0.16 the signed summary binds the
|
|
44
|
+
* reference block commitment, so it only reproduces when re-executed at
|
|
45
|
+
* that block.
|
|
46
|
+
*/
|
|
47
|
+
chainAnchor?: string;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export interface UpdateSignersProposalMetadata extends BaseProposalMetadata {
|
|
@@ -89,6 +97,31 @@ export function isP2idNoteVisibility(value: string): value is P2idNoteVisibility
|
|
|
89
97
|
return value === 'public' || value === 'private';
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
/** Maximum P2IDE block height: heights are `u32` on-chain (`BlockNumber`). */
|
|
101
|
+
export const MAX_P2IDE_BLOCK_HEIGHT = 0xffff_ffff;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validates a P2IDE reclaim/timelock height (issue #366). Heights are `u32`
|
|
105
|
+
* block numbers; `0` is rejected because it is the on-chain encoding for "no
|
|
106
|
+
* constraint", so accepting it would silently build an unconstrained note.
|
|
107
|
+
* An invalid value throws rather than being dropped, which would rebuild a
|
|
108
|
+
* note that could never match the signed tx_summary commitment.
|
|
109
|
+
*/
|
|
110
|
+
export function parseP2ideHeight(
|
|
111
|
+
field: 'reclaimHeight' | 'timelockHeight',
|
|
112
|
+
value: number | undefined,
|
|
113
|
+
): number | undefined {
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_P2IDE_BLOCK_HEIGHT) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`unsupported ${field} '${value}': expected an integer between 1 and ${MAX_P2IDE_BLOCK_HEIGHT}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
export interface P2IdProposalMetadata extends BaseProposalMetadata {
|
|
93
126
|
proposalType: 'p2id';
|
|
94
127
|
recipientId: string;
|
|
@@ -96,14 +129,20 @@ export interface P2IdProposalMetadata extends BaseProposalMetadata {
|
|
|
96
129
|
amount: string;
|
|
97
130
|
/** Visibility of the created note. Absent on the wire => 'public' (pre-#322 proposals). */
|
|
98
131
|
noteType?: P2idNoteVisibility;
|
|
132
|
+
/**
|
|
133
|
+
* Absolute block height at which the sender may reclaim the note (issue
|
|
134
|
+
* #366). Presence of either height means the proposal creates a P2IDE note
|
|
135
|
+
* instead of a plain P2ID note; both absent => plain P2ID (pre-#366
|
|
136
|
+
* proposals).
|
|
137
|
+
*/
|
|
138
|
+
reclaimHeight?: number;
|
|
139
|
+
/** Absolute block height before which the note cannot be consumed. */
|
|
140
|
+
timelockHeight?: number;
|
|
99
141
|
}
|
|
100
142
|
|
|
101
143
|
export interface CustomProposalMetadata extends BaseProposalMetadata {
|
|
102
144
|
proposalType: 'custom';
|
|
103
|
-
/** Original server-defined proposal label,
|
|
104
|
-
* Rust `ProposalMetadata.proposal_type`; it is what lets a custom proposal
|
|
105
|
-
* round-trip back to GUARDIAN/export, so it is required in the domain model.
|
|
106
|
-
* Any wire-level optionality is resolved in the parser/codec boundary. */
|
|
145
|
+
/** Original server-defined proposal label, preserved during round trips. */
|
|
107
146
|
rawProposalType: string;
|
|
108
147
|
}
|
|
109
148
|
|
package/src/types.ts
CHANGED
|
@@ -80,7 +80,6 @@ export interface MultisigConfig {
|
|
|
80
80
|
signerCommitments: string[];
|
|
81
81
|
guardianCommitment: string;
|
|
82
82
|
guardianPublicKey?: string;
|
|
83
|
-
guardianEnabled?: boolean;
|
|
84
83
|
storageMode?: 'private' | 'public';
|
|
85
84
|
procedureThresholds?: ProcedureThreshold[];
|
|
86
85
|
signatureScheme?: SignatureScheme;
|
package/src/utils/signature.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { AdviceMap, Felt, FeltArray, Poseidon2, Signature, Word } from '@miden-sdk/miden-sdk';
|
|
2
2
|
import * as midenSdk from '@miden-sdk/miden-sdk';
|
|
3
|
+
import { EcdsaFormat } from './ecdsa.js';
|
|
3
4
|
import { hexToBytes, normalizeHexWord } from './encoding.js';
|
|
4
5
|
import type { ProposalSignatureEntry, SignatureScheme } from '../types.js';
|
|
5
6
|
|
|
6
7
|
export const ECDSA_AUTH_SCHEME_ID = 1;
|
|
7
8
|
export const FALCON_AUTH_SCHEME_ID = 2;
|
|
8
9
|
|
|
9
|
-
function authSchemeId(scheme: SignatureScheme): number {
|
|
10
|
+
export function authSchemeId(scheme: SignatureScheme): number {
|
|
10
11
|
return scheme === 'ecdsa' ? ECDSA_AUTH_SCHEME_ID : FALCON_AUTH_SCHEME_ID;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -21,31 +22,17 @@ export function signatureHexToBytes(
|
|
|
21
22
|
return withPrefix;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
felts.push(new Felt(BigInt(packed >>> 0)));
|
|
32
|
-
}
|
|
33
|
-
return felts;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function encodeEcdsaSignatureFelts(pubkeyBytes: Uint8Array, sigBytes: Uint8Array): Felt[] {
|
|
37
|
-
const pkFelts = bytesToPackedU32Felts(pubkeyBytes);
|
|
38
|
-
const sigFelts = bytesToPackedU32Felts(sigBytes);
|
|
39
|
-
return [...pkFelts, ...sigFelts];
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
25
|
+
/**
|
|
26
|
+
* `toPreparedSignature` is the SDK binding for the Rust
|
|
27
|
+
* `Signature::to_encoded_signature`, so both Falcon and ECDSA advice payloads
|
|
28
|
+
* come from upstream rather than being packed here. For ECDSA it emits
|
|
29
|
+
* `QX[8] || QY[8] || SIG_R[8] || SIG_S[8]` and recovers the public key from the
|
|
30
|
+
* message, which is why the signature must carry its recovery byte.
|
|
31
|
+
*/
|
|
43
32
|
export function buildSignatureAdviceEntry(
|
|
44
33
|
pubkeyCommitment: Word,
|
|
45
34
|
message: Word,
|
|
46
35
|
signature: Signature,
|
|
47
|
-
ecdsaPubkeyHex?: string,
|
|
48
|
-
ecdsaSigHex?: string,
|
|
49
36
|
): { key: Word; values: Felt[] } {
|
|
50
37
|
const elements = new FeltArray([
|
|
51
38
|
...pubkeyCommitment.toFelts(),
|
|
@@ -53,16 +40,31 @@ export function buildSignatureAdviceEntry(
|
|
|
53
40
|
]);
|
|
54
41
|
const key = Poseidon2.hashElements(elements);
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
43
|
+
return { key, values: signature.toPreparedSignature(message) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Rejects unrecoverable ECDSA signatures before entering WASM. */
|
|
47
|
+
export function assertEcdsaSignatureRecoverable(
|
|
48
|
+
signatureHex: string,
|
|
49
|
+
messageHex: string,
|
|
50
|
+
expectedPublicKeyHex: string,
|
|
51
|
+
): void {
|
|
52
|
+
let recovered: string;
|
|
53
|
+
try {
|
|
54
|
+
recovered = EcdsaFormat.recoverCompressedPublicKeyHex(
|
|
55
|
+
hexToBytes(messageHex),
|
|
56
|
+
hexToBytes(signatureHex),
|
|
57
|
+
);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`ECDSA signature does not recover a public key: ${String(error)}`);
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
|
|
62
|
+
const expected = EcdsaFormat.compressPublicKey(expectedPublicKeyHex);
|
|
63
|
+
if (recovered.toLowerCase() !== expected.toLowerCase()) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`ECDSA signature recovers public key ${recovered}, which does not match the expected ${expected}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export function tryComputeEcdsaCommitmentHex(pubkeyHex: string): string | null {
|
|
@@ -87,41 +89,6 @@ export function tryComputeCommitmentHex(
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
export function verifyEcdsaCommitment(
|
|
91
|
-
pubkeyHex: string,
|
|
92
|
-
expectedCommitmentHex: string,
|
|
93
|
-
): { match: boolean; computedHex: string; packedFelts: string[]; error?: string } {
|
|
94
|
-
try {
|
|
95
|
-
const bytes = hexToBytes(pubkeyHex);
|
|
96
|
-
const packedU32Values: number[] = [];
|
|
97
|
-
for (let i = 0; i < bytes.length; i += 4) {
|
|
98
|
-
let packed = 0;
|
|
99
|
-
for (let j = 0; j < 4 && i + j < bytes.length; j += 1) {
|
|
100
|
-
packed |= bytes[i + j] << (j * 8);
|
|
101
|
-
}
|
|
102
|
-
packedU32Values.push(packed >>> 0);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const packedFelts = bytesToPackedU32Felts(bytes);
|
|
106
|
-
const feltArray = new FeltArray(packedFelts);
|
|
107
|
-
const computed = Poseidon2.hashElements(feltArray);
|
|
108
|
-
const computedHex = normalizeHexWord(computed.toHex());
|
|
109
|
-
const expectedNorm = normalizeHexWord(expectedCommitmentHex);
|
|
110
|
-
return {
|
|
111
|
-
match: computedHex === expectedNorm,
|
|
112
|
-
computedHex,
|
|
113
|
-
packedFelts: packedU32Values.map((value) => value.toString()),
|
|
114
|
-
};
|
|
115
|
-
} catch (error) {
|
|
116
|
-
return {
|
|
117
|
-
match: false,
|
|
118
|
-
computedHex: `ERROR: ${error}`,
|
|
119
|
-
packedFelts: [],
|
|
120
|
-
error: String(error),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
92
|
export function mergeSignatureAdviceMaps(
|
|
126
93
|
advice: AdviceMap,
|
|
127
94
|
entries: Array<{ key: Word; values: Felt[] }>,
|
package/src/utils/word.ts
CHANGED
|
@@ -15,6 +15,23 @@ export function wordElementToBigInt(word: Word, index: number): bigint {
|
|
|
15
15
|
return index < elements.length ? elements[index] : 0n;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* True when every element of the word is zero. The SDK's storage-map reads
|
|
20
|
+
* return `Word::empty()` for a key with no entry (`StorageMap::get` is
|
|
21
|
+
* `unwrap_or_default()` in miden-protocol), so readers use this to detect
|
|
22
|
+
* absent entries.
|
|
23
|
+
*/
|
|
24
|
+
export function isEmptyWord(word: Word): boolean {
|
|
25
|
+
const elements: BigUint64Array | bigint[] =
|
|
26
|
+
typeof word.toU64s === 'function' ? word.toU64s() : word.toFelts().map(f => f.asInt());
|
|
27
|
+
for (const element of elements) {
|
|
28
|
+
if (element !== 0n) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
export function wordToBytes(word: { toFelts: () => Array<{ asInt: () => bigint }> }): Uint8Array {
|
|
19
36
|
const felts = word.toFelts();
|
|
20
37
|
const buf = new Uint8Array(32);
|