From 8421a09d9fabe4ea499d1adabf000b06acc0e730 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Thu, 27 Aug 2026 12:46:40 +0200 Subject: [PATCH] refactor(db!: store the one-off transaction signature by component --- .../2026-02-14-171124-0000_init/up.sql | 14 ++++++-- .../src/actors/proposal_manager.rs | 8 +---- server/crates/arbiter-server/src/db/models.rs | 7 ---- .../src/db/proposal/one_off_transaction.rs | 34 ++++++++++++++++++- server/crates/arbiter-server/src/db/schema.rs | 10 +++--- .../crates/arbiter-server/tests/governance.rs | 21 ++++++++---- 6 files changed, 65 insertions(+), 29 deletions(-) diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 819dbe7..36a5310 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -314,9 +314,17 @@ create table if not exists proposal_vote ( ) 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, +-- The signature the vault produced for an approved transaction, by component. +-- +-- 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')) ) STRICT; diff --git a/server/crates/arbiter-server/src/actors/proposal_manager.rs b/server/crates/arbiter-server/src/actors/proposal_manager.rs index d43bd17..e7ab118 100644 --- a/server/crates/arbiter-server/src/actors/proposal_manager.rs +++ b/server/crates/arbiter-server/src/actors/proposal_manager.rs @@ -661,7 +661,6 @@ impl ProposalManager { tx: one_off_transaction::Settings, ) -> Result<(), Error> { use crate::actors::evm::ClientSignTransaction; - use crate::db::models::NewProposalResult; use alloy::{ consensus::TxEip1559, eips::eip2930::AccessList, @@ -691,12 +690,7 @@ impl ProposalManager { .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) + one_off_transaction::store_signature(proposal_id, &sig, &mut conn) .await .map_err(|e| Error::ExecutionFailed(format!("store proposal result: {e}")))?; diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 3ab5a75..fd749d1 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -515,13 +515,6 @@ pub struct NewProposalVote { pub signature: Vec, } -#[derive(Debug, Insertable)] -#[diesel(table_name = schema::proposal_result, check_for_backend(Sqlite))] -pub struct NewProposalResult { - pub proposal_id: i32, - pub data: Vec, -} - #[derive(Debug, Insertable)] #[diesel(table_name = schema::recovery_proposal_vote, check_for_backend(Sqlite))] pub struct NewRecoveryProposalVote { diff --git a/server/crates/arbiter-server/src/db/proposal/one_off_transaction.rs b/server/crates/arbiter-server/src/db/proposal/one_off_transaction.rs index d66cbd4..4f2ec97 100644 --- a/server/crates/arbiter-server/src/db/proposal/one_off_transaction.rs +++ b/server/crates/arbiter-server/src/db/proposal/one_off_transaction.rs @@ -1,7 +1,10 @@ //! Signing a single EIP-1559 transaction. 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::{ Insertable, QueryDsl as _, QueryResult, Queryable, Selectable, SelectableHelper as _, sqlite::Sqlite, @@ -100,3 +103,32 @@ impl Proposal for OneOffTransaction { 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, + s: Vec, + 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) +} diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index b14da10..8123290 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -270,9 +270,11 @@ diesel::table! { } diesel::table! { - proposal_result (proposal_id) { + proposal_one_off_transaction_result (proposal_id) { proposal_id -> Integer, - data -> Binary, + r -> Binary, + s -> Binary, + y_parity -> 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!(program_client -> client_metadata (metadata_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_grant_wallet_access -> 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!( aead_encrypted, - proposal_result, + proposal_one_off_transaction_result, proposal_approve_sdk_client, proposal_grant_wallet_access, proposal_replace_operator, diff --git a/server/crates/arbiter-server/tests/governance.rs b/server/crates/arbiter-server/tests/governance.rs index e217c9c..426af2d 100644 --- a/server/crates/arbiter-server/tests/governance.rs +++ b/server/crates/arbiter-server/tests/governance.rs @@ -19,7 +19,7 @@ use arbiter_server::{ use arbiter_server::actors::vault::Bootstrap; use arbiter_server::db::schema::{ 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_async::RunQueryDsl; @@ -790,13 +790,20 @@ async fn approve_one_off_transaction_stores_result() { assert_eq!(outcome, VoteOutcome::Approved); 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) + let (r, s, y_parity): (Vec, Vec, i32) = proposal_one_off_transaction_result::table + .find(proposal_id) + .select(( + proposal_one_off_transaction_result::r, + proposal_one_off_transaction_result::s, + proposal_one_off_transaction_result::y_parity, + )) + .first(&mut conn) .await - .unwrap(); - assert_eq!(count, 1); + .expect("an approved transaction must leave its signature"); + + 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]