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