Skip to content

Commit 0de243d

Browse files
authored
Verify subtree inclusion proofs (transparency-dev#227)
* verifysubtreeinclusion implementation * verifysubtreeinclusion test copy + implementation * size --> start, end in tests * duplicate tests to -subtree.json * shift and add description * fix large numbers * add error tests * fix error messages * remove newlines * reviewer's comments * oops
1 parent 37770d7 commit 0de243d

208 files changed

Lines changed: 2677 additions & 8 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

proof/proof.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ func SubtreeInclusion(index, start, end uint64) (Nodes, error) {
6565
if index < start || index >= end {
6666
return Nodes{}, fmt.Errorf("index %d out of bounds for subtree [%d, %d)", index, start, end)
6767
}
68-
if !isSubtreeValid(start, end) {
69-
return Nodes{}, fmt.Errorf("start %d not a multiple of bit_ceil(end - start) = %d", start, end-start)
68+
if err := isSubtreeValid(start, end); err != nil {
69+
return Nodes{}, fmt.Errorf("subtree invalid: %v", err)
7070
}
7171

7272
// Shift the subtree to the left, such that it starts at 0.
@@ -228,15 +228,18 @@ func reverse(ids []compact.NodeID) {
228228
// - all the subtree nodes
229229
// - no extra node to the left of the subtree
230230
// - potentially extra nodes to the right of the subtree
231-
func isSubtreeValid(start, end uint64) bool {
231+
func isSubtreeValid(start, end uint64) error {
232232
l := end - start
233233
if start == 0 {
234-
return true
235-
} else if (l) > uint64(1)<<63 {
234+
return nil
235+
} else if l > uint64(1)<<63 {
236236
// special-case large subtree to avoid panic
237-
return false
237+
return fmt.Errorf("start %d must be 0 when subtree length %d > 1<<63. ", start, l)
238238
}
239-
return start%bitCeil(l) == 0
239+
if bc := bitCeil(l); start%bc != 0 {
240+
return fmt.Errorf("start %d not a multiple of bit_ceil(end - start) = %d", start, bc)
241+
}
242+
return nil
240243
}
241244

242245
// bitCeil returns the smallest power of 2 larger than n.

proof/proof_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ func TestInclusionSubtreeSucceedsUpToTreeSize(t *testing.T) {
425425
const maxSize = uint64(555)
426426
for sbe := uint64(1); sbe <= maxSize; sbe++ {
427427
for sbs := uint64(0); sbs < sbe; sbs++ {
428-
if !isSubtreeValid(sbs, sbe) {
428+
if err := isSubtreeValid(sbs, sbe); err != nil {
429429
continue
430430
}
431431
for i := sbs; i < sbe; i++ {

proof/verify.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,30 @@ func VerifyInclusion(hasher merkle.LogHasher, index, size uint64, leafHash []byt
5151
return verifyMatch(calcRoot, root)
5252
}
5353

54+
// VerifySubtreeInclusion verifies the correctness of the subtree inclusion
55+
// proof for the leaf with the specified hash and index, relative to the
56+
// provided subtree [start, end) subtree and subtree root hash.
57+
// It requires:
58+
// - 0 <= start <= index < end
59+
// - start to be a multiple of the smallest power of two greater than or equal to
60+
// (end - start)
61+
func VerifySubtreeInclusion(hasher merkle.LogHasher, index, start, end uint64, leafHash []byte, proof [][]byte, root []byte) error {
62+
if start >= end {
63+
return fmt.Errorf("start %d greater than or equal to end %d", start, end)
64+
}
65+
if index < start || index >= end {
66+
return fmt.Errorf("index %d out of bounds for subtree [%d, %d)", index, start, end)
67+
}
68+
if err := isSubtreeValid(start, end); err != nil {
69+
return fmt.Errorf("subtree invalid: %v", err)
70+
}
71+
calcRoot, err := RootFromInclusionProof(hasher, index-start, end-start, leafHash, proof)
72+
if err != nil {
73+
return err
74+
}
75+
return verifyMatch(calcRoot, root)
76+
}
77+
5478
// RootFromInclusionProof calculates the expected root hash for a tree of the
5579
// given size, provided a leaf index and hash with the corresponding inclusion
5680
// proof. Requires 0 <= index < size.

proof/verify_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ type inclusionProbe struct {
3838
WantError bool `json:"wantErr"`
3939
}
4040

41+
// subtreeInclusionProbe is a parameter set for subtree inclusion proof
42+
// verification.
43+
type subtreeInclusionProbe struct {
44+
LeafIdx uint64 `json:"leafIdx"`
45+
Start uint64 `json:"start"`
46+
End uint64 `json:"end"`
47+
Root []byte `json:"root"`
48+
LeafHash []byte `json:"leafHash"`
49+
Proof [][]byte `json:"proof"`
50+
51+
Desc string `json:"desc"`
52+
WantError bool `json:"wantErr"`
53+
}
54+
4155
// consistencyProbe is a parameter set for consistency proof verification.
4256
type consistencyProbe struct {
4357
Size1 uint64 `json:"size1"`
@@ -102,6 +116,58 @@ func TestVerifyInclusionProbes(t *testing.T) {
102116
}
103117
}
104118

119+
func TestVerifySubtreeInclusionProbes(t *testing.T) {
120+
var probes []subtreeInclusionProbe
121+
122+
if err := filepath.WalkDir("../testdata/subtreeinclusion", func(path string, d fs.DirEntry, err error) error {
123+
if err != nil {
124+
return err
125+
}
126+
127+
if d.IsDir() {
128+
return nil
129+
}
130+
131+
if filepath.Ext(d.Name()) != ".json" {
132+
return nil
133+
}
134+
135+
data, err := os.ReadFile(path)
136+
if err != nil {
137+
return err
138+
}
139+
140+
var probe subtreeInclusionProbe
141+
if err := json.Unmarshal(data, &probe); err != nil {
142+
return fmt.Errorf("failed to parse subtree inclusion probe json: %s", err)
143+
}
144+
145+
probes = append(probes, probe)
146+
147+
return nil
148+
}); err != nil {
149+
t.Errorf("failed to read subtree inclusion probes: %s", err)
150+
}
151+
152+
var wrong []string
153+
for _, p := range probes {
154+
err := VerifySubtreeInclusion(rfc6962.DefaultHasher, p.LeafIdx, p.Start, p.End, p.LeafHash, p.Proof, p.Root)
155+
if p.WantError && err == nil {
156+
wrong = append(wrong, fmt.Sprintf("expected error but didn't get one: %s", p.Desc))
157+
continue
158+
}
159+
160+
if !p.WantError && err != nil {
161+
wrong = append(wrong, fmt.Sprintf("unexpected error: %s, %s", p.Desc, err))
162+
continue
163+
}
164+
}
165+
166+
if len(wrong) > 0 {
167+
t.Errorf("errors verifying subtree inclusion probes: \n%d out of %d failures \nError messages: \n%s", len(wrong), len(probes), strings.Join(wrong, "\n"))
168+
}
169+
}
170+
105171
func TestVerifyConsistencyProbes(t *testing.T) {
106172
var probes []consistencyProbe
107173

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"leafIdx": 1,
3+
"start": 1,
4+
"end": 2,
5+
"root": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
6+
"leafHash": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
7+
"proof": null,
8+
"desc": "empty root - subtree",
9+
"wantErr": true
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"leafIdx": 0,
3+
"start": 0,
4+
"end": 1,
5+
"root": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
6+
"leafHash": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
7+
"proof": null,
8+
"desc": "empty root",
9+
"wantErr": true
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"leafIdx": 1,
3+
"start": 1,
4+
"end": 2,
5+
"root": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
6+
"leafHash": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
7+
"proof": null,
8+
"desc": "happy path - subtree",
9+
"wantErr": false
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"leafIdx": 0,
3+
"start": 0,
4+
"end": 1,
5+
"root": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
6+
"leafHash": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
7+
"proof": null,
8+
"desc": "happy path",
9+
"wantErr": false
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"leafIdx": 3,
3+
"start": 1,
4+
"end": 2,
5+
"root": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
6+
"leafHash": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
7+
"proof": null,
8+
"desc": "leafIdx XOR @2 - subtree",
9+
"wantErr": true
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"leafIdx": 2,
3+
"start": 0,
4+
"end": 1,
5+
"root": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
6+
"leafHash": "bjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB0=",
7+
"proof": null,
8+
"desc": "leafIdx XOR @2",
9+
"wantErr": true
10+
}

0 commit comments

Comments
 (0)