bech32demo.gno
4.59 Kb · 128 lines
1// Package bech32demo is a small gnoweb demo of the Bech32 and Bech32m codec
2// provided by the [p/moul/x/daily/bech32](/p/moul/x/daily/bech32/v0) library:
3// it takes a real gno.land address apart, shows the BIP-173 and BIP-350 test
4// vectors side by side, and reproduces the length-extension weakness that is
5// the whole reason the second variant exists.
6//
7// It contains no codec logic of its own. Stateless, so Render is deterministic.
8package bech32demo
9
10import (
11 "strconv"
12 "strings"
13
14 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/md/v0"
15 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/ui/v0"
16 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/bech32/v0"
17 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/hexdump/v0"
18)
19
20// sample is a real mainnet address: human-readable part "g", 20 payload bytes.
21const sample = "g1manfred47kzduec920z88wfr64ylksmdcedlf5"
22
23// The BIP-173 (Bech32) and BIP-350 (Bech32m) test vectors, paired by shape so
24// the only difference on each row is the six-character checksum.
25var vectors = [][2]string{
26 {"a12uel5l", "a1lqfn3a"},
27 {"?1ezyfcl", "?1v759aa"},
28 {
29 "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w",
30 "split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
31 },
32}
33
34// The insertion that Bech32 accepts and Bech32m refuses. Both strings below
35// carry a checksum Bech32 verifies; only the second has the extra "q".
36const (
37 extBase = "a1q3g6mnsp"
38 extInserted = "a1q3g6mnsqp"
39 extMBase = "a1q6dp699p"
40 extMInsert = "a1q6dp699qp"
41)
42
43// Render renders the demo for gnoweb.
44//
45// Render("") / Render("/") -> the full demo
46// Render("/<string>") -> decode that string
47func Render(path string) string {
48 var b strings.Builder
49 b.WriteString("# Bech32\n\n")
50 b.WriteString("Addresses that carry their own error-detecting checksum, demoing the ")
51 b.WriteString("[`p/moul/x/daily/bech32`](/p/moul/x/daily/bech32/v0) library.\n\n")
52
53 if in := parseArg(path); in != "" {
54 b.WriteString(decodeOne(in))
55 return b.String()
56 }
57
58 b.WriteString("## A gno.land address, taken apart\n\n")
59 b.WriteString(decodeOne(sample))
60
61 b.WriteString("\n## The two variants\n\n")
62 b.WriteString("Same string shape, same data, different checksum constant: ")
63 b.WriteString("`1` for Bech32, `0x2bc830a3` for Bech32m.\n\n")
64 vt := ui.NewTable("Bech32 (BIP-173)", "Bech32m (BIP-350)")
65 for _, pair := range vectors {
66 vt.Row(md.InlineCode(pair[0]), md.InlineCode(pair[1]))
67 }
68 b.WriteString(vt.String() + "\n")
69
70 b.WriteString("## Why Bech32m exists\n\n")
71 b.WriteString("A Bech32 string ending in `p` keeps a valid checksum when `q` ")
72 b.WriteString("characters are inserted in front of that final `p`, so a reader ")
73 b.WriteString("accepts a string nobody wrote. Bech32m's constant closes it:\n\n")
74 wt := ui.NewTable("string", "variant", "verdict")
75 for _, s := range []string{extBase, extInserted, extMBase, extMInsert} {
76 _, _, v, err := bech32.Decode(s)
77 if err != nil {
78 wt.Row(md.InlineCode(s), "-", "rejected: "+err.Error())
79 continue
80 }
81 wt.Row(md.InlineCode(s), v.String(), "accepted")
82 }
83 b.WriteString(wt.String() + "\n")
84
85 b.WriteString("Pass a string of your own to decode it: ")
86 b.WriteString("`Render(\"/<bech32 string>\")`.\n")
87 return b.String()
88}
89
90// decodeOne renders one string's decode. Everything derived from the input is
91// escaped: the argument reaches this realm from whoever typed the url.
92func decodeOne(in string) string {
93 var b strings.Builder
94 b.WriteString("Decoding " + ui.Inline(in) + ":\n\n")
95
96 hrp, payload, variant, err := bech32.DecodeBytes(in)
97 if err != nil {
98 b.WriteString("> " + ui.Inline(err.Error()) + "\n")
99 return b.String()
100 }
101
102 t := ui.NewTable("field", "value")
103 // hrp bytes run the whole printable-ASCII range, so this cell is escaped,
104 // and escaped text can never sit inside a code span.
105 t.Row("human-readable part", ui.Cell(hrp))
106 t.Row("variant", variant.String())
107 t.Row("payload", strconv.Itoa(len(payload))+" bytes")
108 t.Row("payload (hex)", md.InlineCode(hexOf(payload)))
109 b.WriteString(t.String())
110 return b.String()
111}
112
113// hexOf renders a payload through the hexdump library, which owns the digits.
114func hexOf(payload []byte) string {
115 var b strings.Builder
116 for _, c := range payload {
117 b.WriteString(hexdump.Hex(c))
118 }
119 return b.String()
120}
121
122// parseArg strips the one routing slash in front of the argument and returns
123// the rest verbatim. Nothing else is removed: a slash is legal in a
124// human-readable part, so cutting at one refuses a valid string, and trimming
125// whitespace would render a clean decode of a string nobody typed.
126func parseArg(path string) string {
127 return strings.TrimPrefix(path, "/")
128}