Skip to content

Commit 9a8015a

Browse files
authored
Merge pull request #316 from semiotic-ai/suchapalaver/feat/v2-endpoints
feat(tap-aggregator): add v2 endpoint
2 parents 5d6f4da + 85a0dd4 commit 9a8015a

File tree

7 files changed

+429
-9
lines changed

7 files changed

+429
-9
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ tap_eip712_message = { version = "0.2.1", path = "tap_eip712_message" }
5050
tap_core = { version = "4.1.2", path = "tap_core" }
5151
tap_graph = { version = "0.3.2", path = "tap_graph", features = ["v2"] }
5252
tap_receipt = { version = "1.1.2", path = "tap_receipt" }
53-
thegraph-core = "0.15.0"
53+
thegraph-core = "0.15.1"
5454
thiserror = "2.0.12"
5555
tokio = { version = "1.44.2", features = ["macros", "signal"] }
5656
tonic = { version = "0.13.0", features = ["transport", "zstd"] }

tap_aggregator/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ repository.workspace = true
77
readme = "README.md"
88
description = "A JSON-RPC service for the Timeline Aggregation Protocol that lets clients request an aggregate receipt from a list of individual receipts."
99

10+
[features]
11+
default = ["v2"]
12+
v2 = ["tap_graph/v2"]
13+
1014
[[bin]]
1115
name = "tap_aggregator"
1216
path = "src/main.rs"

tap_aggregator/README.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
A stateless JSON-RPC service that lets clients request an aggregate receipt from a list of individual receipts.
44

5+
Supports both V1 (allocation-based) and V2 (collection-based) aggregation protocols for seamless migration to the Horizon protocol.
6+
57
TAP Aggregator is run by [gateway](https://github.com/edgeandnode/gateway/blob/main/README.md)
68
operators.
79

@@ -189,6 +191,11 @@ In addition to the official spec, we define a few special errors:
189191

190192
### Methods
191193

194+
This aggregator supports both V1 (legacy) and V2 (Horizon) protocols:
195+
196+
- **V1 Endpoints**: `aggregate_receipts` - allocation-based aggregation
197+
- **V2 Endpoints**: `aggregate_receipts_v2` - collection-based aggregation with enhanced fields
198+
192199
#### `api_versions()`
193200

194201
[source](server::RpcServer::api_versions)
@@ -318,3 +325,170 @@ Example:
318325
}
319326
}
320327
```
328+
329+
#### `aggregate_receipts_v2(api_version, receipts, previous_rav)`
330+
331+
Aggregates the given V2 receipts into a V2 receipt aggregate voucher using the Horizon protocol.
332+
This method supports collection-based aggregation with enhanced fields for payer, data service, and service provider tracking.
333+
334+
**V2 Receipt Structure:**
335+
336+
- `collection_id`: 32-byte identifier for the collection (replaces `allocation_id`)
337+
- `payer`: Address of the payer
338+
- `data_service`: Address of the data service
339+
- `service_provider`: Address of the service provider
340+
- `timestamp_ns`: Timestamp in nanoseconds
341+
- `nonce`: Unique nonce
342+
- `value`: Receipt value
343+
344+
**V2 RAV Structure:**
345+
346+
- `collectionId`: Collection identifier (replaces `allocation_id`)
347+
- `payer`: Payer address
348+
- `dataService`: Data service address
349+
- `serviceProvider`: Service provider address
350+
- `timestampNs`: Latest timestamp
351+
- `valueAggregate`: Total aggregated value
352+
- `metadata`: Additional metadata (bytes)
353+
354+
Example:
355+
356+
*Request*:
357+
358+
```json
359+
{
360+
"jsonrpc": "2.0",
361+
"id": 0,
362+
"method": "aggregate_receipts_v2",
363+
"params": [
364+
"1.0.0",
365+
[
366+
{
367+
"message": {
368+
"collection_id": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
369+
"payer": "0xabababababababababababababababababababab",
370+
"data_service": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
371+
"service_provider": "0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef",
372+
"timestamp_ns": 1685670449225087255,
373+
"nonce": 11835827017881841442,
374+
"value": 34
375+
},
376+
"signature": {
377+
"r": "0xa9fa1acf3cc3be503612f75602e68cc22286592db1f4f944c78397cbe529353b",
378+
"s": "0x566cfeb7e80a393021a443d5846c0734d25bcf54ed90d97effe93b1c8aef0911",
379+
"v": 27
380+
}
381+
},
382+
{
383+
"message": {
384+
"collection_id": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
385+
"payer": "0xabababababababababababababababababababab",
386+
"data_service": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
387+
"service_provider": "0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef",
388+
"timestamp_ns": 1685670449225830106,
389+
"nonce": 17711980309995246801,
390+
"value": 23
391+
},
392+
"signature": {
393+
"r": "0x51ca5a2b839558654326d3a3f544a97d94effb9a7dd9cac7492007bc974e91f0",
394+
"s": "0x3d9d398ea6b0dd9fac97726f51c0840b8b314821fb4534cb40383850c431fd9e",
395+
"v": 28
396+
}
397+
}
398+
],
399+
{
400+
"message": {
401+
"collectionId": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
402+
"payer": "0xabababababababababababababababababababab",
403+
"dataService": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
404+
"serviceProvider": "0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef",
405+
"timestampNs": 1685670449224324338,
406+
"valueAggregate": 101,
407+
"metadata": "0x"
408+
},
409+
"signature": {
410+
"r": "0x601a1f399cf6223d1414a89b7bbc90ee13eeeec006bd59e0c96042266c6ad7dc",
411+
"s": "0x3172e795bd190865afac82e3a8be5f4ccd4b65958529986c779833625875f0b2",
412+
"v": 28
413+
}
414+
}
415+
]
416+
}
417+
```
418+
419+
*Response*:
420+
421+
```json
422+
{
423+
"id": 0,
424+
"jsonrpc": "2.0",
425+
"result": {
426+
"data": {
427+
"message": {
428+
"collectionId": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
429+
"payer": "0xabababababababababababababababababababab",
430+
"dataService": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
431+
"serviceProvider": "0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef",
432+
"timestampNs": 1685670449225830106,
433+
"valueAggregate": 158,
434+
"metadata": "0x"
435+
},
436+
"signature": {
437+
"r": "0x60eb38374119bbabf1ac6960f532124ba2a9c5990d9fb50875b512e611847eb5",
438+
"s": "0x1b9a330cc9e2ecbda340a4757afaee8f55b6dbf278428f8cf49dd5ad8438f83d",
439+
"v": 27
440+
}
441+
}
442+
}
443+
}
444+
```
445+
446+
## Feature Flags
447+
448+
### V2 Protocol Support
449+
450+
The aggregator supports both V1 and V2 protocols:
451+
452+
- **V2 is enabled by default** via the `v2` feature flag in `tap_aggregator/Cargo.toml`
453+
- To disable V2: `cargo build --no-default-features`
454+
- Both protocols can run simultaneously for gradual migration
455+
- V1 endpoints remain unchanged and fully functional
456+
457+
### Feature Flag Usage
458+
459+
```bash
460+
# Build with V2 support (default)
461+
cargo build --release
462+
463+
# Build without V2 support (V1 only)
464+
cargo build --release --no-default-features
465+
466+
# Explicitly enable V2 feature
467+
cargo build --release --features v2
468+
```
469+
470+
## Migration Guide
471+
472+
### V1 to V2 Migration
473+
474+
The V2 protocol introduces collection-based aggregation with enhanced tracking. Here's how to migrate:
475+
476+
#### Key Changes
477+
478+
1. **Receipt Structure**: `allocation_id``collection_id` + additional fields
479+
2. **RAV Structure**: `allocation_id``collectionId` + additional fields
480+
3. **New Fields**: `payer`, `data_service`/`dataService`, `service_provider`/`serviceProvider`
481+
4. **Metadata**: V2 RAVs include optional `metadata` field
482+
483+
#### Migration Steps
484+
485+
1. **Phase 1**: Deploy aggregator with V2 support (both endpoints available)
486+
2. **Phase 2**: Update clients to use V2 receipt structure
487+
3. **Phase 3**: Switch clients to `aggregate_receipts_v2` endpoint
488+
4. **Phase 4**: V1 endpoints remain for backward compatibility
489+
490+
#### Backward Compatibility
491+
492+
- **V1 endpoints**: Unchanged and fully functional
493+
- **gRPC support**: Both V1 and V2 with automatic conversion
494+
- **No breaking changes**: Existing V1 clients continue to work

tap_aggregator/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ pub mod error_codes;
77
pub mod grpc;
88
pub mod jsonrpsee_helpers;
99
pub mod metrics;
10+
pub mod protocol_mode;
11+
pub mod receipt_classifier;
1012
pub mod server;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2024 The Graph Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#[derive(Clone, Copy, Debug, PartialEq)]
16+
pub enum ProtocolMode {
17+
/// Pre-horizon: V1 receipts accepted, V1 aggregation used
18+
Legacy,
19+
/// Post-horizon: V2 for new receipts, V1 only for legacy receipt aggregation
20+
Horizon,
21+
}
22+
23+
#[derive(Clone, Copy, Debug, PartialEq)]
24+
pub enum ReceiptType {
25+
/// V1 receipt created before horizon activation (legacy, still needs aggregation)
26+
LegacyV1,
27+
/// V2 receipt (collection-based, created after horizon activation)
28+
V2,
29+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2024 The Graph Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use anyhow::Result;
16+
17+
use crate::protocol_mode::ProtocolMode;
18+
19+
/// Validate that a batch of v1 receipts is valid for legacy processing
20+
pub fn validate_v1_receipt_batch<T>(receipts: &[T]) -> Result<ProtocolMode> {
21+
if receipts.is_empty() {
22+
return Err(anyhow::anyhow!("Cannot aggregate empty receipt batch"));
23+
}
24+
// All v1 receipts use legacy mode
25+
Ok(ProtocolMode::Legacy)
26+
}
27+
28+
/// Validate that a batch of v2 receipts is valid for horizon processing
29+
#[cfg(feature = "v2")]
30+
pub fn validate_v2_receipt_batch<T>(receipts: &[T]) -> Result<ProtocolMode> {
31+
if receipts.is_empty() {
32+
return Err(anyhow::anyhow!("Cannot aggregate empty receipt batch"));
33+
}
34+
// All v2 receipts use horizon mode
35+
Ok(ProtocolMode::Horizon)
36+
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use tap_graph::Receipt;
41+
42+
use super::*;
43+
44+
#[test]
45+
fn test_validate_v1_batch() {
46+
use thegraph_core::alloy::primitives::Address;
47+
let receipt = Receipt::new(Address::ZERO, 100).unwrap();
48+
let receipts = vec![receipt];
49+
assert_eq!(
50+
validate_v1_receipt_batch(&receipts).unwrap(),
51+
ProtocolMode::Legacy
52+
);
53+
}
54+
55+
#[test]
56+
fn test_validate_v1_empty_batch_fails() {
57+
let receipts: Vec<Receipt> = vec![];
58+
assert!(validate_v1_receipt_batch(&receipts).is_err());
59+
}
60+
61+
#[cfg(feature = "v2")]
62+
#[test]
63+
fn test_validate_v2_batch() {
64+
use tap_graph::v2;
65+
use thegraph_core::alloy::primitives::{Address, FixedBytes};
66+
let receipt = v2::Receipt::new(
67+
FixedBytes::ZERO,
68+
Address::ZERO,
69+
Address::ZERO,
70+
Address::ZERO,
71+
100,
72+
)
73+
.unwrap();
74+
let receipts = vec![receipt];
75+
assert_eq!(
76+
validate_v2_receipt_batch(&receipts).unwrap(),
77+
ProtocolMode::Horizon
78+
);
79+
}
80+
81+
#[cfg(feature = "v2")]
82+
#[test]
83+
fn test_validate_v2_empty_batch_fails() {
84+
use tap_graph::v2;
85+
let receipts: Vec<v2::Receipt> = vec![];
86+
assert!(validate_v2_receipt_batch(&receipts).is_err());
87+
}
88+
}

0 commit comments

Comments
 (0)