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

Add Sanction List #30

Merged
merged 25 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
03cc81f
feat: add thread-friendly refresh_list
ppoliani Jan 28, 2024
da4ee6e
feat: implement address verifier what keeps a list of sanctioned addr…
ppoliani Jan 28, 2024
1f15224
chore: format the code
ppoliani Jan 28, 2024
0118e00
feat: extract publish_date
ppoliani Jan 29, 2024
e81e101
chore: add additional logs
ppoliani Jan 29, 2024
4aa65fa
chore: move tick to the top of the loop
ppoliani Jan 29, 2024
e3e680a
chore: remoev regex crate
ppoliani Jan 29, 2024
61be243
feat: implement is_sanctioned
ppoliani Jan 29, 2024
867395e
feat: move the sync logic to a separate fn
ppoliani Jan 29, 2024
7ac17d1
Merge branch 'main' into feat/sanctioned_list
ppoliani Jan 30, 2024
9656352
feat: return JoinHandle from the addresse verifier start fn
ppoliani Jan 31, 2024
bda4e3d
feat: sync sanction list before starting the node
ppoliani Jan 31, 2024
5433684
feat: start address verifier in the node
ppoliani Jan 31, 2024
6bea209
feat: update last_udpate ts in the sync fn
ppoliani Jan 31, 2024
d6abe15
feat: remove async from start fn
ppoliani Jan 31, 2024
b1f4845
refactor: rename to compliance
ppoliani Jan 31, 2024
fcd558e
chore: do not await on the compliance loop
ppoliani Jan 31, 2024
76dfc14
chore: start compliance in the orchestrator
ppoliani Jan 31, 2024
4f112ac
feat: use concurrency primitives for the compliance struct
ppoliani Jan 31, 2024
33a3cd6
feat: wrap compliance instance in Arc
ppoliani Jan 31, 2024
6a1d56d
feat: check compliance of the zkapp tx
ppoliani Jan 31, 2024
9e50f5a
chore: keep sync_internal private
ppoliani Jan 31, 2024
d85cc9e
fix: linting issues
ppoliani Feb 1, 2024
054ed3a
chore: check that it's not sanctioned
ppoliani Feb 5, 2024
2e7f604
chore: update error message
ppoliani Feb 5, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Cargo.lock

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
.vscode
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ tokio = { version = "1.34", features = [
"macros",
] }
tokio-stream = "0.1.14"
xml = "0.8.10"
fancy-regex = "0.13.0"
chrono = "0.4.33"

[patch.crates-io]
# see docs/serialization.md
Expand Down
158 changes: 158 additions & 0 deletions src/address_verifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use anyhow::{Context, Result};
use chrono::prelude::*;
use fancy_regex::Regex;
use futures::StreamExt;
use log::{error, info};
use std::{
collections::HashMap,
sync::Arc,
time::{Duration, Instant},
};
use tokio::{spawn, sync::RwLock, time::interval};
use xml::reader::{EventReader, XmlEvent};

pub struct AddressVerifier {
sanctioned_addresses: Arc<RwLock<HashMap<String, bool>>>,
last_update: Arc<RwLock<i64>>,
}

impl AddressVerifier {
const BTC_ID: &'static str = "344";

pub fn new() -> Self {
Self {
sanctioned_addresses: Arc::new(RwLock::new(HashMap::new())),
last_update: Arc::new(RwLock::new(0)),
}
}

fn extract_from_xml(str_value: &str, tag: &str) -> Result<u32> {
let re = Regex::new(&format!(r"(?<={}>)\s*(\w+)(?=<\/{})", tag, tag)).unwrap();
let value = re.find(&str_value)?.context("no regex result")?.as_str();

Ok(value.parse()?)
}

/// read the first few bytes from the remote XML file and extract the last update date.
/// If there is no fresh data we can skip the parsing of XML which is slow.
async fn publish_date() -> Result<i64> {
let res =
reqwest::get("https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml")
ppoliani marked this conversation as resolved.
Show resolved Hide resolved
.await?;

let head = res
.bytes_stream()
.take(1)
.collect::<Vec<reqwest::Result<_>>>()
.await
.into_iter()
.collect::<reqwest::Result<Vec<_>>>()?;

let str_value = String::from_utf8(head[0].to_vec())?;
let year = Self::extract_from_xml(&str_value, "Year")?;
let day = Self::extract_from_xml(&str_value, "Day")?;
let month = Self::extract_from_xml(&str_value, "Month")?;
let date = Utc
.with_ymd_and_hms(year as i32, month, day, 0, 0, 0)
.single()
.context("date parse error")?
.timestamp();

Ok(date)
}

/// Runs the OFAC list syncronization. Downloads the remote XML file and extracts the sanctioned addresses
async fn sync(sanctioned_addresses: Arc<RwLock<HashMap<String, bool>>>) -> Result<()> {
info!("OFAC list syncing...");
let start = Instant::now();
let res =
reqwest::get("https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml")
ppoliani marked this conversation as resolved.
Show resolved Hide resolved
.await?;

let xml = res.text().await?;
let mut sanctioned_addresses = sanctioned_addresses.write().await;
let parser: EventReader<&[u8]> = EventReader::new(xml.as_bytes());
let mut inside_feature_elem = false;
let mut inside_final_elem = false;

for e in parser {
match e {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
if name.local_name == "Feature" {
if attributes.iter().any(|a| {
a.name.local_name == "FeatureTypeID" && a.value == Self::BTC_ID
}) {
inside_feature_elem = true;
}
} else if name.local_name == "VersionDetail" && inside_feature_elem {
inside_final_elem = true;
}
}
Ok(XmlEvent::Characters(value)) => {
if inside_final_elem {
sanctioned_addresses.insert(value, true);
}
}
Ok(XmlEvent::EndElement { name, .. }) => {
if name.local_name == "VersionDetail" && inside_feature_elem {
inside_feature_elem = false;
inside_final_elem = false;
}
}
Err(e) => {
error!("Error parsing xml: {e}");
break;
}
_ => {}
}
}

let duration = start.elapsed();
info!("OFAC synced in {:?}", duration);
ppoliani marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}

/// Periodically fetces the latest list from https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml
/// and updates the list
pub async fn start(&self) {
let sanctioned_addresses = Arc::clone(&self.sanctioned_addresses);
let last_update = Arc::clone(&self.last_update);

spawn(async move {
let mut interval = interval(Duration::from_secs(600));

loop {
interval.tick().await;

let Ok(publish_date) = Self::publish_date().await else {
error!("couldn't extract the OFAC document publish date");
continue;
};

info!("{:?}", *last_update.read().await);
ppoliani marked this conversation as resolved.
Show resolved Hide resolved
if *last_update.read().await >= publish_date {
info!("OFAC list is up-to-date");
continue;
}

let mut last_update = last_update.write().await;
*last_update = publish_date;

if let Err(error) = Self::sync(Arc::clone(&sanctioned_addresses)).await {
error!("OFAC list sync error: {}", error);
};
}
})
.await
.unwrap();
ppoliani marked this conversation as resolved.
Show resolved Hide resolved
}

/// Returns true if the given address is in the sanction list
pub async fn is_sanctioned(&self, address: &str) -> bool {
let sanctioned_addresses = self.sanctioned_addresses.read().await;
sanctioned_addresses.contains_key(address)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use anyhow::Context;
use secp256k1::hashes::Hash;

pub mod address_verifier;
pub mod committee;
pub mod constants;
pub mod frost;
Expand Down
Loading