Skip to content

Commit ff856d9

Browse files
authored
Merge pull request #233 from semiotic-ai/gusinacio/check-error-recoverable
feat!: add retryable errors to checks
2 parents 9e1915b + 51f04cb commit ff856d9

File tree

7 files changed

+139
-32
lines changed

7 files changed

+139
-32
lines changed

tap_core/src/manager/context/memory.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ impl EscrowHandler for InMemoryContext {
261261
pub mod checks {
262262
use crate::{
263263
receipt::{
264-
checks::{Check, CheckResult, ReceiptCheck},
264+
checks::{Check, CheckError, CheckResult, ReceiptCheck},
265265
state::Checking,
266266
ReceiptError, ReceiptWithState,
267267
},
@@ -306,10 +306,12 @@ pub mod checks {
306306
{
307307
Ok(())
308308
} else {
309-
Err(ReceiptError::InvalidAllocationID {
310-
received_allocation_id,
311-
}
312-
.into())
309+
Err(CheckError::Failed(
310+
ReceiptError::InvalidAllocationID {
311+
received_allocation_id,
312+
}
313+
.into(),
314+
))
313315
}
314316
}
315317
}
@@ -325,14 +327,22 @@ pub mod checks {
325327
let recovered_address = receipt
326328
.signed_receipt()
327329
.recover_signer(&self.domain_separator)
328-
.map_err(|e| ReceiptError::InvalidSignature {
329-
source_error_message: e.to_string(),
330+
.map_err(|e| {
331+
CheckError::Failed(
332+
ReceiptError::InvalidSignature {
333+
source_error_message: e.to_string(),
334+
}
335+
.into(),
336+
)
330337
})?;
338+
331339
if !self.valid_signers.contains(&recovered_address) {
332-
Err(ReceiptError::InvalidSignature {
333-
source_error_message: "Invalid signer".to_string(),
334-
}
335-
.into())
340+
Err(CheckError::Failed(
341+
ReceiptError::InvalidSignature {
342+
source_error_message: "Invalid signer".to_string(),
343+
}
344+
.into(),
345+
))
336346
} else {
337347
Ok(())
338348
}

tap_core/src/manager/tap_manager.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
receipt::{
1010
checks::{CheckBatch, CheckList, TimestampCheck, UniqueCheck},
1111
state::{Failed, Reserved},
12-
ReceiptWithState, SignedReceipt,
12+
ReceiptError, ReceiptWithState, SignedReceipt,
1313
},
1414
Error,
1515
};
@@ -139,7 +139,10 @@ where
139139
failed_receipts.extend(already_failed);
140140

141141
for receipt in checking_receipts.into_iter() {
142-
let receipt = receipt.finalize_receipt_checks(&self.checks).await;
142+
let receipt = receipt
143+
.finalize_receipt_checks(&self.checks)
144+
.await
145+
.map_err(|e| Error::ReceiptError(ReceiptError::RetryableCheck(e)))?;
143146

144147
match receipt {
145148
Ok(checked) => awaiting_reserve_receipts.push(checked),

tap_core/src/receipt/checks.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,15 @@ use std::{
4545
pub type ReceiptCheck = Arc<dyn Check + Sync + Send>;
4646

4747
/// Result of a check operation. It uses the `anyhow` crate to handle errors.
48-
pub type CheckResult = anyhow::Result<()>;
48+
pub type CheckResult = Result<(), CheckError>;
49+
50+
#[derive(thiserror::Error, Debug)]
51+
pub enum CheckError {
52+
#[error(transparent)]
53+
Retryable(anyhow::Error),
54+
#[error(transparent)]
55+
Failed(anyhow::Error),
56+
}
4957

5058
/// CheckList is a NewType pattern to store a list of checks.
5159
/// It is a wrapper around an Arc of ReceiptCheck[].
@@ -115,11 +123,13 @@ impl Check for StatefulTimestampCheck {
115123
let min_timestamp_ns = *self.min_timestamp_ns.read().unwrap();
116124
let signed_receipt = receipt.signed_receipt();
117125
if signed_receipt.message.timestamp_ns <= min_timestamp_ns {
118-
return Err(ReceiptError::InvalidTimestamp {
119-
received_timestamp: signed_receipt.message.timestamp_ns,
120-
timestamp_min: min_timestamp_ns,
121-
}
122-
.into());
126+
return Err(CheckError::Failed(
127+
ReceiptError::InvalidTimestamp {
128+
received_timestamp: signed_receipt.message.timestamp_ns,
129+
timestamp_min: min_timestamp_ns,
130+
}
131+
.into(),
132+
));
123133
}
124134
Ok(())
125135
}

tap_core/src/receipt/error.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ pub enum ReceiptError {
2323
#[error("Attempt to collect escrow failed")]
2424
SubtractEscrowFailed,
2525
#[error("Issue encountered while performing check: {0}")]
26-
CheckFailedToComplete(String),
26+
CheckFailure(String),
27+
#[error("Retryable check error encountered: {0}")]
28+
RetryableCheck(String),
2729
}

tap_core/src/receipt/received_receipt.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
1616
use alloy::dyn_abi::Eip712Domain;
1717

18+
use super::checks::CheckError;
1819
use super::{Receipt, ReceiptError, ReceiptResult, SignedReceipt};
1920
use crate::receipt::state::{AwaitingReserve, Checking, Failed, ReceiptState, Reserved};
2021
use crate::{
@@ -92,10 +93,10 @@ impl ReceiptWithState<Checking> {
9293
pub async fn perform_checks(&mut self, checks: &[ReceiptCheck]) -> ReceiptResult<()> {
9394
for check in checks {
9495
// return early on an error
95-
check
96-
.check(self)
97-
.await
98-
.map_err(|e| ReceiptError::CheckFailedToComplete(e.to_string()))?;
96+
check.check(self).await.map_err(|e| match e {
97+
CheckError::Retryable(e) => ReceiptError::RetryableCheck(e.to_string()),
98+
CheckError::Failed(e) => ReceiptError::CheckFailure(e.to_string()),
99+
})?;
99100
}
100101
Ok(())
101102
}
@@ -108,14 +109,15 @@ impl ReceiptWithState<Checking> {
108109
pub async fn finalize_receipt_checks(
109110
mut self,
110111
checks: &[ReceiptCheck],
111-
) -> ResultReceipt<AwaitingReserve> {
112+
) -> Result<ResultReceipt<AwaitingReserve>, String> {
112113
let all_checks_passed = self.perform_checks(checks).await;
113-
114-
if let Err(e) = all_checks_passed {
115-
Err(self.perform_state_error(e))
114+
if let Err(ReceiptError::RetryableCheck(e)) = all_checks_passed {
115+
Err(e.to_string())
116+
} else if let Err(e) = all_checks_passed {
117+
Ok(Err(self.perform_state_error(e)))
116118
} else {
117119
let checked = self.perform_state_changes(AwaitingReserve);
118-
Ok(checked)
120+
Ok(Ok(checked))
119121
}
120122
}
121123
}

tap_core/tests/manager_test.rs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
use std::{
44
collections::HashMap,
55
str::FromStr,
6-
sync::{Arc, RwLock},
6+
sync::{atomic::AtomicBool, Arc, RwLock},
77
time::{SystemTime, UNIX_EPOCH},
88
};
99

1010
use alloy::{dyn_abi::Eip712Domain, primitives::Address, signers::local::PrivateKeySigner};
11+
use anyhow::anyhow;
1112
use rstest::*;
1213

1314
fn get_current_timestamp_u64_ns() -> anyhow::Result<u64> {
@@ -24,8 +25,9 @@ use tap_core::{
2425
},
2526
rav::ReceiptAggregateVoucher,
2627
receipt::{
27-
checks::{CheckList, StatefulTimestampCheck},
28-
Receipt,
28+
checks::{Check, CheckError, CheckList, StatefulTimestampCheck},
29+
state::Checking,
30+
Receipt, ReceiptWithState,
2931
},
3032
signed_message::EIP712SignedMessage,
3133
tap_eip712_domain,
@@ -530,3 +532,79 @@ async fn manager_create_rav_and_ignore_invalid_receipts(
530532
//Rav Value corresponds only to value of one receipt
531533
assert_eq!(expected_rav.valueAggregate, 20);
532534
}
535+
536+
#[rstest]
537+
#[tokio::test]
538+
async fn test_retryable_checks(
539+
allocation_ids: Vec<Address>,
540+
domain_separator: Eip712Domain,
541+
context: ContextFixture,
542+
) {
543+
struct RetryableCheck(Arc<AtomicBool>);
544+
545+
#[async_trait::async_trait]
546+
impl Check for RetryableCheck {
547+
async fn check(&self, receipt: &ReceiptWithState<Checking>) -> Result<(), CheckError> {
548+
// we want to fail only if nonce is 5 and if is create rav step
549+
if self.0.load(std::sync::atomic::Ordering::SeqCst)
550+
&& receipt.signed_receipt().message.nonce == 5
551+
{
552+
Err(CheckError::Retryable(anyhow!("Retryable error")))
553+
} else {
554+
Ok(())
555+
}
556+
}
557+
}
558+
559+
let ContextFixture {
560+
context,
561+
checks,
562+
escrow_storage,
563+
signer,
564+
..
565+
} = context;
566+
567+
let is_create_rav = Arc::new(AtomicBool::new(false));
568+
569+
let mut checks: Vec<Arc<dyn Check + Send + Sync>> = checks.iter().cloned().collect();
570+
checks.push(Arc::new(RetryableCheck(is_create_rav.clone())));
571+
572+
let manager = Manager::new(
573+
domain_separator.clone(),
574+
context.clone(),
575+
CheckList::new(checks),
576+
);
577+
578+
escrow_storage
579+
.write()
580+
.unwrap()
581+
.insert(signer.address(), 999999);
582+
583+
let mut stored_signed_receipts = Vec::new();
584+
for i in 0..10 {
585+
let receipt = Receipt {
586+
allocation_id: allocation_ids[0],
587+
timestamp_ns: i + 1,
588+
nonce: i,
589+
value: 20u128,
590+
};
591+
let signed_receipt = EIP712SignedMessage::new(&domain_separator, receipt, &signer).unwrap();
592+
stored_signed_receipts.push(signed_receipt.clone());
593+
manager
594+
.verify_and_store_receipt(signed_receipt)
595+
.await
596+
.unwrap();
597+
}
598+
599+
is_create_rav.store(true, std::sync::atomic::Ordering::SeqCst);
600+
601+
let rav_request = manager.create_rav_request(0, None).await;
602+
603+
assert_eq!(
604+
rav_request.expect_err("Didn't fail").to_string(),
605+
tap_core::Error::ReceiptError(tap_core::receipt::ReceiptError::RetryableCheck(
606+
"Retryable error".to_string()
607+
))
608+
.to_string()
609+
);
610+
}

tap_core/tests/received_receipt_test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ async fn partial_then_finalize_valid_receipt(
185185

186186
let awaiting_escrow_receipt = awaiting_escrow_receipt.unwrap();
187187
let receipt = awaiting_escrow_receipt
188+
.unwrap()
188189
.check_and_reserve_escrow(&context, &domain_separator)
189190
.await;
190191
assert!(receipt.is_ok());
@@ -234,6 +235,7 @@ async fn standard_lifetime_valid_receipt(
234235

235236
let awaiting_escrow_receipt = awaiting_escrow_receipt.unwrap();
236237
let receipt = awaiting_escrow_receipt
238+
.unwrap()
237239
.check_and_reserve_escrow(&context, &domain_separator)
238240
.await;
239241
assert!(receipt.is_ok());

0 commit comments

Comments
 (0)