Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upstream avail adapter #905

Merged
merged 15 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,186 changes: 2,016 additions & 170 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"rollup-interface",
"adapters/avail",
"adapters/risc0",
"adapters/celestia",
"examples/const-rollup-config",
Expand Down
40 changes: 40 additions & 0 deletions adapters/avail/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[package]
name = "sov-avail-adapter"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
publish.workspace = true
repository.workspace = true
rust-version.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
borsh = { workspace = true, features = ["bytes"] }
sov-rollup-interface = { path = "../../rollup-interface" }
bytes = { version = "1.2.1", features = ["serde"]}
primitive-types = { version = "0.12.1", features = ["serde"]}
sp-core-hashing = "10.0.0"
subxt = { version = "0.29", optional = true }
avail-subxt = { git = "https://github.com/availproject/avail.git", tag = "v1.6.3", features = ["std"], optional = true }
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "full", "bit-vec"], optional = true }

#Convenience
tokio = { workspace = true, optional = true }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3.17", features = ["fmt"] }
async-trait = { workspace = true }
anyhow = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
reqwest = { version = "0.11", features = ["json"], optional = true }
thiserror = { workspace = true }
sp-keyring = { version = "24", optional = true }
sp-core = { version = "21", optional = true }

[features]
default = ["native"]
native = ["dep:tokio", "dep:codec", "dep:reqwest", "dep:avail-subxt", "dep:subxt", "dep:sp-keyring", "dep:sp-core", "sov-rollup-interface/native"]
verifier = []
8 changes: 8 additions & 0 deletions adapters/avail/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Avail Sovereign DA adapter (presence)

This is a _research-only_ adapter making Avail compatible with the Sovereign SDK.

> **_NOTE:_** None of its code is suitable for production use.

This adapter was originally written by Vibhu Rajeev, Aleksandar Terentić and the Avail team, but is
provided as part of the Sovereign SDK under the Apache 2.0 and MIT licenses.
15 changes: 15 additions & 0 deletions adapters/avail/src/avail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use avail_subxt::primitives::AppUncheckedExtrinsic;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Confidence {
pub block: u32,
pub confidence: f64,
pub serialised_confidence: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ExtrinsicsData {
pub block: u32,
pub extrinsics: Vec<AppUncheckedExtrinsic>,
}
10 changes: 10 additions & 0 deletions adapters/avail/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#[cfg(feature = "native")]
mod avail;
#[cfg(feature = "native")]
pub mod service;
pub mod spec;
pub mod verifier;

// NOTE: Remove once dependency to the node is removed
#[cfg(feature = "native")]
pub use avail_subxt::build_client;
236 changes: 236 additions & 0 deletions adapters/avail/src/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use core::time::Duration;

use anyhow::anyhow;
use async_trait::async_trait;
use avail_subxt::api::runtime_types::sp_core::bounded::bounded_vec::BoundedVec;
use avail_subxt::primitives::AvailExtrinsicParams;
use avail_subxt::{api, AvailConfig};
use reqwest::StatusCode;
use sov_rollup_interface::da::DaSpec;
use sov_rollup_interface::services::da::DaService;
use sp_core::crypto::Pair as PairTrait;
use sp_keyring::sr25519::sr25519::Pair;
use subxt::tx::PairSigner;
use subxt::OnlineClient;
use tracing::info;

use crate::avail::{Confidence, ExtrinsicsData};
use crate::spec::block::AvailBlock;
use crate::spec::header::AvailHeader;
use crate::spec::transaction::AvailBlobTransaction;
use crate::spec::DaLayerSpec;
use crate::verifier::Verifier;

/// Runtime configuration for the DA service
#[derive(Clone, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DaServiceConfig {
pub light_client_url: String,
pub node_client_url: String,
//TODO: Safer strategy to load seed so it is not accidentally revealed.
pub seed: String,
pub polling_timeout: Option<u64>,
pub polling_interval: Option<u64>,
pub app_id: u64,
}

const DEFAULT_POLLING_TIMEOUT: Duration = Duration::from_secs(60);
const DEFAULT_POLLING_INTERVAL: Duration = Duration::from_secs(1);

#[derive(Clone)]
pub struct DaProvider {
pub node_client: OnlineClient<AvailConfig>,
pub light_client_url: String,
signer: PairSigner<AvailConfig, Pair>,
polling_timeout: Duration,
polling_interval: Duration,
app_id: u64,
}

impl DaProvider {
fn appdata_url(&self, block_num: u64) -> String {
let light_client_url = self.light_client_url.clone();
format!("{light_client_url}/v1/appdata/{block_num}")
}

fn confidence_url(&self, block_num: u64) -> String {
let light_client_url = self.light_client_url.clone();
format!("{light_client_url}/v1/confidence/{block_num}")
}

pub async fn new(config: DaServiceConfig) -> Self {
let pair = Pair::from_string_with_seed(&config.seed, None).unwrap();
let signer = PairSigner::<AvailConfig, Pair>::new(pair.0.clone());

let node_client = avail_subxt::build_client(config.node_client_url.to_string(), false)
.await
.unwrap();
let light_client_url = config.light_client_url;

DaProvider {
node_client,
light_client_url,
signer,
polling_timeout: match config.polling_timeout {
Some(i) => Duration::from_secs(i),
None => DEFAULT_POLLING_TIMEOUT,
},
polling_interval: match config.polling_interval {
Some(i) => Duration::from_secs(i),
None => DEFAULT_POLLING_INTERVAL,
},
app_id: config.app_id,
}
}
}

// TODO: Is there a way to avoid coupling to tokio?

async fn wait_for_confidence(confidence_url: &str, polling_timeout: Duration, polling_interval: Duration) -> anyhow::Result<()> {
let start_time = std::time::Instant::now();

loop {
if start_time.elapsed() >= polling_timeout {
return Err(anyhow!("Confidence not received after timeout: {}s", polling_timeout.as_secs()));
}

let response = reqwest::get(confidence_url).await?;
if response.status() != StatusCode::OK {
info!("Confidence not received");
tokio::time::sleep(polling_interval).await;
continue;
}

let response: Confidence = serde_json::from_str(&response.text().await?)?;
if response.confidence < 92.5 {
info!("Confidence not reached");
tokio::time::sleep(polling_interval).await;
continue;
}

break;
}

Ok(())
}

async fn wait_for_appdata(appdata_url: &str, block: u32, polling_timeout: Duration, polling_interval: Duration) -> anyhow::Result<ExtrinsicsData> {
let start_time = std::time::Instant::now();

loop {
if start_time.elapsed() >= polling_timeout {
return Err(anyhow!("RPC call for filtered block to light client timed out. Timeout: {}s", polling_timeout.as_secs()));
}

let response = reqwest::get(appdata_url).await?;
if response.status() == StatusCode::NOT_FOUND {
return Ok(ExtrinsicsData {
block,
extrinsics: vec![],
});
}
if response.status() != StatusCode::OK {
tokio::time::sleep(polling_interval).await;
continue;
}

let appdata: ExtrinsicsData = serde_json::from_str(&response.text().await?)?;
return Ok(appdata);
}
}

#[async_trait]
impl DaService for DaProvider {
type Spec = DaLayerSpec;

type FilteredBlock = AvailBlock;

type Verifier = Verifier;

type Error = anyhow::Error;

// Make an RPC call to the node to get the finalized block at the given height, if one exists.
// If no such block exists, block until one does.
async fn get_finalized_at(&self, height: u64) -> Result<Self::FilteredBlock, Self::Error> {
let node_client = self.node_client.clone();
let confidence_url = self.confidence_url(height);
let appdata_url = self.appdata_url(height);

wait_for_confidence(&confidence_url, self.polling_timeout, self.polling_interval).await?;
let appdata = wait_for_appdata(&appdata_url, height as u32, self.polling_timeout, self.polling_interval).await?;
info!("Appdata: {:?}", appdata);

let hash = match {node_client
.rpc()
.block_hash(Some(height.into()))
.await?} {
Some(i) => i,
None => return Err(anyhow!("Hash for height: {} not found.", height))
};

let header = match {node_client.rpc().header(Some(hash)).await?} {
Some(i) => i,
None => return Err(anyhow!("Header for hash: {} not found.", hash))
};

let header = AvailHeader::new(header, hash);
let transactions: Result<Vec<AvailBlobTransaction>, anyhow::Error> = appdata
.extrinsics
.iter()
.map(AvailBlobTransaction::new)
.collect();

let transactions = transactions?;
Ok(AvailBlock {
header,
transactions,
})
}

// Make an RPC call to the node to get the block at the given height
// If no such block exists, block until one does.
async fn get_block_at(&self, height: u64) -> Result<Self::FilteredBlock, Self::Error> {
self.get_finalized_at(height).await
}

// Extract the blob transactions relevant to a particular rollup from a block.
// NOTE: The avail light client is expected to be run in app specific mode, and hence the
// transactions in the block are already filtered and retrieved by light client.
fn extract_relevant_txs(
&self,
block: &Self::FilteredBlock,
) -> Vec<<Self::Spec as DaSpec>::BlobTransaction> {
block.transactions.clone()
}

// Extract the inclusion and completenss proof for filtered block provided.
// The output of this method will be passed to the verifier.
// NOTE: The light client here has already completed DA sampling and verification of inclusion and soundness.
async fn get_extraction_proof(
&self,
_block: &Self::FilteredBlock,
_blobs: &[<Self::Spec as DaSpec>::BlobTransaction],
) -> (
<Self::Spec as DaSpec>::InclusionMultiProof,
<Self::Spec as DaSpec>::CompletenessProof,
) {
((), ())
}

async fn send_transaction(&self, blob: &[u8]) -> Result<(), Self::Error> {
let data_transfer = api::tx()
.data_availability()
.submit_data(BoundedVec(blob.to_vec()));

let extrinsic_params = AvailExtrinsicParams::new_with_app_id(7.into());
citizen-stig marked this conversation as resolved.
Show resolved Hide resolved

let h = self
.node_client
.tx()
.sign_and_submit_then_watch(&data_transfer, &self.signer, extrinsic_params)
.await?;

info!("Transaction submitted: {:#?}", h.extrinsic_hash());

Ok(())
}
}
48 changes: 48 additions & 0 deletions adapters/avail/src/spec/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use core::fmt::{Display, Formatter};
use std::hash::Hash;
use std::str::FromStr;

use primitive_types::H256;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Eq, Hash)]
pub struct AvailAddress([u8; 32]);

impl sov_rollup_interface::BasicAddress for AvailAddress {}

impl Display for AvailAddress {
fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
let hash = H256(self.0);
write!(f, "{hash}")
}
}

impl AsRef<[u8]> for AvailAddress {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}

impl From<[u8; 32]> for AvailAddress {
fn from(value: [u8; 32]) -> Self {
Self(value)
}
}

impl FromStr for AvailAddress {
type Err = <H256 as FromStr>::Err;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let h_256 = H256::from_str(s)?;

Ok(Self(h_256.to_fixed_bytes()))
}
}

impl<'a> TryFrom<&'a [u8]> for AvailAddress {
type Error = anyhow::Error;

fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
Ok(Self(<[u8; 32]>::try_from(value)?))
}
}
Loading