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
NewKeys()?(*PublicKey, *SecretKey, error)
Fresh keypair from crypto/rand. Public key first. Errors on a zero scalar or identity point.
(*PublicKey, *SecretKey, error)NewKeysFromReader(reader io.Reader)?(*PublicKey, *SecretKey, error)
Same, from a supplied reader — use for deterministic test fixtures.
(*PublicKey, *SecretKey, error)SecretKey.GetPublicKey()?*PublicKey
Scalar multiplication of the Pallas generator. No error return.
*PublicKeyPublicKey.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".
stringPublicKey.ParseAddress(b58 string)?error
Decodes and validates length, all three version bytes, and the checksum (compared in constant time) before recovering the point.
errorSecretKey.MarshalBinary()?([]byte, error)
32 bytes, the Fq scalar. UnmarshalBinary requires exactly 32.
([]byte, error)PublicKey.MarshalBinary()?([]byte, error)
Compressed affine Pallas point. Distinct from the address encoding.
([]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.
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.
(*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.
(*Signature, error)PublicKey.VerifyTransaction(sig, txn)?error
nil means valid. Uses txn.NetworkId.
errorPublicKey.VerifyMessage(sig, message)?error
nil means valid. Also hardcoded to MainNet.
errorSignature 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
Fee?uint64
Fee in nanomina.
uint64FeeToken?uint64
Token id used to pay the fee — 1 for MINA.
uint64FeePayerPk*PublicKey
Must be non-nil; MarshalBinary dereferences it.
*PublicKeyNonce?uint32
Account nonce.
uint32ValidUntil?uint32
Expiry slot.
uint32Memo?string
At most 32 bytes — longer values are silently truncated. See the caveat below.
stringTag?[3]bool
Transaction kind. {false,false,false} is a payment; {false,false,true} is a stake delegation.
[3]boolSourcePk*PublicKey
Sender. Must be non-nil.
*PublicKeyReceiverPk*PublicKey
Recipient, or the new delegate. Must be non-nil.
*PublicKeyTokenId?uint64
Token being moved.
uint64Amount?uint64
Amount in nanomina. Zero for a delegation.
uint64Locked?bool
Timelock flag.
boolNetworkId?NetworkType
Selects the Poseidon sponge IV and enters the nonce derivation. TestNet is the zero value.
NetworkTypeMarshalBinary 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.
Permutation (int)?ThreeW | FiveW | Three
Which Poseidon parameter set to run. Values 0, 1, 2. Every signing path in the package uses ThreeW.
ThreeW | FiveW | ThreeSBox (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.
Cube | Quint | Sept | InverseContext?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.
structBitVector?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.
structPermutation.Permute(ctx *Context)?
Runs the permutation in place on a Context.
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 |
GenerateKey(rand io.Reader)?(PublicKey, PrivateKey, error)
Reads a 32-byte seed (crypto/rand when rand is nil) and expands it. Public key first.
(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.
(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.
([]byte, error)Verify(publicKey PublicKey, message, sig []byte)?(bool, error)
Note the two return values: check both.
(bool, error)Keccak512(data []byte)?([]byte, error)
Exported because the surrounding NEM protocol hashes with it too — addresses, block hashes.
([]byte, error)PrivateKey.Public()?crypto.PublicKey
Returns a nem.PublicKey as crypto.PublicKey. Type-assert it: priv.Public().(nem.PublicKey).
crypto.PublicKeyPrivateKey.Seed()?[]byte
A copy of the leading 32 bytes.
[]bytePrivateKey.Sign(rand, message, opts)?([]byte, error)
The crypto.Signer interface. opts.HashFunc() must be crypto.Hash(0); rand is ignored because signing is deterministic.
([]byte, error)PublicKey.Bytes()?[]byte
The underlying slice.
[]byteGrounded 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
Related
Threshold Ed25519
The FROST signer that MinaTSchnorrHandler plugs its challenge derivation into.
Curve abstraction
Pallas, PointPallas, and ScalarPallas — the types the Mina package builds on.
Signature index
Back to the scheme selection guide.
Security notes
The defects on this page, collected with the rest.