Skip to content

Slot authorization configuration #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name = "init4-bin-base"
description = "Internal utilities for binaries produced by the init4 team"
keywords = ["init4", "bin", "base"]

version = "0.1.3"
version = "0.1.4"
edition = "2021"
rust-version = "1.81"
authors = ["init4", "James Prestwich"]
Expand All @@ -30,6 +30,9 @@ url = "2.5.4"
metrics = "0.24.1"
metrics-exporter-prometheus = "0.16.2"

# Slot Calc
chrono = "0.4.40"

# Other
thiserror = "2.0.11"
alloy = { version = "0.12.6", optional = true, default-features = false, features = ["std"] }
Expand All @@ -44,3 +47,4 @@ tokio = { version = "1.43.0", features = ["macros"] }
[features]
default = ["alloy"]
alloy = ["dep:alloy"]
perms = []
10 changes: 8 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
#![deny(unused_must_use, rust_2018_idioms)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]

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

/// Signet utilities.
pub mod utils {
Expand All @@ -30,6 +32,10 @@ pub mod utils {

/// Tracing utilities.
pub mod tracing;

/// Slot calculator for determining the current slot and timepoint within a
/// slot.
pub mod calc;
}

/// Re-exports of common dependencies.
Expand Down Expand Up @@ -64,7 +70,7 @@ pub mod deps {
///
/// [`init_tracing`]: utils::tracing::init_tracing
/// [`init_metrics`]: utils::metrics::init_metrics
pub fn init4() -> Option<OtelGuard> {
pub fn init4() -> Option<utils::otlp::OtelGuard> {
let guard = utils::tracing::init_tracing();
utils::metrics::init_metrics();
guard
Expand Down
221 changes: 221 additions & 0 deletions src/perms/builders.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
//! #Signet Quincey builder permissioning system.
//!
//! The permissioning system decides which builder can perform a certain action
//! at a given time. The permissioning system uses a simple round-robin design,
//! where each builder is allowed to perform an action at a specific slot.
//! Builders are permissioned based on their sub, which is present in the JWT
//! token they acquire from our OAuth service.

use crate::{
perms::{SlotAuthzConfig, SlotAuthzConfigError},
utils::{
calc::SlotCalculator,
from_env::{FromEnv, FromEnvErr, FromEnvVar},
},
};

/// The builder list env var.
const BUILDERS: &str = "PERMISSIONED_BUILDERS";

fn now() -> u64 {
chrono::Utc::now().timestamp().try_into().unwrap()
}

/// Possible errors when permissioning a builder.
#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
pub enum BuilderPermissionError {
/// Action attempt too early.
#[error("action attempt too early")]
ActionAttemptTooEarly,

/// Action attempt too late.
#[error("action attempt too late")]
ActionAttemptTooLate,

/// Builder not permissioned for this slot.
#[error("builder not permissioned for this slot")]
NotPermissioned,
}

/// Possible errors when loading the builder configuration.
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum BuilderConfigError {
/// Error loading the environment variable.
#[error(
"failed to parse environment variable. Expected a comma-seperated list of UUIDs. Got: {input}"
)]
ParseError {
/// The environment variable name.
env_var: String,
/// The contents of the environment variable.
input: String,
},

/// Error loading the slot authorization configuration.
#[error(transparent)]
SlotAutzConfig(#[from] SlotAuthzConfigError),
}

/// An individual builder.
#[derive(Clone, Debug)]
pub struct Builder {
/// The sub of the builder.
pub sub: String,
}

impl Builder {
/// Create a new builder.
pub fn new(sub: impl AsRef<str>) -> Self {
Self {
sub: sub.as_ref().to_owned(),
}
}
/// Get the sub of the builder.
#[allow(clippy::missing_const_for_fn)] // false positive, non-const deref
pub fn sub(&self) -> &str {
&self.sub
}
}

/// Builders struct to keep track of the builders that are allowed to perform actions.
#[derive(Clone, Debug)]
pub struct Builders {
/// The list of builders.
///
/// This is configured in the environment variable `PERMISSIONED_BUILDERS`,
/// as a list of comma-separated UUIDs.
pub builders: Vec<Builder>,

/// The slot authorization configuration. See [`SlotAuthzConfig`] for more
/// information and env vars
config: SlotAuthzConfig,
}

impl Builders {
/// Create a new Builders struct.
pub const fn new(builders: Vec<Builder>, config: SlotAuthzConfig) -> Self {
Self { builders, config }
}

/// Get the calculator instance.
pub const fn calc(&self) -> SlotCalculator {
self.config.calc()
}

/// Get the slot authorization configuration.
pub const fn config(&self) -> &SlotAuthzConfig {
&self.config
}

/// Get the builder at a specific index.
///
/// # Panics
///
/// Panics if the index is out of bounds from the builders array.
pub fn builder_at(&self, index: usize) -> &Builder {
&self.builders[index]
}

/// Get the builder permissioned at a specific timestamp.
pub fn builder_at_timestamp(&self, timestamp: u64) -> &Builder {
self.builder_at(self.index(timestamp) as usize)
}

/// Get the index of the builder that is allowed to sign a block for a
/// particular timestamp.
pub fn index(&self, timestamp: u64) -> u64 {
self.config.calc().calculate_slot(timestamp) % self.builders.len() as u64
}

/// Get the index of the builder that is allowed to sign a block at the
/// current timestamp.
pub fn index_now(&self) -> u64 {
self.index(now())
}

/// Get the builder that is allowed to sign a block at the current timestamp.
pub fn current_builder(&self) -> &Builder {
self.builder_at(self.index_now() as usize)
}

/// Check the query bounds for the current timestamp.
fn check_query_bounds(&self) -> Result<(), BuilderPermissionError> {
let current_slot_time = self.calc().current_timepoint_within_slot();
if current_slot_time < self.config.block_query_start() {
return Err(BuilderPermissionError::ActionAttemptTooEarly);
}
if current_slot_time > self.config.block_query_cutoff() {
return Err(BuilderPermissionError::ActionAttemptTooLate);
}
Ok(())
}

/// Checks if a builder is allowed to perform an action.
/// This is based on the current timestamp and the builder's sub. It's a
/// round-robin design, where each builder is allowed to perform an action
/// at a specific slot, and what builder is allowed changes with each slot.
pub fn is_builder_permissioned(&self, sub: &str) -> Result<(), BuilderPermissionError> {
self.check_query_bounds()?;

if sub != self.current_builder().sub {
tracing::debug!(
builder = %sub,
permissioned_builder = %self.current_builder().sub,
"Builder not permissioned for this slot"
);
return Err(BuilderPermissionError::NotPermissioned);
}

Ok(())
}
}

impl FromEnv for Builders {
type Error = BuilderConfigError;

fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
let s = String::from_env_var(BUILDERS)
.map_err(FromEnvErr::infallible_into::<BuilderConfigError>)?;
let builders = s.split(',').map(Builder::new).collect();

let config = SlotAuthzConfig::from_env().map_err(FromEnvErr::from)?;

Ok(Self { builders, config })
}
}

#[cfg(test)]
mod test {

use super::*;
use crate::{perms, utils::calc};

#[test]
fn load_builders() {
unsafe {
std::env::set_var(BUILDERS, "0,1,2,3,4,5");

std::env::set_var(calc::START_TIMESTAMP, "1");
std::env::set_var(calc::SLOT_OFFSET, "0");
std::env::set_var(calc::SLOT_DURATION, "12");

std::env::set_var(perms::config::BLOCK_QUERY_START, "1");
std::env::set_var(perms::config::BLOCK_QUERY_CUTOFF, "11");
};

let builders = Builders::from_env().unwrap();
assert_eq!(builders.builder_at(0).sub, "0");
assert_eq!(builders.builder_at(1).sub, "1");
assert_eq!(builders.builder_at(2).sub, "2");
assert_eq!(builders.builder_at(3).sub, "3");
assert_eq!(builders.builder_at(4).sub, "4");
assert_eq!(builders.builder_at(5).sub, "5");

assert_eq!(builders.calc().slot_offset(), 0);
assert_eq!(builders.calc().slot_duration(), 12);
assert_eq!(builders.calc().start_timestamp(), 1);

assert_eq!(builders.config.block_query_start(), 1);
assert_eq!(builders.config.block_query_cutoff(), 11);
}
}
94 changes: 94 additions & 0 deletions src/perms/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::utils::{
calc::{SlotCalcEnvError, SlotCalculator},
from_env::{FromEnv, FromEnvErr, FromEnvVar},
};
use core::num;

// Environment variable names for configuration
pub(crate) const BLOCK_QUERY_CUTOFF: &str = "BLOCK_QUERY_CUTOFF";
pub(crate) const BLOCK_QUERY_START: &str = "BLOCK_QUERY_START";

/// Possible errors when loading the slot authorization configuration.
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum SlotAuthzConfigError {
/// Error reading environment variable.
#[error("error reading chain offset: {0}")]
Calculator(#[from] SlotCalcEnvError),
/// Error reading block query cutoff.
#[error("error reading block query cutoff: {0}")]
BlockQueryCutoff(num::ParseIntError),
/// Error reading block query start.
#[error("error reading block query start: {0}")]
BlockQueryStart(num::ParseIntError),
}

/// Configuration object that describes the slot time settings for a chain.
///
/// This struct is used to configure the slot authorization system
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlotAuthzConfig {
/// A [`SlotCalculator`] instance that can be used to calculate the slot
/// number for a given timestamp.
calc: SlotCalculator,
/// The block query cutoff time in seconds. This is the slot second after
/// which requests will not be serviced. E.g. a value of 1 means that
/// requests will not be serviced for the last second of any given slot.
///
/// On loading from env, the number will be clamped between 0 and 11, as
/// the slot duration is 12 seconds.
block_query_cutoff: u8,
/// The block query start time in seconds. This is the slot second before
/// which requests will not be serviced. E.g. a value of 1 means that
/// requests will not be serviced for the first second of any given slot.
///
/// On loading from env, the number will be clamped between 0 and 11, as
/// the slot duration is 12 seconds.
block_query_start: u8,
}

impl SlotAuthzConfig {
/// Creates a new `SlotAuthzConfig` with the given parameters, clamping the
/// values between 0 and `calc.slot_duration()`.
pub fn new(calc: SlotCalculator, block_query_cutoff: u8, block_query_start: u8) -> Self {
Self {
calc,
block_query_cutoff: block_query_cutoff.clamp(0, calc.slot_duration() as u8),
block_query_start: block_query_start.clamp(0, calc.slot_duration() as u8),
}
}

/// Get the slot calculator instance.
pub const fn calc(&self) -> SlotCalculator {
self.calc
}

/// Get the block query cutoff time in seconds.
pub const fn block_query_cutoff(&self) -> u64 {
self.block_query_cutoff as u64
}

/// Get the block query start time in seconds.
pub const fn block_query_start(&self) -> u64 {
self.block_query_start as u64
}
}

impl FromEnv for SlotAuthzConfig {
type Error = SlotAuthzConfigError;

fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
let calc = SlotCalculator::from_env().map_err(FromEnvErr::from)?;
let block_query_cutoff = u8::from_env_var(BLOCK_QUERY_CUTOFF)
.map_err(|e| e.map(SlotAuthzConfigError::BlockQueryCutoff))?
.clamp(0, 11);
let block_query_start = u8::from_env_var(BLOCK_QUERY_START)
.map_err(|e| e.map(SlotAuthzConfigError::BlockQueryStart))?
.clamp(0, 11);

Ok(Self {
calc,
block_query_cutoff,
block_query_start,
})
}
}
5 changes: 5 additions & 0 deletions src/perms/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub(crate) mod builders;
pub use builders::{Builder, BuilderPermissionError, Builders};

pub(crate) mod config;
pub use config::{SlotAuthzConfig, SlotAuthzConfigError};
Loading