diff --git a/tap_graph/Cargo.toml b/tap_graph/Cargo.toml index 93f7149..a7dde80 100644 --- a/tap_graph/Cargo.toml +++ b/tap_graph/Cargo.toml @@ -17,3 +17,8 @@ tap_receipt = { version = "0.1.0", path = "../tap_receipt" } [dev-dependencies] rstest.workspace = true + + +[features] +default = [] +v2 = [] diff --git a/tap_graph/src/lib.rs b/tap_graph/src/lib.rs index 8a3668c..376de42 100644 --- a/tap_graph/src/lib.rs +++ b/tap_graph/src/lib.rs @@ -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}; diff --git a/tap_graph/src/v1.rs b/tap_graph/src/v1.rs new file mode 100644 index 0000000..cd4ac36 --- /dev/null +++ b/tap_graph/src/v1.rs @@ -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}; diff --git a/tap_graph/src/rav.rs b/tap_graph/src/v1/rav.rs similarity index 99% rename from tap_graph/src/rav.rs rename to tap_graph/src/v1/rav.rs index 6d08b22..a42768e 100644 --- a/tap_graph/src/rav.rs +++ b/tap_graph/src/v1/rav.rs @@ -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; diff --git a/tap_graph/src/receipt.rs b/tap_graph/src/v1/receipt.rs similarity index 100% rename from tap_graph/src/receipt.rs rename to tap_graph/src/v1/receipt.rs diff --git a/tap_graph/src/v2.rs b/tap_graph/src/v2.rs new file mode 100644 index 0000000..cd4ac36 --- /dev/null +++ b/tap_graph/src/v2.rs @@ -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}; diff --git a/tap_graph/src/v2/rav.rs b/tap_graph/src/v2/rav.rs new file mode 100644 index 0000000..9c33d06 --- /dev/null +++ b/tap_graph/src/v2/rav.rs @@ -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; + +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; + } +} + +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], + previous_rav: Option>, + ) -> Result { + //TODO(#29): When receipts in flight struct in created check that the state + // 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 for ReceiptAggregateVoucher { + fn aggregate_receipts( + receipts: &[ReceiptWithState], + previous_rav: Option>, + ) -> Result { + 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::>(); + 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 + } +} diff --git a/tap_graph/src/v2/receipt.rs b/tap_graph/src/v2/receipt.rs new file mode 100644 index 0000000..e42f085 --- /dev/null +++ b/tap_graph/src/v2/receipt.rs @@ -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; + +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 { + Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() as u64) +} +impl Receipt { + /// Returns a receipt with provided values + pub fn new( + allocation_id: Address, + payer: Address, + data_service: Address, + service_provider: Address, + value: u128, + ) -> Result { + let timestamp_ns = get_current_timestamp_u64_ns()?; + let nonce = thread_rng().gen::(); + 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 + // 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 + } +}