Developers
Build a Tangle Blueprint

Building a Tangle Blueprint AVS End-to-End

This guide will walk you through the process of building and deploying a Tangle Blueprint Actively Validated Service (AVS) using the job macro. We'll cover both the development process and deployment to the Tangle Network.

1. Development Process

1.1 Set Up Your Development Environment

First, ensure you have the necessary tools installed:

  • Rust and Cargo
  • Node.js and npm
  • Tangle CLI (installation instructions TBD)

1.2 Create a New Tangle Blueprint Project

Use the Tangle CLI to create a new blueprint project:

1.3 Define Your AVS Functionality

Before implementing your AVS, it's crucial to define its functionality. This involves:

  1. Identifying the core operations your service will perform.
  2. Determining the inputs and outputs for each operation.
  3. Considering any potential error cases.
  4. Planning for performance benchmarks and quality of service metrics.

For our example, let's create an AVS that performs simple cryptographic operations.

1.4 Implement Jobs Using the #[job(...)] Macro

Jobs are the core functions of your AVS. Use the #[job(...)] macro to define them. Here are some examples based on the provided lib.rs:

use gadget_sdk::job;
 
#[job(id = 0, params(n, t), result(_))]
pub fn keygen(ctx: &MyContext, n: u16, t: u16) -> Result<Vec<u8>, Error> {
    let _ = (n, t, ctx);
    Ok(vec![0; 33])
}
 
#[job(id = 1, params(keygen_id, data), result(_))]
pub async fn sign(keygen_id: u64, data: Vec<u8>) -> Result<Vec<u8>, Error> {
    let _ = (keygen_id, data);
    Ok(vec![0; 65])
}

Each job is annotated with:

  • A unique id
  • params: The input parameters
  • result: The output type (use (_) for any type)

1.5 Implement Slashing Reports Using the #[report(...)] Macro

Reports are used for quality assurance and can trigger slashing if the AVS doesn't meet certain criteria. Here's an example:

use gadget_sdk::report;
 
#[report(
    params(uptime, response_time, error_rate),
    result(Vec<u8>),
    report_type = "qos",
    interval = 3600,
    metric_thresholds(uptime = 99, response_time = 1000, error_rate = 5)
)]
fn report_service_health(uptime: f64, response_time: u64, error_rate: f64) -> Vec<u8> {
    let mut issues = Vec::new();
    if uptime < 99.0 {
        issues.push(b"Low uptime".to_vec());
    }
    if response_time > 1000 {
        issues.push(b"High response time".to_vec());
    }
    if error_rate > 5.0 {
        issues.push(b"High error rate".to_vec());
    }
    issues.concat()
}

This report checks service health metrics and reports issues if they fall below specified thresholds.

1.6 Set Up Custom Registration Hooks

Registration hooks are called when an operator registers with your AVS. Here's how to define one:

use gadget_sdk::registration_hook;
 
#[registration_hook(evm = "RegistrationContract")]
pub fn on_register(pubkey: Vec<u8>) {
    // Handle registration logic here
}

1.7 Implement Request Hooks

Request hooks are triggered when a request is made to your AVS. Here's an example:

use gadget_sdk::request_hook;
 
#[request_hook(evm = "RequestContract")]
pub fn on_request(nft_id: u64) {
    // Handle request logic here
}

1.8 Add Benchmarks

Benchmarks help ensure your AVS performs efficiently. Here's how to define a benchmark:

use gadget_sdk::benchmark;
 
#[benchmark(job_id = 0, cores = 2)]
fn keygen_2_of_3() {
    let n = 3;
    let t = 2;
    let result = keygen(&MyContext, n, t);
    assert!(result.is_ok());
}

This benchmark tests the keygen job with specific parameters.

1.9 Error Handling

Define custom error types for your AVS to handle various failure scenarios:

#[derive(Debug, Clone, Copy)]
pub enum Error {
    InvalidKeygen,
    InvalidSignature,
    InvalidRefresh,
}
 
impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let msg = match self {
            Error::InvalidKeygen => "Invalid Keygen",
            Error::InvalidSignature => "Invalid Signature",
            Error::InvalidRefresh => "Invalid Refresh",
        };
        write!(f, "{}", msg)
    }
}
 
impl std::error::Error for Error {}

By following these steps, you'll have a well-structured Tangle Blueprint AVS with defined jobs, reports, hooks, and benchmarks. In the next section, we'll cover how to test and deploy your AVS to the Tangle Network.

2. Implementing the AVS Client (main.rs)

Now that we've defined the core functionality of our AVS, let's implement the client that will handle registration, running, and event listening processes.

2.1 Setting up the Main File

Create a new file named main.rs in your project's src directory. This file will serve as the entry point for your AVS client. If you used the Tangle CLI to create your Blueprint this should already exist.

2.2 Imports and Struct Definition

Start by adding the necessary imports and defining the TangleGadgetRunner struct:

use gadget_sdk::config::{ContextConfig, GadgetCLICoreSettings, StdGadgetConfiguration};
use gadget_sdk::{
    config::Protocol,
    events_watcher::{substrate::SubstrateEventWatcher, tangle::TangleEventsWatcher},
    tangle_subxt::tangle_testnet_runtime::api,
    tx,
    info
};
use std::io::Write;
use crypto_service_blueprint as blueprint;
use structopt::StructOpt;
use gadget_sdk::keystore::KeystoreUriSanitizer;
use gadget_sdk::keystore::sp_core_subxt::Pair;
use gadget_sdk::run::GadgetRunner;
use gadget_sdk::tangle_subxt::subxt::tx::Signer;
 
struct TangleGadgetRunner {
    env: StdGadgetConfiguration,
}

2.3 Implementing the GadgetRunner Trait

Implement the GadgetRunner trait for TangleGadgetRunner:

#[async_trait::async_trait]
impl GadgetRunner for TangleGadgetRunner {
    type Error = Error;
 
    fn config(&self) -> &StdGadgetConfiguration {
        &self.env
    }
 
    async fn register(&mut self) -> std::result::Result<(), Self::Error> {
        // Registration logic here
    }
 
    async fn benchmark(&self) -> std::result::Result<(), Self::Error> {
        todo!()
    }
 
    async fn run(&self) -> Result<()> {
        // Running logic here
    }
}

2.4 Helper Functions

Add helper functions for creating the GadgetRunner and checking for test mode:

async fn create_gadget_runner(
    config: ContextConfig,
) -> (
    GadgetConfiguration<parking_lot::RawRwLock>,
    Box<dyn GadgetRunner<Error = Error>>,
) {
    let env = gadget_sdk::config::load(config).expect("Failed to load environment");
    match env.protocol {
        Protocol::Tangle => (env.clone(), Box::new(TangleGadgetRunner { env })),
        _ => panic!("Unsupported protocol"),
    }
}
 
fn check_for_test(
    _env: &GadgetConfiguration<parking_lot::RawRwLock>,
    config: &ContextConfig,
) -> Result<()> {
    if let GadgetCLICoreSettings::Run {
        keystore_uri: base_path,
        test_mode,
        ..
    } = &config.gadget_core_settings
    {
        if !*test_mode {
            return Ok(());
        }
        let path = base_path.sanitize_file_path().join("test_started.tmp");
        let mut file = std::fs::File::create(&path)?;
        file.write_all(b"test_started")?;
        info!("Successfully wrote test file to {}", path.display())
    }
 
    Ok(())
}

2.5 Main Function

Finally, implement the main function:

This main.rs file sets up the AVS client for our cryptographic service. It handles the registration process, runs the service, and listens for events on the Tangle network. The TangleGadgetRunner struct implements the GadgetRunner trait, which provides the core functionality for running the AVS.

The registration process registers the operator on the Tangle network with the specified preferences. The run function sets up an event watcher that listens for events related to our cryptographic service and handles them accordingly.

The main function ties everything together, setting up the environment, registering the operator if needed, and then running the service. The check_for_test function is included to support test mode functionality. Remember to replace crypto_service_blueprint and CryptoEventHandler with the actual names you've used in your blueprint implementation.