WIP: feat-shamir (old) #103

Draft
CleverWild wants to merge 66 commits from feat-shamir into main
6 changed files with 220 additions and 1 deletions
Showing only changes of commit d12109c2c9 - Show all commits

View File

@@ -237,3 +237,10 @@ create table if not exists proposal_vote (
voted_at integer not null default(unixepoch('now')),
unique (proposal_id, operator_id)
) STRICT;
create table if not exists proposal_result (
proposal_id integer not null primary key references proposal(id) on delete cascade,
data blob not null,
CleverWild marked this conversation as resolved Outdated

why the fuck do you store binary data inside relational database. declare tables, wigga

why the fuck do you store binary data inside relational database. declare tables, wigga
created_at integer not null default(unixepoch('now'))
) STRICT;

View File

@@ -22,6 +22,7 @@ pub enum ProposalKind {
ReplaceOperator { new_pubkey: Vec<u8> },
UpdateShamirParameters { new_n: u8 },
ApprovePersistentGrant { payload_bytes: Vec<u8> },
ApproveOneOffTransaction { payload_bytes: Vec<u8> },
}
impl ProposalKind {
@@ -33,6 +34,7 @@ impl ProposalKind {
Self::ReplaceOperator { .. } => "replace_operator",
Self::UpdateShamirParameters { .. } => "update_shamir_parameters",
Self::ApprovePersistentGrant { .. } => "approve_persistent_grant",
Self::ApproveOneOffTransaction { .. } => "approve_one_off_transaction",
}
CleverWild marked this conversation as resolved Outdated

ProposalKindTag should be derived using this enum and strum crate, wigga

`ProposalKindTag` should be derived using this enum and `strum` crate, wigga
}
@@ -56,6 +58,7 @@ impl ProposalKind {
}
Self::UpdateShamirParameters { new_n } => vec![*new_n],
Self::ApprovePersistentGrant { payload_bytes } => payload_bytes.clone(),
Self::ApproveOneOffTransaction { payload_bytes } => payload_bytes.clone(),
}
}
CleverWild marked this conversation as resolved Outdated

explained alr

explained alr
@@ -98,6 +101,9 @@ impl ProposalKind {
"approve_persistent_grant" => Ok(Self::ApprovePersistentGrant {
payload_bytes: payload.to_vec(),
}),
"approve_one_off_transaction" => Ok(Self::ApproveOneOffTransaction {
payload_bytes: payload.to_vec(),
}),
other => Err(format!("unknown proposal kind: {other}")),
}
}
@@ -436,6 +442,9 @@ impl ProposalManager {
ProposalKind::ApprovePersistentGrant { payload_bytes } => {
self.execute_approve_persistent_grant(payload_bytes).await
}
ProposalKind::ApproveOneOffTransaction { payload_bytes } => {
self.execute_approve_one_off_transaction(proposal.id, payload_bytes).await
}
}
}
@@ -476,6 +485,72 @@ impl ProposalManager {
Ok(())
}
async fn execute_approve_one_off_transaction(
&self,
proposal_id: i32,
payload_bytes: Vec<u8>,
) -> Result<(), Error> {
use arbiter_proto::proto::operator::governance::ApproveOneOffTransactionPayload;
use crate::actors::evm::ClientSignTransaction;
use crate::db::models::NewProposalResult;
use alloy::{
consensus::TxEip1559,
eips::eip2930::AccessList,
primitives::{Address, Bytes, TxKind, U256},
};
use prost::Message as _;
let p = ApproveOneOffTransactionPayload::decode(payload_bytes.as_slice())
.map_err(|e| Error::ExecutionFailed(format!("decode one-off tx payload: {e}")))?;
let wallet_address = Address::from_slice(p.wallet_address.as_slice());
let to = Address::from_slice(p.to.as_slice());
let transaction = TxEip1559 {
chain_id: p.chain_id,
nonce: p.nonce,
gas_limit: p.gas_limit,
max_fee_per_gas: u128::from_be_bytes(
p.max_fee_per_gas
.as_slice()
.try_into()
.map_err(|_| Error::ExecutionFailed("invalid max_fee_per_gas".to_owned()))?,
),
max_priority_fee_per_gas: u128::from_be_bytes(
p.max_priority_fee_per_gas
.as_slice()
.try_into()
.map_err(|_| Error::ExecutionFailed("invalid max_priority_fee_per_gas".to_owned()))?,
),
to: TxKind::Call(to),
value: U256::from_be_slice(p.value.as_slice()),
input: Bytes::from(p.input),
access_list: AccessList::default(),
};
let sig = self
.evm
.ask(ClientSignTransaction {
client_id: p.client_id,
CleverWild marked this conversation as resolved Outdated

OperatorId unit type

`OperatorId` unit type
wallet_address,
transaction,
})
.await
.map_err(|e| Error::ExecutionFailed(format!("sign one-off tx: {e}")))?;
let mut conn = self.db.get().await.map_err(Error::DatabaseConnection)?;
diesel::insert_into(schema::proposal_result::table)
.values(NewProposalResult {
proposal_id,
data: sig.as_bytes().to_vec(),
})
.execute(&mut conn)
.await
.map_err(|e| Error::ExecutionFailed(format!("store proposal result: {e}")))?;
Ok(())
CleverWild marked this conversation as resolved Outdated

OperatorId unit type

`OperatorId` unit type
}
async fn execute_approve_persistent_grant(&self, payload_bytes: Vec<u8>) -> Result<(), Error> {
use arbiter_proto::proto::operator::governance::{
ApprovePersistentGrantPayload,

View File

@@ -520,3 +520,11 @@ pub struct NewProposalVote {
pub approve: bool,
pub signature: Vec<u8>,
}
#[derive(Debug, Insertable)]
#[diesel(table_name = schema::proposal_result, check_for_backend(Sqlite))]
pub struct NewProposalResult {
pub proposal_id: i32,
pub data: Vec<u8>,
}

View File

@@ -184,6 +184,14 @@ diesel::table! {
}
}
diesel::table! {
proposal_result (proposal_id) {
proposal_id -> Integer,
data -> Binary,
created_at -> Integer,
}
}
diesel::table! {
proposal_vote (id) {
id -> Integer,
@@ -249,11 +257,13 @@ diesel::joinable!(evm_wallet_access -> program_client (client_id));
diesel::joinable!(operator -> operator_identity (id));
diesel::joinable!(program_client -> client_metadata (metadata_id));
diesel::joinable!(proposal -> operator_identity (initiator_id));
diesel::joinable!(proposal_result -> proposal (proposal_id));
diesel::joinable!(proposal_vote -> proposal (proposal_id));
diesel::joinable!(proposal_vote -> operator_identity (operator_id));
diesel::allow_tables_to_appear_in_same_query!(
aead_encrypted,
proposal_result,
arbiter_settings,
client_metadata,
client_metadata_history,

View File

@@ -64,6 +64,10 @@ async fn handle_create(
use prost::Message as _;
ProposalKind::ApprovePersistentGrant { payload_bytes: p.encode_to_vec() }
}
Some(ProtoKind::ApproveOneOffTransaction(p)) => {
use prost::Message as _;
ProposalKind::ApproveOneOffTransaction { payload_bytes: p.encode_to_vec() }
}
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::{aead_encrypted, evm_basic_grant, evm_wallet, evm_wallet_access, operator_identity};
use arbiter_server::db::schema::{aead_encrypted, evm_basic_grant, evm_wallet, evm_wallet_access, operator_identity, proposal_result};
use diesel::{ExpressionMethods, QueryDsl, insert_into};
use diesel_async::RunQueryDsl;
@@ -588,6 +588,121 @@ async fn approve_persistent_grant_creates_basic_grant_row() {
assert_eq!(count, 1);
}
#[tokio::test]
async fn approve_one_off_transaction_stores_result() {
use arbiter_proto::proto::operator::governance::ApproveOneOffTransactionPayload;
use arbiter_server::actors::evm::{Generate, OperatorCreateGrant};
use arbiter_server::evm::policies::{
SharedGrantSettings, SpecificGrant, VolumeRateLimit, ether_transfer,
};
use alloy::primitives::{Address, U256};
use chrono::Duration;
use prost::Message as _;
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;
// Create a real encrypted wallet
let (wallet_id, wallet_address) = actors.evm.ask(Generate {}).await.unwrap();
// Create a client and wallet_access
let client_key = authn::SigningKey::generate();
let client_id = insert_unapproved_client(&db, &client_key.public_key()).await;
let mut conn = db.get().await.unwrap();
let wallet_access_id: i32 = insert_into(evm_wallet_access::table)
.values((
evm_wallet_access::wallet_id.eq(wallet_id),
evm_wallet_access::client_id.eq(client_id),
))
.returning(evm_wallet_access::id)
.get_result(&mut conn)
.await
.unwrap();
drop(conn);
// Create a grant that permits ether transfer to address zero
let to_address = Address::ZERO;
actors
.evm
.ask(OperatorCreateGrant {
basic: SharedGrantSettings {
wallet_access_id,
chain: 1,
valid_from: None,
valid_until: None,
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,
},
grant: SpecificGrant::EtherTransfer(ether_transfer::Settings {
target: vec![to_address],
limit: VolumeRateLimit {
max_volume: U256::from(1_000_000_000_000_000_000u128),
window: Duration::hours(24),
},
}),
})
.await
.unwrap();
// Encode the one-off transaction payload
let payload = ApproveOneOffTransactionPayload {
client_id,
wallet_address: wallet_address.as_slice().to_vec(),
chain_id: 1,
nonce: 0,
gas_limit: 21000,
max_fee_per_gas: 1u128.to_be_bytes().to_vec(),
max_priority_fee_per_gas: 1u128.to_be_bytes().to_vec(),
to: to_address.as_slice().to_vec(),
value: U256::from(1u64).to_be_bytes_vec(),
input: vec![],
};
let proposal_id = actors
.proposal_manager
.ask(CreateProposal {
kind: ProposalKind::ApproveOneOffTransaction { payload_bytes: payload.encode_to_vec() },
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 = proposal_result::table
.filter(proposal_result::proposal_id.eq(proposal_id))
.count()
.get_result(&mut conn)
.await
.unwrap();
assert_eq!(count, 1);
}
#[tokio::test]
async fn replace_operator_inserts_identity_row() {
let db = db::create_test_pool().await;