Skip to content

Commit 609ad10

Browse files
authored
Merge pull request #252 from ietf-plants-wg/push-lkpwrxrlkvsq
Add appendix with accumulated subtree test vectors
2 parents 0b45981 + b7fb9b9 commit 609ad10

3 files changed

Lines changed: 312 additions & 0 deletions

File tree

demo/log.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,70 @@ func (mt *MerkleTree) SubtreeInclusionProof(index, start, end int) ([]byte, erro
128128
return proof, nil
129129
}
130130

131+
func (mt *MerkleTree) SubtreeConsistencyProof(start, end, n int) ([]byte, error) {
132+
if !IsValidSubtree(start, end) {
133+
return nil, fmt.Errorf("invalid subtree: [%d, %d)", start, end)
134+
}
135+
if end > n {
136+
return nil, fmt.Errorf("subtree [%d, %d) contains more elements than tree of size %d", start, end, n)
137+
}
138+
if n > mt.Size() {
139+
return nil, fmt.Errorf("tree of size %d is larger than the Merkle Tree of size %d", n, mt.Size())
140+
}
141+
return mt.subtreeSubproof(start, end, 0, n, true)
142+
}
143+
144+
// subtreeSubproof implements SUBTREE_SUBPROOF(start - lo, end - lo, D[lo:hi],
145+
// known) over the tree's entries, with the subtree and window described in
146+
// absolute indices. known reports whether the subtree hash is already known to
147+
// the verifier and so may be omitted from the proof.
148+
func (mt *MerkleTree) subtreeSubproof(start, end, lo, hi int, known bool) ([]byte, error) {
149+
if start == lo && end == hi {
150+
// The subtree is the whole window.
151+
if known {
152+
return nil, nil
153+
}
154+
h, err := mt.SubtreeHash(lo, hi)
155+
if err != nil {
156+
return nil, err
157+
}
158+
return h[:], nil
159+
}
160+
// The window has more than one element, so split it at the largest power
161+
// of two smaller than its size.
162+
k := 1 << (bits.Len(uint(hi-lo-1)) - 1)
163+
split := lo + k
164+
var proof []byte
165+
var siblingStart, siblingEnd int
166+
var err error
167+
switch {
168+
case end <= split:
169+
// The subtree is entirely in the left child, so recurse into it and
170+
// include the right child.
171+
proof, err = mt.subtreeSubproof(start, end, lo, split, known)
172+
siblingStart, siblingEnd = split, hi
173+
case split <= start:
174+
// The subtree is entirely in the right child, so recurse into it and
175+
// include the left child.
176+
proof, err = mt.subtreeSubproof(start, end, split, hi, known)
177+
siblingStart, siblingEnd = lo, split
178+
default:
179+
// The subtree spans the split, which implies start == lo. Recurse into
180+
// the right child, no longer knowing its subtree hash, and include the
181+
// left child.
182+
proof, err = mt.subtreeSubproof(split, end, split, hi, false)
183+
siblingStart, siblingEnd = lo, split
184+
}
185+
if err != nil {
186+
return nil, err
187+
}
188+
h, err := mt.SubtreeHash(siblingStart, siblingEnd)
189+
if err != nil {
190+
return nil, err
191+
}
192+
return append(proof, h[:]...), nil
193+
}
194+
131195
func SubtreesForInterval(start, end int) (start1, end1, start2, end2 int, err error) {
132196
if 0 > start || start >= end {
133197
err = fmt.Errorf("invalid interval [%d, %d)", start, end)

demo/vectors_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package main
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"io"
7+
"testing"
8+
)
9+
10+
// These tests reproduce the accumulated test vectors from the "Subtree Test
11+
// Vectors" appendix. For trees of sizes up to 130, they fold the output of each
12+
// subtree algorithm over every valid input into a single rolling SHA-256, which
13+
// is compared against the value published in the draft.
14+
15+
const subtreeVectorMax = 130
16+
17+
// subtreeVectorTree builds the tree D used by the test vectors, with leaf values
18+
// d[0] = 0x00, d[1] = 0x01, and so on.
19+
func subtreeVectorTree() *MerkleTree {
20+
entries := make([][]byte, subtreeVectorMax)
21+
for i := range entries {
22+
entries[i] = []byte{byte(i)}
23+
}
24+
return NewMerkleTree(entries)
25+
}
26+
27+
// writeProofLine writes prefix followed by, for each hash in the concatenated
28+
// proof, a space and the hash's hexadecimal encoding, then a newline. An empty
29+
// proof contributes no hashes and so leaves no trailing space.
30+
func writeProofLine(w io.Writer, prefix string, proof []byte) {
31+
io.WriteString(w, prefix)
32+
for off := 0; off < len(proof); off += HashSize {
33+
fmt.Fprintf(w, " %x", proof[off:off+HashSize])
34+
}
35+
io.WriteString(w, "\n")
36+
}
37+
38+
func TestSubtreeHashVectors(t *testing.T) {
39+
tree := subtreeVectorTree()
40+
h := sha256.New()
41+
for end := 1; end <= subtreeVectorMax; end++ {
42+
for start := 0; start < end; start++ {
43+
if !IsValidSubtree(start, end) {
44+
continue
45+
}
46+
subtreeHash, err := tree.SubtreeHash(start, end)
47+
if err != nil {
48+
t.Fatalf("SubtreeHash(%d, %d): %v", start, end, err)
49+
}
50+
fmt.Fprintf(h, "[%d, %d) %x\n", start, end, subtreeHash[:])
51+
}
52+
}
53+
const want = "94a95384a8c69acea9b50d035a58285b3a777cb7a724005faa5e1f1e1190007f"
54+
if got := fmt.Sprintf("%x", h.Sum(nil)); got != want {
55+
t.Errorf("subtree hash vector = %s, want %s", got, want)
56+
}
57+
}
58+
59+
func TestSubtreeInclusionProofVectors(t *testing.T) {
60+
tree := subtreeVectorTree()
61+
h := sha256.New()
62+
for end := 1; end <= subtreeVectorMax; end++ {
63+
for start := 0; start < end; start++ {
64+
if !IsValidSubtree(start, end) {
65+
continue
66+
}
67+
for index := start; index < end; index++ {
68+
proof, err := tree.SubtreeInclusionProof(index, start, end)
69+
if err != nil {
70+
t.Fatalf("SubtreeInclusionProof(%d, %d, %d): %v", index, start, end, err)
71+
}
72+
writeProofLine(h, fmt.Sprintf("%d [%d, %d)", index, start, end), proof)
73+
}
74+
}
75+
}
76+
const want = "ac2a8f989e44d99e399db448050ff5f19757df53cfb716aa81015d3955d8163f"
77+
if got := fmt.Sprintf("%x", h.Sum(nil)); got != want {
78+
t.Errorf("subtree inclusion proof vector = %s, want %s", got, want)
79+
}
80+
}
81+
82+
func TestSubtreeConsistencyProofVectors(t *testing.T) {
83+
tree := subtreeVectorTree()
84+
h := sha256.New()
85+
for n := 0; n <= subtreeVectorMax; n++ {
86+
for end := 1; end <= n; end++ {
87+
for start := 0; start < end; start++ {
88+
if !IsValidSubtree(start, end) {
89+
continue
90+
}
91+
proof, err := tree.SubtreeConsistencyProof(start, end, n)
92+
if err != nil {
93+
t.Fatalf("SubtreeConsistencyProof(%d, %d, %d): %v", start, end, n, err)
94+
}
95+
writeProofLine(h, fmt.Sprintf("[%d, %d) %d", start, end, n), proof)
96+
}
97+
}
98+
}
99+
const want = "c586ebbb73a5621baf2140095d87dde934e3b6503a562a1a5215b8209edd083d"
100+
if got := fmt.Sprintf("%x", h.Sum(nil)); got != want {
101+
t.Errorf("subtree consistency proof vector = %s, want %s", got, want)
102+
}
103+
}
104+
105+
func TestEfficientCoveringSubtreeVectors(t *testing.T) {
106+
h := sha256.New()
107+
for end := 1; end <= subtreeVectorMax; end++ {
108+
for start := 0; start < end; start++ {
109+
if IsValidSubtree(start, end) {
110+
fmt.Fprintf(h, "[%d, %d)\n", start, end)
111+
continue
112+
}
113+
start1, end1, start2, end2, err := SubtreesForInterval(start, end)
114+
if err != nil {
115+
t.Fatalf("SubtreesForInterval(%d, %d): %v", start, end, err)
116+
}
117+
fmt.Fprintf(h, "[%d, %d) [%d, %d)\n", start1, end1, start2, end2)
118+
}
119+
}
120+
const want = "e0aecb912a10c57d753b6ecc64db73217f9bc4ed10fcb4e9062be3b6fbe1ebfd"
121+
if got := fmt.Sprintf("%x", h.Sum(nil)); got != want {
122+
t.Errorf("efficient covering subtree vector = %s, want %s", got, want)
123+
}
124+
}

draft-ietf-plants-merkle-tree-certs.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ informative:
184184
author:
185185
org: C2SP
186186

187+
Accumulated:
188+
title: Accumulated Test Vectors
189+
target: https://words.filippo.io/accumulated/
190+
date: October 9, 2024
191+
author:
192+
- name: Filippo Valsorda
193+
187194
...
188195

189196
--- abstract
@@ -421,6 +428,8 @@ As with Merkle Trees, a subtree inclusion proof, defined in {{subtree-inclusion-
421428

422429
Not all intervals can form subtrees. Subtrees are limited to intervals that can be efficiently proven consistent with the original tree, using subtree consistency proofs defined in {{subtree-consistency-proofs}}. However, every interval of a Merkle Tree can be efficiently covered by two subtrees. {{arbitrary-intervals}} describes how to determine these subtrees.
423430

431+
{{subtree-test-vectors}} provides test vectors for the algorithms defined in this section.
432+
424433
## Definition of a Subtree
425434

426435
Given an ordered list of `n` inputs, `D_n = {d[0], d[1], ..., d[n-1]}`, {{Section 2.1.1 of !RFC9162}} defines the Merkle Tree via the Merkle Tree Hash `MTH(D_n)`.
@@ -2251,6 +2260,119 @@ This reconstructs the hashes of the subtree and original tree, which are then co
22512260

22522261
In the case when `fn` is `sn` in step 5, the condition in step 7.2.1 is always false, and `fr` is always equal to `node_hash` in step 8. In this case, steps 6 through 8 are equivalent to verifying an inclusion proof for the truncated subtree `[fn, sn + 1)` and truncated tree `tn + 1`.
22532262

2263+
# Subtree Test Vectors
2264+
2265+
The following are "accumulated" {{Accumulated}} test vectors for the various subtree algorithms defined in {{subtrees}}.
2266+
2267+
They are hash values of the outputs of all possible inputs for each algorithm, for trees of sizes up to 130. They can be used to verify that an implementation matches the specification, without having to include a large number of individual test vectors.
2268+
2269+
For all the test vectors, a tree `D_n` of size `n` is constructed with leaf values `d[0] = 0x00, d[1] = 0x01, ...`. The hash function used is SHA-256. The hash values are encoded in hexadecimal.
2270+
2271+
## Subtree Hashes
2272+
2273+
For each value of `end` from 1 to 130, and each value of `start` from 0 to `end - 1`, if `[start, end)` is a valid subtree, add to the rolling hash the ASCII string `[START, END) HASH` followed by a newline (U+000A), where `START` and `END` are the decimal representations of `start` and `end`, respectively, and `HASH` is the hexadecimal encoding of `MTH(D[start:end])`, according to {{subtrees}}.
2274+
2275+
The final hash value is
2276+
2277+
~~~
2278+
94a95384a8c69acea9b50d035a58285b3a777cb7a724005faa5e1f1e1190007f
2279+
~~~
2280+
2281+
In Python, this can be expressed as:
2282+
2283+
~~~python
2284+
import hashlib
2285+
h = hashlib.sha256()
2286+
for end in range(1, 131):
2287+
for start in range(end):
2288+
if valid_subtree(start, end):
2289+
subtree_hash = MTH(D[start:end])
2290+
h.update(f'[{start}, {end}) {subtree_hash.hex()}\n'.encode())
2291+
assert h.hexdigest() == '94a95384a8c69acea9b50d035a58285b3a777cb7a724005faa5e1f1e1190007f'
2292+
~~~
2293+
2294+
## Subtree Inclusion Proofs {#subtree-inclusion-proof-vectors}
2295+
2296+
For each value of `end` from 1 to 130, and each value of `start` from 0 to `end - 1`, if `[start, end)` is a valid subtree, for each value of `index` from `start` to `end - 1`, add to the rolling hash the ASCII string `INDEX [START, END)`, then, for each hash in the inclusion proof ({{subtree-inclusion-proofs}}) for `d[index]` in the subtree `[start, end)`, a space (U+0020) followed by the hexadecimal encoding of that hash, and finally a newline (U+000A), where `INDEX` is the decimal representation of `index`, and `START` and `END` are the decimal representations of `start` and `end`, respectively.
2297+
2298+
The final hash value is
2299+
2300+
~~~
2301+
ac2a8f989e44d99e399db448050ff5f19757df53cfb716aa81015d3955d8163f
2302+
~~~
2303+
2304+
In Python, this can be expressed as:
2305+
2306+
~~~python
2307+
import hashlib
2308+
h = hashlib.sha256()
2309+
for end in range(1, 131):
2310+
for start in range(end):
2311+
if valid_subtree(start, end):
2312+
for index in range(start, end):
2313+
inclusion_proof = get_inclusion_proof(D, start, end, index)
2314+
line = f'{index} [{start}, {end})'
2315+
for p in inclusion_proof:
2316+
line += f' {p.hex()}'
2317+
h.update(f'{line}\n'.encode())
2318+
assert h.hexdigest() == 'ac2a8f989e44d99e399db448050ff5f19757df53cfb716aa81015d3955d8163f'
2319+
~~~
2320+
2321+
## Subtree Consistency Proofs {#subtree-consistency-proof-vectors}
2322+
2323+
For each value of `n` from 0 to 130, and each value of `end` from 1 to `n`, and each value of `start` from 0 to `end - 1`, if `[start, end)` is a valid subtree, add to the rolling hash the ASCII string `[START, END) N`, then, for each hash in the consistency proof ({{subtree-consistency-proofs}}) for the subtree `[start, end)` and tree of size `n`, a space (U+0020) followed by the hexadecimal encoding of that hash, and finally a newline (U+000A), where `START` and `END` are the decimal representations of `start` and `end`, respectively, and `N` is the decimal representation of `n`.
2324+
2325+
The final hash value is
2326+
2327+
~~~
2328+
c586ebbb73a5621baf2140095d87dde934e3b6503a562a1a5215b8209edd083d
2329+
~~~
2330+
2331+
In Python, this can be expressed as:
2332+
2333+
~~~python
2334+
import hashlib
2335+
h = hashlib.sha256()
2336+
for n in range(131):
2337+
for end in range(1, n + 1):
2338+
for start in range(end):
2339+
if valid_subtree(start, end):
2340+
consistency_proof = get_consistency_proof(D, n, start, end)
2341+
line = f'[{start}, {end}) {n}'
2342+
for p in consistency_proof:
2343+
line += f' {p.hex()}'
2344+
h.update(f'{line}\n'.encode())
2345+
assert h.hexdigest() == 'c586ebbb73a5621baf2140095d87dde934e3b6503a562a1a5215b8209edd083d'
2346+
~~~
2347+
2348+
## Efficient Covering Subtrees
2349+
2350+
For each value of `end` from 1 to 130, and each value of `start` from 0 to `end - 1`:
2351+
2352+
* if `[start, end)` is a valid subtree, add to the rolling hash the ASCII string `[START, END)` followed by a newline (U+000A), where `START` and `END` are the decimal representations of `start` and `end`, respectively;
2353+
* otherwise, add to the rolling hash the ASCII string `[LEFT_START, LEFT_END) [RIGHT_START, RIGHT_END)` followed by a newline (U+000A), where `LEFT_START`, `LEFT_END`, `RIGHT_START`, and `RIGHT_END` are the decimal representations of the start and end of the left and right subtrees, respectively, that efficiently cover ({{arbitrary-intervals}}) `[start, end)`.
2354+
2355+
The final hash value is
2356+
2357+
~~~
2358+
e0aecb912a10c57d753b6ecc64db73217f9bc4ed10fcb4e9062be3b6fbe1ebfd
2359+
~~~
2360+
2361+
In Python, this can be expressed as:
2362+
2363+
~~~python
2364+
import hashlib
2365+
h = hashlib.sha256()
2366+
for end in range(1, 131):
2367+
for start in range(end):
2368+
if valid_subtree(start, end):
2369+
h.update(f'[{start}, {end})\n'.encode())
2370+
else:
2371+
left_start, left_end, right_start, right_end = get_covering_subtrees(start, end)
2372+
h.update(f'[{left_start}, {left_end}) [{right_start}, {right_end})\n'.encode())
2373+
assert h.hexdigest() == 'e0aecb912a10c57d753b6ecc64db73217f9bc4ed10fcb4e9062be3b6fbe1ebfd'
2374+
~~~
2375+
22542376
# Acknowledgements
22552377
{:numbered="false"}
22562378

@@ -2445,3 +2567,5 @@ In draft-04, there is no fast issuance mode. In draft-05, frequent, non-landmark
24452567
- Editorial fixes
24462568

24472569
- Discuss the implications of subordinate CAs in Security Considerations
2570+
2571+
- Added subtree test vector appendix

0 commit comments

Comments
 (0)