---
title: Chain-Specific Schemes
description: Mina-protocol Schnorr over Pallas with Poseidon, and NEM's Keccak-512 flavoured Ed25519 — interop code for two specific networks, not general-purpose primitives.
sidebar:
order: 6
icon: link
---
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](/signatures/bls), [ECDSA utilities](/signatures/ecdsa), or
[threshold Ed25519](/threshold/threshold-ed25519).
## Mina: Schnorr over Pallas
```go
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](https://github.com/MinaProtocol/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
`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.
`Signature` is the only struct here with exported fields:
```go
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`.
```go mina.go
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
`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.
```go
ctx := new(mina.Context).Init(mina.ThreeW, mina.MainNet)
ctx.Update(fields) // []*fp.Fp
digest := ctx.Digest() // *fq.Fq
```
:::warning[`Context.Init` reports failure by returning `nil`]
An out-of-range `Permutation` or `NetworkType` makes `Init` return a nil `*Context` rather than an
error. The very next `ctx.Update(...)` panics with a nil dereference. Check the return value.
:::
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](/threshold/threshold-ed25519) for the signer this plugs into.
```go
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".
:::danger[`DeriveChallenge` ignores the transaction's network id]
The handler hardcodes `msgHash(pk, R.X(), input, ThreeW, MainNet)`. It parses `msg` into a
`Transaction`, which carries a `NetworkId` field, and then discards it. Threshold-signing a testnet
transaction through this handler produces a challenge computed with the **mainnet** sponge IV, so
the resulting signature will not verify with `VerifyTransaction` for any `NetworkId` other than
`MainNet`. There is no way to override this from the outside.
:::
### Mina caveats
:::danger[`Transaction.UnmarshalJSON` does not work]
The method type-asserts `Body[1]` from `any` directly to its concrete struct types
(`txnBodyPaymentJson`, `[2]any`). `encoding/json` decodes an unconstrained `any` into
`map[string]any` and `[]any`, so those assertions can never succeed. Every call returns
`unexpected type`. Confirmed against a well-formed payload built from the fixtures in
`keys_test.go`:
```
UnmarshalJSON err = unexpected type
SourcePk nil: true Amount: 0 TokenId: 0
```
Even if the assertions were fixed, the method never assigns `SourcePk`, `Amount`, `TokenId`,
`Locked`, or `Tag`; it computes a `sourcePk` local and drops it; it swallows a `ParseAddress` error
in the payment branch with a bare `return nil`; and it indexes the decoded memo as
`memo[2 : 2+memo[1]]` without a length check. There is no corresponding `MarshalJSON`. Build
`Transaction` values in Go and use `MarshalBinary` for the wire; do not route Mina transactions
through this JSON path.
:::
:::warning[Memos longer than 32 bytes corrupt the encoding]
`MarshalBinary` writes `out[57] = byte(len(txn.Memo))` and then `copy(out[58:90], txn.Memo)`. The
copy caps at the 32-byte destination, but the recorded length does not — so a 40-byte memo produces
a transaction claiming length 40 with only 32 bytes present, and a 256-byte memo records length 0.
Neither is rejected. Truncate memos to 32 bytes yourself before signing.
:::
:::warning[`SignMessage` and `VerifyMessage` are MainNet-only]
Both hardcode `MainNet`; there is no network parameter and no variant that takes one. Only
`SignTransaction` / `VerifyTransaction` honour `NetworkId`.
:::
:::note[Nil public keys panic]
`Transaction.MarshalBinary` dereferences `FeePayerPk`, `SourcePk`, and `ReceiverPk` without nil
checks. A partially-filled `Transaction` panics rather than returning an error.
:::
## NEM: Ed25519 with Keccak-512
```go
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](https://github.com/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` |
Grounded in `TestPrivToPubkey` and `TestSigs`.
```go nem.go
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
:::danger[These keys and signatures are NOT interchangeable with `crypto/ed25519`]
The hash function differs at every step, and the seed is reversed before expansion. Concretely:
- `nem.NewKeyFromSeed(seed)` and `ed25519.NewKeyFromSeed(seed)` derive **different public keys** from
the same seed.
- A `nem.PrivateKey` handed to `ed25519.Sign` produces a signature that does not verify under the
public key embedded in that same private key.
- A signature from `nem.Sign` will never verify with `ed25519.Verify`, and vice versa.
Both types are `[]byte` with identical 32/64-byte layouts, so nothing in the type system stops you
mixing them. Never share a seed, a key, or a signature between the two. If you need standards
Ed25519, use `crypto/ed25519` or [threshold Ed25519](/threshold/threshold-ed25519).
:::
:::warning[Doc comments promise panics the code does not deliver]
`Sign`, `Verify`, and `NewKeyFromSeed` carry doc comments inherited from the standard library saying
"It will panic if len(...) is not ...". The implementations return an `error` instead. Code written
against the comments — assuming a wrong length is unreachable and therefore ignoring the error — will
silently proceed with a `nil` signature.
:::
:::note[`Verify` returns `(bool, error)`]
Unlike `crypto/ed25519.Verify`, which returns a bare `bool`. The error path fires when the internal
Keccak write fails or the public key is malformed. Check both values.
:::
## Related
The FROST signer that `MinaTSchnorrHandler` plugs its challenge derivation into.
Pallas, `PointPallas`, and `ScalarPallas` — the types the Mina package builds on.
Back to the scheme selection guide.
The defects on this page, collected with the rest.