Class EnrollmentManager

Handle enrollment data and manage the validators set

class EnrollmentManager ;

Constructors

NameDescription
this (stateDB, cacheDB, config, params) Constructor

Fields

NameTypeDescription
enroll_pool EnrollmentPoolEnrollment pool managing enrollments waiting to be a validator
validator_set ValidatorSetValidator set managing validators' information such as Enrollment object enrolled height, and preimages.

Methods

NameDescription
addEnrollment (enroll, pubkey, height, finder, getPenaltyDeposit) Add a enrollment data to the enrollment pool
addValidator (enroll, pubkey, height, finder, getPenaltyDeposit, self_utxos) Add a validator to the validator set or update the enrolled height.
createEnrollment (utxo, height) Build an Enrollment using makeEnrollment, stores and returns it
getCommitment () Get the commitment for the enrollment for this node
getEnrolledUTXO (height, finder)
getEnrollmentKey () Get the key for the enrollment for this node
getEnrollmentPublicKey () Get the public key of node that is used for a enrollment
getEnrollments (height, peekUTXO, getPenaltyDeposit, findUTXO) Get the unregistered enrollments that can be validator in the next block based on the current block height.
getPreimage (height) Get a pre-image for revelation
getValidatorPreimage (utxo) Get validator's pre-image from the validator set.
getValidatorPreimages (start_height) Get validators' pre-image information
isEnrolled (height, finder)
isInvalidCandidateReason (enroll, pubkey, height, findUTXO, getPenaltyDeposit) Check if an enrollment is a valid candidate for the proposed height
makeEnrollment (utxo, key, seed, height, offset) Build enrollment data for an arbitrary utxo + key combination
removeEnrollment (enroll_key) Remove all enrollments associated with a key from pool

Example

tests for member functions of EnrollmentManager

import agora.consensus.data.Transaction;
import std.algorithm;
import std.range;

scope utxo_set = new MemoryUTXOSet;
auto getPenaltyDeposit = (Hash utxo)
{
    UTXO val;
    return utxo_set.peekUTXO(utxo, val) ? 10_000.coins : 0.coins;
};

Hash[] utxo_hashes;

auto gen_key_pair = WK.Keys.Genesis;
KeyPair key_pair = WK.Keys.A;

// genesisSpendable returns 8 outputs
auto pairs = iota(8).map!(idx => WK.Keys[idx]).array;
genesisSpendable()
    .enumerate
    .map!(tup => tup.value
        .refund(pairs[tup.index].address)
        .sign(OutputType.Freeze))
    .each!((tx) {
        utxo_set.put(tx);
        utxo_hashes ~= UTXO.getHash(tx.hashFull(), 0);
    });

auto utxos = utxo_set.storage;

// create an EnrollmentManager object
auto params = new immutable(ConsensusParams)();
auto man = new EnrollmentManager(key_pair, params);
// Useful constant
const EnrollAt1 = Height(1);

// check the return value of `getEnrollmentPublicKey`
assert(key_pair.address == man.getEnrollmentPublicKey());

// create and add the first Enrollment object
auto utxo_hash = utxo_hashes[0];

Enrollment[] enrolls_before = man.getEnrollments(EnrollAt1, &utxo_set.peekUTXO, getPenaltyDeposit);
assert(enrolls_before.length == 0);

Enrollment[] ordered_enrollments;
foreach (idx, kp; pairs[0 .. 3])
{
    auto enroll = EnrollmentManager.makeEnrollment(utxo_hashes[idx], kp, EnrollAt1, params.ValidatorCycle);
    assert(man.addEnrollment(enroll, kp.address, EnrollAt1, &utxo_set.peekUTXO, getPenaltyDeposit));
    assert(man.enroll_pool.count() == idx + 1);
    ordered_enrollments ~= enroll;
}

Enrollment[] enrolls = man.getEnrollments(EnrollAt1, &utxo_set.peekUTXO, getPenaltyDeposit);
assert(enrolls.length == 3);
assert(enrolls.isStrictlyMonotonic!("a.utxo_key < b.utxo_key"));

// get a stored Enrollment object
Enrollment stored_enroll;
assert((stored_enroll = man.enroll_pool.getEnrollment(utxo_hashes[1])) !=
    Enrollment.init);
assert(stored_enroll == ordered_enrollments[1]);

// remove an Enrollment object
man.enroll_pool.remove(utxo_hashes[1]);
assert(man.enroll_pool.count() == 2);

// test for getEnrollment with removed enrollment
assert(man.enroll_pool.getEnrollment(utxo_hashes[1]) == Enrollment.init);

// test for enrollment block height update
const EnrollAt9 = Height(9);
assert(man.validator_set.countActive(EnrollAt9 + 1) == 0);
assert(man.validator_set.getEnrolledHeight(EnrollAt9, utxo_hash) == ulong.max);
// Add removed enrollment to the pool with the new height
auto enroll = EnrollmentManager.makeEnrollment(utxo_hashes[1], pairs[1], EnrollAt9, params.ValidatorCycle);
assert(man.addEnrollment(enroll, pairs[1].address, EnrollAt9, &utxo_set.peekUTXO, getPenaltyDeposit));
// The expired enrollments in the pool from height 1 are cleared on this next call
// to get enrollments at a higher height. We do have an enrollment at height 9
assert(man.getEnrollments(EnrollAt9, &utxo_set.peekUTXO, getPenaltyDeposit).length == 1);
assert(man.addValidator(enroll, pairs[1].address, EnrollAt9, &utxo_set.peekUTXO, getPenaltyDeposit, utxos)
       is null);
assert(man.validator_set.getEnrolledHeight(EnrollAt9 + 1, enroll.utxo_key) == EnrollAt9);
// One Enrollment was moved to validator set
assert(man.validator_set.countActive(EnrollAt9 + 1) == 1);
// Check last block of cycle is still active
assert(man.validator_set.countActive(EnrollAt9 + params.ValidatorCycle) == 1);
// Check block in next cycle is no longer active
assert(man.validator_set.countActive(EnrollAt9 + params.ValidatorCycle + 1) == 0);
// Pool should now be empty
assert(man.enroll_pool.count() == 0);

assert(man.getEnrollments(EnrollAt9, &utxo_set.peekUTXO, getPenaltyDeposit).length == 0);

// clear up all validators
man.validator_set.removeAll();
ordered_enrollments.length = 0;

enroll = EnrollmentManager.makeEnrollment(utxo_hashes[0], pairs[0], Height(10), params.ValidatorCycle);
// A validator is enrolled at the height of 10.
PreImageInfo preimage;
assert(man.addValidator(enroll, WK.Keys[0].address, Height(10),
        &utxo_set.peekUTXO, getPenaltyDeposit, utxos) is null);
preimage = man.getPreimage(Height(12));
assert(preimage.height == Height(12));
assert(preimage.hash == man.cycle[preimage.height]);

// test for getting validators' UTXO keys
Hash[] keys;

enroll = EnrollmentManager.makeEnrollment(utxo_hashes[1], pairs[1], Height(11), params.ValidatorCycle);
// validator A with the `utxo_hash` and the enrolled height of 10.
// validator B with the 'utxo_hash2' and the enrolled height of 11.
// validator C with the 'utxo_hash3' and no enrolled height.
assert(man.addValidator(enroll, WK.Keys[1].address, Height(11),
        &utxo_set.peekUTXO, getPenaltyDeposit, utxos) is null);
assert(man.validator_set.countActive(Height(12)) == 2);
assert(man.validator_set.getEnrolledUTXOs(Height(12), keys));
assert(keys.length == 2);

enroll = EnrollmentManager.makeEnrollment(utxo_hashes[2], pairs[2], Height(12), params.ValidatorCycle);
// set an enrolled height for validator C
// set the block height to 1019, which means validator B is expired.
// there is only one validator in the middle of 1020th block being made.
assert(man.addValidator(
           enroll, WK.Keys[2].address, Height(12),
           &utxo_set.peekUTXO, getPenaltyDeposit, utxos)
       is null);
assert(man.validator_set.countActive(Height(params.ValidatorCycle + 12)) == 1);
assert(man.validator_set.getEnrolledUTXOs(Height(params.ValidatorCycle + 12), keys));
assert(keys.length == 1);
assert(keys[0] == enroll.utxo_key);

Example

tests for `ValidatorSet.countActive

import agora.consensus.data.Transaction;
import std.range;

scope utxo_set = new MemoryUTXOSet;
Hash[] utxo_hashes;
auto getPenaltyDeposit = (Hash utxo)
{
    UTXO val;
    return utxo_set.peekUTXO(utxo, val) ? 10_000.coins : 0.coins;
};

KeyPair key_pair = WK.Keys.A;

// genesisSpendable returns 8 outputs
auto pairs = iota(8).map!(idx => WK.Keys[idx]).array;
genesisSpendable()
    .enumerate
    .map!(tup => tup.value
        .refund(pairs[tup.index].address)
        .sign(OutputType.Freeze))
    .each!((tx) {
        utxo_set.put(tx);
        utxo_hashes ~= UTXO.getHash(tx.hashFull(), 0);
    });
auto utxos = utxo_set.storage;

// create an EnrollmentManager object
auto params = new immutable(ConsensusParams)(20);
auto man = new EnrollmentManager(key_pair, params);

Height height = Height(2);

auto enroll = EnrollmentManager.makeEnrollment(utxo_hashes[0], WK.Keys[0], height, params.ValidatorCycle);
// create and add the first Enrollment object
assert(man.addEnrollment(enroll, WK.Keys[0].address, height,
        utxo_set.getUTXOFinder(), getPenaltyDeposit));
assert(man.validator_set.countActive(height + 1) == 0);  // not active yet

assert(man.addValidator(enroll, WK.Keys[0].address, height, &utxo_set.peekUTXO,
    getPenaltyDeposit, utxos) is null);
assert(man.validator_set.countActive(height + 1) == 1);  // updated

height = 3;
enroll = EnrollmentManager.makeEnrollment(utxo_hashes[1], WK.Keys[1], height, params.ValidatorCycle);

// create and add the second Enrollment object
assert(man.addEnrollment(enroll, WK.Keys[1].address, height,
        utxo_set.getUTXOFinder(), getPenaltyDeposit));
assert(man.validator_set.countActive(height + 1) == 1);  // not active yet

assert(man.addValidator(enroll, WK.Keys[1].address, height, &utxo_set.peekUTXO,
    getPenaltyDeposit, utxos) is null);
assert(man.validator_set.countActive(height + 1) == 2);  // updated

height = 4;
enroll = EnrollmentManager.makeEnrollment(utxo_hashes[2], WK.Keys[2], height, params.ValidatorCycle);

// create and add the third Enrollment object
assert(man.addEnrollment(enroll, WK.Keys[2].address, height,
        utxo_set.getUTXOFinder(), getPenaltyDeposit));
assert(man.validator_set.countActive(height + 1) == 2);  // not active yet

assert(man.addValidator(enroll, WK.Keys[2].address, height, &utxo_set.peekUTXO,
    getPenaltyDeposit, utxos) is null);
assert(man.validator_set.countActive(height + 1) == 3);  // updated

height = 5;    // valid block height : 0 <= H < 20
assert(man.validator_set.countActive(height + 1) == 3);  // not cleared yet

height = Height(1 + params.ValidatorCycle); // valid block height : 2 <= H < 22
assert(man.validator_set.countActive(height + 1) == 3);

height = Height(2 + params.ValidatorCycle); // valid block height : 3 <= H < 23
assert(man.validator_set.countActive(height + 1) == 2);

height = Height(3 + params.ValidatorCycle); // valid block height : 4 <= H < 24
assert(man.validator_set.countActive(height + 1) == 1);

height = Height(4 + params.ValidatorCycle); // valid block height : 5 <= H < 25
assert(man.validator_set.countActive(height + 1) == 0);

Example

Test for the height when the enrollment will be available

import agora.consensus.data.Transaction;
import agora.consensus.state.UTXOSet;

// create an EnrollmentManager
const validator_cycle = 20;
KeyPair key_pair = WK.Keys.A;
scope man = new EnrollmentManager(key_pair,
    new immutable(ConsensusParams)(validator_cycle));

scope utxo_set = new UTXOSet(man.db);
auto getPenaltyDeposit = (Hash utxo)
{
    UTXO val;
    return utxo_set.peekUTXO(utxo, val) ? 10_000.coins : 0.coins;
};
genesisSpendable().map!(txb => txb.refund(key_pair.address).sign(OutputType.Freeze))
    .each!(tx => utxo_set.updateUTXOCache(tx, Height(1), man.params.CommonsBudgetAddress));
auto utxos = utxo_set.getUTXOs(key_pair.address);

// create and add the first enrollment
Enrollment[] enrolls;
const firstEnrolledAt10 = Height(10);
auto enroll = man.createEnrollment(utxos.keys[0], firstEnrolledAt10);
assert(man.addEnrollment(enroll, key_pair.address, firstEnrolledAt10,
        utxo_set.getUTXOFinder(), getPenaltyDeposit));

// if the current height is smaller than the available height,
// we can get no enrollment
enrolls = man.getEnrollments(Height(firstEnrolledAt10 - 1), &utxo_set.peekUTXO, getPenaltyDeposit);
assert(enrolls.length == 0);

// if the current height is equal to the available height we can get enrollments
enrolls = man.getEnrollments(firstEnrolledAt10, &utxo_set.peekUTXO, getPenaltyDeposit);
assert(enrolls.length == 1);

// if the current height is more than the available height we don't
enrolls = man.getEnrollments(firstEnrolledAt10 + 1, &utxo_set.peekUTXO, getPenaltyDeposit);
assert(enrolls.length == 0);

// make the enrollment a validator
man.addValidator(enroll, key_pair.address, firstEnrolledAt10, &utxo_set.peekUTXO, getPenaltyDeposit, utxos);
enrolls = man.getEnrollments(firstEnrolledAt10, &utxo_set.peekUTXO, getPenaltyDeposit);
assert(enrolls.length == 0);

// add the enrollment that is already a validator, and check if
// the enrollment can be nominated at the height before the cycle end
auto re_enroll = man.createEnrollment(utxos.keys[0], firstEnrolledAt10 + validator_cycle);
assert(man.addEnrollment(re_enroll, key_pair.address, firstEnrolledAt10 + validator_cycle, &utxo_set.peekUTXO,
    getPenaltyDeposit));

// Can only enroll at exact height as the preimage is for that height
assert(man.getEnrollments(Height(firstEnrolledAt10 + validator_cycle - 1),
    &utxo_set.peekUTXO, getPenaltyDeposit).length == 0);
enrolls = man.getEnrollments(firstEnrolledAt10 + validator_cycle,
    &utxo_set.peekUTXO, getPenaltyDeposit);
assert(enrolls.length == 1);
// We do this test after as the expired will be remved from the pool
assert(man.getEnrollments(firstEnrolledAt10 + validator_cycle + 1,
    &utxo_set.peekUTXO, getPenaltyDeposit).length == 0);

// make the enrollment a validator again
assert(man.addValidator(enroll, key_pair.address, firstEnrolledAt10 + validator_cycle,
    &utxo_set.peekUTXO, getPenaltyDeposit, utxos));
// Enrollment now gone from the pool
assert(man.getEnrollments(firstEnrolledAt10 + validator_cycle,
    &utxo_set.peekUTXO, getPenaltyDeposit).length == 0);

Example

tests for adding enrollments from the same public key

import agora.consensus.data.Transaction;
import std.algorithm;

scope utxo_set = new MemoryUTXOSet;
auto getPenaltyDeposit = (Hash utxo)
{
    UTXO val;
    return utxo_set.peekUTXO(utxo, val) ? 10_000.coins : 0.coins;
};
KeyPair key_pair = WK.Keys.A;

genesisSpendable().map!(txb => txb.refund(key_pair.address).sign(OutputType.Freeze))
    .each!(tx => utxo_set.put(tx));
Hash[] utxo_hashes = utxo_set.keys;

// create an EnrollmentManager object
auto params = new immutable(ConsensusParams)(10);
auto man = new EnrollmentManager(key_pair, params);

// check the return value of `getEnrollmentPublicKey`
assert(key_pair.address == man.getEnrollmentPublicKey());

// first enrollment succeeds
auto enroll = man.createEnrollment(utxo_hashes[0], Height(1));
assert(man.addEnrollment(enroll, key_pair.address, Height(1),
        &utxo_set.peekUTXO, getPenaltyDeposit));

// adding first enrollment succeeds
assert(man.addValidator(enroll, key_pair.address, Height(1),
        &utxo_set.peekUTXO, getPenaltyDeposit, utxo_set.storage) is null);

// second enrollment with the same public key fails
auto enroll2 = man.createEnrollment(utxo_hashes[1], Height(1));
assert(!man.addEnrollment(enroll2, key_pair.address, Height(1),
        &utxo_set.peekUTXO, getPenaltyDeposit));

// adding second enrollment with the same public key fails
assert(man.addValidator(enroll2, key_pair.address, Height(1),
        &utxo_set.peekUTXO, getPenaltyDeposit, utxo_set.storage) !is null);