Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,380 changes: 1,402 additions & 978 deletions Cargo.lock

Large diffs are not rendered by default.

26 changes: 9 additions & 17 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,24 @@ server = [ "hyper" ]
cli = [ "structopt" ]

[dependencies]
bitcoin = { version = "0.23.0", features = [ "use-serde" ] }
elements = { version = "0.12.1", features = [ "serde-feature" ] }
bitcoin_hashes = { version = "0.7.4", features = [ "serde" ] }
hyper = { version = "0.12.35", optional = true }
bitcoin = { version = "0.31", features = [ "serde" ] }
elements = { version = "0.24", features = [ "serde" ] }
hyper = { version = "0.12", optional = true }
failure = "0.1.7"
hex = "0.4.2"
serde = "1.0.105"
serde_derive = "1.0.105"
serde_json = "1.0.50"
log = "0.4.8"
stderrlog = "0.4.3"
secp256k1 = "0.17.2"
base64 = "0.12.0"
reqwest = { version = "0.10.4", features = [ "blocking", "json" ] }
stderrlog = "0.6"
base64 = "0.22"
reqwest = { version = "0.12", features = [ "blocking", "json" ] }
lazy_static = "1.4.0"
idna = "0.2.0"
regex = "1.1.6"
idna = "0.5"
regex = "1"
structopt = { version = "0.3.12", optional = true }

[dev-dependencies]
rocket = "0.4.4"
rocket_contrib = { version = "0.4.4", default-features = false, features = ["json"] }
rocket = { version = "0.5", features = ["json"] }

[[bin]]
name = "server"
Expand All @@ -42,7 +38,3 @@ required-features = [ "cli", "server" ]
[[bin]]
name = "liquid-asset-registry"
required-features = [ "cli" ]

[patch.crates-io.elements]
git = "https://github.com/elementsproject/rust-elements"
rev = "fc27e53046d531b5ea65617fbfeea62e84ba10d4"
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:1.53 AS builder
FROM rust:1.77.2 AS builder
WORKDIR /src
COPY Cargo.toml Cargo.lock ./
COPY src src
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,8 @@ $ liquid-asset-registry verify-asset "$(cat asset.json)"

## Testing

Uses rocket for mock http servers, which requires nightly.

```
$ cargo +nightly test --features 'cli server client' -- --test-threads 1
$ cargo test --features 'cli server client' -- --test-threads 1
```

## Development
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ services:
- ${GPG_KEY_PATH:-./keys/signing-privkey.asc}:/app/signing-privkey.asc
- ${SSH_KEY_PATH:-./keys/id_ed25519}:/root/.ssh/id_${SSH_KEY_CIPHER:-ed25519}
nginx:
image: nginx:1.21
image: nginx:1.25.4
environment:
- NGINX_HOST=assets.blockstream.info
expose: [ "80" ]
Expand Down
46 changes: 16 additions & 30 deletions src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ use serde_json::Value;
#[cfg(feature = "cli")]
use structopt::StructOpt;

use bitcoin_hashes::{hex::FromHex, hex::ToHex, sha256, Hash};
use bitcoin::secp256k1::{self, Secp256k1};
use elements::{issuance::ContractHash, AssetId, OutPoint};
use secp256k1::Secp256k1;

use crate::chain::{verify_asset_issuance_tx, ChainQuery};
use crate::entity::{verify_asset_link, AssetEntity};
use crate::errors::{OptionExt, Result};
use crate::util::{
serde_from_hex, serde_to_hex, verify_bitcoin_msg, verify_domain_name, verify_pubkey, TxInput,
serde_from_hex, serde_to_hex, verify_bitcoin_msg, verify_domain_name, verify_pubkey,
OutPointSerializer, TxInput,
};

lazy_static! {
Expand All @@ -30,6 +30,8 @@ pub struct Asset {
pub contract: Value,

pub issuance_txin: TxInput,

#[serde(with = "OutPointSerializer")]
pub issuance_prevout: OutPoint,

#[serde(flatten)]
Expand Down Expand Up @@ -135,8 +137,8 @@ impl Asset {
)
}

pub fn contract_hash(&self) -> Result<ContractHash> {
contract_json_hash(&self.contract)
pub fn contract_hash(&self) -> ContractHash {
ContractHash::from_json_contract(&self.contract.to_string()).expect("must be valid json")
}

pub fn from_request(req: AssetRequest, chain: &ChainQuery) -> Result<Self> {
Expand All @@ -148,7 +150,9 @@ impl Asset {
AssetFields::from_contract(&req.contract).context("invalid contract fields")?;

let issuance_txin = serde_json::from_value(asset_data["issuance_txin"].take())?;
let issuance_prevout = serde_json::from_value(asset_data["issuance_prevout"].take())?;

let issuance_prevout =
OutPointSerializer::from_value(asset_data["issuance_prevout"].take())?;

Ok(Asset {
asset_id: req.asset_id,
Expand All @@ -163,7 +167,7 @@ impl Asset {
pub fn validate_contract(contract: &Value, contract_hash: &ContractHash) -> Result<()> {
AssetFields::from_contract(contract)?.validate()?;

let expected_hash = contract_json_hash(contract)?;
let expected_hash = ContractHash::from_json_contract(&contract.to_string())?;
ensure!(
expected_hash == *contract_hash,
"contract hash mismatch, expected {}",
Expand All @@ -173,27 +177,10 @@ impl Asset {
}
}

pub fn contract_json_hash(contract: &Value) -> Result<ContractHash> {
// serde_json sorts keys lexicographically
let contract_str = serde_json::to_string(contract)?;

// use the ContractHash representation for correct (reverse) hex encoding,
// but use a single SHA256 instead of the double hash assumed by ContractHash::hash()
let hash = sha256::Hash::hash(&contract_str.as_bytes());
Ok(ContractHash::from_inner(hash.into_inner()))
}

#[cfg_attr(feature = "cli", derive(StructOpt))]
#[derive(Debug, Serialize, Deserialize)]
pub struct AssetRequest {
#[cfg_attr(
feature = "cli",
structopt(
long = "asset-id",
help = "The asset-id",
parse(try_from_str = AssetId::from_hex)
)
)]
#[cfg_attr(feature = "cli", structopt(long = "asset-id", help = "The asset id"))]
pub asset_id: AssetId,

#[cfg_attr(
Expand All @@ -209,17 +196,17 @@ pub struct AssetRequest {

// Verify the asset id commits to the provided contract and prevout
fn verify_asset_commitment(asset: &Asset) -> Result<()> {
let contract_hash = asset.contract_hash()?;
let contract_hash = asset.contract_hash();
let entropy = AssetId::generate_asset_entropy(asset.issuance_prevout, contract_hash);
let asset_id = AssetId::from_entropy(entropy);

ensure!(asset.asset_id == asset_id, "invalid asset commitment");

debug!(
"verified asset commitment, asset id {} commits to prevout {:?} and contract hash {} ({:?})",
asset_id.to_hex(),
asset_id,
asset.issuance_prevout,
contract_hash.to_hex(),
contract_hash,
asset.contract,
);
Ok(())
Expand Down Expand Up @@ -292,7 +279,6 @@ fn format_deletion_sig_msg(asset: &Asset) -> String {
#[cfg(test)]
mod tests {
use super::*;
use bitcoin_hashes::hex::ToHex;
use std::path::PathBuf;

#[test]
Expand All @@ -304,7 +290,7 @@ mod tests {
fn test1_asset_load() -> Result<()> {
let asset = Asset::load(PathBuf::from("test/asset-b1405e.json")).unwrap();
assert_eq!(
asset.asset_id.to_hex(),
asset.asset_id.to_string(),
"b1405e4eefa91c6690198b4f85d73e8e0babee08f73b2c8af411486dc28dbc05"
);
assert_eq!(asset.fields.ticker, Some("PPP".to_string()));
Expand Down
12 changes: 6 additions & 6 deletions src/bin/liquid-asset-registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ use reqwest::{blocking::Client, StatusCode};
use serde_json::Value;
use structopt::StructOpt;

use bitcoin_hashes::hex::ToHex;
use elements::ContractHash;

use asset_registry::asset::{contract_json_hash, Asset, AssetRequest};
use asset_registry::asset::{Asset, AssetRequest};
use asset_registry::chain::ChainQuery;
use asset_registry::errors::{join_err, Result, ResultExt};

Expand Down Expand Up @@ -84,10 +84,10 @@ fn main() -> Result<()> {
debug!("verifying asset: {:?}", asset);

match asset.verify(chain.as_ref()) {
Ok(()) => println!("{},true", asset.id().to_hex()),
Ok(()) => println!("{},true", asset.id()),
Err(err) => {
warn!("asset verification failed: {}", join_err(&err));
println!("{},false", asset.id().to_hex());
println!("{},false", asset.id());
failed = true;
}
}
Expand Down Expand Up @@ -121,8 +121,8 @@ fn main() -> Result<()> {
let contract: Value = serde_json::from_str(&json).context("invalid contract json")?;

if hash {
let hash = contract_json_hash(&contract)?;
println!("{}", hash.to_hex());
let hash = ContractHash::from_json_contract(&contract.to_string())?;
println!("{}", hash);
} else {
// deserializing and re-serializing gets us canonical encoding, with json keys sorted lexicographically
let contract_str = serde_json::to_string(&contract)?;
Expand Down
75 changes: 39 additions & 36 deletions src/chain.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use reqwest::{blocking::Client as ReqClient, StatusCode};
use serde_json::Value;

use bitcoin::{BlockHash, Txid};
use bitcoin_hashes::{hex::ToHex, Hash};
use elements::{encode::deserialize, issuance::ContractHash, AssetId, Transaction};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::hex::FromHex;
use elements::{
encode::deserialize, issuance::ContractHash, AssetId, BlockHash, Transaction, Txid,
};

use crate::asset::Asset;
use crate::errors::{OptionExt, Result, ResultExt};
Expand Down Expand Up @@ -32,7 +34,7 @@ impl ChainQuery {
pub fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>> {
let resp = self
.rclient
.get(&format!("{}/tx/{}/hex", self.api_url, txid.to_hex()))
.get(&format!("{}/tx/{}/hex", self.api_url, txid))
.send()
.context("failed fetching tx")?;

Expand All @@ -44,15 +46,16 @@ impl ChainQuery {
.context("failed fetching tx")?
.text()
.context("failed reading tx")?;
let raw = Vec::from_hex(hex.trim())?;

Some(deserialize(&hex::decode(hex.trim())?)?)
Some(deserialize(&raw)?)
})
}

pub fn get_tx_status(&self, txid: &Txid) -> Result<Option<BlockId>> {
let status: Value = self
.rclient
.get(&format!("{}/tx/{}/status", self.api_url, txid.to_hex()))
.get(&format!("{}/tx/{}/status", self.api_url, txid))
.send()
.context("failed fetching tx status")?
.error_for_status()
Expand All @@ -69,7 +72,7 @@ impl ChainQuery {
pub fn get_asset(&self, asset_id: &AssetId) -> Result<Option<Value>> {
let resp = self
.rclient
.get(&format!("{}/asset/{}", self.api_url, asset_id.to_hex()))
.get(&format!("{}/asset/{}", self.api_url, asset_id))
.send()
.context("failed fetching tx")?;

Expand Down Expand Up @@ -108,15 +111,17 @@ pub fn verify_asset_issuance_tx(chain: &ChainQuery, asset: &Asset) -> Result<Blo
"issuance prevout mismatch"
);
ensure!(
txin.asset_issuance.asset_entropy == asset.contract_hash()?.into_inner(),
txin.asset_issuance.asset_entropy == asset.contract_hash().to_byte_array(),
"issuance entropy does not match contract hash"
);

// this is already verified as part of verify_asset_commitment, but we double-check here as a
// sanity check
let entropy = AssetId::generate_asset_entropy(
txin.previous_output,
ContractHash::from_inner(txin.asset_issuance.asset_entropy),
ContractHash::from(sha256::Hash::from_byte_array(
txin.asset_issuance.asset_entropy,
)),
);
ensure!(
AssetId::from_entropy(entropy) == asset.asset_id,
Expand All @@ -125,8 +130,7 @@ pub fn verify_asset_issuance_tx(chain: &ChainQuery, asset: &Asset) -> Result<Blo

debug!(
"verified on-chain issuance of asset {}, tx input {:?}",
asset.asset_id.to_hex(),
asset.issuance_txin,
asset.asset_id, asset.issuance_txin,
);

Ok(blockid)
Expand All @@ -136,46 +140,46 @@ pub fn verify_asset_issuance_tx(chain: &ChainQuery, asset: &Asset) -> Result<Blo
#[cfg(test)]
pub mod tests {
use super::*;
use rocket as r;
use rocket_contrib::json::JsonValue;
use std::{fs, str::FromStr};
use rocket::serde::json::Json;
use serde_json::Value;
use std::path::PathBuf;
use std::sync::Once;
use std::{fs, str::FromStr};

static SPAWN_ONCE: Once = Once::new();

// a server that identifies as "test.dev" and verifies any requested asset id
#[rocket::main]
async fn launch_mock_esplora_server() {
let config = rocket::Config::figment().merge(("port", 58713));
let rocket = rocket::custom(config).mount(
"/",
rocket::routes![tx_hex_handler, tx_status_handler, asset_handler],
);
rocket.launch().await.unwrap();
}
pub fn spawn_mock_esplora_server() {
SPAWN_ONCE.call_once(|| {
let config = r::config::Config::build(r::config::Environment::Development)
.port(58713)
.finalize()
.unwrap();
let rocket = r::custom(config).mount(
"/",
routes![tx_hex_handler, tx_status_handler, asset_handler],
);

std::thread::spawn(|| rocket.launch());
})
std::thread::spawn(launch_mock_esplora_server);
});
}

#[get("/tx/<txid>/hex")]
fn tx_hex_handler(txid: String) -> Result<String> {
#[rocket::get("/tx/<txid>/hex")]
fn tx_hex_handler(txid: &str) -> String {
let path = format!("test/issuance-tx-{}.hex", &txid[..6]);
Ok(fs::read_to_string(path)?)
fs::read_to_string(path).unwrap()
}

#[get("/asset/<asset_id>")]
fn asset_handler(asset_id: String) -> Result<JsonValue> {
#[rocket::get("/asset/<asset_id>")]
fn asset_handler(asset_id: &str) -> Json<Value> {
let path = format!("test/asset-{}.json", &asset_id[..6]);
let jsonstr = fs::read_to_string(path)?;
Ok(JsonValue::from(serde_json::Value::from_str(&jsonstr)?))
let jsonstr = fs::read_to_string(path).unwrap();
Json(serde_json::Value::from_str(&jsonstr).unwrap())
}

#[get("/tx/<_txid>/status")]
fn tx_status_handler(_txid: String) -> JsonValue {
JsonValue::from(json!({
#[rocket::get("/tx/<_txid>/status")]
fn tx_status_handler(_txid: &str) -> Json<Value> {
Json(json!({
"confirmed": true,
"block_height": 999,
"block_hash": "6ef1b8ac6cfacae9493e8d214d5ddd70322abe39bc0ab82727849b47bfb1fce6",
Expand All @@ -186,7 +190,6 @@ pub mod tests {
#[test]
fn test0_init() {
stderrlog::new().verbosity(3).init().ok();

spawn_mock_esplora_server();
}

Expand Down
Loading