feat(server): ProposalKind::ReplaceOperator
This commit is contained in:
@@ -15,10 +15,15 @@ message CreateProposalRequest {
|
|||||||
ApproveSdkClientPayload approve_sdk_client = 1;
|
ApproveSdkClientPayload approve_sdk_client = 1;
|
||||||
GrantWalletAccessPayload grant_wallet_access = 3;
|
GrantWalletAccessPayload grant_wallet_access = 3;
|
||||||
ApproveServerUpdatePayload approve_server_update = 4;
|
ApproveServerUpdatePayload approve_server_update = 4;
|
||||||
|
ReplaceOperatorPayload replace_operator = 5;
|
||||||
}
|
}
|
||||||
optional uint32 ttl_secs = 2;
|
optional uint32 ttl_secs = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ReplaceOperatorPayload {
|
||||||
|
bytes new_pubkey = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message ApproveServerUpdatePayload {}
|
message ApproveServerUpdatePayload {}
|
||||||
|
|
||||||
message ApproveSdkClientPayload {
|
message ApproveSdkClientPayload {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub enum ProposalKind {
|
|||||||
ApproveSdkClient { client_id: i32 },
|
ApproveSdkClient { client_id: i32 },
|
||||||
GrantWalletAccess { wallet_id: i32, client_id: i32 },
|
GrantWalletAccess { wallet_id: i32, client_id: i32 },
|
||||||
ApproveServerUpdate,
|
ApproveServerUpdate,
|
||||||
|
ReplaceOperator { new_pubkey: Vec<u8> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProposalKind {
|
impl ProposalKind {
|
||||||
@@ -27,6 +28,7 @@ impl ProposalKind {
|
|||||||
Self::ApproveSdkClient { .. } => "approve_sdk_client",
|
Self::ApproveSdkClient { .. } => "approve_sdk_client",
|
||||||
Self::GrantWalletAccess { .. } => "grant_wallet_access",
|
Self::GrantWalletAccess { .. } => "grant_wallet_access",
|
||||||
Self::ApproveServerUpdate => "approve_server_update",
|
Self::ApproveServerUpdate => "approve_server_update",
|
||||||
|
Self::ReplaceOperator { .. } => "replace_operator",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +42,14 @@ impl ProposalKind {
|
|||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
Self::ApproveServerUpdate => vec![],
|
Self::ApproveServerUpdate => vec![],
|
||||||
|
Self::ReplaceOperator { new_pubkey } => {
|
||||||
|
#[expect(clippy::cast_possible_truncation, reason = "pubkey is always 32 bytes")]
|
||||||
|
let len = new_pubkey.len() as u32;
|
||||||
|
let mut buf = Vec::with_capacity(4 + new_pubkey.len());
|
||||||
|
buf.extend_from_slice(&len.to_be_bytes());
|
||||||
|
buf.extend_from_slice(new_pubkey);
|
||||||
|
buf
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +71,19 @@ impl ProposalKind {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
"approve_server_update" => Ok(Self::ApproveServerUpdate),
|
"approve_server_update" => Ok(Self::ApproveServerUpdate),
|
||||||
|
"replace_operator" => {
|
||||||
|
let (len_bytes, rest) = payload
|
||||||
|
.split_first_chunk::<4>()
|
||||||
|
.ok_or_else(|| "replace_operator payload too short".to_owned())?;
|
||||||
|
let len = u32::from_be_bytes(*len_bytes);
|
||||||
|
let len = usize::try_from(len).unwrap_or(usize::MAX);
|
||||||
|
if rest.len() < len {
|
||||||
|
return Err("replace_operator payload truncated".to_owned());
|
||||||
|
}
|
||||||
|
Ok(Self::ReplaceOperator {
|
||||||
|
new_pubkey: rest[..len].to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
other => Err(format!("unknown proposal kind: {other}")),
|
other => Err(format!("unknown proposal kind: {other}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,6 +412,9 @@ impl ProposalManager {
|
|||||||
self.execute_grant_wallet_access(wallet_id, client_id).await
|
self.execute_grant_wallet_access(wallet_id, client_id).await
|
||||||
}
|
}
|
||||||
ProposalKind::ApproveServerUpdate => Ok(()),
|
ProposalKind::ApproveServerUpdate => Ok(()),
|
||||||
|
ProposalKind::ReplaceOperator { new_pubkey } => {
|
||||||
|
self.execute_replace_operator(new_pubkey).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +435,16 @@ impl ProposalManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn execute_replace_operator(&self, new_pubkey: Vec<u8>) -> Result<(), Error> {
|
||||||
|
let mut conn = self.db.get().await.map_err(Error::DatabaseConnection)?;
|
||||||
|
diesel::insert_into(schema::operator_identity::table)
|
||||||
|
.values(schema::operator_identity::public_key.eq(&new_pubkey))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::ExecutionFailed(format!("replace operator: {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::{
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ async fn handle_create(
|
|||||||
client_id: p.client_id,
|
client_id: p.client_id,
|
||||||
},
|
},
|
||||||
Some(ProtoKind::ApproveServerUpdate(_)) => ProposalKind::ApproveServerUpdate,
|
Some(ProtoKind::ApproveServerUpdate(_)) => ProposalKind::ApproveServerUpdate,
|
||||||
|
Some(ProtoKind::ReplaceOperator(p)) => ProposalKind::ReplaceOperator {
|
||||||
|
new_pubkey: p.new_pubkey,
|
||||||
|
},
|
||||||
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);
|
||||||
|
|||||||
@@ -500,6 +500,56 @@ async fn grant_wallet_access_on_quorum_approval() {
|
|||||||
assert_eq!(count, 1);
|
assert_eq!(count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn replace_operator_inserts_identity_row() {
|
||||||
|
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 new_op_key = authn::SigningKey::generate();
|
||||||
|
let new_pubkey = new_op_key.public_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
let proposal_id = actors
|
||||||
|
.proposal_manager
|
||||||
|
.ask(CreateProposal {
|
||||||
|
kind: ProposalKind::ReplaceOperator { new_pubkey },
|
||||||
|
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 = operator_identity::table
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(count, 2); // original + new
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn approve_server_update_reaches_quorum() {
|
async fn approve_server_update_reaches_quorum() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user