Skip to content

Validate transition against schema at creation time #1

Description

@cymqqqq

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?

Activity

  1. zoedberg commented on Aug 11, 2025

    @zoedberg
    Member

    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.

  2. changed the title [-]Question about how to validate transition[/-] [+]Validate transition against schema at creation time[/+] on Aug 11, 2025
  3. cymqqqq commented on Mar 12, 2026

    @cymqqqq
    Author

    Hi Team, I made a late implementation about validate transition against schema here, 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:

    1. I added an explicit schema‑validation pass inside TransitionBuilder::complete_transition.
    2. 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:

    1. complete_transition now builds prev_state from inputs, constructs the Transition, and validates it against the schema + types.
    2. Added helper functions inside TransitionBuilder:
      2.1 prev_state_from_inputs
      2.2 validate_transition_against_schema

    Notes:
    This check uses existing schema types and OwnedStateSchema::validate to 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.

  4. St333p commented on Mar 16, 2026

    @St333p
    Member

    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 in rgb-consensus validation, 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-consensus code here is probably no easy task, which is why this is still a TODO and 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions