Skip to content

Commit b103f36

Browse files
authored
feat: introduce miden-multisig-client (#72)
* feat: introduce miden-multisig-client * fix_ clippy warns * refactor: use ProposalBuilder for creating proposals * refactor: use ProposalPayload in favor of json objects * refactor: move utility methods to its own file * fix: keep treshold when doing changes in the multisig * refactor: remove unnecesary comments * fix: construct proposal using thresholds and signatures * fmt * feat: add support to remove cosigner execution in demo * fmt + clippy * feat: add support to p2id creation and consuming * refactor: better error handling * fix: update psm masm code + add switch psm tx + support import/export proposals * fix: fmt * fix: workaround for https://github.com/0xMiden/crypto/issues/693\#issuecomment-3617553447 issue * refactor: push utities to the multisig client * refactor: split client into multiple files * docs: add multisig client docs * tests: add unit tests
1 parent 7bc139d commit b103f36

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+7164
-1984
lines changed

Cargo.lock

Lines changed: 25 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"crates/miden-rpc-client",
77
"crates/miden-keystore",
88
"crates/contracts",
9+
"crates/miden-multisig-client",
910
"examples/rust",
1011
"examples/demo",
1112
]
@@ -16,6 +17,7 @@ default-members = [
1617
"crates/miden-rpc-client",
1718
"crates/miden-keystore",
1819
"crates/contracts",
20+
"crates/miden-multisig-client",
1921
]
2022
resolver = "2"
2123

@@ -54,6 +56,7 @@ rand = "0.9"
5456
anyhow = { version = "1.0", features = ["backtrace", "std"] }
5557
assert_matches = "1.5"
5658
rstest = "0.26"
59+
futures = "0.3"
5760

5861
# Miden
5962
miden-objects = "0.12.4"

crates/contracts/masm/auth/psm.masm

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt }
1616
# * PSM_ON => exactly one valid PSM signature is required.
1717
# * PSM_OFF => PSM signature is skipped for that call.
1818
#
19-
# - `verify_psm_signature` reads the selector from initial storage state.
20-
# This means changes made during the same transaction won't affect the check.
21-
#
22-
# - `enable_psm` / `disable_psm` procedures allow explicit control over PSM state.
19+
# - `verify_psm_signature` *always* writes PSM_ON back to `PSM_SELECTOR_SLOT`
20+
# at the end. This means:
21+
# * After `update_psm_public_key` runs, the next transaction will see
22+
# selector = OFF and will NOT require a PSM signature.
23+
# * That same call to `verify_psm_signature` will set the selector to ON,
24+
# so all subsequent transactions WILL require a valid PSM signature
25+
# until the key is rotated again.
2326
#
2427
# - `update_psm_public_key`:
25-
# * Installs a new PSM public key in the map at `PSM_PUBLIC_KEY_MAP_SLOT`.
28+
# * Temporarily disables the PSM selector (PSM_OFF),
29+
# * Installs a new PSM public key in the map at `PSM_PUBLIC_KEY_MAP_SLOT`,
2630
# * Does not itself perform any signature checks.
27-
# * To update the key without requiring PSM signature, ensure selector is OFF.
2831
#
2932
# Storage Layout
3033
# --------------------------------------------------------------------------------
@@ -44,6 +47,14 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt }
4447
# * A map from a fixed key [0, 0, 0, 0] to the single PSM public key:
4548
# [0, 0, 0, 0] => PSM_PUBLIC_KEY
4649
# * PSM_PUBLIC_KEY is a RPO Falcon 512 public key represented as a word.
50+
#
51+
# Events
52+
# --------------------------------------------------------------------------------
53+
# - AUTH_UNAUTHORIZED_EVENT:
54+
# * Emitted when the PSM signature is missing or invalid while the selector
55+
# is ON.
56+
# * Message emitted: "invalid PSM signature".
57+
#
4758

4859
# CONSTANTS
4960
# =================================================================================================
@@ -68,15 +79,17 @@ const AUTH_UNAUTHORIZED_EVENT = event("miden::auth::unauthorized")
6879
# PSM PROCEDURES
6980
# =================================================================================================
7081

71-
#! Enable PSM verification by setting the selector to ON.
82+
83+
#! Conditionally verify a "PSM" signature against a stored public key hash.
84+
#! The condition is controlled by the selector at PSM_SELECTOR_SLOT specified above.
7285
#!
7386
#! Operand stack inputs: []
7487
#! Outputs: []
7588
#!
7689
#! Notes:
7790
#! - Sets PSM_SELECTOR_SLOT to PSM_ON (1)
7891
#! - After this, transactions will require PSM signature verification
79-
pub proc enable_psm
92+
proc enable_psm
8093
push.PSM_ON
8194
# => [PSM_ON]
8295

@@ -98,7 +111,7 @@ end
98111
#! Notes:
99112
#! - Sets PSM_SELECTOR_SLOT to PSM_OFF (0)
100113
#! - After this, transactions will NOT require PSM signature verification
101-
pub proc disable_psm
114+
proc disable_psm
102115
push.PSM_OFF
103116
# => [PSM_OFF]
104117

@@ -124,6 +137,9 @@ end
124137
#! - To update the key without requiring PSM signature, ensure
125138
#! PSM_SELECTOR_SLOT = 0 (OFF) before calling this.
126139
pub proc update_psm_public_key
140+
# ------ Disable the PSM selector ------
141+
exec.disable_psm
142+
127143
# ------ Update the PSM public key ------
128144
adv_loadw
129145
# => [PUB_KEY]
@@ -157,20 +173,55 @@ end
157173
#! - Selector value is read from initial storage state
158174
pub proc verify_psm_signature(msg: BeWord)
159175
push.PSM_SELECTOR_SLOT
160-
exec.active_account::get_initial_item
176+
# => [PSM_SELECTOR_SLOT, MSG]
177+
178+
# Not the initial item!
179+
# But the current item updated by disable procedure
180+
exec.active_account::get_item
181+
# => [0, 0, 0, psm_selector, MSG]
182+
161183
drop drop drop
162-
# => [selector, MSG]
184+
# => [psm_selector, MSG]
163185

164-
push.1 eq
186+
push.1
187+
# => [1, psm_selector, MSG]
188+
189+
eq # is_equal
190+
# [is_equal, MSG]
191+
192+
# ------ Conditionally verify the PSM signature ------
165193
if.true
194+
# => [MSG]
166195
push.1
196+
# [num_approvers, MSG]
197+
167198
push.PSM_PUBLIC_KEY_MAP_SLOT
199+
# => [PSM_PUBLIC_KEY_MAP_SLOT, num_approvers, MSG]
200+
168201
exec.::miden::auth::rpo_falcon512::verify_signatures
169-
push.1 neq
202+
# => [num_verified_signatures, MSG]
203+
204+
push.1
205+
# => [1, num_verified_signatures, MSG]
206+
207+
neq
208+
# => [is_unauthorized, MSG]
209+
# If signatures are non-existent the tx will fail here.
170210
if.true
171211
emit.AUTH_UNAUTHORIZED_EVENT
172212
push.0 assert.err="invalid PSM signature"
173213
end
214+
else
215+
# Return MSG
216+
# => [MSG]
217+
push.0
218+
# => [0, MSG]
219+
220+
drop
221+
# => [MSG]
174222
end
175223
# => [MSG]
224+
225+
# ------ Enable the PSM selector ------
226+
exec.enable_psm
176227
end

crates/contracts/tests/auth/multisig.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ async fn test_multisig_update_psm_public_key() -> anyhow::Result<()> {
533533
// Initialize with PSM selector = OFF so key update doesn't require PSM signature
534534
// This is the expected flow: disable PSM, update key, then enable PSM in a follow-up tx
535535
let multisig_account =
536-
create_multisig_account_with_psm(2, &public_keys, psm_public_key.clone(), false)?;
536+
create_multisig_account_with_psm(2, &public_keys, psm_public_key.clone(), true)?;
537537

538538
// SECTION 1: Execute a transaction script to update PSM public key
539539
// ================================================================================
@@ -574,11 +574,12 @@ async fn test_multisig_update_psm_public_key() -> anyhow::Result<()> {
574574
let psm_library = get_psm_library()?;
575575

576576
// Use call.:: syntax for dynamically linked library procedure calls (v0.12+)
577-
// This script updates the PSM key and then enables PSM verification
577+
// This script only calls update_psm_public_key.
578+
// Note: enable_psm is now a private procedure and is automatically called
579+
// by verify_psm_signature at the end of transaction authentication.
578580
let tx_script_code = r#"
579581
begin
580582
call.::update_psm_public_key
581-
call.::enable_psm
582583
end
583584
"#;
584585

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[package]
2+
name = "miden-multisig-client"
3+
version.workspace = true
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
authors.workspace = true
7+
license.workspace = true
8+
repository.workspace = true
9+
homepage.workspace = true
10+
description = "High-level SDK for interacting with multisig accounts on Miden via PSM"
11+
12+
[dependencies]
13+
# Internal crates
14+
private-state-manager-client = { path = "../client" }
15+
private-state-manager-shared = { path = "../shared" }
16+
miden-confidential-contracts = { path = "../contracts" }
17+
18+
# Miden
19+
miden-client = { workspace = true, features = ["tonic", "testing"] }
20+
miden-client-sqlite-store = { workspace = true }
21+
miden-objects = { workspace = true }
22+
miden-tx = { workspace = true }
23+
miden-lib = { workspace = true }
24+
25+
# Async
26+
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
27+
futures = { workspace = true }
28+
29+
# Serialization
30+
serde = { workspace = true, features = ["derive"] }
31+
serde_json = { workspace = true }
32+
hex = { workspace = true }
33+
base64 = { workspace = true }
34+
35+
# Error handling
36+
thiserror = { workspace = true }
37+
anyhow = { workspace = true }
38+
39+
# Utilities
40+
rand = { workspace = true }
41+
42+
[dev-dependencies]
43+
tempfile = { workspace = true }

0 commit comments

Comments
 (0)