tap_core/
lib.rs

1// Copyright 2023-, Semiotic AI, Inc.
2// SPDX-License-Identifier: Apache-2.0
3#![doc = include_str!("../README.md")]
4//! ## Getting started
5//!
6//! To get started with the TAP protocol, take a look on the [`manager`] module
7//! to see how to manage the state channel and implement the needed adapters.
8
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use thegraph_core::alloy::{dyn_abi::Eip712Domain, primitives::Address, sol_types::eip712_domain};
12use thiserror::Error;
13
14mod error;
15pub mod manager;
16pub mod rav_request;
17pub mod receipt;
18pub mod signed_message;
19
20pub use error::Error;
21use error::Result;
22
23fn get_current_timestamp_u64_ns() -> Result<u64> {
24    Ok(SystemTime::now()
25        .duration_since(UNIX_EPOCH)
26        .map_err(|err| Error::InvalidSystemTime {
27            source_error_message: err.to_string(),
28        })?
29        .as_nanos() as u64)
30}
31
32/// The EIP712 domain separator builder for the TAP protocol.
33///
34/// This is the current domain separator that is used for the [EIP712](https://eips.ethereum.org/EIPS/eip-712) signature scheme.
35///
36///
37/// It's used to validate the signature of the `ReceiptAggregateVoucher` and `Receipt` structs.
38///
39/// You can take a look on deployed [TAPVerfiers](https://github.com/semiotic-ai/timeline-aggregation-protocol-contracts/blob/4dc87fc616680c924b99dbaf285bdd449c777261/src/TAPVerifier.sol)
40/// contracts [here](https://github.com/semiotic-ai/timeline-aggregation-protocol-contracts/blob/4dc87fc616680c924b99dbaf285bdd449c777261/addresses.json)
41///
42/// TAP protocol version for EIP-712 domain separator
43#[derive(Debug, Clone, Copy)]
44pub enum TapVersion {
45    V1,
46    V2,
47}
48
49impl TapVersion {
50    pub fn as_str(&self) -> &'static str {
51        match self {
52            TapVersion::V1 => "1",
53            TapVersion::V2 => "2",
54        }
55    }
56}
57
58/// The domain separator is defined as:
59/// - `name`: "TAP" for V1, "GraphTallyCollector" for V2 - This could be a fn argument but we don't want to change the function signature.
60/// - `version`: always set to "1", what changes is the domain name.
61/// - `chain_id`: The chain ID of the chain where the domain separator is deployed.
62/// - `verifying_contract`: The address of the contract that is verifying the signature.
63pub fn tap_eip712_domain(
64    chain_id: u64,
65    verifying_contract_address: Address,
66    version: TapVersion,
67) -> Eip712Domain {
68    let name = match version {
69        TapVersion::V1 => "TAP",
70        TapVersion::V2 => "GraphTallyCollector",
71    };
72
73    eip712_domain! {
74        name: name,
75        version: "1",
76        chain_id: chain_id,
77        verifying_contract: verifying_contract_address,
78    }
79}
80
81#[cfg(test)]
82mod tap_tests {
83    use std::str::FromStr;
84
85    use rstest::*;
86    use tap_graph::{Receipt, ReceiptAggregateVoucher};
87    use thegraph_core::alloy::{
88        dyn_abi::Eip712Domain, primitives::Address, signers::local::PrivateKeySigner,
89    };
90
91    use crate::{signed_message::Eip712SignedMessage, tap_eip712_domain, TapVersion};
92
93    #[fixture]
94    fn keys() -> (PrivateKeySigner, Address) {
95        let wallet = PrivateKeySigner::random();
96        let address = wallet.address();
97
98        (wallet, address)
99    }
100
101    #[fixture]
102    fn allocation_ids() -> Vec<Address> {
103        vec![
104            Address::from_str("0xabababababababababababababababababababab").unwrap(),
105            Address::from_str("0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead").unwrap(),
106            Address::from_str("0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef").unwrap(),
107            Address::from_str("0x1234567890abcdef1234567890abcdef12345678").unwrap(),
108        ]
109    }
110
111    #[fixture]
112    fn domain_separator() -> Eip712Domain {
113        tap_eip712_domain(1, Address::from([0x11u8; 20]), TapVersion::V1)
114    }
115
116    #[rstest]
117    #[case::basic_rav_test (vec![45,56,34,23])]
118    #[case::rav_from_zero_valued_receipts (vec![0,0,0,0])]
119    #[test]
120    fn signed_rav_is_valid_with_no_previous_rav(
121        keys: (PrivateKeySigner, Address),
122        allocation_ids: Vec<Address>,
123        domain_separator: Eip712Domain,
124        #[case] values: Vec<u128>,
125    ) {
126        // Create receipts
127        let mut receipts = Vec::new();
128        for value in values {
129            receipts.push(
130                Eip712SignedMessage::new(
131                    &domain_separator,
132                    Receipt::new(allocation_ids[0], value).unwrap(),
133                    &keys.0,
134                )
135                .unwrap(),
136            );
137        }
138
139        // Skipping receipts validation in this test, aggregate_receipts assumes receipts are valid.
140
141        let rav = ReceiptAggregateVoucher::aggregate_receipts(allocation_ids[0], &receipts, None)
142            .unwrap();
143        let signed_rav = Eip712SignedMessage::new(&domain_separator, rav, &keys.0).unwrap();
144        assert!(signed_rav.recover_signer(&domain_separator).unwrap() == keys.1);
145    }
146
147    #[rstest]
148    #[case::basic_rav_test(vec![45,56,34,23])]
149    #[case::rav_from_zero_valued_receipts(vec![0,0,0,0])]
150    #[test]
151    fn signed_rav_is_valid_with_previous_rav(
152        keys: (PrivateKeySigner, Address),
153        allocation_ids: Vec<Address>,
154        domain_separator: Eip712Domain,
155        #[case] values: Vec<u128>,
156    ) {
157        // Create receipts
158        let mut receipts = Vec::new();
159        for value in values {
160            receipts.push(
161                Eip712SignedMessage::new(
162                    &domain_separator,
163                    Receipt::new(allocation_ids[0], value).unwrap(),
164                    &keys.0,
165                )
166                .unwrap(),
167            );
168        }
169
170        // Create previous RAV from first half of receipts
171        let prev_rav = ReceiptAggregateVoucher::aggregate_receipts(
172            allocation_ids[0],
173            &receipts[0..receipts.len() / 2],
174            None,
175        )
176        .unwrap();
177        let signed_prev_rav =
178            Eip712SignedMessage::new(&domain_separator, prev_rav, &keys.0).unwrap();
179
180        // Create new RAV from last half of receipts and prev_rav
181        let rav = ReceiptAggregateVoucher::aggregate_receipts(
182            allocation_ids[0],
183            &receipts[receipts.len() / 2..receipts.len()],
184            Some(signed_prev_rav),
185        )
186        .unwrap();
187        let signed_rav = Eip712SignedMessage::new(&domain_separator, rav, &keys.0).unwrap();
188
189        assert!(signed_rav.recover_signer(&domain_separator).unwrap() == keys.1);
190    }
191}