Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add horizon types to tap_graph #270

Merged
merged 1 commit into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions tap_graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ tap_receipt = { version = "0.1.0", path = "../tap_receipt" }

[dev-dependencies]
rstest.workspace = true


[features]
default = []
v2 = []
9 changes: 5 additions & 4 deletions tap_graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
//! These structs are used for communication between The Graph systems.
//!

mod rav;
mod receipt;
mod v1;

pub use rav::{ReceiptAggregateVoucher, SignedRav};
pub use receipt::{Receipt, SignedReceipt};
#[cfg(any(test, feature = "v2"))]
pub mod v2;

pub use v1::{Receipt, ReceiptAggregateVoucher, SignedRav, SignedReceipt};
8 changes: 8 additions & 0 deletions tap_graph/src/v1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright 2023-, Semiotic AI, Inc.
// SPDX-License-Identifier: Apache-2.0

mod rav;
mod receipt;

pub use rav::{ReceiptAggregateVoucher, SignedRav};
pub use receipt::{Receipt, SignedReceipt};
2 changes: 1 addition & 1 deletion tap_graph/src/rav.rs → tap_graph/src/v1/rav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use tap_receipt::{
ReceiptWithState, WithValueAndTimestamp,
};

use crate::{receipt::Receipt, SignedReceipt};
use super::{Receipt, SignedReceipt};

/// A Rav wrapped in an Eip712SignedMessage
pub type SignedRav = Eip712SignedMessage<ReceiptAggregateVoucher>;
Expand Down
File renamed without changes.
8 changes: 8 additions & 0 deletions tap_graph/src/v2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright 2023-, Semiotic AI, Inc.
// SPDX-License-Identifier: Apache-2.0

mod rav;
mod receipt;

pub use rav::{ReceiptAggregateVoucher, SignedRav};
pub use receipt::{Receipt, SignedReceipt};
131 changes: 131 additions & 0 deletions tap_graph/src/v2/rav.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2023-, Semiotic AI, Inc.
// SPDX-License-Identifier: Apache-2.0

//! # Receipt Aggregate Voucher v2

use std::cmp;

use alloy::{
primitives::{Address, Bytes},
sol,
};
use serde::{Deserialize, Serialize};
use tap_eip712_message::Eip712SignedMessage;
use tap_receipt::{
rav::{Aggregate, AggregationError},
state::Checked,
ReceiptWithState, WithValueAndTimestamp,
};

use super::{Receipt, SignedReceipt};

/// EIP712 signed message for ReceiptAggregateVoucher
pub type SignedRav = Eip712SignedMessage<ReceiptAggregateVoucher>;

sol! {
/// Holds information needed for promise of payment signed with ECDSA
///
/// We use camelCase for field names to match the Ethereum ABI encoding
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
struct ReceiptAggregateVoucher {
/// Unique allocation id this RAV belongs to
address allocationId;
// The address of the payer the RAV was issued by
address payer;
// The address of the data service the RAV was issued to
address dataService;
// The address of the service provider the RAV was issued to
address serviceProvider;
// The RAV timestamp, indicating the latest TAP Receipt in the RAV
uint64 timestampNs;
// Total amount owed to the service provider since the beginning of the
// payer-service provider relationship, including all debt that is already paid for.
uint128 valueAggregate;
// Arbitrary metadata to extend functionality if a data service requires it
bytes metadata;
}
}
Comment on lines +25 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gusinacio could you just explain why this needs to be abi encoded, presumably because this is interacting with a contract. Would be great to be able to make the link here. Can also do it in a subsequent PR if you wanna explain here quickly 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definition of encodeType.
https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator
https://docs.rs/alloy/latest/alloy/sol_types/macro.sol.html

sol macro generates the encodeType for EIP712, it doesn't use serde and at the time, there was no way to "rename camelCase" like we do in serde

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like it still doesn't have that alloy-rs/core#570


impl ReceiptAggregateVoucher {
/// Aggregates a batch of validated receipts with optional validated previous RAV,
/// returning a new RAV if all provided items are valid or an error if not.
///
/// # Errors
///
/// Returns [`Error::AggregateOverflow`] if any receipt value causes aggregate
/// value to overflow
pub fn aggregate_receipts(
allocation_id: Address,
payer: Address,
data_service: Address,
service_provider: Address,
receipts: &[Eip712SignedMessage<Receipt>],
previous_rav: Option<Eip712SignedMessage<Self>>,
) -> Result<Self, AggregationError> {
//TODO(#29): When receipts in flight struct in created check that the state
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//TODO(#29): When receipts in flight struct in created check that the state
//TODO(#29): When receipts in flight struct in created check that the state

I don't understand what this is trying to say, can we make the TODO clearer, please! 🙏

// of every receipt is OK with all checks complete (relies on #28)
// If there is a previous RAV get initialize values from it, otherwise get default values
let mut timestamp_max = 0u64;
let mut value_aggregate = 0u128;

if let Some(prev_rav) = previous_rav {
timestamp_max = prev_rav.message.timestampNs;
value_aggregate = prev_rav.message.valueAggregate;
}

for receipt in receipts {
value_aggregate = value_aggregate
.checked_add(receipt.message.value)
.ok_or(AggregationError::AggregateOverflow)?;

timestamp_max = cmp::max(timestamp_max, receipt.message.timestamp_ns)
}

Ok(Self {
allocationId: allocation_id,
timestampNs: timestamp_max,
valueAggregate: value_aggregate,
payer,
dataService: data_service,
serviceProvider: service_provider,
metadata: Bytes::new(),
})
}
}

impl Aggregate<SignedReceipt> for ReceiptAggregateVoucher {
fn aggregate_receipts(
receipts: &[ReceiptWithState<Checked, SignedReceipt>],
previous_rav: Option<Eip712SignedMessage<Self>>,
) -> Result<Self, AggregationError> {
if receipts.is_empty() {
return Err(AggregationError::NoValidReceiptsForRavRequest);
}
let allocation_id = receipts[0].signed_receipt().message.allocation_id;
let payer = receipts[0].signed_receipt().message.payer;
let data_service = receipts[0].signed_receipt().message.data_service;
let service_provider = receipts[0].signed_receipt().message.service_provider;
let receipts = receipts
.iter()
.map(|rx_receipt| rx_receipt.signed_receipt().clone())
.collect::<Vec<_>>();
ReceiptAggregateVoucher::aggregate_receipts(
allocation_id,
payer,
data_service,
service_provider,
receipts.as_slice(),
previous_rav,
)
}
}

impl WithValueAndTimestamp for ReceiptAggregateVoucher {
fn value(&self) -> u128 {
self.valueAggregate
}

fn timestamp_ns(&self) -> u64 {
self.timestampNs
}
}
158 changes: 158 additions & 0 deletions tap_graph/src/v2/receipt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2023-, Semiotic AI, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Receipt v2

use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};

use alloy::{primitives::Address, sol};
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use tap_eip712_message::Eip712SignedMessage;
use tap_receipt::WithValueAndTimestamp;

/// A signed receipt message
pub type SignedReceipt = Eip712SignedMessage<Receipt>;

sol! {
/// Holds information needed for promise of payment signed with ECDSA
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
struct Receipt {
/// Unique allocation id this receipt belongs to
address allocation_id;

// The address of the payer the RAV was issued by
address payer;
// The address of the data service the RAV was issued to
address data_service;
// The address of the service provider the RAV was issued to
address service_provider;

/// Unix Epoch timestamp in nanoseconds (Truncated to 64-bits)
uint64 timestamp_ns;
/// Random value used to avoid collisions from multiple receipts with one timestamp
uint64 nonce;
/// GRT value for transaction (truncate to lower bits)
uint128 value;
}
}

fn get_current_timestamp_u64_ns() -> Result<u64, SystemTimeError> {
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() as u64)
}
impl Receipt {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
impl Receipt {
impl Receipt {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could add a clippy lint rule in a following PR so it formats automatically like you are suggesting.

/// Returns a receipt with provided values
pub fn new(
allocation_id: Address,
payer: Address,
data_service: Address,
service_provider: Address,
value: u128,
) -> Result<Self, SystemTimeError> {
let timestamp_ns = get_current_timestamp_u64_ns()?;
let nonce = thread_rng().gen::<u64>();
Ok(Self {
allocation_id,
payer,
data_service,
service_provider,
timestamp_ns,
nonce,
value,
})
}
}

impl WithValueAndTimestamp for Receipt {
fn value(&self) -> u128 {
self.value
}

fn timestamp_ns(&self) -> u64 {
self.timestamp_ns
}
}

#[cfg(test)]
mod receipt_unit_test {
use std::time::{SystemTime, UNIX_EPOCH};

use alloy::primitives::address;
use rstest::*;

use super::*;

#[fixture]
fn allocation_id() -> Address {
address!("1234567890abcdef1234567890abcdef12345678")
}

#[fixture]
fn payer() -> Address {
address!("abababababababababababababababababababab")
}

#[fixture]
fn data_service() -> Address {
address!("deaddeaddeaddeaddeaddeaddeaddeaddeaddead")
}

#[fixture]
fn service_provider() -> Address {
address!("beefbeefbeefbeefbeefbeefbeefbeefbeefbeef")
}

#[fixture]
fn value() -> u128 {
1234
}

#[fixture]
fn receipt(
allocation_id: Address,
payer: Address,
data_service: Address,
service_provider: Address,
value: u128,
) -> Receipt {
Receipt::new(allocation_id, payer, data_service, service_provider, value).unwrap()
}

#[rstest]
fn test_new_receipt(allocation_id: Address, value: u128, receipt: Receipt) {
assert_eq!(receipt.allocation_id, allocation_id);
assert_eq!(receipt.value, value);

// Check that the timestamp is within a reasonable range
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Current system time should be greater than `UNIX_EPOCH`")
.as_nanos() as u64;
assert!(receipt.timestamp_ns <= now);
assert!(receipt.timestamp_ns >= now - 5000000); // 5 second tolerance
}

#[rstest]
fn test_unique_nonce_and_timestamp(
#[from(receipt)] receipt1: Receipt,
#[from(receipt)] receipt2: Receipt,
) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Current system time should be greater than `UNIX_EPOCH`")
.as_nanos() as u64;

// Check that nonces are different
// Note: This test has an *extremely low* (~1/2^64) probability of false failure, if a failure happens
// once it is not neccessarily a sign of an issue. If this test fails more than once, especially
// in a short period of time (within a ) then there may be an issue with randomness
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// in a short period of time (within a ) then there may be an issue with randomness
// in a short period of time (within a ) then there may be an issue with randomness

"within a "...?

// of the nonce generation.
assert_ne!(receipt1.nonce, receipt2.nonce);

assert!(receipt1.timestamp_ns <= now);
assert!(receipt1.timestamp_ns >= now - 5000000); // 5 second tolerance

assert!(receipt2.timestamp_ns <= now);
assert!(receipt2.timestamp_ns >= now - 5000000); // 5 second tolerance
}
}