feat(server): ProposalKind ::GrantWalletAccess and ::ApproveServerUpdate
This commit is contained in:
@@ -12,15 +12,24 @@ message Request {
|
|||||||
|
|
||||||
message CreateProposalRequest {
|
message CreateProposalRequest {
|
||||||
oneof kind {
|
oneof kind {
|
||||||
ApproveSdkClientPayload approve_sdk_client = 1;
|
ApproveSdkClientPayload approve_sdk_client = 1;
|
||||||
|
GrantWalletAccessPayload grant_wallet_access = 3;
|
||||||
|
ApproveServerUpdatePayload approve_server_update = 4;
|
||||||
}
|
}
|
||||||
optional uint32 ttl_secs = 2;
|
optional uint32 ttl_secs = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ApproveServerUpdatePayload {}
|
||||||
|
|
||||||
message ApproveSdkClientPayload {
|
message ApproveSdkClientPayload {
|
||||||
int32 client_id = 1;
|
int32 client_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GrantWalletAccessPayload {
|
||||||
|
int32 wallet_id = 1;
|
||||||
|
int32 client_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message CastVoteRequest {
|
message CastVoteRequest {
|
||||||
int32 proposal_id = 1;
|
int32 proposal_id = 1;
|
||||||
bool approve = 2;
|
bool approve = 2;
|
||||||
|
|||||||
@@ -17,18 +17,29 @@ pub const DEFAULT_TTL_SECS: i64 = 7 * 24 * 60 * 60; // 7 days
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ProposalKind {
|
pub enum ProposalKind {
|
||||||
ApproveSdkClient { client_id: i32 },
|
ApproveSdkClient { client_id: i32 },
|
||||||
|
GrantWalletAccess { wallet_id: i32, client_id: i32 },
|
||||||
|
ApproveServerUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProposalKind {
|
impl ProposalKind {
|
||||||
pub const fn kind_str(&self) -> &'static str {
|
pub const fn kind_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::ApproveSdkClient { .. } => "approve_sdk_client",
|
Self::ApproveSdkClient { .. } => "approve_sdk_client",
|
||||||
|
Self::GrantWalletAccess { .. } => "grant_wallet_access",
|
||||||
|
Self::ApproveServerUpdate => "approve_server_update",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode_payload(&self) -> Vec<u8> {
|
pub fn encode_payload(&self) -> Vec<u8> {
|
||||||
match self {
|
match self {
|
||||||
Self::ApproveSdkClient { client_id } => client_id.to_be_bytes().to_vec(),
|
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),
|
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}")),
|
other => Err(format!("unknown proposal kind: {other}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,9 +385,30 @@ impl ProposalManager {
|
|||||||
ProposalKind::ApproveSdkClient { client_id } => {
|
ProposalKind::ApproveSdkClient { client_id } => {
|
||||||
self.execute_approve_sdk_client(client_id).await
|
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> {
|
async fn execute_approve_sdk_client(&self, client_id: i32) -> Result<(), Error> {
|
||||||
use arbiter_crypto::authn;
|
use arbiter_crypto::authn;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ async fn handle_create(
|
|||||||
Some(ProtoKind::ApproveSdkClient(p)) => ProposalKind::ApproveSdkClient {
|
Some(ProtoKind::ApproveSdkClient(p)) => ProposalKind::ApproveSdkClient {
|
||||||
client_id: p.client_id,
|
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")),
|
None => return Err(Status::invalid_argument("Missing proposal kind")),
|
||||||
};
|
};
|
||||||
let ttl_secs = req.ttl_secs.map(i64::from);
|
let ttl_secs = req.ttl_secs.map(i64::from);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use arbiter_server::{
|
|||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
use arbiter_server::actors::vault::Bootstrap;
|
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::{ExpressionMethods, QueryDsl, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
|
||||||
@@ -29,6 +29,30 @@ fn make_vote_message(proposal_id: i32, approve: bool) -> Vec<u8> {
|
|||||||
msg
|
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 {
|
async fn insert_unapproved_client(db: &db::DatabasePool, pubkey: &authn::PublicKey) -> i32 {
|
||||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
@@ -422,3 +446,95 @@ async fn approve_sdk_client_writes_integrity_envelope() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(count, 1);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user