mailbox.gno
20.55 Kb · 533 lines
1// Package hlmailbox is the gno.land end of the bridge's message layer (bridge/PLAN.md §5.1): realms send messages to
2// other chains through it and take the messages other chains send them from it. Messages, ids and the merkle tree
3// are Hyperlane's byte for byte (gno.land/p/…/hyperlane/v0), so the Solana side runs Hyperlane's own programs.
4//
5// Out. Dispatch takes a message from the realm that calls it, numbers it, puts its id in the merkle tree, keeps the
6// message, and emits the Dispatch event the Solana light client proves from LastResultsHash (§13: id, nonce, origin,
7// destination, sender, recipient, hash). Reemit emits it again from a fresh block, by anyone.
8//
9// In. A recipient realm pulls its own messages: it calls Process with the message and the verifier's metadata;
10// Process checks the caller is the recipient, that the message is for this chain and was never delivered, marks it
11// delivered, and only then asks the active verifier — a realm that registered itself here, chosen by GovDAO by
12// pkgpath — whether the message is genuine; a verifier's error is a panic, so the transaction and the mark revert
13// together and a refused message stays deliverable.
14//
15// Who may change what. Nobody holds an admin key: every setting moves by a decision made with this realm's
16// builders (gov.gno), at once or after 7 days (§5.3), and nothing but Launch and cancel before the one-shot
17// Launch. Who decides is the governor's (M1k): the compiled launchGovernor alone, by its own call through Govern,
18// until it yields (Yield, once, for every bridge realm at once), and GovDAO's proposals from then on — or from the
19// first block, when no governor was compiled. The guardians and watch keys (flags.gno) can only pause: three flags
20// per key per route — sends, credits, payouts — which the routes ask about with Paused. Every launch value is
21// compiled in below and written by init.
22package hlmailbox
23
24import (
25 "chain"
26 "chain/runtime"
27 "crypto/sha256"
28 "encoding/hex"
29 "strconv"
30 "strings"
31
32 "gno.land/p/nt/avl/v0"
33 "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0"
34 hl "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hyperlane/v0"
35)
36
37const (
38 namespace = "gno.land/r/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/"
39 pkgPath = namespace + "hlmailbox" // must match gnomod.toml
40
41 // DomainMainnet and DomainTest are gno.land's Hyperlane domain ids, "GnoL" and "GnoT" in ASCII; nobody in the
42 // Hyperlane registry holds either (2026-10-10). gnoland-1 is mainnet; every other chain is a test chain.
43 DomainMainnet = uint32(1198419788)
44 DomainTest = uint32(1198419796)
45 mainnetChainID = "gnoland-1"
46
47 // SolanaMainnet and SolanaLocal are the destination's domains: Solana's own on mainnet; on any other gno chain
48 // the one the local test validator and the e2e run use (Hyperlane's "solanatestnet"). Devnet is 1399811151.
49 SolanaMainnet = uint32(1399811149)
50 SolanaLocal = uint32(1399811150)
51
52 // The launch values (§5.3): the active verifier, the signers and their rule, the routes. The signer keys below
53 // are PLACEHOLDERS (the three test signers of the vectors) until the operator's three machines exist; the
54 // launch vote reads the real ones in the deployed source. bridge/local/e2e.sh swaps them for its run's keys.
55 launchVerifier = namespace + "hlverifier"
56 launchThreshold = 2
57 launchSigners = "e2c2b621677535e5b2a0a92f37aec61e7ce0397cf1cf713f91a18227eafec2ef,d837b6369a315b6d26babddc0af241d8908a1809481b29e98365965cf8301689,c0b2dc0da05eb6ee67b68068611a72858f3093c79e8d90589ff2ac1f0ea4da26"
58 launchRouteGNOT = namespace + "hlgnot"
59 launchRouteBUBBLE = namespace + "hlbubble"
60 hlgovPath = namespace + "hlgov" // r/hlgov: the one realm that notes a route's enrolment on Solana (NoteEnrolled, AR1)
61
62 maxSigners = 255 // a signature names its signer in one byte
63)
64
65// launchGovernor is the governor (M1k, §5.3): the one address whose own direct call decides — through Govern, the
66// same closures, checks, queues and delays a GovDAO proposal's execution runs — until it yields (Yield), from
67// which block GovDAO's proposals decide and nothing else; "" means GovDAO from the first block. Every bridge
68// realm compiles the same address and refuses to deploy with another (its init reads Governor here), and reads
69// Yielded here, so no realm is ever governed apart from the rest. A PLACEHOLDER (testutils.TestAddress("hl-
70// governor")) until the launch notice names the real key; bridge/local/e2e.sh swaps it for gnodev's test1 in all
71// six realms. Its own line, so one sed on `^const launchGovernor = address(…)` finds it in every realm.
72const launchGovernor = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6")
73
74var (
75 localDomain = domainOf(runtime.ChainID())
76 launchOrigin = solanaOf(runtime.ChainID())
77 // hook is this realm's address as Hyperlane carries it: the merkle tree hook id inside every checkpoint signed
78 // for a message from here.
79 hook = mustBytes32(chain.PackageAddress(pkgPath))
80
81 tree hl.Tree
82 sent avl.Tree // nonceKey(nonce) → *sentMsg
83 done avl.Tree // message id in hex → the block height it was delivered at
84
85 verifiers avl.Tree // pkgpath → Verifier, every realm that registered
86 active avl.Tree // domainKey(origin) → the pkgpath of the verifier GovDAO chose for that origin
87
88 signerKeys []Key // the one signer set, in the order signatures name them
89 signerRule Rule
90
91 live bool // after the Launch decision executed
92 launchedAt int64 // unix time it did
93
94 yielded int64 // the block height the governor yielded at (Yield); 0 until then — every bridge realm reads it
95
96 ledger decisions.Ledger
97)
98
99// sentMsg is one dispatched message and the checkpoint it made.
100type sentMsg struct {
101 raw string // the message as it went out
102 root [32]byte // the tree's root right after its id went in, at index = its nonce
103 height int64
104}
105
106// Key is a signer's ed25519 public key.
107type Key [32]byte
108
109// Rule is how many signers must sign: Threshold of the set, or — when both groups are named (§11.13) — one key of
110// GroupA and one of GroupB. The verifier applies it; Satisfiable says whether a set can still meet it.
111type Rule struct {
112 Threshold int
113 GroupA []Key
114 GroupB []Key
115}
116
117// TwoGroups is whether the rule is "one of A and one of B" rather than a threshold.
118func (r Rule) TwoGroups() bool { return len(r.GroupA) > 0 && len(r.GroupB) > 0 }
119
120// String says the rule in words.
121func (r Rule) String() string {
122 if r.TwoGroups() {
123 return "one of group A (" + keysHex(r.GroupA) + ") and one of group B (" + keysHex(r.GroupB) + ")"
124 }
125 return strconv.Itoa(r.Threshold) + " of the set"
126}
127
128func (r Rule) equal(o Rule) bool {
129 return r.Threshold == o.Threshold && sameKeys(r.GroupA, o.GroupA) && sameKeys(r.GroupB, o.GroupB)
130}
131
132// sameAs is whether two rules are the same rule, the groups compared as sets.
133func (r Rule) sameAs(o Rule) bool {
134 return r.Threshold == o.Threshold && sameSet(r.GroupA, o.GroupA) && sameSet(r.GroupB, o.GroupB)
135}
136
137// Verifier decides whether an incoming message is genuine (§5.1). A verifier is a realm; it registers once with
138// RegisterVerifier and GovDAO activates it by pkgpath. Both methods are non-crossing. An implementation is a type
139// declared in the registering realm: its methods then run under that realm's authority (borrow rule #1) and can
140// write nothing of this realm's. A type declared in /p/ with no object to anchor on (`type V int`) would run under
141// this realm's own authority instead — the no-anchor case — which RegisterVerifier cannot tell apart, so GovDAO
142// activates a verifier whose source it has read; what the mailbox hands a verifier is the recipient's bytes
143// (Verify) or a copy of its own keys (Satisfiable), never a value it keeps.
144type Verifier interface {
145 Verify(origin uint32, metadata, message []byte) error
146 Satisfiable(signers []Key) bool
147}
148
149func domainOf(chainID string) uint32 {
150 if chainID == mainnetChainID {
151 return DomainMainnet
152 }
153 return DomainTest
154}
155
156func solanaOf(chainID string) uint32 {
157 if chainID == mainnetChainID {
158 return SolanaMainnet
159 }
160 return SolanaLocal
161}
162
163func mustBytes32(a address) [32]byte {
164 b, err := hl.AddressToBytes32(a)
165 if err != nil {
166 panic(err.Error())
167 }
168 return b
169}
170
171func caller(cur realm) address {
172 if !cur.IsCurrent() {
173 panic("hlmailbox: that realm value is not live; pass your own cur")
174 }
175 return cur.Previous().Address()
176}
177
178func u32(n uint32) string { return strconv.FormatUint(uint64(n), 10) }
179
180func hx(b []byte) string { return hex.EncodeToString(b) }
181
182func keysHex(ks []Key) string {
183 s := make([]string, len(ks))
184 for i, k := range ks {
185 s[i] = hx(k[:])
186 }
187 return strings.Join(s, ",")
188}
189
190// nonceKey sorts nonces by number.
191func nonceKey(n uint32) string {
192 s := u32(n)
193 return strings.Repeat("0", 10-len(s)) + s
194}
195
196func domainKey(d uint32) string { return nonceKey(d) }
197
198func init() {
199 signerKeys = parseKeys(launchSigners)
200 signerRule = Rule{Threshold: launchThreshold}
201 active.Set(domainKey(launchOrigin), launchVerifier)
202 initSeats()
203 routes.Set(launchRouteGNOT, &routeRec{})
204 routes.Set(launchRouteBUBBLE, &routeRec{})
205}
206
207// ------------------------------------------------------------- reading --
208
209// LocalDomain is this chain's Hyperlane domain id.
210func LocalDomain() uint32 { return localDomain }
211
212// LaunchOrigin is the Solana domain the launch values name.
213func LaunchOrigin() uint32 { return launchOrigin }
214
215// Address is this realm's address.
216func Address() address { return chain.PackageAddress(pkgPath) }
217
218// Hook is this mailbox's merkle tree hook id in hex: the 32 bytes every checkpoint for a message from here names.
219func Hook() string { return hx(hook[:]) }
220
221// Count is how many messages went out: the next message's nonce.
222func Count() uint32 { return tree.Count }
223
224// Root is the merkle tree's root now, in hex.
225func Root() string {
226 r := tree.Root()
227 return hx(r[:])
228}
229
230// Live is whether the Launch decision has executed: before it the routes refuse sends and credits.
231func Live() bool { return live }
232
233// LaunchedAt is the unix time the Launch decision executed, 0 before: the routes' meters start empty there.
234func LaunchedAt() int64 { return launchedAt }
235
236// Governor is the compiled governor (M1k): the address whose own call decides until it yields; "" when GovDAO
237// governs from the first block. Every other bridge realm compiles the same and checks it here at deploy.
238func Governor() address { return launchGovernor }
239
240// Yielded is the block height the governor yielded at (Yield), 0 until then: what every bridge realm reads before
241// it accepts a decision — the governor's own call before, GovDAO's proxy after (decisions.DAOGoverns).
242func Yielded() int64 { return yielded }
243
244// Message is the message dispatched with this nonce, in hex, and the tree's root right after it ("", "" if none).
245func Message(nonce uint32) (message, root string) {
246 v := sent.Get(nonceKey(nonce))
247 if v == nil {
248 return "", ""
249 }
250 s := v.(*sentMsg)
251 return hx([]byte(s.raw)), hx(s.root[:])
252}
253
254// Delivered is the block height a message (by id, in hex) was delivered at here; 0 if it never was.
255func Delivered(id string) int64 {
256 v := done.Get(cleanID(id))
257 if v == nil {
258 return 0
259 }
260 return v.(int64)
261}
262
263func cleanID(id string) string { return strings.TrimPrefix(strings.ToLower(strings.TrimSpace(id)), "0x") }
264
265// Signers is the signer set and its rule: copies, so a verifier can read them and nobody can change them.
266func Signers() ([]Key, Rule) {
267 ks := append([]Key(nil), signerKeys...)
268 r := Rule{Threshold: signerRule.Threshold, GroupA: append([]Key(nil), signerRule.GroupA...), GroupB: append([]Key(nil), signerRule.GroupB...)}
269 return ks, r
270}
271
272// SignerKeys is the signer set in hex, comma separated, in the order signatures name them.
273func SignerKeys() string { return keysHex(signerKeys) }
274
275// ActiveVerifier is the pkgpath of the verifier GovDAO chose for an origin ("" if none).
276func ActiveVerifier(origin uint32) string {
277 v := active.Get(domainKey(origin))
278 if v == nil {
279 return ""
280 }
281 return v.(string)
282}
283
284// VerifierRegistered is whether a realm at this pkgpath registered as a verifier.
285func VerifierRegistered(path string) bool { return verifiers.Has(path) }
286
287// --------------------------------------------------------------- keys --
288
289func parseKey(s string) Key {
290 p := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(s)), "0x")
291 b, err := hex.DecodeString(p)
292 if err != nil || len(b) != 32 {
293 panic("hlmailbox: not a 32-byte ed25519 key in hex: " + p)
294 }
295 var k Key
296 copy(k[:], b)
297 return k
298}
299
300// parseKeys reads comma-separated keys in hex, refusing a key named twice.
301func parseKeys(s string) []Key {
302 if strings.TrimSpace(s) == "" {
303 return nil
304 }
305 parts := strings.Split(s, ",")
306 if len(parts) > maxSigners {
307 panic("hlmailbox: more than 255 keys")
308 }
309 ks := make([]Key, 0, len(parts))
310 for _, p := range parts {
311 k := parseKey(p)
312 if indexOf(ks, k) >= 0 {
313 panic("hlmailbox: a key named twice: " + hx(k[:]))
314 }
315 ks = append(ks, k)
316 }
317 return ks
318}
319
320func indexOf(ks []Key, k Key) int {
321 for i, x := range ks {
322 if x == k {
323 return i
324 }
325 }
326 return -1
327}
328
329// sameKeys is whether two lists hold the same keys in the same order.
330func sameKeys(a, b []Key) bool {
331 if len(a) != len(b) {
332 return false
333 }
334 for i := range a {
335 if a[i] != b[i] {
336 return false
337 }
338 }
339 return true
340}
341
342// subset is whether every key of a is in b.
343func subset(a, b []Key) bool {
344 for _, k := range a {
345 if indexOf(b, k) < 0 {
346 return false
347 }
348 }
349 return true
350}
351
352// sameSet is whether two lists hold the same keys, in any order (a list never names a key twice).
353func sameSet(a, b []Key) bool { return len(a) == len(b) && subset(a, b) && subset(b, a) }
354
355// ----------------------------------------------------------- verifiers --
356
357// RegisterVerifier is how a verifier realm offers itself: called once from that realm (its init, usually), it is
358// kept under the caller's pkgpath, which GovDAO names to activate it. Only an /r/ realm may register, and once;
359// a sub-realm identity (`cur.Sub(…)`, a token a realm mints for free, whose pkgpath `host#subpath` passes the
360// prefix test) is refused, so every entry costs a package deployment and the listing (bounded; render.gno)
361// cannot be flooded. Nothing is ever unregistered.
362func RegisterVerifier(cur realm, v Verifier) {
363 if !cur.IsCurrent() {
364 panic("hlmailbox: that realm value is not live; pass your own cur")
365 }
366 path := cur.Previous().PkgPath()
367 if !strings.HasPrefix(path, "gno.land/r/") {
368 panic("hlmailbox: only a realm under gno.land/r/ registers a verifier, not " + path)
369 }
370 if cur.Previous().Subpath() != "" {
371 panic("hlmailbox: a realm registers with its own cur, not a sub-identity (" + path + ")")
372 }
373 if v == nil {
374 panic("hlmailbox: a nil verifier")
375 }
376 if verifiers.Has(path) {
377 panic("hlmailbox: " + path + " registered already")
378 }
379 verifiers.Set(path, v)
380 chain.Emit("VerifierRegistered", "pkgpath", path)
381}
382
383// satisfiable asks every active verifier whether the signer set ks can still meet the rule; an active verifier
384// that is not registered answers no. Each verifier is handed its own copy of ks: the builders store ks after
385// asking, and a verifier that runs under this realm's authority (the no-anchor case, see Verifier) must find
386// nothing of the mailbox's to write into.
387func satisfiable(ks []Key) bool {
388 ok := true
389 active.Iterate("", "", func(_ string, v any) bool {
390 path := v.(string)
391 r := verifiers.Get(path)
392 if r == nil || !r.(Verifier).Satisfiable(append([]Key(nil), ks...)) {
393 ok = false
394 return true
395 }
396 return false
397 })
398 return ok
399}
400
401// ---------------------------------------------------------------- out --
402
403// Dispatch sends body to recipient on destination, from whoever calls it — a realm, usually — and returns the
404// message's id. The sender is the caller's address as Hyperlane carries one; the nonce is the message's index in
405// the merkle tree. The event is §13's: id, nonce, origin, destination, sender, recipient, hash.
406func Dispatch(cur realm, destination uint32, recipient [32]byte, body []byte) [32]byte {
407 sender := mustBytes32(caller(cur))
408 // a successor a re-point dropped is a route no more (R35): Solana's token may still name it the router until
409 // the next kind 6, so a message from it would mint against a lock no realm of the line holds
410 if path := cur.Previous().PkgPath(); path != "" {
411 if by := Dropped(path); by != "" {
412 panic("hlmailbox: " + path + " is a route no more (its predecessor " + by + " re-pointed the migration to another successor): it sends nothing")
413 }
414 }
415 m := hl.Message{
416 Version: hl.Version,
417 Nonce: tree.Count,
418 Origin: localDomain,
419 Sender: sender,
420 Destination: destination,
421 Recipient: recipient,
422 Body: body,
423 }
424 raw := m.Bytes()
425 id := hl.ID(raw)
426 tree.Insert(id)
427 root := tree.Root()
428 sent.Set(nonceKey(m.Nonce), &sentMsg{raw: string(raw), root: root, height: runtime.ChainHeight()})
429 emitDispatch(m, raw, id)
430 return id
431}
432
433func emitDispatch(m hl.Message, raw []byte, id [32]byte) {
434 h := sha256.Sum256(raw)
435 chain.Emit("Dispatch",
436 "id", hx(id[:]),
437 "nonce", u32(m.Nonce),
438 "origin", u32(m.Origin),
439 "destination", u32(m.Destination),
440 "sender", hx(m.Sender[:]),
441 "recipient", hx(m.Recipient[:]),
442 "hash", hx(h[:]),
443 )
444}
445
446// Reemit emits the Dispatch event of a message dispatched earlier again, in this block, by anyone: a message
447// dispatched before a validator-set change, or whose results leaf is bigger than the ISM accepts, is then delivered
448// from a fresh header. The id is unchanged, so the other side's processed-message record still refuses a second
449// delivery.
450func Reemit(cur realm, nonce uint32) {
451 caller(cur)
452 v := sent.Get(nonceKey(nonce))
453 if v == nil {
454 panic("hlmailbox: no message with nonce " + u32(nonce))
455 }
456 raw := []byte(v.(*sentMsg).raw)
457 m, err := hl.ParseMessage(raw)
458 if err != nil {
459 panic(err.Error())
460 }
461 emitDispatch(m, raw, hl.ID(raw))
462}
463
464// ----------------------------------------------------------------- in --
465
466// Process takes a message from another chain for the caller, who must be its recipient — or a route that
467// succeeds the recipient along the chain of migrations (AQ1: a burn addressed to a migrated route is the
468// successor's to credit, once its last page is in; the route decides when), a successor a re-point dropped from
469// that chain counted as a hop of its predecessor's (R35: the line's last realm takes what is addressed to it; the
470// dropped realm itself, a route no more, is refused). metadata is whatever the origin's active verifier needs
471// (the signers' checkpoint and signatures, for the ed25519 one). The message must be for this chain and never
472// delivered; it is marked delivered, then the verifier is asked, and a verifier's error is a panic, so the whole
473// transaction reverts with the mark and the message stays deliverable (§5.1). It is then handed back for the
474// recipient to act on.
475func Process(cur realm, metadata, message []byte) (origin uint32, sender [32]byte, body []byte) {
476 who := caller(cur)
477 callerPath := cur.Previous().PkgPath()
478 if by := Dropped(callerPath); by != "" {
479 panic("hlmailbox: " + callerPath + " is a route no more (its predecessor " + by + " re-pointed the migration to another successor): the line's last realm takes this message")
480 }
481 m, err := hl.ParseMessage(message)
482 if err != nil {
483 panic(err.Error())
484 }
485 if m.Version != hl.Version {
486 panic("hlmailbox: message version " + strconv.Itoa(int(m.Version)) + ", not " + strconv.Itoa(hl.Version))
487 }
488 if m.Destination != localDomain {
489 panic("hlmailbox: the message is for domain " + u32(m.Destination) + ", not this one (" + u32(localDomain) + ")")
490 }
491 if mustBytes32(who) != m.Recipient && !successorOf(callerPath, m.Recipient) {
492 panic("hlmailbox: only the message's recipient, or a successor of it, can take it")
493 }
494 idb := hl.ID(message)
495 id := hx(idb[:])
496 if done.Has(id) {
497 panic("hlmailbox: already delivered")
498 }
499 path := active.Get(domainKey(m.Origin))
500 if path == nil {
501 panic("hlmailbox: no verifier for domain " + u32(m.Origin))
502 }
503 v := verifiers.Get(path.(string))
504 if v == nil {
505 panic("hlmailbox: the active verifier " + path.(string) + " is not registered")
506 }
507 done.Set(id, runtime.ChainHeight())
508 if err := v.(Verifier).Verify(m.Origin, metadata, message); err != nil {
509 panic(err.Error())
510 }
511 chain.Emit("Process",
512 "origin", u32(m.Origin),
513 "nonce", u32(m.Nonce),
514 "id", id,
515 "sender", hx(m.Sender[:]),
516 "recipient", hx(m.Recipient[:]),
517 )
518 return m.Origin, m.Sender, m.Body
519}
520
521// MarkDone marks a message id delivered without a message: a route's credit-by-vote (§5.3) does it, so the id
522// can never be credited twice. Only a registered route may call it, and only for an id not done.
523func MarkDone(cur realm, id string) {
524 mustBeRoute(cur)
525 id = cleanID(id)
526 if b, err := hex.DecodeString(id); err != nil || len(b) != 32 {
527 panic("hlmailbox: not a 32-byte message id in hex: " + id)
528 }
529 if done.Has(id) {
530 panic("hlmailbox: already delivered")
531 }
532 done.Set(id, runtime.ChainHeight())
533}