lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

actor.rs (8359B)


      1 //! Actor provenance and authorization context.
      2 //!
      3 //! This module binds identity-owned public values to event-owned author roles.
      4 //! It describes host selection and provenance but does not select accounts,
      5 //! acquire keys, or prove that a host granted a role.
      6 
      7 use radroots_event::contract::AuthorRole;
      8 use radroots_identity::{AccountId, PublicKey};
      9 
     10 use crate::{Error, error::Kind};
     11 
     12 #[cfg(not(feature = "std"))]
     13 use alloc::collections::BTreeSet;
     14 #[cfg(feature = "std")]
     15 use std::collections::BTreeSet;
     16 
     17 /// Why a host supplied an actor to a signing operation.
     18 ///
     19 /// Account-backed variants carry the canonical public [`AccountId`]. The
     20 /// account identifier must represent the same public bytes as the actor key.
     21 #[non_exhaustive]
     22 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     23 pub enum ActorSource {
     24     /// The host resolved its explicitly selected local account.
     25     LocalAccount(AccountId),
     26     /// The caller supplied a public key without selecting an account.
     27     ExplicitPublicKey,
     28     /// The host resolved an account backed by a remote signer.
     29     RemoteSigner(AccountId),
     30     /// A service account was selected by explicit host policy.
     31     Service(AccountId),
     32 }
     33 
     34 impl ActorSource {
     35     /// Returns the selected account identifier for account-backed provenance.
     36     #[must_use]
     37     pub const fn account_id(self) -> Option<AccountId> {
     38         match self {
     39             Self::LocalAccount(account_id)
     40             | Self::RemoteSigner(account_id)
     41             | Self::Service(account_id) => Some(account_id),
     42             Self::ExplicitPublicKey => None,
     43         }
     44     }
     45 }
     46 
     47 /// Declarative host selection for resolving an actor.
     48 ///
     49 /// Resolution is deliberately outside this package; this value carries no
     50 /// database, keyring, UI, or process-global selection authority.
     51 #[non_exhaustive]
     52 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     53 pub enum ActorSelector {
     54     /// Resolve the account already selected by explicit host policy.
     55     SelectedAccount,
     56     /// Resolve one canonical public account.
     57     Account(AccountId),
     58     /// Use one explicit canonical public key.
     59     PublicKey(PublicKey),
     60     /// Resolve the public key frozen into the authored event plan.
     61     PlanAuthorPublicKey,
     62 }
     63 
     64 /// Inputs a host must satisfy when resolving an actor for an authored plan.
     65 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     66 pub struct ActorResolutionRequest {
     67     selector: ActorSelector,
     68     required_role: AuthorRole,
     69     expected_public_key: PublicKey,
     70 }
     71 
     72 impl ActorResolutionRequest {
     73     /// Creates an actor-resolution request from canonical public values.
     74     #[must_use]
     75     pub const fn new(
     76         selector: ActorSelector,
     77         required_role: AuthorRole,
     78         expected_public_key: PublicKey,
     79     ) -> Self {
     80         Self {
     81             selector,
     82             required_role,
     83             expected_public_key,
     84         }
     85     }
     86 
     87     /// Returns the host-owned selection instruction.
     88     #[must_use]
     89     pub const fn selector(&self) -> ActorSelector {
     90         self.selector
     91     }
     92 
     93     /// Returns the event-contract role the resolved actor must hold.
     94     #[must_use]
     95     pub const fn required_role(&self) -> AuthorRole {
     96         self.required_role
     97     }
     98 
     99     /// Returns the exact public key frozen into the plan.
    100     #[must_use]
    101     pub const fn expected_public_key(&self) -> PublicKey {
    102         self.expected_public_key
    103     }
    104 }
    105 
    106 /// An actor-role claim and its public provenance.
    107 ///
    108 /// Construction validates that account-backed provenance and the public key
    109 /// describe the same identity. Role authorization against an event contract
    110 /// is performed by the signing boundary before invoking a signer.
    111 #[derive(Clone, Debug, PartialEq, Eq)]
    112 pub struct Actor {
    113     public_key: PublicKey,
    114     roles: BTreeSet<AuthorRole>,
    115     source: ActorSource,
    116 }
    117 
    118 impl Actor {
    119     /// Creates actor provenance from canonical public values.
    120     pub fn new<I>(public_key: PublicKey, source: ActorSource, roles: I) -> Result<Self, Error>
    121     where
    122         I: IntoIterator<Item = AuthorRole>,
    123     {
    124         if let Some(account_id) = source.account_id()
    125             && account_id.as_bytes() != public_key.as_bytes()
    126         {
    127             return Err(Error::new(Kind::InvalidArgument));
    128         }
    129         Ok(Self {
    130             public_key,
    131             roles: roles.into_iter().collect(),
    132             source,
    133         })
    134     }
    135 
    136     /// Parses a canonical public key and creates validated actor provenance.
    137     pub fn from_public_key_hex<I>(
    138         public_key: &str,
    139         source: ActorSource,
    140         roles: I,
    141     ) -> Result<Self, Error>
    142     where
    143         I: IntoIterator<Item = AuthorRole>,
    144     {
    145         let public_key =
    146             PublicKey::from_hex(public_key).map_err(|_| Error::new(Kind::InvalidArgument))?;
    147         Self::new(public_key, source, roles)
    148     }
    149 
    150     /// Returns the canonical expected author key.
    151     #[must_use]
    152     pub const fn public_key(&self) -> PublicKey {
    153         self.public_key
    154     }
    155 
    156     /// Returns the claimed event-author roles.
    157     #[must_use]
    158     pub const fn roles(&self) -> &BTreeSet<AuthorRole> {
    159         &self.roles
    160     }
    161 
    162     /// Returns the host-supplied actor provenance.
    163     #[must_use]
    164     pub const fn source(&self) -> ActorSource {
    165         self.source
    166     }
    167 
    168     /// Returns the selected public account, when provenance is account-backed.
    169     #[must_use]
    170     pub const fn account_id(&self) -> Option<AccountId> {
    171         self.source.account_id()
    172     }
    173 
    174     /// Reports whether the actor claims the required event-author role.
    175     #[must_use]
    176     pub fn satisfies(&self, required_role: AuthorRole) -> bool {
    177         required_role == AuthorRole::Any || self.roles.contains(&required_role)
    178     }
    179 }
    180 
    181 #[cfg(test)]
    182 mod tests {
    183     use super::*;
    184 
    185     const ALICE: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
    186     const BOB: &str = "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
    187 
    188     fn public_key(value: &str) -> PublicKey {
    189         PublicKey::from_hex(value).expect("valid public key")
    190     }
    191 
    192     fn account_id(value: &str) -> AccountId {
    193         AccountId::from_hex(value).expect("valid account ID")
    194     }
    195 
    196     #[test]
    197     fn roles_and_explicit_provenance_are_preserved() {
    198         let actor = Actor::new(
    199             public_key(ALICE),
    200             ActorSource::ExplicitPublicKey,
    201             [AuthorRole::Farmer, AuthorRole::Seller],
    202         )
    203         .expect("actor");
    204 
    205         assert_eq!(actor.public_key(), public_key(ALICE));
    206         assert_eq!(actor.source(), ActorSource::ExplicitPublicKey);
    207         assert_eq!(actor.account_id(), None);
    208         assert!(actor.satisfies(AuthorRole::Any));
    209         assert!(actor.satisfies(AuthorRole::Farmer));
    210         assert!(actor.satisfies(AuthorRole::Seller));
    211         assert!(!actor.satisfies(AuthorRole::Buyer));
    212     }
    213 
    214     #[test]
    215     fn account_provenance_requires_the_same_public_identity() {
    216         for source in [
    217             ActorSource::LocalAccount(account_id(ALICE)),
    218             ActorSource::RemoteSigner(account_id(ALICE)),
    219             ActorSource::Service(account_id(ALICE)),
    220         ] {
    221             let actor = Actor::new(public_key(ALICE), source, [AuthorRole::Service])
    222                 .expect("matching account provenance");
    223             assert_eq!(actor.account_id(), Some(account_id(ALICE)));
    224         }
    225 
    226         let error = Actor::new(
    227             public_key(ALICE),
    228             ActorSource::LocalAccount(account_id(BOB)),
    229             [AuthorRole::Farmer],
    230         )
    231         .expect_err("mismatched account must fail");
    232         assert_eq!(error.kind(), Kind::InvalidArgument);
    233     }
    234 
    235     #[test]
    236     fn invalid_public_key_text_is_rejected() {
    237         let error =
    238             Actor::from_public_key_hex("not-a-public-key", ActorSource::ExplicitPublicKey, [])
    239                 .expect_err("invalid public key must fail");
    240         assert_eq!(error.kind(), Kind::InvalidArgument);
    241     }
    242 
    243     #[test]
    244     fn resolution_request_preserves_selection_role_and_expected_key() {
    245         let request = ActorResolutionRequest::new(
    246             ActorSelector::Account(account_id(ALICE)),
    247             AuthorRole::Seller,
    248             public_key(ALICE),
    249         );
    250 
    251         assert_eq!(
    252             request.selector(),
    253             ActorSelector::Account(account_id(ALICE))
    254         );
    255         assert_eq!(request.required_role(), AuthorRole::Seller);
    256         assert_eq!(request.expected_public_key(), public_key(ALICE));
    257     }
    258 }