Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement get-compact-range using RFC 6962 methods #18

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
7 changes: 7 additions & 0 deletions exp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Experimental
------------

This directory contains a Go module with experimental features not included into
the main Go module of this repository. These must be used with caution.

The idea of this module is similar to Go's https://pkg.go.dev/golang.org/x/exp.
153 changes: 153 additions & 0 deletions exp/get_compact_range.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright 2022 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package merkle

import (
"fmt"

"github.com/transparency-dev/merkle/compact"
"github.com/transparency-dev/merkle/proof"
)

type HashGetter interface {
GetConsistencyProof(first, second uint64) ([][]byte, error)
GetLeafHashes(begin, end uint64) ([][]byte, error)
}

func GetCompactRange(rf *compact.RangeFactory, begin, end, size uint64, hg HashGetter) (*compact.Range, error) {
if begin > size || end > size {
return nil, fmt.Errorf("[%d, %d) out of range in %d", begin, end, size)
}
if begin >= end {
return rf.NewEmptyRange(begin), nil
}

if size <= 3 || end == 1 {
hashes, err := hg.GetLeafHashes(begin, end)
if err != nil {
return nil, fmt.Errorf("GetLeafHashes(%d, %d): %v", begin, end, err)
}
if got, want := uint64(len(hashes)), end-begin; got != want {
return nil, fmt.Errorf("GetLeafHashes(%d, %d): %d hashes, want %d", begin, end, got, want)
}
r := rf.NewEmptyRange(begin)
for _, h := range hashes {
if err := r.Append(h, nil); err != nil {
return nil, fmt.Errorf("Append: %v", err)
}
}
return r, nil
}
// size >= 4 && end >= 2

known := make(map[compact.NodeID][]byte)

store := func(nodes proof.Nodes, hashes [][]byte) error {
_, b, e := nodes.Ephem()
wantSize := len(nodes.IDs) - (e - b)
if b != e {
wantSize++
}
if got := len(hashes); got != wantSize {
return fmt.Errorf("proof size mismatch: got %d, want %d", got, wantSize)
}

idx := 0
for _, hash := range hashes {
if idx == b && b+1 < e {
idx = e - 1
continue
}
known[nodes.IDs[idx]] = hash
idx++
}
return nil
}

newRange := func(begin, end uint64) (*compact.Range, error) {
size := compact.RangeSize(begin, end)
ids := compact.RangeNodes(begin, end, make([]compact.NodeID, 0, size))
hashes := make([][]byte, 0, len(ids))
for _, id := range ids {
if hash, ok := known[id]; ok {
hashes = append(hashes, hash)
} else {
return nil, fmt.Errorf("hash not known: %+v", id)
}
}
return rf.NewRange(begin, end, hashes)
}

fetch := func(first, second uint64) error {
nodes, err := proof.Consistency(first, second)
if err != nil {
return fmt.Errorf("proof.Consistency: %v", err)
}
hashes, err := hg.GetConsistencyProof(first, second)
if err != nil {
return fmt.Errorf("GetConsistencyProof(%d, %d): %v", first, second, err)
}
store(nodes, hashes)
return nil
}

mid, _ := compact.Decompose(begin, end)
mid += begin
if err := fetch(begin, mid); err != nil {
return nil, err
}

if begin == 0 && end == 2 || end == 3 {
if err := fetch(3, 4); err != nil {
return nil, err
}
}
if end <= 3 {
return newRange(begin, end)
}
// end >= 4

if (end-1)&(end-2) != 0 { // end-1 is not a power of 2.
if err := fetch(end-1, end); err != nil {
return nil, err
}
r, err := newRange(begin, end-1)
if err != nil {
return nil, err
}
if err := r.Append(known[compact.NewNodeID(0, end-1)], nil); err != nil {
return nil, fmt.Errorf("Append: %v", err)
}
return r, nil
}

// At this point: end >= 4, end-1 is a power of 2; thus, end-2 is not a power of 2.
if err := fetch(end-2, end); err != nil {
return nil, err
}
r := rf.NewEmptyRange(begin)
if end-2 > begin {
var err error
if r, err = newRange(begin, end-2); err != nil {
return nil, err
}
}
for index := r.End(); index < end; index++ {
if err := r.Append(known[compact.NewNodeID(0, index)], nil); err != nil {
return nil, fmt.Errorf("Append: %v", err)
}
}
return r, nil
}
130 changes: 130 additions & 0 deletions exp/get_compact_range_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2022 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package merkle_test

import (
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/transparency-dev/merkle"
"github.com/transparency-dev/merkle/compact"
"github.com/transparency-dev/merkle/proof"
)

func TestGetCompactRange(t *testing.T) {
rf := compact.RangeFactory{Hash: func(left, right []byte) []byte {
return append(append(make([]byte, 0, len(left)+len(right)), left...), right...)
}}
tr := newTree(t, 256, &rf)

test := func(begin, end, size uint64) {
t.Run(fmt.Sprintf("%d:%d_%d", size, begin, end), func(t *testing.T) {
got, err := merkle.GetCompactRange(&rf, begin, end, size, tr)
if err != nil {
t.Fatalf("GetCompactRange: %v", err)
}
want, err := tr.getCompactRange(begin, end)
if err != nil {
t.Fatalf("GetCompactRange: %v", err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("Diff: %s", diff)
}
})
}

for begin := uint64(0); begin <= tr.size; begin++ {
for end := begin; end <= tr.size; end++ {
for size := end; size < end+5 && size < tr.size; size++ {
test(begin, end, size)
}
test(begin, end, tr.size)
}
}
}

type tree struct {
rf *compact.RangeFactory
size uint64
nodes map[compact.NodeID][]byte
}

func newTree(t *testing.T, size uint64, rf *compact.RangeFactory) *tree {
hash := func(leaf uint64) []byte {
if leaf >= 256 {
t.Fatalf("leaf %d not supported in this test", leaf)
}
return []byte{byte(leaf)}
}

nodes := make(map[compact.NodeID][]byte, size*2-1)
r := rf.NewEmptyRange(0)
for i := uint64(0); i < size; i++ {
nodes[compact.NewNodeID(0, i)] = hash(i)
if err := r.Append(hash(i), func(id compact.NodeID, hash []byte) {
nodes[id] = hash
}); err != nil {
t.Fatalf("Append: %v", err)
}
}
return &tree{rf: rf, size: size, nodes: nodes}
}

func (t *tree) GetConsistencyProof(first, second uint64) ([][]byte, error) {
if first > t.size || second > t.size {
return nil, fmt.Errorf("%d or %d is beyond %d", first, second, t.size)
}
nodes, err := proof.Consistency(first, second)
if err != nil {
return nil, err
}
hashes, err := t.getNodes(nodes.IDs)
if err != nil {
return nil, err
}
return nodes.Rehash(hashes, t.rf.Hash)
}

func (t *tree) GetLeafHashes(begin, end uint64) ([][]byte, error) {
if begin >= end {
return nil, nil
}
ids := make([]compact.NodeID, 0, end-begin)
for i := begin; i < end; i++ {
ids = append(ids, compact.NewNodeID(0, i))
}
return t.getNodes(ids)
}

func (t *tree) getCompactRange(begin, end uint64) (*compact.Range, error) {
hashes, err := t.getNodes(compact.RangeNodes(begin, end))
if err != nil {
return nil, err
}
return t.rf.NewRange(begin, end, hashes)
}

func (t *tree) getNodes(ids []compact.NodeID) ([][]byte, error) {
hashes := make([][]byte, len(ids))
for i, id := range ids {
if hash, ok := t.nodes[id]; ok {
hashes[i] = hash
} else {
return nil, fmt.Errorf("node %+v not found", id)
}
}
return hashes, nil
}
5 changes: 5 additions & 0 deletions exp/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/transparency-dev/merkle/exp

go 1.16

require github.com/transparency-dev/merkle v0.0.0-20220425113829-c120179f55ad // indirect
4 changes: 4 additions & 0 deletions exp/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/transparency-dev/merkle v0.0.0-20220425113829-c120179f55ad h1:82yvTO+VijfWulMsMQvqQSZ0zNEAgmEUeBG+ArrO9Js=
github.com/transparency-dev/merkle v0.0.0-20220425113829-c120179f55ad/go.mod h1:B8FIw5LTq6DaULoHsVFRzYIUDkl8yuSwCdZnOZGKL/A=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=