From 003452d1fc4ad97fc5c844d8f22b9cfbb4fc03b0 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:25:54 -0500 Subject: [PATCH 1/2] Add ephemeral keypair pool for noise handshakes Pre-generate secp256k1 keypairs in a background goroutine and serve them from a buffered channel pool. This eliminates the per-handshake cost of EC key generation under load, allowing burst handling of concurrent handshakes without blocking on crypto operations. --- pkg/noise/dh.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/pkg/noise/dh.go b/pkg/noise/dh.go index 6c208f07..6daca157 100644 --- a/pkg/noise/dh.go +++ b/pkg/noise/dh.go @@ -9,16 +9,31 @@ import ( "github.com/skycoin/skycoin/src/cipher" ) +const keypairPoolSize = 64 + +// keypairPool holds pre-generated ephemeral keypairs for noise handshakes. +// secp256k1 key generation is expensive (EC multiply + validation), so we +// generate them in the background and serve them from a buffered channel. +var keypairPool = func() chan noise.DHKey { + ch := make(chan noise.DHKey, keypairPoolSize) + go func() { + for { + pk, sk := cipher.GenerateKeyPair() + ch <- noise.DHKey{ + Private: sk[:], + Public: pk[:], + } + } + }() + return ch +}() + // Secp256k1 implements `noise.DHFunc`. type Secp256k1 struct{} // GenerateKeypair helps to implement `noise.DHFunc`. func (Secp256k1) GenerateKeypair(_ io.Reader) (noise.DHKey, error) { - pk, sk := cipher.GenerateKeyPair() - return noise.DHKey{ - Private: sk[:], - Public: pk[:], - }, nil + return <-keypairPool, nil } // DH helps to implement `noise.DHFunc`. From 259b2ebf4a3e7b75e4ad8c4a68065eb0494b9d3a Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:36:17 -0500 Subject: [PATCH 2/2] Optimize noise handshake and encrypt/decrypt hot paths - Eliminate per-encrypt nonce buffer allocation by using a reusable [8]byte field in the Noise struct - Pre-allocate output buffer in EncryptUnsafe to avoid append growth - Add sync.Pool for write frame buffers to reduce allocation pressure - Skip redundant NewPubKey/NewSecKey validation in DH() since keys are already validated by the noise state machine (ECDH still validates) - Skip cipher.NewPubKey validation in RemoteStatic() since the key was already verified during the handshake --- pkg/noise/dh.go | 20 +++++++++++--------- pkg/noise/noise.go | 18 ++++++++++++------ pkg/noise/read_writer.go | 33 ++++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/pkg/noise/dh.go b/pkg/noise/dh.go index 6daca157..1d79b479 100644 --- a/pkg/noise/dh.go +++ b/pkg/noise/dh.go @@ -37,20 +37,22 @@ func (Secp256k1) GenerateKeypair(_ io.Reader) (noise.DHKey, error) { } // DH helps to implement `noise.DHFunc`. +// Keys are already validated by the noise handshake state machine, so we +// skip the redundant NewPubKey/NewSecKey validation and copy directly. +// cipher.ECDH still performs its own internal validation. func (Secp256k1) DH(sk, pk []byte) []byte { - pubKey, err := cipher.NewPubKey(pk) - if err != nil { - panic(fmt.Sprintf("noise DH: invalid public key: %v", err)) - } - secKey, err := cipher.NewSecKey(sk) - if err != nil { - panic(fmt.Sprintf("noise DH: invalid secret key: %v", err)) - } + var pubKey cipher.PubKey + var secKey cipher.SecKey + copy(pubKey[:], pk) + copy(secKey[:], sk) ecdh, err := cipher.ECDH(pubKey, secKey) if err != nil { panic(fmt.Sprintf("noise DH: ECDH failed: %v", err)) } - return append(ecdh, byte(0)) + // DHLen() returns 33; ECDH returns 32-byte SHA256 hash, pad to 33. + out := make([]byte, 33) + copy(out, ecdh) + return out } // DHLen helps to implement `noise.DHFunc`. diff --git a/pkg/noise/noise.go b/pkg/noise/noise.go index 54ba9d29..759131d3 100644 --- a/pkg/noise/noise.go +++ b/pkg/noise/noise.go @@ -42,6 +42,8 @@ type Noise struct { encNonce uint64 // increment after encryption decNonce uint64 // expect increment with each subsequent packet + + encBuf [nonceSize]byte // reusable nonce buffer for encryption } // New creates a new Noise with: @@ -134,11 +136,12 @@ func (ns *Noise) LocalStatic() cipher.PubKey { // RemoteStatic returns the remote static public key. // Returns an empty public key if the peer static key is invalid. func (ns *Noise) RemoteStatic() cipher.PubKey { - pk, err := cipher.NewPubKey(ns.hs.PeerStatic()) - if err != nil { - noiseLogger.WithError(err).Error("Invalid remote static public key") + raw := ns.hs.PeerStatic() + if len(raw) != len(cipher.PubKey{}) { return cipher.PubKey{} } + var pk cipher.PubKey + copy(pk[:], raw) return pk } @@ -146,9 +149,12 @@ func (ns *Noise) RemoteStatic() cipher.PubKey { // be used with external lock. func (ns *Noise) EncryptUnsafe(plaintext []byte) []byte { ns.encNonce++ - buf := make([]byte, nonceSize) - binary.BigEndian.PutUint64(buf, ns.encNonce) - return append(buf, ns.enc.Cipher().Encrypt(nil, ns.encNonce, nil, plaintext)...) + binary.BigEndian.PutUint64(ns.encBuf[:], ns.encNonce) + ciphertext := ns.enc.Cipher().Encrypt(nil, ns.encNonce, nil, plaintext) + out := make([]byte, nonceSize+len(ciphertext)) + copy(out, ns.encBuf[:]) + copy(out[nonceSize:], ciphertext) + return out } // DecryptUnsafe decrypts ciphertext without interlocking, should only diff --git a/pkg/noise/read_writer.go b/pkg/noise/read_writer.go index c2c82d30..76d4bfea 100644 --- a/pkg/noise/read_writer.go +++ b/pkg/noise/read_writer.go @@ -30,6 +30,14 @@ const ( authSize = 24 // noise auth data size ) +// framePool reuses frame buffers to reduce allocations in the hot path. +var framePool = sync.Pool{ + New: func() interface{} { + b := make([]byte, maxFrameSize) + return &b + }, +} + type timeoutError struct{} func (timeoutError) Error() string { return "deadline exceeded" } @@ -273,16 +281,31 @@ func ResponderHandshake(ns *Noise, r *bufio.Reader, w io.Writer) error { // WriteRawFrame writes a raw frame (data prefixed with a uint16 len). // It returns the bytes written. func WriteRawFrame(w io.Writer, p []byte) ([]byte, error) { - buf := make([]byte, prefixSize+len(p)) + frameLen := prefixSize + len(p) lenp, ok := safecast.To[uint16](len(p)) - if ok { + if !ok { + return []byte{}, fmt.Errorf("failed to cast length of slice to uint16") + } + + // Use pooled buffer for frames that fit, fall back to allocation for oversized. + if frameLen <= maxFrameSize { + bp := framePool.Get().(*[]byte) + buf := (*bp)[:frameLen] binary.BigEndian.PutUint16(buf, lenp) copy(buf[prefixSize:], p) - n, err := w.Write(buf) - return buf[:n], err + framePool.Put(bp) + if err != nil { + return buf[:n], err + } + return buf[:n], nil } - return []byte{}, fmt.Errorf("failed to cast length of slice to uint16") + + buf := make([]byte, frameLen) + binary.BigEndian.PutUint16(buf, lenp) + copy(buf[prefixSize:], p) + n, err := w.Write(buf) + return buf[:n], err } // ReadRawFrame attempts to read a raw frame from a buffered reader.