Repository navigation
Validate transition against schema at creation time #1
Description
Activity
Hi! Such check would detect programming errors that could lead to the creation of an invalid transition and hence consignment. It's a nice to have but not a top priority task. As a workaround, a sender could validate the consignment before sending it to the receiver and broadcasting the TX.
- changed the title
[-]Question about how to validate transition[/-][+]Validate transition against schema at creation time[/+]on Aug 11, 2025 Hi Team, I made a late implementation about
validate transition against schemahere, and I will post the code with the principal here. Then your team can check if it makes sense from theory.What I have done:
- I added an explicit schema‑validation pass inside TransitionBuilder::complete_transition.
- The validation mirrors the schema logic used during consignment validation:
2.1 Metadata types and required presence
2.2 Global state types, occurrence constraints, and semantic type validation
2.3 Input (prev state) occurrence constraints
2.4 New assignment occurrence constraints + per‑assignment data validation
2.This is a structural/schema‑only check (no witness, no VM execution), which matches the intent of the TODO: catch programming errors that would produce an invalid transition/consignment.
Changes:
complete_transitionnow builds prev_state from inputs, constructs the Transition, and validates it against the schema + types.- Added helper functions inside
TransitionBuilder:
2.1prev_state_from_inputs
2.2validate_transition_against_schema
Notes:
This check uses existing schema types andOwnedStateSchema::validateto keep logic consistent with consensus validation.
It does not require witness data or VM execution, which is intentional for a builder‑level guard.pub fn complete_transition(self) -> Result<Transition, BuilderError> { let prev_state = Self::prev_state_from_inputs(&self.inputs); let (schema, global, assignments, types, metadata) = self.builder.complete(); let transition = Transition { ffv: none!(), contract_id: self.contract_id, nonce: self.nonce, transition_type: self.transition_type, metadata, globals: global, inputs: NonEmptyOrdSet::from_iter_checked(self.inputs.into_keys()).into(), assignments, signature: none!(), }; Self::validate_transition_against_schema(&schema, &types, &transition, &prev_state)?; Ok(transition) } fn prev_state_from_inputs( inputs: &TinyOrdMap<Opout, AllocatedState>, ) -> BTreeMap<AssignmentType, Vec<RevealedState>> { let mut prev_state = BTreeMap::<AssignmentType, Vec<RevealedState>>::new(); for (opout, state) in inputs.iter() { let revealed = match state { AllocatedState::Void => RevealedState::Void, AllocatedState::Amount(value) => RevealedState::Fungible(*value), AllocatedState::Data(data) => RevealedState::Structured(data.clone()), }; prev_state.entry(opout.ty).or_default().push(revealed); } prev_state } fn validate_transition_against_schema( schema: &Schema, types: &TypeSystem, transition: &Transition, prev_state: &BTreeMap<AssignmentType, Vec<RevealedState>>, ) -> Result<(), ValidationError> { let opid = transition.id(); let transition_schema = match schema.transitions.get(&transition.transition_type) { None => { return Err(ValidationError::InvalidConsignment( Failure::SchemaUnknownTransitionType(opid, transition.transition_type), )) } Some(details) => &details.transition_schema, }; // Validate metadata against schema. for type_id in transition.metadata.keys().copied() { if !transition_schema.metadata.as_unconfined().contains(&type_id) { return Err(ValidationError::InvalidConsignment(Failure::SchemaUnknownMetaType( opid, type_id, ))); } } for type_id in &transition_schema.metadata { let Some(value) = transition.metadata.get(type_id) else { return Err(ValidationError::InvalidConsignment(Failure::SchemaNoMetadata( opid, *type_id, ))); }; let sem_id = schema .meta_types .get(type_id) .expect("schema metadata types are internally consistent") .sem_id; if types.strict_deserialize_type(sem_id, value.as_ref()).is_err() { return Err(ValidationError::InvalidConsignment( Failure::SchemaInvalidMetadata(opid, sem_id), )); } } // Validate global state against schema. for field_id in transition.globals.keys() { if !transition_schema.globals.contains_key(field_id) { return Err(ValidationError::InvalidConsignment( Failure::SchemaUnknownGlobalStateType(opid, *field_id), )); } } for (type_id, occ) in &transition_schema.globals { let set = transition .globals .get(type_id) .cloned() .map(GlobalValues::into_inner) .map(Confined::release) .unwrap_or_default(); let GlobalStateSchema { sem_id, max_items } = schema .global_types .get(type_id) .expect("schema global types are internally consistent") .global_state_schema; let count = set.len() as u16; if let Err(err) = occ.check(count) { return Err(ValidationError::InvalidConsignment( Failure::SchemaGlobalStateOccurrences(opid, *type_id, err), )); } if count as u32 > max_items.to_u32() { return Err(ValidationError::InvalidConsignment( Failure::SchemaGlobalStateLimit(opid, *type_id, count, max_items), )); } for data in set { if types.strict_deserialize_type(sem_id, data.as_ref()).is_err() { return Err(ValidationError::InvalidConsignment( Failure::SchemaInvalidGlobalValue(opid, *type_id, sem_id), )); } } } // Validate input (prev) state occurrences. let type_ids = prev_state .keys() .chain(transition_schema.inputs.keys()) .collect::<HashSet<_>>(); for type_id in type_ids { let Some(schema_occurrences) = transition_schema.inputs.get(type_id) else { return Err(ValidationError::InvalidConsignment( Failure::SchemaUnknownAssignmentType(opid, *type_id), )); }; let len = prev_state.get(type_id).map(|a| a.len() as u16).unwrap_or(0); if let Err(err) = schema_occurrences.check(len) { return Err(ValidationError::InvalidConsignment( Failure::SchemaInputOccurrences(opid, *type_id, err), )); } } // Validate new assignments against schema. let type_ids = transition .assignments .keys() .chain(transition_schema.assignments.keys()) .collect::<HashSet<_>>(); for type_id in type_ids { let Some(schema_occurrences) = transition_schema.assignments.get(type_id) else { return Err(ValidationError::InvalidConsignment( Failure::SchemaUnknownAssignmentType(opid, *type_id), )); }; let len = transition .assignments .get(type_id) .map(TypedAssigns::len_u16) .unwrap_or(0); if let Err(err) = schema_occurrences.check(len) { return Err(ValidationError::InvalidConsignment( Failure::SchemaAssignmentOccurrences(opid, *type_id, err), )); } let assignment = &schema .owned_types .get(type_id) .expect("schema owned types are internally consistent") .owned_state_schema; match transition.assignments.get(type_id) { None => {} Some(TypedAssigns::Declarative(set)) => set .iter() .try_for_each(|data| assignment.validate(opid, *type_id, data, types))?, Some(TypedAssigns::Fungible(set)) => set .iter() .try_for_each(|data| assignment.validate(opid, *type_id, data, types))?, Some(TypedAssigns::Structured(set)) => set .iter() .try_for_each(|data| assignment.validate(opid, *type_id, data, types))?, } } Ok(()) }
And I used ValidationError::InvalidConsignment here, because we have all consignment-level validations.
I don't know whether there is a need for a new ValidationError::InvalidTransition.Hi, thanks for the proposal.
I think it's unacceptable to duplicate part of the validation logic (which is part of consensus code) into
rgb-ops. Either we can reuse the code that is already implemented inrgb-consensusvalidation, or it's actually better to not have any kind of validation here. It's almost impossible to guarantee that the two validation procedures are aligned, especially if they span across two different repositories. So this would lead to a number of false positives and false negatives, which makes it more harmful than useful.Exploiting
rgb-consensuscode here is probably no easy task, which is why this is still aTODOand we have not done it yet, and the benefit is marginal. While you can propose a PR that does that, keep in mind that it's very low priority for us.Reacted by Jack Choi
Hi there, I have a question about how to validate the transition in
complete_transition()method all the time, and here is a TODO// TODO: Validate against schema.Do you have any plans or roadmaps to implement it?