Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"crates/miden-rpc-client",
"crates/miden-keystore",
"crates/contracts",
"crates/miden-multisig-client",
"examples/rust",
"examples/demo",
]
Expand All @@ -16,6 +17,7 @@ default-members = [
"crates/miden-rpc-client",
"crates/miden-keystore",
"crates/contracts",
"crates/miden-multisig-client",
]
resolver = "2"

Expand Down Expand Up @@ -54,6 +56,7 @@ rand = "0.9"
anyhow = { version = "1.0", features = ["backtrace", "std"] }
assert_matches = "1.5"
rstest = "0.26"
futures = "0.3"

# Miden
miden-objects = "0.12.4"
Expand Down
77 changes: 64 additions & 13 deletions crates/contracts/masm/auth/psm.masm
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt }
# * PSM_ON => exactly one valid PSM signature is required.
# * PSM_OFF => PSM signature is skipped for that call.
#
# - `verify_psm_signature` reads the selector from initial storage state.
# This means changes made during the same transaction won't affect the check.
#
# - `enable_psm` / `disable_psm` procedures allow explicit control over PSM state.
# - `verify_psm_signature` *always* writes PSM_ON back to `PSM_SELECTOR_SLOT`
# at the end. This means:
# * After `update_psm_public_key` runs, the next transaction will see
# selector = OFF and will NOT require a PSM signature.
# * That same call to `verify_psm_signature` will set the selector to ON,
# so all subsequent transactions WILL require a valid PSM signature
# until the key is rotated again.
#
# - `update_psm_public_key`:
# * Installs a new PSM public key in the map at `PSM_PUBLIC_KEY_MAP_SLOT`.
# * Temporarily disables the PSM selector (PSM_OFF),
# * Installs a new PSM public key in the map at `PSM_PUBLIC_KEY_MAP_SLOT`,
# * Does not itself perform any signature checks.
# * To update the key without requiring PSM signature, ensure selector is OFF.
#
# Storage Layout
# --------------------------------------------------------------------------------
Expand All @@ -44,6 +47,14 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt }
# * A map from a fixed key [0, 0, 0, 0] to the single PSM public key:
# [0, 0, 0, 0] => PSM_PUBLIC_KEY
# * PSM_PUBLIC_KEY is a RPO Falcon 512 public key represented as a word.
#
# Events
# --------------------------------------------------------------------------------
# - AUTH_UNAUTHORIZED_EVENT:
# * Emitted when the PSM signature is missing or invalid while the selector
# is ON.
# * Message emitted: "invalid PSM signature".
#

# CONSTANTS
# =================================================================================================
Expand All @@ -68,15 +79,17 @@ const AUTH_UNAUTHORIZED_EVENT = event("miden::auth::unauthorized")
# PSM PROCEDURES
# =================================================================================================

#! Enable PSM verification by setting the selector to ON.

#! Conditionally verify a "PSM" signature against a stored public key hash.
#! The condition is controlled by the selector at PSM_SELECTOR_SLOT specified above.
#!
#! Operand stack inputs: []
#! Outputs: []
#!
#! Notes:
#! - Sets PSM_SELECTOR_SLOT to PSM_ON (1)
#! - After this, transactions will require PSM signature verification
pub proc enable_psm
proc enable_psm
push.PSM_ON
# => [PSM_ON]

Expand All @@ -98,7 +111,7 @@ end
#! Notes:
#! - Sets PSM_SELECTOR_SLOT to PSM_OFF (0)
#! - After this, transactions will NOT require PSM signature verification
pub proc disable_psm
proc disable_psm
push.PSM_OFF
# => [PSM_OFF]

Expand All @@ -124,6 +137,9 @@ end
#! - To update the key without requiring PSM signature, ensure
#! PSM_SELECTOR_SLOT = 0 (OFF) before calling this.
pub proc update_psm_public_key
# ------ Disable the PSM selector ------
exec.disable_psm

# ------ Update the PSM public key ------
adv_loadw
# => [PUB_KEY]
Expand Down Expand Up @@ -157,20 +173,55 @@ end
#! - Selector value is read from initial storage state
pub proc verify_psm_signature(msg: BeWord)
push.PSM_SELECTOR_SLOT
exec.active_account::get_initial_item
# => [PSM_SELECTOR_SLOT, MSG]

# Not the initial item!
# But the current item updated by disable procedure
exec.active_account::get_item
# => [0, 0, 0, psm_selector, MSG]

drop drop drop
# => [selector, MSG]
# => [psm_selector, MSG]

push.1 eq
push.1
# => [1, psm_selector, MSG]

eq # is_equal
# [is_equal, MSG]

# ------ Conditionally verify the PSM signature ------
if.true
# => [MSG]
push.1
# [num_approvers, MSG]

push.PSM_PUBLIC_KEY_MAP_SLOT
# => [PSM_PUBLIC_KEY_MAP_SLOT, num_approvers, MSG]

exec.::miden::auth::rpo_falcon512::verify_signatures
push.1 neq
# => [num_verified_signatures, MSG]

push.1
# => [1, num_verified_signatures, MSG]

neq
# => [is_unauthorized, MSG]
# If signatures are non-existent the tx will fail here.
if.true
emit.AUTH_UNAUTHORIZED_EVENT
push.0 assert.err="invalid PSM signature"
end
else
# Return MSG
# => [MSG]
push.0
# => [0, MSG]

drop
# => [MSG]
end
# => [MSG]

# ------ Enable the PSM selector ------
exec.enable_psm
end
7 changes: 4 additions & 3 deletions crates/contracts/tests/auth/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ async fn test_multisig_update_psm_public_key() -> anyhow::Result<()> {
// Initialize with PSM selector = OFF so key update doesn't require PSM signature
// This is the expected flow: disable PSM, update key, then enable PSM in a follow-up tx
let multisig_account =
create_multisig_account_with_psm(2, &public_keys, psm_public_key.clone(), false)?;
create_multisig_account_with_psm(2, &public_keys, psm_public_key.clone(), true)?;

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

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

Expand Down
43 changes: 43 additions & 0 deletions crates/miden-multisig-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[package]
name = "miden-multisig-client"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
description = "High-level SDK for interacting with multisig accounts on Miden via PSM"

[dependencies]
# Internal crates
private-state-manager-client = { path = "../client" }
private-state-manager-shared = { path = "../shared" }
miden-confidential-contracts = { path = "../contracts" }

# Miden
miden-client = { workspace = true, features = ["tonic", "testing"] }
miden-client-sqlite-store = { workspace = true }
miden-objects = { workspace = true }
miden-tx = { workspace = true }
miden-lib = { workspace = true }

# Async
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
futures = { workspace = true }

# Serialization
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }

# Error handling
thiserror = { workspace = true }
anyhow = { workspace = true }

# Utilities
rand = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
Loading