@openzeppelin/miden-multisig-client 0.12.6 → 0.12.8
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 +70 -75
- package/dist/account/builder.d.ts +0 -18
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +16 -24
- package/dist/account/builder.js.map +1 -1
- package/dist/account/masm.d.ts +2 -0
- package/dist/account/masm.d.ts.map +1 -1
- package/dist/account/masm.js +428 -1
- package/dist/account/masm.js.map +1 -1
- package/dist/account/storage.d.ts.map +1 -1
- package/dist/account/storage.js +5 -6
- package/dist/account/storage.js.map +1 -1
- package/dist/client.d.ts +5 -57
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -53
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +28 -0
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +3 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -45
- package/dist/index.js.map +1 -1
- package/dist/inspector.d.ts +4 -30
- package/dist/inspector.d.ts.map +1 -1
- package/dist/inspector.js +13 -43
- package/dist/inspector.js.map +1 -1
- package/dist/multisig/helpers.d.ts.map +1 -1
- package/dist/multisig/helpers.js.map +1 -1
- package/dist/multisig.d.ts +39 -161
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +190 -241
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +41 -31
- package/dist/multisig.test.js.map +1 -1
- package/dist/procedures.d.ts +20 -47
- package/dist/procedures.d.ts.map +1 -1
- package/dist/procedures.js +17 -50
- package/dist/procedures.js.map +1 -1
- package/dist/signer.d.ts +11 -26
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +41 -26
- package/dist/signer.js.map +1 -1
- package/dist/transaction/options.d.ts +2 -0
- package/dist/transaction/options.d.ts.map +1 -1
- package/dist/transaction/updatePsm.d.ts.map +1 -1
- package/dist/transaction/updatePsm.js +21 -3
- package/dist/transaction/updatePsm.js.map +1 -1
- package/dist/transaction/updateSigners.d.ts.map +1 -1
- package/dist/transaction/updateSigners.js +21 -3
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +0 -2
- package/dist/transaction.js.map +1 -1
- package/dist/transaction.test.js +5 -0
- package/dist/transaction.test.js.map +1 -1
- package/dist/types/proposal.d.ts +16 -8
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types.d.ts +18 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/signature.d.ts +10 -2
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +77 -5
- package/dist/utils/signature.js.map +1 -1
- package/dist/utils/signature.test.js +16 -1
- package/dist/utils/signature.test.js.map +1 -1
- package/masm/multisig_ecdsa.masm +424 -0
- package/masm/psm_ecdsa.masm +179 -0
- package/package.json +2 -2
- package/src/account/builder.ts +18 -25
- package/src/account/masm.ts +433 -2
- package/src/account/storage.ts +5 -6
- package/src/client.test.ts +36 -0
- package/src/client.ts +10 -59
- package/src/index.ts +13 -62
- package/src/inspector.ts +15 -44
- package/src/multisig/helpers.ts +1 -2
- package/src/multisig.test.ts +45 -34
- package/src/multisig.ts +246 -273
- package/src/procedures.ts +21 -56
- package/src/signer.ts +48 -28
- package/src/transaction/options.ts +2 -0
- package/src/transaction/updatePsm.ts +24 -3
- package/src/transaction/updateSigners.ts +25 -3
- package/src/transaction.test.ts +6 -0
- package/src/transaction.ts +0 -2
- package/src/types/proposal.ts +15 -8
- package/src/types.ts +26 -16
- package/src/utils/signature.test.ts +24 -1
- package/src/utils/signature.ts +86 -4
package/src/procedures.ts
CHANGED
|
@@ -1,72 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
* known procedure roots for multisig accounts.
|
|
3
|
-
*
|
|
4
|
-
* IMPORTANT: These values are extracted from the TypeScript SDK's compiled MASM output,
|
|
5
|
-
* NOT from the Rust crate. The SDK may compile MASM differently, resulting in different
|
|
6
|
-
* procedure roots. Use the debug output from builder.ts to update these values.
|
|
7
|
-
*/
|
|
1
|
+
import type { SignatureScheme } from '@openzeppelin/psm-client';
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
* Static mapping of procedure names to their deterministic roots.
|
|
11
|
-
*
|
|
12
|
-
* Component ordering: Multisig (auth) -> PSM -> BasicWallet
|
|
13
|
-
*/
|
|
14
|
-
export const PROCEDURE_ROOTS = {
|
|
15
|
-
// Multisig component procedures
|
|
16
|
-
/** Update signer list and threshold configuration */
|
|
3
|
+
export const PROCEDURE_ROOTS_FALCON = {
|
|
17
4
|
update_signers: '0x53d0ad381a193de0cf6af3730141e498274103dfc3b8c8e7367bd49d4a66c72b',
|
|
18
|
-
/** Authenticate transaction with multisig (Falcon512) */
|
|
19
5
|
auth_tx: '0x474c613b38001cc36d68557e9d881495d6a461a9027033445c7672c586509026',
|
|
20
|
-
|
|
21
|
-
// PSM component procedures
|
|
22
|
-
/** Update PSM public key */
|
|
23
6
|
update_psm: '0xb103236807e5bf09c27efc2c5287ca8b03ab9efc1d852b62ebd31f5f1927ec26',
|
|
24
|
-
/** Verify PSM signature */
|
|
25
7
|
verify_psm: '0x30727fc23c6105a678fea8b4c1920f35fa85c03f16a0cf98372c8f56701f8a87',
|
|
26
|
-
|
|
27
|
-
// BasicWallet procedures
|
|
28
|
-
/** Send assets from account (move_asset_to_note) */
|
|
29
8
|
send_asset: '0x0e406b067ed2bcd7de745ca6517f519fd1a9be245f913347ac673ca1db30c1d6',
|
|
30
|
-
/** Receive assets into account */
|
|
31
9
|
receive_asset: '0x6f4bdbdc4b13d7ed933d590d88ac9dfb98020c9e917697845b5e169395b76a01',
|
|
32
10
|
} as const;
|
|
33
11
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
12
|
+
export const PROCEDURE_ROOTS_ECDSA = {
|
|
13
|
+
update_signers: '0x930f9ea86d33c7b3b2d23c4e9ac2492add461359344ed25b1acd38f3713dd79c',
|
|
14
|
+
auth_tx: '0x2bc7664a9dd47b36e7c8b8c3df03412798e4410173f36acfe03d191a38add053',
|
|
15
|
+
update_psm: '0x26ec27195f1fd3eb622b851dfc9eab038bca87522cfc7ec209bfe507682303b1',
|
|
16
|
+
verify_psm: '0xbeb9e08a83eca030968f2f137b5673136f21bc16c82fd408c2ce2495ccbdcd15',
|
|
17
|
+
send_asset: '0xd6c130dba13c67ac4733915f24bea9d19f517f51a65c74ded7bcd27e066b400e',
|
|
18
|
+
receive_asset: '0x016ab79593165e5b849776919e0c0298fb9dac880d593d93edd7134bdcdb4b6f',
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export const PROCEDURE_ROOTS = PROCEDURE_ROOTS_FALCON;
|
|
22
|
+
|
|
23
|
+
export type ProcedureName = keyof typeof PROCEDURE_ROOTS_FALCON;
|
|
38
24
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* @param name - The procedure name
|
|
43
|
-
* @returns The procedure root as a hex string
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```typescript
|
|
47
|
-
* const root = getProcedureRoot('receive_asset');
|
|
48
|
-
* // '0x6f4bdbdc4b13d7ed933d590d88ac9dfb98020c9e917697845b5e169395b76a01'
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
|
-
export function getProcedureRoot(name: ProcedureName): string {
|
|
52
|
-
return PROCEDURE_ROOTS[name];
|
|
25
|
+
export function getProcedureRoot(name: ProcedureName, scheme: SignatureScheme = 'falcon'): string {
|
|
26
|
+
const roots = scheme === 'ecdsa' ? PROCEDURE_ROOTS_ECDSA : PROCEDURE_ROOTS_FALCON;
|
|
27
|
+
return roots[name];
|
|
53
28
|
}
|
|
54
29
|
|
|
55
|
-
/**
|
|
56
|
-
* Check if a string is a valid procedure name.
|
|
57
|
-
*
|
|
58
|
-
* @param name - The string to check
|
|
59
|
-
* @returns true if the string is a valid procedure name
|
|
60
|
-
*/
|
|
61
30
|
export function isProcedureName(name: string): name is ProcedureName {
|
|
62
|
-
return name in
|
|
31
|
+
return name in PROCEDURE_ROOTS_FALCON;
|
|
63
32
|
}
|
|
64
33
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* @returns Array of all valid procedure names
|
|
69
|
-
*/
|
|
70
|
-
export function getProcedureNames(): ProcedureName[] {
|
|
71
|
-
return Object.keys(PROCEDURE_ROOTS) as ProcedureName[];
|
|
34
|
+
export function getProcedureNames(scheme: SignatureScheme = 'falcon'): ProcedureName[] {
|
|
35
|
+
const roots = scheme === 'ecdsa' ? PROCEDURE_ROOTS_ECDSA : PROCEDURE_ROOTS_FALCON;
|
|
36
|
+
return Object.keys(roots) as ProcedureName[];
|
|
72
37
|
}
|
package/src/signer.ts
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Falcon Signer implementation for PSM client authentication.
|
|
3
|
-
*
|
|
4
|
-
* This implements the Signer interface using the Miden SDK's SecretKey
|
|
5
|
-
* for Falcon signatures.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { SecretKey, Word, AccountId, Felt, FeltArray, Rpo256 } from '@demox-labs/miden-sdk';
|
|
9
|
-
import type { Signer } from './types.js';
|
|
2
|
+
import type { Signer, SignatureScheme } from './types.js';
|
|
10
3
|
import { bytesToHex } from './utils/encoding.js';
|
|
11
4
|
|
|
12
|
-
/**
|
|
13
|
-
* A Falcon signer that implements the Signer interface.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```typescript
|
|
17
|
-
* const secretKey = SecretKey.rpoFalconWithRNG(seed);
|
|
18
|
-
* const signer = new FalconSigner(secretKey);
|
|
19
|
-
* console.log(signer.commitment);
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
5
|
export class FalconSigner implements Signer {
|
|
23
6
|
readonly commitment: string;
|
|
24
7
|
readonly publicKey: string;
|
|
8
|
+
readonly scheme: SignatureScheme = 'falcon';
|
|
25
9
|
private readonly secretKey: SecretKey;
|
|
26
10
|
|
|
27
11
|
constructor(secretKey: SecretKey) {
|
|
@@ -33,14 +17,7 @@ export class FalconSigner implements Signer {
|
|
|
33
17
|
this.publicKey = bytesToHex(falconPubKey);
|
|
34
18
|
}
|
|
35
19
|
|
|
36
|
-
/**
|
|
37
|
-
* Signs an account ID with a timestamp.
|
|
38
|
-
* @param accountId - The account ID to sign
|
|
39
|
-
* @param timestamp - Unix timestamp in milliseconds (use Date.now())
|
|
40
|
-
* @returns Hex-encoded Falcon signature
|
|
41
|
-
*/
|
|
42
20
|
signAccountIdWithTimestamp(accountId: string, timestamp: number): string {
|
|
43
|
-
// Parse the account ID from hex
|
|
44
21
|
const paddedHex = accountId.startsWith('0x') ? accountId : `0x${accountId}`;
|
|
45
22
|
const parsedAccountId = AccountId.fromHex(paddedHex);
|
|
46
23
|
const prefix = parsedAccountId.prefix();
|
|
@@ -60,9 +37,6 @@ export class FalconSigner implements Signer {
|
|
|
60
37
|
return bytesToHex(falconSignature);
|
|
61
38
|
}
|
|
62
39
|
|
|
63
|
-
/**
|
|
64
|
-
* Signs a commitment/word for proposal signing.
|
|
65
|
-
*/
|
|
66
40
|
signCommitment(commitmentHex: string): string {
|
|
67
41
|
const paddedHex = commitmentHex.startsWith('0x') ? commitmentHex : `0x${commitmentHex}`;
|
|
68
42
|
const cleanHex = paddedHex.slice(2).padStart(64, '0');
|
|
@@ -73,3 +47,49 @@ export class FalconSigner implements Signer {
|
|
|
73
47
|
return bytesToHex(falconSignature);
|
|
74
48
|
}
|
|
75
49
|
}
|
|
50
|
+
|
|
51
|
+
export class EcdsaSigner implements Signer {
|
|
52
|
+
readonly commitment: string;
|
|
53
|
+
readonly publicKey: string;
|
|
54
|
+
readonly scheme: SignatureScheme = 'ecdsa';
|
|
55
|
+
private readonly secretKey: SecretKey;
|
|
56
|
+
|
|
57
|
+
constructor(secretKey: SecretKey) {
|
|
58
|
+
this.secretKey = secretKey;
|
|
59
|
+
const pubKey = secretKey.publicKey();
|
|
60
|
+
this.commitment = pubKey.toCommitment().toHex();
|
|
61
|
+
const serialized = pubKey.serialize();
|
|
62
|
+
const ecdsaPubKey = serialized.slice(1);
|
|
63
|
+
this.publicKey = bytesToHex(ecdsaPubKey);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
signAccountIdWithTimestamp(accountId: string, timestamp: number): string {
|
|
67
|
+
const paddedHex = accountId.startsWith('0x') ? accountId : `0x${accountId}`;
|
|
68
|
+
const parsedAccountId = AccountId.fromHex(paddedHex);
|
|
69
|
+
const prefix = parsedAccountId.prefix();
|
|
70
|
+
const suffix = parsedAccountId.suffix();
|
|
71
|
+
|
|
72
|
+
const feltArray = new FeltArray([
|
|
73
|
+
prefix,
|
|
74
|
+
suffix,
|
|
75
|
+
new Felt(BigInt(timestamp)),
|
|
76
|
+
new Felt(BigInt(0)),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const digest = Rpo256.hashElements(feltArray);
|
|
80
|
+
const signature = this.secretKey.sign(digest);
|
|
81
|
+
const signatureBytes = signature.serialize();
|
|
82
|
+
const ecdsaSignature = signatureBytes.slice(1);
|
|
83
|
+
return bytesToHex(ecdsaSignature);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
signCommitment(commitmentHex: string): string {
|
|
87
|
+
const paddedHex = commitmentHex.startsWith('0x') ? commitmentHex : `0x${commitmentHex}`;
|
|
88
|
+
const cleanHex = paddedHex.slice(2).padStart(64, '0');
|
|
89
|
+
const word = Word.fromHex(`0x${cleanHex}`);
|
|
90
|
+
const signature = this.secretKey.sign(word);
|
|
91
|
+
const signatureBytes = signature.serialize();
|
|
92
|
+
const ecdsaSignature = signatureBytes.slice(1);
|
|
93
|
+
return bytesToHex(ecdsaSignature);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
Word,
|
|
9
9
|
Word as WordType,
|
|
10
10
|
} from '@demox-labs/miden-sdk';
|
|
11
|
-
import { PSM_MASM } from '../account/masm.js';
|
|
11
|
+
import { PSM_ECDSA_MASM, PSM_MASM } from '../account/masm.js';
|
|
12
12
|
import { normalizeHexWord } from '../utils/encoding.js';
|
|
13
13
|
import { randomWord } from '../utils/random.js';
|
|
14
14
|
import type { SignatureOptions } from './options.js';
|
|
15
15
|
|
|
16
|
-
function
|
|
16
|
+
function buildUpdatePsmFalconScript(webClient: WebClient): TransactionScript {
|
|
17
17
|
const libBuilder = webClient.createScriptBuilder();
|
|
18
18
|
const psmLib = libBuilder.buildLibrary('openzeppelin::psm', PSM_MASM);
|
|
19
19
|
libBuilder.linkDynamicLibrary(psmLib);
|
|
@@ -31,12 +31,33 @@ end
|
|
|
31
31
|
return libBuilder.compileTxScript(scriptSource);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function buildUpdatePsmEcdsaScript(webClient: WebClient): TransactionScript {
|
|
35
|
+
const libBuilder = webClient.createScriptBuilder();
|
|
36
|
+
const psmLib = libBuilder.buildLibrary('openzeppelin::psm_ecdsa', PSM_ECDSA_MASM);
|
|
37
|
+
libBuilder.linkDynamicLibrary(psmLib);
|
|
38
|
+
|
|
39
|
+
const scriptSource = `
|
|
40
|
+
use.openzeppelin::psm_ecdsa
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
adv.push_mapval
|
|
44
|
+
dropw
|
|
45
|
+
call.psm_ecdsa::update_psm_public_key
|
|
46
|
+
end
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
return libBuilder.compileTxScript(scriptSource);
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
export async function buildUpdatePsmTransactionRequest(
|
|
35
53
|
webClient: WebClient,
|
|
36
54
|
newPsmPubkey: string,
|
|
37
55
|
options: SignatureOptions = {},
|
|
38
56
|
): Promise<{ request: TransactionRequest; salt: Word }> {
|
|
39
|
-
const
|
|
57
|
+
const signatureScheme = options.signatureScheme ?? 'falcon';
|
|
58
|
+
const script = signatureScheme === 'ecdsa'
|
|
59
|
+
? buildUpdatePsmEcdsaScript(webClient)
|
|
60
|
+
: buildUpdatePsmFalconScript(webClient);
|
|
40
61
|
|
|
41
62
|
const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
|
|
42
63
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Word,
|
|
11
11
|
Word as WordType,
|
|
12
12
|
} from '@demox-labs/miden-sdk';
|
|
13
|
-
import { MULTISIG_MASM, PSM_MASM } from '../account/masm.js';
|
|
13
|
+
import { MULTISIG_ECDSA_MASM, MULTISIG_MASM, PSM_ECDSA_MASM, PSM_MASM } from '../account/masm.js';
|
|
14
14
|
import { normalizeHexWord } from '../utils/encoding.js';
|
|
15
15
|
import { randomWord } from '../utils/random.js';
|
|
16
16
|
import type { SignatureOptions } from './options.js';
|
|
@@ -35,7 +35,7 @@ function buildMultisigConfigAdvice(
|
|
|
35
35
|
return { configHash, payload };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function
|
|
38
|
+
function buildUpdateSignersFalconScript(webClient: WebClient): TransactionScript {
|
|
39
39
|
const libBuilder = webClient.createScriptBuilder();
|
|
40
40
|
const psmLib = libBuilder.buildLibrary('openzeppelin::psm', PSM_MASM);
|
|
41
41
|
libBuilder.linkStaticLibrary(psmLib);
|
|
@@ -54,12 +54,32 @@ end
|
|
|
54
54
|
return libBuilder.compileTxScript(scriptSource);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function buildUpdateSignersEcdsaScript(webClient: WebClient): TransactionScript {
|
|
58
|
+
const libBuilder = webClient.createScriptBuilder();
|
|
59
|
+
const psmLib = libBuilder.buildLibrary('openzeppelin::psm_ecdsa', PSM_ECDSA_MASM);
|
|
60
|
+
libBuilder.linkStaticLibrary(psmLib);
|
|
61
|
+
|
|
62
|
+
const multisigLib = libBuilder.buildLibrary('auth::multisig', MULTISIG_ECDSA_MASM);
|
|
63
|
+
libBuilder.linkDynamicLibrary(multisigLib);
|
|
64
|
+
|
|
65
|
+
const scriptSource = `
|
|
66
|
+
use.auth::multisig
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
call.multisig::update_signers_and_threshold
|
|
70
|
+
end
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
return libBuilder.compileTxScript(scriptSource);
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
export async function buildUpdateSignersTransactionRequest(
|
|
58
77
|
webClient: WebClient,
|
|
59
78
|
threshold: number,
|
|
60
79
|
signerCommitments: string[],
|
|
61
80
|
options: SignatureOptions = {},
|
|
62
81
|
): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
|
|
82
|
+
const signatureScheme = options.signatureScheme ?? 'falcon';
|
|
63
83
|
const { configHash: configHashForAdvice, payload } = buildMultisigConfigAdvice(threshold, signerCommitments);
|
|
64
84
|
|
|
65
85
|
const { configHash: configHashForScript } = buildMultisigConfigAdvice(threshold, signerCommitments);
|
|
@@ -69,7 +89,9 @@ export async function buildUpdateSignersTransactionRequest(
|
|
|
69
89
|
const advice = new AdviceMap();
|
|
70
90
|
advice.insert(configHashForAdvice, payload);
|
|
71
91
|
|
|
72
|
-
const script =
|
|
92
|
+
const script = signatureScheme === 'ecdsa'
|
|
93
|
+
? buildUpdateSignersEcdsaScript(webClient)
|
|
94
|
+
: buildUpdateSignersFalconScript(webClient);
|
|
73
95
|
|
|
74
96
|
const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
|
|
75
97
|
|
package/src/transaction.test.ts
CHANGED
|
@@ -116,6 +116,12 @@ describe('transaction utilities', () => {
|
|
|
116
116
|
expect(result.slice(1)).toEqual(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
it('should prepend auth scheme byte (1 = ECDSA)', () => {
|
|
120
|
+
const result = signatureHexToBytes('deadbeef', 'ecdsa');
|
|
121
|
+
expect(result[0]).toBe(1); // ECDSA scheme byte
|
|
122
|
+
expect(result.slice(1)).toEqual(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
|
|
123
|
+
});
|
|
124
|
+
|
|
119
125
|
it('should handle 0x prefix', () => {
|
|
120
126
|
const result = signatureHexToBytes('0xaabbccdd');
|
|
121
127
|
expect(result[0]).toBe(0);
|
package/src/transaction.ts
CHANGED
package/src/types/proposal.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type { ProposalType as PsmProposalType } from '@openzeppelin/psm-client';
|
|
1
|
+
import type { ProposalType as PsmProposalType, SignatureScheme } from '@openzeppelin/psm-client';
|
|
2
2
|
export type ProposalType = Exclude<PsmProposalType, 'custom'>;
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type TransactionProposalStatus =
|
|
5
5
|
| { type: 'pending'; signaturesCollected: number; signaturesRequired: number; signers: string[] }
|
|
6
6
|
| { type: 'ready' }
|
|
7
7
|
| { type: 'finalized' };
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface TransactionProposalSignature {
|
|
10
10
|
signerId: string;
|
|
11
|
-
signature: { scheme: 'falcon'; signature: string };
|
|
11
|
+
signature: { scheme: 'falcon' | 'ecdsa'; signature: string; publicKey?: string };
|
|
12
12
|
timestamp: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -55,17 +55,18 @@ export type ProposalMetadata =
|
|
|
55
55
|
| P2IdProposalMetadata
|
|
56
56
|
| UnknownProposalMetadata;
|
|
57
57
|
|
|
58
|
-
export interface
|
|
58
|
+
export interface TransactionProposal {
|
|
59
59
|
id: string;
|
|
60
|
+
commitment: string;
|
|
60
61
|
accountId: string;
|
|
61
62
|
nonce: number;
|
|
62
|
-
status:
|
|
63
|
+
status: TransactionProposalStatus;
|
|
63
64
|
txSummary: string;
|
|
64
|
-
signatures:
|
|
65
|
+
signatures: TransactionProposalSignature[];
|
|
65
66
|
metadata: ProposalMetadata;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
export interface
|
|
69
|
+
export interface ExportedTransactionProposal {
|
|
69
70
|
accountId: string;
|
|
70
71
|
nonce: number;
|
|
71
72
|
commitment: string;
|
|
@@ -78,3 +79,9 @@ export interface ExportedProposal {
|
|
|
78
79
|
metadata?: ProposalMetadata;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
export interface SignTransactionProposalParams {
|
|
83
|
+
commitment: string;
|
|
84
|
+
signature: string;
|
|
85
|
+
publicKey?: string;
|
|
86
|
+
scheme?: SignatureScheme;
|
|
87
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { Account } from '@demox-labs/miden-sdk';
|
|
2
|
+
import type { SignatureScheme } from '@openzeppelin/psm-client';
|
|
2
3
|
import type { ProcedureName } from './procedures.js';
|
|
4
|
+
import type { TransactionProposal } from './types/proposal.js';
|
|
5
|
+
import type { AccountState } from './multisig.js';
|
|
6
|
+
import type { DetectedMultisigConfig } from './inspector.js';
|
|
3
7
|
|
|
4
8
|
export type {
|
|
5
9
|
Signer,
|
|
6
10
|
FalconSignature,
|
|
11
|
+
EcdsaSignature,
|
|
12
|
+
ProposalSignature,
|
|
13
|
+
SignatureScheme,
|
|
7
14
|
CosignerSignature,
|
|
8
15
|
AuthConfig,
|
|
9
16
|
DeltaStatus,
|
|
@@ -19,14 +26,27 @@ export type {
|
|
|
19
26
|
} from '@openzeppelin/psm-client';
|
|
20
27
|
|
|
21
28
|
export type {
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
TransactionProposal,
|
|
30
|
+
TransactionProposalStatus,
|
|
31
|
+
TransactionProposalSignature,
|
|
32
|
+
ExportedTransactionProposal,
|
|
33
|
+
SignTransactionProposalParams,
|
|
24
34
|
ProposalMetadata,
|
|
25
|
-
ProposalSignatureEntry,
|
|
26
|
-
ProposalStatus,
|
|
27
35
|
ProposalType,
|
|
28
36
|
} from './types/proposal.js';
|
|
29
37
|
|
|
38
|
+
export interface SyncResult {
|
|
39
|
+
proposals: TransactionProposal[];
|
|
40
|
+
state: AccountState;
|
|
41
|
+
notes: ConsumableNote[];
|
|
42
|
+
config: DetectedMultisigConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TransactionProposalResult {
|
|
46
|
+
proposal: TransactionProposal;
|
|
47
|
+
proposals: TransactionProposal[];
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
export interface MultisigAccountState {
|
|
31
51
|
id: string;
|
|
32
52
|
nonce: number;
|
|
@@ -34,20 +54,8 @@ export interface MultisigAccountState {
|
|
|
34
54
|
cosignerCommitments: string[];
|
|
35
55
|
}
|
|
36
56
|
|
|
37
|
-
/**
|
|
38
|
-
* Per-procedure threshold override.
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```typescript
|
|
42
|
-
* const thresholds: ProcedureThreshold[] = [
|
|
43
|
-
* { procedure: 'receive_asset', threshold: 1 },
|
|
44
|
-
* { procedure: 'update_signers', threshold: 3 },
|
|
45
|
-
* ];
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
57
|
export interface ProcedureThreshold {
|
|
49
58
|
procedure: ProcedureName;
|
|
50
|
-
/** Threshold for this procedure (1 to numSigners) */
|
|
51
59
|
threshold: number;
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -55,9 +63,11 @@ export interface MultisigConfig {
|
|
|
55
63
|
threshold: number;
|
|
56
64
|
signerCommitments: string[];
|
|
57
65
|
psmCommitment: string;
|
|
66
|
+
psmPublicKey?: string;
|
|
58
67
|
psmEnabled?: boolean;
|
|
59
68
|
storageMode?: 'private' | 'public';
|
|
60
69
|
procedureThresholds?: ProcedureThreshold[];
|
|
70
|
+
signatureScheme?: SignatureScheme;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export interface CreateAccountResult {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
signatureHexToBytes,
|
|
4
|
+
buildSignatureAdviceEntry,
|
|
5
|
+
mergeSignatureAdviceMaps,
|
|
6
|
+
toWord,
|
|
7
|
+
tryComputeEcdsaCommitmentHex,
|
|
8
|
+
} from './signature.js';
|
|
3
9
|
|
|
4
10
|
// Mock the Miden SDK
|
|
5
11
|
vi.mock('@demox-labs/miden-sdk', () => ({
|
|
@@ -29,6 +35,11 @@ vi.mock('@demox-labs/miden-sdk', () => ({
|
|
|
29
35
|
],
|
|
30
36
|
})),
|
|
31
37
|
},
|
|
38
|
+
PublicKey: {
|
|
39
|
+
deserialize: vi.fn().mockReturnValue({
|
|
40
|
+
toCommitment: () => ({ toHex: () => '0x' + 'c'.repeat(64) }),
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
32
43
|
}));
|
|
33
44
|
|
|
34
45
|
describe('signature utilities', () => {
|
|
@@ -42,6 +53,11 @@ describe('signature utilities', () => {
|
|
|
42
53
|
expect(result[0]).toBe(0); // RpoFalcon512 scheme byte
|
|
43
54
|
});
|
|
44
55
|
|
|
56
|
+
it('prepends auth scheme byte (1 = ECDSA)', () => {
|
|
57
|
+
const result = signatureHexToBytes('deadbeef', 'ecdsa');
|
|
58
|
+
expect(result[0]).toBe(1); // ECDSA scheme byte
|
|
59
|
+
});
|
|
60
|
+
|
|
45
61
|
it('converts hex to bytes after prefix', () => {
|
|
46
62
|
const result = signatureHexToBytes('deadbeef');
|
|
47
63
|
expect(result.slice(1)).toEqual(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
|
|
@@ -184,4 +200,11 @@ describe('signature utilities', () => {
|
|
|
184
200
|
expect(Word.fromHex).toHaveBeenCalledWith('0x' + fullHex);
|
|
185
201
|
});
|
|
186
202
|
});
|
|
203
|
+
|
|
204
|
+
describe('tryComputeEcdsaCommitmentHex', () => {
|
|
205
|
+
it('returns normalized commitment when SDK supports ECDSA public keys', () => {
|
|
206
|
+
const commitment = tryComputeEcdsaCommitmentHex('0x' + 'aa'.repeat(33));
|
|
207
|
+
expect(commitment).toBe('0x' + 'c'.repeat(64));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
187
210
|
});
|
package/src/utils/signature.ts
CHANGED
|
@@ -1,28 +1,111 @@
|
|
|
1
1
|
import { AdviceMap, Felt, FeltArray, Rpo256, Signature, Word } from '@demox-labs/miden-sdk';
|
|
2
|
+
import * as midenSdk from '@demox-labs/miden-sdk';
|
|
2
3
|
import { hexToBytes, normalizeHexWord } from './encoding.js';
|
|
4
|
+
import type { SignatureScheme } from '../types.js';
|
|
3
5
|
|
|
4
|
-
export function signatureHexToBytes(hex: string): Uint8Array {
|
|
6
|
+
export function signatureHexToBytes(hex: string, scheme: SignatureScheme = 'falcon'): Uint8Array {
|
|
5
7
|
const sigBytes = hexToBytes(hex);
|
|
6
8
|
const withPrefix = new Uint8Array(sigBytes.length + 1);
|
|
7
|
-
withPrefix[0] = 0;
|
|
9
|
+
withPrefix[0] = scheme === 'ecdsa' ? 1 : 0;
|
|
8
10
|
withPrefix.set(sigBytes, 1);
|
|
9
11
|
return withPrefix;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
function bytesToPackedU32Felts(bytes: Uint8Array): Felt[] {
|
|
15
|
+
const felts: Felt[] = [];
|
|
16
|
+
for (let i = 0; i < bytes.length; i += 4) {
|
|
17
|
+
let packed = 0;
|
|
18
|
+
for (let j = 0; j < 4 && i + j < bytes.length; j++) {
|
|
19
|
+
packed |= bytes[i + j] << (j * 8);
|
|
20
|
+
}
|
|
21
|
+
felts.push(new Felt(BigInt(packed >>> 0)));
|
|
22
|
+
}
|
|
23
|
+
return felts;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function encodeEcdsaSignatureFelts(pubkeyBytes: Uint8Array, sigBytes: Uint8Array): Felt[] {
|
|
27
|
+
const pkFelts = bytesToPackedU32Felts(pubkeyBytes);
|
|
28
|
+
const sigFelts = bytesToPackedU32Felts(sigBytes);
|
|
29
|
+
const encoded = [...pkFelts, ...sigFelts];
|
|
30
|
+
encoded.reverse();
|
|
31
|
+
return encoded;
|
|
32
|
+
}
|
|
33
|
+
|
|
12
34
|
export function buildSignatureAdviceEntry(
|
|
13
35
|
pubkeyCommitment: Word,
|
|
14
36
|
message: Word,
|
|
15
37
|
signature: Signature,
|
|
38
|
+
ecdsaPubkeyHex?: string,
|
|
39
|
+
ecdsaSigHex?: string,
|
|
16
40
|
): { key: Word; values: Felt[] } {
|
|
17
41
|
const elements = new FeltArray([
|
|
18
42
|
...pubkeyCommitment.toFelts(),
|
|
19
43
|
...message.toFelts(),
|
|
20
44
|
]);
|
|
21
45
|
const key = Rpo256.hashElements(elements);
|
|
22
|
-
|
|
46
|
+
|
|
47
|
+
let values: Felt[];
|
|
48
|
+
if (ecdsaPubkeyHex && ecdsaSigHex) {
|
|
49
|
+
const pkBytes = hexToBytes(ecdsaPubkeyHex);
|
|
50
|
+
const sigBytes = hexToBytes(ecdsaSigHex);
|
|
51
|
+
values = encodeEcdsaSignatureFelts(pkBytes, sigBytes);
|
|
52
|
+
} else {
|
|
53
|
+
values = signature.toPreparedSignature(message);
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
return { key, values };
|
|
24
57
|
}
|
|
25
58
|
|
|
59
|
+
export function tryComputeEcdsaCommitmentHex(pubkeyHex: string): string | null {
|
|
60
|
+
const bytes = hexToBytes(pubkeyHex);
|
|
61
|
+
const withPrefix = new Uint8Array(bytes.length + 1);
|
|
62
|
+
withPrefix[0] = 1;
|
|
63
|
+
withPrefix.set(bytes, 1);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const { PublicKey } = midenSdk as any;
|
|
67
|
+
const instance = PublicKey.deserialize(withPrefix);
|
|
68
|
+
return normalizeHexWord(instance.toCommitment().toHex());
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function verifyEcdsaCommitment(
|
|
75
|
+
pubkeyHex: string,
|
|
76
|
+
expectedCommitmentHex: string,
|
|
77
|
+
): { match: boolean; computedHex: string; packedFelts: string[]; error?: string } {
|
|
78
|
+
try {
|
|
79
|
+
const bytes = hexToBytes(pubkeyHex);
|
|
80
|
+
const packedU32Values: number[] = [];
|
|
81
|
+
for (let i = 0; i < bytes.length; i += 4) {
|
|
82
|
+
let packed = 0;
|
|
83
|
+
for (let j = 0; j < 4 && i + j < bytes.length; j++) {
|
|
84
|
+
packed |= bytes[i + j] << (j * 8);
|
|
85
|
+
}
|
|
86
|
+
packedU32Values.push(packed >>> 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const packedFelts = bytesToPackedU32Felts(bytes);
|
|
90
|
+
const feltArray = new FeltArray(packedFelts);
|
|
91
|
+
const computed = Rpo256.hashElements(feltArray);
|
|
92
|
+
const computedHex = normalizeHexWord(computed.toHex());
|
|
93
|
+
const expectedNorm = normalizeHexWord(expectedCommitmentHex);
|
|
94
|
+
return {
|
|
95
|
+
match: computedHex === expectedNorm,
|
|
96
|
+
computedHex,
|
|
97
|
+
packedFelts: packedU32Values.map(v => v.toString()),
|
|
98
|
+
};
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return {
|
|
101
|
+
match: false,
|
|
102
|
+
computedHex: `ERROR: ${e}`,
|
|
103
|
+
packedFelts: [],
|
|
104
|
+
error: String(e),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
26
109
|
export function mergeSignatureAdviceMaps(advice: AdviceMap, entries: Array<{ key: Word; values: Felt[] }>): AdviceMap {
|
|
27
110
|
for (const entry of entries) {
|
|
28
111
|
advice.insert(entry.key, new FeltArray(entry.values));
|
|
@@ -33,4 +116,3 @@ export function mergeSignatureAdviceMaps(advice: AdviceMap, entries: Array<{ key
|
|
|
33
116
|
export function toWord(hex: string): Word {
|
|
34
117
|
return Word.fromHex(normalizeHexWord(hex));
|
|
35
118
|
}
|
|
36
|
-
|