Proof-of-Time
This specification defines a secure proof-of-time randomness beacon construction for Farming sub-protocol of the Dilithium consensus and bundle proposer election among operators.
Terminology
Timekeeper
Timekeepers are the nodes that run the evaluation of the Proof-of-Time chain and gossip their outputs to other nodes. Timekeepers are expected to dedicate a full CPU core to Proof-of-Time.
Node
A Node or Consensus Node (unless otherwise noted as Timekeeper) is a PoT client node that does not run the PoT evaluation component. It only consumes PoT messages from Timekeepers or the blockchain.
Primitives
We construct a PoT function by iterating AES-128 a large number of times. The prover evaluation latency for the fastest available hardware platform is calculated as the number of iterations times the latency for a single iteration of AES-128 on that hardware platform. We may then tune the number of iterations based on the lowest-latency hardware platform at the time to guarantee the minimum wall-clock time required for anyone to evaluate the PoT.
Public Constants
BLOCK_AUTHORING_DELAY
: number of slots between when the new global randomness value is revealed at slott
and when the farmer can claim block with that randomness at slott + BLOCK_AUTHORING_DELAY
, currently set to the maximum time needed to produce a PoS solution (4 slots, ~4 seconds)POT_ENTROPY_INJECTION_INTERVAL
: number of blocks after which the proof-of-time chain input is updated with consensus chain entropy, currently 50 blocks, roughly equal to ~5 minutes at the expected block production rate
POT_ENTROPY_INJECTION_LOOKBACK_DEPTH
: depth at which the injected block is taken, measured in multiples ofPOT_ENTROPY_INJECTION_INTERVAL
s, currently2
(equal to 100 blocks, the archiving depth)POT_ENTROPY_INJECTION_DELAY
: number of slots between when the injection condition is met at slott
and when the injection into the PoT chain happens at slott + POT_ENTROPY_INJECTION_DELAY
, currently equal to 15 slotsNUM_CHECKPOINTS
: number of checkpoint values for the proof-of-time verification published in 1 slot, currently 8EXPECTED_POT_VERIFICATION_SPEEDUP
: expected speed-up of PoT verification to evaluation, currently 7. Used by nodes to decide whether to attempt to verify or evaluate locally under a potential DoS attack.
Parameters
pot_slot_iterations
: number of iterations of PoT function required per single time slot. Should be divisible by2 * NUM_CHECKPOINTS
APIs
prove
prove(seed, pot_iterations)
→ PotCheckpoints
Takes a seed
and number of iterations pot_iterations
and repeatedly evaluates AES-128 for a total of pot_iterations
times. The final output will contain NUM_CHECKPOINTS
results of AES-128 encryption of checkpoint_iterations
each.
- Set
input = seed
- Set
key = hash(seed)
truncated to 128 bits - Set the number of iterations of PoT function required per one checkpoint
checkpoint_iterations = pot_iterations / NUM_CHECKPOINTS
- For
_
in0..NUM_CHECKPOINTS
:- Run evaluation of
output = aes_encrypt(key, input)
forcheckpoint_iterations
iterations. - Append the
output
value tocheckpoints
- Set
input = output
. Thekey
stays the same.
- Run evaluation of
- Output
checkpoints
output
output(checkpoints: PotCheckpoints)
→ PotOutput
Gives the last value in the PoT evaluation result.
Implemented as checkpoints[NUM_CHECKPOINTS-1]
derive_global_randomness
derive_global_randomness(pot_output: PotOutput)
→ global_randomness
Derive global randomness for use on the consensus chain from the output of PoT evaluation.
Implemented as hash(pot_output)
verify
verify(seed, iterations, checkpoints)
→ bool
Verifies that the proof_of_time
was computed correctly for pot_iterations
number of times.
-
Check that
iterations
are divisible by2 * checkpoints.len()
-
Set
checkpoint_iterations = iterations / checkpoints.len()
-
Set
key = hash(seed)
-
Iterate through
checkpoints
inproof_of_time
in steps of 2. The first checkpoint isseed
. -
For each pair of
checkpoints[i]
andcheckpoints[i+1]
run:- Loop evaluation of
aes_encrypt(key, checkpoints[i])
encryption routine forcheckpoint_iterations/2
iterations. - Loop evaluation of
aes_decrypt(key, checkpoint[i+1])
decryption routine forcheckpoint_iterations/2
iterations.
- Loop evaluation of
-
If all pairs are correct, return
True
, otherwise, returnFalse
Building the PoT chain
The PoT evaluation is continuously running since genesis, and all its outputs should form a chain.
Each node on the network keeps a local view of the recent PoT chain that extends the PoT chain of the consensus chain they follow. All nodes perpetually try to extend their PoT chain as high as possible.
There are three ways a node may get the proofs to extend its local view of the PoT chain:
- Blockchain: Proofs included in blocks of the consensus chain
- Gossip: Proofs received from other nodes on the network
- Evaluation: Proofs computed locally (common case for Timekeepers)
- When a node comes online (for the first time or otherwise), it should perform a major sync of the consensus chain (Consensus Chain sync), if needed (if the best block tip is behind the chain tip), and get the latest “committed” PoT from the newest block header.
- If the node previously held a local view of the PoT tree, which is no longer consistent with the PoT synced consensus chain (i.e., proofs are outdated or orphaned), discard the outdated view and reorg the tip of the PoT chain and restart Timekeeper to build on top of this new tip.
- Listen to gossip for PoT messages newer than the last PoT included on the consensus chain.
- Process the received gossip PoT proofs and extend the local PoT chain with each new slot proof verified as per PoT Gossip Processing.
- As soon as local best proof is up-to-date with observed gossip, Timekeepers should start evaluating on top of it.
- The
slot_number
in the best valid proof known to this node becomes the tip of the PoT chain in the view of this node for all intents and purposes.
Timekeeper Algorithms
Timekeepers run PoT evaluation for a specified number of iterations for the next slot from the best_proof
at each time slot. The best_proof
is the latest PoT value considered valid by this Timekeeper.
Slot Inputs
At the consensus chain genesis, the Timekeepers must start the PoT chain evaluation before block production.
Slot Number
The genesis slot_number
is set to pot_genesis_slot = 0
.
After each Evaluation is computed the slot_number
is increased by one.
Seed
The genesis seed
value is set to hash(genesis_block_hash || external_entropy)
. The external_entropy
should be set to a common value for all nodes (i.e., the last Bitcoin block hash before mainnet T (like Spacemesh) to ensure no one has a headstart in PoT computation provided via CLI.
For subsequent slots (with slot_number > 0
), there are two cases for seed value:
seed = best_proof.output()
- or
seed = hash(entropy||tip.output)
ifslot_number == injection_slot
(see: Entropy Injection).
Slot Iterations
Each slot requires pot_slot_iterations
defined in the genesis config.
Occasionally, we may update the number of iterations per slot, which will take effect at the nearest injection slot. All subsequent slots will be evaluated and verified with respect to the updated number of iterations.
Evaluation
- Compute
checkpoints = prove(seed, pot_slot_iterations)
- Gossip the
ProofOfTime:{slot_number, seed, pot_slot_iterations, checkpoints}
(definition) to other nodes.
Note for Randomness Updates: The global_randomness
that farmers will use for the slot next_slot
is computed as derive_global_randomness(new_proof_of_time.output())
Consensus Chain Entropy Injection into PoT Randomness
Entropy from the consensus chain is injected into the PoT chain every POT_ENTROPY_INJECTION_INTERVAL
blocks, starting from block number POT_ENTROPY_INJECTION_INTERVAL * (POT_ENTROPY_INJECTION_LOOKBACK_DEPTH + 1)
since genesis:
- Entropy Sourcing
- For every block where
block_number % POT_ENTROPY_INJECTION_INTERVAL == 0
, derive entropy asentropy = hash(chunk || proof_of_time)
, wherechunk
is the winning PoS solution chunk andproof_of_time
is the PoT output of the slot claimed by this block. - Store the
entropy
associated withblock_number
and undefined (for now) futureinjection_slot
at which it will be injected into the PoT chain.
- For every block where
- Setting Injection Slot
- Set
injection_slot = slot + POT_INJECTION_DELAY
for entropy entry associated with blockblock_number - POT_ENTROPY_INJECTION_INTERVAL * POT_ENTROPY_INJECTION_LOOKBACK_DEPTH
, whereslot
corresponds to the slot claimed by current blockblock_number
. - If no such entry exists because the subtraction from
block_number
underflows or hits genesis block number 0), no injection needs to be done.
- Set
- Entropy Injection:
- When PoT evaluation reaches
injection_slot
, inject the entropy value into the seed as specified. - Once a block is built on a slot higher than
injection_slot
, the corresponding entropy entry can be erased as no longer needed.
- When PoT evaluation reaches
PoT Gossipping
The gossip mechanism that we already have from Substrate can be used for message propagation of encoded ProofOfTime
.
Timekeeper nodes that compute PoT, gossip the outputs to their peers.
Each PoT output is 160 bytes, so propagating them should be relatively lightweight and not significantly burden the network. PoT messages are unsigned and canonical.
The proof-of-time sent by Timekeepers to their peers should contain the following fields:
struct ProofOfTime {
// version of PoT used, necessary to potentially replace AES with VDF
V0 {
slot_number: u64
// input into PoT evaluation function
seed: [u8;16],
// iterations computed for this slot
slot_iterations: u64,
// NUM_CHECKPOINTS intermediate evaluations (including output)
checkpoints: Vec<[u8;16]>
}
}
All nodes will verify before re-gossiping and as a result propagation to whole network takes some time.
PoT Gossip Processing
Upon receiving a new gossiped proof proof_of_time
, all up-to-date nodes:
- Verify that its
slot_number
is not older than that of thebest_proof
verified by this node. If it is, ignore. - If the node has already seen and gossiped the same
proof_of_time
, do not re-gossip it. - If the node already has a valid PoT for this
slot_number
and- Newly received one has a different set of
proof_of_time.checkpoints
for the same inputsseed
andslot_iterations
they ignore it and ban the peer that sent it - if
seed
orslot_iterations
is different we assume that the proof might be coming from a fork and simply ignore it without banning the peer
- Newly received one has a different set of
- Verify that its
slot_number
not too far in the future .(Exact value TBD, suggested +10-15) - If the node doesn’t have a valid PoT for
slot_number-1
, they should save theproof_of_time
in a tree and wait until they receive and verify the previous PoTs before proceeding. - If the node already has a valid PoT for
slot_number-1
, they can verify the received proofs forslot_number
:- For all proofs for this slot, verify that the slot inputs (
seed
andslot_iterations
) match the expected as defined in Slot Inputs accounting for eventual injection and iteration updates if necessary. Discard the proofs with incorrect inputs. - Let
num_proofs
be the number of proofs remaining for this slot after the input check. - If
num_proofs < EXPECTED_POT_VERIFICATION_SPEEDUP
, attempt to find the correct proof by verifying the checkpoints were computed correctly withverify(seed, slot_iterations, checkpoints)
for each proof, starting with proofs sent by most reputable peer.- If a proof validates successfully, forward PoT message to their peers and proceed with the audit and solving.
- Ban the peers who sent all other proofs.
- Else, if
num_proofs >= EXPECTED_POT_VERIFICATION_SPEEDUP
, suspect an attack is underway:- Verify checkpoints from the peer with the best reputation with
verify(seed, slot_iterations, checkpoints)
- If verification fails, fall back to local evaluation for this slot with
local_checkpoints = prove(seed, slot_iterations)
. Comparelocal_checkpoints
to all the received proofs for this slot and ban the peers who sent wrong proofs. Gossip the proof withlocal_checkpoints
.
- Verify checkpoints from the peer with the best reputation with
- For all proofs for this slot, verify that the slot inputs (
Farming
-
The farmer tracks slots based on PoT chain. The last valid proof output they receive is used to derive latest randomness to then derive challenge for audit.
-
For every
slot_number
farmer audits their plot as described in Audit for potential solutions for all as far into the future as revealed randomness allows based on the global randomness they have received from the node so far. -
If they win a challenge, they start solving and proving it, as described in Proving, to later claim the slot and produce a block with the precomputed solution.
-
In the block, the farmer should include the output of proof-of-time at the slot they claim for verifiability.
To keep the header minimal size, only the outputs of claimed slot
x
andx + BLOCK_AUTHORING_DELAY
are included in block header pre-digest items. The output of claimed slotx
is sufficient to verify PoS solution.The
BLOCK_AUTHORING_DELAY
is required to make sure faster farmers (farmers that can produce PoS faster due to faster SSD and/or CPU) do not get advantage in the race to produce a block.All checkpoints of all slots passed since the parent block’s future proof of time (if any) up to
slot_number + BLOCK_AUTHORING_DELAY
of current block are included in justifications to allow more parallelism during PoT verification. Essentially all checkpoints that were not yet seen on chain must be included and subsequently archived as part of the block.
Consensus Chain sync
Major Sync
(Extends Synchronization)
Since PoT is currently costly to verify, it makes verifying the blocks costly too. Even more so if a node has to sync a significant amount of blocks.
When syncing, during block import verification, the node generally verifies everything except PoT.
- During verification of an individual block (stateless part of the process that happens before block is imported)
block_number
, make an on the spot decision whether to verify PoT for this block depending on distance fromtarget
withshould_verify_pot(block_number, target)
- If
true
, fully verify PoT of this block. Otherwise, skip PoT verification. - If the verification passes, the block’s PoT is valid.
- If the verification fails, reject the block and the whole fork as invalid. Seek to sync on another fork.
should_verify_pot(block_number, target)
→ bool
Depending on how far block_number
is from the target
we define a few threshold for number of blocks verified. Let diff = target - block_number
- For
diff ≤ 1581
blocks, returntrue
to verify all blocks - for
diff ≤ 6234
blocks, verifysample_size = 1581
blocks - for
diff ≤ 63240
blocks, verifysample_size = 3162*(diff-3162)/(diff-1)
blocks - for
diff ≤ 3 162 000
blocks, verifysample_size = 3162
blocks - for
diff > 3 162 000
blocks, verifysample_size = diff/1000
blocks
Generate a random number n
in the range 0..=(diff)
and if n<sample_size
return true
, otherwise return false
.
New Blocks
(Extends Verification)
When a new block is received, in addition to PoS and consensus log checks, compare the PoT values in the header to the local view of the PoT chain. If the proof-of-time included in the block header covers local proofs that have already been verified, the block’s PoT passes validation.
If the proof-of-time is not consistent with local view or the local view is missing some required slots — do necessary verification, including proving.