README.md raw

p8k.mleku.dev

Go bindings for libsecp256k1 without CGO. This package uses dynamic library loading via purego to call C functions directly, eliminating the need for CGO.

Features

Requirements

Linux AMD64: A bundled libsecp256k1 is included for Linux AMD64 systems. No installation required! See LIBRARY.md for details.

Other Platforms: You must have libsecp256k1 installed on your system. The library can be built from source or installed via package managers.

Installation

Ubuntu/Debian:

sudo apt-get install libsecp256k1-dev

macOS (Homebrew):

brew install libsecp256k1

From Source:

git clone https://github.com/bitcoin-core/secp256k1
cd secp256k1
./autogen.sh
./configure --enable-module-recovery --enable-module-schnorrsig --enable-module-ecdh
make
sudo make install

Installation

go get p8k.mleku.dev

Benchmarks

A comprehensive benchmark suite is included in the /bench directory that compares this implementation against:

The benchmarks cover:

Running Benchmarks

cd bench

# Run all comparative benchmarks
make bench-quick

# Run comprehensive benchmarks with statistical analysis
make bench-all

# Run individual operation benchmarks
make bench-pubkey     # Public key derivation
make bench-sign       # Schnorr signing
make bench-verify     # Schnorr verification
make bench-ecdh       # ECDH key exchange

See the bench/README.md for more details.

Performance Results

P8K consistently outperforms pure Go implementations:

OperationBTCECP256K1P8KSpeedup
Schnorr Sign225,536 ns28,855 ns19,982 ns11.3x faster 🚀
Schnorr Verify153,205 ns133,235 ns36,541 ns4.2x faster âš¡
ECDH125,679 ns97,435 ns41,087 ns3.1x faster 💨
Pubkey Derivation32,226 ns28,098 ns19,329 ns1.7x faster ✨

See bench/BENCHMARK_RESULTS.md for detailed analysis.

Usage

Basic ECDSA Operations

package main

import (
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"log"

	"p8k.mleku.dev"
)

func main() {
	// Create a context for signing and verification
	ctx, err := secp.NewContext(secp.ContextSign | secp.ContextVerify)
	if err != nil {
		log.Fatal(err)
	}
	defer ctx.Destroy()

	// Generate a private key (32 random bytes)
	privKey := make([]byte, 32)
	if _, err := rand.Read(privKey); err != nil {
		log.Fatal(err)
	}

	// Create public key from private key
	pubKey, err := ctx.CreatePublicKey(privKey)
	if err != nil {
		log.Fatal(err)
	}

	// Serialize public key (compressed)
	pubKeyBytes, err := ctx.SerializePublicKey(pubKey, true)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Public key: %x\n", pubKeyBytes)

	// Sign a message
	message := []byte("Hello, libsecp256k1!")
	msgHash := sha256.Sum256(message)
	
	sig, err := ctx.Sign(msgHash[:], privKey)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Signature: %x\n", sig)

	// Verify the signature
	valid, err := ctx.Verify(msgHash[:], sig, pubKey)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Signature valid: %v\n", valid)
}

Schnorr Signatures (BIP-340)

package main

import (
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"log"

	"p8k.mleku.dev"
)

func main() {
	ctx, err := secp.NewContext(secp.ContextSign | secp.ContextVerify)
	if err != nil {
		log.Fatal(err)
	}
	defer ctx.Destroy()

	// Generate private key
	privKey := make([]byte, 32)
	if _, err := rand.Read(privKey); err != nil {
		log.Fatal(err)
	}

	// Create keypair for Schnorr
	keypair, err := ctx.CreateKeypair(privKey)
	if err != nil {
		log.Fatal(err)
	}

	// Extract x-only public key
	xonly, _, err := ctx.KeypairXOnlyPub(keypair)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("X-only public key: %x\n", xonly)

	// Sign with Schnorr
	message := []byte("Hello, Schnorr!")
	msgHash := sha256.Sum256(message)
	
	auxRand := make([]byte, 32)
	rand.Read(auxRand)
	
	sig, err := ctx.SchnorrSign(msgHash[:], keypair, auxRand)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Schnorr signature: %x\n", sig)

	// Verify Schnorr signature
	valid, err := ctx.SchnorrVerify(sig, msgHash[:], xonly[:])
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Schnorr signature valid: %v\n", valid)
}

ECDH Key Exchange

package main

import (
	"crypto/rand"
	"fmt"
	"log"

	"p8k.mleku.dev"
)

func main() {
	ctx, err := secp.NewContext(secp.ContextSign)
	if err != nil {
		log.Fatal(err)
	}
	defer ctx.Destroy()

	// Alice's keys
	alicePriv := make([]byte, 32)
	rand.Read(alicePriv)
	alicePub, _ := ctx.CreatePublicKey(alicePriv)

	// Bob's keys
	bobPriv := make([]byte, 32)
	rand.Read(bobPriv)
	bobPub, _ := ctx.CreatePublicKey(bobPriv)

	// Alice computes shared secret with Bob's public key
	aliceShared, err := ctx.ECDH(bobPub, alicePriv)
	if err != nil {
		log.Fatal(err)
	}

	// Bob computes shared secret with Alice's public key
	bobShared, err := ctx.ECDH(alicePub, bobPriv)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Alice's shared secret: %x\n", aliceShared)
	fmt.Printf("Bob's shared secret:   %x\n", bobShared)
	fmt.Printf("Secrets match: %v\n", string(aliceShared) == string(bobShared))
}

Public Key Recovery

package main

import (
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"log"

	"p8k.mleku.dev"
)

func main() {
	ctx, err := secp.NewContext(secp.ContextSign | secp.ContextVerify)
	if err != nil {
		log.Fatal(err)
	}
	defer ctx.Destroy()

	// Generate keys
	privKey := make([]byte, 32)
	rand.Read(privKey)
	originalPubKey, _ := ctx.CreatePublicKey(privKey)

	// Sign with recovery
	message := []byte("Recover me!")
	msgHash := sha256.Sum256(message)
	
	recSig, err := ctx.SignRecoverable(msgHash[:], privKey)
	if err != nil {
		log.Fatal(err)
	}

	// Serialize to get recovery ID
	sigBytes, recID, err := ctx.SerializeRecoverableSignatureCompact(recSig)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Signature: %x\n", sigBytes)
	fmt.Printf("Recovery ID: %d\n", recID)

	// Parse back
	parsedSig, err := ctx.ParseRecoverableSignatureCompact(sigBytes, recID)
	if err != nil {
		log.Fatal(err)
	}

	// Recover public key
	recoveredPubKey, err := ctx.Recover(parsedSig, msgHash[:])
	if err != nil {
		log.Fatal(err)
	}

	// Serialize both for comparison
	origSer, _ := ctx.SerializePublicKey(originalPubKey, true)
	recSer, _ := ctx.SerializePublicKey(recoveredPubKey, true)

	fmt.Printf("Original public key:  %x\n", origSer)
	fmt.Printf("Recovered public key: %x\n", recSer)
	fmt.Printf("Keys match: %v\n", string(origSer) == string(recSer))
}

API Reference

Context Management

Public Key Operations

ECDSA Signatures

Schnorr Signatures (BIP-340)

ECDH

Recovery

Constants

Context Flags

EC Flags

Size Constants

Performance

Since this library uses direct C calls via dynamic loading, performance is nearly identical to using CGO, with the added benefit of not requiring a C compiler during builds.

License

MIT License - See LICENSE file for details

Credits

This package provides Go bindings to libsecp256k1 by the Bitcoin Core developers.

Contributing

Contributions are welcome! Please open an issue or submit a pull request.