Skip to content

Commit 949dc28

Browse files
authored
Slot authorization configuration (#6)
* feat: perms module * lint: clippy * feat: slot calculator in config * fix: add calculator * refactor: improve structure, calc to utils * fix: chrono dep spec * chore: version * fix: remove unused const * fix: docs * docs: more
1 parent 697f776 commit 949dc28

File tree

6 files changed

+570
-3
lines changed

6 files changed

+570
-3
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ name = "init4-bin-base"
44
description = "Internal utilities for binaries produced by the init4 team"
55
keywords = ["init4", "bin", "base"]
66

7-
version = "0.1.3"
7+
version = "0.1.4"
88
edition = "2021"
99
rust-version = "1.81"
1010
authors = ["init4", "James Prestwich"]
@@ -30,6 +30,9 @@ url = "2.5.4"
3030
metrics = "0.24.1"
3131
metrics-exporter-prometheus = "0.16.2"
3232

33+
# Slot Calc
34+
chrono = "0.4.40"
35+
3336
# Other
3437
thiserror = "2.0.11"
3538
alloy = { version = "0.12.6", optional = true, default-features = false, features = ["std"] }
@@ -44,3 +47,4 @@ tokio = { version = "1.43.0", features = ["macros"] }
4447
[features]
4548
default = ["alloy"]
4649
alloy = ["dep:alloy"]
50+
perms = []

src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
#![deny(unused_must_use, rust_2018_idioms)]
1313
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
1414

15-
use utils::otlp::OtelGuard;
15+
#[cfg(feature = "perms")]
16+
/// Permissioning and authorization utilities for Signet builders.
17+
pub mod perms;
1618

1719
/// Signet utilities.
1820
pub mod utils {
@@ -30,6 +32,10 @@ pub mod utils {
3032

3133
/// Tracing utilities.
3234
pub mod tracing;
35+
36+
/// Slot calculator for determining the current slot and timepoint within a
37+
/// slot.
38+
pub mod calc;
3339
}
3440

3541
/// Re-exports of common dependencies.
@@ -64,7 +70,7 @@ pub mod deps {
6470
///
6571
/// [`init_tracing`]: utils::tracing::init_tracing
6672
/// [`init_metrics`]: utils::metrics::init_metrics
67-
pub fn init4() -> Option<OtelGuard> {
73+
pub fn init4() -> Option<utils::otlp::OtelGuard> {
6874
let guard = utils::tracing::init_tracing();
6975
utils::metrics::init_metrics();
7076
guard

src/perms/builders.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//! #Signet Quincey builder permissioning system.
2+
//!
3+
//! The permissioning system decides which builder can perform a certain action
4+
//! at a given time. The permissioning system uses a simple round-robin design,
5+
//! where each builder is allowed to perform an action at a specific slot.
6+
//! Builders are permissioned based on their sub, which is present in the JWT
7+
//! token they acquire from our OAuth service.
8+
9+
use crate::{
10+
perms::{SlotAuthzConfig, SlotAuthzConfigError},
11+
utils::{
12+
calc::SlotCalculator,
13+
from_env::{FromEnv, FromEnvErr, FromEnvVar},
14+
},
15+
};
16+
17+
/// The builder list env var.
18+
const BUILDERS: &str = "PERMISSIONED_BUILDERS";
19+
20+
fn now() -> u64 {
21+
chrono::Utc::now().timestamp().try_into().unwrap()
22+
}
23+
24+
/// Possible errors when permissioning a builder.
25+
#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
26+
pub enum BuilderPermissionError {
27+
/// Action attempt too early.
28+
#[error("action attempt too early")]
29+
ActionAttemptTooEarly,
30+
31+
/// Action attempt too late.
32+
#[error("action attempt too late")]
33+
ActionAttemptTooLate,
34+
35+
/// Builder not permissioned for this slot.
36+
#[error("builder not permissioned for this slot")]
37+
NotPermissioned,
38+
}
39+
40+
/// Possible errors when loading the builder configuration.
41+
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
42+
pub enum BuilderConfigError {
43+
/// Error loading the environment variable.
44+
#[error(
45+
"failed to parse environment variable. Expected a comma-seperated list of UUIDs. Got: {input}"
46+
)]
47+
ParseError {
48+
/// The environment variable name.
49+
env_var: String,
50+
/// The contents of the environment variable.
51+
input: String,
52+
},
53+
54+
/// Error loading the slot authorization configuration.
55+
#[error(transparent)]
56+
SlotAutzConfig(#[from] SlotAuthzConfigError),
57+
}
58+
59+
/// An individual builder.
60+
#[derive(Clone, Debug)]
61+
pub struct Builder {
62+
/// The sub of the builder.
63+
pub sub: String,
64+
}
65+
66+
impl Builder {
67+
/// Create a new builder.
68+
pub fn new(sub: impl AsRef<str>) -> Self {
69+
Self {
70+
sub: sub.as_ref().to_owned(),
71+
}
72+
}
73+
/// Get the sub of the builder.
74+
#[allow(clippy::missing_const_for_fn)] // false positive, non-const deref
75+
pub fn sub(&self) -> &str {
76+
&self.sub
77+
}
78+
}
79+
80+
/// Builders struct to keep track of the builders that are allowed to perform actions.
81+
#[derive(Clone, Debug)]
82+
pub struct Builders {
83+
/// The list of builders.
84+
///
85+
/// This is configured in the environment variable `PERMISSIONED_BUILDERS`,
86+
/// as a list of comma-separated UUIDs.
87+
pub builders: Vec<Builder>,
88+
89+
/// The slot authorization configuration. See [`SlotAuthzConfig`] for more
90+
/// information and env vars
91+
config: SlotAuthzConfig,
92+
}
93+
94+
impl Builders {
95+
/// Create a new Builders struct.
96+
pub const fn new(builders: Vec<Builder>, config: SlotAuthzConfig) -> Self {
97+
Self { builders, config }
98+
}
99+
100+
/// Get the calculator instance.
101+
pub const fn calc(&self) -> SlotCalculator {
102+
self.config.calc()
103+
}
104+
105+
/// Get the slot authorization configuration.
106+
pub const fn config(&self) -> &SlotAuthzConfig {
107+
&self.config
108+
}
109+
110+
/// Get the builder at a specific index.
111+
///
112+
/// # Panics
113+
///
114+
/// Panics if the index is out of bounds from the builders array.
115+
pub fn builder_at(&self, index: usize) -> &Builder {
116+
&self.builders[index]
117+
}
118+
119+
/// Get the builder permissioned at a specific timestamp.
120+
pub fn builder_at_timestamp(&self, timestamp: u64) -> &Builder {
121+
self.builder_at(self.index(timestamp) as usize)
122+
}
123+
124+
/// Get the index of the builder that is allowed to sign a block for a
125+
/// particular timestamp.
126+
pub fn index(&self, timestamp: u64) -> u64 {
127+
self.config.calc().calculate_slot(timestamp) % self.builders.len() as u64
128+
}
129+
130+
/// Get the index of the builder that is allowed to sign a block at the
131+
/// current timestamp.
132+
pub fn index_now(&self) -> u64 {
133+
self.index(now())
134+
}
135+
136+
/// Get the builder that is allowed to sign a block at the current timestamp.
137+
pub fn current_builder(&self) -> &Builder {
138+
self.builder_at(self.index_now() as usize)
139+
}
140+
141+
/// Check the query bounds for the current timestamp.
142+
fn check_query_bounds(&self) -> Result<(), BuilderPermissionError> {
143+
let current_slot_time = self.calc().current_timepoint_within_slot();
144+
if current_slot_time < self.config.block_query_start() {
145+
return Err(BuilderPermissionError::ActionAttemptTooEarly);
146+
}
147+
if current_slot_time > self.config.block_query_cutoff() {
148+
return Err(BuilderPermissionError::ActionAttemptTooLate);
149+
}
150+
Ok(())
151+
}
152+
153+
/// Checks if a builder is allowed to perform an action.
154+
/// This is based on the current timestamp and the builder's sub. It's a
155+
/// round-robin design, where each builder is allowed to perform an action
156+
/// at a specific slot, and what builder is allowed changes with each slot.
157+
pub fn is_builder_permissioned(&self, sub: &str) -> Result<(), BuilderPermissionError> {
158+
self.check_query_bounds()?;
159+
160+
if sub != self.current_builder().sub {
161+
tracing::debug!(
162+
builder = %sub,
163+
permissioned_builder = %self.current_builder().sub,
164+
"Builder not permissioned for this slot"
165+
);
166+
return Err(BuilderPermissionError::NotPermissioned);
167+
}
168+
169+
Ok(())
170+
}
171+
}
172+
173+
impl FromEnv for Builders {
174+
type Error = BuilderConfigError;
175+
176+
fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
177+
let s = String::from_env_var(BUILDERS)
178+
.map_err(FromEnvErr::infallible_into::<BuilderConfigError>)?;
179+
let builders = s.split(',').map(Builder::new).collect();
180+
181+
let config = SlotAuthzConfig::from_env().map_err(FromEnvErr::from)?;
182+
183+
Ok(Self { builders, config })
184+
}
185+
}
186+
187+
#[cfg(test)]
188+
mod test {
189+
190+
use super::*;
191+
use crate::{perms, utils::calc};
192+
193+
#[test]
194+
fn load_builders() {
195+
unsafe {
196+
std::env::set_var(BUILDERS, "0,1,2,3,4,5");
197+
198+
std::env::set_var(calc::START_TIMESTAMP, "1");
199+
std::env::set_var(calc::SLOT_OFFSET, "0");
200+
std::env::set_var(calc::SLOT_DURATION, "12");
201+
202+
std::env::set_var(perms::config::BLOCK_QUERY_START, "1");
203+
std::env::set_var(perms::config::BLOCK_QUERY_CUTOFF, "11");
204+
};
205+
206+
let builders = Builders::from_env().unwrap();
207+
assert_eq!(builders.builder_at(0).sub, "0");
208+
assert_eq!(builders.builder_at(1).sub, "1");
209+
assert_eq!(builders.builder_at(2).sub, "2");
210+
assert_eq!(builders.builder_at(3).sub, "3");
211+
assert_eq!(builders.builder_at(4).sub, "4");
212+
assert_eq!(builders.builder_at(5).sub, "5");
213+
214+
assert_eq!(builders.calc().slot_offset(), 0);
215+
assert_eq!(builders.calc().slot_duration(), 12);
216+
assert_eq!(builders.calc().start_timestamp(), 1);
217+
218+
assert_eq!(builders.config.block_query_start(), 1);
219+
assert_eq!(builders.config.block_query_cutoff(), 11);
220+
}
221+
}

src/perms/config.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use crate::utils::{
2+
calc::{SlotCalcEnvError, SlotCalculator},
3+
from_env::{FromEnv, FromEnvErr, FromEnvVar},
4+
};
5+
use core::num;
6+
7+
// Environment variable names for configuration
8+
pub(crate) const BLOCK_QUERY_CUTOFF: &str = "BLOCK_QUERY_CUTOFF";
9+
pub(crate) const BLOCK_QUERY_START: &str = "BLOCK_QUERY_START";
10+
11+
/// Possible errors when loading the slot authorization configuration.
12+
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
13+
pub enum SlotAuthzConfigError {
14+
/// Error reading environment variable.
15+
#[error("error reading chain offset: {0}")]
16+
Calculator(#[from] SlotCalcEnvError),
17+
/// Error reading block query cutoff.
18+
#[error("error reading block query cutoff: {0}")]
19+
BlockQueryCutoff(num::ParseIntError),
20+
/// Error reading block query start.
21+
#[error("error reading block query start: {0}")]
22+
BlockQueryStart(num::ParseIntError),
23+
}
24+
25+
/// Configuration object that describes the slot time settings for a chain.
26+
///
27+
/// This struct is used to configure the slot authorization system
28+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29+
pub struct SlotAuthzConfig {
30+
/// A [`SlotCalculator`] instance that can be used to calculate the slot
31+
/// number for a given timestamp.
32+
calc: SlotCalculator,
33+
/// The block query cutoff time in seconds. This is the slot second after
34+
/// which requests will not be serviced. E.g. a value of 1 means that
35+
/// requests will not be serviced for the last second of any given slot.
36+
///
37+
/// On loading from env, the number will be clamped between 0 and 11, as
38+
/// the slot duration is 12 seconds.
39+
block_query_cutoff: u8,
40+
/// The block query start time in seconds. This is the slot second before
41+
/// which requests will not be serviced. E.g. a value of 1 means that
42+
/// requests will not be serviced for the first second of any given slot.
43+
///
44+
/// On loading from env, the number will be clamped between 0 and 11, as
45+
/// the slot duration is 12 seconds.
46+
block_query_start: u8,
47+
}
48+
49+
impl SlotAuthzConfig {
50+
/// Creates a new `SlotAuthzConfig` with the given parameters, clamping the
51+
/// values between 0 and `calc.slot_duration()`.
52+
pub fn new(calc: SlotCalculator, block_query_cutoff: u8, block_query_start: u8) -> Self {
53+
Self {
54+
calc,
55+
block_query_cutoff: block_query_cutoff.clamp(0, calc.slot_duration() as u8),
56+
block_query_start: block_query_start.clamp(0, calc.slot_duration() as u8),
57+
}
58+
}
59+
60+
/// Get the slot calculator instance.
61+
pub const fn calc(&self) -> SlotCalculator {
62+
self.calc
63+
}
64+
65+
/// Get the block query cutoff time in seconds.
66+
pub const fn block_query_cutoff(&self) -> u64 {
67+
self.block_query_cutoff as u64
68+
}
69+
70+
/// Get the block query start time in seconds.
71+
pub const fn block_query_start(&self) -> u64 {
72+
self.block_query_start as u64
73+
}
74+
}
75+
76+
impl FromEnv for SlotAuthzConfig {
77+
type Error = SlotAuthzConfigError;
78+
79+
fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
80+
let calc = SlotCalculator::from_env().map_err(FromEnvErr::from)?;
81+
let block_query_cutoff = u8::from_env_var(BLOCK_QUERY_CUTOFF)
82+
.map_err(|e| e.map(SlotAuthzConfigError::BlockQueryCutoff))?
83+
.clamp(0, 11);
84+
let block_query_start = u8::from_env_var(BLOCK_QUERY_START)
85+
.map_err(|e| e.map(SlotAuthzConfigError::BlockQueryStart))?
86+
.clamp(0, 11);
87+
88+
Ok(Self {
89+
calc,
90+
block_query_cutoff,
91+
block_query_start,
92+
})
93+
}
94+
}

src/perms/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub(crate) mod builders;
2+
pub use builders::{Builder, BuilderPermissionError, Builders};
3+
4+
pub(crate) mod config;
5+
pub use config::{SlotAuthzConfig, SlotAuthzConfigError};

0 commit comments

Comments
 (0)