Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9c3e8b4
feat: add GroupKeyHolder for GMS key management
moudyellaz Apr 15, 2026
55b7ba0
fix: rebuild artifacts
moudyellaz Apr 16, 2026
8f1deb4
fix: derive per-PDA SSK
moudyellaz Apr 16, 2026
6a3be9f
feat: redact Debug and rename gms() to dangerous_raw_gms
moudyellaz Apr 16, 2026
52785ed
test: pin end-to-end derivation to private PDA AccountId
moudyellaz Apr 16, 2026
13bc795
test: GroupKeyHolder serde round-trip
moudyellaz Apr 16, 2026
52f186e
test: group derivation does not collide with personal path at shared …
moudyellaz Apr 16, 2026
0cd95a9
test: degenerate GMS produces distinct non-zero keys
moudyellaz Apr 16, 2026
dcb0a87
feat: add epoch field and forward-ratchet to GroupKeyHolder
moudyellaz Apr 17, 2026
8fff2f7
feat: add seal_for and unseal to GroupKeyHolder
moudyellaz Apr 17, 2026
ce35e5c
feat: add GroupKeyHolder storage to NSSAUserData
moudyellaz Apr 19, 2026
acaf4cd
feat: add PrivateGroupPda variant for mask-3 wallet transactions
moudyellaz Apr 19, 2026
0aff194
fix: remove redundant assert in unseal and add forward-secrecy test
moudyellaz Apr 19, 2026
66b2156
fix: nightly fmt and clippy lint
moudyellaz Apr 19, 2026
38cdd6f
fix: clippy
moudyellaz Apr 20, 2026
735ac2b
fix: clippy
moudyellaz Apr 20, 2026
985913c
fix: rebuild artifacts
moudyellaz Apr 20, 2026
a0ff22d
fix: install just with --locked
moudyellaz Apr 20, 2026
1fdc2da
fix: use renamed AccountId::for_private_pda
moudyellaz Apr 22, 2026
1b2c3be
fix: rebuild artifacts
moudyellaz Apr 22, 2026
60d4928
fix: replace PdaSeed::as_bytes with AsRef and improve docs
moudyellaz Apr 23, 2026
6f581b1
fix: rebuild artifacts
moudyellaz Apr 23, 2026
7a34500
feat: add group PDA lifecycle integration test
moudyellaz Apr 23, 2026
b6fda0a
fix: integration test compile errors and doc lint
moudyellaz Apr 23, 2026
da8a182
fix: chain to auth_transfer for balance ops and add must_use
moudyellaz Apr 23, 2026
9b5dbe2
fix: add must_use to public AccountManager methods
moudyellaz Apr 24, 2026
ec31fe3
fix: rewrite group_pda_spender without chained calls
moudyellaz Apr 24, 2026
1eef30c
fix: lint and integration tests
moudyellaz Apr 24, 2026
81625b7
fix: cache group PDA state locally for cross-tx reads
moudyellaz Apr 24, 2026
2e232f1
fix: clippy redundant clone in group_pda_spender
moudyellaz Apr 24, 2026
af33a60
fix: clippy doc backtick
moudyellaz Apr 24, 2026
69ee3cd
fix: clippy
moudyellaz Apr 25, 2026
b77f03a
fix: clippy lint expectations
moudyellaz Apr 25, 2026
aab5579
fix: use new_claimed_if_default for existing PDAs
moudyellaz Apr 25, 2026
3f8e7f4
fix: split deposit and spend paths in group_pda_spender
moudyellaz Apr 25, 2026
b7aab92
feat: add group_pda_router for existing mask-3 PDA binding
moudyellaz Apr 25, 2026
5021e20
fix: use router as PDA owner for consistent AccountId binding
moudyellaz Apr 25, 2026
f8f9a38
fix: re-design
moudyellaz Apr 26, 2026
c8c6fa4
fix: remove unused imports
moudyellaz Apr 26, 2026
5dbaefa
fix: doc backticks
moudyellaz Apr 26, 2026
f843420
fix: simplify integration test to key agreement and forward secrecy
moudyellaz Apr 26, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ jobs:
- uses: ./.github/actions/install-risc0

- name: Install just
run: cargo install just
run: cargo install --locked just

- name: Build artifacts
run: just build-artifacts
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

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

Binary file modified artifacts/program_methods/amm.bin
Binary file not shown.
Binary file modified artifacts/program_methods/associated_token_account.bin
Binary file not shown.
Binary file modified artifacts/program_methods/authenticated_transfer.bin
Binary file not shown.
Binary file modified artifacts/program_methods/clock.bin
Binary file not shown.
Binary file modified artifacts/program_methods/pinata.bin
Binary file not shown.
Binary file modified artifacts/program_methods/pinata_token.bin
Binary file not shown.
Binary file modified artifacts/program_methods/privacy_preserving_circuit.bin
Binary file not shown.
Binary file modified artifacts/program_methods/token.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/auth_asserting_noop.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/burner.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/chain_caller.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/changer_claimer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/claimer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/clock_chain_caller.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/data_changer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/extra_output.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/flash_swap_callback.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/flash_swap_initiator.bin
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified artifacts/test_program_methods/malicious_authorization_changer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/malicious_caller_program_id.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/malicious_self_program_id.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/minter.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/missing_output.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/modified_transfer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/nonce_changer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/noop.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/pda_claimer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/pinata_cooldown.bin
Binary file not shown.
Binary file not shown.
Binary file modified artifacts/test_program_methods/private_pda_delegator.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/program_owner_changer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/simple_balance_transfer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/time_locked_transfer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/two_pda_claimer.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/validity_window.bin
Binary file not shown.
Binary file modified artifacts/test_program_methods/validity_window_chain_caller.bin
Binary file not shown.
1 change: 1 addition & 0 deletions integration_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ata_core.workspace = true
indexer_service_rpc.workspace = true
sequencer_service_rpc = { workspace = true, features = ["client"] }
wallet-ffi.workspace = true
test_program_methods.workspace = true
testnet_initial_state.workspace = true

url.workspace = true
Expand Down
145 changes: 145 additions & 0 deletions integration_tests/tests/group_pda.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#![expect(
clippy::tests_outside_test_module,
reason = "Integration test file, not inside a #[cfg(test)] module"
)]

//! Group-owned private PDA lifecycle integration test.
//!
//! Demonstrates:
//! 1. GMS creation and sealed distribution between controllers.
//! 2. Key agreement: both controllers derive identical keys from the shared GMS.
//! 3. Forward secrecy: ratcheting the GMS produces different keys, locking out removed members.

use anyhow::{Context as _, Result};
use integration_tests::TestContext;
use key_protocol::key_management::group_key_holder::GroupKeyHolder;
use log::info;
use nssa::{AccountId, program::Program};
use nssa_core::program::PdaSeed;
use tokio::test;

/// Group PDA lifecycle: create group, distribute GMS, verify key agreement, revoke.
#[test]
async fn group_pda_lifecycle() -> Result<()> {
let _ctx = TestContext::new().await?;

let alice_holder = GroupKeyHolder::new();
assert_eq!(alice_holder.epoch(), 0);
let pda_seed = PdaSeed::new([42_u8; 32]);
let group_pda_spender =
Program::new(test_program_methods::GROUP_PDA_SPENDER_ELF.to_vec()).unwrap();

// -----------------------------------------------------------------------
// Act 1: GMS creation and sealed distribution
// -----------------------------------------------------------------------

info!("Act 1: creating group and distributing GMS");

let alice_npk = alice_holder
.derive_keys_for_pda(&pda_seed)
.generate_nullifier_public_key();

let bob_private_account = _ctx.existing_private_accounts()[1];
let (bob_keychain, _) = _ctx
.wallet()
.storage()
.user_data
.get_private_account(bob_private_account)
.cloned()
.context("Bob's private account not found")?;

// Alice seals GMS for Bob, Bob unseals
let sealed = alice_holder.seal_for(&bob_keychain.viewing_public_key);
let bob_holder =
GroupKeyHolder::unseal(&sealed, &bob_keychain.private_key_holder.viewing_secret_key)
.expect("Bob should unseal the GMS");

// -----------------------------------------------------------------------
// Act 2: Key agreement
//
// Both controllers independently derive identical keys for the same PDA
// seed. Neither communicated any per-PDA keys β€” they derived them from
// the shared GMS.
// -----------------------------------------------------------------------

info!("Act 2: verifying key agreement");

let bob_npk = bob_holder
.derive_keys_for_pda(&pda_seed)
.generate_nullifier_public_key();
assert_eq!(
alice_npk, bob_npk,
"Key agreement: identical NPK from shared GMS"
);

let group_account_id =
AccountId::for_private_pda(&group_pda_spender.id(), &pda_seed, &alice_npk);
info!("Group PDA AccountId: {group_account_id}");

// Both derive the same AccountId independently
let bob_account_id = AccountId::for_private_pda(&group_pda_spender.id(), &pda_seed, &bob_npk);
assert_eq!(group_account_id, bob_account_id);

info!("Act 2 complete: key agreement verified");

// -----------------------------------------------------------------------
// Act 3: Revocation and forward secrecy
//
// Alice ratchets the GMS to exclude Bob. The new keys produce a different
// NPK and therefore a different AccountId. Bob's frozen holder can no
// longer derive the new keys.
// -----------------------------------------------------------------------

info!("Act 3: ratchet and forward secrecy");

let mut ratcheted_holder = alice_holder.clone();
ratcheted_holder.ratchet([99_u8; 32]);
assert_eq!(ratcheted_holder.epoch(), 1);

let ratcheted_npk = ratcheted_holder
.derive_keys_for_pda(&pda_seed)
.generate_nullifier_public_key();

let bob_stale_npk = bob_holder
.derive_keys_for_pda(&pda_seed)
.generate_nullifier_public_key();

// Forward secrecy: ratcheted keys differ from Bob's stale keys
assert_ne!(ratcheted_npk, bob_stale_npk);
assert_ne!(ratcheted_npk, alice_npk);

// Different AccountId after ratchet
let new_account_id =
AccountId::for_private_pda(&group_pda_spender.id(), &pda_seed, &ratcheted_npk);
assert_ne!(group_account_id, new_account_id);

// Bob's stale keys still point to the old address
let bob_stale_account_id =
AccountId::for_private_pda(&group_pda_spender.id(), &pda_seed, &bob_stale_npk);
assert_eq!(bob_stale_account_id, group_account_id);
assert_ne!(bob_stale_account_id, new_account_id);

// Sealed round-trip of ratcheted GMS
let (alice_kc, _) = _ctx
.wallet()
.storage()
.user_data
.get_private_account(_ctx.existing_private_accounts()[0])
.cloned()
.context("Alice's keys not found")?;
let sealed_ratcheted = ratcheted_holder.seal_for(&alice_kc.viewing_public_key);
let restored = GroupKeyHolder::unseal(
&sealed_ratcheted,
&alice_kc.private_key_holder.viewing_secret_key,
)
.expect("Should unseal ratcheted GMS");
assert_eq!(
restored.dangerous_raw_gms(),
ratcheted_holder.dangerous_raw_gms()
);
assert_eq!(restored.epoch(), 1);

info!("Act 3 complete: forward secrecy verified");
info!("Group PDA lifecycle test complete");
Ok(())
}
1 change: 1 addition & 0 deletions key_protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ itertools.workspace = true

[dev-dependencies]
base58.workspace = true
bincode.workspace = true
Loading
Loading