feat(tnt-core-v0.13.0): bind quotes to requester, end live operator-tooling outage#1406
Merged
Merged
Conversation
…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).
PR Quality Gate Summary
Blocking issues
|
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.
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 incrates/pricing-engine/src/signer.rs::build_abi_quote_details), and the on-chainverifyQuoteBatchrejects wildcard quotes — making every operator-issued quote unredeemable.This PR threads a non-zero
requesterend-to-end (proto → gRPC → signer → EIP-712 digest → on-chain submission), restoring correctness against tnt-coremain(PRs tnt-core#124 and tnt-core#125, audit Round 2 economic finding F1).Migration spec
1. Cargo dep bump
Cargo.toml:129flipped fromtnt-core-bindings = "0.11.3"to a git dep on tnt-core'smain. 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 migrationrequester: Addressas the first field ofJobQuoteDetails.JOB_QUOTE_TYPEHASH_STRto"JobQuoteDetails(address requester,uint64 serviceId,uint8 jobIndex,uint256 price,uint64 timestamp,uint64 expiry,uint8 confidentiality)".hash_job_quote_detailsto abi-encoderequesterimmediately after the typehash.From<SignedJobQuote>→ITangleTypes::SignedJobQuoteimpl.tnt-core/test/tangle/EIP712Compatibility.t.sol:0x81efa1579f66bc16802d9c482eb23561fa1a86e1288cb65902b4619005a04a870xfd2339fda45c2e7e30f8d5dbcc062f82af12757ad80175cbdd6972627fb3c54c0xc21c630f71383acd4d8f5465a13264f9e376dfb323acfe97d5202bc9a5baa2210xebd98b504cfdbe392ddf9813148e2f7808bb6f7ef85c376315fe0446c2ffc9ee0x9d22c9909f6ebbcadc4ec85467c487e3d29afa8409f058371894af17f176db4ctest_requester_changes_hashregression: 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-endThis is the live-outage fix.
build_abi_quote_detailsno longer hardcodesAddress::ZERO; it now takes arequester: Addressparameter, which propagates throughSignableQuote::newandSignableQuote::with_confidentiality.proto_to_native_job_quoteparses + validates the proto bytes (length 20, non-zero) before delegating toblueprint_tangle_extra::job_quote.4.
crates/pricing-engine/proto/pricing.proto— schema additionsAdded
bytes requesterto:GetPriceRequest(field 9)GetJobPriceRequest(field 6)QuoteDetails(field 8)JobQuoteDetails(field 7)5.
crates/pricing-engine/src/service/rpc/server.rs— gRPC validationNew
parse_requesterhelper rejects empty / wrong-length / zero-address inputs at the gRPC boundary withStatus::invalid_argument. The validatedAddressis forwarded into the signer and echoed into the proto response so clients can verify the bound address. Bothget_priceandget_job_priceare wired up.6.
crates/pricing-engine/tests/evm_listener.rs— integration fixturesGetPriceRequest/ 4GetJobPriceRequestcallers updated to send the buyer's address (SERVICE_OWNER_ADDRESS, anvil account #0).ITangleServicesTypes::QuoteDetailsrevert-path fixtures updated with a non-zeroREJECTION_PATH_REQUESTER = 0xbEEFso the intended rejection (invalid sig / expired / mismatched blueprint) fires before the new wildcard-quote rejection.convert_to_onchain_quotehelper takes arequester: Addressparameter, threaded into the on-chainQuoteDetails.After this commit, the e2e flow signs a quote with
requester = SERVICE_OWNER_ADDRESSand submits the on-chain transaction from the same address, round-tripping the v0.13.0 verifier.7.
crates/pricing-engine/README.mdUpdated 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 pindependencies/tnt-core-0.10.4/via soldeer. Bumping these picks up unrelated tnt-core changes (e.g.forceRemoveAllowsBelowMindefault) — out of scope for this PR's correctness fix and best handled in a follow-up.Process / quality
requester: Addressflows asAddress, with conversion only at the proto boundary.debug_assert..unwrap()on user input — gRPC validation returns typedStatus::invalid_argument; signer-layer parsing returnsPricingError::Signing.tnt-core/test/tangle/EIP712Compatibility.t.solare reproduced byte-for-byte by the Rust SDK.Test plan
cargo check --workspace --all-targets— cleancargo test -p blueprint-tangle-extra --lib --features keepers job_quote— 14 tests pass (incl. all 4 v0.13.0 cross-repo vectors + newtest_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— cleancargo 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 confirmverifyQuoteBatchround-trip