bech32_test.gno
7.26 Kb · 230 lines
1package bech32
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9// BIP-173 §Test vectors: strings that must decode with the Bech32 constant.
10func TestValidBech32(t *testing.T) {
11 cases := []struct{ s, hrp string }{
12 {"A12UEL5L", "a"},
13 {"a12uel5l", "a"},
14 {"?1ezyfcl", "?"},
15 {"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", "abcdef"},
16 {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", "split"},
17 }
18 for _, c := range cases {
19 hrp, data, v, err := Decode(c.s)
20 uassert.NoError(t, err, c.s)
21 uassert.Equal(t, c.hrp, hrp)
22 uassert.Equal(t, "bech32", v.String())
23 // Re-encoding the decoded parts must reproduce the lowercase input.
24 back, err2 := Encode(hrp, data)
25 uassert.NoError(t, err2)
26 uassert.Equal(t, lower(c.s), back)
27 }
28}
29
30// BIP-350 §Test vectors: the same shapes under the Bech32m constant.
31func TestValidBech32m(t *testing.T) {
32 for _, c := range []struct{ s, hrp string }{
33 {"A1LQFN3A", "a"},
34 {"a1lqfn3a", "a"},
35 {"?1v759aa", "?"},
36 {"split1checkupstagehandshakeupstreamerranterredcaperredlc445v", "split"},
37 } {
38 hrp, data, v, err := Decode(c.s)
39 uassert.NoError(t, err, c.s)
40 uassert.Equal(t, c.hrp, hrp)
41 uassert.Equal(t, "bech32m", v.String())
42 back, err2 := EncodeVariant(hrp, data, Bech32m)
43 uassert.NoError(t, err2)
44 uassert.Equal(t, lower(c.s), back)
45 }
46}
47
48// A gno.land address is a Bech32 string with hrp "g" over a 20-byte payload,
49// which is the externally-anchored check that the checksum maths is right.
50func TestGnoAddress(t *testing.T) {
51 const addr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5"
52 hrp, payload, v, err := DecodeBytes(addr)
53 uassert.NoError(t, err)
54 uassert.Equal(t, "g", hrp)
55 uassert.Equal(t, 20, len(payload))
56 uassert.Equal(t, "bech32", v.String())
57
58 back, err2 := EncodeBytes(hrp, payload)
59 uassert.NoError(t, err2)
60 uassert.Equal(t, addr, back)
61}
62
63// One flipped data character invalidates the checksum.
64func TestBadChecksum(t *testing.T) {
65 const addr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5"
66 broken := addr[:len(addr)-1] + "4"
67 _, _, _, err := Decode(broken)
68 uassert.ErrorIs(t, err, ErrChecksum)
69}
70
71func TestDecodeRejects(t *testing.T) {
72 cases := []struct {
73 s string
74 want error
75 }{
76 {"A12UEL5l", ErrMixedCase},
77 {"abcdef", ErrNoSeparator},
78 {"1qzzfhee", ErrInvalidHRP}, // empty human-readable part
79 {"a1qqqqq", ErrTooShort}, // data part shorter than the 6-char checksum
80 {"a1bqqqqqq", ErrInvalidChar}, // 'b' is not in the charset
81 }
82 for _, c := range cases {
83 _, _, _, err := Decode(c.s)
84 uassert.ErrorIs(t, err, c.want, c.s)
85 }
86}
87
88func TestTooLong(t *testing.T) {
89 long := "a1"
90 for i := 0; i < MaxLen; i++ {
91 long += "q"
92 }
93 _, _, _, err := Decode(long)
94 uassert.ErrorIs(t, err, ErrTooLong)
95
96 payload := make([]byte, 80)
97 _, err2 := EncodeBytes("hrp", payload)
98 uassert.ErrorIs(t, err2, ErrTooLong)
99}
100
101func TestConvertBits(t *testing.T) {
102 // 8 -> 5 with padding, then back, is the identity.
103 for _, in := range [][]byte{{}, {0x00}, {0xff}, {0xde, 0xad, 0xbe, 0xef}, {1, 2, 3, 4, 5, 6, 7}} {
104 five, err := ConvertBits(in, 8, 5, true)
105 uassert.NoError(t, err)
106 eight, err2 := ConvertBits(five, 5, 8, false)
107 uassert.NoError(t, err2)
108 uassert.Equal(t, len(in), len(eight))
109 for i := range in {
110 uassert.Equal(t, int(in[i]), int(eight[i]))
111 }
112 }
113 // Every produced group fits in 5 bits.
114 five, _ := ConvertBits([]byte{0xff, 0xff}, 8, 5, true)
115 for _, g := range five {
116 uassert.True(t, g < 32)
117 }
118 // A 5-bit input holding a 6-bit value is refused.
119 _, err := ConvertBits([]byte{32}, 5, 8, true)
120 uassert.ErrorIs(t, err, ErrInvalidData)
121 // An unsupported width is refused.
122 _, err2 := ConvertBits([]byte{1}, 0, 5, true)
123 uassert.ErrorIs(t, err2, ErrInvalidBits)
124}
125
126func TestEncodeRejects(t *testing.T) {
127 _, err := Encode("", []byte{0})
128 uassert.ErrorIs(t, err, ErrInvalidHRP)
129 // A human-readable part must stay in the printable US-ASCII range.
130 _, err2 := Encode("a\x7f", []byte{0})
131 uassert.ErrorIs(t, err2, ErrInvalidHRP)
132 // A data group wider than 5 bits is not representable.
133 _, err3 := Encode("a", []byte{32})
134 uassert.ErrorIs(t, err3, ErrInvalidData)
135}
136
137func TestVariantString(t *testing.T) {
138 uassert.Equal(t, "bech32", Bech32.String())
139 uassert.Equal(t, "bech32m", Bech32m.String())
140}
141
142func lower(s string) string {
143 out := []byte(s)
144 for i, c := range out {
145 if c >= 'A' && c <= 'Z' {
146 out[i] = c + 32
147 }
148 }
149 return string(out)
150}
151
152// Why Bech32m exists. A Bech32 string whose final character is 'p' keeps a valid
153// checksum when 'q' characters are inserted in front of that 'p', so a reader
154// accepts a string it was never handed. Both of these verify under the Bech32
155// constant, and they decode to different data parts.
156func TestLengthExtensionWeakness(t *testing.T) {
157 const base = "a1q3g6mnsp"
158 const extended = "a1q3g6mnsqp"
159
160 _, d1, v1, err := Decode(base)
161 uassert.NoError(t, err)
162 uassert.Equal(t, "bech32", v1.String())
163
164 _, d2, v2, err2 := Decode(extended)
165 uassert.NoError(t, err2, "the inserted 'q' does not break the Bech32 checksum")
166 uassert.Equal(t, "bech32", v2.String())
167 uassert.Equal(t, len(d1)+1, len(d2))
168
169 // Bech32m's constant closes it: the same insertion on a Bech32m string of the
170 // same shape is refused.
171 _, _, _, err3 := Decode("a1q6dp699qp")
172 uassert.ErrorIs(t, err3, ErrChecksum)
173 _, _, vm, err4 := Decode("a1q6dp699p")
174 uassert.NoError(t, err4)
175 uassert.Equal(t, "bech32m", vm.String())
176}
177
178// Variant is an exported int, so a caller can hand over a value that is neither
179// constant. Encoding one as Bech32 anyway would answer a question nobody asked.
180func TestUnknownVariant(t *testing.T) {
181 _, err := EncodeVariant("a", nil, Variant(2))
182 uassert.ErrorIs(t, err, ErrInvalidVariant)
183 _, err2 := EncodeVariant("a", nil, Variant(-1))
184 uassert.ErrorIs(t, err2, ErrInvalidVariant)
185 uassert.Equal(t, "invalid", Variant(2).String())
186}
187
188// ConvertBits promises ErrInvalidPadding, and a round trip never reaches it:
189// the padded direction always lands on a group boundary.
190func TestConvertBitsPadding(t *testing.T) {
191 // Five bits in, eight out, unpadded: one group is not a whole byte.
192 _, err := ConvertBits([]byte{1}, 5, 8, false)
193 uassert.ErrorIs(t, err, ErrInvalidPadding)
194 // Ten bits in, eight out: the two leftover bits are not zero, so they are
195 // data the caller would lose rather than padding the encoder added.
196 _, err2 := ConvertBits([]byte{0, 1}, 5, 8, false)
197 uassert.ErrorIs(t, err2, ErrInvalidPadding)
198 // The same ten bits with zero leftovers convert cleanly.
199 out, err3 := ConvertBits([]byte{0, 4}, 5, 8, false)
200 uassert.NoError(t, err3)
201 uassert.Equal(t, 1, len(out))
202}
203
204// An oversized payload is refused before it is regrouped, so the work stays
205// bounded by the 90-character format rather than by what the caller sent.
206func TestEncodeBytesBoundsBeforeWork(t *testing.T) {
207 // "g" plus the separator plus six checksum characters leaves 82 groups,
208 // which is 51 whole bytes.
209 uassert.Equal(t, 51, MaxPayloadLen("g"))
210
211 ok, err := EncodeBytes("g", make([]byte, MaxPayloadLen("g")))
212 uassert.NoError(t, err)
213 uassert.Equal(t, MaxLen, len(ok))
214
215 _, err2 := EncodeBytes("g", make([]byte, MaxPayloadLen("g")+1))
216 uassert.ErrorIs(t, err2, ErrTooLong)
217
218 // A human-readable part that leaves no room at all.
219 uassert.Equal(t, 0, MaxPayloadLen(longHRP()))
220 _, err3 := EncodeBytes(longHRP(), []byte{1})
221 uassert.ErrorIs(t, err3, ErrTooLong)
222}
223
224func longHRP() string {
225 s := ""
226 for i := 0; i < MaxLen; i++ {
227 s += "a"
228 }
229 return s
230}