Assessment reports>Radix>Low findings>Inefficient ,Metadata, array validation sequence
Category: Optimization

Inefficient Metadata array validation sequence

Low Severity
Low Impact
Medium Likelihood

Description

The Metadata native blueprint's create_with_data() function performs validation in a suboptimal order that could lead to excessive resource consumption. Specifically, the validate_metadata_value() function validates array contents before their length is checked in init_system_struct().

For array types like MetadataValue::UrlArray, this means expensive validation operations (e.g., URL validation that involves regex matching) are performed on all elements before the array size is verified to be within acceptable bounds:

pub fn instantiate_test_blueprint() -> Global<TestBlueprint> {
    // Create large vector of URLs to validate
    let metadata_config = metadata!(init {
        "test" => Self::test_create_vec(), // Vector of 15000+ URLs
        locked;
    });
    let metadata = Metadata::new_with_data(metadata_config.init);
    // ...
}

fn test_create_vec() -> Vec<UncheckedUrl> {
    let mut temp_vec: Vec<UncheckedUrl> = vec![];
    for i in 0..15000 {
        temp_vec.push(UncheckedUrl::of(
            format!("https://www.google.com/{i}")
        ));
    }
    temp_vec
}

The validation sequence flows as follows. First, validate_metadata_value() performs URL/origin validation:

pub fn validate_metadata_value(value: &MetadataValue) -> Result<(), MetadataValidationError> {
    match value {
        MetadataValue::UrlArray(urls) => {
            for url in urls {
                CheckedUrl::of(url.as_str())
                    .ok_or(MetadataValidationError::InvalidURL(url.as_str().to_owned()))?;
            }
        }
        MetadataValue::OriginArray(origins) => {
            for origin in origins {
                CheckedOrigin::of(origin.as_str())
                    .ok_or(MetadataValidationError::InvalidOrigin(origin.as_str().to_owned()))?;
            }
        }
        // ...
    }
    Ok(())
}

Only later in init_system_struct() is the array size checked against the 4,096 byte limit.

Preliminary testing shows that vector creation in blueprints is limited by WASM memory allocation (failing with UnreachableCodeReached trap when exceeded). Even within these memory bounds, validation can take 7--8 seconds for large vectors. Testing also shows that the transaction cost is ~4.69 XRD + 0.014 XRD for storage. The validation time is not properly reflected in the execution cost.

Impact

The validation-sequence inefficiency enables DOS vectors that

  1. allow an attacker to cause disproportionate CPU consumption relative to transaction costs, and

  2. Could potentially delay transaction processing when multiple such transactions are queued.

While the system's tip mechanism helps prioritize legitimate transactions during congestion, this issue provides a more cost-effective way to create that congestion in the first place.

Recommendations

We recommend the following changes to the implementation:

  1. Reorder the validation sequence.

fn validate_metadata_value(value: &MetadataValue) -> Result<(), MetadataValidationError> {
    match value {
        MetadataValue::UrlArray(urls) => {
            // Check size before expensive validation
            let total_size = urls.iter()
            .fold(0, |acc, url| acc + url.as_str().len());
            if total_size > MAX_METADATA_SIZE {
               return Err(MetadataValidationError::ValueTooLarge);
               }
            // Proceed with validation knowing size is bounded
        }
        // ...
    }
}
  1. Consider raising an error on the host side when memory allocation fails as opposed to returning -1 from memory.grow as per WASM specification since the memory allocator used by blueprints simply trap on this condition, leading to more confusing errors for developers.

Remediation

This issue has been acknowledged by Radix Publishing Limited, and a fix was implemented in commit 4dffd441.

Zellic © 2025Back to top ↑