Skip to content

Commit 6ce7e42

Browse files
authored
feat(tnt-core-v0.13.0): bind quotes to requester, end live operator-tooling outage (#1406)
* chore(deps): bump tnt-core-bindings to v0.13.0 (git, audit Round 2 economic 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. * fix(tangle-extra): bind JobQuoteDetails to requester (audit Round 2 economic 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). * feat(pricing-engine): add `requester` to proto schema (audit Round 2 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. * fix(pricing-engine): thread requester end-to-end (audit Round 2 economic 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. * fix(pricing-engine): bind requester in evm_listener integration fixtures 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. * docs(pricing-engine): update typehash strings + flow diagram for v0.13.0 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. * test(pricing-engine): pin requester gRPC validation behavior 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). * style(pricing-engine): cargo fmt evm_listener fixtures
1 parent 1ef2235 commit 6ce7e42

10 files changed

Lines changed: 466 additions & 53 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,11 @@ broken_intra_doc_links = "deny"
126126
[workspace.dependencies]
127127
# SDKs (overarching crates that include all other crates)
128128
blueprint-sdk = { version = "0.2.0-alpha.4", path = "./crates/sdk", default-features = false }
129-
tnt-core-bindings = "0.11.3"
129+
# tnt-core v0.13.0 binds quotes to the requester address (audit Round 2 economic F1).
130+
# Upstream tnt-core PRs #124 and #125 land the change. Bindings 0.13.0 is not yet on
131+
# crates.io, so we pin to the `main` branch in git until the publish lands. Flip this
132+
# back to a versioned crates.io dep (`"0.13.0"`) once published.
133+
tnt-core-bindings = { git = "https://github.com/tangle-network/tnt-core", branch = "main" }
130134

131135
# Job system
132136
blueprint-core = { version = "0.2.0-alpha.3", path = "crates/core", default-features = false }

crates/pricing-engine/README.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ Price is computed from the operator's resource pricing config (`default_pricing.
2121
Used with `submitJobFromQuote()` on the Tangle contract. The operator quotes a specific price for a single job execution.
2222

2323
```
24-
Consumer → GetJobPrice(service_id, job_index) → Operator
25-
Operator → signs JobQuoteDetails{serviceId, jobIndex, price, timestamp, expiry}
24+
Consumer → GetJobPrice(service_id, job_index, requester) → Operator
25+
Operator → signs JobQuoteDetails{requester, serviceId, jobIndex, price, timestamp, expiry, confidentiality}
2626
Consumer → submitJobFromQuote(serviceId, jobIndex, inputs, [signedQuotes])
2727
```
2828

29+
Since tnt-core v0.13.0, every quote is bound to a specific `requester`
30+
address (audit Round 2 economic F1, PRs #124 and #125). The on-chain
31+
verifier rejects `requester == address(0)` and rejects any submitter
32+
whose `msg.sender` doesn't match. The gRPC layer enforces non-zero
33+
`requester` at the boundary.
34+
2935
Price is looked up from the operator's per-job pricing config: a `(service_id, job_index) → price_in_wei` map.
3036

3137
## Pricing Configuration
@@ -154,28 +160,34 @@ chainId: <chain_id>
154160
verifyingContract: <tangle_proxy_address>
155161
```
156162

157-
**Service quotes** use `QUOTE_TYPEHASH`:
163+
**Service quotes** use `QUOTE_TYPEHASH` (tnt-core v0.13.0+):
158164
```
159-
QuoteDetails(uint64 blueprintId,uint64 ttlBlocks,uint256 totalCost,uint64 timestamp,uint64 expiry,AssetSecurityCommitment[] securityCommitments,ResourceCommitment[] resourceCommitments)
165+
QuoteDetails(address requester,uint64 blueprintId,uint64 ttlBlocks,uint256 totalCost,uint64 timestamp,uint64 expiry,uint8 confidentiality,AssetSecurityCommitment[] securityCommitments,ResourceCommitment[] resourceCommitments)
160166
```
161167

162-
**Job quotes** use `JOB_QUOTE_TYPEHASH`:
168+
**Job quotes** use `JOB_QUOTE_TYPEHASH` (tnt-core v0.13.0+):
163169
```
164-
JobQuoteDetails(uint64 serviceId,uint8 jobIndex,uint256 price,uint64 timestamp,uint64 expiry)
170+
JobQuoteDetails(address requester,uint64 serviceId,uint8 jobIndex,uint256 price,uint64 timestamp,uint64 expiry,uint8 confidentiality)
165171
```
166172

173+
`requester` MUST be non-zero — wildcard quotes are rejected by the
174+
on-chain verifier. The address is bound into the EIP-712 signature so a
175+
quote can only be redeemed by the buyer it was issued to.
176+
167177
For standalone signing without the full pricing engine, use `JobQuoteSigner` from `blueprint-tangle-extra`:
168178

169179
```rust
170180
use blueprint_tangle_extra::job_quote::{JobQuoteSigner, JobQuoteDetails, QuoteSigningDomain};
171181

172182
let signer = JobQuoteSigner::new(keypair, QuoteSigningDomain { chain_id, verifying_contract });
173183
let signed = signer.sign(&JobQuoteDetails {
184+
requester: buyer_address, // MUST be non-zero
174185
service_id: 1,
175186
job_index: 7,
176187
price: U256::from(250_000_000_000_000_000u64),
177188
timestamp: now,
178189
expiry: now + 3600,
190+
confidentiality: 0,
179191
});
180192
```
181193

crates/pricing-engine/proto/pricing.proto

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ message GetPriceRequest {
7777
PricingModelHint pricing_model = 7;
7878
// Whether the customer requires TEE-attested service execution.
7979
bool require_tee = 8;
80+
// Requester address (20 bytes). Must be non-zero — wildcard quotes are
81+
// rejected by the on-chain verifier in tnt-core v0.13.0+. Bound into the
82+
// EIP-712 signature so a quote can only be redeemed by this address.
83+
bytes requester = 9;
8084
}
8185

8286
// Resource requirement for a specific resource type
@@ -119,6 +123,10 @@ message QuoteDetails {
119123
repeated ResourcePricing resources = 6;
120124
// Security commitments for assets
121125
repeated AssetSecurityCommitment security_commitments = 7;
126+
// Requester address (20 bytes). Must be non-zero — wildcard quotes are
127+
// rejected by the on-chain verifier in tnt-core v0.13.0+. Bound into the
128+
// EIP-712 signature so a quote can only be redeemed by this address.
129+
bytes requester = 8;
122130
}
123131

124132
// Pricing for a specific resource type
@@ -147,6 +155,10 @@ message GetJobPriceRequest {
147155
uint64 challenge_timestamp = 4;
148156
// Whether the customer requires TEE-attested service execution.
149157
bool require_tee = 5;
158+
// Requester address (20 bytes). Must be non-zero — wildcard quotes are
159+
// rejected by the on-chain verifier in tnt-core v0.13.0+. Bound into the
160+
// EIP-712 signature so a quote can only be redeemed by this address.
161+
bytes requester = 6;
150162
}
151163

152164
// Response message for GetJobPrice RPC
@@ -189,7 +201,7 @@ message SettlementOption {
189201
string scheme = 6;
190202
}
191203

192-
// Per-job quote details matching tnt-core Types.JobQuoteDetails
204+
// Per-job quote details matching tnt-core Types.JobQuoteDetails (v0.13.0+)
193205
message JobQuoteDetails {
194206
// The service instance ID
195207
uint64 service_id = 1;
@@ -205,4 +217,8 @@ message JobQuoteDetails {
205217
// Bound into the EIP-712 signature to prevent replay of
206218
// a non-TEE quote for a TEE-required service.
207219
uint32 confidentiality = 6;
220+
// Requester address (20 bytes). Must be non-zero — wildcard quotes are
221+
// rejected by the on-chain verifier in tnt-core v0.13.0+. Bound into the
222+
// EIP-712 signature so a quote can only be redeemed by this address.
223+
bytes requester = 7;
208224
}

0 commit comments

Comments
 (0)