//go:build !js package p256k1 import ( "errors" "sync" "github.com/ebitengine/purego" ) // LibSecp256k1 wraps the native libsecp256k1.so library using purego for CGO-free operation. // This provides a way to benchmark against the C implementation without CGO. type LibSecp256k1 struct { lib uintptr ctx uintptr loaded bool mu sync.RWMutex // Function pointers contextCreate func(uint) uintptr contextDestroy func(uintptr) contextRandomize func(uintptr, *byte) int schnorrsigSign32 func(uintptr, *byte, *byte, *byte, *byte) int schnorrsigVerify func(uintptr, *byte, *byte, uint, *byte) int keypairCreate func(uintptr, *byte, *byte) int keypairXonlyPub func(uintptr, *byte, *int, *byte) int xonlyPubkeyParse func(uintptr, *byte, *byte) int ecPubkeyCreate func(uintptr, *byte, *byte) int ecPubkeyParse func(uintptr, *byte, *byte, uint) int ecPubkeySerialize func(uintptr, *byte, *uint, *byte, uint) int xonlyPubkeySerialize func(uintptr, *byte, *byte) int ecdh func(uintptr, *byte, *byte, *byte, uintptr, uintptr) int } // Secp256k1 context flags // In modern libsecp256k1, SECP256K1_CONTEXT_NONE = 1 is the only valid flag. // The old SIGN (256) and VERIFY (257) flags are deprecated. const ( libContextNone = 1 ) // Global instance var ( libSecp *LibSecp256k1 libSecpOnce sync.Once libSecpInitErr error ) // GetLibSecp256k1 returns the global LibSecp256k1 instance, loading it if necessary. // Returns nil and an error if the library cannot be loaded. func GetLibSecp256k1() (*LibSecp256k1, error) { libSecpOnce.Do(func() { libSecp = &LibSecp256k1{} // Try multiple paths to find the library paths := []string{ "./libsecp256k1.so", "../libsecp256k1.so", "/home/mleku/src/p256k1.mleku.dev/libsecp256k1.so", "libsecp256k1.so", } for _, path := range paths { err := libSecp.Load(path) if err == nil { libSecpInitErr = nil return } libSecpInitErr = err } }) if libSecpInitErr != nil { return nil, libSecpInitErr } return libSecp, nil } // Load loads the libsecp256k1.so library from the given path. func (l *LibSecp256k1) Load(path string) error { l.mu.Lock() defer l.mu.Unlock() if l.loaded { return nil } lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_GLOBAL) if err != nil { return err } l.lib = lib // Register function pointers purego.RegisterLibFunc(&l.contextCreate, lib, "secp256k1_context_create") purego.RegisterLibFunc(&l.contextDestroy, lib, "secp256k1_context_destroy") purego.RegisterLibFunc(&l.contextRandomize, lib, "secp256k1_context_randomize") purego.RegisterLibFunc(&l.schnorrsigSign32, lib, "secp256k1_schnorrsig_sign32") purego.RegisterLibFunc(&l.schnorrsigVerify, lib, "secp256k1_schnorrsig_verify") purego.RegisterLibFunc(&l.keypairCreate, lib, "secp256k1_keypair_create") purego.RegisterLibFunc(&l.keypairXonlyPub, lib, "secp256k1_keypair_xonly_pub") purego.RegisterLibFunc(&l.xonlyPubkeyParse, lib, "secp256k1_xonly_pubkey_parse") purego.RegisterLibFunc(&l.ecPubkeyCreate, lib, "secp256k1_ec_pubkey_create") purego.RegisterLibFunc(&l.ecPubkeyParse, lib, "secp256k1_ec_pubkey_parse") purego.RegisterLibFunc(&l.ecPubkeySerialize, lib, "secp256k1_ec_pubkey_serialize") purego.RegisterLibFunc(&l.xonlyPubkeySerialize, lib, "secp256k1_xonly_pubkey_serialize") purego.RegisterLibFunc(&l.ecdh, lib, "secp256k1_ecdh") // Create context (modern libsecp256k1 uses SECP256K1_CONTEXT_NONE = 1) l.ctx = l.contextCreate(libContextNone) if l.ctx == 0 { return errors.New("failed to create secp256k1 context") } // Randomize context for better security var seed [32]byte // Use zero seed for deterministic benchmarks l.contextRandomize(l.ctx, &seed[0]) l.loaded = true return nil } // Close releases the library resources. func (l *LibSecp256k1) Close() { l.mu.Lock() defer l.mu.Unlock() if !l.loaded { return } if l.ctx != 0 { l.contextDestroy(l.ctx) l.ctx = 0 } if l.lib != 0 { purego.Dlclose(l.lib) l.lib = 0 } l.loaded = false } // IsLoaded returns true if the library is loaded. func (l *LibSecp256k1) IsLoaded() bool { l.mu.RLock() defer l.mu.RUnlock() return l.loaded } // SchnorrSign signs a 32-byte message using a 32-byte secret key. // Returns a 64-byte signature. func (l *LibSecp256k1) SchnorrSign(msg32, seckey32 []byte) ([]byte, error) { l.mu.RLock() defer l.mu.RUnlock() if !l.loaded { return nil, errors.New("library not loaded") } if len(msg32) != 32 { return nil, errors.New("message must be 32 bytes") } if len(seckey32) != 32 { return nil, errors.New("secret key must be 32 bytes") } // Create keypair from secret key keypair := make([]byte, 96) // secp256k1_keypair is 96 bytes if l.keypairCreate(l.ctx, &keypair[0], &seckey32[0]) != 1 { return nil, errors.New("failed to create keypair") } // Sign sig := make([]byte, 64) if l.schnorrsigSign32(l.ctx, &sig[0], &msg32[0], &keypair[0], nil) != 1 { return nil, errors.New("signing failed") } return sig, nil } // SchnorrVerify verifies a Schnorr signature. func (l *LibSecp256k1) SchnorrVerify(sig64, msg32, pubkey32 []byte) bool { l.mu.RLock() defer l.mu.RUnlock() if !l.loaded { return false } if len(sig64) != 64 || len(msg32) != 32 || len(pubkey32) != 32 { return false } // Parse x-only pubkey using secp256k1_xonly_pubkey_parse xonlyPubkey := make([]byte, 64) // secp256k1_xonly_pubkey is 64 bytes if l.xonlyPubkeyParse(l.ctx, &xonlyPubkey[0], &pubkey32[0]) != 1 { return false } result := l.schnorrsigVerify(l.ctx, &sig64[0], &msg32[0], 32, &xonlyPubkey[0]) return result == 1 } // CreatePubkey derives a public key from a secret key. // Returns the 32-byte x-only public key. func (l *LibSecp256k1) CreatePubkey(seckey32 []byte) ([]byte, error) { l.mu.RLock() defer l.mu.RUnlock() if !l.loaded { return nil, errors.New("library not loaded") } if len(seckey32) != 32 { return nil, errors.New("secret key must be 32 bytes") } // Create keypair keypair := make([]byte, 96) if l.keypairCreate(l.ctx, &keypair[0], &seckey32[0]) != 1 { return nil, errors.New("failed to create keypair") } // Extract x-only pubkey (internal representation is 64 bytes) xonlyPubkey := make([]byte, 64) var parity int if l.keypairXonlyPub(l.ctx, &xonlyPubkey[0], &parity, &keypair[0]) != 1 { return nil, errors.New("failed to extract x-only pubkey") } // Serialize to get the 32-byte x-coordinate pubkey32 := make([]byte, 32) if l.xonlyPubkeySerialize(l.ctx, &pubkey32[0], &xonlyPubkey[0]) != 1 { return nil, errors.New("failed to serialize x-only pubkey") } return pubkey32, nil } // ECDH computes the shared secret using ECDH. func (l *LibSecp256k1) ECDH(seckey32, pubkey33 []byte) ([]byte, error) { l.mu.RLock() defer l.mu.RUnlock() if !l.loaded { return nil, errors.New("library not loaded") } if len(seckey32) != 32 { return nil, errors.New("secret key must be 32 bytes") } if len(pubkey33) != 33 && len(pubkey33) != 65 { return nil, errors.New("public key must be 33 or 65 bytes") } // Parse pubkey pubkey := make([]byte, 64) // secp256k1_pubkey is 64 bytes if l.ecPubkeyParse(l.ctx, &pubkey[0], &pubkey33[0], uint(len(pubkey33))) != 1 { return nil, errors.New("failed to parse public key") } // Compute ECDH output := make([]byte, 32) if l.ecdh(l.ctx, &output[0], &pubkey[0], &seckey32[0], 0, 0) != 1 { return nil, errors.New("ECDH failed") } return output, nil }