Skip to content

Commit a19e347

Browse files
committed
Merge #182: ELIP-0100 implementation
a8f4c8c remove --all from cargo test (Riccardo Casatta) 0a78d54 ELIP-0100 implementation (Riccardo Casatta) 1852ef5 ci: cargo update byteorder (Riccardo Casatta) Pull request description: as defined in https://github.com/ElementsProject/ELIPs/blob/main/elip-0100.mediawiki but considering proposed fixes in ElementsProject/ELIPs#7 ACKs for top commit: apoelstra: ACK a8f4c8c Tree-SHA512: e2fb7de7df27622842126486d11d91a7dacea9bccb9580109e23ebdb94e0f295394ade008c920215f129892fd137ae49f4fb2f6e8c333ce3451e84e572f34d04
2 parents ecfc231 + a8f4c8c commit a19e347

File tree

3 files changed

+286
-3
lines changed

3 files changed

+286
-3
lines changed

contrib/test.sh

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@ if cargo --version | grep "1\.48"; then
1414

1515
cargo update -p log --precise 0.4.18
1616
cargo update -p tempfile --precise 3.6.0
17+
cargo update -p byteorder --precise 1.4.3
1718
fi
1819

1920
if [ "$DO_FEATURE_MATRIX" = true ]
2021
then
2122
# Test without any features first
22-
cargo test --all --verbose --no-default-features
23+
cargo test --verbose --no-default-features
2324
# Then test with the default features
24-
cargo test --all --verbose
25+
cargo test --verbose
2526
# Then test with the default features
26-
cargo test --all --all-features --verbose
27+
cargo test --all-features --verbose
2728

2829
# Also build and run each example to catch regressions
2930
cargo build --examples

src/pset/elip100.rs

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
//!
2+
//! An implementation of ELIP0100 as defined in
3+
//! <https://github.com/ElementsProject/ELIPs/blob/main/elip-0100.mediawiki>
4+
//! but excluding contract validation.
5+
//!
6+
//! ELIP0100 defines how to inlcude assets metadata, such as the contract defining the asset and
7+
//! the issuance prevout inside a PSET
8+
//!
9+
//! To use check [`PartiallySignedTransaction::add_asset_metadata`] and
10+
//! [`PartiallySignedTransaction::get_asset_metadata`]
11+
//!
12+
13+
use std::io::Cursor;
14+
15+
use super::{raw::ProprietaryKey, PartiallySignedTransaction};
16+
use crate::{
17+
encode::{self, Decodable, Encodable},
18+
AssetId, OutPoint,
19+
};
20+
21+
/// keytype as defined in ELIP0100
22+
pub const PSBT_ELEMENTS_HWW_GLOBAL_ASSET_METADATA: u8 = 0x00u8;
23+
24+
/// Prefix for PSET hardware wallet extension as defined in ELIP0100
25+
pub const PSET_HWW_PREFIX: &[u8] = b"pset_hww";
26+
27+
/// Contains extension to add and retrieve from the PSET contract informations related to an asset
28+
impl PartiallySignedTransaction {
29+
/// Add contract information to the PSET, returns None if it wasn't present or Some with the old
30+
/// data if already in the PSET
31+
pub fn add_asset_metadata(
32+
&mut self,
33+
asset_id: AssetId,
34+
asset_meta: &AssetMetadata,
35+
) -> Option<Result<AssetMetadata, encode::Error>> {
36+
let key = prop_key(&asset_id);
37+
self.global
38+
.proprietary
39+
.insert(key, asset_meta.serialize())
40+
.map(|old| AssetMetadata::deserialize(&old))
41+
}
42+
43+
/// Get contract information from the PSET, returns None if there are no information regarding
44+
/// the given `asset_id`` in the PSET
45+
pub fn get_asset_metadata(
46+
&self,
47+
asset_id: AssetId,
48+
) -> Option<Result<AssetMetadata, encode::Error>> {
49+
let key = prop_key(&asset_id);
50+
51+
self.global
52+
.proprietary
53+
.get(&key)
54+
.map(|data| AssetMetadata::deserialize(data))
55+
}
56+
}
57+
58+
/// Asset metadata, the contract and the outpoint used to issue the asset
59+
#[derive(Debug, PartialEq, Eq)]
60+
pub struct AssetMetadata {
61+
contract: String,
62+
issuance_prevout: OutPoint,
63+
}
64+
65+
fn prop_key(asset_id: &AssetId) -> ProprietaryKey {
66+
let mut key = Vec::with_capacity(32);
67+
asset_id
68+
.consensus_encode(&mut key)
69+
.expect("vec doesn't err"); // equivalent to asset_tag
70+
71+
ProprietaryKey {
72+
prefix: PSET_HWW_PREFIX.to_vec(),
73+
subtype: 0x00,
74+
key,
75+
}
76+
}
77+
78+
impl AssetMetadata {
79+
/// Returns the contract as string containing a json
80+
pub fn contract(&self) -> &str {
81+
&self.contract
82+
}
83+
84+
/// Returns the issuance prevout where the asset has been issued
85+
pub fn issuance_prevout(&self) -> OutPoint {
86+
self.issuance_prevout
87+
}
88+
89+
/// Serialize this metadata as defined by ELIP0100
90+
///
91+
/// `<compact size uint contractLen><contract><32-byte prevoutTxid><32-bit little endian uint prevoutIndex>`
92+
pub fn serialize(&self) -> Vec<u8> {
93+
let mut result = vec![];
94+
95+
encode::consensus_encode_with_size(self.contract.as_bytes(), &mut result)
96+
.expect("vec doesn't err");
97+
98+
self.issuance_prevout
99+
.consensus_encode(&mut result)
100+
.expect("vec doesn't err");
101+
102+
result
103+
}
104+
105+
/// Deserialize this metadata as defined by ELIP0100
106+
pub fn deserialize(data: &[u8]) -> Result<AssetMetadata, encode::Error> {
107+
let mut cursor = Cursor::new(data);
108+
let str_bytes = Vec::<u8>::consensus_decode(&mut cursor)?;
109+
110+
let contract = String::from_utf8(str_bytes).map_err(|_| {
111+
encode::Error::ParseFailed("utf8 conversion fail on the contract string")
112+
})?;
113+
114+
let issuance_prevout = OutPoint::consensus_decode(&mut cursor)?;
115+
116+
Ok(AssetMetadata {
117+
contract,
118+
issuance_prevout,
119+
})
120+
}
121+
}
122+
123+
#[cfg(test)]
124+
mod test {
125+
use std::str::FromStr;
126+
127+
use crate::encode::serialize;
128+
use crate::{OutPoint, Txid};
129+
use bitcoin::hashes::hex::FromHex;
130+
use bitcoin::hashes::Hash;
131+
132+
use crate::{
133+
encode::{serialize_hex, Encodable},
134+
hex::ToHex,
135+
pset::{elip100::PSET_HWW_PREFIX, map::Map, PartiallySignedTransaction},
136+
AssetId,
137+
};
138+
139+
use super::{prop_key, AssetMetadata};
140+
141+
#[cfg(feature = "json-contract")]
142+
const CONTRACT_HASH: &str = "3c7f0a53c2ff5b99590620d7f6604a7a3a7bfbaaa6aa61f7bfc7833ca03cde82";
143+
144+
const VALID_CONTRACT: &str = r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":"USDt","version":0}"#;
145+
const ISSUANCE_PREVOUT: &str =
146+
"9596d259270ef5bac0020435e6d859aea633409483ba64e232b8ba04ce288668:0";
147+
const ASSET_ID: &str = "ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2";
148+
149+
const ELIP0100_IDENTIFIER: &str = "fc08707365745f68777700";
150+
const ELIP0100_ASSET_TAG: &str =
151+
"48f835622f34e8fdc313c90d4a8659aa4afe993e32dcb03ae6ec9ccdc6fcbe18";
152+
153+
const ELIP0100_CONTRACT: &str = r#"{"entity":{"domain":"example.com"},"issuer_pubkey":"03455ee7cedc97b0ba435b80066fc92c963a34c600317981d135330c4ee43ac7a3","name":"Testcoin","precision":2,"ticker":"TEST","version":0}"#;
154+
const ELIP0100_PREVOUT_TXID: &str =
155+
"3514a07cf4812272c24a898c482f587a51126beef8c9b76a9e30bf41b0cbe53c";
156+
157+
const ELIP0100_PREVOUT_VOUT: u32 = 1;
158+
const ELIP0100_ASSET_METADATA_RECORD_KEY: &str =
159+
"fc08707365745f6877770018befcc6cd9cece63ab0dc323e99fe4aaa59864a0dc913c3fde8342f6235f848";
160+
const ELIP0100_ASSET_METADATA_RECORD_VALUE_WRONG: &str = "b47b22656e74697479223a7b22646f6d61696e223a226578616d706c652e636f6d227d2c226973737565725f7075626b6579223a22303334353565653763656463393762306261343335623830303636666339326339363361333463363030333137393831643133353333306334656534336163376133222c226e616d65223a2254657374636f696e222c22707265636973696f6e223a322c227469636b6572223a2254455354222c2276657273696f6e223a307d3514a07cf4812272c24a898c482f587a51126beef8c9b76a9e30bf41b0cbe53c01000000";
161+
162+
const ELIP0100_ASSET_METADATA_RECORD_VALUE: &str = "b47b22656e74697479223a7b22646f6d61696e223a226578616d706c652e636f6d227d2c226973737565725f7075626b6579223a22303334353565653763656463393762306261343335623830303636666339326339363361333463363030333137393831643133353333306334656534336163376133222c226e616d65223a2254657374636f696e222c22707265636973696f6e223a322c227469636b6572223a2254455354222c2276657273696f6e223a307d3ce5cbb041bf309e6ab7c9f8ee6b12517a582f488c894ac2722281f47ca0143501000000";
163+
fn mockup_asset_metadata() -> (AssetId, AssetMetadata) {
164+
(
165+
AssetId::from_str(ASSET_ID).unwrap(),
166+
AssetMetadata {
167+
contract: VALID_CONTRACT.to_string(),
168+
issuance_prevout: ISSUANCE_PREVOUT.parse().unwrap(),
169+
},
170+
)
171+
}
172+
173+
#[cfg(feature = "json-contract")]
174+
#[test]
175+
fn asset_metadata_roundtrip() {
176+
let (_, asset_metadata) = mockup_asset_metadata();
177+
let contract_hash = crate::ContractHash::from_str(CONTRACT_HASH).unwrap();
178+
assert_eq!(
179+
crate::ContractHash::from_json_contract(VALID_CONTRACT).unwrap(),
180+
contract_hash
181+
);
182+
assert_eq!(asset_metadata.serialize().to_hex(),"b47b22656e74697479223a7b22646f6d61696e223a227465746865722e746f227d2c226973737565725f7075626b6579223a22303333376363656563306265656130323332656265313463626130313937613966626434356663663265633934363734396465393230653731343334633262393034222c226e616d65223a2254657468657220555344222c22707265636973696f6e223a382c227469636b6572223a2255534474222c2276657273696f6e223a307d688628ce04bab832e264ba83944033a6ae59d8e6350402c0baf50e2759d2969500000000");
183+
184+
assert_eq!(
185+
AssetMetadata::deserialize(&asset_metadata.serialize()).unwrap(),
186+
asset_metadata
187+
);
188+
}
189+
190+
#[test]
191+
fn prop_key_serialize() {
192+
let asset_id = AssetId::from_str(ASSET_ID).unwrap();
193+
194+
let key = prop_key(&asset_id);
195+
let mut vec = vec![];
196+
key.consensus_encode(&mut vec).unwrap();
197+
198+
assert_eq!(
199+
vec.to_hex(),
200+
format!("08{}00{}", PSET_HWW_PREFIX.to_hex(), asset_id.into_tag())
201+
);
202+
203+
assert!(vec.to_hex().starts_with(&ELIP0100_IDENTIFIER[2..])); // cut prefix "fc: which is PSET_GLOBAL_PROPRIETARY serialized one level up
204+
}
205+
206+
#[test]
207+
fn set_get_asset_metadata() {
208+
let mut pset = PartiallySignedTransaction::new_v2();
209+
let (asset_id, asset_meta) = mockup_asset_metadata();
210+
211+
let old = pset.add_asset_metadata(asset_id, &asset_meta);
212+
assert!(old.is_none());
213+
let old = pset
214+
.add_asset_metadata(asset_id, &asset_meta)
215+
.unwrap()
216+
.unwrap();
217+
assert_eq!(old, asset_meta);
218+
219+
assert!(serialize_hex(&pset).contains(ELIP0100_IDENTIFIER));
220+
221+
let get = pset.get_asset_metadata(asset_id).unwrap().unwrap();
222+
assert_eq!(get, asset_meta);
223+
}
224+
225+
#[test]
226+
fn elip0100_test_vector() {
227+
let mut pset = PartiallySignedTransaction::new_v2();
228+
229+
let asset_id = AssetId::from_str(ELIP0100_ASSET_TAG).unwrap();
230+
let txid = Txid::from_str(ELIP0100_PREVOUT_TXID).unwrap();
231+
232+
let asset_meta = AssetMetadata {
233+
contract: ELIP0100_CONTRACT.to_string(),
234+
issuance_prevout: OutPoint {
235+
txid,
236+
vout: ELIP0100_PREVOUT_VOUT,
237+
},
238+
};
239+
240+
pset.add_asset_metadata(asset_id, &asset_meta);
241+
242+
let expected_key = Vec::<u8>::from_hex(ELIP0100_ASSET_METADATA_RECORD_KEY).unwrap();
243+
244+
let values: Vec<Vec<u8>> = pset
245+
.global
246+
.get_pairs()
247+
.unwrap()
248+
.into_iter()
249+
.filter(|p| serialize(&p.key)[1..] == expected_key[..]) // NOTE key serialization contains an initial varint with the lenght of the key which is not present in the test vector
250+
.map(|p| p.value)
251+
.collect();
252+
assert_eq!(values.len(), 1);
253+
assert_eq!(values[0].to_hex(), ELIP0100_ASSET_METADATA_RECORD_VALUE);
254+
255+
let txid_hex_non_convention = txid.as_byte_array().to_vec().to_hex();
256+
assert_eq!(
257+
ELIP0100_ASSET_METADATA_RECORD_VALUE,
258+
ELIP0100_ASSET_METADATA_RECORD_VALUE_WRONG
259+
.replace(ELIP0100_PREVOUT_TXID, &txid_hex_non_convention),
260+
"only change in the value is the txid"
261+
);
262+
}
263+
264+
#[cfg(feature = "json-contract")]
265+
#[test]
266+
fn elip0100_contract() {
267+
let txid = Txid::from_str(ELIP0100_PREVOUT_TXID).unwrap();
268+
let prevout = OutPoint {
269+
txid,
270+
vout: ELIP0100_PREVOUT_VOUT,
271+
};
272+
273+
let contract_hash = crate::ContractHash::from_json_contract(ELIP0100_CONTRACT).unwrap();
274+
let entropy = AssetId::generate_asset_entropy(prevout, contract_hash);
275+
let asset_id = AssetId::from_entropy(entropy);
276+
277+
let expected = AssetId::from_str(ELIP0100_ASSET_TAG).unwrap();
278+
279+
assert_eq!(asset_id.to_hex(), expected.to_hex());
280+
}
281+
}

src/pset/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod macros;
2929
mod map;
3030
pub mod raw;
3131
pub mod serialize;
32+
pub mod elip100;
3233

3334
#[cfg(feature = "base64")]
3435
mod str;

0 commit comments

Comments
 (0)