diff --git a/rust-libs/duniter-dbs-read-ops/src/find_inputs.rs b/rust-libs/duniter-dbs-read-ops/src/find_inputs.rs index 8bdef91f8c4096f148659a21199e68157669a24f..2bb772865711cb01010a53c75efa06305939b0e3 100644 --- a/rust-libs/duniter-dbs-read-ops/src/find_inputs.rs +++ b/rust-libs/duniter-dbs-read-ops/src/find_inputs.rs @@ -22,9 +22,45 @@ pub fn find_inputs<BcDb: BcV2DbReadable, GvaDb: GvaV1DbReadable, TxsMpDb: TxsMpV txs_mp_db: &TxsMpDb, amount: SourceAmount, script: &WalletScriptV10, + use_mempool_sources: bool, ) -> KvResult<(BlockMetaV2, Vec<TransactionInputV10>, SourceAmount)> { if let Some(current_block) = crate::get_current_block_meta(bc_db)? { - let (mut inputs, uds_sum) = if script.nodes.is_empty() { + // Pending UTXOs + let (mut inputs, mut inputs_sum) = if use_mempool_sources { + txs_mp_db + .outputs_by_script() + .get_ref_slice(duniter_dbs::WalletConditionsV2::from_ref(script), |utxos| { + let mut sum = SourceAmount::ZERO; + let inputs = utxos + .iter() + .filter(|utxo| { + !txs_mp_db + .utxos_ids() + .contains_key(&UtxoIdDbV2(*utxo.tx_hash(), utxo.output_index())) + .unwrap_or(true) + }) + .copied() + .map(|utxo| { + let amount = *utxo.amount(); + sum = sum + amount; + TransactionInputV10 { + amount, + id: SourceIdV10::Utxo(UtxoIdV10 { + tx_hash: *utxo.tx_hash(), + output_index: utxo.output_index() as usize, + }), + } + }) + .collect(); + let sum = utxos.iter().map(|utxo| *utxo.amount()).sum(); + Ok((inputs, sum)) + })? + .unwrap_or((Vec::with_capacity(50), SourceAmount::ZERO)) + } else { + (Vec::with_capacity(50), SourceAmount::ZERO) + }; + // UDs + if script.nodes.is_empty() { if let WalletSubScriptV10::Single(WalletConditionV10::Sig(issuer)) = script.root { let pending_uds_bn = txs_mp_db.uds_ids().iter(.., |it| { it.keys() @@ -37,41 +73,33 @@ pub fn find_inputs<BcDb: BcV2DbReadable, GvaDb: GvaV1DbReadable, TxsMpDb: TxsMpV issuer, .., Some(&pending_uds_bn), - Some(amount), + Some(amount - inputs_sum), )?; - let inputs = uds - .into_iter() - .map(|(block_number, source_amount)| TransactionInputV10 { + inputs.extend(uds.into_iter().map(|(block_number, source_amount)| { + TransactionInputV10 { amount: source_amount, id: SourceIdV10::Ud(UdSourceIdV10 { issuer, block_number, }), - }) - .collect::<Vec<_>>(); - (inputs, uds_sum) - } else { - (vec![], SourceAmount::ZERO) + } + })); + inputs_sum = inputs_sum + uds_sum; } + } + if inputs_sum < amount { + // Written UTXOs + let (written_utxos, written_utxos_sum) = + crate::utxos::find_script_utxos(gva_db, txs_mp_db, amount - inputs_sum, &script)?; + inputs.extend(written_utxos.into_iter().map( + |(_written_time, utxo_id, source_amount)| TransactionInputV10 { + amount: source_amount, + id: SourceIdV10::Utxo(utxo_id), + }, + )); + Ok((current_block, inputs, inputs_sum + written_utxos_sum)) } else { - (vec![], SourceAmount::ZERO) - }; - if uds_sum < amount { - let (utxos, utxos_sum) = - crate::utxos::find_script_utxos(gva_db, txs_mp_db, amount - uds_sum, &script)?; - inputs.extend( - utxos - .into_iter() - .map( - |(_written_time, utxo_id, source_amount)| TransactionInputV10 { - amount: source_amount, - id: SourceIdV10::Utxo(utxo_id), - }, - ), - ); - Ok((current_block, inputs, uds_sum + utxos_sum)) - } else { - Ok((current_block, inputs, uds_sum)) + Ok((current_block, inputs, inputs_sum)) } } else { Err(KvError::Custom("no blockchain".into())) @@ -85,7 +113,7 @@ mod tests { use super::*; use duniter_dbs::{ bc_v2::BcV2DbWritable, gva_v1::GvaV1DbWritable, txs_mp_v2::TxsMpV2DbWritable, - SourceAmountValV2, UdIdV2, UtxoIdDbV2, UtxosOfScriptV1, WalletConditionsV2, + SourceAmountValV2, UdIdV2, UtxoIdDbV2, UtxoValV2, UtxosOfScriptV1, WalletConditionsV2, }; const UD0: i64 = 10; @@ -102,8 +130,14 @@ mod tests { }; let pk = PublicKey::default(); let script = WalletScriptV10::single(WalletConditionV10::Sig(pk)); - let mut utxos = BTreeMap::new(); - utxos.insert( + let mut pending_utxos = BTreeSet::new(); + pending_utxos.insert(UtxoValV2::new( + SourceAmount::with_base0(90), + Hash::default(), + 10, + )); + let mut written_utxos = BTreeMap::new(); + written_utxos.insert( 0, smallvec::smallvec![ ( @@ -130,9 +164,13 @@ mod tests { bc_db .uds_write() .upsert(UdIdV2(PublicKey::default(), BlockNumber(0)), ())?; - gva_db - .utxos_by_script_write() - .upsert(WalletConditionsV2(script.clone()), UtxosOfScriptV1(utxos))?; + gva_db.utxos_by_script_write().upsert( + WalletConditionsV2(script.clone()), + UtxosOfScriptV1(written_utxos), + )?; + txs_mp_db + .outputs_by_script_write() + .upsert(WalletConditionsV2(script.clone()), pending_utxos)?; // Gen tx1 let (cb, inputs, inputs_sum) = find_inputs( @@ -141,6 +179,7 @@ mod tests { &txs_mp_db, SourceAmount::with_base0(55), &script, + false, )?; assert_eq!(cb, b0); assert_eq!(inputs.len(), 2); @@ -161,11 +200,30 @@ mod tests { &txs_mp_db, SourceAmount::with_base0(55), &script, + false, )?; assert_eq!(cb, b0); assert_eq!(inputs.len(), 1); assert_eq!(inputs_sum, SourceAmount::with_base0(80)); + // Insert tx2 inputs in mempool + txs_mp_db + .utxos_ids_write() + .upsert(UtxoIdDbV2(Hash::default(), 1), ())?; + + // Gen tx3 (use pending utxo) + let (cb, inputs, inputs_sum) = find_inputs( + &bc_db, + &gva_db, + &txs_mp_db, + SourceAmount::with_base0(75), + &script, + true, + )?; + assert_eq!(cb, b0); + assert_eq!(inputs.len(), 1); + assert_eq!(inputs_sum, SourceAmount::with_base0(90)); + Ok(()) } } diff --git a/rust-libs/duniter-dbs-read-ops/src/lib.rs b/rust-libs/duniter-dbs-read-ops/src/lib.rs index 83db8c7d6b3891dae9d7c3572887edf625a9e75f..53fedd5978c97b6d63e850fe56d74ad57dbd719b 100644 --- a/rust-libs/duniter-dbs-read-ops/src/lib.rs +++ b/rust-libs/duniter-dbs-read-ops/src/lib.rs @@ -34,7 +34,7 @@ use dubp::{common::prelude::BlockNumber, wallet::prelude::SourceAmount}; use duniter_dbs::bc_v2::BcV2DbReadable; use duniter_dbs::{ kv_typed::prelude::*, BlockMetaV2, GvaV1DbReadable, HashKeyV2, PubKeyKeyV2, TxDbV2, - TxsMpV2DbReadable, + TxsMpV2DbReadable, UtxoIdDbV2, }; use resiter::map::Map; use std::{ diff --git a/rust-libs/duniter-dbs-write-ops/src/lib.rs b/rust-libs/duniter-dbs-write-ops/src/lib.rs index 15bc7130b095252c720442013a8e3f446d49d908..540fb68f0a9529c8e2ba61f8d21a97b8964307e6 100644 --- a/rust-libs/duniter-dbs-write-ops/src/lib.rs +++ b/rust-libs/duniter-dbs-write-ops/src/lib.rs @@ -41,7 +41,7 @@ use duniter_dbs::gva_v1::{TxsByIssuerEvent, TxsByRecipientEvent, TxsEvent}; use duniter_dbs::{ kv_typed::prelude::*, BlockMetaV2, DuniterDbs, GvaV1Db, GvaV1DbReadable, GvaV1DbWritable, HashKeyV2, PendingTxDbV2, PubKeyKeyV2, PubKeyValV2, SourceAmountValV2, TxDbV2, TxsMpV2Db, - TxsMpV2DbReadable, TxsMpV2DbWritable, WalletConditionsV2, + TxsMpV2DbReadable, TxsMpV2DbWritable, UtxoValV2, WalletConditionsV2, }; use resiter::filter_map::FilterMap; use resiter::flatten::Flatten; diff --git a/rust-libs/duniter-dbs-write-ops/src/txs_mp.rs b/rust-libs/duniter-dbs-write-ops/src/txs_mp.rs index 7cf0fdaaa19c616e6559fd0fda64c5b22272936b..c47a52dd2103c58a696087d5f48e523e03c4320f 100644 --- a/rust-libs/duniter-dbs-write-ops/src/txs_mp.rs +++ b/rust-libs/duniter-dbs-write-ops/src/txs_mp.rs @@ -57,6 +57,7 @@ pub fn add_pending_tx< txs_mp_db.txs_write(), txs_mp_db.uds_ids_write(), txs_mp_db.utxos_ids_write(), + txs_mp_db.outputs_by_script_write(), ) .write( |( @@ -66,6 +67,7 @@ pub fn add_pending_tx< mut txs, mut uds_ids, mut utxos_ids, + mut outputs_by_script, )| { control(&tx, &txs)?; // Insert on col `txs_by_recv_time` @@ -100,6 +102,14 @@ pub fn add_pending_tx< .upsert(duniter_dbs::UtxoIdDbV2(tx_hash, output_index as u32), ()), } } + // Insert tx outputs in col `outputs` + for (output_index, output) in tx.get_outputs().iter().enumerate() { + let script = WalletConditionsV2(output.conditions.script.to_owned()); + let utxo = UtxoValV2::new(output.amount, tx_hash, output_index as u32); + let mut script_outputs = outputs_by_script.get(&script)?.unwrap_or_default(); + script_outputs.insert(utxo); + outputs_by_script.upsert(script, script_outputs); + } // Insert tx itself txs.upsert(HashKeyV2(tx_hash), PendingTxDbV2(tx.into_owned())); Ok(()) @@ -155,9 +165,17 @@ fn remove_one_pending_tx<B: Backend>(txs_mp_db: &TxsMpV2Db<B>, tx_hash: Hash) -> txs_mp_db.txs_write(), txs_mp_db.uds_ids_write(), txs_mp_db.utxos_ids_write(), + txs_mp_db.outputs_by_script_write(), ) .write( - |(mut txs_by_issuer, mut txs_by_recipient, mut txs, mut uds_ids, mut utxos_ids)| { + |( + mut txs_by_issuer, + mut txs_by_recipient, + mut txs, + mut uds_ids, + mut utxos_ids, + mut outputs_by_script, + )| { // Remove tx inputs in cols `uds_ids` and `utxos_ids` for input in tx.0.get_inputs() { match input.id { @@ -187,6 +205,15 @@ fn remove_one_pending_tx<B: Backend>(txs_mp_db: &TxsMpV2Db<B>, tx_hash: Hash) -> hashs_.remove(&tx_hash); txs_by_recipient.upsert(PubKeyKeyV2(pubkey), hashs_) } + // Remove tx outputs in col `outputs` + for (output_index, output) in tx.0.get_outputs().iter().enumerate() { + let script = WalletConditionsV2(output.conditions.script.to_owned()); + let utxo = UtxoValV2::new(output.amount, tx_hash, output_index as u32); + let mut script_outputs = + outputs_by_script.get(&script)?.unwrap_or_default(); + script_outputs.remove(&utxo); + outputs_by_script.upsert(script, script_outputs); + } // Remove tx itself txs.remove(HashKeyV2(tx_hash)); Ok(true) diff --git a/rust-libs/duniter-dbs/src/lib.rs b/rust-libs/duniter-dbs/src/lib.rs index e6a5325e4300de5fbf8709ba439d53a6921bd679..f6f05f10e4a8ddf2ccb7582fb49b54b7ad782b82 100644 --- a/rust-libs/duniter-dbs/src/lib.rs +++ b/rust-libs/duniter-dbs/src/lib.rs @@ -85,6 +85,7 @@ pub use values::sindex_db::{SIndexDBV1, SourceKeyArrayDbV1}; pub use values::source_amount::SourceAmountValV2; pub use values::tx_db::{PendingTxDbV2, TxDbV2}; pub use values::ud_entry_db::{ConsumedUdDbV1, UdAmountDbV1, UdEntryDbV1}; +pub use values::utxo::UtxoValV2; pub use values::utxos_of_script::UtxosOfScriptV1; pub use values::wallet_db::WalletDbV1; pub use values::wallet_script_array::WalletScriptArrayV2; diff --git a/rust-libs/duniter-dbs/src/txs_mp_v2.rs b/rust-libs/duniter-dbs/src/txs_mp_v2.rs index 00574a255e60ea44372e09afd87b6b80d3bb29b0..e055328076de489711d120030a1f328f4e12d36e 100644 --- a/rust-libs/duniter-dbs/src/txs_mp_v2.rs +++ b/rust-libs/duniter-dbs/src/txs_mp_v2.rs @@ -24,5 +24,6 @@ db_schema!( ["txs_by_received_time", TxsByRecvTime, i64, BTreeSet<Hash>], ["uds_ids", UdsIds, UdIdV2, ()], ["utxos_ids", UtxosIds, UtxoIdDbV2, ()], + ["outputs_by_script", OutputsByScript, WalletConditionsV2, BTreeSet<UtxoValV2>], ] ); diff --git a/rust-libs/duniter-dbs/src/values.rs b/rust-libs/duniter-dbs/src/values.rs index 8f9369419b83d539d0117b4c00a78566046f04f6..8221a4b516cf75acf4f90351a0da13619de64bf8 100644 --- a/rust-libs/duniter-dbs/src/values.rs +++ b/rust-libs/duniter-dbs/src/values.rs @@ -28,6 +28,7 @@ pub mod sindex_db; pub mod source_amount; pub mod tx_db; pub mod ud_entry_db; +pub mod utxo; pub mod utxos_of_script; pub mod wallet_db; pub mod wallet_script_array; diff --git a/rust-libs/duniter-dbs/src/values/utxo.rs b/rust-libs/duniter-dbs/src/values/utxo.rs new file mode 100644 index 0000000000000000000000000000000000000000..f24ef57c2244bf06ff323e65a7b142f51448f2bb --- /dev/null +++ b/rust-libs/duniter-dbs/src/values/utxo.rs @@ -0,0 +1,126 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use std::ops::Deref; + +#[derive( + Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, zerocopy::AsBytes, zerocopy::FromBytes, +)] +#[repr(transparent)] +pub struct UtxoValV2([u8; 52]); // 16(SourceAmount) + 32(Hash) + 4(u32) +impl UtxoValV2 { + pub fn new(amount: SourceAmount, tx_hash: Hash, output_index: u32) -> Self { + let mut buffer = [0; 52]; + use zerocopy::AsBytes as _; + buffer[..16].copy_from_slice(amount.as_bytes()); + buffer[16..48].copy_from_slice(tx_hash.as_ref()); + buffer[48..].copy_from_slice(&output_index.to_le_bytes()[..]); + Self(buffer) + } + pub fn amount(&self) -> &SourceAmount { + let layout = + zerocopy::LayoutVerified::<_, SourceAmount>::new(&self.0[..16]).expect("dev error"); + + unsafe { std::mem::transmute(layout.deref()) } + } + pub fn tx_hash(&self) -> &Hash { + let layout = zerocopy::LayoutVerified::<_, Hash>::new(&self.0[16..48]).expect("dev error"); + + unsafe { std::mem::transmute(layout.deref()) } + } + pub fn output_index(&self) -> u32 { + zerocopy::LayoutVerified::<_, zerocopy::U32<byteorder::LittleEndian>>::new(&self.0[48..]) + .expect("dev error") + .get() + } +} + +impl Default for UtxoValV2 { + fn default() -> Self { + UtxoValV2([0u8; 52]) + } +} + +impl std::fmt::Display for UtxoValV2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let amount = self.amount(); + write!( + f, + "{}:{}:T:{}:{}", + amount.amount(), + amount.base(), + self.tx_hash(), + self.output_index() + ) + } +} + +impl FromStr for UtxoValV2 { + type Err = StringErr; + + fn from_str(_s: &str) -> std::result::Result<Self, Self::Err> { + unimplemented!() + } +} + +impl ValueAsBytes for UtxoValV2 { + fn as_bytes<T, F: FnMut(&[u8]) -> KvResult<T>>(&self, mut f: F) -> KvResult<T> { + f(self.0.as_ref()) + } +} + +impl kv_typed::prelude::FromBytes for UtxoValV2 { + type Err = StringErr; + + fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> { + let layout = zerocopy::LayoutVerified::<_, UtxoValV2>::new(bytes) + .ok_or_else(|| StringErr("corrupted db".to_owned()))?; + Ok(*layout) + } +} + +impl ToDumpString for UtxoValV2 { + fn to_dump_string(&self) -> String { + todo!() + } +} + +#[cfg(feature = "explorer")] +impl ExplorableValue for UtxoValV2 { + fn from_explorer_str(_: &str) -> std::result::Result<Self, StringErr> { + unimplemented!() + } + fn to_explorer_json(&self) -> KvResult<serde_json::Value> { + Ok(serde_json::Value::String(self.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn utxo_val_v2() { + let amount = SourceAmount::with_base0(42); + let tx_hash = Hash::default(); + let output_index = 3; + let utxo_val = UtxoValV2::new(amount, tx_hash, output_index); + + assert_eq!(utxo_val.amount(), &amount); + assert_eq!(utxo_val.tx_hash(), &tx_hash); + assert_eq!(utxo_val.output_index(), output_index); + } +} diff --git a/rust-libs/modules/duniter-gva/src/queries/gen_txs.rs b/rust-libs/modules/duniter-gva/src/queries/gen_txs.rs index 515aa170cf3451e3995730e013ac6aa0bb24dd02..3db81157dc723666ce2d0817c22bb283cf73bcf1 100644 --- a/rust-libs/modules/duniter-gva/src/queries/gen_txs.rs +++ b/rust-libs/modules/duniter-gva/src/queries/gen_txs.rs @@ -15,7 +15,7 @@ use crate::*; -const MAX_INPUTS_PER_TX: usize = 40; +const MAX_INPUTS_PER_SIMPLE_TX: usize = 46; #[derive(Default)] pub(crate) struct GenTxsQuery; @@ -29,6 +29,7 @@ impl GenTxsQuery { #[graphql(desc = "Transaction comment")] comment: Option<String>, #[graphql(desc = "Ed25519 public key on base 58 representation")] issuer: String, #[graphql(desc = "Ed25519 public key on base 58 representation")] recipient: String, + #[graphql(desc = "Use mempool sources", default = false)] use_mempool_sources: bool, ) -> async_graphql::Result<Vec<String>> { let amount = SourceAmount::with_base0(amount as i64); let comment = comment.unwrap_or_default(); @@ -50,6 +51,7 @@ impl GenTxsQuery { &dbs.txs_mp_db, amount, &WalletScriptV10::single(WalletConditionV10::Sig(issuer)), + use_mempool_sources, ) }) .await??; @@ -67,7 +69,7 @@ impl GenTxsQuery { current_blockstamp, currency, (inputs, inputs_sum), - MAX_INPUTS_PER_TX, + MAX_INPUTS_PER_SIMPLE_TX, issuer, recipient, (amount, comment),