Skip to content

Commit e0d5c73

Browse files
committed
feat: add escape hatch to trusted senders
Signed-off-by: Gustavo Inacio <[email protected]>
1 parent 32f30db commit e0d5c73

File tree

7 files changed

+125
-14
lines changed

7 files changed

+125
-14
lines changed

crates/config/maximal-config-example.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ max_receipt_value_grt = "0.001" # 0.001 GRT. We use strings to prevent rounding
124124
# max_amount_willing_to_lose_grt = "0.1"
125125
max_amount_willing_to_lose_grt = 20
126126

127+
# List of Senders that are allowed to spend up to `max_amount_willing_to_lose_grt`
128+
# over the escrow balance
129+
trusted_senders = ["0xdeadbeefcafebabedeadbeefcafebabedeadbeef"]
130+
131+
127132
# Receipts query timeout
128133
sender_timeout_secs = 30
129134

crates/config/src/config.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use std::{
5-
collections::HashMap,
5+
collections::{HashMap, HashSet},
66
env,
77
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
88
path::PathBuf,
@@ -382,6 +382,11 @@ pub struct TapConfig {
382382
pub sender_timeout_secs: Duration,
383383

384384
pub sender_aggregator_endpoints: HashMap<Address, Url>,
385+
386+
/// Senders that are allowed to spend up to `max_amount_willing_to_lose_grt`
387+
/// over the escrow balance
388+
#[serde(default)]
389+
pub trusted_senders: HashSet<Address>,
385390
}
386391

387392
#[derive(Debug, Deserialize)]
@@ -431,11 +436,11 @@ pub struct RavRequestConfig {
431436

432437
#[cfg(test)]
433438
mod tests {
434-
use std::{env, fs, path::PathBuf, str::FromStr};
439+
use std::{collections::HashSet, env, fs, path::PathBuf, str::FromStr};
435440

436441
use figment::value::Uncased;
437442
use sealed_test::prelude::*;
438-
use thegraph_core::alloy::primitives::{Address, FixedBytes};
443+
use thegraph_core::alloy::primitives::{address, Address, FixedBytes};
439444
use tracing_test::traced_test;
440445

441446
use super::{DatabaseConfig, SHARED_PREFIX};
@@ -458,6 +463,8 @@ mod tests {
458463
Some(PathBuf::from("minimal-config-example.toml")).as_ref(),
459464
)
460465
.unwrap();
466+
max_config.tap.trusted_senders =
467+
HashSet::from([address!("deadbeefcafebabedeadbeefcafebabedeadbeef")]);
461468
max_config.dips = Some(crate::DipsConfig {
462469
allowed_payers: vec![Address(
463470
FixedBytes::<20>::from_str("0x3333333333333333333333333333333333333333").unwrap(),

crates/dips/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ mod test {
373373
time::{Duration, SystemTime, UNIX_EPOCH},
374374
};
375375

376+
use rand::{distr::Alphanumeric, Rng};
376377
use thegraph_core::alloy::{
377378
primitives::{Address, FixedBytes, U256},
378379
signers::local::PrivateKeySigner,
@@ -386,8 +387,6 @@ mod test {
386387
price::PriceCalculator, CancellationRequest, DipsError, IndexingAgreementVoucher,
387388
SignedIndexingAgreementVoucher, SubgraphIndexingVoucherMetadata,
388389
};
389-
use rand::distr::Alphanumeric;
390-
use rand::Rng;
391390

392391
#[tokio::test]
393392
async fn test_validate_and_create_agreement() -> anyhow::Result<()> {

crates/dips/src/server.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
use std::sync::Arc;
55

66
use async_trait::async_trait;
7-
use thegraph_core::alloy::primitives::Address;
8-
use thegraph_core::alloy::sol_types::Eip712Domain;
7+
use thegraph_core::alloy::{primitives::Address, sol_types::Eip712Domain};
98
use tonic::{Request, Response, Status};
109

1110
use crate::{

crates/tap-agent/src/agent/sender_account.rs

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ pub struct State {
317317
// reset in case of a successful response
318318
backoff_info: BackoffInfo,
319319

320+
/// Allows the sender to go over escrow balance
321+
/// limited to `max_amount_willing_to_lose_grt`
322+
trusted_sender: bool,
323+
320324
// Config forwarded to [SenderAllocation]
321325
config: &'static SenderAccountConfig,
322326
}
@@ -343,6 +347,9 @@ pub struct SenderAccountConfig {
343347
///
344348
/// This is reached if the database is too slow
345349
pub tap_sender_timeout: Duration,
350+
/// Senders that are allowed to spend up to `max_amount_willing_to_lose_grt`
351+
/// over the escrow balance
352+
pub trusted_senders: HashSet<Address>,
346353
}
347354

348355
impl SenderAccountConfig {
@@ -357,6 +364,7 @@ impl SenderAccountConfig {
357364
trigger_value: config.tap.get_trigger_value(),
358365
rav_request_timeout: config.tap.rav_request.request_timeout_secs,
359366
tap_sender_timeout: config.tap.sender_timeout_secs,
367+
trusted_senders: config.tap.trusted_senders.clone(),
360368
}
361369
}
362370
}
@@ -531,14 +539,22 @@ impl State {
531539
fn deny_condition_reached(&self) -> bool {
532540
let pending_ravs = self.rav_tracker.get_total_fee();
533541
let unaggregated_fees = self.sender_fee_tracker.get_total_fee();
534-
let pending_fees_over_balance =
535-
U256::from(pending_ravs + unaggregated_fees) >= self.sender_balance;
536542
let max_amount_willing_to_lose = self.config.max_amount_willing_to_lose_grt;
543+
544+
// if it's a trusted sender, allow to spend up to max_amount_willing_to_lose
545+
let balance = if self.trusted_sender {
546+
self.sender_balance + U256::from(max_amount_willing_to_lose)
547+
} else {
548+
self.sender_balance
549+
};
550+
551+
let pending_fees_over_balance = U256::from(pending_ravs + unaggregated_fees) >= balance;
537552
let invalid_receipt_fees = self.invalid_receipts_tracker.get_total_fee();
538553
let total_fee_over_max_value =
539554
unaggregated_fees + invalid_receipt_fees >= max_amount_willing_to_lose;
540555

541556
tracing::trace!(
557+
trusted_sender = %self.trusted_sender,
542558
%pending_fees_over_balance,
543559
%total_fee_over_max_value,
544560
"Verifying if deny condition was reached.",
@@ -550,6 +566,7 @@ impl State {
550566
/// Will update [`State::denied`], as well as the denylist table in the database.
551567
async fn add_to_denylist(&mut self) {
552568
tracing::warn!(
569+
trusted_sender = %self.trusted_sender,
553570
fee_tracker = self.sender_fee_tracker.get_total_fee(),
554571
rav_tracker = self.rav_tracker.get_total_fee(),
555572
max_amount_willing_to_lose = self.config.max_amount_willing_to_lose_grt,
@@ -841,6 +858,7 @@ impl Actor for SenderAccount {
841858
aggregator_v1,
842859
aggregator_v2,
843860
backoff_info: BackoffInfo::default(),
861+
trusted_sender: config.trusted_senders.contains(&sender_id),
844862
config,
845863
};
846864

@@ -1284,7 +1302,7 @@ pub mod tests {
12841302
Mock, MockServer, ResponseTemplate,
12851303
};
12861304

1287-
use super::SenderAccountMessage;
1305+
use super::{RavInformation, SenderAccountMessage};
12881306
use crate::{
12891307
agent::{
12901308
sender_account::ReceiptFees, sender_accounts_manager::AllocationId,
@@ -1294,7 +1312,7 @@ pub mod tests {
12941312
assert_not_triggered, assert_triggered,
12951313
test::{
12961314
actors::{create_mock_sender_allocation, MockSenderAllocation},
1297-
create_rav, create_sender_account, store_rav_with_options, TRIGGER_VALUE,
1315+
create_rav, create_sender_account, store_rav_with_options, ESCROW_VALUE, TRIGGER_VALUE,
12981316
},
12991317
};
13001318

@@ -1343,7 +1361,6 @@ pub mod tests {
13431361
}
13441362

13451363
/// Prefix shared between tests so we don't have conflicts in the global registry
1346-
const ESCROW_VALUE: u128 = 1000;
13471364
const BUFFER_DURATION: Duration = Duration::from_millis(100);
13481365
const RETRY_DURATION: Duration = Duration::from_millis(1000);
13491366

@@ -1986,6 +2003,76 @@ pub mod tests {
19862003
sender_account.stop_and_wait(None, None).await.unwrap();
19872004
}
19882005

2006+
#[sqlx::test(migrations = "../../migrations")]
2007+
async fn test_trusted_sender(pgpool: PgPool) {
2008+
let max_amount_willing_to_lose_grt = ESCROW_VALUE / 10;
2009+
// initialize with no trigger value and no max receipt deny
2010+
let (sender_account, notify, prefix, _) = create_sender_account()
2011+
.pgpool(pgpool)
2012+
.trusted_sender(true)
2013+
.rav_request_trigger_value(u128::MAX)
2014+
.max_amount_willing_to_lose_grt(max_amount_willing_to_lose_grt)
2015+
.call()
2016+
.await;
2017+
2018+
let (mock_sender_allocation, _) =
2019+
MockSenderAllocation::new_with_next_rav_value(sender_account.clone());
2020+
2021+
let name = format!("{}:{}:{}", prefix, SENDER.1, ALLOCATION_ID_0);
2022+
let (allocation, _) = MockSenderAllocation::spawn(Some(name), mock_sender_allocation, ())
2023+
.await
2024+
.unwrap();
2025+
2026+
async fn get_deny_status(sender_account: &ActorRef<SenderAccountMessage>) -> bool {
2027+
call!(sender_account, SenderAccountMessage::GetDeny).unwrap()
2028+
}
2029+
2030+
macro_rules! update_receipt_fees {
2031+
($value:expr) => {
2032+
sender_account
2033+
.cast(SenderAccountMessage::UpdateRav(RavInformation {
2034+
allocation_id: ALLOCATION_ID_0,
2035+
value_aggregate: $value,
2036+
}))
2037+
.unwrap();
2038+
2039+
flush_messages(&notify).await;
2040+
};
2041+
}
2042+
2043+
let deny = call!(sender_account, SenderAccountMessage::GetDeny).unwrap();
2044+
assert!(!deny);
2045+
2046+
update_receipt_fees!(ESCROW_VALUE - 1);
2047+
let deny = get_deny_status(&sender_account).await;
2048+
assert!(!deny, "it shouldn't deny a sender below escrow balance");
2049+
2050+
update_receipt_fees!(ESCROW_VALUE);
2051+
let deny = get_deny_status(&sender_account).await;
2052+
assert!(
2053+
!deny,
2054+
"it shouldn't deny a trusted sender below escrow balance + max willing to lose"
2055+
);
2056+
2057+
update_receipt_fees!(ESCROW_VALUE + max_amount_willing_to_lose_grt - 1);
2058+
let deny = get_deny_status(&sender_account).await;
2059+
assert!(
2060+
!deny,
2061+
"it shouldn't deny a trusted sender below escrow balance + max willing to lose"
2062+
);
2063+
2064+
update_receipt_fees!(ESCROW_VALUE + max_amount_willing_to_lose_grt);
2065+
let deny = get_deny_status(&sender_account).await;
2066+
assert!(
2067+
deny,
2068+
"it should deny a trusted sender over escrow balance + max willing to lose"
2069+
);
2070+
2071+
allocation.stop_and_wait(None, None).await.unwrap();
2072+
2073+
sender_account.stop_and_wait(None, None).await.unwrap();
2074+
}
2075+
19892076
#[sqlx::test(migrations = "../../migrations")]
19902077
async fn test_pending_rav_already_redeemed_and_redeem(pgpool: PgPool) {
19912078
// Start a mock graphql server using wiremock

crates/tap-agent/src/test.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ lazy_static! {
6464
pub const TRIGGER_VALUE: u128 = 500;
6565
pub const RECEIPT_LIMIT: u64 = 10000;
6666
pub const DUMMY_URL: &str = "http://localhost:1234";
67-
const ESCROW_VALUE: u128 = 1000;
67+
pub const ESCROW_VALUE: u128 = 1000;
6868
const BUFFER_DURATION: Duration = Duration::from_millis(100);
6969
const RETRY_DURATION: Duration = Duration::from_millis(1000);
7070
const RAV_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
@@ -93,6 +93,7 @@ pub fn get_sender_account_config() -> &'static SenderAccountConfig {
9393
indexer_address: INDEXER.1,
9494
escrow_polling_interval: ESCROW_POLLING_INTERVAL,
9595
tap_sender_timeout: Duration::from_secs(63),
96+
trusted_senders: HashSet::new(),
9697
}))
9798
}
9899

@@ -107,12 +108,18 @@ pub async fn create_sender_account(
107108
network_subgraph_endpoint: Option<&str>,
108109
#[builder(default = RECEIPT_LIMIT)] rav_request_receipt_limit: u64,
109110
aggregator_endpoint: Option<Url>,
111+
#[builder(default = false)] trusted_sender: bool,
110112
) -> (
111113
ActorRef<SenderAccountMessage>,
112114
Arc<Notify>,
113115
String,
114116
Sender<EscrowAccounts>,
115117
) {
118+
let trusted_senders = if trusted_sender {
119+
HashSet::from([SENDER.1])
120+
} else {
121+
HashSet::new()
122+
};
116123
let config = Box::leak(Box::new(SenderAccountConfig {
117124
rav_request_buffer: BUFFER_DURATION,
118125
max_amount_willing_to_lose_grt,
@@ -122,6 +129,7 @@ pub async fn create_sender_account(
122129
indexer_address: INDEXER.1,
123130
escrow_polling_interval: Duration::default(),
124131
tap_sender_timeout: TAP_SENDER_TIMEOUT,
132+
trusted_senders,
125133
}));
126134

127135
let network_subgraph = Box::leak(Box::new(

crates/tap-agent/tests/tap_agent_test.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
4+
use std::{
5+
collections::{HashMap, HashSet},
6+
str::FromStr,
7+
sync::Arc,
8+
time::Duration,
9+
};
510

611
use indexer_monitor::{DeploymentDetails, EscrowAccounts, SubgraphClient};
712
use indexer_tap_agent::{
@@ -87,6 +92,7 @@ pub async fn start_agent(
8792
indexer_address: INDEXER_ADDRESS,
8893
escrow_polling_interval: Duration::from_secs(10),
8994
tap_sender_timeout: Duration::from_secs(30),
95+
trusted_senders: HashSet::new(),
9096
}));
9197

9298
let args = SenderAccountsManagerArgs {

0 commit comments

Comments
 (0)