Skip to content

feat(stork): verify signatures against a list of trusted signer addresses - #25

Open
2dpetkov wants to merge 4 commits into
dydxprotocol:mainfrom
dydxopsdao:stork-multi-signer
Open

2dpetkov wants to merge 4 commits into
dydxprotocol:mainfrom
dydxopsdao:stork-multi-signer

Conversation

@2dpetkov

@2dpetkov 2dpetkov commented Sep 8, 2026 •

Copy link
Copy Markdown

Why

Stork is moving its signing logic into a TEE and rotating the aggregator signer key on September 15, 2026 at 10am ET.
Consumers that verify signatures themselves must accept 0x0bb53E0d5E89778DCD13C2720667D292368dD053 as the signer from that point.

The Stork provider currently compares the recovered signer against exactly one address, taken from STORK_PUB_KEY or the hardcoded default.
Once the key rotates, every Stork price fails verification and the feed goes stale until each operator updates the variable.
A single-address check also cannot cover a rollout where different environments see different signers at the same time.

How

STORK_PUB_KEY now accepts a comma-separated list of Ethereum addresses, and a signature is valid when the recovered address matches any entry.
A single address remains a valid one-element list, so existing configuration keeps working unchanged.

  • The list is parsed once in NewAPIHandler and stored on the handler, rather than read from the environment on every price.
  • Parsing fails closed: an empty entry, a trailing comma, or a malformed address makes handler construction return an error instead of silently shrinking the trust set.
  • When the variable is unset, DefaultSignerAddresses is used and contains both the current address 0x0a803F9b1CCe32e2773e0d2e98b37E0775cA5d44 and the new one 0x0bb53E0d5E89778DCD13C2720667D292368dD053.
  • VerifyStorkSignature takes the signer list as an explicit argument and returns an error when the list is empty.
  • A rejected signature reports the recovered address alongside the configured trusted signers.

Trade-offs:

  • Defaulting to both addresses widens the default trust set to two Stork-controlled keys for the transition. The alternative is defaulting to the new address only, which would break any deployment still served by the old key. Dropping the old address is a follow-up once the rotation is complete everywhere.
  • Signers are fixed at construction, so changing STORK_PUB_KEY requires a restart. That was already the case in practice since the process environment does not change at runtime.

What

  • providers/apis/stork/utils.go: DefaultSignerAddresses, SignerAddressesFromEnv, ParseSignerAddresses; VerifyStorkSignature now takes []common.Address and checks membership.
  • providers/apis/stork/api_handler.go: APIHandler holds signers, populated in NewAPIHandler; construction fails on an invalid STORK_PUB_KEY.
  • providers/apis/stork/api_handler_test.go: tests for list parsing (whitespace, prefixes, repeated separators), environment fallback and copy-on-fallback, fail-fast construction, and multi-signer verification against the real XAU-USD production signature in first, middle, and last position. TestCreateURL expects the &provider=stork query parameter, and the unset-variable case asserts the defaults are used.
  • providers/apis/pyth/api_handler_test.go: the unset-variable case asserts the default PYTH_PUB_KEY is used for verification; a new case covers an invalid value.
  • providers/base/provider_test.go: the unchanged-result timestamp test derives its timestamps from time.Now() instead of fixed calendar dates, so the "after now" assertion holds regardless of when the test runs.

Verified locally with Go 1.25.4: make lint, go mod tidy, go build ./..., and go test -race over all non-integration packages.

@2dpetkov
2dpetkov requested a review from a team as a code owner September 8, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants