Skip to content

Commit f323ed7

Browse files
committed
feat: add horizon types to tap_graph
Signed-off-by: Gustavo Inacio <[email protected]>
1 parent 660905d commit f323ed7

File tree

8 files changed

+321
-5
lines changed

8 files changed

+321
-5
lines changed

tap_graph/Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ tap_receipt = { version = "0.1.0", path = "../tap_receipt" }
1717

1818
[dev-dependencies]
1919
rstest.workspace = true
20+
21+
22+
[features]
23+
default = []
24+
v2 = []

tap_graph/src/lib.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
//! These structs are used for communication between The Graph systems.
77
//!
88
9-
mod rav;
10-
mod receipt;
9+
mod v1;
1110

12-
pub use rav::{ReceiptAggregateVoucher, SignedRav};
13-
pub use receipt::{Receipt, SignedReceipt};
11+
#[cfg(any(test, feature = "v2"))]
12+
pub mod v2;
13+
14+
pub use v1::{Receipt, ReceiptAggregateVoucher, SignedRav, SignedReceipt};

tap_graph/src/v1.rs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright 2023-, Semiotic AI, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
mod rav;
5+
mod receipt;
6+
7+
pub use rav::{ReceiptAggregateVoucher, SignedRav};
8+
pub use receipt::{Receipt, SignedReceipt};

tap_graph/src/rav.rs tap_graph/src/v1/rav.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ use tap_receipt::{
4848
ReceiptWithState, WithValueAndTimestamp,
4949
};
5050

51-
use crate::{receipt::Receipt, SignedReceipt};
51+
use super::{Receipt, SignedReceipt};
5252

5353
/// A Rav wrapped in an Eip712SignedMessage
5454
pub type SignedRav = Eip712SignedMessage<ReceiptAggregateVoucher>;
File renamed without changes.

tap_graph/src/v2.rs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2023-, Semiotic AI, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
mod rav;
5+
mod receipt;
6+
7+
pub use rav::{ReceiptAggregateVoucher, SignedRav};
8+
pub use receipt::{Receipt, SignedReceipt};
9+
10+
pub enum TapReceipt {
11+
V1(crate::v1::Receipt),
12+
V2(crate::v2::Receipt),
13+
}

tap_graph/src/v2/rav.rs

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2023-, Semiotic AI, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! # Receipt Aggregate Voucher v2
5+
6+
use std::cmp;
7+
8+
use alloy::{
9+
primitives::{Address, Bytes},
10+
sol,
11+
};
12+
use serde::{Deserialize, Serialize};
13+
use tap_eip712_message::Eip712SignedMessage;
14+
use tap_receipt::{
15+
rav::{Aggregate, AggregationError},
16+
state::Checked,
17+
ReceiptWithState, WithValueAndTimestamp,
18+
};
19+
20+
use super::{Receipt, SignedReceipt};
21+
22+
/// EIP712 signed message for ReceiptAggregateVoucher
23+
pub type SignedRav = Eip712SignedMessage<ReceiptAggregateVoucher>;
24+
25+
sol! {
26+
/// Holds information needed for promise of payment signed with ECDSA
27+
///
28+
/// We use camelCase for field names to match the Ethereum ABI encoding
29+
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
30+
struct ReceiptAggregateVoucher {
31+
/// Unique allocation id this RAV belongs to
32+
address allocationId;
33+
// The address of the payer the RAV was issued by
34+
address payer;
35+
// The address of the data service the RAV was issued to
36+
address dataService;
37+
// The address of the service provider the RAV was issued to
38+
address serviceProvider;
39+
// The RAV timestamp, indicating the latest TAP Receipt in the RAV
40+
uint64 timestampNs;
41+
// Total amount owed to the service provider since the beginning of the
42+
// payer-service provider relationship, including all debt that is already paid for.
43+
uint128 valueAggregate;
44+
// Arbitrary metadata to extend functionality if a data service requires it
45+
bytes metadata;
46+
}
47+
}
48+
49+
impl ReceiptAggregateVoucher {
50+
/// Aggregates a batch of validated receipts with optional validated previous RAV,
51+
/// returning a new RAV if all provided items are valid or an error if not.
52+
///
53+
/// # Errors
54+
///
55+
/// Returns [`Error::AggregateOverflow`] if any receipt value causes aggregate
56+
/// value to overflow
57+
pub fn aggregate_receipts(
58+
allocation_id: Address,
59+
payer: Address,
60+
data_service: Address,
61+
service_provider: Address,
62+
receipts: &[Eip712SignedMessage<Receipt>],
63+
previous_rav: Option<Eip712SignedMessage<Self>>,
64+
) -> Result<Self, AggregationError> {
65+
//TODO(#29): When receipts in flight struct in created check that the state
66+
// of every receipt is OK with all checks complete (relies on #28)
67+
// If there is a previous RAV get initialize values from it, otherwise get default values
68+
let mut timestamp_max = 0u64;
69+
let mut value_aggregate = 0u128;
70+
71+
if let Some(prev_rav) = previous_rav {
72+
timestamp_max = prev_rav.message.timestampNs;
73+
value_aggregate = prev_rav.message.valueAggregate;
74+
}
75+
76+
for receipt in receipts {
77+
value_aggregate = value_aggregate
78+
.checked_add(receipt.message.value)
79+
.ok_or(AggregationError::AggregateOverflow)?;
80+
81+
timestamp_max = cmp::max(timestamp_max, receipt.message.timestamp_ns)
82+
}
83+
84+
Ok(Self {
85+
allocationId: allocation_id,
86+
timestampNs: timestamp_max,
87+
valueAggregate: value_aggregate,
88+
payer,
89+
dataService: data_service,
90+
serviceProvider: service_provider,
91+
metadata: Bytes::new(),
92+
})
93+
}
94+
}
95+
96+
impl Aggregate<SignedReceipt> for ReceiptAggregateVoucher {
97+
fn aggregate_receipts(
98+
receipts: &[ReceiptWithState<Checked, SignedReceipt>],
99+
previous_rav: Option<Eip712SignedMessage<Self>>,
100+
) -> Result<Self, AggregationError> {
101+
if receipts.is_empty() {
102+
return Err(AggregationError::NoValidReceiptsForRavRequest);
103+
}
104+
let allocation_id = receipts[0].signed_receipt().message.allocation_id;
105+
let payer = receipts[0].signed_receipt().message.payer;
106+
let data_service = receipts[0].signed_receipt().message.data_service;
107+
let service_provider = receipts[0].signed_receipt().message.service_provider;
108+
let receipts = receipts
109+
.iter()
110+
.map(|rx_receipt| rx_receipt.signed_receipt().clone())
111+
.collect::<Vec<_>>();
112+
ReceiptAggregateVoucher::aggregate_receipts(
113+
allocation_id,
114+
payer,
115+
data_service,
116+
service_provider,
117+
receipts.as_slice(),
118+
previous_rav,
119+
)
120+
}
121+
}
122+
123+
impl WithValueAndTimestamp for ReceiptAggregateVoucher {
124+
fn value(&self) -> u128 {
125+
self.valueAggregate
126+
}
127+
128+
fn timestamp_ns(&self) -> u64 {
129+
self.timestampNs
130+
}
131+
}

tap_graph/src/v2/receipt.rs

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2023-, Semiotic AI, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Receipt v2
5+
6+
use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
7+
8+
use alloy::{primitives::Address, sol};
9+
use rand::{thread_rng, Rng};
10+
use serde::{Deserialize, Serialize};
11+
use tap_eip712_message::Eip712SignedMessage;
12+
use tap_receipt::WithValueAndTimestamp;
13+
14+
/// A signed receipt message
15+
pub type SignedReceipt = Eip712SignedMessage<Receipt>;
16+
17+
sol! {
18+
/// Holds information needed for promise of payment signed with ECDSA
19+
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
20+
struct Receipt {
21+
/// Unique allocation id this receipt belongs to
22+
address allocation_id;
23+
24+
// The address of the payer the RAV was issued by
25+
address payer;
26+
// The address of the data service the RAV was issued to
27+
address data_service;
28+
// The address of the service provider the RAV was issued to
29+
address service_provider;
30+
31+
/// Unix Epoch timestamp in nanoseconds (Truncated to 64-bits)
32+
uint64 timestamp_ns;
33+
/// Random value used to avoid collisions from multiple receipts with one timestamp
34+
uint64 nonce;
35+
/// GRT value for transaction (truncate to lower bits)
36+
uint128 value;
37+
}
38+
}
39+
40+
fn get_current_timestamp_u64_ns() -> Result<u64, SystemTimeError> {
41+
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() as u64)
42+
}
43+
impl Receipt {
44+
/// Returns a receipt with provided values
45+
pub fn new(
46+
allocation_id: Address,
47+
payer: Address,
48+
data_service: Address,
49+
service_provider: Address,
50+
value: u128,
51+
) -> Result<Self, SystemTimeError> {
52+
let timestamp_ns = get_current_timestamp_u64_ns()?;
53+
let nonce = thread_rng().gen::<u64>();
54+
Ok(Self {
55+
allocation_id,
56+
payer,
57+
data_service,
58+
service_provider,
59+
timestamp_ns,
60+
nonce,
61+
value,
62+
})
63+
}
64+
}
65+
66+
impl WithValueAndTimestamp for Receipt {
67+
fn value(&self) -> u128 {
68+
self.value
69+
}
70+
71+
fn timestamp_ns(&self) -> u64 {
72+
self.timestamp_ns
73+
}
74+
}
75+
76+
#[cfg(test)]
77+
mod receipt_unit_test {
78+
use std::time::{SystemTime, UNIX_EPOCH};
79+
80+
use alloy::primitives::address;
81+
use rstest::*;
82+
83+
use super::*;
84+
85+
#[fixture]
86+
fn allocation_id() -> Address {
87+
address!("1234567890abcdef1234567890abcdef12345678")
88+
}
89+
90+
#[fixture]
91+
fn payer() -> Address {
92+
address!("abababababababababababababababababababab")
93+
}
94+
95+
#[fixture]
96+
fn data_service() -> Address {
97+
address!("deaddeaddeaddeaddeaddeaddeaddeaddeaddead")
98+
}
99+
100+
#[fixture]
101+
fn service_provider() -> Address {
102+
address!("beefbeefbeefbeefbeefbeefbeefbeefbeefbeef")
103+
}
104+
105+
#[fixture]
106+
fn value() -> u128 {
107+
1234
108+
}
109+
110+
#[fixture]
111+
fn receipt(
112+
allocation_id: Address,
113+
payer: Address,
114+
data_service: Address,
115+
service_provider: Address,
116+
value: u128,
117+
) -> Receipt {
118+
Receipt::new(allocation_id, payer, data_service, service_provider, value).unwrap()
119+
}
120+
121+
#[rstest]
122+
fn test_new_receipt(allocation_id: Address, value: u128, receipt: Receipt) {
123+
assert_eq!(receipt.allocation_id, allocation_id);
124+
assert_eq!(receipt.value, value);
125+
126+
// Check that the timestamp is within a reasonable range
127+
let now = SystemTime::now()
128+
.duration_since(UNIX_EPOCH)
129+
.expect("Current system time should be greater than `UNIX_EPOCH`")
130+
.as_nanos() as u64;
131+
assert!(receipt.timestamp_ns <= now);
132+
assert!(receipt.timestamp_ns >= now - 5000000); // 5 second tolerance
133+
}
134+
135+
#[rstest]
136+
fn test_unique_nonce_and_timestamp(
137+
#[from(receipt)] receipt1: Receipt,
138+
#[from(receipt)] receipt2: Receipt,
139+
) {
140+
let now = SystemTime::now()
141+
.duration_since(UNIX_EPOCH)
142+
.expect("Current system time should be greater than `UNIX_EPOCH`")
143+
.as_nanos() as u64;
144+
145+
// Check that nonces are different
146+
// Note: This test has an *extremely low* (~1/2^64) probability of false failure, if a failure happens
147+
// once it is not neccessarily a sign of an issue. If this test fails more than once, especially
148+
// in a short period of time (within a ) then there may be an issue with randomness
149+
// of the nonce generation.
150+
assert_ne!(receipt1.nonce, receipt2.nonce);
151+
152+
assert!(receipt1.timestamp_ns <= now);
153+
assert!(receipt1.timestamp_ns >= now - 5000000); // 5 second tolerance
154+
155+
assert!(receipt2.timestamp_ns <= now);
156+
assert!(receipt2.timestamp_ns >= now - 5000000); // 5 second tolerance
157+
}
158+
}

0 commit comments

Comments
 (0)