use crate::identity::IdentityType;
use crate::primitives::{Address, BlockHeight, Hash};
use serde::{Deserialize, Serialize};
pub const MAX_DELEGATION_DEPTH: u8 = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PrincipalRole {
Controller,
DelegatedAgent,
AutonomousAgent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrincipalLink {
pub did: String,
pub identity_type: IdentityType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delegation_scope_id: Option<Hash>,
pub role: PrincipalRole,
#[serde(default, skip_serializing_if = "is_false")]
pub tombstone: bool,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
!b
}
impl PrincipalLink {
pub fn new(
did: impl Into<String>,
identity_type: IdentityType,
delegation_scope_id: Option<Hash>,
role: PrincipalRole,
) -> Self {
Self {
did: did.into(),
identity_type,
delegation_scope_id,
role,
tombstone: false,
}
}
pub fn tombstoned(did: impl Into<String>, role: PrincipalRole) -> Self {
Self {
did: did.into(),
identity_type: IdentityType::Machine,
delegation_scope_id: None,
role,
tombstone: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrincipalChain {
pub actor: String,
pub chain: Vec<PrincipalLink>,
pub controller: PrincipalLink,
pub controller_kyc_tier: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub controller_bond: Option<u128>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actor_bond: Option<u128>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub controller_bond_aggregate: Option<u128>,
pub delegation_depth: u8,
pub frozen_at_block: BlockHeight,
}
impl PrincipalChain {
pub fn direct(
actor_did: impl Into<String>,
identity_type: IdentityType,
kyc_tier: u8,
bond: Option<u128>,
block: impl Into<BlockHeight>,
) -> Self {
let did = actor_did.into();
let role = match identity_type {
IdentityType::Human => PrincipalRole::Controller,
IdentityType::Machine => PrincipalRole::AutonomousAgent,
};
let controller = PrincipalLink::new(did.clone(), identity_type, None, role);
Self {
actor: did,
chain: Vec::new(),
controller,
controller_kyc_tier: kyc_tier,
controller_bond: bond,
actor_bond: None,
controller_bond_aggregate: None,
delegation_depth: 0,
frozen_at_block: block.into(),
}
}
pub fn from_chain(
actor_did: impl Into<String>,
chain: Vec<PrincipalLink>,
kyc_tier: u8,
bond: Option<u128>,
block: impl Into<BlockHeight>,
) -> Self {
debug_assert!(!chain.is_empty(), "use PrincipalChain::direct for empty chain");
debug_assert!(
chain.len() <= MAX_DELEGATION_DEPTH as usize,
"chain exceeds MAX_DELEGATION_DEPTH"
);
let controller = chain[0].clone();
let depth = chain.len() as u8;
Self {
actor: actor_did.into(),
chain,
controller,
controller_kyc_tier: kyc_tier,
controller_bond: bond,
actor_bond: None,
controller_bond_aggregate: None,
delegation_depth: depth,
frozen_at_block: block.into(),
}
}
pub fn with_actor_bond(mut self, bond: Option<u128>) -> Self {
self.actor_bond = bond;
self
}
pub fn with_controller_bond_aggregate(mut self, aggregate: Option<u128>) -> Self {
self.controller_bond_aggregate = aggregate;
self
}
pub fn controller_did(&self) -> &str {
&self.controller.did
}
pub fn has_tombstone(&self) -> bool {
self.controller.tombstone || self.chain.iter().any(|l| l.tombstone)
}
pub fn summary(&self) -> PrincipalChainSummary {
PrincipalChainSummary {
actor: self.actor.clone(),
controller: self.controller.did.clone(),
controller_kyc_tier: self.controller_kyc_tier,
controller_bond: self.controller_bond,
actor_bond: self.actor_bond,
controller_bond_aggregate: self.controller_bond_aggregate,
delegation_depth: self.delegation_depth,
has_tombstone: self.has_tombstone(),
frozen_at_block: self.frozen_at_block,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrincipalChainSummary {
pub actor: String,
pub controller: String,
pub controller_kyc_tier: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub controller_bond: Option<u128>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actor_bond: Option<u128>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub controller_bond_aggregate: Option<u128>,
pub delegation_depth: u8,
pub has_tombstone: bool,
pub frozen_at_block: BlockHeight,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControllerActivitySummary {
pub controller_did: String,
pub receipt_count: u64,
pub total_value_wei: u128,
pub agents_acted_under: Vec<String>,
pub kill_switch_events: u32,
pub kyc_tier_at_oldest: u8,
pub kyc_tier_at_newest: u8,
pub bond_min: u128,
pub bond_max: u128,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub until: Option<u64>,
}
pub trait BondLookup: Send + Sync {
fn actor_bond(&self, did: &str) -> Option<u128>;
fn controller_aggregate(&self, controller_did: &str) -> Option<u128>;
}
pub trait PrincipalChainResolver: Send + Sync {
fn resolve_by_did(&self, did: &str, frozen_at_block: BlockHeight) -> PrincipalChain;
fn resolve_by_address(
&self,
address: &Address,
frozen_at_block: BlockHeight,
) -> PrincipalChain;
}
pub const ANONYMOUS_DID_PREFIX: &str = "did:tenzro:anonymous:";
pub fn anonymous_chain_for_address(
address: &Address,
frozen_at_block: impl Into<BlockHeight>,
) -> PrincipalChain {
let did = format!("{}{}", ANONYMOUS_DID_PREFIX, hex::encode(address.as_bytes()));
let mut pc = PrincipalChain::direct(
did,
IdentityType::Machine,
0,
None,
frozen_at_block,
);
pc.controller.tombstone = true;
pc
}
pub fn anonymous_chain_for_did(
did: impl Into<String>,
frozen_at_block: impl Into<BlockHeight>,
) -> PrincipalChain {
let mut pc = PrincipalChain::direct(
did,
IdentityType::Machine,
0,
None,
frozen_at_block,
);
pc.controller.tombstone = true;
pc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn direct_controller_action_yields_empty_chain() {
let pc = PrincipalChain::direct(
"did:tenzro:human:alice:uuid",
IdentityType::Human,
2,
Some(10_000_000_000_000_000_000_000u128),
184_523,
);
assert_eq!(pc.delegation_depth, 0);
assert!(pc.chain.is_empty());
assert_eq!(pc.actor, pc.controller.did);
assert_eq!(pc.controller.role, PrincipalRole::Controller);
}
#[test]
fn machine_direct_action_uses_autonomous_role() {
let pc = PrincipalChain::direct(
"did:tenzro:machine:bot:uuid",
IdentityType::Machine,
0,
None,
10,
);
assert_eq!(pc.controller.role, PrincipalRole::AutonomousAgent);
}
#[test]
fn nested_chain_records_full_path() {
let chain = vec![
PrincipalLink::new(
"did:tenzro:human:alice:uuid",
IdentityType::Human,
None,
PrincipalRole::Controller,
),
PrincipalLink::new(
"did:tenzro:machine:alicebot:uuid",
IdentityType::Machine,
Some(Hash::zero()),
PrincipalRole::DelegatedAgent,
),
];
let pc = PrincipalChain::from_chain(
"did:tenzro:machine:bot42:uuid",
chain,
2,
Some(1_000_000_000),
42,
);
assert_eq!(pc.delegation_depth, 2);
assert_eq!(pc.controller_did(), "did:tenzro:human:alice:uuid");
assert!(!pc.has_tombstone());
}
#[test]
fn tombstoned_link_is_detected() {
let chain = vec![
PrincipalLink::new(
"did:tenzro:human:alice:uuid",
IdentityType::Human,
None,
PrincipalRole::Controller,
),
PrincipalLink::tombstoned(
"did:tenzro:machine:revoked:uuid",
PrincipalRole::DelegatedAgent,
),
];
let pc = PrincipalChain::from_chain(
"did:tenzro:machine:actor:uuid",
chain,
2,
None,
1,
);
assert!(pc.has_tombstone());
}
#[test]
fn summary_round_trip() {
let pc = PrincipalChain::direct(
"did:tenzro:human:alice:uuid",
IdentityType::Human,
3,
Some(50),
5,
);
let s = pc.summary();
assert_eq!(s.controller, pc.controller_did());
assert_eq!(s.delegation_depth, 0);
assert_eq!(s.controller_kyc_tier, 3);
}
#[test]
fn spec9_bond_fields_default_to_none() {
let pc = PrincipalChain::direct(
"did:tenzro:human:alice:uuid",
IdentityType::Human,
2,
None,
10,
);
assert!(pc.actor_bond.is_none());
assert!(pc.controller_bond_aggregate.is_none());
}
#[test]
fn spec9_bond_fields_settable_via_builder() {
let pc = PrincipalChain::direct(
"did:tenzro:machine:bot:uuid",
IdentityType::Machine,
0,
None,
100,
)
.with_actor_bond(Some(5_000_000_000_000_000_000u128))
.with_controller_bond_aggregate(Some(25_000_000_000_000_000_000u128));
assert_eq!(pc.actor_bond, Some(5_000_000_000_000_000_000u128));
assert_eq!(
pc.controller_bond_aggregate,
Some(25_000_000_000_000_000_000u128)
);
let s = pc.summary();
assert_eq!(s.actor_bond, pc.actor_bond);
assert_eq!(s.controller_bond_aggregate, pc.controller_bond_aggregate);
}
}