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

v0 source pure

Package wesh implements the publishable, non-secret half of the Wesh protocol (weshnet, the network layer behind Bert...

Readme View source

gno.land/p/moul/x/wesh/v0

The publishable half of the Wesh protocol (weshnet, the network behind Berty) as a pure gno library: Contact, WebLink, ParseWebLink, RendezvousPoint, SeedCommitment, BindStatement, DeviceStatement, VerifyBind, VerifyDevice, HMACSHA256.

1import "gno.land/p/moul/x/wesh/v0"
2
3c := wesh.Contact{AccountPK: pk, Seed: seed, DisplayName: "Alice"}
4link, _ := c.WebLink()   // https://berty.tech/id#contact/<base58>/name=Alice
5pt, _ := c.RendezvousPointAt(now, wesh.DefaultRotationInterval)

Why a chain at all

Wesh is off-grid, peer-to-peer and end-to-end encrypted. A public chain is the opposite in nearly every dimension, so this is deliberately not "weshnet on chain". It carries only the parts that need an authenticated, ordered, publicly auditable record, the one thing a peer-to-peer network cannot give itself, and which Wesh is missing in three specific places:

  • Identities do not resolve. A Berty ID travels out of band as a QR code or a https://berty.tech/id# link, and there is no lookup.
  • Seed resets are silent. ContactRequestResetReference gives the account a new rendezvous seed, which kills every link ever shared, with no channel to announce it.
  • Devices can never be revoked. The protocol documentation says so outright: the account metadata log is append-only and no authority can void an entry.

What is in here

piece what it is
Contact weshnet's ShareableContact: account key + public rendezvous seed + display name, with weshnet's own CheckFormat length rules
Blob, WebLink, ParseWebLink the Berty link codec, hand-rolled protobuf + base58
RendezvousPoint, RoundPeriod, NextPeriod weshnet's rotating DHT topic derivation
SeedCommitment, OpenCommitment publish H(seed ‖ salt) instead of the seed
BindStatement, DeviceStatement, VerifyBind, VerifyDevice the canonical texts an account signs, and their ed25519 verification
HMACSHA256 RFC 2104, because gno has sha256.Sum256 but no crypto/hmac

The link codec is conformance-tested, not guessed: TestBlobEncodingMatches BertyGoldenVector reproduces the exact base58 payload from berty's own links_test.go, byte for byte. The rendezvous derivation is pinned against vectors computed with weshnet's GenerateRendezvousPointForPeriod, and the HMAC against RFC 4231, including case 6, the longer-than-block-size key.

What is deliberately absent

There is no type here that can hold a secret. Group secrets, device chain keys, message keys and ciphertexts never touch a chain: publishing a group secret hands the group to everyone, and publishing ciphertext is permanent, expensive, and leaks the social graph through access patterns.

The privacy trade-off

Publishing a rendezvous seed is equivalent to printing your Berty QR code on a billboard: anyone can then derive today's rendezvous point and watch the DHT for it. That is the right trade only for an identity that wants to be found. SeedCommitment is the alternative: the chain attests the binding, the seed travels out of band, and the rendezvous point stays private.

Cost

WebLink is base58 over a ~70-byte payload, which is big-integer work: TestWebLink reports ~19M gas for one encode, TestWebLinkRoundTrip ~61M per encode-plus-decode. That is fine in a Render (a query) and worth avoiding per-row in a list. Signature verification is a native op and costs far less.

Live realm: r/moul/x/wesh · render it at /r/moul/x/wesh/v0.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/p/moul/x/wesh/v0 dependency graph

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package wesh implements the publishable, non-secret half of the Wesh protocol (weshnet, the network layer behind Berty) as a pure gno library.

What this is, and what it deliberately is not

Wesh is an off-grid, peer-to-peer, end-to-end-encrypted messaging protocol. A public blockchain is the opposite of that in almost every dimension: globally replicated, permanent, publicly readable and totally ordered. So "put Wesh on chain" is the wrong goal, and this package does not pursue it.

What a chain *is* good at is the one thing Wesh has no answer for: an authenticated, ordered, publicly auditable record that no single party owns. Wesh has three gaps of exactly that shape:

  1. A Berty identity travels out of band, as a QR code or a https://berty.tech/id# link. There is no way to look one up, and the web prefix is a single host that can be blocked or spoofed.
  2. Resetting the public rendezvous seed (ContactRequestResetReference) silently invalidates every link ever shared. There is no revocation channel, so a business card keeps pointing at a dead rendezvous point.
  3. A device, once linked to an account, can never be revoked. The Wesh protocol documentation states this outright. The account metadata log is append-only and there is no authority that can mark an entry void.

This package carries the pieces needed to address those on chain, and nothing else. Everything here is public by construction:

  • Contact, the publishable identity, weshnet's ShareableContact (account public key + public rendezvous seed + display name).
  • The Berty link codec, byte-compatible with berty's own MarshalLink.
  • RendezvousPoint, the rotating DHT address derivation.
  • The canonical statements an account signs to bind itself to a gno address, rotate its seed, and append to a device sigchain.

What must never reach a chain

Group secrets (weshnet's Group.secret, Group.link_key), device chain keys, message keys, ciphertexts. Publishing a group secret hands the group to everyone; publishing ciphertext is permanent, expensive, and leaks the social graph through access patterns. This package has no type that can hold one, which is the point: there is no struct field here for a secret to accidentally land in.

The privacy trade-off, stated plainly

Publishing a rendezvous seed is equivalent to printing your Berty QR code on a billboard. Anyone can then derive today's rendezvous point with RendezvousPoint and watch the DHT for who shows up. That is a real deanonymization surface, and it is why publication must be an explicit, opt-in act for an identity that *wants* to be found: a support line, a shop, a DAO's public channel.

For an identity that does not want that, SeedCommitment publishes H(seed ‖ salt) instead. The chain then proves that a seed handed over out of band really does belong to the named account, without broadcasting the rendezvous point to the world.

Live realm using this library: r/moul/x/wesh(/r/moul/x/wesh/v0).

Constants 5

const SeedLen, AccountPKLen, DevicePKLen, MaxDisplayNameLen

 1const (
 2	// SeedLen is the length of a public rendezvous seed. Matches weshnet's
 3	// protocoltypes.RendezvousSeedLength.
 4	SeedLen = 32
 5
 6	// AccountPKLen is the length of an account public key. weshnet validates
 7	// it with libp2p's UnmarshalEd25519PublicKey, which accepts raw 32-byte
 8	// ed25519 public keys only.
 9	AccountPKLen = 32
10
11	// DevicePKLen is the length of a device public key: also a raw ed25519
12	// public key.
13	DevicePKLen = 32
14
15	// MaxDisplayNameLen bounds the display name so a link stays scannable and
16	// gas stays predictable. Not a protocol constant.
17	MaxDisplayNameLen = 64
18)
source

const DefaultRotationInterval

1const DefaultRotationInterval int64 = 86400
source

DefaultRotationInterval is weshnet's rendezvous rotation period, in seconds (rendezvous.DefaultRotationInterval = 24h). The rendezvous point an account announces on changes once per interval, so an observer who learns one point does not learn every future one.

const DigestLen

1const DigestLen = 32
source

DigestLen is the length of a statement digest (SHA-256).

const WebLinkPrefix

1const WebLinkPrefix = "https://berty.tech/id#"
source

WebLinkPrefix is the prefix of a shareable Berty web link. The fragment marker is part of it on purpose: everything after '#' stays in the browser and is never sent to berty.tech, so the web host never learns the identity being shared.

Variables 3

var ErrBadOp, ErrBadSignature, ErrBadSeq, ErrBadPrevLen

1var (
2	ErrBadOp        = errors.New("wesh: operation must be add or revoke")
3	ErrBadSignature = errors.New("wesh: signature does not verify under the account key")
4	ErrBadSeq       = errors.New("wesh: sequence number must not be negative")
5	ErrBadPrevLen   = errors.New("wesh: previous digest must be 32 bytes")
6)
source

var ErrBadSeedLen, ErrBadAccountPKLen, ErrBadDevicePKLen, ErrDisplayNameTooLong, ErrBadCommitmentLen, ErrBadHex

1var (
2	ErrBadSeedLen         = errors.New("wesh: public rendezvous seed must be 32 bytes")
3	ErrBadAccountPKLen    = errors.New("wesh: account public key must be a raw 32-byte ed25519 key")
4	ErrBadDevicePKLen     = errors.New("wesh: device public key must be a raw 32-byte ed25519 key")
5	ErrDisplayNameTooLong = errors.New("wesh: display name exceeds MaxDisplayNameLen")
6	ErrBadCommitmentLen   = errors.New("wesh: seed commitment must be 32 bytes")
7	ErrBadHex             = errors.New("wesh: value is not valid hex")
8)
source

Functions 18

func BindStatement

1func BindStatement(chainID, gnoAddr string, accountPK, payload []byte, revision int) string
source

BindStatement returns the canonical text an account signs, with its Wesh account private key, to claim a directory entry.

The signature is what makes a directory entry meaningful. Without it anyone could publish anyone else's account key next to a seed of their choosing and harvest the contact requests that followed. With it, a directory entry is a statement by the account itself, verifiable by anyone, replayable nowhere:

  • the kind prefix stops a signature made for one purpose being reused for another;
  • chainID stops a binding signed for a testnet being replayed on mainnet;
  • gnoAddr binds the Wesh identity to exactly one gno account, so a leaked signature cannot be used to claim the identity from a different address;
  • revision is monotonic, so an old, superseded binding cannot be replayed to roll a rotation back.

payload is the seed for a publicly published identity, or the seed commitment from SeedCommitment for one that keeps its rendezvous point off chain. The two are indistinguishable on the wire, which is deliberate: an observer cannot tell a committed identity from a published one without the realm's own mode flag.

Fields are newline-framed and hex-encoded, never concatenated raw: with plain concatenation a different split of the same bytes would produce the same statement.

func DecodeAccountPK

1func DecodeAccountPK(s string) ([]byte, error)
source

DecodeAccountPK parses a hex-encoded account public key.

func DecodeCommitment

1func DecodeCommitment(s string) ([]byte, error)
source

DecodeCommitment parses a hex-encoded seed commitment.

func DecodeDevicePK

1func DecodeDevicePK(s string) ([]byte, error)
source

DecodeDevicePK parses a hex-encoded device public key.

func DecodeSeed

1func DecodeSeed(s string) ([]byte, error)
source

DecodeSeed parses a hex-encoded public rendezvous seed.

func DeviceStatement

1func DeviceStatement(chainID string, accountPK []byte, seq int, prev []byte, op string, devicePK []byte) string
source

DeviceStatement returns the canonical text an account signs to append one entry to its device sigchain.

This is the piece the Wesh protocol has no answer for. Its own documentation says a device, once linked, can never be revoked: the account metadata log is append-only and no authority can mark an entry void. A chain cannot undo that (the ratchets are already out there and forward secrecy is a local property), but it can host the record that was missing, so a revocation becomes publicly visible, ordered, and attributable to the account key.

prev is the digest of the preceding statement, or GenesisDigest for seq 0. Chaining each entry to its predecessor means the log cannot be reordered or have an entry quietly dropped: any gap changes every digest after it.

func GenesisDigest

1func GenesisDigest() []byte
source

GenesisDigest is the all-zero digest that the first sigchain entry chains to.

func HMACSHA256

1func HMACSHA256(key, msg []byte) []byte
source

HMACSHA256 computes HMAC-SHA256 as specified in RFC 2104.

gno's standard library exposes sha256.Sum256 but no streaming hash.Hash and no crypto/hmac, so the construction is spelled out here:

Example
1HMAC(K, m) = H((K' ⊕ opad) ‖ H((K' ⊕ ipad) ‖ m))

with K' the key hashed down when longer than the block size, then zero-padded up to it. This is the primitive weshnet's rendezvous point derivation is built on, so it has to agree byte-for-byte; see RendezvousPoint.

func NextPeriod

1func NextPeriod(unixSec, interval int64) int64
source

NextPeriod returns the start of the period after the one containing unixSec. Mirrors weshnet's rendezvous.NextTimePeriod.

func OpenCommitment

1func OpenCommitment(commitment, seed, salt []byte) bool
source

OpenCommitment reports whether commitment is H(seed ‖ salt).

The comparison is constant-time over the digest: a short-circuiting compare leaks, through gas, how many leading bytes of a guess were right.

func RendezvousPoint

1func RendezvousPoint(topic, seed []byte, periodStart int64) []byte
source

RendezvousPoint derives the rotating DHT topic an account announces on during the period starting at periodStart.

It reproduces weshnet's rendezvous.GenerateRendezvousPointForPeriod exactly:

Example
1HMAC-SHA256(key = topic ‖ seed, msg = big-endian uint64(periodStart))

For contact requests, weshnet passes the account public key as the topic and the public rendezvous seed as the seed (see swiper.WatchTopic in contact_request_manager.go), which is what Contact.RendezvousPointAt does.

Deriving this on chain is what makes a published identity checkable: anyone can confirm that the seed in the directory really is the one the account is announcing under, without trusting the directory.

func RoundPeriod

1func RoundPeriod(unixSec, interval int64) int64
source

RoundPeriod returns the start of the rotation period containing unixSec. Mirrors weshnet's rendezvous.RoundTimePeriod.

func SeedCommitment

1func SeedCommitment(seed, salt []byte) ([]byte, error)
source

SeedCommitment returns H(seed ‖ salt), the value published instead of the seed itself when an account wants the chain to attest the binding without broadcasting its rendezvous point.

The salt is not optional: a seed is 32 bytes of entropy, but a commitment with no salt is a deterministic function of the seed, so two accounts that (impossibly, but still) shared a seed would be linkable, and a seed later disclosed out of band retroactively confirms every past commitment. The salt keeps disclosure a deliberate act.

func StatementDigest

1func StatementDigest(statement string) []byte
source

StatementDigest returns SHA-256 of a canonical statement. It is the value the next sigchain entry chains to.

func ValidOp

1func ValidOp(op string) bool
source

ValidOp reports whether op is a known sigchain operation.

func VerifyBind

1func VerifyBind(chainID, gnoAddr string, accountPK, payload []byte, revision int, sig []byte) error
source

VerifyBind checks a binding signature against the account key.

func VerifyDevice

1func VerifyDevice(chainID string, accountPK []byte, seq int, prev []byte, op string, devicePK, sig []byte) ([]byte, error)
source

VerifyDevice checks a sigchain entry's shape and its signature, and returns the digest the next entry must chain to.

Types 1

type Contact

struct
1type Contact struct {
2	AccountPK   []byte
3	Seed        []byte
4	DisplayName string
5}
source

Contact is the publishable identity of a Wesh account: weshnet's ShareableContact. It is everything a stranger needs to send a contact request, and it contains no secret.

AccountPK is the raw ed25519 account public key. Seed is the public rendezvous seed; together they derive the rotating rendezvous point the account announces on, see RendezvousPoint. DisplayName is free-form app metadata and is not authenticated by anything.

Methods on Contact

func Blob

method on Contact
1func (c Contact) Blob() ([]byte, error)
source

Blob returns the protobuf-encoded machine payload of a contact invite link: a BertyLink carrying only a BertyID with the rendezvous seed and account key.

The kind and the display name are deliberately absent. berty's MarshalLink puts the kind in the human-readable path segment and the display name in the query string, so a blob that carried them would not match a link produced by a real Berty client. The encoding here is verified against berty's own golden test vector; see link_test.gno.

func RendezvousPointAt

method on Contact
1func (c Contact) RendezvousPointAt(unixSec, interval int64) ([]byte, error)
source

RendezvousPointAt returns the contact's rendezvous point for the rotation period containing unixSec.

func Validate

method on Contact
1func (c Contact) Validate() error
source

Validate applies weshnet's own ShareableContact.CheckFormat rules: the seed must be exactly SeedLen bytes, and the account key must be a raw ed25519 public key. It deliberately does not verify that AccountPK is a valid curve point; that costs a scalar multiplication and the chain gets the same guarantee for free the first time it verifies a signature under the key.

Imports 9

Source Files 12