Class TransactionPool

A transaction pool that is serializable to disk, backed by SQLite

class TransactionPool ;

Constructors

NameDescription
this (db, double_spent_selector)

Fields

NameTypeDescription
selector ulong delegate(Transaction[])A delegate to select one of the double spent TXs
utxo_set Output[geod24.bitblob.BitBlob!(64L)]UTXO set

Methods

NameDescription
add (tx, fee) Add a transaction to the pool
gatherDoubleSpentTXs (tx, double_spent_txs) Gather TXs that share inputs with the given TX
getAverageFeeRate ()
getFrom (from, count)
getPoolSize ()
getTransactionByHash (hash) Get a transaction from pool by hash
getTxFeeRate (tx_hash, rate) Looks up fee rate of the transaction with hash tx_hash from memory
getUnknownTXsFromSet (hashes)
hasTransactionHash (tx) Check if a transaction hash exists in the transaction pool.
hasTxSet (hashes)
isValidTxSet (hashes, spent_utxos) Check if a set of hashes represent a valid TX set
length ()
opApply (dg) Walk over the transactions in the pool and call the provided delegate with each hash and transaction
peekUTXO (utxo, value) Get an UTXO, no double-spend protection.
remove (txs, rm_double_spent) Remove the transaction with the given key from the pool
removeSpenders (utxo_hash) Remove all TXs that spend the utxo_hash

Inner structs

NameDescription
KnownTx Data associated with TXs in the pool for fast access

Example

hasTransactionHash tests

auto pool = new TransactionPool();
auto gen_key = WK.Keys.Genesis;
auto txs = genesisSpendable().map!(txb => txb.sign()).array();

txs.each!(tx => pool.add(tx, 0.coins));
assert(pool.length == txs.length);

foreach (const ref tx; txs)
{
    const(Hash) hash = hashFull(tx);
    assert(pool.hasTransactionHash(hash));
    pool.remove(tx);
    assert(!pool.hasTransactionHash(hash));
}

txs.each!(tx => pool.add(tx, 0.coins));
assert(pool.length == txs.length);
assert(pool.hasTxSet(Set!Hash.from(txs.map!(tx => tx.hashFull))));
assert(!pool.hasTxSet(Set!Hash.from([txs.front().hashFull(), hashFull(1), hashFull(2)])));
auto unknowns = [hashFull(1), hashFull(2)];
assert(unknowns == pool.getUnknownTXsFromSet(Set!Hash.from([txs.front().hashFull()] ~ unknowns)));

auto from_txs = pool.getFrom(Hash.init, pool.length + 1);
assert(pool.length == from_txs.length);

Transaction[] fetched_txs;
auto start_hash = Hash.init;
while(true)
{
    auto new_fetched = pool.getFrom(start_hash, 2);
    if (new_fetched.length == 0)
        break;
    fetched_txs ~= new_fetched;
    start_hash = fetched_txs[$-1].hashFull;
}
assert(from_txs == fetched_txs);

const(Hash) hash = Hash.init;
assert(!pool.hasTransactionHash(hash));
// 'or 1=1-- SQL Injection attack Check
static immutable SqlInjectHash =
    "0x276f7220313d312d2d20"
    ~ "20202020202020202020"
    ~ "20202020202020202020"
    ~ "20202020202020202020"
    ~ "20202020202020202020"
    ~ "20202020202020202020"
    ~ "20202020";
const(Hash) sql_inject_hash = Hash(SqlInjectHash);
assert(!pool.hasTransactionHash(sql_inject_hash));

Example

add & opApply / remove tests (through take())

import std.exception;

auto pool = new TransactionPool();
auto gen_key = WK.Keys.Genesis;
auto txs = genesisSpendable().map!(txb => txb.sign()).array();

txs.each!(tx => pool.add(tx, 0.coins));
assert(pool.length == txs.length);

auto pool_txs = pool.take(txs.length);
assert(pool.length == 0);
assert(txs == pool_txs);

txs.each!(tx => pool.add(tx, 0.coins));
assert(pool.length == txs.length);

auto half_txs = pool.take(txs.length / 2);
assert(half_txs.length == txs.length / 2);
assert(pool.length == txs.length / 2);

// adding duplicate tx hash => return false
pool.add(txs[0], 0.coins);
assert(!pool.add(txs[0], 0.coins));
pool.remove(txs);
assert(pool.length == 0);

Example

memory reclamation tests

import agora.consensus.data.Block;
import std.exception;
import core.memory;

auto pool = new TransactionPool();
auto gen_key = WK.Keys.Genesis;
auto txs = genesisSpendable().map!(txb => txb.sign()).array();

txs.each!(tx => pool.add(tx, 0.coins));
assert(pool.length == txs.length);

// store the txes in serialized form
ubyte[][] txs_bytes;
txs.each!(tx => txs_bytes ~= serializeFull(tx));

txs = null;

// deserialize the transactions
txs_bytes.each!((data)
    {
        scope DeserializeDg dg = (size) nothrow @safe
        {
            ubyte[] res = data[0 .. size];
            data = data[size .. $];
            return res;
        };

        txs ~= deserializeFull!Transaction(dg);
    });

auto pool_txs = pool.take(txs.length);
assert(pool.length == 0);
assert(txs == pool_txs);

Example

test double-spending on the Transaction pool

// create first transaction pool
auto pool = new TransactionPool();

// create first transaction
Transaction tx1 = Transaction(
    [Input(Hash.init, 0)],
    [Output(Amount(0), WK.Keys.A.address)]);

// create second transaction
Transaction tx2 = Transaction(
    [Input(Hash.init, 0)],
    [Output(Amount(0), WK.Keys.C.address)]);

// add txs to the pool
assert(pool.add(tx1, 0.coins));
assert(pool.add(tx2, 0.coins));

assert(pool.length == 2);
pool.remove(tx1);
assert(pool.length == 0);

assert(pool.add(tx1, 0.coins));
assert(pool.add(tx2, 0.coins));
assert(pool.length == 2);
pool.remove(tx2);
assert(pool.length == 0);

Example

test double-spending on the Transaction pool with different unlock age

// create first transaction pool
auto pool = new TransactionPool();

// create first transaction
Transaction tx1 = Transaction(
    [Input(Hash.init, 0, 1)],
    [Output(Amount(0), WK.Keys.A.address)]);

// create second transaction
Transaction tx2 = Transaction(
    [Input(Hash.init, 0, 2)],
    [Output(Amount(0), WK.Keys.C.address)]);

// add txs to the pool
assert(pool.add(tx1, 0.coins));
assert(pool.add(tx2, 0.coins));

assert(pool.length == 2);
pool.remove(tx1);
assert(pool.length == 0);

assert(pool.add(tx1, 0.coins));
assert(pool.add(tx2, 0.coins));
assert(pool.length == 2);
pool.remove(tx2);
assert(pool.length == 0);