duniter-gva/indexer/src/tx.rs
2021-04-22 16:44:17 +02:00

514 lines
18 KiB
Rust

// 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::*;
pub(crate) type ScriptsHash = HashMap<WalletScriptV10, Hash>;
fn get_script_hash(script: &WalletScriptV10, scripts_hash: &mut ScriptsHash) -> Hash {
if let Some(script_hash) = scripts_hash.get(script) {
*script_hash
} else {
let script_hash = Hash::compute(script.to_string().as_bytes());
scripts_hash.insert(script.clone(), script_hash);
script_hash
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn apply_tx<B: Backend>(
current_blockstamp: Blockstamp,
current_time: i64,
gva_db: &mut GvaV1DbTxRw<B::Col>,
scripts_hash: &mut ScriptsHash,
tx_hash: Hash,
tx: &TransactionDocumentV10,
txs_by_issuer_mem: &mut HashMap<WalletHashWithBnV1Db, BTreeSet<Hash>>,
txs_by_recipient_mem: &mut HashMap<WalletHashWithBnV1Db, BTreeSet<Hash>>,
) -> KvResult<()> {
let mut issuers_scripts_hashs = BTreeSet::new();
for input in tx.get_inputs() {
let (account_script_hash, account_script) = match input.id {
SourceIdV10::Utxo(utxo_id) => {
// Get issuer script & written block
let db_tx_origin = gva_db
.txs
.get(&HashKeyV2::from_ref(&utxo_id.tx_hash))?
.ok_or_else(|| {
KvError::DbCorrupted(format!("Not found origin tx of uxto {}", utxo_id))
})?;
let utxo_script = db_tx_origin.tx.get_outputs()[utxo_id.output_index]
.conditions
.script
.clone();
let utxo_script_hash = get_script_hash(&utxo_script, scripts_hash);
// Remove consumed UTXOs
super::utxos::remove_utxo_v10::<B>(
&mut gva_db.scripts_by_pubkey,
&mut gva_db.gva_utxos,
utxo_id,
&utxo_script,
utxo_script_hash,
db_tx_origin.written_block.number.0,
)?;
// Return utxo_script with hash
(utxo_script_hash, utxo_script)
}
SourceIdV10::Ud(UdSourceIdV10 { issuer, .. }) => {
let script = WalletScriptV10::single_sig(issuer);
(Hash::compute(script.to_string().as_bytes()), script)
}
};
issuers_scripts_hashs.insert(account_script_hash);
// Insert on col `txs_by_issuer`
txs_by_issuer_mem
.entry(WalletHashWithBnV1Db::new(
account_script_hash,
current_blockstamp.number,
))
.or_default()
.insert(tx_hash);
// Decrease account balance
decrease_account_balance::<B>(
account_script,
account_script_hash,
&mut gva_db.balances,
input.amount,
&mut gva_db.gva_identities,
false,
&mut gva_db.txs_by_recipient,
)?;
}
for (output_index, output) in tx.get_outputs().iter().enumerate() {
let utxo_script_hash = get_script_hash(&output.conditions.script, scripts_hash);
// Insert created UTXOs
super::utxos::write_utxo_v10::<B>(
&mut gva_db.scripts_by_pubkey,
&mut gva_db.gva_utxos,
UtxoV10 {
id: UtxoIdV10 {
tx_hash,
output_index,
},
amount: output.amount,
script: &output.conditions.script,
written_block: current_blockstamp.number,
},
utxo_script_hash,
)?;
// Insert on col `txs_by_recipient`
if !issuers_scripts_hashs.contains(&utxo_script_hash) {
txs_by_recipient_mem
.entry(WalletHashWithBnV1Db::new(
utxo_script_hash,
current_blockstamp.number,
))
.or_default()
.insert(tx_hash);
}
// Increase account balance
let balance = gva_db
.balances
.get(WalletConditionsV2::from_ref(&output.conditions.script))?
.unwrap_or_default();
gva_db.balances.upsert(
WalletConditionsV2(output.conditions.script.clone()),
SourceAmountValV2(balance.0 + output.amount),
);
}
// Insert tx itself
gva_db.txs.upsert(
HashKeyV2(tx_hash),
GvaTxDbV1 {
tx: tx.clone(),
written_block: current_blockstamp,
written_time: current_time,
},
);
Ok(())
}
pub(crate) fn revert_tx<B: Backend>(
block_number: BlockNumber,
gva_db: &mut GvaV1DbTxRw<B::Col>,
scripts_hash: &mut ScriptsHash,
tx_hash: &Hash,
) -> KvResult<Option<TransactionDocumentV10>> {
if let Some(tx_db) = gva_db.txs.get(&HashKeyV2::from_ref(tx_hash))? {
use dubp::documents::transaction::TransactionDocumentTrait as _;
for (output_index, output) in tx_db.tx.get_outputs().iter().enumerate() {
let script = &output.conditions.script;
let utxo_script_hash = get_script_hash(&script, scripts_hash);
// Remove UTXOs created by this tx
super::utxos::remove_utxo_v10::<B>(
&mut gva_db.scripts_by_pubkey,
&mut gva_db.gva_utxos,
UtxoIdV10 {
tx_hash: *tx_hash,
output_index,
},
script,
utxo_script_hash,
block_number.0,
)?;
// Remove on col `txs_by_recipient`
let k = WalletHashWithBnV1Db::new(utxo_script_hash, block_number);
gva_db.txs_by_recipient.remove(k);
// Decrease account balance
decrease_account_balance::<B>(
script.clone(),
utxo_script_hash,
&mut gva_db.balances,
output.amount,
&mut gva_db.gva_identities,
true,
&mut gva_db.txs_by_recipient,
)?;
}
// Recreate UTXOs consumed by this tx (and update balance)
for input in tx_db.tx.get_inputs() {
let (account_script_hash, account_script) = match input.id {
SourceIdV10::Utxo(utxo_id) => {
let db_tx_origin = gva_db
.txs
.get(&HashKeyV2::from_ref(&utxo_id.tx_hash))?
.ok_or_else(|| {
KvError::DbCorrupted(format!("Not found origin tx of uxto {}", utxo_id))
})?;
let utxo_script = db_tx_origin.tx.get_outputs()[utxo_id.output_index]
.conditions
.script
.clone();
let utxo_script_hash = get_script_hash(&utxo_script, scripts_hash);
super::utxos::write_utxo_v10::<B>(
&mut gva_db.scripts_by_pubkey,
&mut gva_db.gva_utxos,
UtxoV10 {
id: utxo_id,
amount: input.amount,
script: &utxo_script,
written_block: db_tx_origin.written_block.number,
},
utxo_script_hash,
)?;
// Return utxo_script
(utxo_script_hash, utxo_script)
}
SourceIdV10::Ud(UdSourceIdV10 { issuer, .. }) => {
let script = WalletScriptV10::single_sig(issuer);
(Hash::compute(script.to_string().as_bytes()), script)
}
};
// Remove on col `txs_by_issuer`
gva_db
.txs_by_issuer
.remove(WalletHashWithBnV1Db::new(account_script_hash, block_number));
// Increase account balance
let balance = gva_db
.balances
.get(WalletConditionsV2::from_ref(&account_script))?
.unwrap_or_default();
gva_db.balances.upsert(
WalletConditionsV2(account_script),
SourceAmountValV2(balance.0 + input.amount),
);
}
// Remove tx itself
gva_db.txs.remove(HashKeyV2(*tx_hash));
Ok(Some(tx_db.tx))
} else {
Ok(None)
}
}
fn decrease_account_balance<B: Backend>(
account_script: WalletScriptV10,
account_script_hash: Hash,
balances: &mut TxColRw<B::Col, BalancesEvent>,
decrease_amount: SourceAmount,
identities: &mut TxColRw<B::Col, GvaIdentitiesEvent>,
revert: bool,
txs_by_recipients: &mut TxColRw<B::Col, TxsByRecipientEvent>,
) -> KvResult<()> {
if let Some(SourceAmountValV2(balance)) =
balances.get(WalletConditionsV2::from_ref(&account_script))?
{
let new_balance = balance - decrease_amount;
let remove_balance = if revert && new_balance == SourceAmount::ZERO {
let (k_min, k_max) = WalletHashWithBnV1Db::wallet_hash_interval(account_script_hash);
if txs_by_recipients
.iter(k_min..k_max, |it| it.keys().next_res())?
.is_some()
{
false
} else if let Some(pubkey) = account_script.as_single_sig() {
if let Some(idty) = identities.get(&PubKeyKeyV2(pubkey))? {
idty.first_ud.is_none()
} else {
true
}
} else {
true
}
} else {
false
};
if remove_balance {
balances.remove(WalletConditionsV2(account_script));
} else {
balances.upsert(
WalletConditionsV2(account_script),
SourceAmountValV2(new_balance),
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use dubp::{
crypto::keys::ed25519::Ed25519KeyPair, crypto::keys::KeyPair as _,
documents::smallvec::smallvec as svec, documents::transaction::v10::*,
documents::transaction::UTXOConditions,
};
use duniter_core::dbs::BlockMetaV2;
use maplit::btreeset;
#[test]
fn test_apply_tx() -> KvResult<()> {
let kp = Ed25519KeyPair::generate_random().expect("gen rand kp");
let kp2 = Ed25519KeyPair::generate_random().expect("gen rand kp");
let ud0_amount = SourceAmount::with_base0(1000);
let o1_amount = ud0_amount - SourceAmount::with_base0(600);
let o2_amount = ud0_amount - SourceAmount::with_base0(400);
let gva_db = GvaV1Db::<Mem>::open(MemConf::default())?;
let b0 = BlockMetaV2 {
dividend: Some(ud0_amount),
..Default::default()
};
let current_blockstamp = b0.blockstamp();
let pk = kp.public_key();
//println!("TMP pk1={}", pk);
let pk2 = kp2.public_key();
//println!("TMP pk2={}", pk2);
let script = WalletScriptV10::single_sig(pk);
let script2 = WalletScriptV10::single_sig(pk2);
let script_hash = Hash::compute(script.to_string().as_bytes());
let script2_hash = Hash::compute(script2.to_string().as_bytes());
gva_db.balances_write().upsert(
WalletConditionsV2(script.clone()),
SourceAmountValV2(ud0_amount),
)?;
let tx1 = TransactionDocumentV10Builder {
currency: "test",
blockstamp: current_blockstamp,
locktime: 0,
issuers: svec![pk],
inputs: &[TransactionInputV10 {
amount: ud0_amount,
id: SourceIdV10::Ud(UdSourceIdV10 {
issuer: pk,
block_number: BlockNumber(0),
}),
}],
unlocks: &[TransactionInputUnlocksV10::default()],
outputs: svec![
TransactionOutputV10 {
amount: o1_amount,
conditions: UTXOConditions::from(script2.clone()),
},
TransactionOutputV10 {
amount: o2_amount,
conditions: UTXOConditions::from(script.clone()),
}
],
comment: "",
hash: None,
}
.build_and_sign(vec![kp.generate_signator()]);
let tx1_hash = tx1.get_hash();
let mut scripts_hash = HashMap::new();
let mut txs_by_issuer_mem = HashMap::new();
let mut txs_by_recipient_mem = HashMap::new();
(&gva_db).write(|mut db| {
apply_tx::<Mem>(
current_blockstamp,
b0.median_time as i64,
&mut db,
&mut scripts_hash,
tx1_hash,
&tx1,
&mut txs_by_issuer_mem,
&mut txs_by_recipient_mem,
)
})?;
assert_eq!(txs_by_issuer_mem.len(), 1);
assert_eq!(
txs_by_issuer_mem.get(&WalletHashWithBnV1Db::new(script_hash, BlockNumber(0))),
Some(&btreeset![tx1_hash])
);
assert_eq!(txs_by_recipient_mem.len(), 1);
assert_eq!(
txs_by_recipient_mem.get(&WalletHashWithBnV1Db::new(script2_hash, BlockNumber(0))),
Some(&btreeset![tx1_hash])
);
assert_eq!(
gva_db
.balances()
.get(WalletConditionsV2::from_ref(&script2))?,
Some(SourceAmountValV2(o1_amount))
);
assert_eq!(
gva_db
.balances()
.get(WalletConditionsV2::from_ref(&script))?,
Some(SourceAmountValV2(o2_amount))
);
let tx2 = TransactionDocumentV10Builder {
currency: "test",
blockstamp: current_blockstamp,
locktime: 0,
issuers: svec![pk2],
inputs: &[TransactionInputV10 {
amount: o1_amount,
id: SourceIdV10::Utxo(UtxoIdV10 {
tx_hash: tx1_hash,
output_index: 0,
}),
}],
unlocks: &[TransactionInputUnlocksV10::default()],
outputs: svec![TransactionOutputV10 {
amount: o1_amount,
conditions: UTXOConditions::from(script.clone()),
},],
comment: "",
hash: None,
}
.build_and_sign(vec![kp.generate_signator()]);
let tx2_hash = tx2.get_hash();
let mut txs_by_issuer_mem = HashMap::new();
let mut txs_by_recipient_mem = HashMap::new();
(&gva_db).write(|mut db| {
apply_tx::<Mem>(
current_blockstamp,
b0.median_time as i64,
&mut db,
&mut scripts_hash,
tx2_hash,
&tx2,
&mut txs_by_issuer_mem,
&mut txs_by_recipient_mem,
)
})?;
assert_eq!(txs_by_issuer_mem.len(), 1);
assert_eq!(
txs_by_issuer_mem.get(&WalletHashWithBnV1Db::new(script2_hash, BlockNumber(0))),
Some(&btreeset![tx2_hash])
);
assert_eq!(txs_by_recipient_mem.len(), 1);
assert_eq!(
txs_by_recipient_mem.get(&WalletHashWithBnV1Db::new(script_hash, BlockNumber(0))),
Some(&btreeset![tx2_hash])
);
assert_eq!(
gva_db
.balances()
.get(WalletConditionsV2::from_ref(&script2))?,
Some(SourceAmountValV2(SourceAmount::ZERO))
);
assert_eq!(
gva_db
.balances()
.get(WalletConditionsV2::from_ref(&script))?,
Some(SourceAmountValV2(ud0_amount))
);
(&gva_db).write(|mut db| {
revert_tx::<Mem>(
current_blockstamp.number,
&mut db,
&mut scripts_hash,
&tx2_hash,
)
})?;
assert_eq!(
gva_db
.balances()
.get(WalletConditionsV2::from_ref(&script2))?,
Some(SourceAmountValV2(o1_amount))
);
assert_eq!(
gva_db
.balances()
.get(WalletConditionsV2::from_ref(&script))?,
Some(SourceAmountValV2(o2_amount))
);
(&gva_db).write(|mut db| {
revert_tx::<Mem>(
current_blockstamp.number,
&mut db,
&mut scripts_hash,
&tx1_hash,
)
})?;
assert_eq!(
gva_db
.balances()
.get(WalletConditionsV2::from_ref(&script2))?,
None
);
assert_eq!(
gva_db
.balances()
.get(WalletConditionsV2::from_ref(&script))?,
Some(SourceAmountValV2(ud0_amount))
);
Ok(())
}
}