Skip to content
Sonr Crypto
Esc
navigateopen⌘Jpreview
On this page

Chain-Specific Schemes

Mina-protocol Schnorr over Pallas with Poseidon, and NEM's Keccak-512 flavoured Ed25519 — interop code for two specific networks, not general-purpose primitives.

Everything under signatures/schnorr exists to produce bytes that one particular blockchain will accept. These are not primitives you choose on cryptographic merit; you use them because you are talking to Mina or to NEM/Symbol and their consensus rules define the signature format down to the hash function. Both live under a schnorr directory, but only Mina is actually Schnorr — NEM is Ed25519 with a hash substitution.

If you are not integrating with those two networks, nothing on this page is for you. For general signing see BLS, ECDSA utilities, or threshold Ed25519.

Mina: Schnorr over Pallas

import "github.com/sonr-io/crypto/signatures/schnorr/mina"

Mina’s signature scheme is Schnorr on the Pallas curve with the Poseidon algebraic hash. Both choices exist because Mina’s recursive SNARKs must verify signatures inside a circuit, where SHA-256 is ruinously expensive and Poseidon is cheap. The package mirrors Mina’s C reference signer — the tests use that project’s key and transaction fixtures.

Signing computes k deterministically from the key, the public key, the network id, and the message (msgDerive), negates k when R has an odd y-coordinate, and returns (R.x, s) where s = k + e·sk and e is the Poseidon hash of the public key, R.x, the message, and the network id. There is no randomness at signing time.

Keys and addresses

PropType
NewKeys()?(*PublicKey, *SecretKey, error)

Fresh keypair from crypto/rand. Public key first. Errors on a zero scalar or identity point.

Type(*PublicKey, *SecretKey, error)
NewKeysFromReader(reader io.Reader)?(*PublicKey, *SecretKey, error)

Same, from a supplied reader — use for deterministic test fixtures.

Type(*PublicKey, *SecretKey, error)
SecretKey.GetPublicKey()?*PublicKey

Scalar multiplication of the Pallas generator. No error return.

Type*PublicKey
PublicKey.GenerateAddress()?string

Base58 Mina address: 0xcb version byte, 0x01 non-zero-curve-point version, 0x01 compressed flag, the 32-byte x coordinate, a y-parity byte, and a 4-byte double-SHA-256 checksum — 40 bytes encoded. These are the strings beginning "B62q".

Typestring
PublicKey.ParseAddress(b58 string)?error

Decodes and validates length, all three version bytes, and the checksum (compared in constant time) before recovering the point.

Typeerror
SecretKey.MarshalBinary()?([]byte, error)

32 bytes, the Fq scalar. UnmarshalBinary requires exactly 32.

Type([]byte, error)
PublicKey.MarshalBinary()?([]byte, error)

Compressed affine Pallas point. Distinct from the address encoding.

Type([]byte, error)

SetPointPallas(*curves.PointPallas) and SetFq(*fq.Fq) are the escape hatches that let a threshold signer inject externally-produced key material — see the FROST bridge below.

Signing

SignTransaction is the real API; SignMessage is a convenience for signing a plain string.

PropType
SecretKey.SignTransaction(txn *Transaction)?(*Signature, error)

Builds a random-oracle input with 3 field elements and 75 bytes of packed data, then signs under txn.NetworkId.

Type(*Signature, error)
SecretKey.SignMessage(message string)?(*Signature, error)

Signs the raw string bytes. Non-standard — the Mina reference signer does the same thing. Hardcoded to MainNet.

Type(*Signature, error)
PublicKey.VerifyTransaction(sig, txn)?error

nil means valid. Uses txn.NetworkId.

Typeerror
PublicKey.VerifyMessage(sig, message)?error

nil means valid. Also hardcoded to MainNet.

Typeerror

Signature is the only struct here with exported fields:

type Signature struct {
	R *fp.Fp // x coordinate of the nonce point, base field
	S *fq.Fq // response scalar, scalar field
}

MarshalBinary produces exactly 64 bytes, R then S; UnmarshalBinary requires exactly 64 and validates both field elements.

Grounded in TestSecretKeySignTransaction.

package main

import (
	"fmt"
	"log"

	"github.com/sonr-io/crypto/signatures/schnorr/mina"
)

func main() {
	pk, sk, err := mina.NewKeys()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("address:", pk.GenerateAddress())

	feePayer := new(mina.PublicKey)
	if err := feePayer.ParseAddress("B62qiy32p8kAKnny8ZFwoMhYpBppM1DWVCqAPBYNcXnsAHhnfAAuXgg"); err != nil {
		log.Fatal(err)
	}
	receiver := new(mina.PublicKey)
	if err := receiver.ParseAddress("B62qrcFstkpqXww1EkSGrqMCwCNho86kuqBd4FrAAUsPxNKdiPzAUsy"); err != nil {
		log.Fatal(err)
	}

	txn := &mina.Transaction{
		Fee:        3,
		FeeToken:   1,
		Nonce:      200,
		ValidUntil: 10000,
		Memo:       "this is a memo",
		FeePayerPk: feePayer,
		SourcePk:   feePayer,
		ReceiverPk: receiver,
		TokenId:    1,
		Amount:     42,
		Locked:     false,
		Tag:        [3]bool{false, false, false}, // all false = payment
		NetworkId:  mina.MainNet,
	}

	sig, err := sk.SignTransaction(txn)
	if err != nil {
		log.Fatal(err)
	}
	if err := sk.GetPublicKey().VerifyTransaction(sig, txn); err != nil {
		log.Fatal("invalid: ", err)
	}

	raw, err := sig.MarshalBinary()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("signature bytes:", len(raw)) // 64
}

Setting Tag: [3]bool{false, false, true} makes it a stake delegation instead of a payment, as in TestSecretKeySignTransactionStaking.

The Transaction type

PropType
Fee?uint64

Fee in nanomina.

Typeuint64
FeeToken?uint64

Token id used to pay the fee — 1 for MINA.

Typeuint64
FeePayerPk*PublicKey

Must be non-nil; MarshalBinary dereferences it.

Type*PublicKey
Nonce?uint32

Account nonce.

Typeuint32
ValidUntil?uint32

Expiry slot.

Typeuint32
Memo?string

At most 32 bytes — longer values are silently truncated. See the caveat below.

Typestring
Tag?[3]bool

Transaction kind. {false,false,false} is a payment; {false,false,true} is a stake delegation.

Type[3]bool
SourcePk*PublicKey

Sender. Must be non-nil.

Type*PublicKey
ReceiverPk*PublicKey

Recipient, or the new delegate. Must be non-nil.

Type*PublicKey
TokenId?uint64

Token being moved.

Typeuint64
Amount?uint64

Amount in nanomina. Zero for a delegation.

Typeuint64
Locked?bool

Timelock flag.

Typebool
NetworkId?NetworkType

Selects the Poseidon sponge IV and enters the nonce derivation. TestNet is the zero value.

TypeNetworkType

MarshalBinary writes a fixed 175-byte layout: fee, fee token, fee-payer point, nonce, valid until, a 0x01 marker, memo length, 32 memo bytes, three tag bytes, source point, receiver point, token id, amount, locked flag, and finally the network id at offset 174. UnmarshalBinary reverses it and requires that exact length. This encoding is what the FROST bridge parses.

Network types

NetworkType selects the Poseidon sponge initialisation vector and is mixed into the nonce derivation, so a signature made for one network is invalid on another — that is deliberate replay protection.

Constant Value Meaning
TestNet 0 Mina testnet IV. Also the zero value of NetworkType, so a Transaction you forgot to fill in is a testnet transaction.
MainNet 1 Mina mainnet IV.
NullNet 2 Zero-initialised sponge state, no IV. Used by the Poseidon unit tests for raw-permutation vectors.

Poseidon internals

You do not need these to sign, but they are exported and occasionally useful for testing a circuit against the same hash.

PropType
Permutation (int)?ThreeW | FiveW | Three

Which Poseidon parameter set to run. Values 0, 1, 2. Every signing path in the package uses ThreeW.

TypeThreeW | FiveW | Three
SBox (int)?Cube | Quint | Sept | Inverse

The exponentiation applied in each round: x^3, x^5, x^7, x^-1. Values 0..3. Selected by the parameter set, not by the caller. SBox.Exp(f *fp.Fp) mutates f in place.

TypeCube | Quint | Sept | Inverse
Context?struct

The Poseidon sponge. Init(pType, networkId) loads round constants, MDS matrix, and IV; Update(fields []*fp.Fp) absorbs, permuting whenever the rate fills; Digest() permutes a final time and returns state[0] reinterpreted as an Fq scalar.

Typestruct
BitVector?struct

Variable-length bit buffer with Append, Insert, Delete, Set, Element, Length, Bytes. Used to pack transaction fields into field elements. Documented as not thread safe.

Typestruct
Permutation.Permute(ctx *Context)?

Runs the permutation in place on a Context.

Type
ctx := new(mina.Context).Init(mina.ThreeW, mina.MainNet)
ctx.Update(fields)     // []*fp.Fp
digest := ctx.Digest() // *fq.Fq

The task-facing type roinput — the random-oracle input builder that packs a transaction into field elements and bits — is unexported. You cannot construct one, and the only way to reach that packing logic is through SignTransaction, SignMessage, or MinaTSchnorrHandler.

Bridging to threshold signing

MinaTSchnorrHandler adapts Mina’s challenge derivation to the library’s FROST-style threshold Schnorr signer, so a Mina key can be split across parties. See threshold Ed25519 for the signer this plugs into.

func (m MinaTSchnorrHandler) DeriveChallenge(
	msg []byte,
	pubKey curves.Point, // must be a *curves.PointPallas
	r curves.Point,      // must be a *curves.PointPallas
) (curves.Scalar, error)

msg is not an arbitrary message: the handler calls Transaction.UnmarshalBinary(msg) on it, so it must be the 175-byte transaction encoding produced by Transaction.MarshalBinary. Anything else returns “invalid byte sequence”.

Mina caveats

NEM: Ed25519 with Keccak-512

import "github.com/sonr-io/crypto/signatures/schnorr/nem"

NEM (and its successor Symbol) adopted Ed25519 before the standard settled and substituted Keccak-512 for SHA-512 in every hashing step — key expansion, nonce derivation, and challenge computation. There is one further quirk: the seed is byte-reversed before hashing, which the source comments call a “weird required step to get compatibility with the NEM test vectors”.

Everything else is textbook Ed25519 over Edwards25519, and this package is unusually well grounded: ed25519_keccak_test.go checks derivation and signing against fixtures pulled from symbol/test-vectors, with a comment noting that all 10000 vectors passed at the time of writing.

Constants and API

Constant Value
PublicKeySize 32
PrivateKeySize 64
SignatureSize 64
SeedSize 32
PropType
GenerateKey(rand io.Reader)?(PublicKey, PrivateKey, error)

Reads a 32-byte seed (crypto/rand when rand is nil) and expands it. Public key first.

Type(PublicKey, PrivateKey, error)
NewKeyFromSeed(seed []byte)?(PrivateKey, error)

Deterministic derivation from a 32-byte seed. Reverses the seed, hashes with Keccak-512, clamps the low 32 bytes into a scalar, and stores seed ‖ publicKey.

Type(PrivateKey, error)
Sign(privateKey PrivateKey, message []byte)?([]byte, error)

64-byte signature. Errors — does not panic — on a wrong-length key, despite what the doc comment says.

Type([]byte, error)
Verify(publicKey PublicKey, message, sig []byte)?(bool, error)

Note the two return values: check both.

Type(bool, error)
Keccak512(data []byte)?([]byte, error)

Exported because the surrounding NEM protocol hashes with it too — addresses, block hashes.

Type([]byte, error)
PrivateKey.Public()?crypto.PublicKey

Returns a nem.PublicKey as crypto.PublicKey. Type-assert it: priv.Public().(nem.PublicKey).

Typecrypto.PublicKey
PrivateKey.Seed()?[]byte

A copy of the leading 32 bytes.

Type[]byte
PrivateKey.Sign(rand, message, opts)?([]byte, error)

The crypto.Signer interface. opts.HashFunc() must be crypto.Hash(0); rand is ignored because signing is deterministic.

Type([]byte, error)
PublicKey.Bytes()?[]byte

The underlying slice.

Type[]byte

Grounded in TestPrivToPubkey and TestSigs.

package main

import (
	"encoding/hex"
	"fmt"
	"log"

	"github.com/sonr-io/crypto/signatures/schnorr/nem"
)

func main() {
	// A NEM test vector: private key and its expected public key.
	seed, err := hex.DecodeString(
		"575DBB3062267EFF57C970A336EBBC8FBCFE12C5BD3ED7BC11EB0481D7704CED")
	if err != nil {
		log.Fatal(err)
	}

	priv, err := nem.NewKeyFromSeed(seed)
	if err != nil {
		log.Fatal(err)
	}

	pub := priv.Public().(nem.PublicKey)
	fmt.Println("public key:",
		hex.EncodeToString(pub.Bytes()))
	// c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844
	// — the public key the symbol/test-vectors fixture pairs with that seed.

	msg := []byte("transfer")
	sig, err := nem.Sign(priv, msg)
	if err != nil {
		log.Fatal(err)
	}

	ok, err := nem.Verify(pub, msg, sig)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("valid:", ok, "bytes:", len(sig)) // true 64
}

NEM caveats

Last updated on September 2, 2026

Was this page helpful?