diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..c8f25dc
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/shannon-sdk.iml b/.idea/shannon-sdk.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/shannon-sdk.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..1672818
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 717754a..55067d9 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,8 @@ require (
github.com/cosmos/cosmos-sdk v0.53.0
github.com/cosmos/gogoproto v1.7.0
github.com/pokt-network/poktroll v0.1.30-0.20250926212324-1588b0a53acb
- github.com/pokt-network/ring-go v0.1.1-0.20250925213458-782cc69bc1ec
+ // Use ring-go with SignerContext optimization (perf/sign-precompute branch)
+ github.com/pokt-network/ring-go v0.1.1-0.20251219190013-b576b71e4648
github.com/stretchr/testify v1.10.0
google.golang.org/grpc v1.72.0
google.golang.org/protobuf v1.36.6
diff --git a/go.sum b/go.sum
index e800053..5a62009 100644
--- a/go.sum
+++ b/go.sum
@@ -972,8 +972,8 @@ github.com/pokt-network/go-dleq v0.0.0-20250925202155-488f42ad642a h1:zgSFcOX8m9
github.com/pokt-network/go-dleq v0.0.0-20250925202155-488f42ad642a/go.mod h1:KVsT8HO2EXHwMY1wnyYhR/lSZFNFIit8dJ+aDb1dSkA=
github.com/pokt-network/poktroll v0.1.30-0.20250926212324-1588b0a53acb h1:6pCcbr8wOiKDxCkHtOMDsXqILpJGCbGWe8jCVHNb3jU=
github.com/pokt-network/poktroll v0.1.30-0.20250926212324-1588b0a53acb/go.mod h1:8A8+i05sggfn79YQCX6B0cLEn0ihbi01nck1/qNoznM=
-github.com/pokt-network/ring-go v0.1.1-0.20250925213458-782cc69bc1ec h1:ItogqaNYkfmQp7fJV1CfP9cTdhIEkjEtMDmlYrPvbjg=
-github.com/pokt-network/ring-go v0.1.1-0.20250925213458-782cc69bc1ec/go.mod h1:jpllhLLlmtz+/0KYwhFjtO/Qpdy+UUDPkgQKSQJRc7Q=
+github.com/pokt-network/ring-go v0.1.1-0.20251219190013-b576b71e4648 h1:0jrITIlVjMz69Iwlhr5Tlisvxj+cUoWmY4yttkRzT/w=
+github.com/pokt-network/ring-go v0.1.1-0.20251219190013-b576b71e4648/go.mod h1:jpllhLLlmtz+/0KYwhFjtO/Qpdy+UUDPkgQKSQJRc7Q=
github.com/pokt-network/smt v0.14.1 h1:q8pZCo01RY+kzGurRcHArSGGEV3UhBywUZnbVn9nDM8=
github.com/pokt-network/smt v0.14.1/go.mod h1:TehzlxITd3EqLzo428VY0QID7Ajdn7QJP4ZzdPiYbZE=
github.com/pokt-network/smt/kvstore/pebble v0.0.0-20240822175047-21ea8639c188 h1:QK1WmFKQ/OzNVob/br55Brh+EFbWhcdq41WGC8UMihM=
diff --git a/signer.go b/signer.go
index 2e0cdfd..a01ec4f 100644
--- a/signer.go
+++ b/signer.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/hex"
"fmt"
+ "sync"
servicetypes "github.com/pokt-network/poktroll/x/service/types"
"github.com/pokt-network/ring-go"
@@ -20,6 +21,11 @@ type Signer struct {
PrivateKeyHex string
// privateKeyBytes caches the 32-byte private key to avoid repeated hex decoding.
privateKeyBytes []byte
+
+ // signerContextCache caches SignerContext per ring to avoid redundant
+ // cryptographic operations (public key computation, hash-to-curve, key image).
+ // Key is the ring's memory address as a unique identifier.
+ signerContextCache sync.Map // map[*ring.Ring]*ring.SignerContext
}
// NewSignerFromHex creates a new Signer instance from a hex-encoded private key.
@@ -37,10 +43,35 @@ func NewSignerFromHex(privateKeyHex string) (*Signer, error) {
}, nil
}
+// getOrCreateSignerContext returns a cached SignerContext for the given ring,
+// or creates and caches a new one if not present.
+func (s *Signer) getOrCreateSignerContext(sessionRing *ring.Ring) (*ring.SignerContext, error) {
+ // Check cache first
+ if cached, ok := s.signerContextCache.Load(sessionRing); ok {
+ return cached.(*ring.SignerContext), nil
+ }
+
+ // Decode private key to scalar
+ privKey, err := ring.Secp256k1().DecodeToScalar(s.privateKeyBytes)
+ if err != nil {
+ return nil, fmt.Errorf("getOrCreateSignerContext: error decoding private key: %w", err)
+ }
+
+ // Create new SignerContext with pre-computed values
+ ctx, err := sessionRing.NewSignerContext(privKey)
+ if err != nil {
+ return nil, fmt.Errorf("getOrCreateSignerContext: error creating signer context: %w", err)
+ }
+
+ // Cache it (use LoadOrStore to handle concurrent creation)
+ actual, _ := s.signerContextCache.LoadOrStore(sessionRing, ctx)
+ return actual.(*ring.SignerContext), nil
+}
+
// Sign signs the given relay request using the signer's private key and the application's ring.
//
-// This method delegates to the pluggable crypto backend for optimal performance.
-// The backend choice provides different performance characteristics:
+// This method uses SignerContext caching for optimal performance when signing
+// multiple requests with the same ring (which is common within a session).
//
// Returns a pointer instead of directly setting the signature on the input relay request to avoid implicit output.
func (s *Signer) Sign(
@@ -64,16 +95,14 @@ func (s *Signer) Sign(
return nil, fmt.Errorf("Sign: error getting signable bytes hash from the relay request: %w", err)
}
- // Sign the request using the session ring and signer's private key
- // TODO_OPTIMIZE: Pre-cache the decoded scalar to avoid per-call DecodeToScalar
- // once ring-go exposes a stable public scalar type for long-lived reuse.
- // Decode private key bytes to scalar (fast, but still cheaper than hex decoding each call)
- signerPrivKey, err := ring.Secp256k1().DecodeToScalar(s.privateKeyBytes)
+ // Get or create cached SignerContext for this ring
+ signerCtx, err := s.getOrCreateSignerContext(sessionRing)
if err != nil {
- return nil, fmt.Errorf("Sign: error decoding private key to scalar: %w", err)
+ return nil, fmt.Errorf("Sign: error getting signer context: %w", err)
}
- ringSig, err := sessionRing.Sign(signableBz, signerPrivKey)
+ // Sign using the cached context (avoids redundant ScalarBaseMul, hashToCurve, etc.)
+ ringSig, err := sessionRing.SignWithContext(signableBz, signerCtx)
if err != nil {
return nil, fmt.Errorf(
"Sign: error signing using the ring of application with address %s: %w",
@@ -96,3 +125,49 @@ func (s *Signer) Sign(
relayRequest.Meta.Signature = signature
return relayRequest, nil
}
+
+// SignWithRing signs the given relay request using the signer's private key and a pre-built ring.
+//
+// This method is useful when the caller caches the ring externally (e.g., by session)
+// to ensure the same ring pointer is reused, enabling SignerContext cache hits.
+//
+// Returns a pointer instead of directly setting the signature on the input relay request to avoid implicit output.
+func (s *Signer) SignWithRing(
+ ctx context.Context,
+ relayRequest *servicetypes.RelayRequest,
+ sessionRing *ring.Ring,
+) (*servicetypes.RelayRequest, error) {
+ // Get the signable bytes hash from the relay request
+ signableBz, err := relayRequest.GetSignableBytesHash()
+ if err != nil {
+ return nil, fmt.Errorf("SignWithRing: error getting signable bytes hash from the relay request: %w", err)
+ }
+
+ // Get or create cached SignerContext for this ring
+ signerCtx, err := s.getOrCreateSignerContext(sessionRing)
+ if err != nil {
+ return nil, fmt.Errorf("SignWithRing: error getting signer context: %w", err)
+ }
+
+ // Sign using the cached context (avoids redundant ScalarBaseMul, hashToCurve, etc.)
+ ringSig, err := sessionRing.SignWithContext(signableBz, signerCtx)
+ if err != nil {
+ return nil, fmt.Errorf("SignWithRing: error signing relay request: %w", err)
+ }
+
+ // Serialize the signature
+ signature, err := ringSig.Serialize()
+ if err != nil {
+ return nil, fmt.Errorf("SignWithRing: error serializing the signature: %w", err)
+ }
+
+ // Set the signature on the relay request
+ relayRequest.Meta.Signature = signature
+ return relayRequest, nil
+}
+
+// ClearSignerContextCache clears the cached SignerContexts.
+// This should be called when sessions roll over and old rings are no longer needed.
+func (s *Signer) ClearSignerContextCache() {
+ s.signerContextCache = sync.Map{}
+}
\ No newline at end of file