feat: add ZkTlsAttestationHook#46
Conversation
Profile A hook that binds a job's deliverable to one or more zkTLS attestations of the off-chain HTTPS calls the provider promised to make. Client pins, at fund time, each step's URL/method/body/response shape and cross-step static value bindings; the provider attaches one attestation per step at submit; the hook routes each through a pluggable IZkTlsVerifier, enforces the pinned shape, and binds the deliverable to the parsed response. Optional IAttestationExtensionVerifier for business-level checks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zkTLS attestors (Primus and the rest of the deployed ecosystem) stamp attestations with millisecond timestamps, but the original check compared against block.timestamp in seconds — every real attestation tripped the underflow guard and reverted AttestationStale. Divide att.timestamp by 1000 before the compare. maxAge stays in seconds so the spec interface is unchanged. Found by an end-to-end test against a real Primus attestation on Base Sepolia. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Hi @xiangxiecrypto, Thank you for the hook PR. The lifecycle wiring is correct. The trust posture is open by default in a few places. I would like to see those closed before this lands. None of this needs a rewrite. Please address the following items:
If any of these items has a design reason I have missed, please reply on the thread before changing the code. I would rather update my review than have you write a fix you do not agree with. |
ariessa
left a comment
There was a problem hiding this comment.
Please address the comments above
|
@ariessa Thanks for the thorough review. Accepting all 12; proceeding with implementation. Quick confirmation of what I'm doing per item, so you can flag any misreads before the push:
Will push as a single follow-up commit with failing-then-passing tests added for each path. Toolkit (SDK + docs + on-chain demos) will be updated to match in a separate PR on the companion repo. |
1. Cross-job replay defense
- RequestStep.expectedJobBinding stores keccak256(jobId, hook, chainid)
- _postFund verifies the spec's binding equals what the hook computes
- _verifyOneStep checks the ASCII-hex binding appears in att.additionParams
2. EOA-verifier rejection
- _requireContract (extcodesize) helper
- Called in constructor for zkTlsVerifier
- Called in _postFund for customVerifier (when not address(0))
- Called in setTrustedExtensionVerifier(verifier, true)
- Called in proposeZkTlsVerifier
3. maxAge bounds
- MIN_MAX_AGE = 1, MAX_MAX_AGE = 24h
- _postFund rejects out-of-range values per step
4. Quorum allowlist replaces single pinnedAttestor
- RequestStep.allowedAttestors[] + minAttestorsRequired
- MAX_ATTESTORS_PER_STEP = 8 to bound the O(N*M) loop
- _verifyOneStep counts attestation.attestors ∩ allowedAttestors
5. Empty optParams in _postFund reverts SpecRequired
- No more silent no-op leaving the escrow bricked
6. Owner-curated customVerifier allowlist
- trustedExtensionVerifiers mapping + onlyOwner setter
- _postFund rejects ExtensionVerifierNotTrusted when spec sets a
customVerifier not on the allowlist
7. TimeUnit enum on RequestStep
- Seconds | Milliseconds; divisor follows the enum
- FORWARD_SKEW_TOLERANCE (30s) on the staleness check
- Two-sided check: rejects both far-past and far-future timestamps
8. Dynamic data bindings
- DataBinding.fromExtractKey: keyName in atts[fromStep].data
- _extractFieldValue handles JSON string values incl. escape sequences
- Static `value` and dynamic `fromExtractKey` are mutually exclusive
- _postFund enforces exactly one of them is non-empty
9. _containsBounded with delimiter set
- Replaces _contains in binding validation
- Match must be bracketed by " , } : & = / (or by string edges)
- Defends against false positives like "100" matching inside "1000"
10. Two-step verifier rotation
- zkTlsVerifier is no longer immutable; owner can rotate
- proposeZkTlsVerifier sets pending + 7-day activation timestamp
- activateZkTlsVerifier promotes after the delay
- Per-job snapshot in AttestationSpec.zkTlsVerifierSnapshot, written
at _postFund; _preSubmit uses the snapshot so in-flight jobs are
immune to rotation
11. Reject unsatisfiable specs at config time
- _postFund requires at least one of methodHash / urlHash / bodyHash
to be non-zero per step
12. Drop AlreadyValidated check
- Core state machine already prevents resubmission
- envelopeCommitments[jobId] write retained for off-chain indexing
Helper changes:
+ Ownable (OZ) — adds owner_ constructor arg
+ jobBindingFor(jobId) public view for SDK/spec-author convenience
+ getVerifierSnapshot(jobId) — splits getSpec to keep stack depth in budget
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Profile A (Simple Policy) hook that cryptographically binds an ERC-8183 job's
deliverableto one or more zkTLS attestations of the off-chain HTTPS calls the provider promised to make. The client pins, atfundtime, each step's URL / method / body / response shape and any cross-step value bindings; the provider attaches one zkTLS attestation per step atsubmit; the hook routes every attestation through a pluggableIZkTlsVerifier, enforces the pinned shape, and binds the deliverable to the parsed response.Use case
When the provider in an ERC-8183 job is an AI agent calling external HTTPS APIs (price oracles, LLMs, fact endpoints, …), the protocol has no on-chain trace of those calls. The evaluator has to either trust the provider's word or duplicate the work themselves. zkTLS closes that gap: a cryptographic protocol allows the agents to prove "this request really happened, this is the response." This hook binds that proof to the on-chain deliverable, with no modification to the ERC-8183 core.
Flow
createJob(provider, evaluator, expiredAt, description, hook=this, …)fund(jobId, expectedBudget, optParams=abi.encode(AttestationSpec))→
_postFundstores the spec immutably (client-only by core semantics).Attestationper step.submit(jobId, deliverable, optParams=abi.encode(Attestation[], bytes))→
_preSubmit:i,IZkTlsVerifier.verifyAttestation(atts[i])viastaticcall, then enforce every pinned-field hash + timestamp window +pinnedAttestor(if set);atts[fromStep].dataandatts[toStep].request.{url|header|body};keccak256(atts[sourceStep].data) == deliverable;customVerifier != 0, delegate to it for business-level checks viastaticcall.complete/reject— normal flow.Trust model
The hook validates attestation shape and binding only:
additionParamshash to what the spec pinned.The hook does not enforce:
customVerifierfor ranges / thresholds / cross-field arithmetic.Per-job spec is frozen at fund time. The hook is immutable; to change behaviour, deploy a new hook and rotate the whitelist.
Compatibility
BaseERC8183Hook, implementsIERC8183HookMetadata, ERC-165-advertised.requiredSelectors()returnsfund+submit— both must be wired together when used behindMultiHookRouter..solfile; all custom structs / interfaces / errors inlined; only imports from this repo areBaseERC8183HookandIERC8183HookMetadata.IZkTlsVerifierinterface; theAttestationstruct uses the de facto on-chain layout (thereponseResolvespelling is preserved for ABI compatibility with already-deployed verifiers).forge buildcompiles cleanly. Adds a row to theREADME.mdHook Examples table per the contribution rules.Full toolkit, SDK, tests, and live deployment
For end-to-end validation against a real zkTLS provider (Primus), an off-chain SDK that builds and encodes the
AttestationSpec, a referenceIAttestationExtensionVerifier, deployment scripts, and a complete test suite (33 unit + 16 integration + 6 fork + on-chain E2E), see the companion toolkit:👉 https://github.com/xiangxiecrypto/primus-zktls-hook-toolkit
The toolkit holds everything the upstream rules say doesn't belong in a hook PR (tests, fixtures, SDK, vendor-specific wiring), wired against the deployed Primus PrimusZKTLS verifier on Base Sepolia. Five completed on-chain job lifecycles cover single-step, multi-step, LLM, and
customVerifierscenarios.