feat(server): ProposalKind ::GrantWalletAccess and ::ApproveServerUpdate
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful

This commit is contained in:
CleverWild
2026-06-13 15:51:22 +02:00
parent 0b331d90bf
commit 514a4cb2d1
4 changed files with 173 additions and 2 deletions

View File

@@ -17,18 +17,29 @@ pub const DEFAULT_TTL_SECS: i64 = 7 * 24 * 60 * 60; // 7 days
#[derive(Debug, Clone)]
pub enum ProposalKind {
ApproveSdkClient { client_id: i32 },
GrantWalletAccess { wallet_id: i32, client_id: i32 },
ApproveServerUpdate,
}
impl ProposalKind {
pub const fn kind_str(&self) -> &'static str {
match self {
Self::ApproveSdkClient { .. } => "approve_sdk_client",
Self::GrantWalletAccess { .. } => "grant_wallet_access",
Self::ApproveServerUpdate => "approve_server_update",
}
}
pub fn encode_payload(&self) -> Vec<u8> {
match self {
Self::ApproveSdkClient { client_id } => client_id.to_be_bytes().to_vec(),
Self::GrantWalletAccess { wallet_id, client_id } => {
let mut buf = Vec::with_capacity(8);
buf.extend_from_slice(&wallet_id.to_be_bytes());
buf.extend_from_slice(&client_id.to_be_bytes());
buf
}
Self::ApproveServerUpdate => vec![],
}
}
@@ -41,6 +52,15 @@ impl ProposalKind {
client_id: i32::from_be_bytes(bytes),
})
}
"grant_wallet_access" => {
let bytes = <[u8; 8]>::try_from(payload)
.map_err(|_| "invalid payload for grant_wallet_access".to_owned())?;
Ok(Self::GrantWalletAccess {
wallet_id: i32::from_be_bytes(bytes[..4].try_into().unwrap()),
client_id: i32::from_be_bytes(bytes[4..].try_into().unwrap()),
})
}
"approve_server_update" => Ok(Self::ApproveServerUpdate),
other => Err(format!("unknown proposal kind: {other}")),
}
}
@@ -365,9 +385,30 @@ impl ProposalManager {
ProposalKind::ApproveSdkClient { client_id } => {
self.execute_approve_sdk_client(client_id).await
}
ProposalKind::GrantWalletAccess { wallet_id, client_id } => {
self.execute_grant_wallet_access(wallet_id, client_id).await
}
ProposalKind::ApproveServerUpdate => Ok(()),
}
}
async fn execute_grant_wallet_access(&self, wallet_id: i32, client_id: i32) -> Result<(), Error> {
use crate::db::models::EvmWalletId;
let mut conn = self.db.get().await.map_err(Error::DatabaseConnection)?;
diesel::insert_into(schema::evm_wallet_access::table)
.values((
schema::evm_wallet_access::wallet_id.eq(EvmWalletId::from_raw(wallet_id)),
schema::evm_wallet_access::client_id.eq(client_id),
))
.execute(&mut conn)
.await
.map_err(|e| Error::ExecutionFailed(format!("grant wallet access: {e}")))?;
Ok(())
}
async fn execute_approve_sdk_client(&self, client_id: i32) -> Result<(), Error> {
use arbiter_crypto::authn;
use crate::{

View File

@@ -48,6 +48,11 @@ async fn handle_create(
Some(ProtoKind::ApproveSdkClient(p)) => ProposalKind::ApproveSdkClient {
client_id: p.client_id,
},
Some(ProtoKind::GrantWalletAccess(p)) => ProposalKind::GrantWalletAccess {
wallet_id: p.wallet_id,
client_id: p.client_id,
},
Some(ProtoKind::ApproveServerUpdate(_)) => ProposalKind::ApproveServerUpdate,
None => return Err(Status::invalid_argument("Missing proposal kind")),
};
let ttl_secs = req.ttl_secs.map(i64::from);

View File

@@ -8,7 +8,7 @@ use arbiter_server::{
db,
};
use arbiter_server::actors::vault::Bootstrap;
use arbiter_server::db::schema::operator_identity;
use arbiter_server::db::schema::{aead_encrypted, evm_wallet, evm_wallet_access, operator_identity};
use diesel::{ExpressionMethods, QueryDsl, insert_into};
use diesel_async::RunQueryDsl;
@@ -29,6 +29,30 @@ fn make_vote_message(proposal_id: i32, approve: bool) -> Vec<u8> {
msg
}
async fn insert_evm_wallet(db: &db::DatabasePool) -> i32 {
let mut conn = db.get().await.unwrap();
let aead_id: i32 = insert_into(aead_encrypted::table)
.values((
aead_encrypted::current_nonce.eq(vec![0u8; 4]),
aead_encrypted::ciphertext.eq(vec![0u8; 32]),
aead_encrypted::tag.eq(vec![0u8; 16]),
aead_encrypted::associated_root_key_id.eq(0i32),
))
.returning(aead_encrypted::id)
.get_result::<i32>(&mut conn)
.await
.unwrap();
insert_into(evm_wallet::table)
.values((
evm_wallet::address.eq(vec![0u8; 20]),
evm_wallet::aead_encrypted_id.eq(aead_id),
))
.returning(evm_wallet::id)
.get_result::<i32>(&mut conn)
.await
.unwrap()
}
async fn insert_unapproved_client(db: &db::DatabasePool, pubkey: &authn::PublicKey) -> i32 {
use arbiter_server::db::schema::{client_metadata, program_client};
let mut conn = db.get().await.unwrap();
@@ -422,3 +446,95 @@ async fn approve_sdk_client_writes_integrity_envelope() {
.unwrap();
assert_eq!(count, 1);
}
#[tokio::test]
async fn grant_wallet_access_on_quorum_approval() {
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 wallet_id = insert_evm_wallet(&db).await;
let client_key = authn::SigningKey::generate();
let client_id = insert_unapproved_client(&db, &client_key.public_key()).await;
let proposal_id = actors
.proposal_manager
.ask(CreateProposal {
kind: ProposalKind::GrantWalletAccess { wallet_id, client_id },
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);
let mut conn = db.get().await.unwrap();
let count: i64 = evm_wallet_access::table
.filter(evm_wallet_access::wallet_id.eq(wallet_id))
.filter(evm_wallet_access::client_id.eq(client_id))
.count()
.get_result(&mut conn)
.await
.unwrap();
assert_eq!(count, 1);
}
#[tokio::test]
async fn approve_server_update_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::ApproveServerUpdate,
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);
}