Class TransactionPool
A transaction pool that is serializable to disk, backed by SQLite
class TransactionPool
;
Constructors
Name | Description |
---|---|
this
(db, double_spent_selector)
|
Fields
Name | Type | Description |
---|---|---|
selector
|
ulong delegate(Transaction[]) | A delegate to select one of the double spent TXs |
utxo_set
|
Output[geod24 | UTXO set |
Methods
Name | Description |
---|---|
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
Name | Description |
---|---|
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);