refactor(db!: store the one-off transaction signature by component

This commit is contained in:
CleverWild
2026-08-27 12:46:40 +02:00
parent 15b826310b
commit 8421a09d9f
6 changed files with 65 additions and 29 deletions

View File

@@ -314,9 +314,17 @@ create table if not exists proposal_vote (
) STRICT; ) STRICT;
create table if not exists proposal_result ( -- The signature the vault produced for an approved transaction, by component.
proposal_id integer not null primary key references proposal(id) on delete cascade, --
data blob not null, -- secp256k1 signatures have three common encodings (Electrum v=27/28, raw parity,
-- ERC-2098 compact); a single blob would not say which one it holds. `y_parity` is
-- the raw bit -- add 27 to rebuild the Electrum form `Signature::as_bytes` emits.
create table if not exists proposal_one_off_transaction_result (
proposal_id integer not null primary key
references proposal_one_off_transaction (proposal_id) on delete cascade,
r blob not null check (length(r) = 32),
s blob not null check (length(s) = 32),
y_parity integer not null check (y_parity in (0, 1)),
created_at integer not null default(unixepoch('now')) created_at integer not null default(unixepoch('now'))
) STRICT; ) STRICT;

View File

@@ -661,7 +661,6 @@ impl ProposalManager {
tx: one_off_transaction::Settings, tx: one_off_transaction::Settings,
) -> Result<(), Error> { ) -> Result<(), Error> {
use crate::actors::evm::ClientSignTransaction; use crate::actors::evm::ClientSignTransaction;
use crate::db::models::NewProposalResult;
use alloy::{ use alloy::{
consensus::TxEip1559, consensus::TxEip1559,
eips::eip2930::AccessList, eips::eip2930::AccessList,
@@ -691,12 +690,7 @@ impl ProposalManager {
.map_err(|e| Error::ExecutionFailed(format!("sign one-off tx: {e}")))?; .map_err(|e| Error::ExecutionFailed(format!("sign one-off tx: {e}")))?;
let mut conn = self.db.get().await.map_err(Error::DatabaseConnection)?; let mut conn = self.db.get().await.map_err(Error::DatabaseConnection)?;
diesel::insert_into(schema::proposal_result::table) one_off_transaction::store_signature(proposal_id, &sig, &mut conn)
.values(NewProposalResult {
proposal_id,
data: sig.as_bytes().to_vec(),
})
.execute(&mut conn)
.await .await
.map_err(|e| Error::ExecutionFailed(format!("store proposal result: {e}")))?; .map_err(|e| Error::ExecutionFailed(format!("store proposal result: {e}")))?;

View File

@@ -515,13 +515,6 @@ pub struct NewProposalVote {
pub signature: Vec<u8>, 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>,
}
#[derive(Debug, Insertable)] #[derive(Debug, Insertable)]
#[diesel(table_name = schema::recovery_proposal_vote, check_for_backend(Sqlite))] #[diesel(table_name = schema::recovery_proposal_vote, check_for_backend(Sqlite))]
pub struct NewRecoveryProposalVote { pub struct NewRecoveryProposalVote {

View File

@@ -1,7 +1,10 @@
//! Signing a single EIP-1559 transaction. //! Signing a single EIP-1559 transaction.
use super::{Proposal, ProposalKindTag, as_i64, as_u64, fixed}; use super::{Proposal, ProposalKindTag, as_i64, as_u64, fixed};
use crate::db::{DatabaseConnection, schema::proposal_one_off_transaction}; use crate::db::{
DatabaseConnection,
schema::{proposal_one_off_transaction, proposal_one_off_transaction_result},
};
use diesel::{ use diesel::{
Insertable, QueryDsl as _, QueryResult, Queryable, Selectable, SelectableHelper as _, Insertable, QueryDsl as _, QueryResult, Queryable, Selectable, SelectableHelper as _,
sqlite::Sqlite, sqlite::Sqlite,
@@ -100,3 +103,32 @@ impl Proposal for OneOffTransaction {
row.into_settings() row.into_settings()
} }
} }
/// The signature the vault produced for an approved transaction.
#[derive(Debug, Insertable)]
#[diesel(table_name = proposal_one_off_transaction_result, check_for_backend(Sqlite))]
struct SignatureRow {
proposal_id: i32,
r: Vec<u8>,
s: Vec<u8>,
y_parity: i32,
}
/// Records the signature produced for an approved transaction, by component, so what
/// came back is as readable as what was signed.
pub async fn store_signature(
proposal_id: i32,
signature: &alloy::signers::Signature,
conn: &mut DatabaseConnection,
) -> QueryResult<()> {
diesel::insert_into(proposal_one_off_transaction_result::table)
.values(&SignatureRow {
proposal_id,
r: signature.r().to_be_bytes::<32>().to_vec(),
s: signature.s().to_be_bytes::<32>().to_vec(),
y_parity: i32::from(signature.v()),
})
.execute(conn)
.await
.map(drop)
}

View File

@@ -270,9 +270,11 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
proposal_result (proposal_id) { proposal_one_off_transaction_result (proposal_id) {
proposal_id -> Integer, proposal_id -> Integer,
data -> Binary, r -> Binary,
s -> Binary,
y_parity -> Integer,
created_at -> Integer, created_at -> Integer,
} }
} }
@@ -383,7 +385,7 @@ diesel::joinable!(evm_wallet_access -> program_client (client_id));
diesel::joinable!(operator -> operator_identity (id)); diesel::joinable!(operator -> operator_identity (id));
diesel::joinable!(program_client -> client_metadata (metadata_id)); diesel::joinable!(program_client -> client_metadata (metadata_id));
diesel::joinable!(proposal -> operator_identity (initiator_id)); diesel::joinable!(proposal -> operator_identity (initiator_id));
diesel::joinable!(proposal_result -> proposal (proposal_id)); diesel::joinable!(proposal_one_off_transaction_result -> proposal_one_off_transaction (proposal_id));
diesel::joinable!(proposal_approve_sdk_client -> proposal (proposal_id)); diesel::joinable!(proposal_approve_sdk_client -> proposal (proposal_id));
diesel::joinable!(proposal_grant_wallet_access -> proposal (proposal_id)); diesel::joinable!(proposal_grant_wallet_access -> proposal (proposal_id));
diesel::joinable!(proposal_replace_operator -> proposal (proposal_id)); diesel::joinable!(proposal_replace_operator -> proposal (proposal_id));
@@ -400,7 +402,7 @@ diesel::joinable!(recovery_wakeup_request -> operator_identity (requested_by));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
aead_encrypted, aead_encrypted,
proposal_result, proposal_one_off_transaction_result,
proposal_approve_sdk_client, proposal_approve_sdk_client,
proposal_grant_wallet_access, proposal_grant_wallet_access,
proposal_replace_operator, proposal_replace_operator,

View File

@@ -19,7 +19,7 @@ use arbiter_server::{
use arbiter_server::actors::vault::Bootstrap; use arbiter_server::actors::vault::Bootstrap;
use arbiter_server::db::schema::{ use arbiter_server::db::schema::{
aead_encrypted, evm_basic_grant, evm_wallet, evm_wallet_access, operator_identity, aead_encrypted, evm_basic_grant, evm_wallet, evm_wallet_access, operator_identity,
proposal_result, recovery_operator_identity, proposal_one_off_transaction_result, recovery_operator_identity,
}; };
use diesel::{ExpressionMethods, QueryDsl, insert_into}; use diesel::{ExpressionMethods, QueryDsl, insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
@@ -790,13 +790,20 @@ async fn approve_one_off_transaction_stores_result() {
assert_eq!(outcome, VoteOutcome::Approved); assert_eq!(outcome, VoteOutcome::Approved);
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let count: i64 = proposal_result::table let (r, s, y_parity): (Vec<u8>, Vec<u8>, i32) = proposal_one_off_transaction_result::table
.filter(proposal_result::proposal_id.eq(proposal_id)) .find(proposal_id)
.count() .select((
.get_result(&mut conn) proposal_one_off_transaction_result::r,
proposal_one_off_transaction_result::s,
proposal_one_off_transaction_result::y_parity,
))
.first(&mut conn)
.await .await
.unwrap(); .expect("an approved transaction must leave its signature");
assert_eq!(count, 1);
assert_eq!(r.len(), 32, "r must be a 32-byte scalar");
assert_eq!(s.len(), 32, "s must be a 32-byte scalar");
assert!(y_parity == 0 || y_parity == 1, "y_parity must be a bit");
} }
#[tokio::test] #[tokio::test]