Skip to content

feat(samples): add Tenzro DID-based settlement sample#257

Closed
hilarl wants to merge 2 commits intogoogle-agentic-commerce:mainfrom
hilarl:tenzro/did-settlement-sample
Closed

feat(samples): add Tenzro DID-based settlement sample#257
hilarl wants to merge 2 commits intogoogle-agentic-commerce:mainfrom
hilarl:tenzro/did-settlement-sample

Conversation

@hilarl
Copy link
Copy Markdown

@hilarl hilarl commented May 2, 2026

feat(samples): add Tenzro DID-based settlement sample

Summary

Adds a new runnable Python sample under
code/samples/python/tenzro_did_settlement/ that demonstrates the AP2
mandate chain (IntentMandateCartMandatePaymentMandate)
running against a DID-based identity layer, with an on-chain
delegation primitive and a STARK-based settlement commitment.

The sample uses TDIP and
did:tenzro: as one concrete instance of a DID-based identity layer,
but the structure is identity-system-agnostic — the chain-specific glue
is isolated in a single thin tenzro_client.TenzroClient module so the
same sample skeleton works against did:web, did:key, did:ion,
did:eth, or any other W3C DID method whose chain exposes a comparable
delegation primitive and a settlement-proof primitive.

What's in the sample

code/samples/python/tenzro_did_settlement/
├── README.md           # sample-level explanation
├── main.py             # ~440-line runnable end-to-end flow
├── tenzro_client.py    # thin Tenzro JSON-RPC client (~210 lines)
├── requirements.txt    # pinned deps: requests, pydantic, pytest, responses
└── test_main.py        # pytest end-to-end test against a stub RPC

The flow exercises all four nested validation ceilings that an AP2
mandate chain can be subject to in a DID-based deployment:

  1. AP2 IntentMandate constraintsmax_amount, allowed
    merchants, allowed SKUs, expiry. (Plain AP2 SDK semantics.)
  2. AP2 CartMandate consistency — total = sum of line items,
    parent intent matches, expiry. (Plain AP2 SDK semantics.)
  3. TDIP DelegationScope::enforce_operation — a protocol-level
    ceiling set when the machine identity was registered
    (max_transaction_value, allowed_operations,
    allowed_payment_protocols, allowed_chains, time_bound).
  4. TDIP runtime SpendingPolicy::check — an execution-level
    ceiling that the agent's runtime registry enforces (e.g. daily-spend
    windows). Mutable; orthogonal to the protocol ceiling.

Settlement: a Plonky3 STARK over the settlement AIR is generated via
tenzro_createZkProof; its 32-byte SHA-256 commitment matches the
on-chain ZkCommitmentRegistry format so the EVM ZK_VERIFY
precompile becomes an O(1) HashSet lookup.

The PaymentMandate.payment_method.supported_methods is
tenzro:micropayment-channel, dispatching to a Tenzro per-message
billing channel.

The sample also adds a one-paragraph subsection
"DID-based identity layer (example: TDIP)" to
docs/ap2/implementation_considerations.md (see the diff below).

Why this fills a gap

The existing samples in code/samples/python/scenarios/a2a/ are
excellent demonstrations of card-based and x402-based flows, but they
use opaque identifiers for the user / agent / merchant parties. They do
not show what AP2 looks like when those identifiers are first-class
DIDs with on-chain delegation enforcement and verifiable-credential-
style scope binding.

AP2's mandate types are identity-system-agnostic — the principal,
agent, and merchant identifiers are opaque strings. This PR makes
that explicit by giving implementers a runnable, end-to-end example
of layering AP2 on top of a DID method.

What this PR does NOT touch

  • No spec changes. docs/ap2/specification.md, payment_mandate.md,
    flows.md, etc. are FIDO Alliance's responsibility now.
  • No SDK changes. code/sdk/python/ap2/ is untouched. The sample
    consumes the SDK exactly as published.
  • No new external dependencies. The sample uses requests,
    pydantic (a transitive dep of ap2 already), and the test uses
    responses for HTTP mocking. All deps are publicly available on
    PyPI; nothing private, nothing wallet-secret.

Test plan

The sample ships with test_main.py, which runs the full
main.run() flow end-to-end against a stub Tenzro JSON-RPC backend
mocked with responses:

cd code/samples/python/tenzro_did_settlement
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pip install -e ../../../sdk/python/ap2   # AP2 SDK from this repo
pytest test_main.py -v

Expected: 5 tests pass.

  • test_intent_and_cart_build_and_round_trip — AP2 SDK objects
    build correctly and round-trip into the Tenzro VDC envelope shape.
  • test_local_validation_passes_within_all_ceilings — all four
    ceilings green-light a within-budget cart.
  • test_local_validation_rejects_when_daily_window_exceeded — the
    SpendingPolicy ceiling correctly fails when the daily window is
    too small.
  • test_compute_zk_commitment_matches_expected_shape — the SHA-256
    commitment matches Tenzro's compute_zk_commitment shape.
  • test_run_end_to_end_against_stub_rpc — the full main.run()
    flow drives the stub RPC through resolve_did,
    ap2ValidateMandatePair, and createZkProof.
  • test_run_offline_mode_skips_rpc--offline flag works.

The sample also runs against the live testnet at rpc.tenzro.network
without modification — the read-only RPCs (DID resolution, mandate
validation, ZK proof generation) require no funded keys; the channel
update is intentionally simulated because that step requires a
pre-opened, funded channel.

CLA acknowledgment

TODO(@hilarl) — replace this block with a confirmation reference
from https://cla.developers.google.com/ (individual or covered by
employer corporate CLA) before submitting the PR.

I have signed the Google CLA. This sample is a Tenzro-funded
contribution to the AP2 ecosystem; the copyright headers in
tenzro_client.py and main.py declare Apache-2.0 licensing,
matching the rest of the repository.

AI assistance disclosure

Author: Hilal Agil <hilal@tenzro.com>, GitHub
@hilarl.

Parts of this contribution were AI-assisted via Claude Code. All
technical decisions — the choice to layer AP2 on top of TDIP, the
mapping from AP2's three mandate types onto Tenzro's IntentMandate /
CartMandate VDC envelope, the use of the settlement Plonky3 AIR for
the commitment, the four-ceiling validation model — are human-authored
and consistent with Tenzro's production AP2 validator
(tenzro_payments::ap2::MandateValidator::validate_with_delegation_and_policy).
The drafting of boilerplate (docstrings, repetitive Pydantic model
construction, test scaffolding) was AI-assisted; every file was
reviewed and tested by the author before submission.

Checklist

  • Google CLA signed (placeholder above must be filled in).
  • Sample under code/samples/python/tenzro_did_settlement/ only.
  • requirements.txt pinned to specific versions.
  • Test passes locally (pytest test_main.py).
  • No changes to docs/ap2/specification.md (FIDO scope).
  • No changes to code/sdk/python/ap2/ (SDK scope).
  • One small additive change to
    docs/ap2/implementation_considerations.md (descriptive doc, not
    spec) — single paragraph cross-referencing the sample. Reviewers
    who consider that file out of scope can drop it; the sample
    stands on its own.
  • Apache-2.0 copyright headers on all new source files, matching
    the existing repo convention.
  • Sample runs against the live rpc.tenzro.network testnet
    end-to-end, no environment variables required.
  • AI-assistance disclosure included.

DCO

Signed-off-by: Hilal Agil <hilal@tenzro.com>

Adds a runnable Python sample demonstrating the AP2 mandate chain (IntentMandate → CartMandate → PaymentMandate) running against a DID-based identity layer with on-chain delegation and a STARK settlement commitment. Uses TDIP/did:tenzro: as one concrete instance; the structure is identity-system-agnostic.

Signed-off-by: Hilal Agil <hilaal@gmail.com>
@hilarl hilarl requested a review from a team as a code owner May 2, 2026 12:45
@google-cla
Copy link
Copy Markdown

google-cla Bot commented May 2, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a DID-based settlement sample using the Tenzro Decentralized Identity Protocol (TDIP), including a main script, a JSON-RPC client, and tests. Feedback addresses a critical bug regarding ZK witness field overflow and provides suggestions for improving time-handling consistency and thread safety.

service_proof_hash = hashlib.sha256(
payment_mandate.model_dump_json().encode("utf-8")
).digest()
service_proof_int = int.from_bytes(service_proof_hash[:4], "little")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The service_proof_int is derived from 4 bytes of a hash, which can result in a value up to $2^{32}-1$. As noted in tenzro_client.py (line 186), the KoalaBear field modulus is $2^{31} - 2^{24} + 1$ (2,130,706,433). There is a significant chance (~50.4%) that this integer will exceed the field modulus. If the RPC strictly validates that witness fields are within the field range, this will cause intermittent failures. It is safer to reduce the value modulo the field size to ensure it is always a valid field element.

Suggested change
service_proof_int = int.from_bytes(service_proof_hash[:4], "little")
service_proof_int = int.from_bytes(service_proof_hash[:4], "little") % 2130706433

Comment on lines +337 to +361
ip = intent_vdc["payload"]
cp = cart_vdc["payload"]
results: list[tuple[str, bool, str]] = []

# 1. AP2 IntentMandate constraints
intent_ok = (
cp["total_amount"] <= ip["max_amount"]
and ip["agent_did"] == cp["agent_did"]
and (not ip["allowed_merchants"] or cp["merchant_did"] in ip["allowed_merchants"])
and datetime.fromisoformat(ip["expires_at"]) > datetime.now(UTC)
)
results.append(
(
"AP2 IntentMandate constraints",
intent_ok,
f"max_amount={ip['max_amount']} cents, cart total={cp['total_amount']} cents",
)
)

# 2. AP2 CartMandate consistency
recomputed = sum(item["total"] for item in cp["items"])
cart_ok = (
recomputed == cp["total_amount"]
and datetime.fromisoformat(cp["expires_at"]) > datetime.now(UTC)
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

The local_validate_mandates function calls datetime.now(UTC) multiple times (lines 346 and 360). This can lead to minor inconsistencies if the system clock advances between calls, and it makes the function less deterministic for testing. It is better to capture the current time once at the start of the function.

    now = datetime.now(UTC)
    ip = intent_vdc["payload"]
    cp = cart_vdc["payload"]
    results: list[tuple[str, bool, str]] = []

    # 1. AP2 IntentMandate constraints
    intent_ok = (
        cp["total_amount"] <= ip["max_amount"]
        and ip["agent_did"] == cp["agent_did"]
        and (not ip["allowed_merchants" ] or cp["merchant_did"] in ip["allowed_merchants"])
        and datetime.fromisoformat(ip["expires_at"]) > now
    )
    results.append(
        (
            "AP2 IntentMandate constraints",
            intent_ok,
            f"max_amount={ip['max_amount']} cents, cart total={cp['total_amount']} cents",
        )
    )

    # 2. AP2 CartMandate consistency
    recomputed = sum(item["total"] for item in cp["items"])
    cart_ok = (
        recomputed == cp["total_amount"]
        and datetime.fromisoformat(cp["expires_at"]) > now
    )

Comment on lines +97 to +98
rpc_id = self._next_id
self._next_id += 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

The increment of self._next_id is not thread-safe. While this sample is likely used in a single-threaded context, if the TenzroClient were used in a multi-threaded environment (e.g., within a concurrent web server), multiple requests could end up with the same JSON-RPC id. Although many servers handle duplicate IDs, it is best practice to use a thread-safe counter or a lock to ensure uniqueness per session.

Spellcheck: add domain-specific words (tenzro, tdip, plonky, micropayment, rustaceans, pdis) and standard test-fixture names (rsps for the responses library, capsys for pytest). Two local variable / string-prefix tweaks (deleg_ok -> delegation_ok, chan- -> channel-) avoid adding non-words to the dictionary.

main.py: reduce service_proof_int modulo the KoalaBear field prime (2,130,706,433) so the witness is always a valid field element. Without this, ~50% of 4-byte LE samples exceed the modulus and the prover would reject the witness. Resolves Gemini HIGH review comment.

main.py: capture datetime.now(UTC) once at the start of local_validate_mandates so all expiry checks observe the same instant. Resolves Gemini LOW review comment.

tenzro_client.py: guard self._next_id increment with a threading.Lock so the same TenzroClient instance can be safely shared across threads without two concurrent calls picking the same JSON-RPC id. Resolves Gemini LOW review comment.

Signed-off-by: Hilal Agil <hilaal@gmail.com>
@hilarl
Copy link
Copy Markdown
Author

hilarl commented May 2, 2026

@googlebot I signed it!

1 similar comment
@hilarl
Copy link
Copy Markdown
Author

hilarl commented May 2, 2026

@googlebot I signed it!

@hilarl
Copy link
Copy Markdown
Author

hilarl commented May 3, 2026

Withdrawing.

@hilarl hilarl closed this May 3, 2026
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.

1 participant