Repository navigation
fix(x402): sign an authorization window that has not already expired - #31
Open
pucedoteth wants to merge 1 commit into
Open
pucedoteth wants to merge 1 commit into
pucedoteth wants to merge 1 commit into
Conversation
EIP-3009 validAfter/validBefore are absolute unix timestamps — the token
contract enforces `block.timestamp < validBefore`. _build_payment_header signed
the challenge's maxTimeoutSeconds directly:
"validAfter": "0",
"validBefore": str(option["maxTimeoutSeconds"]),
maxTimeoutSeconds is a duration (how long the resource server waits), so a
typical challenge produced validBefore = 60, i.e. 1970-01-01 00:01:00 UTC. Every
authorization was signed already expired and cannot settle:
validBefore = 60 -> 1970-01-01 00:01:00 UTC
now = 1787752097 -> 2026-08-26 13:48:17 UTC
Compute the window from the clock instead: validBefore = now +
maxTimeoutSeconds, and backdate validAfter by 600s so clock skew against the
settling node can't make a fresh authorization look not-yet-valid — matching
what the x402 reference client does.
The value is written twice, into the EIP-712 message that gets signed and into
the authorization that is submitted, so both now read from one computed pair; a
test pins them to each other.
Co-Authored-By: Claude Opus 5 <[email protected]>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
EIP-3009
validAfter/validBeforeare absolute unix timestamps — the token contract enforcesblock.timestamp < validBefore._build_payment_headersigns the challenge'smaxTimeoutSecondsdirectly:maxTimeoutSecondsis a duration (how long the resource server will wait for settlement), not a deadline. For a typical challenge advertising 60 it producesvalidBefore = 60, which is 1 January 1970, 00:01:00 UTC.Running
_build_payment_headeragainstmainand decoding the header it returns:Every authorization the CLI signs is already expired when it is submitted, so it cannot settle — the transfer reverts on the
require(block.timestamp < validBefore)check.The same pair is written twice, once into the EIP-712 message that is signed and once into the
authorizationobject that is sent, so both copies carry it.The fix
Derive the window from the clock:
validBefore = now + maxTimeoutSeconds— the advertised timeout, as a deadlinevalidAfter = now - 600— backdated so a small clock difference against the settling node can't make a freshly signed authorization look not-yet-validThis matches what the x402 reference client does. Both call sites now read from one computed pair rather than repeating the expression.
After:
Test plan
tests/test_x402.pyis new — there was no coverage of this module. Six cases: the two_select_payment_optionbranches, then the validity window (deadline in the future,validAfteralready past but bounded, window spanning the advertised timeout), plus one that pins the signed EIP-712 message to the submitted authorization field-by-field, so the two copies can't drift apart again.Reverting only
cli/clients/x402.pyand keeping the tests reproduces the table above.One assumption worth confirming
I read
maxTimeoutSecondsas a duration, per the x402 spec's "maximum number of seconds the resource server will wait for settlement", and the 1970 timestamp it currently yields is consistent with that. If your facilitator instead expects the raw advertised value passed through untouched, this is wrong and I'd want to know.