Skip to content

feat: add verifyOffchainMessage helper for signed v1 offchain messages#1725

Draft
amilz wants to merge 1 commit into
anza-xyz:mainfrom
amilz:feat/ocms-verification-helper
Draft

feat: add verifyOffchainMessage helper for signed v1 offchain messages#1725
amilz wants to merge 1 commit into
anza-xyz:mainfrom
amilz:feat/ocms-verification-helper

Conversation

@amilz

@amilz amilz commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Adds a verifyOffchainMessage helper to @solana/offchain-messages that verifies a signed version 1 offchain message returned by an untrusted signer (eg. a wallet) in a single call.

When you ask a wallet to sign an offchain message, it hands back the bytes it signed plus a signature. Verifying the signature alone is not enough — a compromised signer could return a perfectly valid signature over data that has nothing to do with what you asked it to sign. From the message and requiredSigners you requested, verifyOffchainMessage reconstructs the version 1 message you expected the signer to sign, asserts it matches the bytes the signer reports having signed, then asserts the signature is a valid Ed25519 signature of those bytes by the signer.

This is distinct from the existing verifyOffchainMessageEnvelope, which trusts the envelope's content as-is and has no notion of an expected message to compare against.

  • Only version 1 messages are supported, matching the upcoming solana:signOffchainMessage wallet feature.
  • For the common case of a connected wallet signing a message only it needs to sign, requiredSigners is optional and defaults to [signer].
  • The signer returns one signature per signer, so call this once per signature you collect.
  • Reconstructing the expected bytes via the v1 encoder reuses the spec's structural checks (non-empty, duplicate-free, sorted signers; non-empty content), so a malformed request throws before any comparison.
  • Adds error code SOLANA_ERROR__OFFCHAIN_MESSAGE__CONTENT_DOES_NOT_MATCH_EXPECTED (5607018), thrown when the signed bytes don't match the expected message.

Test plan

  • New verifyOffchainMessage test suite covering: happy path, requiredSigners default, content mismatch, short-circuit before signature verification, invalid signature, signer-not-required guard, out-of-order signers, version tampering, truncated bytes, and the structural-validation cases.
  • pnpm test:lint, test:typecheck, test:unit:node, test:unit:browser all pass.

…sages

Adds a `verifyOffchainMessage` helper to `@solana/offchain-messages` that verifies a signed version 1 offchain message returned by an untrusted signer (eg. a wallet) in a single call. Verifying the returned signature alone is insufficient, because a compromised wallet can hand back a valid signature over data unrelated to what the dApp requested. The helper encodes the message the caller expected the signer to sign, asserts that it matches the bytes the signer reports having signed, and then asserts that the signature is a valid Ed25519 signature of those bytes by the signer.

It supports version 1 messages only, matching the v1-only `solana:signOffchainMessage` wallet-standard feature. A new `SOLANA_ERROR__OFFCHAIN_MESSAGE__CONTENT_DOES_NOT_MATCH_EXPECTED` error code is thrown when the signed bytes do not match the expected message. Because encoding the expected message already enforces the spec's structural rules (non-empty and duplicate-free required signatories serialized in sorted order, and non-empty content), the helper does not re-implement those checks and instead surfaces the encoder's errors.
@changeset-bot

changeset-bot Bot commented Jun 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b63dcdb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 47 packages
Name Type
@solana/offchain-messages Minor
@solana/errors Minor
@solana/kit Minor
@solana/signers Minor
@solana/accounts Minor
@solana/addresses Minor
@solana/assertions Minor
@solana/codecs-core Minor
@solana/codecs-data-structures Minor
@solana/codecs-numbers Minor
@solana/codecs-strings Minor
@solana/compat Minor
@solana/fixed-points Minor
@solana/instruction-plans Minor
@solana/instructions Minor
@solana/keys Minor
@solana/options Minor
@solana/program-client-core Minor
@solana/programs Minor
@solana/react Minor
@solana/rpc-api Minor
@solana/rpc-spec Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions-spec Minor
@solana/rpc-subscriptions Minor
@solana/rpc-transformers Minor
@solana/rpc-transport-http Minor
@solana/rpc-types Minor
@solana/rpc Minor
@solana/subscribable Minor
@solana/sysvars Minor
@solana/transaction-confirmation Minor
@solana/transaction-messages Minor
@solana/transactions Minor
@solana/wallet-account-signer Minor
@solana/plugin-interfaces Minor
@solana/rpc-graphql Minor
@solana/rpc-parsed-types Minor
@solana/rpc-subscriptions-api Minor
@solana/codecs Minor
@solana/fast-stable-stringify Minor
@solana/functional Minor
@solana/nominal-types Minor
@solana/plugin-core Minor
@solana/promises Minor
@solana/rpc-spec-types Minor
@solana/webcrypto-ed25519-polyfill Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@mcintyre94 mcintyre94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I think this would be a good addition to Kit, various consumers of offchain message signing should be verifying that the message is as they expect.

I think this API is doing too much though, and in particular is tied to the v1 spec such that a hypothetical v2 would probably require a breaking change.

I think the easiest way to handle that is probably to make this v1 specific, and make it the responsibility of apps to call the appropriate version. That would move decoding to the app, but I think reducing the scope of this method would be good.

I'd then restructure it so that it has a signature like:

function assertV1OffchainMessagesEqual(
    receivedMessage: OffchainMessageV1,
    expectedMessage: OffchainMessageV1 // or omit `version`
)

Then it would:

  • check version of receivedMessage, throw if not 1
  • check content equal, throw otherwise
  • check requiredSignatories equal, throw otherwise

I'd use different error codes for each check, with the precise expected/received value that mismatches in context. We should double check we don't consider that sensitive, but I think it'd make sense.

Then the consumer would:

await verifySignature(publicKey, signature, signedMessageBytes) // if it needs to check multiple signatures, the app can do so here
const receivedOffchainMessage = getOffchainMessageDecoder().decode(signedMessageBytes)
if (receivedOffchainMessage.version === 1) {
    assertV1OffchainMessagesEqual(receivedOffchainMessage, myInputOffchainMessage)
} else {
  // throw, not the version the app expected
}

Now if the spec moves on and the dapp starts sending version 2 messages to the wallet and we add a v2 verify function, it just needs to change:

if (receivedOffchainMessage.version === 2) {
    assertV2OffchainMessagesEqual(receivedOffchainMessage, myInputOffchainMessage)
} else {
  // throw, not the version the app expected
}

On another note, thinking about this API in the context of multiple requiredSigners makes the feature as proposed feel quite limited. An app would need to piece together the envelope with multiple signatures itself. We'll be able to hide that in the signer API (as we currently do with signMessage) though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants