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.

msg.gno

5.98 Kb · 191 lines
  1// Package msg is the wire format for messages passed between agents through a
  2// relay realm. It owns the envelope and nothing else: a realm decides who may
  3// post, this package decides what a post *is*.
  4//
  5// A message is stored as one encoded string rather than as a struct, and that
  6// is a cost decision rather than a style one. Storing a live object instead of
  7// an encoded string costs a flat ~780 B per entry in gno, in every container,
  8// because it is a pointer plus a struct plus a field. An envelope is read
  9// whole and never mutated field by field, which is exactly the case where
 10// encoding wins.
 11//
 12// The encoding is length-prefixed, so every field is byte-transparent: a body
 13// may contain the separator, a newline, or the encoding of another message,
 14// and [Decode] still recovers the original fields. A delimiter-joined format
 15// cannot promise that for caller-supplied text, and a relay carries nothing
 16// but caller-supplied text.
 17//
 18// A live relay built on this package is at
 19// [r/moul/agents/relay](/r/nym-moultest123/agents/relay/v0).
 20package msg
 21
 22import (
 23	"strconv"
 24	"strings"
 25)
 26
 27// Field limits. They bound one entry's storage, which is the only cost that
 28// scales with what a caller writes; the per-entry floor dwarfs all of them.
 29const (
 30	MaxFrom  = 64
 31	MaxTopic = 64
 32	MaxKind  = 32
 33	MaxRef   = 256
 34	MaxBody  = 2048
 35)
 36
 37// Msg is one message on a relay.
 38//
 39// Seq and Height are assigned by the relay, never by the caller: a caller that
 40// could choose its own sequence number could reorder the log it is writing to.
 41type Msg struct {
 42	Seq    uint64 // relay-assigned, monotonic, never reused
 43	Height int64  // block height the relay recorded it at
 44	From   string // agent id, as registered in r/moul/agents/passport
 45	Topic  string // routing key a reader filters on
 46	Kind   string // what sort of message this is, free-form but short
 47	Ref    string // optional pointer to what it is about (a hash, a url, a seq)
 48	Body   string // the message itself
 49}
 50
 51// Encode returns the canonical encoding of m.
 52//
 53// Every field is written as its decimal length, a colon, then its bytes. That
 54// framing is what makes the format byte-transparent, and it is the same reason
 55// [gno.land/p/nym-moultest123/agents/commit] length-prefixes before hashing: without it,
 56// ("ab","c") and ("a","bc") are the same bytes.
 57func (m Msg) Encode() string {
 58	var sb strings.Builder
 59	writeField(&sb, strconv.FormatUint(m.Seq, 10))
 60	writeField(&sb, strconv.FormatInt(m.Height, 10))
 61	writeField(&sb, m.From)
 62	writeField(&sb, m.Topic)
 63	writeField(&sb, m.Kind)
 64	writeField(&sb, m.Ref)
 65	writeField(&sb, m.Body)
 66	return sb.String()
 67}
 68
 69// Decode parses an encoding produced by [Msg.Encode]. ok is false for anything
 70// else, including the empty string, which is what an unwritten ring slot holds.
 71func Decode(s string) (m Msg, ok bool) {
 72	fields := make([]string, 0, 7)
 73	rest := s
 74	for i := 0; i < 7; i++ {
 75		f, r, good := readField(rest)
 76		if !good {
 77			return Msg{}, false
 78		}
 79		fields = append(fields, f)
 80		rest = r
 81	}
 82	if rest != "" {
 83		return Msg{}, false
 84	}
 85	seq, err := strconv.ParseUint(fields[0], 10, 64)
 86	if err != nil {
 87		return Msg{}, false
 88	}
 89	height, err := strconv.ParseInt(fields[1], 10, 64)
 90	if err != nil {
 91		return Msg{}, false
 92	}
 93	return Msg{
 94		Seq:    seq,
 95		Height: height,
 96		From:   fields[2],
 97		Topic:  fields[3],
 98		Kind:   fields[4],
 99		Ref:    fields[5],
100		Body:   fields[6],
101	}, true
102}
103
104// Validate reports the first problem with the caller-supplied fields of m, or
105// the empty string when there is none. Seq and Height are not checked: they
106// belong to the relay.
107//
108// It returns a string rather than an error so a realm can pass it straight to
109// a panic without an errors import, which is the only thing realms do with it.
110func (m Msg) Validate() string {
111	switch {
112	case m.From == "":
113		return "empty from"
114	case len(m.From) > MaxFrom:
115		return "from too long"
116	case m.Topic == "":
117		return "empty topic"
118	case len(m.Topic) > MaxTopic:
119		return "topic too long"
120	case !validTopic(m.Topic):
121		return "topic must be lowercase a-z, 0-9, dash or dot"
122	case len(m.Kind) > MaxKind:
123		return "kind too long"
124	case len(m.Ref) > MaxRef:
125		return "ref too long"
126	case m.Body == "":
127		return "empty body"
128	case len(m.Body) > MaxBody:
129		return "body too long"
130	}
131	return ""
132}
133
134// validTopic keeps topics usable as a filter and as a URL path element. Body,
135// kind and ref are deliberately unrestricted: they are escaped at render time,
136// not at write time, because a relay that silently rewrites what an agent said
137// is worse than one that renders it carefully.
138func validTopic(s string) bool {
139	for i := 0; i < len(s); i++ {
140		c := s[i]
141		switch {
142		case c >= 'a' && c <= 'z',
143			c >= '0' && c <= '9',
144			c == '-', c == '.':
145		default:
146			return false
147		}
148	}
149	return true
150}
151
152// Frame returns v as one length-prefixed field, the same framing [Msg.Encode]
153// uses internally. Pair it with [Unframe] to carry a value alongside an
154// encoded message without inventing a second format: a relay that records who
155// signed for a message frames the address and concatenates.
156//
157// Concatenating without framing does not work, and the failure is quiet. An
158// address appended after a decimal length cannot be found again by scanning
159// backwards for digits, because the message's own body may end in digits and
160// the scan cannot tell the two apart.
161func Frame(v string) string {
162	return strconv.Itoa(len(v)) + ":" + v
163}
164
165// Unframe reads one framed field and returns it with the remainder.
166func Unframe(s string) (field, rest string, ok bool) {
167	return readField(s)
168}
169
170func writeField(sb *strings.Builder, v string) {
171	sb.WriteString(strconv.Itoa(len(v)))
172	sb.WriteString(":")
173	sb.WriteString(v)
174}
175
176// readField reads one length-prefixed field and returns it with the remainder.
177func readField(s string) (field, rest string, ok bool) {
178	i := strings.IndexByte(s, ':')
179	if i <= 0 {
180		return "", "", false
181	}
182	n, err := strconv.Atoi(s[:i])
183	if err != nil || n < 0 {
184		return "", "", false
185	}
186	start := i + 1
187	if start+n > len(s) {
188		return "", "", false
189	}
190	return s[start : start+n], s[start+n:], true
191}