Function getGenesisEnrollmentFinder
nothrow @trusted bool delegate(in ref geod24 .bitblob .BitBlob!(64L), out EnrollmentState) getGenesisEnrollmentFinder() nothrow @trusted;
Returns
EnrollmentFinder for GenesisBlock, a delegate to query enrollments in GenesisBlock
Example
import std .algorithm;
import std .range;
scope engine = new Engine();
scope utxos = new MemoryUTXOSet();
scope findUTXO = &utxos .peekUTXO;
scope fee_man = new FeeManager();
scope checker = &fee_man .check;
scope findGenesisEnrollments = getGenesisEnrollmentFinder();
auto gen_key = WK .Keys .Genesis;
assert(GenesisBlock .isGenesisBlockValid());
auto gen_hash = GenesisBlock .header .hashFull();
GenesisBlock .txs .each!(tx => utxos .put(tx));
auto block = GenesisBlock .makeNewTestBlock(genesisSpendable() .map!(txb => txb .sign()));
// height check
block .assertValid(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
block .header .height = 100;
block .assertValid!false(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
block .header .height = GenesisBlock .header .height + 1;
block .assertValid(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
/// .prev_block check
block .header .prev_block = block .header .hashFull();
block .assertValid!false(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
block .header .prev_block = gen_hash;
block .assertValid(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
/// Check consistency of `txs` field
{
auto saved_txs = block .txs;
block .txs = saved_txs[0 .. $ - 1];
block .assertValid!false(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
block .txs = (saved_txs ~ saved_txs) .sort .array;
block .assertValid!false(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
block .txs = saved_txs;
block .assertValid(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
/// Txs sorting check
block .txs .reverse;
block .assertValid!false(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
block .txs .reverse;
block .assertValid(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
}
/// no matching utxo => fail
utxos .clear();
block .assertValid!false(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
GenesisBlock .txs .each!(tx => utxos .put(tx));
block .assertValid(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
utxos .clear(); // genesis is spent
auto prev_txs = block .txs;
prev_txs .each!(tx => utxos .put(tx)); // these will be spent
auto prev_block = block;
block = block .makeNewTestBlock(prev_txs .map!(tx => TxBuilder(tx) .sign()));
block .assertValid(engine, prev_block .header .height, prev_block .header .hashFull(),
findUTXO, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
assert(prev_txs .length > 0); // sanity check
foreach (tx; prev_txs)
{
// one utxo missing from the set => fail
utxos .storage .remove(UTXO .getHash(tx .hashFull(), 0));
block .assertValid!false(engine, prev_block .header .height, prev_block .header .hashFull(),
findUTXO, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
utxos .put(tx);
block .assertValid(engine, prev_block .header .height, prev_block .header .hashFull(),
findUTXO, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
}
// the key is hashMulti(hash(prev_tx), index)
Output[Hash] utxo_set;
foreach (tx; GenesisBlock .txs)
foreach (idx, ref output; tx .outputs)
utxo_set[hashMulti(tx .hashFull, idx)] = output;
assert(utxo_set .length != 0);
const utxo_set_len = utxo_set .length;
// contains the used set of UTXOs during validation (to prevent double-spend)
Output[Hash] used_set;
scope UTXOFinder findNonSpent = (in Hash utxo_hash, out UTXO value)
{
if (utxo_hash in used_set)
return false; // double-spend
if (auto utxo = utxo_hash in utxo_set)
{
used_set[utxo_hash] = *utxo;
value .unlock_height = 0;
value .output = *utxo;
return true;
}
return false;
};
// consumed all utxo => fail
block = GenesisBlock .makeNewTestBlock(genesisSpendable() .map!(txb => txb .sign()));
block .assertValid(engine, GenesisBlock .header .height, GenesisBlock .header .hashFull(),
findNonSpent, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
// All `payment` utxos have been consumed
assert(used_set .length + GenesisBlock .frozens .map!(frozen => frozen .outputs .length) .sum() == utxo_set_len);
// reset state
used_set .clear();
// Double spend => fail
auto double_spend = block .txs .dup;
double_spend[$ - 1] = double_spend[$ - 2];
block = makeNewTestBlock(GenesisBlock, double_spend);
block .assertValid!false(engine, GenesisBlock .header .height, GenesisBlock .header .hashFull(),
findNonSpent, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
// we stopped validation due to a double-spend
assert(used_set .length == double_spend .length - 1);
block = GenesisBlock .makeNewTestBlock(prev_txs .map!(tx => TxBuilder(tx) .sign()));
block .assertValid(engine, GenesisBlock .header .height, GenesisBlock .header .hashFull(),
findUTXO, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
// modify the last hex byte of the merkle root
block .header .merkle_root[][$ - 1]++;
block .assertValid!false(engine, GenesisBlock .header .height, GenesisBlock .header .hashFull(),
findUTXO, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
// now restore it back to what it was
block .header .merkle_root[][$ - 1]--;
block .assertValid(engine, GenesisBlock .header .height, GenesisBlock .header .hashFull(),
findUTXO, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
const last_root = block .header .merkle_root;
block = GenesisBlock .makeNewTestBlock(prev_txs .enumerate .map!(en =>
TxBuilder(en .value) .split(WK .Keys .byRange() .take(en .index + 1) .map!(k => k .address)) .sign()));
block .assertValid(engine, GenesisBlock .header .height, GenesisBlock .header .hashFull(),
findUTXO, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
// the previous merkle root should not match the new txs
block .header .merkle_root = last_root;
block .assertValid!false(engine, GenesisBlock .header .height, GenesisBlock .header .hashFull(),
findUTXO, Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
Example
import agora .common .Amount;
import agora .consensus .data .Enrollment;
import agora .consensus .data .Transaction;
import std .algorithm;
import std .range;
scope engine = new Engine();
scope utxo_set = new MemoryUTXOSet();
UTXOFinder findUTXO = utxo_set .getUTXOFinder();
scope fee_man = new FeeManager();
scope checker = &fee_man .check;
scope findGenesisEnrollments = getGenesisEnrollmentFinder();
auto gen_key = WK .Keys .Genesis;
assert(GenesisBlock .isGenesisBlockValid());
auto gen_hash = GenesisBlock .header .hashFull();
foreach (ref tx; GenesisBlock .txs)
utxo_set .put(tx);
auto txs_1 = genesisSpendable() .map!(txb => txb .sign()) .array();
auto block1 = makeNewTestBlock(GenesisBlock, txs_1);
block1 .assertValid(engine, GenesisBlock .header .height, gen_hash, findUTXO,
genesis_validator_keys .length, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
foreach (ref tx; txs_1)
utxo_set .put(tx);
KeyPair keypair = KeyPair .random();
Transaction[] txs_2;
foreach (idx, pre_tx; txs_1)
{
Input input = Input(hashFull(pre_tx), 0);
Transaction tx = Transaction([input], null);
if (idx == 7)
{
foreach (_; 0 .. 8)
{
Output output;
output .value = Amount(100);
output .lock = genKeyLock(keypair .address);
output .type = OutputType .Payment;
tx .outputs ~= output;
}
}
else
{
Output output;
output .value = Amount .MinFreezeAmount;
output .lock = genKeyLock(keypair .address);
output .type = OutputType .Freeze;
tx .outputs ~= output;
}
tx .outputs .sort;
tx .inputs[0] .unlock = VTx .signUnlock(gen_key, tx);
txs_2 ~= tx;
}
auto block2 = makeNewTestBlock(block1, txs_2);
block2 .assertValid(engine, block1 .header .height, hashFull(block1 .header), findUTXO,
genesis_validator_keys .length, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
foreach (ref tx; txs_2)
utxo_set .put(tx);
KeyPair keypair2 = KeyPair .random();
Transaction[] txs_3;
foreach (idx; 0 .. 8)
{
Input input = Input(hashFull(txs_2[7]), idx);
Transaction tx = Transaction(
[input],
[Output(Amount(1), keypair2 .address)]);
tx .inputs[0] .unlock = VTx .signUnlock(keypair, tx);
txs_3 ~= tx;
}
Pair signature_noise = Pair .random;
Pair node_key_pair = Pair .fromScalar(keypair .secret);
auto utxo_hash1 = UTXO .getHash(hashFull(txs_2[0]), 0);
Enrollment enroll1;
enroll1 .utxo_key = utxo_hash1;
enroll1 .commitment = hashFull(Scalar .random());
enroll1 .enroll_sig = sign(node_key_pair .v, node_key_pair .V, signature_noise .V,
signature_noise .v, hashMulti(block2 .header .height + 1, enroll1));
auto utxo_hash2 = UTXO .getHash(hashFull(txs_2[1]), 0);
Enrollment enroll2;
enroll2 .utxo_key = utxo_hash2;
enroll2 .commitment = hashFull(Scalar .random());
enroll2 .enroll_sig = sign(node_key_pair .v, node_key_pair .V, signature_noise .V,
signature_noise .v, hashMulti(block2 .header .height + 1, enroll2));
Enrollment[] enrollments;
enrollments ~= enroll1;
enrollments ~= enroll2;
enrollments .sort!("a.utxo_key < b.utxo_key");
auto preimage_root = Hash("0x47c993d409aa7d77651ecaa5a5d29e47a7aee609c7" ~
"cb376f5f8ff2a868c738233a2df5ba11d635c8576a47" ~
"3864fc1c8fd1469f4be80b853764da53f6a5b41661");
uint[] missing_validators = [];
auto block3 = makeNewTestBlock(block2, txs_3, genesis_validator_keys, enrollments,
missing_validators);
block3 .assertValid(engine, block2 .header .height, hashFull(block2 .header), findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
block3 .header .enrollments .sort!("a.utxo_key > b.utxo_key");
findUTXO = utxo_set .getUTXOFinder();
// Block: The enrollments are not sorted in ascending order
block3 .assertValid!false(engine, block2 .header .height, hashFull(block2 .header), findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
Example
test that there must always exist active validators
import agora .common .Amount;
import agora .consensus .data .Enrollment;
import agora .consensus .data .Transaction;
import std .algorithm;
import std .range;
scope engine = new Engine();
scope utxo_set = new MemoryUTXOSet();
UTXOFinder findUTXO = utxo_set .getUTXOFinder();
scope fee_man = new FeeManager();
scope checker = &fee_man .check;
scope findGenesisEnrollments = getGenesisEnrollmentFinder();
auto gen_key = WK .Keys .Genesis;
assert(GenesisBlock .isGenesisBlockValid());
auto gen_hash = GenesisBlock .header .hashFull();
foreach (ref tx; GenesisBlock .txs)
utxo_set .put(tx);
auto txs_1 = genesisSpendable() .map!(txb => txb .sign()) .array();
auto block1 = makeNewTestBlock(GenesisBlock, txs_1);
block1 .assertValid(engine, GenesisBlock .header .height, gen_hash, findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
foreach (ref tx; txs_1)
utxo_set .put(tx);
KeyPair keypair = KeyPair .random();
Transaction[] txs_2;
foreach (idx, pre_tx; txs_1)
{
Transaction tx = Transaction(
[Input(hashFull(pre_tx), 0)],
null);
if (idx <= 2)
{
tx .outputs ~= Output(Amount .MinFreezeAmount, keypair .address, OutputType .Freeze);
tx .outputs ~= Output(Amount .MinFreezeAmount, keypair .address, OutputType .Freeze);
tx .outputs ~= Output(Amount .MinFreezeAmount, keypair .address, OutputType .Freeze);
}
else
{
foreach (_; 0 .. 8)
tx .outputs ~= Output(Amount(100), keypair .address);
}
tx .outputs .sort;
tx .inputs[0] .unlock = VTx .signUnlock(gen_key, tx);
txs_2 ~= tx;
}
auto block2 = makeNewTestBlock(block1, txs_2);
block2 .assertValid(engine, block1 .header .height, hashFull(block1 .header), findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
foreach (ref tx; txs_2)
utxo_set .put(tx);
// When all existing validators expire at the new block height and the number of enrollments
// in the new block is 0, the block is considered invalid.
{
KeyPair keypair2 = KeyPair .random();
Transaction[] txs_3;
foreach (idx; 0 .. 8)
{
Transaction tx = Transaction(
[Input(hashFull(txs_2[$-4]), idx)],
[Output(Amount(1), keypair2 .address)]);
tx .inputs[0] .unlock = VTx .signUnlock(keypair, tx);
txs_3 ~= tx;
}
Pair signature_noise = Pair .random;
Pair node_key_pair = Pair .fromScalar(keypair .secret);
auto block3 = makeNewTestBlock(block2, txs_3);
assert(block3 .header .enrollments .length == 0);
block3 .assertValid!false(engine, block2 .header .height, hashFull(block2 .header),
findUTXO, 0, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
}
// When all existing validators expire at the new block height but the number of enrollments
// in the new block is at least 1, the block may be considered valid.
{
KeyPair keypair2 = KeyPair .random();
Transaction[] txs_3;
foreach (idx; 0 .. 8)
{
Transaction tx = Transaction(
[Input(hashFull(txs_2[$-3]), idx)],
[Output(Amount(1), keypair2 .address)]);
tx .inputs[0] .unlock = VTx .signUnlock(keypair, tx);
txs_3 ~= tx;
}
Pair signature_noise = Pair .random;
Pair node_key_pair = Pair .fromScalar(keypair .secret);
auto utxo_hash1 = UTXO .getHash(hashFull(txs_2[1]), 0);
Enrollment enroll1;
enroll1 .utxo_key = utxo_hash1;
enroll1 .commitment = hashFull(Scalar .random());
enroll1 .enroll_sig = sign(node_key_pair .v, node_key_pair .V, signature_noise .V,
signature_noise .v, hashMulti(block2 .header .height + 1, enroll1));
Enrollment[] enrollments;
enrollments ~= enroll1;
enrollments .sort!("a.utxo_key < b.utxo_key");
auto preimage_root = Hash("0x47c993d409aa7d77651ecaa5a5d29e47a7aee609c7" ~
"cb376f5f8ff2a868c738233a2df5ba11d635c8576a47" ~
"3864fc1c8fd1469f4be80b853764da53f6a5b41661");
uint[] missing_validators = [];
auto block3 = makeNewTestBlock(block2, txs_3, genesis_validator_keys, enrollments,
missing_validators);
assert(block3 .header .enrollments .length == Enrollment .MinValidatorCount);
block3 .assertValid(engine, block2 .header .height, hashFull(block2 .header),
findUTXO, 0, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
}
// When there are still active validators at the new block height,
// then new block does not need to contain new enrollments to be considered valid
{
KeyPair keypair2 = KeyPair .random();
Transaction[] txs_3;
foreach (idx; 0 .. 8)
{
Transaction tx = Transaction(
[Input(hashFull(txs_2[$-1]), idx)],
[Output(Amount(1), keypair2 .address)]);
tx .inputs[0] .unlock = VTx .signUnlock(keypair, tx);
txs_3 ~= tx;
}
auto block3 = makeNewTestBlock(block2, txs_3);
assert(block3 .header .enrollments .length == 0);
block3 .assertValid!false(engine, block2 .header .height, hashFull(block2 .header),
findUTXO, 0, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
findUTXO = utxo_set .getUTXOFinder();
block3 .assertValid(engine, block2 .header .height, hashFull(block2 .header), findUTXO,
Enrollment .MinValidatorCount, checker, findGenesisEnrollments, toDelegate(utGetPenaltyDeposit));
}