From ab767fe15818007f8d598d9df08f6a52c942091e Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 13 Jun 2026 21:20:06 +0200 Subject: [PATCH] feat(server): ProposalKind::UpdateShamirParameters --- protobufs/operator/governance.proto | 7 +++- .../src/actors/proposal_manager.rs | 22 +++++++++++ .../src/grpc/operator/governance.rs | 4 ++ .../crates/arbiter-server/tests/governance.rs | 39 +++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/protobufs/operator/governance.proto b/protobufs/operator/governance.proto index b138a53..2f5b485 100644 --- a/protobufs/operator/governance.proto +++ b/protobufs/operator/governance.proto @@ -15,7 +15,8 @@ message CreateProposalRequest { ApproveSdkClientPayload approve_sdk_client = 1; GrantWalletAccessPayload grant_wallet_access = 3; ApproveServerUpdatePayload approve_server_update = 4; - ReplaceOperatorPayload replace_operator = 5; + ReplaceOperatorPayload replace_operator = 5; + UpdateShamirParametersPayload update_shamir_parameters = 6; } optional uint32 ttl_secs = 2; } @@ -24,6 +25,10 @@ message ReplaceOperatorPayload { bytes new_pubkey = 1; } +message UpdateShamirParametersPayload { + uint32 new_n = 1; +} + message ApproveServerUpdatePayload {} message ApproveSdkClientPayload { diff --git a/server/crates/arbiter-server/src/actors/proposal_manager.rs b/server/crates/arbiter-server/src/actors/proposal_manager.rs index d70cc0f..9d90ff4 100644 --- a/server/crates/arbiter-server/src/actors/proposal_manager.rs +++ b/server/crates/arbiter-server/src/actors/proposal_manager.rs @@ -20,6 +20,7 @@ pub enum ProposalKind { GrantWalletAccess { wallet_id: i32, client_id: i32 }, ApproveServerUpdate, ReplaceOperator { new_pubkey: Vec }, + UpdateShamirParameters { new_n: u8 }, } impl ProposalKind { @@ -29,6 +30,7 @@ impl ProposalKind { Self::GrantWalletAccess { .. } => "grant_wallet_access", Self::ApproveServerUpdate => "approve_server_update", Self::ReplaceOperator { .. } => "replace_operator", + Self::UpdateShamirParameters { .. } => "update_shamir_parameters", } } @@ -50,6 +52,7 @@ impl ProposalKind { buf.extend_from_slice(new_pubkey); buf } + Self::UpdateShamirParameters { new_n } => vec![*new_n], } } @@ -84,6 +87,12 @@ impl ProposalKind { new_pubkey: rest[..len].to_vec(), }) } + "update_shamir_parameters" => { + let &[new_n] = payload else { + return Err("invalid payload for update_shamir_parameters".to_owned()); + }; + Ok(Self::UpdateShamirParameters { new_n }) + } other => Err(format!("unknown proposal kind: {other}")), } } @@ -415,6 +424,9 @@ impl ProposalManager { ProposalKind::ReplaceOperator { new_pubkey } => { self.execute_replace_operator(new_pubkey).await } + ProposalKind::UpdateShamirParameters { new_n } => { + self.execute_update_shamir_parameters(new_n) + } } } @@ -445,6 +457,16 @@ impl ProposalManager { Ok(()) } + #[expect( + clippy::unused_self, + clippy::unnecessary_wraps, + reason = "signature must match other execute_* methods" + )] + fn execute_update_shamir_parameters(&self, new_n: u8) -> Result<(), Error> { + warn!(new_n, "UpdateShamirParameters approved; Shamir re-keying must be performed out-of-band"); + Ok(()) + } + async fn execute_approve_sdk_client(&self, client_id: i32) -> Result<(), Error> { use arbiter_crypto::authn; use crate::{ diff --git a/server/crates/arbiter-server/src/grpc/operator/governance.rs b/server/crates/arbiter-server/src/grpc/operator/governance.rs index f45f1f2..82fb6c5 100644 --- a/server/crates/arbiter-server/src/grpc/operator/governance.rs +++ b/server/crates/arbiter-server/src/grpc/operator/governance.rs @@ -56,6 +56,10 @@ async fn handle_create( Some(ProtoKind::ReplaceOperator(p)) => ProposalKind::ReplaceOperator { new_pubkey: p.new_pubkey, }, + Some(ProtoKind::UpdateShamirParameters(p)) => ProposalKind::UpdateShamirParameters { + #[expect(clippy::cast_possible_truncation, clippy::as_conversions, reason = "new_n is always a small operator count")] + new_n: p.new_n as u8, + }, None => return Err(Status::invalid_argument("Missing proposal kind")), }; let ttl_secs = req.ttl_secs.map(i64::from); diff --git a/server/crates/arbiter-server/tests/governance.rs b/server/crates/arbiter-server/tests/governance.rs index 3f2491c..34230a0 100644 --- a/server/crates/arbiter-server/tests/governance.rs +++ b/server/crates/arbiter-server/tests/governance.rs @@ -550,6 +550,45 @@ async fn replace_operator_inserts_identity_row() { assert_eq!(count, 2); // original + new } +#[tokio::test] +async fn update_shamir_parameters_reaches_quorum() { + let db = db::create_test_pool().await; + let actors = GlobalActors::spawn(db.clone()).await.unwrap(); + actors + .vault + .ask(Bootstrap { seal_key: KeyCell::from([0u8; 32]) }) + .await + .unwrap(); + + let signing_key = authn::SigningKey::generate(); + let op_id = register_operator(&db, &signing_key.public_key()).await; + + let proposal_id = actors + .proposal_manager + .ask(CreateProposal { + kind: ProposalKind::UpdateShamirParameters { new_n: 5 }, + initiator_id: op_id, + ttl_secs: None, + }) + .await + .unwrap(); + + let msg = make_vote_message(proposal_id, true); + let sig = signing_key.sign_message(&msg, GOVERNANCE_CONTEXT).unwrap(); + let outcome = actors + .proposal_manager + .ask(CastVote { + proposal_id, + operator_id: op_id, + approve: true, + signature: sig.to_bytes(), + }) + .await + .unwrap(); + + assert_eq!(outcome, VoteOutcome::QuorumApproved); +} + #[tokio::test] async fn approve_server_update_reaches_quorum() { let db = db::create_test_pool().await;