wevm / wevm/mppx

tempo: signed transaction validBefore is ~45s after sign, much shorter than challenge.expires (5min)

Open
#395 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
176
Forks
66
Avg merge
1d 1h
Merged PRs (30d)
52

Description

Summary

When mppx sign produces a transaction-type credential for a tempo.charge challenge, the underlying Tempo transaction's validBefore is set to roughly 45 seconds after sign time, regardless of the challenge's expires field (typically 5 minutes out per the mppx server defaults). On Tempo mainnet, this causes the signed transaction to be rejected on-chain (current_block_timestamp >= validBefore) whenever the round-trip from sign → submit → server verify exceeds ~45 seconds.

This is invisible in fast end-to-end tests but consistently breaks agent-driven MCP flows, where each tool call (challenge → mppx sign → submit) easily takes 30-90 seconds of agent overhead.

Environment

  • mppx CLI: 0.6.5
  • mppx server lib: tested both 0.5.7 and 0.6.5 — same symptom, different surfaced error class
  • Tempo network: mainnet (chainId 4217, https://rpc.tempo.xyz)
  • Tempo network: testnet (chainId 42431, https://rpc.moderato.tempo.xyz) — does NOT exhibit this (transaction validity is more lenient or block timestamp progresses differently)

Repro

Standalone bash repro using mppx sign + curl against any mppx-protected endpoint that returns a Tempo charge challenge with a 5-minute expires:

# 1. Get challenge (any mppx-server-protected URL on Tempo mainnet)
CHAL=$(curl -sS https://your-server/path | extract WWW-Authenticate)
echo "$CHAL"
# → Payment id="...", method="tempo", expires="2026-04-26T15:05:04.853Z" ...
#   (challenge expires ~5 minutes from now)

# 2. Sign immediately
CRED=$(npx mppx sign --account my-wallet --challenge "$CHAL")

# 3. Decode the signed transaction's validBefore
node -e '
import { Credential } from "mppx";
import { Transaction } from "viem/tempo";
const c = Credential.deserialize(process.argv[1].replace(/^Payment /, ""));
const tx = Transaction.deserialize(c.payload.signature);
console.log("challenge.expires:", c.challenge.expires);
console.log("tx.validBefore (unix):", tx.validBefore);
console.log("tx.validBefore (date):", new Date(Number(tx.validBefore) * 1000).toISOString());
console.log("delta sign-time → validBefore:", (Number(tx.validBefore) - Math.floor(Date.now()/1000)), "seconds");
console.log("delta validBefore → challenge.expires:", (Math.floor(Date.parse(c.challenge.expires)/1000) - Number(tx.validBefore)), "seconds");
' "$CRED"

Sample output observed in production:

challenge.expires: 2026-04-26T15:05:04.853Z
tx.validBefore (unix): 1777215649
tx.validBefore (date): 2026-04-26T15:00:49.000Z
delta sign-time → validBefore: ~45 seconds
delta validBefore → challenge.expires: ~255 seconds (the unused window)

Symptom on mainnet

When the credential is submitted to a server that runs tempo.charge verify(), the server's pre-flight viem_call simulation fails with the raw RPC error:

Revm error: transaction expired: current block timestamp 1777216094 >= validBefore 1777215649

(viem wraps this as CallExecutionError: An internal error was received in 0.6.5, or InvalidInputRpcError: Missing or invalid parameters in 0.5.7.)

The actual Tempo mainnet RPC is healthy — eth_chainId and eth_blockNumber work fine. The chain just refuses to simulate / accept a transaction whose validBefore is in the past.

Confirming it's purely timing

A fast-path repro succeeds first try:

# Fast (4 seconds end-to-end, mainnet, fresh wallet) → succeeds
T=$(date +%s); CHAL=$(curl ...); CRED=$(mppx sign ...); curl -d {credential=$CRED}
# → {"paid": true, ...}, ~$0.01 USDC settles on chain

A slow-path repro (sign, wait 60s, submit) fails with transaction expired. Same code, same wallet, same RPC.

Root cause (best guess)

In dist/tempo/server/Charge.js, the challenge issuance side uses defaults.expires or a 5-minute window. But on the client side, tempo.charge (or whatever mppx CLI calls under the hood) appears to set transaction validBefore to a much shorter window — possibly hardcoded to a default like now + 60s rather than challenge.expires.

I haven't dug into the CLI's signing code path, but the credential's TX consistently shows validBefore ~45s after sign-time across many test signs, regardless of how far out challenge.expires is.

Suggested fix

In mppx sign (or wherever the Tempo transaction is constructed when responding to a charge challenge), set tx.validBefore to Math.floor(Date.parse(challenge.expires) / 1000) — i.e., let the transaction be valid for the entire window the server already promised. The challenge expiry is the upper bound the server committed to honoring; the TX validity should match.

Alternative: make the validBefore window configurable via a mppx sign --valid-for <seconds> flag and/or MPPX_VALID_FOR env var.

Impact

  • Severity: high for any agent-driven flow on Tempo mainnet
  • Workarounds: retry the sign+submit cycle (sometimes works if the second attempt is faster); or use the Stripe SPT rail (no on-chain expiry)
  • Not affected: testnet (chainId 42431), fast direct HTTP integrations

Happy to test a fix or contribute a PR if helpful.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Trace the mppx sign Tempo charge path and compare it with challenge issuance in dist/tempo/server/Charge.js. Run the standalone bash reproduction, decode the credential, and compare tx.validBefore with challenge.expires; done means the signed transaction remains usable through the promised challenge window and the slow mainnet submission succeeds.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
payments
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.