Skip to content

feat(tnt-core-v0.13.0): bind quotes to requester, end live operator-tooling outage#1406

Merged
drewstone merged 8 commits into
mainfrom
chore/tnt-core-v0.13.0-quote-binding
May 8, 2026
Merged

feat(tnt-core-v0.13.0): bind quotes to requester, end live operator-tooling outage#1406
drewstone merged 8 commits into
mainfrom
chore/tnt-core-v0.13.0-quote-binding

Conversation

@drewstone
Copy link
Copy Markdown
Contributor

Headline

The pricing engine has been emitting invalid quotes against any tnt-core v0.12.0+ deployment since the v0.12.0 contract upgrade. Every quote is signed with requester: Address::ZERO (hardcoded in crates/pricing-engine/src/signer.rs::build_abi_quote_details), and the on-chain verifyQuoteBatch rejects wildcard quotes — making every operator-issued quote unredeemable.

This PR threads a non-zero requester end-to-end (proto → gRPC → signer → EIP-712 digest → on-chain submission), restoring correctness against tnt-core main (PRs tnt-core#124 and tnt-core#125, audit Round 2 economic finding F1).

Migration spec

1. Cargo dep bump

Cargo.toml:129 flipped from tnt-core-bindings = "0.11.3" to a git dep on tnt-core's main. Reason: bindings v0.13.0 isn't on crates.io yet. Action item for the merger: flip to a versioned crates.io dep (tnt-core-bindings = "0.13.0") once the publish lands.

2. crates/tangle-extra/src/job_quote.rs — full struct migration

  • Added requester: Address as the first field of JobQuoteDetails.
  • Updated JOB_QUOTE_TYPEHASH_STR to "JobQuoteDetails(address requester,uint64 serviceId,uint8 jobIndex,uint256 price,uint64 timestamp,uint64 expiry,uint8 confidentiality)".
  • Updated hash_job_quote_details to abi-encode requester immediately after the typehash.
  • Updated the From<SignedJobQuote>ITangleTypes::SignedJobQuote impl.
  • Refreshed the 4 cross-repo EIP-712 deterministic test vectors against tnt-core/test/tangle/EIP712Compatibility.t.sol:
    • Vector 1 struct hash: 0x81efa1579f66bc16802d9c482eb23561fa1a86e1288cb65902b4619005a04a87
    • Vector 1 digest: 0xfd2339fda45c2e7e30f8d5dbcc062f82af12757ad80175cbdd6972627fb3c54c
    • Vector 2 digest: 0xc21c630f71383acd4d8f5465a13264f9e376dfb323acfe97d5202bc9a5baa221
    • Vector 3 digest: 0xebd98b504cfdbe392ddf9813148e2f7808bb6f7ef85c376315fe0446c2ffc9ee
    • Vector 4 r: 0x9d22c9909f6ebbcadc4ec85467c487e3d29afa8409f058371894af17f176db4c
  • Added test_requester_changes_hash regression: rebinding to a different requester produces a different struct hash.

Domain separator (0x14a60a86c57fe72bdcbdc59af9a05606ca542a7ed2eeb732756b210d3306f149) is unchanged.

3. crates/pricing-engine/src/signer.rs — thread requester end-to-end

This is the live-outage fix. build_abi_quote_details no longer hardcodes Address::ZERO; it now takes a requester: Address parameter, which propagates through SignableQuote::new and SignableQuote::with_confidentiality. proto_to_native_job_quote parses + validates the proto bytes (length 20, non-zero) before delegating to blueprint_tangle_extra::job_quote.

4. crates/pricing-engine/proto/pricing.proto — schema additions

Added bytes requester to:

  • GetPriceRequest (field 9)
  • GetJobPriceRequest (field 6)
  • QuoteDetails (field 8)
  • JobQuoteDetails (field 7)

5. crates/pricing-engine/src/service/rpc/server.rs — gRPC validation

New parse_requester helper rejects empty / wrong-length / zero-address inputs at the gRPC boundary with Status::invalid_argument. The validated Address is forwarded into the signer and echoed into the proto response so clients can verify the bound address. Both get_price and get_job_price are wired up.

6. crates/pricing-engine/tests/evm_listener.rs — integration fixtures

  • 5 GetPriceRequest / 4 GetJobPriceRequest callers updated to send the buyer's address (SERVICE_OWNER_ADDRESS, anvil account #0).
  • 3 inline ITangleServicesTypes::QuoteDetails revert-path fixtures updated with a non-zero REJECTION_PATH_REQUESTER = 0xbEEF so the intended rejection (invalid sig / expired / mismatched blueprint) fires before the new wildcard-quote rejection.
  • convert_to_onchain_quote helper takes a requester: Address parameter, threaded into the on-chain QuoteDetails.

After this commit, the e2e flow signs a quote with requester = SERVICE_OWNER_ADDRESS and submits the on-chain transaction from the same address, round-tripping the v0.13.0 verifier.

7. crates/pricing-engine/README.md

Updated typehash strings + flow diagram to reflect the v0.13.0 layout.

8. Vendored example BSMs (deferred)

examples/oauth-blueprint/contracts/, examples/incredible-squaring/contracts/, examples/apikey-blueprint/contracts/ still pin dependencies/tnt-core-0.10.4/ via soldeer. Bumping these picks up unrelated tnt-core changes (e.g. forceRemoveAllowsBelowMin default) — out of scope for this PR's correctness fix and best handled in a follow-up.

Process / quality

  • 7 logical commits, each tagged with the audit finding (Round 2 economic F1) and tnt-core PRs [CHECKLIST] Shell: Dynamic Protocol Loading #124 / [SPEC] Add service for IPFS pinning service #125. Conventional commits.
  • Type-safe propagation: requester: Address flows as Address, with conversion only at the proto boundary.
  • Validation at the boundary: rejected once at gRPC entry; downstream code asserts non-zero with debug_assert.
  • No .unwrap() on user input — gRPC validation returns typed Status::invalid_argument; signer-layer parsing returns PricingError::Signing.
  • Cross-repo digest pins are LOAD-BEARING. All 4 Solidity test vectors in tnt-core/test/tangle/EIP712Compatibility.t.sol are reproduced byte-for-byte by the Rust SDK.

Test plan

  • cargo check --workspace --all-targets — clean
  • cargo test -p blueprint-tangle-extra --lib --features keepers job_quote — 14 tests pass (incl. all 4 v0.13.0 cross-repo vectors + new test_requester_changes_hash)
  • cargo test -p blueprint-pricing-engine --lib — 63 unit tests pass (incl. 5 new requester validation tests)
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo test -p blueprint-pricing-engine --test evm_listener --features pricing-engine-e2e-tests — requires anvil + tnt-core artifacts; recommended to run on CI / locally before merge to confirm verifyQuoteBatch round-trip

drewstone added 8 commits May 8, 2026 15:04
…onomic F1)

tnt-core PRs #124 and #125 add `address requester` to QuoteDetails and
JobQuoteDetails, binding every signed quote to the address allowed to
redeem it on-chain. Wildcard (`address(0)`) quotes are rejected by the
v0.13.0 verifier.

Bindings v0.13.0 is not yet on crates.io, so this pins to the upstream
`main` branch in git. Flip back to `"0.13.0"` (crates.io) once the
publish lands.

This commit alone breaks `blueprint-tangle-extra` and `pricing-engine`;
the follow-up commits in this branch update the Rust types and gRPC
surface to thread `requester` end-to-end.
…conomic F1)

tnt-core v0.13.0 (PRs #124 and #125) adds `address requester` as the
first field of `JobQuoteDetails`, baking it into the EIP-712 typehash:

    JobQuoteDetails(address requester,uint64 serviceId,uint8 jobIndex,
                    uint256 price,uint64 timestamp,uint64 expiry,
                    uint8 confidentiality)

The on-chain verifier rejects `requester == address(0)` (no more wildcard
quotes) and rejects any submitter whose `msg.sender` doesn't match the
quote's requester.

This commit:

- Adds `requester: Address` as the first field of `JobQuoteDetails`.
- Updates `JOB_QUOTE_TYPEHASH_STR` and `hash_job_quote_details` to include
  `requester` directly after the typehash in the abi-encoded preimage.
- Updates the `From<SignedJobQuote>` impl for the on-chain
  `ITangleTypes::JobQuoteDetails` to forward `requester`.
- Refreshes the 4 cross-repo EIP-712 deterministic test vectors against
  `tnt-core/test/tangle/EIP712Compatibility.t.sol`:
    Vector 1 struct hash: 0x81efa1579f66bc16802d9c482eb23561fa1a86e1288cb65902b4619005a04a87
    Vector 1 digest:      0xfd2339fda45c2e7e30f8d5dbcc062f82af12757ad80175cbdd6972627fb3c54c
    Vector 2 digest:      0xc21c630f71383acd4d8f5465a13264f9e376dfb323acfe97d5202bc9a5baa221
    Vector 3 digest:      0xebd98b504cfdbe392ddf9813148e2f7808bb6f7ef85c376315fe0446c2ffc9ee
    Vector 4 r:           0x9d22c9909f6ebbcadc4ec85467c487e3d29afa8409f058371894af17f176db4c
- Adds a new `test_requester_changes_hash` regression that asserts
  rebinding to a different requester produces a different struct hash.
- Updates module-level rustdoc and the Solidity reference docstring
  to reflect the v0.13.0 layout.

Domain separator is unchanged (TangleQuote/v1).
…economic F1)

tnt-core v0.13.0 (PRs #124 and #125) binds every signed quote to a
requester address. Plumb that through the gRPC surface:

- `GetPriceRequest.requester` (field 9) — buyer address (20 bytes)
- `GetJobPriceRequest.requester` (field 6) — buyer address (20 bytes)
- `QuoteDetails.requester` (field 8) — echoed back in the response
- `JobQuoteDetails.requester` (field 7) — echoed back in the response

All four fields are documented as MUST be non-zero; the on-chain
verifier rejects wildcard (`address(0)`) quotes. Validation is enforced
at the gRPC boundary in a follow-up commit.

This commit only changes the schema; the regenerated prost types break
`signer.rs` and `server.rs`, which are fixed in the next commit.
…mic F1)

This is the live operator-tooling outage fix. Before this commit,
`build_abi_quote_details` hardcoded `requester: Address::ZERO`, so every
quote signed by the pricing engine was rejected by `verifyQuoteBatch`
on tnt-core v0.12.0+ deployments (wildcard quotes are no longer permitted).

Fix:

- gRPC entry: `parse_requester` validates the 20-byte field is non-zero
  and rejects with `Status::invalid_argument("requester required and
  must be non-zero")` otherwise. Validation lives at the boundary so
  downstream signing code can rely on a non-zero requester.
- `SignableQuote::new` and `SignableQuote::with_confidentiality` take
  `requester: Address` and pass it into `build_abi_quote_details`.
- `build_abi_quote_details` writes the validated requester into the
  on-chain `ITangleTypes::QuoteDetails`. The hardcoded `Address::ZERO`
  is gone.
- `proto_to_native_job_quote` parses + validates the proto job-quote
  `requester` bytes (20-byte length, non-zero) before delegating to
  `blueprint_tangle_extra::job_quote`.
- `get_price` and `get_job_price` echo the requester back into the
  proto response (`QuoteDetails.requester` / `JobQuoteDetails.requester`)
  so clients can verify the bound address matches what they asked for.

Tests:

- All 24 inline RPC tests in `server.rs` updated to send the new
  `requester` field via a `test_requester_bytes()` helper.
- `tests/utils.rs` populates `requester` in `create_test_quote_details`
  and exposes `test_requester_address` / `test_requester_bytes` helpers.
- `tests/signer_test.rs` updated to pass requester into
  `SignableQuote::new`.

`evm_listener` integration tests (gated behind the
`pricing-engine-e2e-tests` feature) are updated in the next commit.
tnt-core v0.13.0+ rejects `requester == address(0)` quotes on-chain
(audit Round 2 economic F1, PRs #124 and #125), and the on-chain
verifier cross-checks `msg.sender == quote.details.requester`.

Update the e2e fixtures gated behind `pricing-engine-e2e-tests`:

- Define `SERVICE_OWNER_ADDRESS` (the buyer's anvil-derived address) and
  pin all gRPC `GetPriceRequest`s + `convert_to_onchain_quote` callers
  to that requester. The buyer is the same anvil account that submits
  the on-chain transaction, so `msg.sender` matches the bound requester.
- Define `REJECTION_PATH_REQUESTER = 0xbEEF` for the 3 fixtures that
  exercise revert paths (invalid signature, expired quote, mismatched
  blueprint). Using a non-zero placeholder keeps these tests targeting
  their intended rejection reason rather than the new wildcard-quote
  rejection that would short-circuit them.
- Add a `requester: Address` parameter to the `convert_to_onchain_quote`
  helper, threaded through to the on-chain `ITangleServicesTypes::QuoteDetails`.
- Plumb `SERVICE_OWNER_ADDRESS` into the two `SignableQuote::new` callers
  that re-derive the EIP-712 digest for client-side verification.

After this commit, an unmodified e2e run signs a quote with
`requester = SERVICE_OWNER_ADDRESS` and submits the on-chain
`createServiceFromQuotes` from the same address — round-tripping the
new v0.13.0 verifier.
Refresh the inline `QUOTE_TYPEHASH` / `JOB_QUOTE_TYPEHASH` strings and
the per-job RFQ flow diagram in the pricing-engine README to reflect
the v0.13.0 on-chain layout (audit Round 2 economic F1, tnt-core PRs
#124 and #125):

- Both typehash strings now lead with `address requester`.
- The `JobQuoteDetails` typehash also includes the `uint8 confidentiality`
  field (which had been added previously but never made it into the
  README copy).
- The flow diagram shows the `requester` parameter on `GetJobPrice` and
  the `requester` field on the signed payload.
- The standalone signing example sets a non-zero requester explicitly
  and includes `confidentiality`, matching the current Rust struct.

A short callout below the flow diagram explains that wildcard
(`address(0)`) quotes are rejected by the on-chain verifier and that
the gRPC layer enforces non-zero at the boundary.
Add 5 regression tests for the new gRPC requester validation introduced
earlier in this branch:

- `test_get_job_price_rejects_zero_requester` — confirms a 20-byte
  zero-address request is rejected with InvalidArgument.
- `test_get_job_price_rejects_empty_requester` — confirms an empty
  bytes field is rejected (the proto default).
- `test_get_job_price_rejects_short_requester` — confirms a wrong-length
  buffer is rejected with the expected error message.
- `test_get_price_rejects_zero_requester` — same coverage on the
  GetPrice path.
- `test_get_job_price_echoes_requester_in_response` — confirms the
  signed response echoes back the requester so the client can verify
  the binding.

These tests pin the contract that downstream signing code can rely on
a non-zero requester (audit Round 2 economic F1; tnt-core PRs #124 / #125).
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

PR Quality Gate Summary

  • Status: fail
  • Selected class: not set
  • Required class: Class C
  • Reason: Multiple crates touched; cross-crate behavior likely.
  • Changed files: 10

Blocking issues

  • Missing required section: '## Summary'
  • Missing required section: '## Change Class'
  • Missing required section: '## Behavior Contract'
  • Missing required section: '## Risk And Scope'
  • Missing required section: '## Verification'
  • Missing required section: '## Harness Evidence'
  • Missing required section: '## Checklist'
  • Change Class section must specify 'Selected class: ...'
  • Verification section must include at least one command (inline or fenced).

@drewstone drewstone merged commit 6ce7e42 into main May 8, 2026
27 of 28 checks passed
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