provable_test.gno
4.68 Kb · 165 lines
1package provable
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7
8 "gno.land/p/moul/x/merkle/v0"
9)
10
11func TestAppendAndVerify(cur realm, t *testing.T) {
12 seed()
13 i := Append(cross(cur), "a fourth commitment")
14 if i != 3 || Size() != 4 {
15 t.Fatalf("Append returned %d with Size %d, want 3 and 4", i, Size())
16 }
17 if Entry(i) != "a fourth commitment" {
18 t.Errorf("Entry(%d) = %q", i, Entry(i))
19 }
20
21 root := Root()
22 for j := 0; j < Size(); j++ {
23 path, before, after := ProofOf(j)
24 if !Verify(root, j, Size(), Entry(j), path, before, after) {
25 t.Errorf("entry %d: its own proof does not verify", j)
26 }
27 }
28}
29
30// Append trims, so the proof must be over the trimmed entry.
31func TestAppendTrims(cur realm, t *testing.T) {
32 seed()
33 i := Append(cross(cur), " padded ")
34 if Entry(i) != "padded" {
35 t.Fatalf("Entry = %q, want %q", Entry(i), "padded")
36 }
37 path, before, after := ProofOf(i)
38 if !Verify(Root(), i, Size(), "padded", path, before, after) {
39 t.Error("trimmed entry does not verify")
40 }
41 if Verify(Root(), i, Size(), " padded ", path, before, after) {
42 t.Error("the untrimmed string verified, so the leaf is not what is stored")
43 }
44}
45
46func TestVerifyRejects(cur realm, t *testing.T) {
47 seed()
48 root := Root()
49 path, before, after := ProofOf(1)
50 other := "0000000000000000000000000000000000000000000000000000000000000000"
51
52 tests := []struct {
53 name string
54 root string
55 index, total int
56 entry, path, before, after string
57 }{
58 {"wrong entry", root, 1, 3, "not in the log", path, before, after},
59 {"another real entry", root, 1, 3, Entry(0), path, before, after},
60 {"wrong index", root, 0, 3, Entry(1), path, before, after},
61 {"wrong total", root, 1, 4, Entry(1), path, before, after},
62 {"wrong root", other, 1, 3, Entry(1), path, before, after},
63 {"root not hex", "zz", 1, 3, Entry(1), path, before, after},
64 {"root wrong length", "abcd", 1, 3, Entry(1), path, before, after},
65 {"empty path", root, 1, 3, Entry(1), "", before, after},
66 {"peaks swapped", root, 1, 3, Entry(1), path, after, before},
67 {"garbage path", root, 1, 3, Entry(1), "zz", before, after},
68 }
69 for _, tc := range tests {
70 if Verify(tc.root, tc.index, tc.total, tc.entry, tc.path, tc.before, tc.after) {
71 t.Errorf("%s: accepted, must be rejected", tc.name)
72 }
73 }
74}
75
76// The log root is also the Tendermint simple-tree root over the same entries,
77// so the fixed-leaf-set proof encoding verifies against it too. This is the
78// claim the realm makes in Render; it is tested, not asserted.
79func TestVerifyFixedAcceptsTendermintProof(cur realm, t *testing.T) {
80 seed()
81 Append(cross(cur), "a fourth commitment")
82 Append(cross(cur), "a fifth commitment")
83
84 leaves := make([][]byte, Size())
85 for i := range leaves {
86 leaves[i] = []byte(Entry(i))
87 }
88 tree := merkle.New(leaves)
89 if tree.RootHex() != Root() {
90 t.Fatalf("tree root %s, log root %s", tree.RootHex(), Root())
91 }
92 for i := 0; i < Size(); i++ {
93 p, err := tree.Proof(i)
94 if err != nil {
95 t.Fatalf("i=%d: %v", i, err)
96 }
97 if !VerifyFixed(Root(), i, Size(), Entry(i), p.Hex()) {
98 t.Errorf("i=%d: Tendermint proof rejected by the realm", i)
99 }
100 if VerifyFixed(Root(), i, Size(), "forged", p.Hex()) {
101 t.Errorf("i=%d: forged leaf accepted", i)
102 }
103 }
104}
105
106func TestCaps(t *testing.T) {
107 seed()
108 tests := []struct {
109 name string
110 entry string
111 }{
112 {"empty", ""},
113 {"whitespace only", " "},
114 {"too long", strings.Repeat("x", MaxEntryLen+1)},
115 }
116 for _, tc := range tests {
117 if got := panicked(func() { appendEntry(tc.entry) }); got == "" {
118 t.Errorf("%s: accepted, must panic", tc.name)
119 }
120 }
121 // A note of exactly MaxEntryLen is fine.
122 if got := panicked(func() { appendEntry(strings.Repeat("x", MaxEntryLen)) }); got != "" {
123 t.Errorf("entry of exactly MaxEntryLen panicked: %s", got)
124 }
125}
126
127func TestLogIsBounded(t *testing.T) {
128 seed()
129 for Size() < MaxEntries {
130 appendEntry("filler-" + strconv.Itoa(Size()))
131 }
132 if got := panicked(func() { appendEntry("one too many") }); got == "" {
133 t.Error("appending past MaxEntries was accepted")
134 }
135 seed()
136}
137
138func TestEntryRangeChecked(t *testing.T) {
139 seed()
140 for _, i := range []int{-1, 3, 999} {
141 if got := panicked(func() { Entry(i) }); got == "" {
142 t.Errorf("Entry(%d) did not panic", i)
143 }
144 if got := panicked(func() { ProofOf(i) }); got == "" {
145 t.Errorf("ProofOf(%d) did not panic", i)
146 }
147 }
148}
149
150// panicked runs f and returns the panic message, or "" if it returned
151// normally. Safe here because these calls are in-package and do not cross a
152// realm boundary, where a panic would be an unrecoverable abort instead.
153func panicked(f func()) (msg string) {
154 defer func() {
155 if r := recover(); r != nil {
156 if s, ok := r.(string); ok {
157 msg = s
158 } else {
159 msg = "panic"
160 }
161 }
162 }()
163 f()
164 return ""
165}