Skip to content

Commit 34ce44b

Browse files
committed
cli: probe the dynamic access pass PDA in check_accesspass
The pre-flight looked up only the (client_ip, payer) PDA. A wildcard-seat holder — a pass stored at 0.0.0.0, including the EdgeSeat passes issued by the shred oracle — has no pass at their specific IP, so connect reported no valid AccessPass and bailed before reaching the program, which accepts either PDA. Fall back to the UNSPECIFIED PDA when the specific-IP lookup misses, applying the same epoch check. The two copies of this logic, each carrying a comment asking the next person to keep them in sync, now delegate to one helper in the SDK.
1 parent db50898 commit 34ce44b

4 files changed

Lines changed: 218 additions & 34 deletions

File tree

crates/doublezero-daemon-cli/src/connect.rs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use doublezero_cli_core::CliContext;
1414
use doublezero_ip_proof::IpOwnershipProof;
1515
use doublezero_sdk::{
1616
commands::{
17+
accesspass::check::check_accesspass as check_accesspass_shared,
1718
multicastgroup::{
1819
subscribe::{UpdateMulticastGroupRolesCommand, MAX_GROUPS_PER_TRANSACTION},
1920
subscribe_feed::SubscribeFeedCommand,
@@ -168,26 +169,21 @@ enum FeedJoinUser {
168169
},
169170
}
170171

171-
/// AccessPass pre-flight: `Ok(false)` when no pass exists for
172-
/// `(client_ip, payer)` so the caller can render its own diagnostic before
173-
/// bailing. With `enforce_epoch`, the pass must also cover the current epoch.
174-
///
175-
/// Mirrors `check_accesspass` in `smartcontract/cli/src/requirements.rs` —
176-
/// keep the two in sync if AccessPass validity semantics change.
172+
/// AccessPass pre-flight: `Ok(false)` when the caller holds no usable pass, so the caller can
173+
/// render its own diagnostic before bailing. Delegates to the shared
174+
/// [`doublezero_sdk::commands::accesspass::check::check_accesspass`], which also probes the
175+
/// dynamic (0.0.0.0) PDA so a wildcard-seat holder is not turned away here.
177176
fn check_accesspass<L: LedgerClient>(
178177
ledger: &L,
179178
client_ip: Ipv4Addr,
180179
enforce_epoch: bool,
181180
) -> eyre::Result<bool> {
182-
let Some(accesspass) = ledger.get_accesspass(client_ip, ledger.get_payer())? else {
183-
return Ok(false);
184-
};
185-
186-
if !enforce_epoch {
187-
return Ok(true);
188-
}
189-
let epoch = ledger.get_epoch()?;
190-
Ok(accesspass.last_access_epoch >= epoch)
181+
check_accesspass_shared(
182+
client_ip,
183+
enforce_epoch,
184+
|client_ip| ledger.get_accesspass(client_ip, ledger.get_payer()),
185+
|| ledger.get_epoch(),
186+
)
191187
}
192188

193189
/// Fetch the caller's AccessPass, rendering the same diagnostic and error that

smartcontract/cli/src/requirements.rs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use std::net::Ipv4Addr;
33
use crate::doublezerocommand::CliCommand;
44
use doublezero_sdk::{
55
commands::{
6-
accesspass::get::GetAccessPassCommand,
6+
accesspass::{
7+
check::check_accesspass as check_accesspass_shared, get::GetAccessPassCommand,
8+
},
79
allowlist::foundation::list::ListFoundationAllowlistCommand,
810
},
911
get_doublezero_pubkey,
@@ -81,30 +83,27 @@ pub fn check_balance(client: &dyn CliCommand, spinner: Option<&ProgressBar>) ->
8183
}
8284
}
8385

84-
/// Mirrors `check_accesspass` in `doublezero-daemon-cli`
85-
/// (`crates/doublezero-daemon-cli/src/connect.rs`) — keep the two in sync if
86-
/// AccessPass validity semantics change.
86+
/// AccessPass pre-flight for `connect`. Delegates to the shared
87+
/// [`doublezero_sdk::commands::accesspass::check::check_accesspass`], which also probes the
88+
/// dynamic (0.0.0.0) PDA so a wildcard-seat holder is not turned away here.
8789
pub fn check_accesspass(
8890
client: &dyn CliCommand,
8991
client_ip: Ipv4Addr,
9092
enforce_epoch: bool,
9193
) -> eyre::Result<bool> {
92-
// A missing AccessPass returns Ok(false) (not an error) so the caller can
93-
// render its own diagnostic (e.g. the client IP and UserPayer) before
94-
// bailing, rather than surfacing a generic "not found" message.
95-
let Some((_, accesspass)) = client.get_accesspass(GetAccessPassCommand {
94+
check_accesspass_shared(
9695
client_ip,
97-
user_payer: client.get_payer(),
98-
})?
99-
else {
100-
return Ok(false);
101-
};
102-
103-
if !enforce_epoch {
104-
return Ok(true);
105-
}
106-
let epoch = client.get_epoch()?;
107-
Ok(accesspass.last_access_epoch >= epoch)
96+
enforce_epoch,
97+
|client_ip| {
98+
Ok(client
99+
.get_accesspass(GetAccessPassCommand {
100+
client_ip,
101+
user_payer: client.get_payer(),
102+
})?
103+
.map(|(_, accesspass)| accesspass))
104+
},
105+
|| client.get_epoch(),
106+
)
108107
}
109108

110109
pub fn check_allowlist(
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//! The AccessPass pre-flight shared by `doublezero connect` and the serviceability CLI.
2+
//!
3+
//! Both surfaces answer the same question before they send anything: does the caller hold a pass
4+
//! the program will accept for this `client_ip`? They reach the ledger through different client
5+
//! abstractions, so the ledger read is a closure and only the decision lives here — previously it
6+
//! was two copies with a comment on each asking the next person to keep them in sync.
7+
8+
use doublezero_serviceability::state::accesspass::AccessPass;
9+
use std::net::Ipv4Addr;
10+
11+
/// Reports whether the caller holds a usable AccessPass for `client_ip`.
12+
///
13+
/// `lookup` resolves the pass stored at a given `client_ip` for the caller's payer; `epoch` reads
14+
/// the current ledger epoch, and is only called when `enforce_epoch` is set so the common
15+
/// "no pass at all" answer costs one RPC.
16+
///
17+
/// A pass stored at [`Ipv4Addr::UNSPECIFIED`] (0.0.0.0) is valid for any client IP — that is how
18+
/// dynamic seats, including the `EdgeSeat` passes issued by the shred oracle, are held. The
19+
/// program accepts either the exact-IP PDA or the UNSPECIFIED one (see `create_core.rs`), so
20+
/// probing only the exact IP would report "no valid AccessPass" for a wildcard holder and bail
21+
/// before ever reaching the program, which would have accepted them. Hence the fallback.
22+
///
23+
/// `Ok(false)` rather than an error when no pass exists: the caller renders its own diagnostic
24+
/// (the client IP and payer) before bailing, which is more use than a generic "not found".
25+
pub fn check_accesspass<L, E>(
26+
client_ip: Ipv4Addr,
27+
enforce_epoch: bool,
28+
lookup: L,
29+
epoch: E,
30+
) -> eyre::Result<bool>
31+
where
32+
L: Fn(Ipv4Addr) -> eyre::Result<Option<AccessPass>>,
33+
E: FnOnce() -> eyre::Result<u64>,
34+
{
35+
let accesspass = match lookup(client_ip)? {
36+
Some(accesspass) => Some(accesspass),
37+
// Already the dynamic PDA — a second identical lookup would tell us nothing.
38+
None if client_ip == Ipv4Addr::UNSPECIFIED => None,
39+
None => lookup(Ipv4Addr::UNSPECIFIED)?,
40+
};
41+
42+
let Some(accesspass) = accesspass else {
43+
return Ok(false);
44+
};
45+
46+
if !enforce_epoch {
47+
return Ok(true);
48+
}
49+
Ok(accesspass.last_access_epoch >= epoch()?)
50+
}
51+
52+
#[cfg(test)]
53+
mod tests {
54+
use super::*;
55+
use doublezero_serviceability::state::{
56+
accesspass::{AccessPassStatus, AccessPassType},
57+
accounttype::AccountType,
58+
};
59+
use solana_sdk::pubkey::Pubkey;
60+
use std::cell::RefCell;
61+
62+
const CLIENT_IP: Ipv4Addr = Ipv4Addr::new(203, 0, 113, 7);
63+
const EPOCH: u64 = 100;
64+
65+
fn pass(client_ip: Ipv4Addr, last_access_epoch: u64) -> AccessPass {
66+
AccessPass {
67+
account_type: AccountType::AccessPass,
68+
owner: Pubkey::new_unique(),
69+
bump_seed: 0,
70+
accesspass_type: AccessPassType::Prepaid,
71+
client_ip,
72+
user_payer: Pubkey::new_unique(),
73+
last_access_epoch,
74+
connection_count: 0,
75+
status: AccessPassStatus::Connected,
76+
mgroup_pub_allowlist: vec![],
77+
mgroup_sub_allowlist: vec![],
78+
flags: 0,
79+
tenant_allowlist: vec![],
80+
unicast_user_count: 0,
81+
max_unicast_users: 1,
82+
multicast_user_count: 0,
83+
max_multicast_users: 1,
84+
}
85+
}
86+
87+
/// Records the IPs looked up, so the tests can assert the fallback happened (or did not).
88+
fn recording_lookup<'a>(
89+
seen: &'a RefCell<Vec<Ipv4Addr>>,
90+
answer: impl Fn(Ipv4Addr) -> Option<AccessPass> + 'a,
91+
) -> impl Fn(Ipv4Addr) -> eyre::Result<Option<AccessPass>> + 'a {
92+
move |ip| {
93+
seen.borrow_mut().push(ip);
94+
Ok(answer(ip))
95+
}
96+
}
97+
98+
#[test]
99+
fn specific_ip_pass_is_found_without_probing_the_dynamic_pda() {
100+
let seen = RefCell::new(vec![]);
101+
let found = check_accesspass(
102+
CLIENT_IP,
103+
true,
104+
recording_lookup(&seen, |ip| (ip == CLIENT_IP).then(|| pass(ip, EPOCH))),
105+
|| Ok(EPOCH),
106+
)
107+
.unwrap();
108+
109+
assert!(found);
110+
assert_eq!(*seen.borrow(), vec![CLIENT_IP]);
111+
}
112+
113+
#[test]
114+
fn falls_back_to_a_valid_dynamic_pass() {
115+
let seen = RefCell::new(vec![]);
116+
let found = check_accesspass(
117+
CLIENT_IP,
118+
true,
119+
recording_lookup(&seen, |ip| {
120+
(ip == Ipv4Addr::UNSPECIFIED).then(|| pass(ip, EPOCH))
121+
}),
122+
|| Ok(EPOCH),
123+
)
124+
.unwrap();
125+
126+
assert!(found);
127+
assert_eq!(*seen.borrow(), vec![CLIENT_IP, Ipv4Addr::UNSPECIFIED]);
128+
}
129+
130+
#[test]
131+
fn no_pass_at_either_pda_is_not_an_error() {
132+
let seen = RefCell::new(vec![]);
133+
let found = check_accesspass(CLIENT_IP, true, recording_lookup(&seen, |_| None), || {
134+
panic!("the epoch is not read when there is no pass")
135+
})
136+
.unwrap();
137+
138+
assert!(!found);
139+
assert_eq!(*seen.borrow(), vec![CLIENT_IP, Ipv4Addr::UNSPECIFIED]);
140+
}
141+
142+
#[test]
143+
fn an_epoch_expired_dynamic_pass_does_not_count() {
144+
let seen = RefCell::new(vec![]);
145+
let found = check_accesspass(
146+
CLIENT_IP,
147+
true,
148+
recording_lookup(&seen, |ip| {
149+
(ip == Ipv4Addr::UNSPECIFIED).then(|| pass(ip, EPOCH - 1))
150+
}),
151+
|| Ok(EPOCH),
152+
)
153+
.unwrap();
154+
155+
assert!(!found);
156+
}
157+
158+
#[test]
159+
fn an_expired_pass_still_counts_when_the_epoch_is_not_enforced() {
160+
let seen = RefCell::new(vec![]);
161+
let found = check_accesspass(
162+
CLIENT_IP,
163+
false,
164+
recording_lookup(&seen, |ip| {
165+
(ip == Ipv4Addr::UNSPECIFIED).then(|| pass(ip, EPOCH - 1))
166+
}),
167+
|| panic!("the epoch is not read when it is not enforced"),
168+
)
169+
.unwrap();
170+
171+
assert!(found);
172+
}
173+
174+
#[test]
175+
fn an_unspecified_client_ip_is_looked_up_once() {
176+
let seen = RefCell::new(vec![]);
177+
let found = check_accesspass(
178+
Ipv4Addr::UNSPECIFIED,
179+
true,
180+
recording_lookup(&seen, |_| None),
181+
|| Ok(EPOCH),
182+
)
183+
.unwrap();
184+
185+
assert!(!found);
186+
assert_eq!(*seen.borrow(), vec![Ipv4Addr::UNSPECIFIED]);
187+
}
188+
}

smartcontract/sdk/rs/src/commands/accesspass/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod check;
12
pub mod check_status;
23
pub mod close;
34
pub mod get;

0 commit comments

Comments
 (0)