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:
- BTCEC - The btcsuite implementation (github.com/btcsuite/btcd/btcec)
- P256K1 - Pure Go implementation (github.com/mleku/p256k1)
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 contextContext.Destroy()- Destroy the contextContext.Randomize(seed32 []byte) error- Randomize the context with entropy
Public Key Operations
Context.CreatePublicKey(seckey []byte) ([]byte, error)- Generate public key from private keyContext.SerializePublicKey(pubkey []byte, compressed bool) ([]byte, error)- Serialize public keyContext.ParsePublicKey(input []byte) ([]byte, error)- Parse serialized public key
ECDSA Signatures
Context.Sign(msg32 []byte, seckey []byte) ([]byte, error)- Create ECDSA signatureContext.Verify(msg32 []byte, sig []byte, pubkey []byte) (bool, error)- Verify ECDSA signatureContext.SerializeSignatureDER(sig []byte) ([]byte, error)- Serialize signature to DERContext.ParseSignatureDER(input []byte) ([]byte, error)- Parse DER signatureContext.SerializeSignatureCompact(sig []byte) ([]byte, error)- Serialize to compact formatContext.ParseSignatureCompact(input64 []byte) ([]byte, error)- Parse compact signatureContext.NormalizeSignature(sig []byte) ([]byte, bool, error)- Normalize to lower-S form
Schnorr Signatures (BIP-340)
Context.CreateKeypair(seckey []byte) (Keypair, error)- Create keypair for SchnorrContext.KeypairXOnlyPub(keypair Keypair) (XOnlyPublicKey, int32, error)- Extract x-only pubkeyContext.SchnorrSign(msg32 []byte, keypair Keypair, auxRand32 []byte) ([]byte, error)- Sign with SchnorrContext.SchnorrVerify(sig64 []byte, msg []byte, xonlyPubkey []byte) (bool, error)- Verify Schnorr signatureContext.ParseXOnlyPublicKey(input32 []byte) ([]byte, error)- Parse x-only public keyContext.SerializeXOnlyPublicKey(xonly []byte) ([]byte, error)- Serialize x-only public keyContext.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 signatureContext.SerializeRecoverableSignatureCompact(sig []byte) ([]byte, int32, error)- Serialize recoverable sigContext.ParseRecoverableSignatureCompact(input64 []byte, recid int32) ([]byte, error)- Parse recoverable sigContext.Recover(sig []byte, msg32 []byte) ([]byte, error)- Recover public key from signature
Constants
Context Flags
ContextNone- No context flagsContextVerify- Context for signature verificationContextSign- Context for signature creationContextDeclassify- 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 bytesUncompressedPublicKeySize- 65 bytesSignatureSize- 64 bytes (internal format)CompactSignatureSize- 64 bytesPrivateKeySize- 32 bytesSharedSecretSize- 32 bytesSchnorrSignatureSize- 64 bytesRecoverableSignatureSize- 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.