-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into gustavo/fix-release-ci
- Loading branch information
Showing
8 changed files
with
316 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
// 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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
/// 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 | ||
// 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 | ||
} | ||
} |