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));
}