Implementing Local Laws
BytesSerializer and Serializer trait are currently not exported from
delta_domain_sdk. Use delta_serializers as a work-around.
Local laws are domain-specific rules that you can enforce on top of the global laws of the delta network. While global laws ensure fundamental network invariants (like valid signatures and state consistency), local laws allow you to add custom constraints that apply only to your domain.
This tutorial walks through implementing local laws, from a simple example using a mock proving client to a production-ready setup with SP1 proofs.
Understanding Local Laws
Local laws 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 local laws
- Proving Client: Generates zero-knowledge proofs (mock for testing, SP1 for production)
- Verification Key: Cryptographic identifier of your local laws program
Step 1: Create a Simple Local Law with Mock Proving
Let's start by creating a basic local law 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 local law by implementing the LocalLaws trait:
use delta_domain_sdk::{
base::{
verifiable::{VerifiableType, VerifiableWithDiffs, VerificationContext},
vaults::TokenKind,
},
proving::local_laws::{LocalLaws, LocalLawsError},
};
/// A 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 local law with a mock proving client:
use delta_domain_sdk::{
crypto::HashDigest,
proving::{Proving, mock}
};
// Create a mock proving client with your 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 local laws 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 local laws implementation, you can directly call
validate on your local laws implementation. Additionally, in a real domain,
the domain client 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 Local Law
Most real-world local laws 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>,
}
/// Local law that enforces a whitelist 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 local law 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 local law input - allow native token and a specific fungible token
let local_laws_input = AllowedTokens {
tokens: vec![
TokenKind::Native,
TokenKind::Fungible(TokenId::from([1u8; 32])),
],
};
// Serialize the input to bytes
let input_bytes = BytesSerializer::serialize(&local_laws_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 local laws program with the following structure:
# Cargo.toml
[package]
name = "my_local_laws_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 local laws, 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 local law 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 local laws input
let local_laws_input = sp1_zkvm::io::read::<Vec<u8>>();
// Deserialize the input to your specific type
let input: token_policy::AllowedTokens =
BytesSerializer::deserialize(&local_laws_input).expect("Deserialize input");
// Validate the 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 local laws program to an ELF binary, create a build script:
// build.rs
use sp1_build::BuildArgs;
fn main() {
sp1_build::build_program("./my_local_laws_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 local laws.
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 LOCAL_LAWS_ELF: &[u8] = include_elf!("my_local_laws_program");
// Create an SP1 CPU-based proving client
let proving_client = sp1::Client::local_laws_cpu(LOCAL_LAWS_ELF);
// Or use the Succinct Network for faster proving:
// let proving_client = Client::local_laws_succinct(
// LOCAL_LAWS_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 LOCAL_LAWS_ELF: &[u8] = include_elf!("my_local_laws_program");
Runtime::builder(shard, operator_keypair)
.with_proving_client(
sp1::Client::local_laws_cpu(LOCAL_LAWS_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 local laws. This cryptographic key ensures that only proofs generated by your specific local laws program will be accepted by the base 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 LOCAL_LAWS_ELF: &[u8] = include_elf!("my_local_laws_program");
let runtime = Runtime::builder(shard, keypair)
.with_proving_client(
sp1::Client::local_laws_cpu(LOCAL_LAWS_ELF)
)
.build()
.await?;
// Submit the domain agreement.
// Internally reads the local laws 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
local laws verification key by calling .local_laws_vkey() on your proving
client.
When you submit this domain agreement to the network, validators will:
- Record your local laws verification key
- Only accept SDL proofs that include matching local laws proofs
- Verify that your proofs were generated by your specific local laws program
This ensures that your domain's custom rules are cryptographically enforced at the base layer network level.
Next Steps
- Explore the local_laws crate documentation for more examples
- Check out SP1 documentation for advanced proving techniques