mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-13 06:14:44 +00:00
Monero Planner
Finishes the Monero processor.
This commit is contained in:
parent
e23176deeb
commit
0616085109
13 changed files with 406 additions and 755 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -8525,6 +8525,7 @@ dependencies = [
|
||||||
"monero-simple-request-rpc",
|
"monero-simple-request-rpc",
|
||||||
"monero-wallet",
|
"monero-wallet",
|
||||||
"parity-scale-codec",
|
"parity-scale-codec",
|
||||||
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"serai-client",
|
"serai-client",
|
||||||
"serai-db",
|
"serai-db",
|
||||||
|
|
|
@ -223,15 +223,6 @@ impl Network for Bitcoin {
|
||||||
self.rpc.get_block_number(id).await.unwrap()
|
self.rpc.get_block_number(id).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn check_eventuality_by_claim(
|
|
||||||
&self,
|
|
||||||
eventuality: &Self::Eventuality,
|
|
||||||
_: &EmptyClaim,
|
|
||||||
) -> bool {
|
|
||||||
self.rpc.get_transaction(&eventuality.0).await.is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
async fn get_transaction_by_eventuality(&self, _: usize, id: &Eventuality) -> Transaction {
|
async fn get_transaction_by_eventuality(&self, _: usize, id: &Eventuality) -> Transaction {
|
||||||
self.rpc.get_transaction(&id.0).await.unwrap()
|
self.rpc.get_transaction(&id.0).await.unwrap()
|
||||||
|
|
|
@ -49,7 +49,7 @@ impl scheduler::Transaction for Transaction {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct SignableTransaction {
|
pub(crate) struct SignableTransaction {
|
||||||
pub(crate) inputs: Vec<ReceivedOutput>,
|
pub(crate) inputs: Vec<ReceivedOutput>,
|
||||||
pub(crate) payments: Vec<(Address, u64)>,
|
pub(crate) payments: Vec<(ScriptBuf, u64)>,
|
||||||
pub(crate) change: Option<Address>,
|
pub(crate) change: Option<Address>,
|
||||||
pub(crate) fee_per_vbyte: u64,
|
pub(crate) fee_per_vbyte: u64,
|
||||||
}
|
}
|
||||||
|
@ -58,12 +58,7 @@ impl SignableTransaction {
|
||||||
fn signable(self) -> Result<BSignableTransaction, TransactionError> {
|
fn signable(self) -> Result<BSignableTransaction, TransactionError> {
|
||||||
BSignableTransaction::new(
|
BSignableTransaction::new(
|
||||||
self.inputs,
|
self.inputs,
|
||||||
&self
|
&self.payments,
|
||||||
.payments
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|(address, amount)| (ScriptBuf::from(address), amount))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
self.change.map(ScriptBuf::from),
|
self.change.map(ScriptBuf::from),
|
||||||
None,
|
None,
|
||||||
self.fee_per_vbyte,
|
self.fee_per_vbyte,
|
||||||
|
@ -108,11 +103,19 @@ impl scheduler::SignableTransaction for SignableTransaction {
|
||||||
inputs
|
inputs
|
||||||
};
|
};
|
||||||
|
|
||||||
let payments = <_>::deserialize_reader(reader)?;
|
let payments = Vec::<(Vec<u8>, u64)>::deserialize_reader(reader)?;
|
||||||
let change = <_>::deserialize_reader(reader)?;
|
let change = <_>::deserialize_reader(reader)?;
|
||||||
let fee_per_vbyte = <_>::deserialize_reader(reader)?;
|
let fee_per_vbyte = <_>::deserialize_reader(reader)?;
|
||||||
|
|
||||||
Ok(Self { inputs, payments, change, fee_per_vbyte })
|
Ok(Self {
|
||||||
|
inputs,
|
||||||
|
payments: payments
|
||||||
|
.into_iter()
|
||||||
|
.map(|(address, amount)| (ScriptBuf::from_bytes(address), amount))
|
||||||
|
.collect(),
|
||||||
|
change,
|
||||||
|
fee_per_vbyte,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
||||||
writer.write_all(&u32::try_from(self.inputs.len()).unwrap().to_le_bytes())?;
|
writer.write_all(&u32::try_from(self.inputs.len()).unwrap().to_le_bytes())?;
|
||||||
|
@ -120,7 +123,9 @@ impl scheduler::SignableTransaction for SignableTransaction {
|
||||||
input.write(writer)?;
|
input.write(writer)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.payments.serialize(writer)?;
|
for payment in &self.payments {
|
||||||
|
(payment.0.as_script().as_bytes(), payment.1).serialize(writer)?;
|
||||||
|
}
|
||||||
self.change.serialize(writer)?;
|
self.change.serialize(writer)?;
|
||||||
self.fee_per_vbyte.serialize(writer)?;
|
self.fee_per_vbyte.serialize(writer)?;
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ fn address_from_serai_key(key: <Secp256k1 as Ciphersuite>::G, kind: OutputType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signable_transaction<D: Db>(
|
fn signable_transaction<D: Db>(
|
||||||
fee_per_vbyte: u64,
|
_reference_block: &BlockFor<Rpc<D>>,
|
||||||
inputs: Vec<OutputFor<Rpc<D>>>,
|
inputs: Vec<OutputFor<Rpc<D>>>,
|
||||||
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
||||||
change: Option<KeyFor<Rpc<D>>>,
|
change: Option<KeyFor<Rpc<D>>>,
|
||||||
|
@ -49,12 +49,15 @@ fn signable_transaction<D: Db>(
|
||||||
<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::MAX_OUTPUTS
|
<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::MAX_OUTPUTS
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
let fee_per_vbyte = 1;
|
||||||
|
|
||||||
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
|
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut payments = payments
|
let mut payments = payments
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|payment| {
|
.map(|payment| {
|
||||||
(payment.address().clone(), {
|
(ScriptBuf::from(payment.address().clone()), {
|
||||||
let balance = payment.balance();
|
let balance = payment.balance();
|
||||||
assert_eq!(balance.coin, Coin::Bitcoin);
|
assert_eq!(balance.coin, Coin::Bitcoin);
|
||||||
balance.amount.0
|
balance.amount.0
|
||||||
|
@ -68,7 +71,7 @@ fn signable_transaction<D: Db>(
|
||||||
*/
|
*/
|
||||||
payments.push((
|
payments.push((
|
||||||
// The generator is even so this is valid
|
// The generator is even so this is valid
|
||||||
Address::new(p2tr_script_buf(<Secp256k1 as Ciphersuite>::G::GENERATOR).unwrap()).unwrap(),
|
p2tr_script_buf(<Secp256k1 as Ciphersuite>::G::GENERATOR).unwrap(),
|
||||||
// This uses the minimum output value allowed, as defined as a constant in bitcoin-serai
|
// 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`
|
// TODO: Add a test for this comparing to bitcoin's `minimal_non_dust`
|
||||||
bitcoin_serai::wallet::DUST,
|
bitcoin_serai::wallet::DUST,
|
||||||
|
@ -79,11 +82,7 @@ fn signable_transaction<D: Db>(
|
||||||
|
|
||||||
BSignableTransaction::new(
|
BSignableTransaction::new(
|
||||||
inputs.clone(),
|
inputs.clone(),
|
||||||
&payments
|
&payments,
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|(address, amount)| (ScriptBuf::from(address), amount))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
change.clone().map(ScriptBuf::from),
|
change.clone().map(ScriptBuf::from),
|
||||||
None,
|
None,
|
||||||
fee_per_vbyte,
|
fee_per_vbyte,
|
||||||
|
@ -95,7 +94,6 @@ fn signable_transaction<D: Db>(
|
||||||
pub(crate) struct Planner;
|
pub(crate) struct Planner;
|
||||||
impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Planner {
|
impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Planner {
|
||||||
type EphemeralError = ();
|
type EphemeralError = ();
|
||||||
type FeeRate = u64;
|
|
||||||
|
|
||||||
type SignableTransaction = SignableTransaction;
|
type SignableTransaction = SignableTransaction;
|
||||||
|
|
||||||
|
@ -119,12 +117,6 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
|
||||||
// to unstick any transactions which had too low of a fee.
|
// to unstick any transactions which had too low of a fee.
|
||||||
const MAX_OUTPUTS: usize = 519;
|
const MAX_OUTPUTS: usize = 519;
|
||||||
|
|
||||||
fn fee_rate(block: &BlockFor<Rpc<D>>, coin: Coin) -> Self::FeeRate {
|
|
||||||
assert_eq!(coin, Coin::Bitcoin);
|
|
||||||
// TODO
|
|
||||||
1
|
|
||||||
}
|
|
||||||
|
|
||||||
fn branch_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
|
fn branch_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
|
||||||
address_from_serai_key(key, OutputType::Branch)
|
address_from_serai_key(key, OutputType::Branch)
|
||||||
}
|
}
|
||||||
|
@ -136,12 +128,14 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_fee(
|
fn calculate_fee(
|
||||||
fee_rate: Self::FeeRate,
|
&self,
|
||||||
|
reference_block: &BlockFor<Rpc<D>>,
|
||||||
inputs: Vec<OutputFor<Rpc<D>>>,
|
inputs: Vec<OutputFor<Rpc<D>>>,
|
||||||
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
||||||
change: Option<KeyFor<Rpc<D>>>,
|
change: Option<KeyFor<Rpc<D>>>,
|
||||||
) -> Amount {
|
) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>> {
|
||||||
match signable_transaction::<D>(fee_rate, inputs, payments, change) {
|
async move {
|
||||||
|
Ok(match signable_transaction::<D>(reference_block, inputs, payments, change) {
|
||||||
Ok(tx) => Amount(tx.1.needed_fee()),
|
Ok(tx) => Amount(tx.1.needed_fee()),
|
||||||
Err(
|
Err(
|
||||||
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
|
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
|
||||||
|
@ -153,12 +147,13 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
|
||||||
TransactionError::TooLargeTransaction,
|
TransactionError::TooLargeTransaction,
|
||||||
) => unreachable!(),
|
) => unreachable!(),
|
||||||
Err(TransactionError::NotEnoughFunds { fee, .. }) => Amount(fee),
|
Err(TransactionError::NotEnoughFunds { fee, .. }) => Amount(fee),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plan(
|
fn plan(
|
||||||
&self,
|
&self,
|
||||||
fee_rate: Self::FeeRate,
|
reference_block: &BlockFor<Rpc<D>>,
|
||||||
inputs: Vec<OutputFor<Rpc<D>>>,
|
inputs: Vec<OutputFor<Rpc<D>>>,
|
||||||
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
|
||||||
change: Option<KeyFor<Rpc<D>>>,
|
change: Option<KeyFor<Rpc<D>>>,
|
||||||
|
@ -176,7 +171,7 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
|
||||||
}
|
}
|
||||||
|
|
||||||
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
|
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
|
||||||
match signable_transaction::<D>(fee_rate, inputs.clone(), payments, change) {
|
match signable_transaction::<D>(reference_block, inputs.clone(), payments, change) {
|
||||||
Ok(tx) => Ok(PlannedTransaction {
|
Ok(tx) => Ok(PlannedTransaction {
|
||||||
signable: tx.0,
|
signable: tx.0,
|
||||||
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output },
|
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output },
|
||||||
|
|
|
@ -18,6 +18,7 @@ workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand_core = { version = "0.6", default-features = false }
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
zeroize = { version = "1", default-features = false, features = ["std"] }
|
zeroize = { version = "1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
|
|
|
@ -1,319 +0,0 @@
|
||||||
/*
|
|
||||||
// TODO: Consider ([u8; 32], TransactionPruned)
|
|
||||||
#[async_trait]
|
|
||||||
impl TransactionTrait<Monero> for Transaction {
|
|
||||||
type Id = [u8; 32];
|
|
||||||
fn id(&self) -> Self::Id {
|
|
||||||
self.hash()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn fee(&self, _: &Monero) -> u64 {
|
|
||||||
match self {
|
|
||||||
Transaction::V1 { .. } => panic!("v1 TX in test-only function"),
|
|
||||||
Transaction::V2 { ref proofs, .. } => proofs.as_ref().unwrap().base.fee,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventualityTrait for Eventuality {
|
|
||||||
type Claim = [u8; 32];
|
|
||||||
type Completion = Transaction;
|
|
||||||
|
|
||||||
// Use the TX extra to look up potential matches
|
|
||||||
// While anyone can forge this, a transaction with distinct outputs won't actually match
|
|
||||||
// Extra includess the one time keys which are derived from the plan ID, so a collision here is a
|
|
||||||
// hash collision
|
|
||||||
fn lookup(&self) -> Vec<u8> {
|
|
||||||
self.extra()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
Eventuality::read(reader)
|
|
||||||
}
|
|
||||||
fn serialize(&self) -> Vec<u8> {
|
|
||||||
self.serialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn claim(tx: &Transaction) -> [u8; 32] {
|
|
||||||
tx.id()
|
|
||||||
}
|
|
||||||
fn serialize_completion(completion: &Transaction) -> Vec<u8> {
|
|
||||||
completion.serialize()
|
|
||||||
}
|
|
||||||
fn read_completion<R: io::Read>(reader: &mut R) -> io::Result<Transaction> {
|
|
||||||
Transaction::read(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SignableTransaction(MSignableTransaction);
|
|
||||||
impl SignableTransactionTrait for SignableTransaction {
|
|
||||||
fn fee(&self) -> u64 {
|
|
||||||
self.0.necessary_fee()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MakeSignableTransactionResult {
|
|
||||||
Fee(u64),
|
|
||||||
SignableTransaction(MSignableTransaction),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Monero {
|
|
||||||
pub async fn new(url: String) -> Monero {
|
|
||||||
let mut res = SimpleRequestRpc::new(url.clone()).await;
|
|
||||||
while let Err(e) = res {
|
|
||||||
log::error!("couldn't connect to Monero node: {e:?}");
|
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
||||||
res = SimpleRequestRpc::new(url.clone()).await;
|
|
||||||
}
|
|
||||||
Monero { rpc: res.unwrap() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_pair(spend: EdwardsPoint) -> GuaranteedViewPair {
|
|
||||||
GuaranteedViewPair::new(spend.0, Zeroizing::new(additional_key::<Monero>(0).0)).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn address_internal(spend: EdwardsPoint, subaddress: Option<SubaddressIndex>) -> Address {
|
|
||||||
Address::new(Self::view_pair(spend).address(MoneroNetwork::Mainnet, subaddress, None)).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scanner(spend: EdwardsPoint) -> GuaranteedScanner {
|
|
||||||
let mut scanner = GuaranteedScanner::new(Self::view_pair(spend));
|
|
||||||
debug_assert!(EXTERNAL_SUBADDRESS.is_none());
|
|
||||||
scanner.register_subaddress(BRANCH_SUBADDRESS.unwrap());
|
|
||||||
scanner.register_subaddress(CHANGE_SUBADDRESS.unwrap());
|
|
||||||
scanner.register_subaddress(FORWARD_SUBADDRESS.unwrap());
|
|
||||||
scanner
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn median_fee(&self, block: &Block) -> Result<FeeRate, NetworkError> {
|
|
||||||
let mut fees = vec![];
|
|
||||||
for tx_hash in &block.transactions {
|
|
||||||
let tx =
|
|
||||||
self.rpc.get_transaction(*tx_hash).await.map_err(|_| NetworkError::ConnectionError)?;
|
|
||||||
// Only consider fees from RCT transactions, else the fee property read wouldn't be accurate
|
|
||||||
let fee = match &tx {
|
|
||||||
Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
fees.push(fee / u64::try_from(tx.weight()).unwrap());
|
|
||||||
}
|
|
||||||
fees.sort();
|
|
||||||
let fee = fees.get(fees.len() / 2).copied().unwrap_or(0);
|
|
||||||
|
|
||||||
// TODO: Set a sane minimum fee
|
|
||||||
const MINIMUM_FEE: u64 = 1_500_000;
|
|
||||||
Ok(FeeRate::new(fee.max(MINIMUM_FEE), 10000).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn test_view_pair() -> ViewPair {
|
|
||||||
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn test_scanner() -> Scanner {
|
|
||||||
Scanner::new(Self::test_view_pair())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn test_address() -> Address {
|
|
||||||
Address::new(Self::test_view_pair().legacy_address(MoneroNetwork::Mainnet)).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Network for Monero {
|
|
||||||
const NETWORK: NetworkId = NetworkId::Monero;
|
|
||||||
const ID: &'static str = "Monero";
|
|
||||||
const ESTIMATED_BLOCK_TIME_IN_SECONDS: usize = 120;
|
|
||||||
const CONFIRMATIONS: usize = 10;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
const COST_TO_AGGREGATE: u64 = 0;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn external_address(&self, key: EdwardsPoint) -> Address {
|
|
||||||
Self::address_internal(key, EXTERNAL_SUBADDRESS)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn branch_address(key: EdwardsPoint) -> Option<Address> {
|
|
||||||
Some(Self::address_internal(key, BRANCH_SUBADDRESS))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_address(key: EdwardsPoint) -> Option<Address> {
|
|
||||||
Some(Self::address_internal(key, CHANGE_SUBADDRESS))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forward_address(key: EdwardsPoint) -> Option<Address> {
|
|
||||||
Some(Self::address_internal(key, FORWARD_SUBADDRESS))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn needed_fee(
|
|
||||||
&self,
|
|
||||||
block_number: usize,
|
|
||||||
inputs: &[Output],
|
|
||||||
payments: &[Payment<Self>],
|
|
||||||
change: &Option<Address>,
|
|
||||||
) -> Result<Option<u64>, NetworkError> {
|
|
||||||
let res = self
|
|
||||||
.make_signable_transaction(block_number, &[0; 32], inputs, payments, change, true)
|
|
||||||
.await?;
|
|
||||||
let Some(res) = res else { return Ok(None) };
|
|
||||||
let MakeSignableTransactionResult::Fee(fee) = res else {
|
|
||||||
panic!("told make_signable_transaction calculating_fee and got transaction")
|
|
||||||
};
|
|
||||||
Ok(Some(fee))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn signable_transaction(
|
|
||||||
&self,
|
|
||||||
block_number: usize,
|
|
||||||
plan_id: &[u8; 32],
|
|
||||||
_key: EdwardsPoint,
|
|
||||||
inputs: &[Output],
|
|
||||||
payments: &[Payment<Self>],
|
|
||||||
change: &Option<Address>,
|
|
||||||
(): &(),
|
|
||||||
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError> {
|
|
||||||
let res = self
|
|
||||||
.make_signable_transaction(block_number, plan_id, inputs, payments, change, false)
|
|
||||||
.await?;
|
|
||||||
let Some(res) = res else { return Ok(None) };
|
|
||||||
let MakeSignableTransactionResult::SignableTransaction(signable) = res else {
|
|
||||||
panic!("told make_signable_transaction not calculating_fee and got fee")
|
|
||||||
};
|
|
||||||
|
|
||||||
let signable = SignableTransaction(signable);
|
|
||||||
let eventuality = signable.0.clone().into();
|
|
||||||
Ok(Some((signable, eventuality)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn attempt_sign(
|
|
||||||
&self,
|
|
||||||
keys: ThresholdKeys<Self::Curve>,
|
|
||||||
transaction: SignableTransaction,
|
|
||||||
) -> Result<Self::TransactionMachine, NetworkError> {
|
|
||||||
match transaction.0.clone().multisig(keys) {
|
|
||||||
Ok(machine) => Ok(machine),
|
|
||||||
Err(e) => panic!("failed to create a multisig machine for TX: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn publish_completion(&self, tx: &Transaction) -> Result<(), NetworkError> {
|
|
||||||
match self.rpc.publish_transaction(tx).await {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
log::debug!("Monero ConnectionError: {e}");
|
|
||||||
Err(NetworkError::ConnectionError)?
|
|
||||||
}
|
|
||||||
// TODO: Distinguish already in pool vs double spend (other signing attempt succeeded) vs
|
|
||||||
// invalid transaction
|
|
||||||
Err(e) => panic!("failed to publish TX {}: {e}", hex::encode(tx.hash())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn get_block_number(&self, id: &[u8; 32]) -> usize {
|
|
||||||
self.rpc.get_block(*id).await.unwrap().number().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn check_eventuality_by_claim(
|
|
||||||
&self,
|
|
||||||
eventuality: &Self::Eventuality,
|
|
||||||
claim: &[u8; 32],
|
|
||||||
) -> bool {
|
|
||||||
return eventuality.matches(&self.rpc.get_pruned_transaction(*claim).await.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn get_transaction_by_eventuality(
|
|
||||||
&self,
|
|
||||||
block: usize,
|
|
||||||
eventuality: &Eventuality,
|
|
||||||
) -> Transaction {
|
|
||||||
let block = self.rpc.get_block_by_number(block).await.unwrap();
|
|
||||||
for tx in &block.transactions {
|
|
||||||
let tx = self.rpc.get_transaction(*tx).await.unwrap();
|
|
||||||
if eventuality.matches(&tx.clone().into()) {
|
|
||||||
return tx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic!("block didn't have a transaction for this eventuality")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn mine_block(&self) {
|
|
||||||
// https://github.com/serai-dex/serai/issues/198
|
|
||||||
sleep(std::time::Duration::from_millis(100)).await;
|
|
||||||
self.rpc.generate_blocks(&Self::test_address().into(), 1).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn test_send(&self, address: Address) -> Block {
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, OsRng};
|
|
||||||
use monero_wallet::rpc::FeePriority;
|
|
||||||
|
|
||||||
let new_block = self.get_latest_block_number().await.unwrap() + 1;
|
|
||||||
for _ in 0 .. 80 {
|
|
||||||
self.mine_block().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_block = self.rpc.get_block_by_number(new_block).await.unwrap();
|
|
||||||
let mut outputs = Self::test_scanner()
|
|
||||||
.scan(self.rpc.get_scannable_block(new_block.clone()).await.unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.ignore_additional_timelock();
|
|
||||||
let output = outputs.swap_remove(0);
|
|
||||||
|
|
||||||
let amount = output.commitment().amount;
|
|
||||||
// The dust should always be sufficient for the fee
|
|
||||||
let fee = Monero::DUST;
|
|
||||||
|
|
||||||
let rct_type = match new_block.header.hardfork_version {
|
|
||||||
14 => RctType::ClsagBulletproof,
|
|
||||||
15 | 16 => RctType::ClsagBulletproofPlus,
|
|
||||||
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = OutputWithDecoys::fingerprintable_deterministic_new(
|
|
||||||
&mut OsRng,
|
|
||||||
&self.rpc,
|
|
||||||
match rct_type {
|
|
||||||
RctType::ClsagBulletproof => 11,
|
|
||||||
RctType::ClsagBulletproofPlus => 16,
|
|
||||||
_ => panic!("selecting decoys for an unsupported RctType"),
|
|
||||||
},
|
|
||||||
self.rpc.get_height().await.unwrap(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut outgoing_view_key = Zeroizing::new([0; 32]);
|
|
||||||
OsRng.fill_bytes(outgoing_view_key.as_mut());
|
|
||||||
let tx = MSignableTransaction::new(
|
|
||||||
rct_type,
|
|
||||||
outgoing_view_key,
|
|
||||||
vec![output],
|
|
||||||
vec![(address.into(), amount - fee)],
|
|
||||||
Change::fingerprintable(Some(Self::test_address().into())),
|
|
||||||
vec![],
|
|
||||||
self.rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.sign(&mut OsRng, &Zeroizing::new(Scalar::ONE.0))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let block = self.get_latest_block_number().await.unwrap() + 1;
|
|
||||||
self.rpc.publish_transaction(&tx).await.unwrap();
|
|
||||||
for _ in 0 .. 10 {
|
|
||||||
self.mine_block().await;
|
|
||||||
}
|
|
||||||
self.get_block(block).await.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -41,3 +41,149 @@ async fn main() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[async_trait]
|
||||||
|
impl TransactionTrait<Monero> for Transaction {
|
||||||
|
#[cfg(test)]
|
||||||
|
async fn fee(&self, _: &Monero) -> u64 {
|
||||||
|
match self {
|
||||||
|
Transaction::V1 { .. } => panic!("v1 TX in test-only function"),
|
||||||
|
Transaction::V2 { ref proofs, .. } => proofs.as_ref().unwrap().base.fee,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Monero {
|
||||||
|
async fn median_fee(&self, block: &Block) -> Result<FeeRate, NetworkError> {
|
||||||
|
let mut fees = vec![];
|
||||||
|
for tx_hash in &block.transactions {
|
||||||
|
let tx =
|
||||||
|
self.rpc.get_transaction(*tx_hash).await.map_err(|_| NetworkError::ConnectionError)?;
|
||||||
|
// Only consider fees from RCT transactions, else the fee property read wouldn't be accurate
|
||||||
|
let fee = match &tx {
|
||||||
|
Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
fees.push(fee / u64::try_from(tx.weight()).unwrap());
|
||||||
|
}
|
||||||
|
fees.sort();
|
||||||
|
let fee = fees.get(fees.len() / 2).copied().unwrap_or(0);
|
||||||
|
|
||||||
|
// TODO: Set a sane minimum fee
|
||||||
|
const MINIMUM_FEE: u64 = 1_500_000;
|
||||||
|
Ok(FeeRate::new(fee.max(MINIMUM_FEE), 10000).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn test_view_pair() -> ViewPair {
|
||||||
|
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn test_scanner() -> Scanner {
|
||||||
|
Scanner::new(Self::test_view_pair())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn test_address() -> Address {
|
||||||
|
Address::new(Self::test_view_pair().legacy_address(MoneroNetwork::Mainnet)).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Network for Monero {
|
||||||
|
#[cfg(test)]
|
||||||
|
async fn get_block_number(&self, id: &[u8; 32]) -> usize {
|
||||||
|
self.rpc.get_block(*id).await.unwrap().number().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
async fn get_transaction_by_eventuality(
|
||||||
|
&self,
|
||||||
|
block: usize,
|
||||||
|
eventuality: &Eventuality,
|
||||||
|
) -> Transaction {
|
||||||
|
let block = self.rpc.get_block_by_number(block).await.unwrap();
|
||||||
|
for tx in &block.transactions {
|
||||||
|
let tx = self.rpc.get_transaction(*tx).await.unwrap();
|
||||||
|
if eventuality.matches(&tx.clone().into()) {
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("block didn't have a transaction for this eventuality")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
async fn mine_block(&self) {
|
||||||
|
// https://github.com/serai-dex/serai/issues/198
|
||||||
|
sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
self.rpc.generate_blocks(&Self::test_address().into(), 1).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
async fn test_send(&self, address: Address) -> Block {
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use rand_core::{RngCore, OsRng};
|
||||||
|
use monero_wallet::rpc::FeePriority;
|
||||||
|
|
||||||
|
let new_block = self.get_latest_block_number().await.unwrap() + 1;
|
||||||
|
for _ in 0 .. 80 {
|
||||||
|
self.mine_block().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_block = self.rpc.get_block_by_number(new_block).await.unwrap();
|
||||||
|
let mut outputs = Self::test_scanner()
|
||||||
|
.scan(self.rpc.get_scannable_block(new_block.clone()).await.unwrap())
|
||||||
|
.unwrap()
|
||||||
|
.ignore_additional_timelock();
|
||||||
|
let output = outputs.swap_remove(0);
|
||||||
|
|
||||||
|
let amount = output.commitment().amount;
|
||||||
|
// The dust should always be sufficient for the fee
|
||||||
|
let fee = Monero::DUST;
|
||||||
|
|
||||||
|
let rct_type = match new_block.header.hardfork_version {
|
||||||
|
14 => RctType::ClsagBulletproof,
|
||||||
|
15 | 16 => RctType::ClsagBulletproofPlus,
|
||||||
|
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = OutputWithDecoys::fingerprintable_deterministic_new(
|
||||||
|
&mut OsRng,
|
||||||
|
&self.rpc,
|
||||||
|
match rct_type {
|
||||||
|
RctType::ClsagBulletproof => 11,
|
||||||
|
RctType::ClsagBulletproofPlus => 16,
|
||||||
|
_ => panic!("selecting decoys for an unsupported RctType"),
|
||||||
|
},
|
||||||
|
self.rpc.get_height().await.unwrap(),
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut outgoing_view_key = Zeroizing::new([0; 32]);
|
||||||
|
OsRng.fill_bytes(outgoing_view_key.as_mut());
|
||||||
|
let tx = MSignableTransaction::new(
|
||||||
|
rct_type,
|
||||||
|
outgoing_view_key,
|
||||||
|
vec![output],
|
||||||
|
vec![(address.into(), amount - fee)],
|
||||||
|
Change::fingerprintable(Some(Self::test_address().into())),
|
||||||
|
vec![],
|
||||||
|
self.rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.sign(&mut OsRng, &Zeroizing::new(Scalar::ONE.0))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let block = self.get_latest_block_number().await.unwrap() + 1;
|
||||||
|
self.rpc.publish_transaction(&tx).await.unwrap();
|
||||||
|
for _ in 0 .. 10 {
|
||||||
|
self.mine_block().await;
|
||||||
|
}
|
||||||
|
self.get_block(block).await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -34,13 +34,6 @@ impl AsMut<[u8]> for OutputId {
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub(crate) struct Output(pub(crate) WalletOutput);
|
pub(crate) struct Output(pub(crate) WalletOutput);
|
||||||
|
|
||||||
impl Output {
|
|
||||||
pub(crate) fn new(output: WalletOutput) -> Self {
|
|
||||||
Self(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReceivedOutput<<Ed25519 as Ciphersuite>::G, Address> for Output {
|
impl ReceivedOutput<<Ed25519 as Ciphersuite>::G, Address> for Output {
|
||||||
type Id = OutputId;
|
type Id = OutputId;
|
||||||
type TransactionId = [u8; 32];
|
type TransactionId = [u8; 32];
|
||||||
|
|
|
@ -34,8 +34,8 @@ impl scheduler::Transaction for Transaction {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct SignableTransaction {
|
pub(crate) struct SignableTransaction {
|
||||||
id: [u8; 32],
|
pub(crate) id: [u8; 32],
|
||||||
signable: MSignableTransaction,
|
pub(crate) signable: MSignableTransaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -81,8 +81,8 @@ impl scheduler::SignableTransaction for SignableTransaction {
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub(crate) struct Eventuality {
|
pub(crate) struct Eventuality {
|
||||||
id: [u8; 32],
|
pub(crate) id: [u8; 32],
|
||||||
singular_spent_output: Option<OutputId>,
|
pub(crate) singular_spent_output: Option<OutputId>,
|
||||||
pub(crate) eventuality: MEventuality,
|
pub(crate) eventuality: MEventuality,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,146 +1,9 @@
|
||||||
/*
|
|
||||||
async fn make_signable_transaction(
|
|
||||||
block_number: usize,
|
|
||||||
plan_id: &[u8; 32],
|
|
||||||
inputs: &[Output],
|
|
||||||
payments: &[Payment<Self>],
|
|
||||||
change: &Option<Address>,
|
|
||||||
calculating_fee: bool,
|
|
||||||
) -> Result<Option<MakeSignableTransactionResult>, NetworkError> {
|
|
||||||
for payment in payments {
|
|
||||||
assert_eq!(payment.balance.coin, Coin::Monero);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO2: Use an fee representative of several blocks, cached inside Self
|
|
||||||
let block_for_fee = self.get_block(block_number).await?;
|
|
||||||
let fee_rate = self.median_fee(&block_for_fee).await?;
|
|
||||||
|
|
||||||
// Determine the RCT proofs to make based off the hard fork
|
|
||||||
// TODO: Make a fn for this block which is duplicated with tests
|
|
||||||
let rct_type = match block_for_fee.header.hardfork_version {
|
|
||||||
14 => RctType::ClsagBulletproof,
|
|
||||||
15 | 16 => RctType::ClsagBulletproofPlus,
|
|
||||||
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut transcript =
|
|
||||||
RecommendedTranscript::new(b"Serai Processor Monero Transaction Transcript");
|
|
||||||
transcript.append_message(b"plan", plan_id);
|
|
||||||
|
|
||||||
// All signers need to select the same decoys
|
|
||||||
// All signers use the same height and a seeded RNG to make sure they do so.
|
|
||||||
let mut inputs_actual = Vec::with_capacity(inputs.len());
|
|
||||||
for input in inputs {
|
|
||||||
inputs_actual.push(
|
|
||||||
OutputWithDecoys::fingerprintable_deterministic_new(
|
|
||||||
&mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")),
|
|
||||||
&self.rpc,
|
|
||||||
// TODO: Have Decoys take RctType
|
|
||||||
match rct_type {
|
|
||||||
RctType::ClsagBulletproof => 11,
|
|
||||||
RctType::ClsagBulletproofPlus => 16,
|
|
||||||
_ => panic!("selecting decoys for an unsupported RctType"),
|
|
||||||
},
|
|
||||||
block_number + 1,
|
|
||||||
input.0.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(map_rpc_err)?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monero requires at least two outputs
|
|
||||||
// If we only have one output planned, add a dummy payment
|
|
||||||
let mut payments = payments.to_vec();
|
|
||||||
let outputs = payments.len() + usize::from(u8::from(change.is_some()));
|
|
||||||
if outputs == 0 {
|
|
||||||
return Ok(None);
|
|
||||||
} else if outputs == 1 {
|
|
||||||
payments.push(Payment {
|
|
||||||
address: Address::new(
|
|
||||||
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
|
|
||||||
.unwrap()
|
|
||||||
.legacy_address(MoneroNetwork::Mainnet),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
balance: Balance { coin: Coin::Monero, amount: Amount(0) },
|
|
||||||
data: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let payments = payments
|
|
||||||
.into_iter()
|
|
||||||
.map(|payment| (payment.address.into(), payment.balance.amount.0))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
match MSignableTransaction::new(
|
|
||||||
rct_type,
|
|
||||||
// Use the plan ID as the outgoing view key
|
|
||||||
Zeroizing::new(*plan_id),
|
|
||||||
inputs_actual,
|
|
||||||
payments,
|
|
||||||
Change::fingerprintable(change.as_ref().map(|change| change.clone().into())),
|
|
||||||
vec![],
|
|
||||||
fee_rate,
|
|
||||||
) {
|
|
||||||
Ok(signable) => Ok(Some({
|
|
||||||
if calculating_fee {
|
|
||||||
MakeSignableTransactionResult::Fee(signable.necessary_fee())
|
|
||||||
} else {
|
|
||||||
MakeSignableTransactionResult::SignableTransaction(signable)
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
Err(e) => match e {
|
|
||||||
SendError::UnsupportedRctType => {
|
|
||||||
panic!("trying to use an RctType unsupported by monero-wallet")
|
|
||||||
}
|
|
||||||
SendError::NoInputs |
|
|
||||||
SendError::InvalidDecoyQuantity |
|
|
||||||
SendError::NoOutputs |
|
|
||||||
SendError::TooManyOutputs |
|
|
||||||
SendError::NoChange |
|
|
||||||
SendError::TooMuchArbitraryData |
|
|
||||||
SendError::TooLargeTransaction |
|
|
||||||
SendError::WrongPrivateKey => {
|
|
||||||
panic!("created an invalid Monero transaction: {e}");
|
|
||||||
}
|
|
||||||
SendError::MultiplePaymentIds => {
|
|
||||||
panic!("multiple payment IDs despite not supporting integrated addresses");
|
|
||||||
}
|
|
||||||
SendError::NotEnoughFunds { inputs, outputs, necessary_fee } => {
|
|
||||||
log::debug!(
|
|
||||||
"Monero NotEnoughFunds. inputs: {:?}, outputs: {:?}, necessary_fee: {necessary_fee:?}",
|
|
||||||
inputs,
|
|
||||||
outputs
|
|
||||||
);
|
|
||||||
match necessary_fee {
|
|
||||||
Some(necessary_fee) => {
|
|
||||||
// If we're solely calculating the fee, return the fee this TX will cost
|
|
||||||
if calculating_fee {
|
|
||||||
Ok(Some(MakeSignableTransactionResult::Fee(necessary_fee)))
|
|
||||||
} else {
|
|
||||||
// If we're actually trying to make the TX, return None
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We didn't have enough funds to even cover the outputs
|
|
||||||
None => {
|
|
||||||
// Ensure we're not misinterpreting this
|
|
||||||
assert!(outputs > inputs);
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SendError::MaliciousSerialization | SendError::ClsagError(_) | SendError::FrostError(_) => {
|
|
||||||
panic!("supposedly unreachable (at this time) Monero error: {e}");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
use core::future::Future;
|
use core::future::Future;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use rand_core::SeedableRng;
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
use ciphersuite::{Ciphersuite, Ed25519};
|
use ciphersuite::{Ciphersuite, Ed25519};
|
||||||
|
|
||||||
use monero_wallet::rpc::{FeeRate, RpcError};
|
use monero_wallet::rpc::{FeeRate, RpcError};
|
||||||
|
@ -154,11 +17,17 @@ use primitives::{OutputType, ReceivedOutput, Payment};
|
||||||
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
|
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
|
||||||
use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
|
use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
|
||||||
|
|
||||||
use monero_wallet::address::Network;
|
use monero_wallet::{
|
||||||
|
ringct::RctType,
|
||||||
|
address::{Network, AddressType, MoneroAddress},
|
||||||
|
OutputWithDecoys,
|
||||||
|
send::{
|
||||||
|
Change, SendError, SignableTransaction as MSignableTransaction, Eventuality as MEventuality,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, view_pair,
|
EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, view_pair,
|
||||||
output::Output,
|
|
||||||
transaction::{SignableTransaction, Eventuality},
|
transaction::{SignableTransaction, Eventuality},
|
||||||
rpc::Rpc,
|
rpc::Rpc,
|
||||||
};
|
};
|
||||||
|
@ -179,13 +48,108 @@ fn address_from_serai_key(key: <Ed25519 as Ciphersuite>::G, kind: OutputType) ->
|
||||||
.expect("created address which wasn't representable")
|
.expect("created address which wasn't representable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn signable_transaction(
|
||||||
|
rpc: &Rpc,
|
||||||
|
reference_block: &BlockFor<Rpc>,
|
||||||
|
inputs: Vec<OutputFor<Rpc>>,
|
||||||
|
payments: Vec<Payment<AddressFor<Rpc>>>,
|
||||||
|
change: Option<KeyFor<Rpc>>,
|
||||||
|
) -> Result<Result<(SignableTransaction, MSignableTransaction), SendError>, RpcError> {
|
||||||
|
assert!(inputs.len() < <Planner as TransactionPlanner<Rpc, ()>>::MAX_INPUTS);
|
||||||
|
assert!(
|
||||||
|
(payments.len() + usize::from(u8::from(change.is_some()))) <
|
||||||
|
<Planner as TransactionPlanner<Rpc, ()>>::MAX_OUTPUTS
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Set a sane minimum fee
|
||||||
|
const MINIMUM_FEE: u64 = 1_500_000;
|
||||||
|
// TODO: Set a fee rate based on the reference block
|
||||||
|
let fee_rate = FeeRate::new(MINIMUM_FEE, 10000).unwrap();
|
||||||
|
|
||||||
|
// Determine the RCT proofs to make based off the hard fork
|
||||||
|
let rct_type = match reference_block.0.block.header.hardfork_version {
|
||||||
|
14 => RctType::ClsagBulletproof,
|
||||||
|
15 | 16 => RctType::ClsagBulletproofPlus,
|
||||||
|
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// We need a unique ID to distinguish this transaction from another transaction with an identical
|
||||||
|
// set of payments (as our Eventualities only match over the payments). The output's ID is
|
||||||
|
// guaranteed to be unique, making it satisfactory
|
||||||
|
let id = inputs.first().unwrap().id().0;
|
||||||
|
|
||||||
|
let mut inputs_actual = Vec::with_capacity(inputs.len());
|
||||||
|
for input in inputs {
|
||||||
|
inputs_actual.push(
|
||||||
|
OutputWithDecoys::fingerprintable_deterministic_new(
|
||||||
|
// We need a deterministic RNG here with *some* seed
|
||||||
|
// The unique ID means we don't pick some static seed
|
||||||
|
// It is a public value, yet that's fine as this is assumed fully transparent
|
||||||
|
// It is a reused value (with later code), but that's not an issue. Just an oddity
|
||||||
|
&mut ChaCha20Rng::from_seed(id),
|
||||||
|
&rpc.rpc,
|
||||||
|
// TODO: Have Decoys take RctType
|
||||||
|
match rct_type {
|
||||||
|
RctType::ClsagBulletproof => 11,
|
||||||
|
RctType::ClsagBulletproofPlus => 16,
|
||||||
|
_ => panic!("selecting decoys for an unsupported RctType"),
|
||||||
|
},
|
||||||
|
reference_block.0.block.number().unwrap() + 1,
|
||||||
|
input.0.clone(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let inputs = inputs_actual;
|
||||||
|
|
||||||
|
let mut payments = payments
|
||||||
|
.into_iter()
|
||||||
|
.map(|payment| {
|
||||||
|
(MoneroAddress::from(*payment.address()), {
|
||||||
|
let balance = payment.balance();
|
||||||
|
assert_eq!(balance.coin, Coin::Monero);
|
||||||
|
balance.amount.0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if (payments.len() + usize::from(u8::from(change.is_some()))) == 1 {
|
||||||
|
// Monero requires at least two outputs, so add a dummy payment
|
||||||
|
payments.push((
|
||||||
|
MoneroAddress::new(
|
||||||
|
Network::Mainnet,
|
||||||
|
AddressType::Legacy,
|
||||||
|
<Ed25519 as Ciphersuite>::generator().0,
|
||||||
|
<Ed25519 as Ciphersuite>::generator().0,
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let change = if let Some(change) = change {
|
||||||
|
Change::guaranteed(view_pair(change), Some(CHANGE_SUBADDRESS))
|
||||||
|
} else {
|
||||||
|
Change::fingerprintable(None)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
MSignableTransaction::new(
|
||||||
|
rct_type,
|
||||||
|
Zeroizing::new(id),
|
||||||
|
inputs,
|
||||||
|
payments,
|
||||||
|
change,
|
||||||
|
vec![],
|
||||||
|
fee_rate,
|
||||||
|
)
|
||||||
|
.map(|signable| (SignableTransaction { id, signable: signable.clone() }, signable)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Planner(pub(crate) Rpc);
|
pub(crate) struct Planner(pub(crate) Rpc);
|
||||||
impl TransactionPlanner<Rpc, ()> for Planner {
|
impl TransactionPlanner<Rpc, ()> for Planner {
|
||||||
type EphemeralError = RpcError;
|
type EphemeralError = RpcError;
|
||||||
|
|
||||||
type FeeRate = FeeRate;
|
|
||||||
|
|
||||||
type SignableTransaction = SignableTransaction;
|
type SignableTransaction = SignableTransaction;
|
||||||
|
|
||||||
// wallet2 will not create a transaction larger than 100 KB, and Monero won't relay a transaction
|
// wallet2 will not create a transaction larger than 100 KB, and Monero won't relay a transaction
|
||||||
|
@ -195,12 +159,6 @@ impl TransactionPlanner<Rpc, ()> for Planner {
|
||||||
const MAX_INPUTS: usize = 120;
|
const MAX_INPUTS: usize = 120;
|
||||||
const MAX_OUTPUTS: usize = 16;
|
const MAX_OUTPUTS: usize = 16;
|
||||||
|
|
||||||
fn fee_rate(block: &BlockFor<Rpc>, coin: Coin) -> Self::FeeRate {
|
|
||||||
assert_eq!(coin, Coin::Monero);
|
|
||||||
// TODO
|
|
||||||
todo!("TODO")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn branch_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
fn branch_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
||||||
address_from_serai_key(key, OutputType::Branch)
|
address_from_serai_key(key, OutputType::Branch)
|
||||||
}
|
}
|
||||||
|
@ -212,218 +170,101 @@ impl TransactionPlanner<Rpc, ()> for Planner {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_fee(
|
fn calculate_fee(
|
||||||
fee_rate: Self::FeeRate,
|
&self,
|
||||||
|
reference_block: &BlockFor<Rpc>,
|
||||||
inputs: Vec<OutputFor<Rpc>>,
|
inputs: Vec<OutputFor<Rpc>>,
|
||||||
payments: Vec<Payment<AddressFor<Rpc>>>,
|
payments: Vec<Payment<AddressFor<Rpc>>>,
|
||||||
change: Option<KeyFor<Rpc>>,
|
change: Option<KeyFor<Rpc>>,
|
||||||
) -> Amount {
|
) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>> {
|
||||||
todo!("TODO")
|
async move {
|
||||||
|
Ok(match signable_transaction(&self.0, reference_block, inputs, payments, change).await? {
|
||||||
|
Ok(tx) => Amount(tx.1.necessary_fee()),
|
||||||
|
Err(SendError::NotEnoughFunds { necessary_fee, .. }) => {
|
||||||
|
Amount(necessary_fee.expect("outputs value exceeded inputs value"))
|
||||||
|
}
|
||||||
|
Err(SendError::UnsupportedRctType) => {
|
||||||
|
panic!("tried to use an RctType monero-wallet doesn't support")
|
||||||
|
}
|
||||||
|
Err(SendError::NoInputs | SendError::NoOutputs | SendError::TooManyOutputs) => {
|
||||||
|
panic!("malformed plan passed to calculate_fee")
|
||||||
|
}
|
||||||
|
Err(SendError::InvalidDecoyQuantity) => panic!("selected the wrong amount of decoys"),
|
||||||
|
Err(SendError::NoChange) => {
|
||||||
|
panic!("didn't add a dummy payment to satisfy the 2-output minimum")
|
||||||
|
}
|
||||||
|
Err(SendError::MultiplePaymentIds) => {
|
||||||
|
panic!("included multiple payment IDs despite not supporting addresses with payment IDs")
|
||||||
|
}
|
||||||
|
Err(SendError::TooMuchArbitraryData) => {
|
||||||
|
panic!("included too much arbitrary data despite not including any")
|
||||||
|
}
|
||||||
|
Err(SendError::TooLargeTransaction) => {
|
||||||
|
panic!("too large transaction despite MAX_INPUTS/MAX_OUTPUTS")
|
||||||
|
}
|
||||||
|
Err(
|
||||||
|
SendError::WrongPrivateKey |
|
||||||
|
SendError::MaliciousSerialization |
|
||||||
|
SendError::ClsagError(_) |
|
||||||
|
SendError::FrostError(_),
|
||||||
|
) => unreachable!("signing/serialization error when not signing/serializing"),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plan(
|
fn plan(
|
||||||
&self,
|
&self,
|
||||||
fee_rate: Self::FeeRate,
|
reference_block: &BlockFor<Rpc>,
|
||||||
inputs: Vec<OutputFor<Rpc>>,
|
inputs: Vec<OutputFor<Rpc>>,
|
||||||
payments: Vec<Payment<AddressFor<Rpc>>>,
|
payments: Vec<Payment<AddressFor<Rpc>>>,
|
||||||
change: Option<KeyFor<Rpc>>,
|
change: Option<KeyFor<Rpc>>,
|
||||||
) -> impl Send
|
) -> impl Send
|
||||||
+ Future<Output = Result<PlannedTransaction<Rpc, Self::SignableTransaction, ()>, RpcError>>
|
+ Future<Output = Result<PlannedTransaction<Rpc, Self::SignableTransaction, ()>, RpcError>>
|
||||||
{
|
{
|
||||||
async move { todo!("TODO") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
|
|
||||||
|
|
||||||
/*
|
|
||||||
use ciphersuite::{Ciphersuite, Ed25519};
|
|
||||||
|
|
||||||
use bitcoin_serai::{
|
|
||||||
bitcoin::ScriptBuf,
|
|
||||||
wallet::{TransactionError, SignableTransaction as BSignableTransaction, p2tr_script_buf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serai_client::{
|
|
||||||
primitives::{Coin, Amount},
|
|
||||||
networks::bitcoin::Address,
|
|
||||||
};
|
|
||||||
|
|
||||||
use serai_db::Db;
|
|
||||||
use primitives::{OutputType, ReceivedOutput, Payment};
|
|
||||||
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
|
|
||||||
use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
scan::{offsets_for_key, scanner},
|
|
||||||
output::Output,
|
|
||||||
transaction::{SignableTransaction, Eventuality},
|
|
||||||
rpc::Rpc,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn address_from_serai_key(key: <Ed25519 as Ciphersuite>::G, kind: OutputType) -> Address {
|
|
||||||
let offset = <Ed25519 as Ciphersuite>::G::GENERATOR * offsets_for_key(key)[&kind];
|
|
||||||
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>(
|
|
||||||
fee_per_vbyte: u64,
|
|
||||||
inputs: Vec<OutputFor<Rpc>>,
|
|
||||||
payments: Vec<Payment<AddressFor<Rpc>>>,
|
|
||||||
change: Option<KeyFor<Rpc>>,
|
|
||||||
) -> Result<(SignableTransaction, BSignableTransaction), TransactionError> {
|
|
||||||
assert!(
|
|
||||||
inputs.len() <
|
|
||||||
<Planner as TransactionPlanner<Rpc, ()>>::MAX_INPUTS
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
(payments.len() + usize::from(u8::from(change.is_some()))) <
|
|
||||||
<Planner as TransactionPlanner<Rpc, ()>>::MAX_OUTPUTS
|
|
||||||
);
|
|
||||||
|
|
||||||
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut payments = payments
|
|
||||||
.into_iter()
|
|
||||||
.map(|payment| {
|
|
||||||
(payment.address().clone(), {
|
|
||||||
let balance = payment.balance();
|
|
||||||
assert_eq!(balance.coin, Coin::Monero);
|
|
||||||
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
|
|
||||||
Address::new(p2tr_script_buf(<Ed25519 as Ciphersuite>::G::GENERATOR).unwrap()).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, ()>>::change_address);
|
|
||||||
|
|
||||||
BSignableTransaction::new(
|
|
||||||
inputs.clone(),
|
|
||||||
&payments
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|(address, amount)| (ScriptBuf::from(address), amount))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
change.clone().map(ScriptBuf::from),
|
|
||||||
None,
|
|
||||||
fee_per_vbyte,
|
|
||||||
)
|
|
||||||
.map(|bst| (SignableTransaction { inputs, payments, change, fee_per_vbyte }, bst))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Planner;
|
|
||||||
impl TransactionPlanner<Rpc, ()> for Planner {
|
|
||||||
type EphemeralError = RpcError;
|
|
||||||
|
|
||||||
type FeeRate = u64;
|
|
||||||
|
|
||||||
type SignableTransaction = SignableTransaction;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Monero 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 fee_rate(block: &BlockFor<Rpc>, coin: Coin) -> Self::FeeRate {
|
|
||||||
assert_eq!(coin, Coin::Monero);
|
|
||||||
// TODO
|
|
||||||
1
|
|
||||||
}
|
|
||||||
|
|
||||||
fn branch_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
|
||||||
address_from_serai_key(key, OutputType::Branch)
|
|
||||||
}
|
|
||||||
fn change_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
|
||||||
address_from_serai_key(key, OutputType::Change)
|
|
||||||
}
|
|
||||||
fn forwarding_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
|
|
||||||
address_from_serai_key(key, OutputType::Forwarded)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_fee(
|
|
||||||
fee_rate: Self::FeeRate,
|
|
||||||
inputs: Vec<OutputFor<Rpc>>,
|
|
||||||
payments: Vec<Payment<AddressFor<Rpc>>>,
|
|
||||||
change: Option<KeyFor<Rpc>>,
|
|
||||||
) -> Amount {
|
|
||||||
match signable_transaction::<D>(fee_rate, 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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plan(
|
|
||||||
fee_rate: Self::FeeRate,
|
|
||||||
inputs: Vec<OutputFor<Rpc>>,
|
|
||||||
payments: Vec<Payment<AddressFor<Rpc>>>,
|
|
||||||
change: Option<KeyFor<Rpc>>,
|
|
||||||
) -> PlannedTransaction<Rpc, Self::SignableTransaction, ()> {
|
|
||||||
let key = inputs.first().unwrap().key();
|
|
||||||
for input in &inputs {
|
|
||||||
assert_eq!(key, input.key());
|
|
||||||
}
|
|
||||||
|
|
||||||
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
|
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
|
||||||
match signable_transaction::<D>(fee_rate, inputs.clone(), payments, change) {
|
|
||||||
Ok(tx) => PlannedTransaction {
|
async move {
|
||||||
|
Ok(match signable_transaction(&self.0, reference_block, inputs, payments, change).await? {
|
||||||
|
Ok(tx) => {
|
||||||
|
let id = tx.0.id;
|
||||||
|
PlannedTransaction {
|
||||||
signable: tx.0,
|
signable: tx.0,
|
||||||
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output },
|
eventuality: Eventuality {
|
||||||
auxilliary: (),
|
id,
|
||||||
|
singular_spent_output,
|
||||||
|
eventuality: MEventuality::from(tx.1),
|
||||||
},
|
},
|
||||||
Err(
|
auxilliary: (),
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(SendError::NotEnoughFunds { .. }) => panic!("failed to successfully amortize the fee"),
|
||||||
|
Err(SendError::UnsupportedRctType) => {
|
||||||
|
panic!("tried to use an RctType monero-wallet doesn't support")
|
||||||
|
}
|
||||||
|
Err(SendError::NoInputs | SendError::NoOutputs | SendError::TooManyOutputs) => {
|
||||||
|
panic!("malformed plan passed to calculate_fee")
|
||||||
|
}
|
||||||
|
Err(SendError::InvalidDecoyQuantity) => panic!("selected the wrong amount of decoys"),
|
||||||
|
Err(SendError::NoChange) => {
|
||||||
|
panic!("didn't add a dummy payment to satisfy the 2-output minimum")
|
||||||
|
}
|
||||||
|
Err(SendError::MultiplePaymentIds) => {
|
||||||
|
panic!("included multiple payment IDs despite not supporting addresses with payment IDs")
|
||||||
|
}
|
||||||
|
Err(SendError::TooMuchArbitraryData) => {
|
||||||
|
panic!("included too much arbitrary data despite not including any")
|
||||||
|
}
|
||||||
|
Err(SendError::TooLargeTransaction) => {
|
||||||
|
panic!("too large transaction despite MAX_INPUTS/MAX_OUTPUTS")
|
||||||
|
}
|
||||||
|
Err(
|
||||||
|
SendError::WrongPrivateKey |
|
||||||
|
SendError::MaliciousSerialization |
|
||||||
|
SendError::ClsagError(_) |
|
||||||
|
SendError::FrostError(_),
|
||||||
|
) => unreachable!("signing/serialization error when not signing/serializing"),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
|
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
|
||||||
*/
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
use core::{fmt::Debug, future::Future};
|
use core::{fmt::Debug, future::Future};
|
||||||
|
|
||||||
use serai_primitives::{Coin, Amount};
|
use serai_primitives::Amount;
|
||||||
|
|
||||||
use primitives::{ReceivedOutput, Payment};
|
use primitives::{ReceivedOutput, Payment};
|
||||||
use scanner::{ScannerFeed, KeyFor, AddressFor, OutputFor, EventualityFor, BlockFor};
|
use scanner::{ScannerFeed, KeyFor, AddressFor, OutputFor, EventualityFor, BlockFor};
|
||||||
|
@ -48,9 +48,6 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
|
||||||
/// resolve manual intervention/changing the arguments.
|
/// resolve manual intervention/changing the arguments.
|
||||||
type EphemeralError: Debug;
|
type EphemeralError: Debug;
|
||||||
|
|
||||||
/// The type representing a fee rate to use for transactions.
|
|
||||||
type FeeRate: Send + Clone + Copy;
|
|
||||||
|
|
||||||
/// The type representing a signable transaction.
|
/// The type representing a signable transaction.
|
||||||
type SignableTransaction: SignableTransaction;
|
type SignableTransaction: SignableTransaction;
|
||||||
|
|
||||||
|
@ -59,11 +56,6 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
|
||||||
/// The maximum amount of outputs allowed in a transaction, including the change output.
|
/// The maximum amount of outputs allowed in a transaction, including the change output.
|
||||||
const MAX_OUTPUTS: usize;
|
const MAX_OUTPUTS: usize;
|
||||||
|
|
||||||
/// Obtain the fee rate to pay.
|
|
||||||
///
|
|
||||||
/// This must be constant to the block and coin.
|
|
||||||
fn fee_rate(block: &BlockFor<S>, coin: Coin) -> Self::FeeRate;
|
|
||||||
|
|
||||||
/// The branch address for this key of Serai's.
|
/// The branch address for this key of Serai's.
|
||||||
fn branch_address(key: KeyFor<S>) -> AddressFor<S>;
|
fn branch_address(key: KeyFor<S>) -> AddressFor<S>;
|
||||||
/// The change address for this key of Serai's.
|
/// The change address for this key of Serai's.
|
||||||
|
@ -76,11 +68,12 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
|
||||||
/// The fee rate, inputs, and payments, will all be for the same coin. The returned fee is
|
/// The fee rate, inputs, and payments, will all be for the same coin. The returned fee is
|
||||||
/// denominated in this coin.
|
/// denominated in this coin.
|
||||||
fn calculate_fee(
|
fn calculate_fee(
|
||||||
fee_rate: Self::FeeRate,
|
&self,
|
||||||
|
reference_block: &BlockFor<S>,
|
||||||
inputs: Vec<OutputFor<S>>,
|
inputs: Vec<OutputFor<S>>,
|
||||||
payments: Vec<Payment<AddressFor<S>>>,
|
payments: Vec<Payment<AddressFor<S>>>,
|
||||||
change: Option<KeyFor<S>>,
|
change: Option<KeyFor<S>>,
|
||||||
) -> Amount;
|
) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>>;
|
||||||
|
|
||||||
/// Plan a transaction.
|
/// Plan a transaction.
|
||||||
///
|
///
|
||||||
|
@ -91,7 +84,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
|
||||||
/// output must be created.
|
/// output must be created.
|
||||||
fn plan(
|
fn plan(
|
||||||
&self,
|
&self,
|
||||||
fee_rate: Self::FeeRate,
|
reference_block: &BlockFor<S>,
|
||||||
inputs: Vec<OutputFor<S>>,
|
inputs: Vec<OutputFor<S>>,
|
||||||
payments: Vec<Payment<AddressFor<S>>>,
|
payments: Vec<Payment<AddressFor<S>>>,
|
||||||
change: Option<KeyFor<S>>,
|
change: Option<KeyFor<S>>,
|
||||||
|
@ -112,7 +105,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
|
||||||
fn plan_transaction_with_fee_amortization(
|
fn plan_transaction_with_fee_amortization(
|
||||||
&self,
|
&self,
|
||||||
operating_costs: &mut u64,
|
operating_costs: &mut u64,
|
||||||
fee_rate: Self::FeeRate,
|
reference_block: &BlockFor<S>,
|
||||||
inputs: Vec<OutputFor<S>>,
|
inputs: Vec<OutputFor<S>>,
|
||||||
mut payments: Vec<Payment<AddressFor<S>>>,
|
mut payments: Vec<Payment<AddressFor<S>>>,
|
||||||
mut change: Option<KeyFor<S>>,
|
mut change: Option<KeyFor<S>>,
|
||||||
|
@ -156,7 +149,8 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
|
||||||
// Sort payments from high amount to low amount
|
// Sort payments from high amount to low amount
|
||||||
payments.sort_by(|a, b| a.balance().amount.0.cmp(&b.balance().amount.0).reverse());
|
payments.sort_by(|a, b| a.balance().amount.0.cmp(&b.balance().amount.0).reverse());
|
||||||
|
|
||||||
let mut fee = Self::calculate_fee(fee_rate, inputs.clone(), payments.clone(), change).0;
|
let mut fee =
|
||||||
|
self.calculate_fee(reference_block, inputs.clone(), payments.clone(), change).await?.0;
|
||||||
let mut amortized = 0;
|
let mut amortized = 0;
|
||||||
while !payments.is_empty() {
|
while !payments.is_empty() {
|
||||||
// We need to pay the fee, and any accrued operating costs, minus what we've already
|
// We need to pay the fee, and any accrued operating costs, minus what we've already
|
||||||
|
@ -176,7 +170,10 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
|
||||||
if payments.last().unwrap().balance().amount.0 <= (per_payment_fee + S::dust(coin).0) {
|
if payments.last().unwrap().balance().amount.0 <= (per_payment_fee + S::dust(coin).0) {
|
||||||
amortized += payments.pop().unwrap().balance().amount.0;
|
amortized += payments.pop().unwrap().balance().amount.0;
|
||||||
// Recalculate the fee and try again
|
// Recalculate the fee and try again
|
||||||
fee = Self::calculate_fee(fee_rate, inputs.clone(), payments.clone(), change).0;
|
fee = self
|
||||||
|
.calculate_fee(reference_block, inputs.clone(), payments.clone(), change)
|
||||||
|
.await?
|
||||||
|
.0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Break since all of these payments shouldn't be dropped
|
// Break since all of these payments shouldn't be dropped
|
||||||
|
@ -237,7 +234,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
|
||||||
let has_change = change.is_some();
|
let has_change = change.is_some();
|
||||||
|
|
||||||
let PlannedTransaction { signable, eventuality, auxilliary } =
|
let PlannedTransaction { signable, eventuality, auxilliary } =
|
||||||
self.plan(fee_rate, inputs, payments, change).await?;
|
self.plan(reference_block, inputs, payments, change).await?;
|
||||||
Ok(Some(AmortizePlannedTransaction {
|
Ok(Some(AmortizePlannedTransaction {
|
||||||
effected_payments,
|
effected_payments,
|
||||||
has_change,
|
has_change,
|
||||||
|
|
|
@ -56,7 +56,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
|
||||||
.planner
|
.planner
|
||||||
.plan_transaction_with_fee_amortization(
|
.plan_transaction_with_fee_amortization(
|
||||||
&mut operating_costs,
|
&mut operating_costs,
|
||||||
P::fee_rate(block, coin),
|
block,
|
||||||
to_aggregate,
|
to_aggregate,
|
||||||
vec![],
|
vec![],
|
||||||
Some(key_for_change),
|
Some(key_for_change),
|
||||||
|
@ -176,7 +176,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
|
||||||
.plan_transaction_with_fee_amortization(
|
.plan_transaction_with_fee_amortization(
|
||||||
// Uses 0 as there's no operating costs to incur/amortize here
|
// Uses 0 as there's no operating costs to incur/amortize here
|
||||||
&mut 0,
|
&mut 0,
|
||||||
P::fee_rate(block, coin),
|
block,
|
||||||
vec![output],
|
vec![output],
|
||||||
payments,
|
payments,
|
||||||
None,
|
None,
|
||||||
|
@ -254,7 +254,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
|
||||||
.planner
|
.planner
|
||||||
.plan_transaction_with_fee_amortization(
|
.plan_transaction_with_fee_amortization(
|
||||||
&mut operating_costs,
|
&mut operating_costs,
|
||||||
P::fee_rate(block, coin),
|
block,
|
||||||
outputs.clone(),
|
outputs.clone(),
|
||||||
tree[0]
|
tree[0]
|
||||||
.payments::<S>(coin, &branch_address, tree[0].value())
|
.payments::<S>(coin, &branch_address, tree[0].value())
|
||||||
|
@ -327,7 +327,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
|
||||||
.planner
|
.planner
|
||||||
.plan_transaction_with_fee_amortization(
|
.plan_transaction_with_fee_amortization(
|
||||||
&mut operating_costs,
|
&mut operating_costs,
|
||||||
P::fee_rate(block, coin),
|
block,
|
||||||
outputs,
|
outputs,
|
||||||
vec![],
|
vec![],
|
||||||
Some(to),
|
Some(to),
|
||||||
|
@ -487,7 +487,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> SchedulerTrait<S> for Schedul
|
||||||
// This uses 0 for the operating costs as we don't incur any here
|
// This uses 0 for the operating costs as we don't incur any here
|
||||||
// If the output can't pay for itself to be forwarded, we simply drop it
|
// If the output can't pay for itself to be forwarded, we simply drop it
|
||||||
&mut 0,
|
&mut 0,
|
||||||
P::fee_rate(block, forward.balance().coin),
|
block,
|
||||||
vec![forward.clone()],
|
vec![forward.clone()],
|
||||||
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)],
|
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)],
|
||||||
None,
|
None,
|
||||||
|
@ -508,7 +508,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> SchedulerTrait<S> for Schedul
|
||||||
// This uses 0 for the operating costs as we don't incur any here
|
// This uses 0 for the operating costs as we don't incur any here
|
||||||
// If the output can't pay for itself to be returned, we simply drop it
|
// If the output can't pay for itself to be returned, we simply drop it
|
||||||
&mut 0,
|
&mut 0,
|
||||||
P::fee_rate(block, out_instruction.balance().coin),
|
block,
|
||||||
vec![to_return.output().clone()],
|
vec![to_return.output().clone()],
|
||||||
vec![out_instruction],
|
vec![out_instruction],
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -86,7 +86,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||||
.planner
|
.planner
|
||||||
.plan_transaction_with_fee_amortization(
|
.plan_transaction_with_fee_amortization(
|
||||||
&mut operating_costs,
|
&mut operating_costs,
|
||||||
P::fee_rate(block, coin),
|
block,
|
||||||
to_aggregate,
|
to_aggregate,
|
||||||
vec![],
|
vec![],
|
||||||
Some(key_for_change),
|
Some(key_for_change),
|
||||||
|
@ -229,7 +229,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||||
.planner
|
.planner
|
||||||
.plan_transaction_with_fee_amortization(
|
.plan_transaction_with_fee_amortization(
|
||||||
&mut operating_costs,
|
&mut operating_costs,
|
||||||
P::fee_rate(block, coin),
|
block,
|
||||||
outputs.clone(),
|
outputs.clone(),
|
||||||
tree[0]
|
tree[0]
|
||||||
.payments::<S>(coin, &branch_address, tree[0].value())
|
.payments::<S>(coin, &branch_address, tree[0].value())
|
||||||
|
@ -323,7 +323,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||||
.plan_transaction_with_fee_amortization(
|
.plan_transaction_with_fee_amortization(
|
||||||
// Uses 0 as there's no operating costs to incur/amortize here
|
// Uses 0 as there's no operating costs to incur/amortize here
|
||||||
&mut 0,
|
&mut 0,
|
||||||
P::fee_rate(block, coin),
|
block,
|
||||||
vec![branch_output],
|
vec![branch_output],
|
||||||
payments,
|
payments,
|
||||||
None,
|
None,
|
||||||
|
@ -379,7 +379,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||||
.planner
|
.planner
|
||||||
.plan_transaction_with_fee_amortization(
|
.plan_transaction_with_fee_amortization(
|
||||||
&mut operating_costs,
|
&mut operating_costs,
|
||||||
P::fee_rate(block, coin),
|
block,
|
||||||
outputs,
|
outputs,
|
||||||
vec![],
|
vec![],
|
||||||
Some(to),
|
Some(to),
|
||||||
|
@ -505,7 +505,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||||
// This uses 0 for the operating costs as we don't incur any here
|
// This uses 0 for the operating costs as we don't incur any here
|
||||||
// If the output can't pay for itself to be forwarded, we simply drop it
|
// If the output can't pay for itself to be forwarded, we simply drop it
|
||||||
&mut 0,
|
&mut 0,
|
||||||
P::fee_rate(block, forward.balance().coin),
|
block,
|
||||||
vec![forward.clone()],
|
vec![forward.clone()],
|
||||||
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)],
|
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)],
|
||||||
None,
|
None,
|
||||||
|
@ -526,7 +526,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||||
// This uses 0 for the operating costs as we don't incur any here
|
// This uses 0 for the operating costs as we don't incur any here
|
||||||
// If the output can't pay for itself to be returned, we simply drop it
|
// If the output can't pay for itself to be returned, we simply drop it
|
||||||
&mut 0,
|
&mut 0,
|
||||||
P::fee_rate(block, out_instruction.balance().coin),
|
block,
|
||||||
vec![to_return.output().clone()],
|
vec![to_return.output().clone()],
|
||||||
vec![out_instruction],
|
vec![out_instruction],
|
||||||
None,
|
None,
|
||||||
|
|
Loading…
Reference in a new issue