-
Notifications
You must be signed in to change notification settings - Fork 5
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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}; |
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}; |
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; | ||||||
} | ||||||
} | ||||||
|
||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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 | ||||||
} | ||||||
} |
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 { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
"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 | ||||||||
} | ||||||||
} |
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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