fix(tempo/server): full Tempo CLI 1.6+ compatibility (keychain + 0x78 sponsored form)#34
fix(tempo/server): full Tempo CLI 1.6+ compatibility (keychain + 0x78 sponsored form)#34jake-bp wants to merge 3 commits into
Conversation
73fb118 to
775e41d
Compare
The original keychain (AA-wallet) signature handling added in PR tempoxyz#34 covers the pre-co-sign sender-recovery path in verifyTransaction, but not the post-co-sign dual-signature verification at the end of the fee-payer branch. tempotx.VerifyDualSignatures calls VerifySignature which is secp256k1-only and returns "only secp256k1 can be verified off-chain" for keychain envelopes — so any sponsored charge from a Tempo CLI managed wallet failed at this step with "co-signed transaction failed signature verification" after sponsorship was otherwise wired up. Mirrors the existing pre-co-sign keychain branch: - When tx.Signature.Type == "keychain": re-verify the access key via keychain.VerifyAccessKeySignature (with the same YParity 27/28 → 0/1 fixup the pre-co-sign branch uses), then verify the fee-payer signature separately via tempotx.VerifyFeePayerSignature against the recovered root account. - Otherwise: fall back to tempotx.VerifyDualSignatures (the existing plain-ECDSA path). The recovered root account is then compared against the pre-co-sign sender, preserving the invariant that the same wallet is referenced before and after the co-signature. Mirrored from blockparty-global/sre-services PR #193.
The patched chargeserver.verifyTransaction currently swallows the underlying tx hash on broadcast and the revert reason on a non-0x1 receipt. Operators investigating "transaction reverted" failures have no way to inspect the failed tx without code-level instrumentation. This is the same diagnostic gap PR tempoxyz#34 (this PR) closed for the keychain signature path; extending it to broadcast/receipt: - Log txHash on successful broadcast (and on SendRawTransaction failure, with the underlying error). - On a non-0x1 receipt: log status / blockNumber / gasUsed / revertReason and the full receipt map. Surface the revert reason in the response body when the chain provides one. - Log fetchReceipt errors with the txHash for context. Receipt bodies are chain-public; no secrets exposed. Production cost is one extra log line per successful broadcast plus a multi-field log on failure. Mirrored from blockparty-global/sre-services PR #190 + #194.
5d05375 to
90b0536
Compare
…atures
Tempo CLI ≥ v1.6 signs every MPP charge with a Keychain (0x04) envelope —
the AA-style wrapper that lets a session key sign on behalf of the
smart-account root. Three things in mpp-go conspired to reject every
such payment:
1. `verifyTransaction` called only `tempotx.VerifySignature`, whose
off-chain ecrecover path is hard-wired to secp256k1 and returns
`ErrUnsupportedSignatureType` for any other envelope. Every
keychain-signed payment surfaced as `transaction signature is invalid`
before the on-chain submission step ever ran.
2. The Tempo CLI emits the inner secp256k1 signature inside the keychain
envelope with V in the legacy Ethereum {27, 28} form rather than the
{0, 1} form tempo-go's `RecoverAddress` requires under EIP-2 strict
validation, so even a hand-rolled keychain branch would fail
`ValidateSignatureValues` until the byte was normalised.
3. `transactionMatches` rejected any tx where `tx.KeyAuthorization != nil`,
but AA wallets populate that field on every tx to scope which session
key is authorised to execute. The reject fired before sender recovery
could even run.
Fix:
- Branch on `tx.Signature.Type == "keychain"` in `verifyTransaction`.
Patch byte 85 of a copy of `tx.Signature.Raw` if it's in {27, 28},
delegate to the upstream `keychain.VerifyAccessKeySignature` to
recover the access key, and treat the embedded root account as the
authorising sender. The original `tx.Signature.Raw` bytes are
untouched, so `SendRawTransaction` broadcasts whatever the client
sent. We deliberately do not pre-check `isActiveAccessKey` against
the keychain precompile: that would false-reject counterfactual
smart accounts whose access key is registered as part of execution
at first-tx time, and the chain rejects unauthorised keys at
submission anyway.
- Drop the `tx.KeyAuthorization != nil` clause from `transactionMatches`.
The field controls execution authority; payment correctness is
established by walking `tx.Calls` (target == request.Currency,
value zero, decoded transfer matches expected destination, amount,
memo). `tx.AccessList != 0` is intentionally left as a reject —
EIP-2930 prewarming is unrelated to AA and tempo-cli does not emit
it for charges.
Bumps tempo-go to v0.4.1 for `keychain.VerifyAccessKeySignature` and
`KeychainSignatureLength`.
Test coverage:
- `TestChargeFlow_TransactionCredentialKeychain` — happy-path through
`intent.Verify` against a keychain-signed tx, with a subtest that
flips the inner V to {27, 28} after signing. The mock RPC's onSend
asserts the broadcast envelope is byte-identical to what the verifier
received, proving the YParity normalisation didn't mutate the original.
- `TestTransactionMatches_AllowsKeyAuthorization` — direct
`transactionMatches` test that asserts a tx with `KeyAuthorization`
set is accepted while a tx with non-empty `AccessList` is still
rejected.
Independently fixed downstream by @shawnwollenberg at
blockparty-global/sre-services#181 (vendored `mpp-go-patched/` fork wired
via `replace`); this PR upstreams the same approach so consumers don't
need to maintain their own fork.
…dentials
Tempo CLI 1.6.0 sends sponsored credentials in the fee-payer-signing
form (0x78 prefix) rather than the broadcast/normal form (0x76).
tempo-go's Deserialize hard-codes 0x76 acceptance, so verifyTransaction
rejects every managed-wallet credential with "expected TempoTransaction
prefix 0x76, got 0x78".
Per tempo-go/pkg/transaction/serialize.go::buildRLPList, the two
formats share the same 13/14/15-field RLP body; they only differ in
field 11 (0x76 holds the fee-payer signature or 0x00 marker; 0x78
holds the sender address). The deserializer's field-11 logic falls
through "non-tuple non-0x00 bytes" as an unusual-case no-op, leaving
Tx.FeePayerSignature nil and AwaitingFeePayer false — and the sender's
signing digest is computed via SerializeForSigning (FormatNormal +
ForSigning, with skipFeeToken=true when fee-payer is involved), so
it's independent of which prefix the wire form used.
This change adds a translateFeePayerFormToNormal helper that swaps
0x78 → 0x76 for parsing, then post-processes the resulting Tx:
- tx.AwaitingFeePayer = true (the 0x78 prefix inherently means it).
- tx.FeeToken cleared (0x78 always includes it; the downstream
"must omit fee token before co-signing" check expects empty, and
the server overwrites with request.Currency on co-sign anyway).
Tests:
- TestTranslateFeePayerFormToNormal: 7 sub-cases covering the
case-insensitive 0x prefix matrix.
- TestVerifyTransaction_AcceptsFeePayerSigningForm: round-trips a
real 0x78 payload through tempo-go's own Serialize(FormatFeePayer)
+ the prefix-swap + tempo-go's Deserialize, asserting the post-
fixup Tx state matches what verifyTransaction's downstream code
expects.
Mirrored from blockparty-global/sre-services PR #192.
The keychain signature handling added in the previous commit covers
the pre-co-sign sender-recovery path in verifyTransaction, but not
the post-co-sign dual-signature verification at the end of the
fee-payer branch. tempotx.VerifyDualSignatures calls VerifySignature
which is secp256k1-only and returns "only secp256k1 can be verified
off-chain" for keychain envelopes — so any sponsored charge from a
Tempo CLI managed wallet failed at this step with "co-signed
transaction failed signature verification" after sponsorship was
otherwise wired up.
Mirrors the existing pre-co-sign keychain branch:
- When tx.Signature.Type == "keychain": re-verify the access key
via keychain.VerifyAccessKeySignature (with the same YParity
27/28 → 0/1 fixup the pre-co-sign branch uses), then verify the
fee-payer signature separately via tempotx.VerifyFeePayerSignature
against the recovered root account.
- Otherwise: fall back to tempotx.VerifyDualSignatures (the existing
plain-ECDSA path).
The recovered root account is then compared against the pre-co-sign
sender, preserving the invariant that the same wallet is referenced
before and after the co-signature.
Mirrored from blockparty-global/sre-services PR #193.
90b0536 to
42982f2
Compare
|
Rebased onto One question on direction, since v0.1.1 added keychain handling in the proof-signer path ( The 0x78 fee-payer-signing-form fix (commit 2) is independent of the keychain question — I can split it into its own PR if that's easier to review separately. |
|
Hi! can you retry this with the latest cli version? |
|
Retried with the latest CLI — Latest CLI against minimal charge servers (charge intent, USDC.e,
So on Two things I checked in case
This minimal repro is the non-fee-payer path (commit 1). The |
Summary
Three bug fixes that together let
mpp-go's Tempo charge server accept payments from Tempo CLI ≥ 1.6 wallets end-to-end. The first commit (the original PR scope) handles the keychain signature itself; the next two cover failures that surface only after the first one is fixed:775e41dblockparty-global/sre-services#181fb202d20x78) credentialsblockparty-global/sre-services#19290b0536blockparty-global/sre-services#193All three were independently merged into a downstream
mpp-go-patchedfork atblockparty-global/sre-servicesand validated end-to-end againsthttps://skills.onesource.iowith a Tempo CLI 1.6 wallet (USDC.e access key, USDC.e-funded). Live successful tx:0x00658a50…77e0b3.Why each fix
1. Keychain signature verification (commit
775e41d)Tempo CLI ≥ v1.6 signs every MPP charge with a Keychain (
0x04) envelope — the AA-style wrapper that lets a session key sign on behalf of the smart-account root. Three things inmpp-gov0.1.0 conspired to reject every such payment with400 transaction signature is invalid:verifyTransactiononly handled secp256k1. It calledtempotx.VerifySignature, whose off-chain ecrecover path returnsErrUnsupportedSignatureTypefor any non-secp256k1 envelope.{27, 28}rather than EIP-2{0, 1}, so even a hand-rolled keychain branch tripscrypto.ValidateSignatureValueswithinvalid signature values.transactionMatchesrejectedKeyAuthorization. AA wallets populatetx.KeyAuthorizationon every tx to scope which session key may execute on the root's behalf. The reject fired before sender recovery could even run.I deliberately did not add a
getKey()precompile pre-check before accepting the charge. That would false-reject counterfactual smart accounts whose access key is registered as part of execution at first-tx time — and the chain rejects unauthorised keys at submission anyway. Trading early-bail latency for accepting the wallet category that matters in practice.2.
0x78fee-payer-signing-form deserialize (commitfb202d2)After (1), live
tempo requestcalls reach the gateway and fail at deserialization. Tempo CLI 1.6 sends sponsored credentials in the fee-payer-signing form (0x78prefix) rather than the broadcast/normal form (0x76).tempo-gov0.4.1'sDeserializehard-codes0x76acceptance.Per
tempo-go/pkg/transaction/serialize.go::buildRLPList, the two formats share the same 13/14/15-field RLP body — they only differ in:0x76holds the fee-payer signature (or0x00marker / empty);0x78holds the sender addressSender's signing digest is computed via
SerializeForSigning(FormatNormal+ForSigning,skipFeeToken=truewhen fee-payer is involved) — independent of which prefix the wire form used. So the patch swaps0x78→0x76for parsing, then post-processes:tx.AwaitingFeePayer = true(the prefix inherently means it; the deserializer can only infer this from a0x00field-11 marker, which0x78doesn't carry).tx.FeeTokencleared (the downstream "must omit fee token before co-signing" check expects empty; the server overwrites withrequest.Currencyon co-sign anyway, and the sender's digest excludes fee_token in fee-payer flows so the signature stays valid).3. Keychain-aware post-co-sign verification (commit
90b0536)Once (1) and (2) ship, sponsored Tempo CLI requests get past the access-key check and fee-payer co-sign, then fail at the next step with
"co-signed transaction failed signature verification".tempotx.VerifyDualSignaturescallsVerifySignature, which is secp256k1-only — the same asymmetry as (1), but on the post-co-sign verification path. Mirrors the existing keychain branch: re-verify the access key viakeychain.VerifyAccessKeySignature(with the YParity 27/28 → 0/1 fixup), then verify the fee-payer signature separately viatempotx.VerifyFeePayerSignatureagainst the recovered root account. ThecoSignedSender == senderinvariant is preserved.Test coverage
TestChargeFlow_TransactionCredentialKeychain(existing) — happy path throughintent.Verifyagainst a keychain-signed tx, with a subtest that flips the inner V to{27, 28}after signing. The mock RPC'sonSendasserts the broadcast envelope is byte-identical to what the verifier received, proving the YParity normalisation didn't mutate the original.TestTransactionMatches_AllowsKeyAuthorization(existing) — directtransactionMatchestest that asserts a tx withKeyAuthorizationset is accepted while a tx with non-emptyAccessListis still rejected.TestTranslateFeePayerFormToNormal— 7 sub-cases for the prefix swap (lowercase0x78, uppercase0X78, no0xprefix,0x76unchanged, unknown prefix unchanged, empty, too-short).TestVerifyTransaction_AcceptsFeePayerSigningForm— round-trips a synthetic0x78payload throughtempotx.Serialize(FormatFeePayer)+ the prefix-swap +tempotx.Deserialize, asserts the post-fixupTxmatches whatverifyTransaction's downstream code expects.go test ./...— full suite green.Repro that motivated each fix
Any
tempoMPP server (e.g.https://skills.onesource.io) hit by a tempo-cli ≥ v1.6 wallet:Pre-(1):
transaction signature is invalid.Pre-(2):
failed to deserialize transaction payload(chain rejected0x78prefix before sender recovery ran).Pre-(3):
co-signed transaction failed signature verification(under sponsorship).With all three:
200 OKwith the JSON body.Prior art / parallel investigation
Independently fixed downstream by @shawnwollenberg + @jake-bp at
blockparty-global/sre-servicesPRs #181, #192, #193, all integrated into a vendoredmpp-go-patched/wired via areplacein their gatewaygo.mod. This PR upstreams the same approach so consumers don't need to maintain their own fork.🤖 Generated with Claude Code