use crate::{
perms::SlotAuthzConfig,
utils::{
calc::SlotCalculator,
from_env::{FromEnv, FromEnvErr, FromEnvVar},
},
};
use serde::{Deserialize, Deserializer};
fn now() -> u64 {
chrono::Utc::now().timestamp().try_into().unwrap()
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum BuilderPermissionError {
#[error("action attempt too early")]
ActionAttemptTooEarly,
#[error("action attempt too late")]
ActionAttemptTooLate,
#[error(
"builder not permissioned for this slot: requesting builder {0}, permissioned builder {1}"
)]
NotPermissioned(String, String),
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(from = "String")]
pub struct Builder {
pub sub: String,
}
impl From<String> for Builder {
fn from(sub: String) -> Self {
Self { sub }
}
}
impl Builder {
pub fn new(sub: impl AsRef<str>) -> Self {
Self {
sub: sub.as_ref().to_owned(),
}
}
#[allow(clippy::missing_const_for_fn)] pub fn sub(&self) -> &str {
&self.sub
}
}
impl FromEnvVar for Builder {
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
Ok(Self {
sub: String::from_env_var(env_var)?,
})
}
}
#[derive(Clone, Debug, serde::Deserialize, FromEnv)]
#[from_env(crate)]
pub struct Builders {
#[serde(deserialize_with = "deser_builders")]
#[from_env(
infallible,
var = "BUILDERS",
desc = "A comma-separated list of UUIDs representing the builders that are allowed to perform actions."
)]
pub builders: Vec<Builder>,
config: SlotAuthzConfig,
}
fn deser_builders<'de, D>(deser: D) -> Result<Vec<Builder>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deser)?;
Ok(split_builders(&s))
}
fn split_builders(s: &str) -> Vec<Builder> {
s.split(',').map(Builder::new).collect()
}
impl Builders {
pub const fn new(builders: Vec<Builder>, config: SlotAuthzConfig) -> Self {
Self { builders, config }
}
pub const fn calc(&self) -> SlotCalculator {
self.config.calc()
}
pub const fn config(&self) -> &SlotAuthzConfig {
&self.config
}
pub fn builder_at(&self, index: usize) -> &Builder {
&self.builders[index]
}
pub fn builder_at_timestamp(&self, timestamp: u64) -> &Builder {
self.builder_at(self.index(timestamp))
}
pub fn index(&self, timestamp: u64) -> usize {
self.config
.calc()
.slot_containing(timestamp)
.expect("host chain has started")
% self.builders.len()
}
pub fn index_now(&self) -> usize {
self.index(now())
}
pub fn current_builder(&self) -> &Builder {
self.builder_at(self.index_now())
}
fn check_query_bounds(&self) -> Result<(), BuilderPermissionError> {
let current_slot_time = self
.calc()
.current_point_within_slot()
.expect("host chain has started");
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(())
}
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(
sub.to_owned(),
self.current_builder().sub.to_owned(),
));
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn load_builders() {
unsafe {
std::env::set_var("BUILDERS", "0,1,2,3,4,5");
std::env::set_var("START_TIMESTAMP", "1");
std::env::set_var("SLOT_OFFSET", "0");
std::env::set_var("SLOT_DURATION", "12");
std::env::set_var("BLOCK_QUERY_START", "1");
std::env::set_var("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);
}
}