Skip to content

Instantly share code, notes, and snippets.

@pdaoust
Last active May 1, 2025 18:42
Show Gist options
  • Save pdaoust/30a517aa79eb8c4cb816fa71f2dd1995 to your computer and use it in GitHub Desktop.
Save pdaoust/30a517aa79eb8c4cb816fa71f2dd1995 to your computer and use it in GitHub Desktop.
A simpler approach to validation
// I'm undecided whether this could actually _contain_ the context --
// like, could EntryCrudReference be EntryCrudReference(Action, Entry)?
// We wouldn't want the whole source chain in `SourceChain`
// (tho the last entry might be helpful)
// and we don't know what's at the link base so we can't deliver that.
pub enum ValidationContext {
// Store* ops don't have any context except themselves.
// I originally named this `Record` but then realised that
// the record being validated isn't really part of the context,
// if we define 'context' as 'other stuff I have local access to'.
None,
SourceChain,
LinkBase,
LinkBaseAndCreateReference,
EntryCrudReference,
}
pub RecordWithContext {
action: Action,
entry: Option<Entry>,
context: ValidationContext,
}
pub trait SimplifyOp {
pub fn simplify() -> RecordWithContext;
}
impl SimplifyOp for Op {
pub fn simplify(&self) -> ExternResult<RecordWithContext> {
Ok(match self {
Op::StoreRecord(StoreRecord { record }) => RecordWithContext {
action: record.action(),
entry: record.entry.as_option(),
context: ValidationContext::None,
},
Op::StoreEntry(StoreEntry { action, entry }) => RecordWithContext {
action: action.into(),
entry: Some(entry),
context: ValidationContext::None,
}
Op::RegisterUpdate(RegisterUpdate { update, new_entry }) => RecordWithContext {
action: Action::Update(update.hashed.content),
entry: new_entry,
context: ValidationContext::EntryCrudReference(must_get_action(update.hashed.content.original_action_address)),
},
Op::RegisterUpdate(RegisterDelete { delete }) => RecordWithContext {
action: Action::Delete(delete.hashed.content),
entry: None,
context: ValidationContext::EntryCrudReference,
},
Op::RegisterAgentActivity(RegisterAgentActivity { action, cached_entry }) => RecordWithContext {
action: action.hashed.content,
entry: cached_entry,
context: ValidationContext::SourceChain,
},
Op::RegisterCreateLink(RegisterCreateLink { create_link }) => RecordWithContext {
action: Action::CreateLink(create_link.hashed.content),
entry: None,
context: ValidationContext::LinkBase,
},
Op::RegisterDeleteLink(RegisterDeleteLink { delete_link, create_link }) => RecordWithContext {
action: Action::DeleteLink(delete_link.hashed.content),
entry: None,
context: ValidationContext::LinkBaseAndCreateReference,
}
})
}
}
// ----------- testing out my toy code
// feels only somewhat awkward
#[hdk_extern]
pub fn validate(op: Op) -> ExternResult<ValidateCallbackResult> {
let { action, entry, context } = op.simplify();
// If I have local access to the source chain, I want to do some really basic and user-unfriendly rate limiting.
if context == ValidationContext::SourceChain {
match action {
Action::Dna(_)
| Action::AgentValidationPkg(_)
| Action::Create(Create { entry_type: EntryType::AgentPubKey, .. })
| Action::Create(Create { entry_type: EntryType::CapClaim, .. })
| Action::Create(Create { entry_type: EntryType::CapGrant, .. })
| Action::InitZomesComplete(_)
| Action::CloseChain(_)
| Action::OpenChain(_) => { /* don't rate-limit these action types */}
_ => {
let prev_action = must_get_action(action.prev_action.unwrap())?;
// This won't compile but whatever
if (action.timestamp() - prev_action.timestamp() < Duration::new(30, 0)) {
// No buckets!!! What a terrible UX!
return Ok(ValidateCallbackResult::Invalid("Went over rate limit"));
}
}
}
}
// If I have local access to the full record, I want to validate the record content.
if let Action::Create(Create { entry_type: EntryType::App(app_entry_def), .. })
| Action::Update(Update { entry_type: EntryType::App(app_entry_def), .. }) = action {
// Imaginary function that I wish we had
match app_entry_def.into_unit_entry_type<UnitEntryTypes>() {
UnitEntryTypes::Message => {
match context {
ValidationContext::None => {
// Do record validation!
// is the message 280 characters or less?
}
ValidationContext::SourceChain | ValidationContext::EntryCrudReference => { /* save work by not validating in these contexts */ }
_ => unreachable!("Link base contexts don't apply to entry CRUD");
}
}
_ => { },
};
}
// If I have local access to the entry being deleted/updated or the link being deleted,
// I want to enforce write privileges.
if context == ValidationContext::EntryCrudReference
|| context == ValidationContext::LinkBaseAndCreateReference {
if let Action::Update(Update { original_action_address: old_action_hash, .. })
|| Action::Delete(Delete { deletes_address: old_action_hash, .. })
|| Action::DeleteLink(DeleteLink { link_add_address: old_action_hash, .. }) = action {
// TODO: Do we know that this is a local get for all the above actions in the above contexts?
let old_action = must_get_action(old_action_hash)?;
// In this DNA, all CRUD must be on things written by the same author.
if (old_action.author() != action.author()) {
return Ok(ValidateCallbackResult::Invalid("Can't edit or delete other people's stuff"));
}
} else {
unreachable!("If it's a CRUD context, why would we have a non-CRUD action?");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment