Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions kip-0006.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,16 @@ The consensus state of a Kaspa node includes a list of selected chain block head

Currently, posterity headers are taken from blocks used as pruning blocks, and a posterity header is stored once every 24 hours, whereby they are also commonly referred to as *pruning headers*. Later in this proposal I consider decoupling pruning headers from posterity headers, though I propose to delay this step to a separate update (mainly due to engineering complexity considerations). That being said, I currently disregard the pruning motivation and refer to these headers as *posterity headers* for convenience.

Given a block ``B`` let ``posterity(B)`` be the *earliest* posterity header such that ``B ∈ Past(posterity(B))``, or ``null`` if such a stored header does not exist yet. Let ``posterity_depth(B)`` output the integer ``n`` satisfying ``B=parent(posterity(B),n)``.
Given a block ``B`` let ``next_posterity(B)`` be the *earliest* posterity header such that ``B ∈ Past(next_posterity(B))``, or ``null`` if such a stored header does not exist yet. Let ``posterity_depth(B)`` output the integer ``n`` satisfying ``B=parent(next_posterity(B),n)``.

For any block ``B`` let ``next_posterity(B)`` be the block header with the following property: if ``B`` is a posterity header, then ``next_posterity(B)`` is the next posterity header. Note that this is well-defined even if ``B`` is not a posterity header.
For any block ``B`` let ``prev_posterity(B)`` be the block header with the following property: if ``B`` is a posterity header, then ``prev_posterity(B)`` is the next posterity header. Note that this is well-defined even if ``B`` is not a posterity header.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still ambiguous, should possibly be the " the latest posterity header in B's past".

The note makes no sense to me. this should be defined in a way that disregards whether B was posterity or not


Posterity headers have the following important properties:
* They are determined in consensus. That is, all nodes store the same posterity headers.
* They are headers of blocks in the *selected chain*.
* Each block ``B`` contains a pointer to ``posterity(posterity(posterity(B)))``. Hence, the chain of posterity headers is verifiable all the way down to genesis. In particular, obtaining and verifying the posterity chain is part of the process of syncing a new node.
* Each block ``B`` contains a pointer to ``prev_posterity(prev_posterity(prev_posterity(B)))``. Hence, the chain of posterity headers is verifiable all the way down to genesis. In particular, obtaining and verifying the posterity chain is part of the process of syncing a new node.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this accurate?


The reason that ``B`` points to ``posterity(posterity(posterity(B)))`` and not is ``posterity(B)`` that the original motivation for storing these blocks comes from the pruning mechanism, where these depth-3 pointers are required (for reasons outside the scope of the current discussion).
The reason that ``B`` points to ``prev_posterity(prev_posterity(prev_posterity(B)))`` and not is ``next_posterity(B)`` that the original motivation for storing these blocks comes from the pruning mechanism, where these depth-3 pointers are required (for reasons outside the scope of the current discussion).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this sentence as a whole should be rewritten or removed.


# Accepted Transactions Merkle Root (ATMR)

Expand All @@ -69,11 +69,11 @@ The first item is a straightforward Merkle proof. However, the second item is tr

# PoChM Without Hard-Fork

Currently, the most straightforward way to construct a PoChM for ``B`` is to store the entire set ``Future(B) ∩ Past(posterity(B))``. The result is a "diamond shaped" DAG whose top block is ``posterity(B)`` and bottom block is ``B``. Given a DAG of this shape as proof, any node could verify that the top block is a posterity block, and that following the selected parent from the top block leads to the bottom block.
Currently, the most straightforward way to construct a PoChM for ``B`` is to store the entire set ``Future(B) ∩ Past(next_posterity(B))``. The result is a "diamond shaped" DAG whose top block is ``next_posterity(B)`` and bottom block is ``B``. Given a DAG of this shape as proof, any node could verify that the top block is a posterity block, and that following the selected parent from the top block leads to the bottom block.

The problem with this proof is its size. In the worst case, it would be about as large as 24 hours worth of headers. At the current 1BPS and header size of 248 bytes, this sums to about 24 megabytes.

Remark: This could be improved slightly by, instead of storing the entire set, only storing the headers of the selected chain blocks and their parents. This data suffices to compute the selected parent of each selected chain block and validate the proof. However, this does not seem to improve the size by much. Also note that proofs for many transactions that were accepted in chain blocks with the same ``posterity`` can be aggregated. In particular, a PoChM for a block ``B`` is also a PoChM for any chain block ``C ∈ Future(B) ∩ Past(posterity(B))``
Remark: This could be improved slightly by, instead of storing the entire set, only storing the headers of the selected chain blocks and their parents. This data suffices to compute the selected parent of each selected chain block and validate the proof. However, this does not seem to improve the size by much. Also note that proofs for many transactions that were accepted in chain blocks with the same ``posterity`` can be aggregated. In particular, a PoChM for a block ``B`` is also a PoChM for any chain block ``C ∈ Future(B) ∩ Past(next_posterity(B))``

Remark: Note that a PoP does not actually require the entire PoChM. It suffices to provide a chain of headers going from any posterity block to any block merging ``txn``. Since in PoP we don't care whether ``txn`` was eventually accepted, we are not concerned about whether this chain ever strays from the selected chain. However, the improvement is only by a constant factor (the ratio of chain blocks to total blocks), which is currently estimated to be a factor of around two.

Expand All @@ -93,15 +93,15 @@ We will provide non-asymptotic bounds after specifying the solution. For now we

# Our Proposal

The block header will contain a new field called the *PoChM Merkle root* (PMR) defined as follows: let k be the least integer such that ``parent(B,2^k) ∈ Past(next_posterity(B))``, then PMR is the root of the Merkle tree containing the headers ``parent(B,2^i)`` for ``i = 0,...,k-1``.
The block header will contain a new field called the *PoChM Merkle root* (PMR) defined as follows: let k be the least integer such that ``parent(B,2^k) ∈ Past(prev_posterity(B))``, then PMR is the root of the Merkle tree containing the headers ``parent(B,2^i)`` for ``i = 0,...,k-1``.

Let ``PMR(B,i)`` be the function that outputs a Merkle proof that ``hash(parent(B,2^i))`` is in the tree whose root is the PMR of B.

The process of header validation of chain block candidates will include verifying the PMR. I propose that the PMR will not be validated for blocks that are not chain candidates. In particular, a block whose PMR is invalid but is otherwise valid will remain in the DAG but will be disqualified from being a selected tip/parent. A similar approach is employed e.g. when validating UTXO commitments or checking that the block does not contain double spends. (A crucial subtlety is that, eventually, the selected parent of a block is *always* the parent with the highest blue accumulated work (BAW). If while validating the selected parent the block turns out to be disqualified, then the pointer to this block is *removed*. This rule allows us to read the selected parent of any block from the headers of its parents alone, without worrying about the parent with the highest BAW is disqualified for some external reason that requires additional data to notice).

The procedure for generating a *PoChM* for a block ``B`` is as follows:

Let C = posterity(B)
Let C = next_posterity(B)
If C=null:
Return error
Let d = posterity_depth(B)
Expand All @@ -118,12 +118,12 @@ The procedure for generating a *PoChM* for a block ``B`` is as follows:

To understand how to validate the proof we first consider it in two simple cases:

If there is some i such that ``posterity_depth(B) = 2^i`` then ``B`` itself is a member of the PMR of ``posterity(B)`` and the entire PoChM is a single Merkle proof.
If there is some i such that ``posterity_depth(B) = 2^i`` then ``B`` itself is a member of the PMR of ``next_posterity(B)`` and the entire PoChM is a single Merkle proof.

If there are some i>j such that ``posterity_depth(B) = 2^i + 2^j`` then the proof would contain three items:
* A Merkle proof that ``hash(parent(posterity(B),2^i))`` is in the PMR of ``posterity(B)``
* The header ``parent(posterity(B),2^i)`` (that in particular includes its PMR)
* A Merkle proof that ``hash(B)`` is in the PMR of ``parent(posterity(B),2^i)``
* A Merkle proof that ``hash(parent(next_posterity(B),2^i))`` is in the PMR of ``next_posterity(B)``
* The header ``parent(next_posterity(B),2^i)`` (that in particular includes its PMR)
* A Merkle proof that ``hash(B)`` is in the PMR of ``parent(next_posterity(B),2^i)``

By verifying the proofs and hashes above, one verifies that ``B`` is indeed a chain block. The general procedure extends similarly.

Expand All @@ -150,9 +150,9 @@ I recommend first implementing the current KIP using the current posterity/pruni

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 139: the ATMR does not actually contain the transactions of the corresponding block, rather only the accepted txs of its mergeset. These makes for a subtler concept where it is proven that a tx was accepted according to some block at some point, but maybe not according to the selected chain.
Maybe this concept has merits of itself but I don't see them as of now and surely the intention was not for it - so it should be changed to "merkle proof that txn is in C's block transactions" or something of this line, and the section on atmr should also change accordingly.

# Size of PoChM

Computing the actual size of a PoChM is a bit tricky, as it depends on the number of *chain* blocks between ``C`` and ``next_posterity(C)`` for several blocks ``C``. Buy staring at the KGI for sufficiently long, one can get convinced that the selected chain grows by about one block every two seconds (that is, in 1BPS we see that about half of the blocks are chain blocks). To provide a reliable upper bound, I will assume that the selected chain grows at a rate of about 1 block per second. Note that the growth of the selected chain is not governed by block rates, but by network conditions. Hence, I assume this holds with overwhelming probability for any block rate. This might not hold if network conditions improve greatly. However, we'll soon see that the growth asymptotics (as a function of the number of chain blocks between two consecutive posterity blocks) are a very mild log*loglog, whereby this is hardly a concern. I will demonstrate this with concrete numbers after we obtain an expression for the size of a PoChM.
Computing the actual size of a PoChM is a bit tricky, as it depends on the number of *chain* blocks between ``C`` and ``prev_posterity(C)`` for several blocks ``C``. Buy staring at the KGI for sufficiently long, one can get convinced that the selected chain grows by about one block every two seconds (that is, in 1BPS we see that about half of the blocks are chain blocks). To provide a reliable upper bound, I will assume that the selected chain grows at a rate of about 1 block per second. Note that the growth of the selected chain is not governed by block rates, but by network conditions. Hence, I assume this holds with overwhelming probability for any block rate. This might not hold if network conditions improve greatly. However, we'll soon see that the growth asymptotics (as a function of the number of chain blocks between two consecutive posterity blocks) are a very mild log*loglog, whereby this is hardly a concern. I will demonstrate this with concrete numbers after we obtain an expression for the size of a PoChM.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this particular instance it should have stayed next_posterity


Let ``L_e`` and ``L_a`` denote the size of a header and a hash, respectively. Let ``N`` be the number of seconds between two consecutive posterity blocks. For a block ``B`` let ``|B|`` denote the Hamming weight of the binary representation of ``posterity_depth(B)``. It follows that a PaChM for ``B`` contains ``|B|`` Merkle proofs and ``|B|-1`` headers (in particular, if ``B=posterity(B)`` (equivalently ``posterity_depth(B)=0``) then ``|B|=0``, and indeed the "proof" is empty, since the consensus data itself proves that ``B`` is a chain block).
Let ``L_e`` and ``L_a`` denote the size of a header and a hash, respectively. Let ``N`` be the number of seconds between two consecutive posterity blocks. For a block ``B`` let ``|B|`` denote the Hamming weight of the binary representation of ``posterity_depth(B)``. It follows that a PaChM for ``B`` contains ``|B|`` Merkle proofs and ``|B|-1`` headers (in particular, if ``B=next_posterity(B)`` (equivalently ``posterity_depth(B)=0``) then ``|B|=0``, and indeed the "proof" is empty, since the consensus data itself proves that ``B`` is a chain block).

The size of each merkle proof is ``(2*log(logN))+1)*L_a``, so the total size of the PaChM is ``(2*log(logN))+1)*L_a|B| + L_e*(|B|-1)``. In the worst case, we have that ``|B| = log(N)`` so we obtain a bound of ``(2*log(logN))+1)*logN*L_a + L_e*(logN-1)``. Assuming ``L_e=32 Bytes`` and ``L_a=280 Bytes`` and ``N=86400`` (that is, that a posterity block is sampled once every 24 hours), this comes up to 9 kilobytes.

Expand Down