Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}