Class ValidatingLedger
A ledger that participate in the consensus protocol
This ledger is held by validators, as they need to do additional bookkeeping when e.g. proposing transactions.
Constructors
Name | Description |
---|---|
this
(params, database, storage, enroll_man, pool, onAcceptedBlock)
|
See parent class |
Fields
Name | Type | Description |
---|---|---|
nominated_tx_sets
|
geod24 | Nominated TX sets |
params
|
immutable(ConsensusParams) | Consensus-critical constants used by this Ledger |
sig_missing_heights
|
Set!(ulong) | Keep track of missing signatures since last paid out block in a set |
cached_coinbase
|
Ledger | Cache for Coinbase tx to be used during payout height |
engine
|
Engine | Script execution engine |
enroll_man
|
EnrollmentManager | Enrollment manager |
fee_man
|
FeeManager | The checker of transaction data payload |
frozen_utxos
|
UTXOTracker | The list of frozen UTXOs known to this Ledger |
last_block
|
Block | The last block in the ledger |
log
|
Logger | Logger instance |
onAcceptedBlock
|
@safe void delegate(in ref Block, bool) | A delegate to be called when a block was externalized (unless null )
|
pool
|
TransactionPool | Pool of transactions to pick from when generating blocks |
rewards
|
Reward | Block rewards calculator |
stateDB
|
ManagedDatabase | The database in which the Ledger state is stored |
storage
|
IBlockStorage | data storage for all the blocks |
unknown_txs
|
Set!(geod24.bitblob.BitBlob!(64L).BitBlob) | Hashes of transactions the Ledger encountered but doesn't have in the pool |
utxo_set
|
UTXOCache | UTXO set |
validator_set
|
ValidatorSet | The object controlling the validator set |
Methods
Name | Description |
---|---|
acceptBlock
(block)
|
See Ledger
|
getCandidateMissingValidators
(height, findUTXO)
|
|
getCandidateTransactions
(height, utxo_finder)
|
|
getValidTXSet
(data, tx_set)
|
Get the valid TX set that data is representing
|
isValidTXSet
(data)
|
Get the valid TX set that data is representing
|
prepareNominatingSet
(data)
|
Collect up to a maximum number of transactions to nominate |
validateConsensusData
(data, initial_missing_validators)
|
Check whether the consensus data is valid. |
validateSlashingData
(height, data, initial_missing_validators)
|
Validate slashing data, including checking if the node is slef slashing |
acceptTransaction
(tx, double_spent_threshold_pct, min_fee_pct)
|
Called when a new transaction is received. |
addPreimage
(preimage)
|
Add a pre-image information to the validator data |
buildBlock
(txs, enrollments, missing_validators)
|
Create a new block based on the current previous block. |
expectedHeight
(utcTime)
|
Get the Height this Ledger should be at if fully synchronized
|
getBlocks
(rng)
|
Get a range of blocks, starting from the provided block height. |
getBlocksFrom
(start)
|
Get a range of blocks from start to Ledger
|
getCandidateEnrollments
(height, utxo_finder)
|
|
getDoubleSpentHighestFee
(tx)
|
Returns the highest fee among all the transactions which would be
considered as a double spent, if tx transaction was in the transaction
pool.
|
getEnrolledUTXOs
(height)
|
|
getExpectedBlockTime
(height)
|
Gets the expected block nomination time offset from Genesis start time. |
getLastPaidHeight
()
|
return the last paid out block before the current block |
getPenaltyDeposit
(utxo)
|
|
getStakes
()
|
Get a set of all the stakes currently active at this height |
getTransactionByHash
(hash)
|
Get a transaction from pool by hash |
getTxFeeRate
(tx, rate)
|
Forwards to FeeManager , using this Ledger's UTXO.
|
getTxFeeRate
(tx_hash, rate)
|
Looks up transaction with hash tx_hash , then forwards to
FeeManager , using this Ledger's UTXO.
|
getUnknownTXHashes
()
|
Get a set of TX Hashes that Ledger is missing |
getUnknownTXsFromSet
(hashes)
|
|
getUTXOFinder
()
|
Prepare tracking double-spent transactions and return the UTXOFinder delegate |
getValidators
(height, empty)
|
Expose the list of validators at a given height |
hasMajoritySignature
(header)
|
|
height
()
|
Returns the height at which this Ledger is currently at
|
isAcceptableDoubleSpent
(tx, threshold_pct)
|
Checks whether the tx is an acceptable double spend transaction.
|
isCoinbaseBlock
(height)
|
Check if this is the height for payouts of a payout period
|
isRelatedRewardBlock
(reward_height, coinbase_height)
|
Check if block is related to given payout block |
isStake
(hash, utxo)
|
|
lastBlock
()
|
Returns a reference to the last block in the Ledger
|
peekUTXO
(utxo, value)
|
Get an UTXO, no double-spend protection. |
updateBlockMultiSig
(header)
|
Update the Schnorr multi-signature for an externalized block in the Ledger. |
utxos
()
|
Expose an object that interacts with the UTXO set |
validateBlock
(block)
|
Check whether the block is valid. |
validateBlockSignature
(header)
|
Validate the signature of a block |
validatorCount
(height)
|
Get the count of active validators at a given height. |
addValidatedBlock
(block)
|
See Ledger.addValidatedBlock` |
applySlashing
(header)
|
Apply slashing to the current state |
getCoinbaseTX
(height)
|
Create the Coinbase transaction for this payout block and append it
to the transaction set
|
handleNotSignedByMajority
(header, validators)
|
Used to handle behaviour when less than half the validators have signed
the block. This is overridden in the ValidatingLedger
|
replayStoredBlock
(block)
|
Update the ledger state from a block which was read from storage |
updateUTXOSet
(block)
|
See Ledger
|
updateValidatorSet
(block)
|
See Ledger
|
Inner structs
Name | Description |
---|---|
CachedCoinbase
|
Cache for Coinbase tx to be used during payout height |
Enums
Name | Description |
---|---|
InvalidConsensusDataReason
|
Error message describing the reason of validation failure |
Example
scope ledger = new TestLedger(WK .Keys .NODE3);
assert(ledger .height() == 0);
auto blocks = ledger .getBlocksFrom(Height(0)) .take(10) .array;
assert(blocks[$ - 1] == ledger .params .Genesis);
Transaction[] last_txs;
void genBlockTransactions (size_t count)
{
foreach (_; 0 .. count)
last_txs = ledger .makeTestBlock(last_txs);
}
genBlockTransactions(2);
blocks = ledger .getBlocksFrom(Height(0)) .take(10) .array;
assert(blocks[0] == ledger .params .Genesis);
assert(blocks .length == 3); // two blocks + genesis block
/// now generate 98 more blocks to make it 100 + genesis block (101 total)
genBlockTransactions(98);
assert(ledger .height() == 100);
blocks = ledger .getBlocksFrom(Height(0)) .takeExactly(10) .array;
assert(blocks[0] == ledger .params .Genesis);
assert(blocks .length == 10);
/// lower limit
blocks = ledger .getBlocksFrom(Height(0)) .takeExactly(5) .array;
assert(blocks[0] == ledger .params .Genesis);
assert(blocks .length == 5);
/// different indices
blocks = ledger .getBlocksFrom(Height(1)) .takeExactly(10) .array;
assert(blocks[0] .header .height == 1);
assert(blocks .length == 10);
blocks = ledger .getBlocksFrom(Height(50)) .takeExactly(10) .array;
assert(blocks[0] .header .height == 50);
assert(blocks .length == 10);
blocks = ledger .getBlocksFrom(Height(95)) .take(10) .array; // only 6 left from here (block 100 included)
assert(blocks .front .header .height == 95);
assert(blocks .walkLength() == 6);
blocks = ledger .getBlocksFrom(Height(99)) .take(10) .array; // only 2 left from here (ditto)
assert(blocks .front .header .height == 99);
assert(blocks .walkLength() == 2);
blocks = ledger .getBlocksFrom(Height(100)) .take(10) .array; // only 1 block available
assert(blocks .front .header .height == 100);
assert(blocks .walkLength() == 1);
// over the limit => return up to the highest block
assert(ledger .getBlocksFrom(Height(0)) .take(1000) .walkLength() == 101);
// higher index than available => return nothing
assert(ledger .getBlocksFrom(Height(1000)) .take(10) .walkLength() == 0);
Example
basic block verification
scope ledger = new TestLedger(genesis_validator_keys[0]);
Block invalid_block; // default-initialized should be invalid
assert(ledger .acceptBlock(invalid_block));
Example
Situation
Ledger is constructed with blocks present in storage
Expectation
The UTXOSet is populated with all up-to-date UTXOs
import agora .consensus .data .genesis .Test;
const(Block)[] blocks = [
GenesisBlock,
makeNewTestBlock(GenesisBlock, GenesisBlock .spendable() .map!(txb => txb .sign()))
];
// Make 3 more blocks to put in storage
foreach (idx; 2 .. 5)
{
blocks ~= makeNewTestBlock(
blocks[$ - 1],
blocks[$ - 1] .spendable() .map!(txb => txb .sign()));
}
// And provide it to the ledger
scope ledger = new TestLedger(genesis_validator_keys[0], blocks);
assert(ledger .utxo_set .length
== /* Genesis, Frozen */ 12 + 8 /* Block #1 Payments*/);
// Ensure that all previously-generated outputs are in the UTXO set
{
auto findUTXO = ledger .getUTXOFinder();
UTXO utxo;
assert(
blocks[$ - 1] .txs .all!(
tx => iota(tx .outputs .length) .all!(
(idx) {
return findUTXO(UTXO .getHash(tx .hashFull(), idx), utxo) &&
utxo .output == tx .outputs[idx];
}
)
)
);
}
Example
test enrollments in the genesis block
import std .exception : assertThrown;
// Default test genesis block has 6 validators
{
scope ledger = new TestLedger(WK .Keys .A);
assert(ledger .getValidators(Height(1)) .length == 6);
}
// One block before `ValidatorCycle`, validator is still active
{
const ValidatorCycle = 20;
auto params = new immutable(ConsensusParams)(ValidatorCycle);
const blocks = genBlocksToIndex(ValidatorCycle - 1, params);
scope ledger = new TestLedger(WK .Keys .A, blocks, params);
Hash[] keys;
assert(ledger .getValidators(Height(ValidatorCycle)) .length == 6);
}
// Past `ValidatorCycle`, validator is inactive
{
const ValidatorCycle = 20;
auto params = new immutable(ConsensusParams)(ValidatorCycle);
const blocks = genBlocksToIndex(ValidatorCycle, params);
// Enrollment: Insufficient number of active validators
auto ledger = new TestLedger(WK .Keys .A, blocks, params);
assertThrown(ledger .getValidators(Height(ValidatorCycle + 1)));
}
Example
test atomicity of adding blocks and rolling back
import std .conv;
import std .exception : assertThrown;
import core .stdc .time : time;
static class ThrowingLedger : Ledger
{
bool throw_in_update_utxo;
bool throw_in_update_validators;
public this (KeyPair kp, const(Block)[] blocks, immutable(ConsensusParams) params)
{
auto stateDB = new ManagedDatabase(":memory:");
ValidatorConfig vconf = ValidatorConfig(true, kp);
super(params,
stateDB,
new MemBlockStorage(blocks),
new ValidatorSet(stateDB, params));
}
///
protected override void replayStoredBlock (in Block block) @safe
{
if (block .header .height > 0)
this .simulatePreimages(block .header .height);
super .replayStoredBlock(block);
}
override void updateUTXOSet (in Block block) @safe
{
super .updateUTXOSet(block);
if (this .throw_in_update_utxo)
throw new Exception("");
}
override void updateValidatorSet (in Block block) @safe
{
super .updateValidatorSet(block);
if (this .throw_in_update_validators)
throw new Exception("");
}
/// Expose the UTXO set to this module
@property public UTXOCache utxo_set () @safe pure nothrow @nogc
{
return super .utxo_set;
}
}
const params = new immutable(ConsensusParams)();
// throws in updateUTXOSet() => rollback() called, UTXO set reverted,
// Validator set was not modified
{
const blocks = genBlocksToIndex(params .ValidatorCycle, params);
assert(blocks .length == params .ValidatorCycle + 1); // +1 for genesis
scope ledger = new ThrowingLedger(
WK .Keys .A, blocks .takeExactly(params .ValidatorCycle), params);
assert(ledger .getValidators(Height(params .ValidatorCycle)) .length == 6);
auto utxos = ledger .utxo_set .getUTXOs(WK .Keys .Genesis .address);
assert(utxos .length == 8);
utxos .each!(utxo => assert(utxo .unlock_height == params .ValidatorCycle));
ledger .throw_in_update_utxo = true;
auto next_block = blocks[$ - 1];
assertThrown!Exception(assert(ledger .acceptBlock(next_block) is null));
assert(ledger .lastBlock() == blocks[$ - 2]); // not updated
utxos = ledger .utxo_set .getUTXOs(WK .Keys .Genesis .address);
assert(utxos .length == 8);
utxos .each!(utxo => assert(utxo .unlock_height == params .ValidatorCycle)); // reverted
// not updated
assert(ledger .getValidators(Height(params .ValidatorCycle)) .length == 6);
}
// throws in updateValidatorSet() => rollback() called, UTXO set and
// Validator set reverted
{
const blocks = genBlocksToIndex(params .ValidatorCycle, params);
assert(blocks .length == 21); // +1 for genesis
scope ledger = new ThrowingLedger(
WK .Keys .A, blocks .takeExactly(params .ValidatorCycle), params);
assert(ledger .getValidators(Height(params .ValidatorCycle)) .length == 6);
auto utxos = ledger .utxo_set .getUTXOs(WK .Keys .Genesis .address);
assert(utxos .length == 8);
utxos .each!(utxo => assert(utxo .unlock_height == params .ValidatorCycle));
ledger .throw_in_update_validators = true;
auto next_block = blocks[$ - 1];
assertThrown!Exception(assert(ledger .acceptBlock(next_block) is null));
assert(ledger .lastBlock() == blocks[$ - 2]); // not updated
utxos = ledger .utxo_set .getUTXOs(WK .Keys .Genesis .address);
assert(utxos .length == 8);
utxos .each!(utxo => assert(utxo .unlock_height == params .ValidatorCycle)); // reverted
assert(ledger .getValidators(ledger .lastBlock() .header .height) .length == 6);
}
Example
throw if the gen block in block storage is different to the configured one
import agora .consensus .data .genesis .Test;
import agora .consensus .data .genesis .Coinnet : CoinGenesis = GenesisBlock;
// ConsensusParams is instantiated by default with the test genesis block
immutable params = new immutable(ConsensusParams)(CoinGenesis, WK .Keys .CommonsBudget .address);
try
{
scope ledger = new TestLedger(WK .Keys .A, [GenesisBlock], params);
assert(0);
}
catch (Exception ex)
{
assert(ex .message ==
"Genesis block loaded from disk " ~
"(0x8365f069fe37ee02f2c4dc6ad816702088fab5fc875c3c67b01f82c285aa" ~
"2d90b605f57e068139eba1f20ce20578d712f75be4d8568c8f3a7a34604e72aa3175) "~
"is different from the one in the config file " ~
"(0x70c39bda1082ff0715afecd942650bca1773ce4a2fe83fc206234141b8c0e" ~
"a5199c5c46f1705c48cb717bea633e5d5c3b6dba08e4fc9e1aa28b09e3bf268eaaa)");
}
immutable good_params = new immutable(ConsensusParams)();
// will not fail
scope ledger = new TestLedger(WK .Keys .A, [GenesisBlock], good_params);
// Neither will the default
scope other_ledger = new TestLedger(WK .Keys .A, [GenesisBlock]);
Example
Testing accumulated fees paid to Commons Budget and non slashed Validators
import agora .consensus .data .genesis .Test;
import agora .consensus .PreImage;
import agora .utils .WellKnownKeys : CommonsBudget;
const testPayoutPeriod = 5;
ConsensusConfig config = { validator_cycle: 20, payout_period: testPayoutPeriod };
auto params = new immutable(ConsensusParams)(GenesisBlock,
CommonsBudget .address, config);
assert(params .PayoutPeriod == testPayoutPeriod);
const(Block)[] blocks = [ GenesisBlock ];
scope ledger = new TestLedger(genesis_validator_keys[0], blocks, params);
// Add preimages for all validators (except for two of them) till end of cycle
uint[] skip_indexes = [ 2, 5 ];
auto validators = ledger .getValidators(Height(1));
UTXO[] mpv_stakes;
foreach (skip; skip_indexes)
assert(ledger .utxo_set .peekUTXO(validators[skip] .utxo, mpv_stakes[(++mpv_stakes .length) - 1]));
ledger .simulatePreimages(Height(params .ValidatorCycle), skip_indexes);
assert(ledger .params .BlockInterval .total!"seconds" == 600);
Amount allocated_validator_rewards = Amount .UnitPerCoin * 27 * (600 / 5);
assert(allocated_validator_rewards == 3_240 .coins);
Amount commons_reward = Amount .UnitPerCoin * 50 * (600 / 5);
assert(commons_reward == 6_000 .coins);
Amount total_rewards = (allocated_validator_rewards + commons_reward) * testPayoutPeriod;
auto tx_set_fees = Amount(0);
auto total_fees = Amount(0);
Amount[] next_payout_total;
// Create blocks from height 1 to 11 (only block 5 and 10 should have a coinbase tx)
foreach (height; 1..11)
{
auto txs = blocks[$-1] .spendable .map!(txb => txb .sign()) .array();
txs .each!(tx => assert(ledger .acceptTransaction(tx) is null));
tx_set_fees = txs .map!(tx => tx .getFee(&ledger .utxo_set .peekUTXO, &ledger .getPenaltyDeposit)) .reduce!((a,b) => a + b);
// Add the fees for this height
total_fees += tx_set_fees;
auto data = ConsensusData .init;
ledger .prepareNominatingSet(data);
// Do some Coinbase tests with the data tx_set
if (height >= 2 * testPayoutPeriod && height % testPayoutPeriod == 0)
{
// Remove the coinbase TX
auto tx_set = ledger .nominated_tx_sets[data .tx_set][0 .. $ - 1];
ledger .nominated_tx_sets[tx_set .hashFull] = tx_set;
data .tx_set = tx_set .hashFull;
assert(ledger .validateConsensusData(data, skip_indexes) == "Missing matching Coinbase transaction");
// Add different hash to tx_set
tx_set ~= "Not Coinbase tx" .hashFull();
ledger .nominated_tx_sets[tx_set .hashFull] = tx_set;
data .tx_set = tx_set .hashFull;
assert(ledger .validateConsensusData(data, skip_indexes) == "Missing matching Coinbase transaction");
}
// Now externalize the block
ledger .prepareNominatingSet(data);
total_fees += ledger .params .SlashPenaltyAmount * data .missing_validators .length;
if (height % testPayoutPeriod == 0)
{
next_payout_total ~= total_fees + total_rewards;
total_fees = Amount(0);
}
assert(ledger .externalize(data) is null);
assert(ledger .height() == blocks .length);
blocks ~= ledger .getBlocksFrom(Height(blocks .length)) .front;
auto cb_txs = blocks[$-1] .txs .filter!(tx => tx .isCoinbase) .array;
if (height >= 2 * testPayoutPeriod && height % testPayoutPeriod == 0)
{
assert(cb_txs .length == 1);
// Payout block should pay the CommonsBudget + all validators (excluding slashed validators)
assert(cb_txs[0] .outputs .length == 1 + genesis_validator_keys .length - skip_indexes .length);
assert(cb_txs[0] .outputs .map!(o => o .value) .reduce!((a,b) => a + b) == next_payout_total[0]);
next_payout_total = next_payout_total[1 .. $];
// Slashed validators should never be paid
mpv_stakes .each!((mpv_stake)
{
assert(cb_txs[0] .outputs .filter!(output => output .address ==
mpv_stake .output .address) .array .length == 0);
});
}
else
assert(cb_txs .length == 0);
}
Example
throw if the gen block in block storage is for a different chain id
import agora .consensus .data .genesis .Test;
immutable params = new immutable(ConsensusParams)();
try
{
setHashMagic(0x44); // Test GenesisBlock is signed for ChainID=0
scope ledger = new TestLedger(WK .Keys .A, [GenesisBlock], params);
assert(0);
} catch (Exception ex) {}
setHashMagic(0); // should work just fine
scope ledger = new TestLedger(WK .Keys .A, [GenesisBlock], params);