Skip to content

Commit

Permalink
validate ens names upon message submit (#176)
Browse files Browse the repository at this point in the history
Validate ens names via ens resolution. 

Tested with a local migration-- ens resolution failures on hubs matches
failures on snapchain.
  • Loading branch information
aditiharini authored Dec 19, 2024
1 parent f26ba24 commit 976b4ac
Show file tree
Hide file tree
Showing 17 changed files with 1,984 additions and 235 deletions.
1,571 changes: 1,415 additions & 156 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ tracing = "0.1.40"
thiserror = "1.0.66"
reqwest = { version = "0.12.9", features = ["json"] }
figment = { version = "0.10.19", features = ["env", "toml"] }
alloy = { version = "0.5.4", features = ["full"] }
futures-util = "0.3.31"
url = "2.5.3"
alloy-transport = "0.5.4"
alloy-sol-types = "0.8.11"
alloy-transport = "0.8.0"
alloy-transport-http = "0.8.0"
alloy-sol-types = { version = "0.8.15", features = ["json"] }
alloy-provider = "0.8.0"
alloy-rpc-types = "0.8.0"
alloy-primitives = "0.8.14"
alloy-contract = "0.8.0"
ed25519-dalek = "2.1.1"
pre-commit = "0.5.2"
rocksdb = {git = "https://github.com/rust-rocksdb/rust-rocksdb.git", rev="1cf906dc4087f06631820f13855e6b27bd21b972", features=["multi-threaded-cf"]}
Expand All @@ -52,6 +56,7 @@ humantime = "2.1.0"
itertools = "0.13.0"
cadence = "1.5.0"
tempfile = "3.13.0"
foundry-common = { git = "https://github.com/foundry-rs/foundry", version = "0.2.0" }

[build-dependencies]
tonic-build = "0.9.2"
Expand Down
14 changes: 7 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:1.82 AS builder
FROM rust:1.83 AS builder

WORKDIR /usr/src/app

Expand Down Expand Up @@ -55,12 +55,12 @@ WORKDIR /app
COPY --from=builder /usr/src/app/src/proto /app/proto
COPY --from=builder /usr/src/app/nodes /app/nodes
COPY --from=builder \
/usr/src/app/target/release/snapchain \
/usr/src/app/target/release/follow_blocks \
/usr/src/app/target/release/setup_local_testnet \
/usr/src/app/target/release/submit_message \
/usr/src/app/target/release/perftest \
/app/
/usr/src/app/target/release/snapchain \
/usr/src/app/target/release/follow_blocks \
/usr/src/app/target/release/setup_local_testnet \
/usr/src/app/target/release/submit_message \
/usr/src/app/target/release/perftest \
/app/

ENV RUSTFLAGS="-Awarnings"
CMD ["./snapchain", "--id", "1"]
5 changes: 5 additions & 0 deletions src/bin/setup_local_testnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ struct Args {
#[arg(long, value_parser = parse_duration, default_value = "250ms")]
propose_value_delay: Duration,

#[arg(long, default_value = "")]
l1_rpc_url: String,

/// Statsd prefix. note: node ID will be appended before config file written
#[arg(long, default_value = "snapchain")]
statsd_prefix: String,
Expand Down Expand Up @@ -66,11 +69,13 @@ async fn main() {
let statsd_prefix = format!("{}{}", args.statsd_prefix, id);
let statsd_addr = args.statsd_addr.clone();
let statsd_use_tags = args.statsd_use_tags;
let l1_rpc_url = args.l1_rpc_url.clone();

let config_file_content = format!(
r#"
rpc_address="{rpc_address}"
rocksdb_dir="{db_dir}"
l1_rpc_url="{l1_rpc_url}"
[statsd]
prefix="{statsd_prefix}"
Expand Down
2 changes: 2 additions & 0 deletions src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub struct Config {
pub clear_db: bool,
pub statsd: StatsdConfig,
pub trie_branching_factor: u32,
pub l1_rpc_url: String,
}

impl Default for Config {
Expand All @@ -54,6 +55,7 @@ impl Default for Config {
clear_db: false,
statsd: StatsdConfig::default(),
trie_branching_factor: 16,
l1_rpc_url: "".to_string(),
}
}
}
Expand Down
46 changes: 37 additions & 9 deletions src/connectors/onchain_events/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::collections::HashMap;

use alloy::{
primitives::{address, Address, Bytes, FixedBytes, Uint},
providers::{Provider, ProviderBuilder, RootProvider},
rpc::types::{Filter, Log},
sol,
sol_types::SolEvent,
transports::http::{Client, Http},
};
use alloy_primitives::{address, Address, Bytes, FixedBytes, Uint};
use alloy_provider::{Provider, ProviderBuilder, RootProvider};
use alloy_rpc_types::{Filter, Log};
use alloy_sol_types::{sol, SolEvent};
use alloy_transport_http::{Client, Http};
use async_trait::async_trait;
use foundry_common::ens::EnsError;
use futures_util::stream::StreamExt;
use serde::{Deserialize, Serialize};
use thiserror::Error;
Expand Down Expand Up @@ -173,6 +172,35 @@ pub struct Event {
event_type: EventType,
}

#[async_trait]
pub trait L1Client: Send + Sync {
async fn resolve_ens_name(&self, name: String) -> Result<Address, EnsError>;
}

pub struct RealL1Client {
provider: RootProvider<Http<Client>>,
}

impl RealL1Client {
pub fn new(rpc_url: String) -> Result<RealL1Client, SubscribeError> {
if rpc_url.is_empty() {
return Err(SubscribeError::EmptyRpcUrl);
}
let url = rpc_url.parse()?;
let provider = ProviderBuilder::new().on_http(url);
Ok(RealL1Client { provider })
}
}

#[async_trait]
impl L1Client for RealL1Client {
async fn resolve_ens_name(&self, name: String) -> Result<Address, EnsError> {
foundry_common::ens::NameOrAddress::Name(name)
.resolve(&self.provider)
.await
}
}

pub struct Subscriber {
provider: RootProvider<Http<Client>>,
onchain_events_by_block: HashMap<u64, Vec<Event>>,
Expand Down Expand Up @@ -227,7 +255,7 @@ impl Subscriber {
async fn get_block_timestamp(&self, block_hash: FixedBytes<32>) -> Result<u64, SubscribeError> {
let block = self
.provider
.get_block_by_hash(block_hash, alloy::rpc::types::BlockTransactionsKind::Hashes)
.get_block_by_hash(block_hash, alloy_rpc_types::BlockTransactionsKind::Hashes)
.await?
.ok_or(SubscribeError::UnableToFindBlockByHash)?;
Ok(block.header.timestamp)
Expand Down
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use malachite_metrics::{Metrics, SharedRegistry};
use snapchain::connectors::onchain_events::{L1Client, RealL1Client};
use snapchain::consensus::consensus::SystemMessage;
use snapchain::core::types::proto;
use snapchain::mempool::routing;
Expand Down Expand Up @@ -167,13 +168,18 @@ async fn main() -> Result<(), Box<dyn Error>> {

let rpc_block_store = block_store.clone();
tokio::spawn(async move {
let l1_client: Option<Box<dyn L1Client>> = match RealL1Client::new(app_config.l1_rpc_url) {
Ok(client) => Some(Box::new(client)),
Err(_) => None,
};
let service = MyHubService::new(
rpc_block_store,
rpc_shard_stores,
rpc_shard_senders,
statsd_client.clone(),
app_config.consensus.num_shards,
Box::new(routing::ShardRouter {}),
l1_client,
);

let resp = Server::builder()
Expand Down
122 changes: 121 additions & 1 deletion src/network/server.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use super::rpc_extensions::{AsMessagesResponse, AsSingleMessageResponse};
use crate::connectors::onchain_events::L1Client;
use crate::core::error::HubError;
use crate::mempool::routing;
use crate::proto;
use crate::proto::hub_service_server::HubService;
use crate::proto::on_chain_event::Body;
use crate::proto::GetInfoResponse;
use crate::proto::HubEvent;
use crate::proto::UserNameProof;
use crate::proto::UserNameType;
use crate::proto::{Block, CastId, DbStats};
use crate::proto::{BlocksRequest, ShardChunksRequest, ShardChunksResponse, SubscribeRequest};
use crate::proto::{FidRequest, FidTimestampRequest};
Expand Down Expand Up @@ -39,6 +43,7 @@ pub struct MyHubService {
num_shards: u32,
message_router: Box<dyn routing::MessageRouter>,
statsd_client: StatsdClientWrapper,
l1_client: Option<Box<dyn L1Client>>,
}

impl MyHubService {
Expand All @@ -49,6 +54,7 @@ impl MyHubService {
statsd_client: StatsdClientWrapper,
num_shards: u32,
message_router: Box<dyn routing::MessageRouter>,
l1_client: Option<Box<dyn L1Client>>,
) -> Self {
Self {
block_store,
Expand All @@ -57,6 +63,7 @@ impl MyHubService {
statsd_client,
message_router,
num_shards,
l1_client,
}
}

Expand Down Expand Up @@ -111,6 +118,26 @@ impl MyHubService {
err.to_string()
)));
}

// We're doing the ens validations here for now because we don't want ens resolution to be on the consensus critical path. Eventually this will move to the fname server.
if let Some(message_data) = &message.data {
match &message_data.body {
Some(proto::message_data::Body::UserDataBody(user_data)) => {
if user_data.r#type() == proto::UserDataType::Username {
if user_data.value.ends_with(".eth") {
self.validate_ens_username(fid, user_data.value.to_string())
.await?;
}
};
}
Some(proto::message_data::Body::UsernameProofBody(proof)) => {
if proof.r#type() == UserNameType::UsernameTypeEnsL1 {
self.validate_ens_username_proof(fid, &proof).await?;
}
}
_ => {}
}
}
}

match sender
Expand All @@ -128,7 +155,7 @@ impl MyHubService {
}
Err(e) => {
self.statsd_client.count("rpc.submit_message.failure", 1);
info!("error sending: {:?}", e.to_string());
println!("error sending: {:?}", e.to_string());
return Err(Status::internal("failed to submit message"));
}
}
Expand All @@ -145,6 +172,99 @@ impl MyHubService {
)),
}
}

pub async fn validate_ens_username_proof(
&self,
fid: u64,
proof: &UserNameProof,
) -> Result<(), Status> {
match &self.l1_client {
None => {
// Fail validation, can be fixed with config change
Err(Status::invalid_argument(
"unable to validate ens name because there's no l1 client",
))
}
Some(l1_client) => {
let name = std::str::from_utf8(&proof.name)
.map_err(|err| Status::from_error(Box::new(err)))?;

if !name.ends_with(".eth") {
return Err(Status::invalid_argument(
"invalid ens name, doesn't end with .eth",
));
}

let resolved_ens_address = l1_client
.resolve_ens_name(name.to_string())
.await
.map_err(|err| Status::from_error(Box::new(err)))?
.to_vec();

if resolved_ens_address != proof.owner {
return Err(Status::invalid_argument(
"invalid ens name, resolved address doesn't match proof owner address",
));
}

let stores = self
.get_stores_for(fid)
.map_err(|err| Status::from_error(Box::new(err)))?;

let id_register = stores
.onchain_event_store
.get_id_register_event_by_fid(fid)
.map_err(|err| Status::from_error(Box::new(err)))?;

match id_register {
None => return Err(Status::invalid_argument("missing id registration")),
Some(id_register) => {
match id_register.body {
Some(Body::IdRegisterEventBody(id_register)) => {
// Check verified addresses if the resolved address doesn't match the custody address
if id_register.to != resolved_ens_address {
let verification = VerificationStore::get_verification_add(
&stores.verification_store,
fid,
&resolved_ens_address,
)
.map_err(|err| Status::from_error(Box::new(err)))?;

match verification {
None => Err(Status::invalid_argument(
"invalid ens proof, no matching custody address or verified addresses",
)),
Some(_) => Ok(()),
}
} else {
Ok(())
}
}
_ => return Err(Status::invalid_argument("missing id registration")),
}
}
}
}
}
}

async fn validate_ens_username(&self, fid: u64, fname: String) -> Result<(), Status> {
let stores = self
.get_stores_for(fid)
.map_err(|err| Status::from_error(Box::new(err)))?;
let proof = UserDataStore::get_username_proof(
&stores.user_data_store,
&mut RocksDbTransactionBatch::new(),
fname.as_bytes(),
)
.map_err(|err| Status::from_error(Box::new(err)))?;
match proof {
Some(proof) => self.validate_ens_username_proof(fid, &proof).await,
None => Err(Status::invalid_argument(
"missing username proof for username",
)),
}
}
}

#[tonic::async_trait]
Expand Down
Loading

0 comments on commit 976b4ac

Please sign in to comment.