Skip to content

Commit

Permalink
proof: Add introspection into the ephemeral node (#15)
Browse files Browse the repository at this point in the history
Examples of when information about the ephemeral node can be used:

 - Storage engines for Merkle trees that contains ephemeral nodes as
   well as immutable ones. In this case Ephem can be used to replace
   IDs[begin:end] before making a query, and then Rehash is not
   necessary.
 - Merkle tree drawing tools. The ephemeral node needs to be displayed,
   and sometimes with a style distinct from other nodes.
 - Code that needs to extract individual hashes from inclusion and
   consistency proofs. The Nodes type, with the introduction of Ephem
   method, provides a complete reflection of the proof.
  • Loading branch information
pav-kv authored Apr 1, 2022
1 parent 63fa71a commit 3a5b1d7
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 1 deletion.
16 changes: 15 additions & 1 deletion proof/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ type Nodes struct {
// end is the ending (exclusive) index into the IDs[begin:end] subslice of
// the nodes which will be used to re-create the ephemeral node.
end int
// ephem is the ID of the ephemeral node in the proof. This node is a common
// ancestor of all nodes in IDs[begin:end]. It is the node that otherwise
// would have been used in the proof if the tree was perfect.
ephem compact.NodeID
}

// Inclusion returns the information on how to fetch and construct an inclusion
Expand Down Expand Up @@ -129,7 +133,17 @@ func nodes(index uint64, level uint, size uint64) Nodes {
len1, len2 = 0, 0
}

return Nodes{IDs: nodes, begin: len1, end: len2}
return Nodes{IDs: nodes, begin: len1, end: len2, ephem: node.Sibling()}
}

// Ephem returns the ephemeral node, and indices begin and end, such that
// IDs[begin:end] slice contains the child nodes of the ephemeral node.
//
// The list is empty iff there are no ephemeral nodes in the proof. Some
// examples of when this can happen: a proof in a perfect tree; an inclusion
// proof for a leaf in a perfect subtree at the right edge of the tree.
func (n Nodes) Ephem() (compact.NodeID, int, int) {
return n.ephem, n.begin, n.end
}

// Rehash computes the proof based on the slice of node hashes corresponding to
Expand Down
50 changes: 50 additions & 0 deletions proof/proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func TestInclusion(t *testing.T) {
} else if err != nil {
t.Fatalf("Inclusion: %v", err)
}
// Ignore the ephemeral node, it is tested separately.
proof.ephem = compact.NodeID{}
if diff := cmp.Diff(tc.want, proof, cmp.AllowUnexported(Nodes{})); diff != "" {
t.Errorf("paths mismatch:\n%v", diff)
}
Expand Down Expand Up @@ -226,6 +228,8 @@ func TestConsistency(t *testing.T) {
} else if err != nil {
t.Fatalf("Consistency: %v", err)
}
// Ignore the ephemeral node, it is tested separately.
proof.ephem = compact.NodeID{}
if diff := cmp.Diff(tc.want, proof, cmp.AllowUnexported(Nodes{})); diff != "" {
t.Errorf("paths mismatch:\n%v", diff)
}
Expand Down Expand Up @@ -255,6 +259,52 @@ func TestConsistencySucceedsUpToTreeSize(t *testing.T) {
}
}

func TestEphem(t *testing.T) {
id := compact.NewNodeID
for _, tc := range []struct {
index uint64
size uint64
want compact.NodeID
}{
// Edge case: For perfect trees the ephemeral node is the sibling of the
// root. However, it will not be used in the proof, as the corresponding
// subtree is empty.
{index: 3, size: 32, want: id(5, 1)},

{index: 0, size: 9, want: id(3, 1)},
{index: 0, size: 13, want: id(3, 1)},
{index: 7, size: 13, want: id(3, 1)},
{index: 8, size: 13, want: id(2, 3)},
{index: 11, size: 13, want: id(2, 3)},
// More edge cases when the computed ephemeral node is not used in the
// proof, because it is fully outside the tree border.
{index: 12, size: 13, want: id(0, 13)},
{index: 13, size: 14, want: id(1, 7)},

// There is only one node (level 0, index 1024) in the right subtree, but
// the ephemeral node is at level 10 rather then level 0. This is because
// for the purposes of the proof this node is *effectively* at level 10.
{index: 123, size: 1025, want: id(10, 1)},

{index: 0, size: 0xFFFF, want: id(15, 1)},
{index: 0xF000, size: 0xFFFF, want: id(11, 0x1F)},
{index: 0xFF00, size: 0xFFFF, want: id(7, 0x1FF)},
{index: 0xFFF0, size: 0xFFFF, want: id(3, 0x1FFF)},
{index: 0xFFFF - 1, size: 0xFFFF, want: id(0, 0xFFFF)},
} {
t.Run(fmt.Sprintf("%d:%d", tc.index, tc.size), func(t *testing.T) {
nodes, err := Inclusion(tc.index, tc.size)
if err != nil {
t.Fatalf("Inclusion: %v", err)
}
got, _, _ := nodes.Ephem()
if want := tc.want; got != want {
t.Errorf("Ephem: got %+v, want %+v", got, want)
}
})
}
}

func TestRehash(t *testing.T) {
th := rfc6962.DefaultHasher
h := [][]byte{
Expand Down

0 comments on commit 3a5b1d7

Please sign in to comment.