Files
serai/processor/bitcoin/src/scheduler.rs

215 lines
7.6 KiB
Rust
Raw Normal View History

use core::future::Future;
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
use ciphersuite::*;
2025-08-25 09:17:29 -04:00
use ciphersuite_kp256::Secp256k1;
2024-09-11 00:01:40 -04:00
use bitcoin_serai::{
bitcoin::ScriptBuf,
wallet::{TransactionError, SignableTransaction as BSignableTransaction, p2tr_script_buf},
};
use serai_client::{
primitives::{coin::ExternalCoin, balance::Amount},
2024-09-11 00:01:40 -04:00
networks::bitcoin::Address,
};
use serai_db::Db;
2024-09-11 00:01:40 -04:00
use primitives::{OutputType, ReceivedOutput, Payment};
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
use transaction_chaining_scheduler::{EffectedReceivedOutputs, Scheduler as GenericScheduler};
use crate::{
scan::{offsets_for_key, scanner},
output::Output,
transaction::{SignableTransaction, Eventuality},
2024-09-11 00:48:52 -04:00
rpc::Rpc,
2024-09-11 00:01:40 -04:00
};
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
fn address_from_serai_key(key: <Secp256k1 as WrappedGroup>::G, kind: OutputType) -> Address {
let offset = <Secp256k1 as WrappedGroup>::G::GENERATOR * offsets_for_key(key)[&kind];
2024-09-11 00:01:40 -04:00
Address::new(
p2tr_script_buf(key + offset)
.expect("creating address from Serai key which wasn't properly tweaked"),
)
.expect("couldn't create Serai-representable address for P2TR script")
}
fn signable_transaction<D: Db>(
_reference_block: &BlockFor<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
change: Option<KeyFor<Rpc<D>>>,
2024-09-11 00:01:40 -04:00
) -> Result<(SignableTransaction, BSignableTransaction), TransactionError> {
assert!(
inputs.len() <
<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::MAX_INPUTS
);
assert!(
(payments.len() + usize::from(u8::from(change.is_some()))) <
<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::MAX_OUTPUTS
);
2024-09-11 00:01:40 -04:00
// TODO
let fee_per_vbyte = 1;
2024-09-11 00:01:40 -04:00
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
let mut payments = payments
2024-09-11 00:01:40 -04:00
.into_iter()
.map(|payment| {
(ScriptBuf::from(payment.address().clone()), {
2024-09-11 00:01:40 -04:00
let balance = payment.balance();
2025-01-30 03:14:24 -05:00
assert_eq!(balance.coin, ExternalCoin::Bitcoin);
2024-09-11 00:01:40 -04:00
balance.amount.0
})
})
.collect::<Vec<_>>();
/*
Push a payment to a key with a known private key which anyone can spend. If this transaction
gets stuck, this lets anyone create a child transaction spending this output, raising the fee,
getting the transaction unstuck (via CPFP).
*/
payments.push((
// The generator is even so this is valid
Smash the singular `Ciphersuite` trait into multiple This helps identify where the various functionalities are used, or rather, not used. The `Ciphersuite` trait present in `patches/ciphersuite`, facilitating the entire FCMP++ tree, only requires the markers _and_ canonical point decoding. I've opened a PR to upstream such a trait into `group` (https://github.com/zkcrypto/group/pull/68). `WrappedGroup` is still justified for as long as `Group::generator` exists. Moving `::generator()` to its own trait, on an independent structure (upstream) would be massively appreciated. @tarcieri also wanted to update from `fn generator()` to `const GENERATOR`, which would encourage further discussion on https://github.com/zkcrypto/group/issues/32 and https://github.com/zkcrypto/group/issues/45, which have been stagnant. The `Id` trait is occasionally used yet really should be first off the chopping block. Finally, `WithPreferredHash` is only actually used around a third of the time, which more than justifies it being a separate trait. --- Updates `dalek_ff_group::Scalar` to directly re-export `curve25519_dalek::Scalar`, as without issue. `dalek_ff_group::RistrettoPoint` also could be replaced with an export of `curve25519_dalek::RistrettoPoint`, yet the coordinator relies on how we implemented `Hash` on it for the hell of it so it isn't worth it at this time. `dalek_ff_group::EdwardsPoint` can't be replaced for an re-export of `curve25519_dalek::SubgroupPoint` as it doesn't implement `zeroize`, `subtle` traits within a released, non-yanked version. Relevance to https://github.com/serai-dex/serai/issues/201 and https://github.com/dalek-cryptography/curve25519-dalek/issues/811#issuecomment-3247732746. Also updates the `Ristretto` ciphersuite to prefer `Blake2b-512` over `SHA2-512`. In order to maintain compliance with FROST's IETF standard, `modular-frost` defines its own ciphersuite for Ristretto which still uses `SHA2-512`.
2025-09-03 12:25:37 -04:00
p2tr_script_buf(<Secp256k1 as WrappedGroup>::G::GENERATOR).unwrap(),
// This uses the minimum output value allowed, as defined as a constant in bitcoin-serai
// TODO: Add a test for this comparing to bitcoin's `minimal_non_dust`
bitcoin_serai::wallet::DUST,
));
let change = change
.map(<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::change_address);
2024-09-11 00:01:40 -04:00
BSignableTransaction::new(
inputs.clone(),
&payments,
2024-09-11 00:01:40 -04:00
change.clone().map(ScriptBuf::from),
None,
fee_per_vbyte,
)
.map(|bst| (SignableTransaction { inputs, payments, change, fee_per_vbyte }, bst))
}
#[derive(Clone)]
2024-09-11 00:01:40 -04:00
pub(crate) struct Planner;
impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Planner {
type EphemeralError = ();
2024-09-11 00:01:40 -04:00
type SignableTransaction = SignableTransaction;
/*
Bitcoin has a max weight of 400,000 (MAX_STANDARD_TX_WEIGHT).
A non-SegWit TX will have 4 weight units per byte, leaving a max size of 100,000 bytes. While
our inputs are entirely SegWit, such fine tuning is not necessary and could create issues in
the future (if the size decreases or we misevaluate it). It also offers a minimal amount of
benefit when we are able to logarithmically accumulate inputs/fulfill payments.
For 128-byte inputs (36-byte output specification, 64-byte signature, whatever overhead) and
64-byte outputs (40-byte script, 8-byte amount, whatever overhead), they together take up 192
bytes.
100,000 / 192 = 520
520 * 192 leaves 160 bytes of overhead for the transaction structure itself.
*/
const MAX_INPUTS: usize = 520;
// We always reserve one output to create an anyone-can-spend output enabling anyone to use CPFP
// to unstick any transactions which had too low of a fee.
const MAX_OUTPUTS: usize = 519;
fn branch_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
2024-09-11 00:01:40 -04:00
address_from_serai_key(key, OutputType::Branch)
}
fn change_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
2024-09-11 00:01:40 -04:00
address_from_serai_key(key, OutputType::Change)
}
fn forwarding_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
2024-09-11 00:01:40 -04:00
address_from_serai_key(key, OutputType::Forwarded)
}
fn calculate_fee(
&self,
reference_block: &BlockFor<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
change: Option<KeyFor<Rpc<D>>>,
) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>> {
async move {
Ok(match signable_transaction::<D>(reference_block, inputs, payments, change) {
Ok(tx) => Amount(tx.1.needed_fee()),
Err(
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
) => panic!("malformed arguments to calculate_fee"),
// No data, we have a minimum fee rate, we checked the amount of inputs/outputs
Err(
TransactionError::TooMuchData |
TransactionError::TooLowFee |
TransactionError::TooLargeTransaction,
) => unreachable!(),
Err(TransactionError::NotEnoughFunds { fee, .. }) => Amount(fee),
})
2024-09-11 00:01:40 -04:00
}
}
fn plan(
&self,
reference_block: &BlockFor<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
change: Option<KeyFor<Rpc<D>>>,
) -> impl Send
+ Future<
Output = Result<
PlannedTransaction<Rpc<D>, Self::SignableTransaction, EffectedReceivedOutputs<Rpc<D>>>,
Self::EphemeralError,
>,
> {
async move {
let key = inputs.first().unwrap().key();
for input in &inputs {
assert_eq!(key, input.key());
}
2024-09-11 00:01:40 -04:00
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
match signable_transaction::<D>(reference_block, inputs.clone(), payments, change) {
Ok(tx) => Ok(PlannedTransaction {
signable: tx.0,
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output },
auxilliary: EffectedReceivedOutputs({
let tx = tx.1.transaction();
let scanner = scanner(key);
let mut res = vec![];
for output in scanner.scan_transaction(tx) {
res.push(Output::new_with_presumed_origin(
key,
tx,
// It shouldn't matter if this is wrong as we should never try to return these
// We still provide an accurate value to ensure a lack of discrepancies
Some(Address::new(inputs[0].output.output().script_pubkey.clone()).unwrap()),
output,
));
}
res
}),
2024-09-11 00:01:40 -04:00
}),
Err(
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
) => panic!("malformed arguments to plan"),
// No data, we have a minimum fee rate, we checked the amount of inputs/outputs
Err(
TransactionError::TooMuchData |
TransactionError::TooLowFee |
TransactionError::TooLargeTransaction,
) => unreachable!(),
Err(TransactionError::NotEnoughFunds { .. }) => {
panic!("plan called for a transaction without enough funds")
}
2024-09-11 00:01:40 -04:00
}
}
}
}
pub(crate) type Scheduler<D> = GenericScheduler<Rpc<D>, Planner>;