Skip to main content

Implementing Guardrails

info

Coming soon: How to use guardrails with the delta Gateway.

Guardrails are domain-defined rules which govern the transactions on that domain. delta's default guardrails ensure fundamental network properties (like valid signatures and state consistency), but domain builders can use the guardrail model to add custom constraints enforcing any desired business logic.

This tutorial walks through implementing guardrails, from a simple example using a mock proving client to a production-ready setup with SP1 proofs.

Understanding Guardrails​

Guardrails are implemented as Rust programs that run inside a zero-knowledge virtual machine (zkVM). They validate verifiables against your custom rules and produce cryptographic proofs that the rules were followed.

Key concepts:

  • Local Laws Trait: Defines the interface for your custom validation logic
  • Input Type: Custom data structure that parameterizes your guardrails
  • Proving Client: Generates zero-knowledge proofs (mock for testing, SP1 for production)
  • Verification Key: Cryptographic identifier of your guardrail program

Step 1: Create a Simple Guardrail with Mock Proving​

Let's start by creating a basic guardrail that validates debits. We'll implement a policy that only allows debits with the native token. Initially, we'll use the mock proving client, which is perfect for development and testing since it doesn't require expensive proof generation.

First, define your guardrail by implementing the LocalLaws trait:

use delta_domain_sdk::{
base::{
verifiable::{VerifiableType, VerifiableWithDiffs, VerificationContext},
vaults::TokenKind,
},
proving::local_laws::{LocalLaws, LocalLawsError},
};

/// A guardrail/local law that only allows native token transactions
#[derive(Debug, Copy, Clone, Default)]
pub struct SingleTokenPolicy {}

impl LocalLaws for SingleTokenPolicy {
type Input<'a> = ();

fn validate<'a>(
verifiables: &[VerifiableWithDiffs],
_verification_context: &VerificationContext,
_input: &Self::Input<'a>,
) -> Result<(), LocalLawsError> {
// Only allow the native token
const ALLOWED_TOKEN: TokenKind = TokenKind::Native;

// Check each verifiable transaction
for verifiable in verifiables {
if let VerifiableType::DebitAllowance(debit_allowance) = &verifiable.verifiable {
for (&token_kind, _amount) in &debit_allowance.payload().allowances {
if token_kind != ALLOWED_TOKEN {
return Err(LocalLawsError::new(format!(
"Token {token_kind} is not allowed. Only native tokens are permitted."
)));
}
}
}
}

Ok(())
}
}

To use this guardrail with a mock proving client:

use delta_domain_sdk::{
crypto::HashDigest,
proving::{Proving, mock}
};

// Create a mock proving client with your guardrail/local law
let proving_client = mock::Client::global_laws()
.with_local_laws::<SingleTokenPolicy>();

// Generate a proof (with empty input since SingleTokenPolicy takes no parameters)
let proof = proving_client.prove(
HashDigest::from([0u8; 32]), // made up SDL hash
&verifiables_with_diffs,
&verification_context,
Default::default(), // empty input
)?;

The mock client validates your guardrail logic without generating cryptographic proofs. This is useful for:

  • Rapid development and testing
  • Debugging validation logic
  • Integration tests that don't need real proofs

Note that to test your guardrail implementation, you can directly call validate on your implementation. Additionally, in a real domain, the delta gateway will take care of calling the proving client internally. Here, we just do it for demonstration purposes. Below, we'll look at how to integrate a proving client with your domain.

Step 2: Add Custom Inputs to Your Guardrail​

Most real-world guardrails need additional inputs or parameters. Let's extend our token policy to accept a configurable list of allowed tokens instead of hard-coding a single token:

use delta_domain_sdk::{
base::{
verifiable::{VerifiableType, VerifiableWithDiffs, VerificationContext},
vaults::{TokenKind, TokenId},
},
proving::{
local_laws::{LocalLaws, LocalLawsError},
},
};
use serde::{Deserialize, Serialize};
use std::collections::hash_set::HashSet;

/// Input structure defining which tokens are allowed
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllowedTokens {
/// List of tokens that are permitted in transactions
pub tokens: HashSet<TokenKind>,
}

/// Guardrail/local law that enforces an allowlist of allowed tokens
#[derive(Debug, Copy, Clone, Default)]
pub struct MultiTokenPolicy {}

impl LocalLaws for MultiTokenPolicy {
type Input<'a> = AllowedTokens;

fn validate<'a>(
verifiables: &[VerifiableWithDiffs],
_verification_context: &VerificationContext,
input: &AllowedTokens,
) -> Result<(), LocalLawsError> {
// Check each verifiable transaction
for verifiable in verifiables {
if let VerifiableType::DebitAllowance(debit_allowance) = &verifiable.verifiable {
for (&token_kind, _amount) in &debit_allowance.payload().allowances {
// Check if the token is in the allowed list
if !input.tokens.contains(&token_kind) {
return Err(LocalLawsError::new(format!(
"Token {token_kind} is not allowed. Permitted tokens: {:?}",
input.tokens
)));
}
}
}
}

Ok(())
}
}

To use this guardrail with inputs, you need to serialize the input data:

use delta_domain_sdk::{
base::vaults::{TokenKind, TokenId},
proving::{mock, Proving},
};
use delta_serializers::{bytes::BytesSerializer, serializer::Serializer};

// Create a mock proving client
let proving_client = mock::Client::global_laws()
.with_local_laws::<MultiTokenPolicy>();


// Create the guardrail input - allow native token and a specific fungible token
let guardrails_input = AllowedTokens {
tokens: vec![
TokenKind::Native,
TokenKind::Fungible(TokenId::from([1u8; 32])),
],
};

// Serialize the input to bytes
let input_bytes = BytesSerializer::serialize(&guardrails_input).unwrap();

// Generate a proof with the input
let proof = proving_client.prove(
HashDigest::from([2u8; 32]), // made up SDL hash
&verifiables_with_diffs,
&verification_context,
&input_bytes,
)?;

Step 3: Create a zkVM Program for SP1​

To generate real cryptographic proofs, you need to create a zkVM program that runs in the SP1 zkVM. This program will be compiled to an ELF binary that executes in the zero-knowledge environment.

Create a new crate for your guardrail program with the following structure:

# Cargo.toml
[package]
name = "my_guardrail_program"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
delta_domain_sdk = { version = "*" }
delta_serializers = { version = "*" }
serde = { version = "1.0", features = ["derive"] }
sp1-zkvm = { version = "5.2" }

The main program reads inputs from SP1's I/O, validates the guardrails, and commits the results:

// src/main.rs
#![no_main]

use delta_domain_sdk::{
base::{
verifiable::{VerifiableType, VerifiableWithDiffs, VerificationContext},
vaults::TokenKind,
},
proving::local_laws::{LocalLaws, LocalLawsError},
};
use delta_serializers::{bytes::BytesSerializer, serializer::Serializer};
use serde::{Deserialize, Serialize};

// Define your guardrail implementation
mod token_policy {
use super::*;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllowedTokens {
pub tokens: Vec<TokenKind>,
}

#[derive(Debug, Copy, Clone, Default)]
pub struct MultiTokenPolicy {}

impl LocalLaws for MultiTokenPolicy {
// ... same as above
}
}

sp1_zkvm::entrypoint!(main);

fn main() {
// Read standard inputs (used by global laws)
let verifiables = sp1_zkvm::io::read::<Vec<VerifiableWithDiffs>>();
let verification_context = sp1_zkvm::io::read::<VerificationContext>();

// Read guardrails/local laws input
let guardrails_input = sp1_zkvm::io::read::<Vec<u8>>();

// Deserialize the input to your specific type
let input: token_policy::AllowedTokens =
BytesSerializer::deserialize(&guardrails_input).expect("Deserialize input");

// Validate the guardrails/local laws
token_policy::MultiTokenPolicy::validate(&verifiables, &verification_context, &input)
.expect("The local laws are not satisfied by the verifiables.");

// Commit the inputs to the proof's public values
sp1_zkvm::io::commit(&verifiables);
sp1_zkvm::io::commit(&verification_context);
}

Step 4: Build the ELF and Configure SP1 Proving​

To compile your guardrail program to an ELF binary, create a build script:

// build.rs
use sp1_build::BuildArgs;

fn main() {
sp1_build::build_program("./my_guardrail_program")
}

See SP1 build for more details. Build the ELF by running:

cargo build --release

Now you can create an SP1 proving client for delta that uses your guardrails. Make sure that the cargo feature sp1 is active on delta_domain_sdk.

use delta_domain_sdk::proving::{Proving, sp1};
use sp1_sdk::include_elf;

// Include the compiled ELF at compile time
const GUARDRAIL_ELF: &[u8] = include_elf!("my_guardrail_program");

// Create an SP1 CPU-based proving client
let proving_client = sp1::Client::local_laws_cpu(GUARDRAIL_ELF);

// Or use the Succinct Network for faster proving:
// let proving_client = Client::local_laws_succinct(
// GUARDRAIL_ELF,
// &private_key,
// "https://rpc.mainnet.succinct.xyz"
// );

// Generate a real cryptographic proof (this may take some time)
let proof = proving_client.prove(
HashDigest::from([3u8; 32]), // made up SDL hash
&verifiables_with_diffs,
&verification_context,
&input_bytes,
)?;

Again, real domains won't create proofs like this but rather through the domain client.

The SP1 client supports multiple backends:

  • CPU: - generates proofs locally on CPU
  • Remote: - connects to a custom remote prover
  • CUDA: - uses a local CUDE-enabled GPU
  • Succinct Network: - uses Succinct's prover network

Step 5: Integrating with the delta domain​

To use your proving client in your domain use .with_proving_client():

use delta_domain_sdk::{proving::sp1, Runtime};
use sp1_sdk::include_elf;

const GUARDRAIL_ELF: &[u8] = include_elf!("my_guardrail_program");

Runtime::builder(shard, operator_keypair)
.with_proving_client(
sp1::Client::local_laws_cpu(GUARDRAIL_ELF)
)
.build()
.await?;

For testing with the mock proving client:

use delta_domain_sdk::{proving::mock, Runtime};

Runtime::builder(shard, operator_keypair)
.with_proving_client(
mock::Client::global_laws()
.with_local_laws::<MultiTokenPolicy>()
)
.build()
.await?;

Step 6: Configure Your Domain Agreement​

When submitting a domain agreement, you need to add the verification key of your guardrails. This cryptographic key ensures that only proofs generated by your specific guardrail program will be accepted by the settlement layer. The domain client can do this automatically for you:

use delta_domain_sdk::{
base::core::Planck,
proving::sp1,
Runtime,
};
use sp1_sdk::include_elf;

const GUARDRAIL_ELF: &[u8] = include_elf!("my_guardrail_program");

let runtime = Runtime::builder(shard, keypair)
.with_proving_client(
sp1::Client::local_laws_cpu(GUARDRAIL_ELF)
)
.build()
.await?;

// Submit the domain agreement.
// Internally reads the guardrails verification key and adds it on the domain agreement
let new_shard_fee: Planck = 1000;
runtime
.submit_domain_agreement(new_shard_fee)
.await?;

If you use another way to submit the domain agreement, you can retrieve the guardrail verification key by calling .local_laws_vkey() on your proving client.

When you submit this domain agreement to the network, validators will:

  1. Record your guardrail verification key
  2. Only accept SDL proofs that include matching guardrail proofs
  3. Verify that your proofs were generated by your specific guardrail program

This ensures that your domain's custom rules are cryptographically enforced at the settlement layer level.

Next Steps​