-
Notifications
You must be signed in to change notification settings - Fork 7
feat: add horizon types to tap_graph #270
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 | ||
suchapalaver marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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 hidden or 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 { | ||
suchapalaver marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// 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 | ||
suchapalaver marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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 | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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