Skip to content

Commit 525f42f

Browse files
committed
feat: add tap v2 aggregator
Signed-off-by: Gustavo Inacio <[email protected]>
1 parent 84c065c commit 525f42f

File tree

5 files changed

+1008
-359
lines changed

5 files changed

+1008
-359
lines changed

tap_aggregator/src/aggregator.rs

Lines changed: 2 additions & 347 deletions
Original file line numberDiff line numberDiff line change
@@ -1,350 +1,5 @@
11
// Copyright 2023-, Semiotic AI, Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
use std::collections::{hash_set, HashSet};
5-
6-
use alloy::{
7-
dyn_abi::Eip712Domain, primitives::Address, signers::local::PrivateKeySigner,
8-
sol_types::SolStruct,
9-
};
10-
use anyhow::{bail, Ok, Result};
11-
use rayon::prelude::*;
12-
use tap_core::signed_message::{Eip712SignedMessage, SignatureBytes, SignatureBytesExt};
13-
use tap_graph::{Receipt, ReceiptAggregateVoucher};
14-
15-
pub fn check_and_aggregate_receipts(
16-
domain_separator: &Eip712Domain,
17-
receipts: &[Eip712SignedMessage<Receipt>],
18-
previous_rav: Option<Eip712SignedMessage<ReceiptAggregateVoucher>>,
19-
wallet: &PrivateKeySigner,
20-
accepted_addresses: &HashSet<Address>,
21-
) -> Result<Eip712SignedMessage<ReceiptAggregateVoucher>> {
22-
check_signatures_unique(receipts)?;
23-
24-
// Check that the receipts are signed by an accepted signer address
25-
receipts.par_iter().try_for_each(|receipt| {
26-
check_signature_is_from_one_of_addresses(receipt, domain_separator, accepted_addresses)
27-
})?;
28-
29-
// Check that the previous rav is signed by an accepted signer address
30-
if let Some(previous_rav) = &previous_rav {
31-
check_signature_is_from_one_of_addresses(
32-
previous_rav,
33-
domain_separator,
34-
accepted_addresses,
35-
)?;
36-
}
37-
38-
// Check that the receipts timestamp is greater than the previous rav
39-
check_receipt_timestamps(receipts, previous_rav.as_ref())?;
40-
41-
// Get the allocation id from the first receipt, return error if there are no receipts
42-
let allocation_id = match receipts.first() {
43-
Some(receipt) => receipt.message.allocation_id,
44-
None => return Err(tap_core::Error::NoValidReceiptsForRavRequest.into()),
45-
};
46-
47-
// Check that the receipts all have the same allocation id
48-
check_allocation_id(receipts, allocation_id)?;
49-
50-
// Check that the rav has the correct allocation id
51-
if let Some(previous_rav) = &previous_rav {
52-
let prev_id = previous_rav.message.allocationId;
53-
if prev_id != allocation_id {
54-
return Err(tap_core::Error::RavAllocationIdMismatch {
55-
prev_id: format!("{prev_id:#X}"),
56-
new_id: format!("{allocation_id:#X}"),
57-
}
58-
.into());
59-
}
60-
}
61-
62-
// Aggregate the receipts
63-
let rav = ReceiptAggregateVoucher::aggregate_receipts(allocation_id, receipts, previous_rav)?;
64-
65-
// Sign the rav and return
66-
Ok(Eip712SignedMessage::new(domain_separator, rav, wallet)?)
67-
}
68-
69-
fn check_signature_is_from_one_of_addresses<M: SolStruct>(
70-
message: &Eip712SignedMessage<M>,
71-
domain_separator: &Eip712Domain,
72-
accepted_addresses: &HashSet<Address>,
73-
) -> Result<()> {
74-
let recovered_address = message.recover_signer(domain_separator)?;
75-
if !accepted_addresses.contains(&recovered_address) {
76-
bail!(tap_core::Error::InvalidRecoveredSigner {
77-
address: recovered_address,
78-
});
79-
}
80-
Ok(())
81-
}
82-
83-
fn check_allocation_id(
84-
receipts: &[Eip712SignedMessage<Receipt>],
85-
allocation_id: Address,
86-
) -> Result<()> {
87-
for receipt in receipts.iter() {
88-
let receipt = &receipt.message;
89-
if receipt.allocation_id != allocation_id {
90-
return Err(tap_core::Error::RavAllocationIdNotUniform.into());
91-
}
92-
}
93-
Ok(())
94-
}
95-
96-
fn check_signatures_unique(receipts: &[Eip712SignedMessage<Receipt>]) -> Result<()> {
97-
let mut receipt_signatures: hash_set::HashSet<SignatureBytes> = hash_set::HashSet::new();
98-
for receipt in receipts.iter() {
99-
let signature = receipt.signature.get_signature_bytes();
100-
if !receipt_signatures.insert(signature) {
101-
return Err(tap_core::Error::DuplicateReceiptSignature(format!(
102-
"{:?}",
103-
receipt.signature
104-
))
105-
.into());
106-
}
107-
}
108-
Ok(())
109-
}
110-
111-
fn check_receipt_timestamps(
112-
receipts: &[Eip712SignedMessage<Receipt>],
113-
previous_rav: Option<&Eip712SignedMessage<ReceiptAggregateVoucher>>,
114-
) -> Result<()> {
115-
if let Some(previous_rav) = &previous_rav {
116-
for receipt in receipts.iter() {
117-
let receipt = &receipt.message;
118-
if previous_rav.message.timestampNs >= receipt.timestamp_ns {
119-
return Err(tap_core::Error::ReceiptTimestampLowerThanRav {
120-
rav_ts: previous_rav.message.timestampNs,
121-
receipt_ts: receipt.timestamp_ns,
122-
}
123-
.into());
124-
}
125-
}
126-
}
127-
128-
Ok(())
129-
}
130-
131-
#[cfg(test)]
132-
mod tests {
133-
use std::str::FromStr;
134-
135-
use alloy::{dyn_abi::Eip712Domain, primitives::Address, signers::local::PrivateKeySigner};
136-
use rstest::*;
137-
use tap_core::{signed_message::Eip712SignedMessage, tap_eip712_domain};
138-
use tap_graph::{Receipt, ReceiptAggregateVoucher};
139-
140-
use crate::aggregator;
141-
142-
#[fixture]
143-
fn keys() -> (PrivateKeySigner, Address) {
144-
let wallet = PrivateKeySigner::random();
145-
let address = wallet.address();
146-
(wallet, address)
147-
}
148-
149-
#[fixture]
150-
fn allocation_ids() -> Vec<Address> {
151-
vec![
152-
Address::from_str("0xabababababababababababababababababababab").unwrap(),
153-
Address::from_str("0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead").unwrap(),
154-
Address::from_str("0xbeefbeefbeefbeefbeefbeefbeefbeefbeefbeef").unwrap(),
155-
Address::from_str("0x1234567890abcdef1234567890abcdef12345678").unwrap(),
156-
]
157-
}
158-
159-
#[fixture]
160-
fn domain_separator() -> Eip712Domain {
161-
tap_eip712_domain(1, Address::from([0x11u8; 20]))
162-
}
163-
164-
#[rstest]
165-
#[test]
166-
fn check_signatures_unique_fail(
167-
keys: (PrivateKeySigner, Address),
168-
allocation_ids: Vec<Address>,
169-
domain_separator: Eip712Domain,
170-
) {
171-
// Create the same receipt twice (replay attack)
172-
let mut receipts = Vec::new();
173-
let receipt = Eip712SignedMessage::new(
174-
&domain_separator,
175-
Receipt::new(allocation_ids[0], 42).unwrap(),
176-
&keys.0,
177-
)
178-
.unwrap();
179-
receipts.push(receipt.clone());
180-
receipts.push(receipt);
181-
182-
let res = aggregator::check_signatures_unique(&receipts);
183-
assert!(res.is_err());
184-
}
185-
186-
#[rstest]
187-
#[test]
188-
fn check_signatures_unique_ok(
189-
keys: (PrivateKeySigner, Address),
190-
allocation_ids: Vec<Address>,
191-
domain_separator: Eip712Domain,
192-
) {
193-
// Create 2 different receipts
194-
let receipts = vec![
195-
Eip712SignedMessage::new(
196-
&domain_separator,
197-
Receipt::new(allocation_ids[0], 42).unwrap(),
198-
&keys.0,
199-
)
200-
.unwrap(),
201-
Eip712SignedMessage::new(
202-
&domain_separator,
203-
Receipt::new(allocation_ids[0], 43).unwrap(),
204-
&keys.0,
205-
)
206-
.unwrap(),
207-
];
208-
209-
let res = aggregator::check_signatures_unique(&receipts);
210-
assert!(res.is_ok());
211-
}
212-
213-
#[rstest]
214-
#[test]
215-
/// Test that a receipt with a timestamp greater then the rav timestamp passes
216-
fn check_receipt_timestamps(
217-
keys: (PrivateKeySigner, Address),
218-
allocation_ids: Vec<Address>,
219-
domain_separator: Eip712Domain,
220-
) {
221-
// Create receipts with consecutive timestamps
222-
let receipt_timestamp_range = 10..20;
223-
let mut receipts = Vec::new();
224-
for i in receipt_timestamp_range.clone() {
225-
receipts.push(
226-
Eip712SignedMessage::new(
227-
&domain_separator,
228-
Receipt {
229-
allocation_id: allocation_ids[0],
230-
timestamp_ns: i,
231-
nonce: 0,
232-
value: 42,
233-
},
234-
&keys.0,
235-
)
236-
.unwrap(),
237-
);
238-
}
239-
240-
// Create rav with max_timestamp below the receipts timestamps
241-
let rav = Eip712SignedMessage::new(
242-
&domain_separator,
243-
ReceiptAggregateVoucher {
244-
allocationId: allocation_ids[0],
245-
timestampNs: receipt_timestamp_range.clone().min().unwrap() - 1,
246-
valueAggregate: 42,
247-
},
248-
&keys.0,
249-
)
250-
.unwrap();
251-
assert!(aggregator::check_receipt_timestamps(&receipts, Some(&rav)).is_ok());
252-
253-
// Create rav with max_timestamp equal to the lowest receipt timestamp
254-
// Aggregation should fail
255-
let rav = Eip712SignedMessage::new(
256-
&domain_separator,
257-
ReceiptAggregateVoucher {
258-
allocationId: allocation_ids[0],
259-
timestampNs: receipt_timestamp_range.clone().min().unwrap(),
260-
valueAggregate: 42,
261-
},
262-
&keys.0,
263-
)
264-
.unwrap();
265-
assert!(aggregator::check_receipt_timestamps(&receipts, Some(&rav)).is_err());
266-
267-
// Create rav with max_timestamp above highest receipt timestamp
268-
// Aggregation should fail
269-
let rav = Eip712SignedMessage::new(
270-
&domain_separator,
271-
ReceiptAggregateVoucher {
272-
allocationId: allocation_ids[0],
273-
timestampNs: receipt_timestamp_range.clone().max().unwrap() + 1,
274-
valueAggregate: 42,
275-
},
276-
&keys.0,
277-
)
278-
.unwrap();
279-
assert!(aggregator::check_receipt_timestamps(&receipts, Some(&rav)).is_err());
280-
}
281-
282-
#[rstest]
283-
#[test]
284-
/// Test check_allocation_id with 2 receipts that have the correct allocation id
285-
/// and 1 receipt that has the wrong allocation id
286-
fn check_allocation_id_fail(
287-
keys: (PrivateKeySigner, Address),
288-
allocation_ids: Vec<Address>,
289-
domain_separator: Eip712Domain,
290-
) {
291-
let receipts = vec![
292-
Eip712SignedMessage::new(
293-
&domain_separator,
294-
Receipt::new(allocation_ids[0], 42).unwrap(),
295-
&keys.0,
296-
)
297-
.unwrap(),
298-
Eip712SignedMessage::new(
299-
&domain_separator,
300-
Receipt::new(allocation_ids[0], 43).unwrap(),
301-
&keys.0,
302-
)
303-
.unwrap(),
304-
Eip712SignedMessage::new(
305-
&domain_separator,
306-
Receipt::new(allocation_ids[1], 44).unwrap(),
307-
&keys.0,
308-
)
309-
.unwrap(),
310-
];
311-
312-
let res = aggregator::check_allocation_id(&receipts, allocation_ids[0]);
313-
314-
assert!(res.is_err());
315-
}
316-
317-
#[rstest]
318-
#[test]
319-
/// Test check_allocation_id with 3 receipts that have the correct allocation id
320-
fn check_allocation_id_ok(
321-
keys: (PrivateKeySigner, Address),
322-
allocation_ids: Vec<Address>,
323-
domain_separator: Eip712Domain,
324-
) {
325-
let receipts = vec![
326-
Eip712SignedMessage::new(
327-
&domain_separator,
328-
Receipt::new(allocation_ids[0], 42).unwrap(),
329-
&keys.0,
330-
)
331-
.unwrap(),
332-
Eip712SignedMessage::new(
333-
&domain_separator,
334-
Receipt::new(allocation_ids[0], 43).unwrap(),
335-
&keys.0,
336-
)
337-
.unwrap(),
338-
Eip712SignedMessage::new(
339-
&domain_separator,
340-
Receipt::new(allocation_ids[0], 44).unwrap(),
341-
&keys.0,
342-
)
343-
.unwrap(),
344-
];
345-
346-
let res = aggregator::check_allocation_id(&receipts, allocation_ids[0]);
347-
348-
assert!(res.is_ok());
349-
}
350-
}
4+
pub mod v1;
5+
pub mod v2;

0 commit comments

Comments
 (0)