diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 939e76a..cbde877 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -35,7 +35,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: - version: v2.6.0 + version: v2.11.3 args: --timeout 20m --verbose - name: Test diff --git a/docs/migration-ring-go-hash-cache.md b/docs/migration-ring-go-hash-cache.md new file mode 100644 index 0000000..f8d8e62 --- /dev/null +++ b/docs/migration-ring-go-hash-cache.md @@ -0,0 +1,106 @@ +# Migration: `signer-context-cache` → ring-go `hash-cache` signing API + +Guidance for downstream consumers (notably **PATH**) currently building against +the SDK's `perf/signer-context-cache` branch, moving to the new +`perf/ring-go-hash-cache` branch. + +## TL;DR + +- `Signer.Sign` no longer performs the off-chain optimization. It is now the + **deterministic, consensus-safe** default (ring-go `Ring.Sign`), matching the + SDK's original `main` behavior. +- The off-chain fast path moved to **new named methods**: `SignOffChain` and + `SignOffChainWithRing`. +- If you leave your `Sign` call sites unchanged, **nothing breaks** — signatures + are still valid and verify identically. You simply lose the speedup until you + switch to the off-chain methods. +- Requires the **Go 1.26** toolchain (propagates from the SDK). +- ⚠️ The off-chain context cache is **unbounded** — call `ClearSignerContextCache()` + on session rollover or it leaks slowly. See step 3. + +## Why it changed + +On `signer-context-cache`, `Signer.Sign` was silently the cached off-chain path. +That overloaded a single method with off-chain-only behavior (skipped self-checks, +non-deterministic per-signature work). The new design mirrors ring-go itself: +`Sign` stays the safe deterministic default, and the optimization is an explicit, +named opt-in — so nothing on a consensus-critical path can pick it up by accident. + +## API mapping + +| `signer-context-cache` (old) | `hash-cache` (new) | Notes | +|----------------------------------|----------------------------------------|-------| +| `signer.Sign(ctx, req, appRing)` | `signer.SignOffChain(ctx, req, appRing)` | to keep the speedup on off-chain relay signing | +| `signer.SignWithRing(ctx, req, ring)` | `signer.SignOffChainWithRing(ctx, req, ring)` | external/session-cached ring | +| `signer.ClearSignerContextCache()` | `signer.ClearSignerContextCache()` | unchanged | +| `signer.Sign(...)` (want deterministic) | `signer.Sign(ctx, req, appRing)` | now genuinely deterministic — use on consensus-critical paths | + +`NewSignerFromHex` is unchanged. + +## Steps + +### 1. Bump the SDK dependency + +```sh +go get github.com/pokt-network/shannon-sdk@perf/ring-go-hash-cache +go mod tidy +``` + +> The SDK pins ring-go to the `perf/hash-cache` branch commit (not a tagged +> release yet) and requires **Go 1.26**. Make sure CI and every downstream of +> PATH has the 1.26 toolchain before merging. (ring-go also bumped go-ethereum to +> v1.17.4 for CVEs — only active with the optional `ethereum_secp256k1` cgo tag.) + +### 2. Switch off-chain relay/session signing to the new methods + +```go +// before (signer-context-cache): Sign WAS the cached off-chain path +signedReq, err := signer.Sign(ctx, relayRequest, appRing) + +// after (hash-cache): opt into the off-chain fast path explicitly +signedReq, err := signer.SignOffChain(ctx, relayRequest, appRing) +``` + +```go +// before +signedReq, err := signer.SignWithRing(ctx, relayRequest, sessionRing) +// after +signedReq, err := signer.SignOffChainWithRing(ctx, relayRequest, sessionRing) +``` + +Only apply this on the **off-chain** relay/session signing path. Keep plain +`Sign` for anything consensus-critical / gas-metered. + +### 3. Reuse the signer and ring for cache hits + +The `SignerContext` is cached per **ring pointer**. To hit the cache across many +signs, reuse: + +- the same `*Signer` instance, and +- the same ring pointer — prefer `SignOffChainWithRing` with a ring you cache by + session, so rebuilding the ring per message does not discard the precompute. + +> ### ⚠️ You MUST evict the cache on session rollover +> +> The off-chain `signerContextCache` is keyed by **ring pointer** and is +> **unbounded** — it grows one entry per distinct ring (i.e. per session), +> alongside your own ring cache. It is **not evicted automatically**. If you never +> clear it, it leaks slowly (memory climbs over days). +> +> Call **`ClearSignerContextCache()` on session rollover**, when the old rings are +> no longer used. This clears the whole cache; rebuild lazily on the next sign. +> (PATH wires this into its existing per-session rollover eviction.) + +### 4. Validate + +- Your unit tests should pass unchanged after the call-site edit. +- End-to-end: measure per-relay signing latency + throughput before/after under + load, and report the ring/session size — gains scale with it. + +## Notes + +- `SkipSelfCheck` is already enabled inside the off-chain path; no action needed. + It is safe here because the context is always signed with the same private key + it was built from. +- Both `Sign` and `SignOffChain*` produce signatures that **verify identically**; + on-chain verification is unchanged. diff --git a/go.mod b/go.mod index abdc6a9..dd46468 100644 --- a/go.mod +++ b/go.mod @@ -19,8 +19,9 @@ require ( github.com/cometbft/cometbft v0.38.21 github.com/cosmos/cosmos-sdk v0.53.7 github.com/cosmos/gogoproto v1.7.2 + github.com/pokt-network/go-dleq v0.0.0-20250925202155-488f42ad642a github.com/pokt-network/poktroll v0.1.34 - github.com/pokt-network/ring-go v0.1.1-0.20250925213458-782cc69bc1ec + github.com/pokt-network/ring-go v0.2.0 github.com/stretchr/testify v1.11.1 google.golang.org/grpc v1.82.0 google.golang.org/protobuf v1.36.11 @@ -49,7 +50,7 @@ require ( cosmossdk.io/x/feegrant v0.1.1 // indirect cosmossdk.io/x/tx v0.14.0 // indirect cosmossdk.io/x/upgrade v0.1.4 // indirect - filippo.io/edwards25519 v1.1.1 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect github.com/DataDog/datadog-go v3.2.0+incompatible // indirect @@ -108,7 +109,7 @@ require ( github.com/cosmos/ledger-cosmos-go v1.0.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.2.0 // indirect @@ -118,7 +119,7 @@ require ( github.com/emicklei/dot v1.6.2 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect - github.com/ethereum/go-ethereum v1.16.9 // indirect + github.com/ethereum/go-ethereum v1.17.4 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -189,7 +190,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/pokt-network/go-dleq v0.0.0-20250925202155-488f42ad642a // indirect github.com/pokt-network/smt v0.14.1 // indirect github.com/pokt-network/smt/kvstore/pebble v0.0.0-20240822175047-21ea8639c188 // indirect github.com/prometheus/client_golang v1.23.0 // indirect @@ -235,14 +235,14 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.17.0 // indirect - golang.org/x/crypto v0.50.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/term v0.42.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/api v0.271.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect diff --git a/go.sum b/go.sum index cadea17..76964b7 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ cosmossdk.io/x/tx v0.14.0/go.mod h1:Tn30rSRA1PRfdGB3Yz55W4Sn6EIutr9xtMKSHij+9PM= cosmossdk.io/x/upgrade v0.1.4 h1:/BWJim24QHoXde8Bc64/2BSEB6W4eTydq0X/2f8+g38= cosmossdk.io/x/upgrade v0.1.4/go.mod h1:9v0Aj+fs97O+Ztw+tG3/tp5JSlrmT7IcFhAebQHmOPo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= @@ -282,8 +282,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= @@ -324,8 +324,8 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= -github.com/ethereum/go-ethereum v1.16.9 h1:UTJ93yoXD7BEMWg+9lSZ8/Zvf0oZfy2ZUmv0Gn0ZclE= -github.com/ethereum/go-ethereum v1.16.9/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-ethereum v1.17.4 h1:uA4q+qiLp7QImBsjdRbINu8iX6OEVmj4DPc5/E5Fsxc= +github.com/ethereum/go-ethereum v1.17.4/go.mod h1:qMdgwqqRAen+aT8P7KKQKi0Qt6RzG4cfejVAbCpJgqA= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -749,8 +749,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.34 h1:QlcUOyLgpNuEXIyukECPDWZQROUXUZD+nCTpQWmq5xw= github.com/pokt-network/poktroll v0.1.34/go.mod h1:K7v1yjw6ZQFGBOd31LiTjPjIUBSzKTlOIckFaKC8PUg= -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.2.0 h1:jXF/SOS8DgRfCCJBKXket5dBkTTqMDxplX+EnE4+aNU= +github.com/pokt-network/ring-go v0.2.0/go.mod h1:B6wWq+Pj19jNyV93Hyr1TD26gDsrIYV9tYGiaJR89RE= 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= @@ -963,8 +963,8 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= @@ -983,8 +983,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1013,8 +1013,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1028,8 +1028,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1084,20 +1084,20 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -1122,8 +1122,8 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/signer.go b/signer.go index 2e0cdfd..5d872be 100644 --- a/signer.go +++ b/signer.go @@ -4,7 +4,9 @@ import ( "context" "encoding/hex" "fmt" + "sync" + dleqtypes "github.com/pokt-network/go-dleq/types" servicetypes "github.com/pokt-network/poktroll/x/service/types" "github.com/pokt-network/ring-go" ) @@ -15,11 +17,29 @@ import ( // The backend is selected at build time: // - With "ethereum_secp256k1" tag: Uses Ethereum's libsecp256k1 (fastest, requires CGO) // - Without tag: Uses Decred's implementation (excellent performance, pure Go) +// +// Two signing families are exposed, mirroring ring-go: +// - Sign: the deterministic, consensus-safe default (ring-go's Ring.Sign). +// - SignOffChain / SignOffChainWithRing: the faster off-chain path (ring-go's +// SignWithContext) that caches per-ring precompute. OFF-CHAIN ONLY — its +// per-signature work is not deterministic across nodes. type Signer struct { // PrivateKeyHex is retained for compatibility and debugging. PrivateKeyHex string // privateKeyBytes caches the 32-byte private key to avoid repeated hex decoding. privateKeyBytes []byte + // signerScalar is the decoded private-key scalar, cached once at construction + // to avoid a per-call DecodeToScalar. Supplied per call to both Ring.Sign and + // Ring.SignWithContext. Held for the signer's lifetime alongside + // privateKeyBytes; both are secret material. + signerScalar dleqtypes.Scalar + + // signerContextCache caches a secret-free SignerContext per ring for the + // off-chain path only, avoiding redundant cryptographic setup (public key, + // hash-to-curve, key image, and the hash-to-curve of every ring member). + // Key is the ring pointer. The SignerContext holds no secret material, so it + // is safe to cache. The deterministic Sign path never touches this cache. + signerContextCache sync.Map // map[*ring.Ring]*ring.SignerContext } // NewSignerFromHex creates a new Signer instance from a hex-encoded private key. @@ -31,16 +51,25 @@ func NewSignerFromHex(privateKeyHex string) (*Signer, error) { if len(keyBytes) != 32 { return nil, fmt.Errorf("invalid private key length: expected 32 bytes, got %d", len(keyBytes)) } + // Decode the private key to a scalar once; reused for every sign call. + signerScalar, err := ring.Secp256k1().DecodeToScalar(keyBytes) + if err != nil { + return nil, fmt.Errorf("error decoding private key to scalar: %w", err) + } return &Signer{ PrivateKeyHex: privateKeyHex, privateKeyBytes: keyBytes, + signerScalar: signerScalar, }, 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 is the deterministic, consensus-safe default (ring-go's Ring.Sign): its +// per-signature work is identical across nodes. Use it anywhere correctness or +// gas-metering depends on deterministic work. For high-throughput off-chain +// relay signing, prefer SignOffChain — both produce signatures that verify +// identically. // // Returns a pointer instead of directly setting the signature on the input relay request to avoid implicit output. func (s *Signer) Sign( @@ -64,16 +93,8 @@ 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) - if err != nil { - return nil, fmt.Errorf("Sign: error decoding private key to scalar: %w", err) - } - - ringSig, err := sessionRing.Sign(signableBz, signerPrivKey) + // Deterministic path: plain ring.Sign, no SignerContext and no SkipSelfCheck. + ringSig, err := sessionRing.Sign(signableBz, s.signerScalar) if err != nil { return nil, fmt.Errorf( "Sign: error signing using the ring of application with address %s: %w", @@ -96,3 +117,112 @@ func (s *Signer) Sign( relayRequest.Meta.Signature = signature return relayRequest, nil } + +// getOrCreateSignerContext returns a cached SignerContext for the given ring, +// or creates and caches a new one if not present. Used only by the off-chain +// signing path. +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 + } + + // Create a new SignerContext with pre-computed values for this ring. + signerCtx, err := sessionRing.NewSignerContext(s.signerScalar) + if err != nil { + return nil, fmt.Errorf("getOrCreateSignerContext: error creating signer context: %w", err) + } + + // Off-chain path: skip the closing self-checks for a bit more speed + // (~4 scalar-muls/sign). Safe here — the signature stays fully verifiable and + // this path is never consensus-critical/gas-metered. The mismatched-key + // caveat does not apply: the context is built from, and always signed with, + // the same s.signerScalar. + signerCtx.SkipSelfCheck = true + + // Cache it (use LoadOrStore to handle concurrent creation) + actual, _ := s.signerContextCache.LoadOrStore(sessionRing, signerCtx) + return actual.(*ring.SignerContext), nil +} + +// SignOffChain signs the given relay request using the faster off-chain path +// (ring-go's SignWithContext) with a per-ring cached SignerContext. +// +// It is the throughput-optimized counterpart to Sign for the off-chain +// relay/session signing path. OFF-CHAIN ONLY: the precompute means its +// per-signature work differs from Sign, so it must NOT be used on +// consensus-critical / gas-metered paths — use Sign there. The produced +// signature verifies identically to one from Sign. +// +// Returns a pointer instead of directly setting the signature on the input relay request to avoid implicit output. +func (s *Signer) SignOffChain( + ctx context.Context, + relayRequest *servicetypes.RelayRequest, + appRing *ApplicationRing, +) (*servicetypes.RelayRequest, error) { + // Get the session ring for the application's session end block height + sessionRing, err := appRing.GetRing(ctx, uint64(relayRequest.Meta.SessionHeader.SessionEndBlockHeight)) + if err != nil { + return nil, fmt.Errorf( + "SignOffChain: error getting a ring for application address %s: %w", + appRing.Address, + err, + ) + } + + return s.SignOffChainWithRing(ctx, relayRequest, sessionRing) +} + +// SignOffChainWithRing signs the given relay request using the faster off-chain +// path and a pre-built ring. +// +// This 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. +// Same OFF-CHAIN ONLY constraint as SignOffChain. +// +// Returns a pointer instead of directly setting the signature on the input relay request to avoid implicit output. +func (s *Signer) SignOffChainWithRing( + 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("SignOffChainWithRing: 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("SignOffChainWithRing: error getting signer context: %w", err) + } + + // Sign using the cached context (avoids redundant ScalarBaseMul, hashToCurve, etc.). + // The private key scalar is supplied per call in the hash-cache API. + ringSig, err := sessionRing.SignWithContext(signableBz, s.signerScalar, signerCtx) + if err != nil { + return nil, fmt.Errorf("SignOffChainWithRing: error signing relay request: %w", err) + } + + // Serialize the signature + signature, err := ringSig.Serialize() + if err != nil { + return nil, fmt.Errorf("SignOffChainWithRing: 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 used by the off-chain +// signing path. +// +// The cache is keyed by ring pointer and is UNBOUNDED: it grows one entry per +// distinct ring (i.e. per session) and is never evicted automatically. Callers +// using SignOffChain/SignOffChainWithRing MUST call this on session rollover, +// once the old rings are no longer used, or memory leaks slowly over time. +func (s *Signer) ClearSignerContextCache() { + s.signerContextCache = sync.Map{} +} diff --git a/signer_test.go b/signer_test.go new file mode 100644 index 0000000..b6cda4b --- /dev/null +++ b/signer_test.go @@ -0,0 +1,138 @@ +package sdk + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + apptypes "github.com/pokt-network/poktroll/x/application/types" + servicetypes "github.com/pokt-network/poktroll/x/service/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + ring "github.com/pokt-network/ring-go" + "github.com/stretchr/testify/require" +) + +// setupSignerTestData mirrors setupBenchmarkData but for *testing.T. +func setupSignerTestData(t *testing.T) (*Signer, *servicetypes.RelayRequest, *ApplicationRing) { + t.Helper() + + appPrivKey := secp256k1.GenPrivKey() + supplierPrivKey1 := secp256k1.GenPrivKey() + supplierPrivKey2 := secp256k1.GenPrivKey() + + signer, err := NewSignerFromHex(hex.EncodeToString(appPrivKey.Bytes())) + require.NoError(t, err) + + pubKeyFetcher := &mockPublicKeyFetcher{ + publicKeys: map[string]cryptotypes.PubKey{ + "pokt1app1": appPrivKey.PubKey(), + "pokt1supplier1": supplierPrivKey1.PubKey(), + "pokt1supplier2": supplierPrivKey2.PubKey(), + }, + } + + appRing := &ApplicationRing{ + Application: apptypes.Application{Address: "pokt1app1"}, + PublicKeyFetcher: pubKeyFetcher, + } + + relayRequest := &servicetypes.RelayRequest{ + Meta: servicetypes.RelayRequestMetadata{ + SessionHeader: &sessiontypes.SessionHeader{ + ApplicationAddress: "pokt1app1", + ServiceId: "test-service", + SessionStartBlockHeight: 1, + SessionEndBlockHeight: 10, + }, + SupplierOperatorAddress: "pokt1supplier1", + }, + Payload: []byte(`{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}`), + } + + return signer, relayRequest, appRing +} + +func cachedContextCount(s *Signer) int { + count := 0 + s.signerContextCache.Range(func(_, _ any) bool { count++; return true }) + return count +} + +// TestSignerSignRoundTrip proves the deterministic (consensus-safe) default path +// produces verifiable signatures, rejects a tampered message, and never touches +// the off-chain SignerContext cache. +func TestSignerSignRoundTrip(t *testing.T) { + ctx := context.Background() + signer, relayRequest, appRing := setupSignerTestData(t) + + signableBz, err := relayRequest.GetSignableBytesHash() + require.NoError(t, err) + + signed, err := signer.Sign(ctx, relayRequest, appRing) + require.NoError(t, err) + require.NotEmpty(t, signed.Meta.Signature) + + var sig ring.RingSig + require.NoError(t, sig.Deserialize(ring.Secp256k1(), signed.Meta.Signature)) + require.True(t, sig.Verify(signableBz), "deterministic signature must verify") + + // Negative case: must not verify against a tampered message hash. + wrong := signableBz + wrong[0] ^= 0xFF + require.False(t, sig.Verify(wrong), "signature must not verify against a different message") + + // The deterministic path must not populate the off-chain context cache. + require.Equal(t, 0, cachedContextCount(signer), "Sign must not use the context cache") +} + +// TestSignerSignOffChainRoundTrip proves the off-chain path (per-call key + +// cached SignerContext + SkipSelfCheck) produces verifiable signatures and +// populates the cache. +func TestSignerSignOffChainRoundTrip(t *testing.T) { + ctx := context.Background() + signer, relayRequest, appRing := setupSignerTestData(t) + + signableBz, err := relayRequest.GetSignableBytesHash() + require.NoError(t, err) + + signed, err := signer.SignOffChain(ctx, relayRequest, appRing) + require.NoError(t, err) + require.NotEmpty(t, signed.Meta.Signature) + + var sig ring.RingSig + require.NoError(t, sig.Deserialize(ring.Secp256k1(), signed.Meta.Signature)) + require.True(t, sig.Verify(signableBz), "off-chain signature must verify") + + require.Equal(t, 1, cachedContextCount(signer), "SignOffChain must cache one context for the ring") +} + +// TestSignerOffChainCacheReuse proves repeated off-chain signs reuse a single +// cached SignerContext for one ring, every signature verifies, and clearing +// empties the cache. +func TestSignerOffChainCacheReuse(t *testing.T) { + ctx := context.Background() + signer, relayRequest, appRing := setupSignerTestData(t) + + sessionRing, err := appRing.GetRing(ctx, uint64(relayRequest.Meta.SessionHeader.SessionEndBlockHeight)) + require.NoError(t, err) + + for i := 0; i < 3; i++ { + signableBz, err := relayRequest.GetSignableBytesHash() + require.NoError(t, err) + + signed, err := signer.SignOffChainWithRing(ctx, relayRequest, sessionRing) + require.NoError(t, err) + + var sig ring.RingSig + require.NoError(t, sig.Deserialize(ring.Secp256k1(), signed.Meta.Signature)) + require.True(t, sig.Verify(signableBz), "off-chain sign #%d must verify", i) + } + + require.Equal(t, 1, cachedContextCount(signer), "expected a single cached SignerContext for one ring") + + // Clearing the cache empties it. + signer.ClearSignerContextCache() + require.Equal(t, 0, cachedContextCount(signer), "cache must be empty after ClearSignerContextCache") +}