From 97659203546be0ea0ed5ed051e9f98d723d7bb55 Mon Sep 17 00:00:00 2001
From: librelois <c@elo.tf>
Date: Wed, 18 Nov 2020 19:36:11 +0100
Subject: [PATCH] [feat] gva:gentxs: use pending outputs on mempool for inputs
 search

---
 .../duniter-dbs-read-ops/src/find_inputs.rs   | 126 +++++++++++++-----
 rust-libs/duniter-dbs-read-ops/src/lib.rs     |   2 +-
 rust-libs/duniter-dbs-write-ops/src/lib.rs    |   2 +-
 rust-libs/duniter-dbs-write-ops/src/txs_mp.rs |  29 +++-
 rust-libs/duniter-dbs/src/lib.rs              |   1 +
 rust-libs/duniter-dbs/src/txs_mp_v2.rs        |   1 +
 rust-libs/duniter-dbs/src/values.rs           |   1 +
 rust-libs/duniter-dbs/src/values/utxo.rs      | 126 ++++++++++++++++++
 .../duniter-gva/src/queries/gen_txs.rs        |   6 +-
 9 files changed, 255 insertions(+), 39 deletions(-)
 create mode 100644 rust-libs/duniter-dbs/src/values/utxo.rs

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 8bdef91f8..2bb772865 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 83db8c7d6..53fedd597 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 15bc7130b..540fb68f0 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 7cf0fdaaa..c47a52dd2 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 e6a5325e4..f6f05f10e 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 00574a255..e05532807 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 8f9369419..8221a4b51 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 000000000..f24ef57c2
--- /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 515aa170c..3db81157d 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),
-- 
GitLab