Files
p8k/README.md

11 KiB

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

  • No CGO Required: Uses dynamic library loading instead of CGO
  • Complete API Coverage: Bindings for all major libsecp256k1 functions
  • Module Support: ECDSA, Schnorr (BIP-340), ECDH, and Recovery modules
  • Cross-Platform: Works on Linux, macOS, and Windows
  • Performance: Direct C calls with minimal overhead

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:

  • Public key derivation
  • Schnorr signature creation (BIP-340)
  • Schnorr signature verification (BIP-340)
  • ECDH shared secret computation

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:

Operation BTCEC P256K1 P8K Speedup
Schnorr Sign 225,536 ns 28,855 ns 19,982 ns 11.3x faster 🚀
Schnorr Verify 153,205 ns 133,235 ns 36,541 ns 4.2x faster
ECDH 125,679 ns 97,435 ns 41,087 ns 3.1x faster 💨
Pubkey Derivation 32,226 ns 28,098 ns 19,329 ns 1.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

  • NewContext(flags uint32) (*Context, error) - Create a new secp256k1 context
  • Context.Destroy() - Destroy the context
  • Context.Randomize(seed32 []byte) error - Randomize the context with entropy

Public Key Operations

  • Context.CreatePublicKey(seckey []byte) ([]byte, error) - Generate public key from private key
  • Context.SerializePublicKey(pubkey []byte, compressed bool) ([]byte, error) - Serialize public key
  • Context.ParsePublicKey(input []byte) ([]byte, error) - Parse serialized public key

ECDSA Signatures

  • Context.Sign(msg32 []byte, seckey []byte) ([]byte, error) - Create ECDSA signature
  • Context.Verify(msg32 []byte, sig []byte, pubkey []byte) (bool, error) - Verify ECDSA signature
  • Context.SerializeSignatureDER(sig []byte) ([]byte, error) - Serialize signature to DER
  • Context.ParseSignatureDER(input []byte) ([]byte, error) - Parse DER signature
  • Context.SerializeSignatureCompact(sig []byte) ([]byte, error) - Serialize to compact format
  • Context.ParseSignatureCompact(input64 []byte) ([]byte, error) - Parse compact signature
  • Context.NormalizeSignature(sig []byte) ([]byte, bool, error) - Normalize to lower-S form

Schnorr Signatures (BIP-340)

  • Context.CreateKeypair(seckey []byte) (Keypair, error) - Create keypair for Schnorr
  • Context.KeypairXOnlyPub(keypair Keypair) (XOnlyPublicKey, int32, error) - Extract x-only pubkey
  • Context.SchnorrSign(msg32 []byte, keypair Keypair, auxRand32 []byte) ([]byte, error) - Sign with Schnorr
  • Context.SchnorrVerify(sig64 []byte, msg []byte, xonlyPubkey []byte) (bool, error) - Verify Schnorr signature
  • Context.ParseXOnlyPublicKey(input32 []byte) ([]byte, error) - Parse x-only public key
  • Context.SerializeXOnlyPublicKey(xonly []byte) ([]byte, error) - Serialize x-only public key
  • Context.XOnlyPublicKeyFromPublicKey(pubkey []byte) ([]byte, int32, error) - Convert to x-only

ECDH

  • Context.ECDH(pubkey []byte, seckey []byte) ([]byte, error) - Compute ECDH shared secret

Recovery

  • Context.SignRecoverable(msg32 []byte, seckey []byte) ([]byte, error) - Create recoverable signature
  • Context.SerializeRecoverableSignatureCompact(sig []byte) ([]byte, int32, error) - Serialize recoverable sig
  • Context.ParseRecoverableSignatureCompact(input64 []byte, recid int32) ([]byte, error) - Parse recoverable sig
  • Context.Recover(sig []byte, msg32 []byte) ([]byte, error) - Recover public key from signature

Constants

Context Flags

  • ContextNone - No context flags
  • ContextVerify - Context for signature verification
  • ContextSign - Context for signature creation
  • ContextDeclassify - Context for declassification

EC Flags

  • ECCompressed - Compressed public key format (33 bytes)
  • ECUncompressed - Uncompressed public key format (65 bytes)

Size Constants

  • PublicKeySize - 64 bytes (internal format)
  • CompressedPublicKeySize - 33 bytes
  • UncompressedPublicKeySize - 65 bytes
  • SignatureSize - 64 bytes (internal format)
  • CompactSignatureSize - 64 bytes
  • PrivateKeySize - 32 bytes
  • SharedSecretSize - 32 bytes
  • SchnorrSignatureSize - 64 bytes
  • RecoverableSignatureSize - 65 bytes

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.