Skip to main content

qcoin_types/
lib.rs

1use qcoin_crypto::{PublicKey, Signature};
2use serde::{Deserialize, Serialize};
3
4pub type Hash256 = [u8; 32];
5
6#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub struct BlockHeader {
8    pub parent_hash: Hash256,
9    pub state_root: Hash256,
10    pub tx_root: Hash256,
11    pub height: u64,
12    pub timestamp: u64,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Block {
17    pub header: BlockHeader,
18    pub transactions: Vec<Transaction>,
19    pub proposer_public_key: PublicKey,
20    pub signature: Signature,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24pub enum AssetKind {
25    Fungible,
26    NonFungible,
27    SemiFungible,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct AssetId(pub Hash256);
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34pub struct AssetDefinition {
35    pub issuer_script_hash: Hash256,
36    pub metadata_root: Hash256,
37    pub max_supply: Option<u128>,
38    pub decimals: u8,
39    pub kind: AssetKind,
40}
41
42pub fn derive_asset_id(definition: &AssetDefinition, chain_id: u32) -> AssetId {
43    let mut preimage = Vec::new();
44    const DOMAIN_SEPARATOR: &[u8] = b"QCOIN_ASSET_ID_V1";
45
46    preimage.extend_from_slice(DOMAIN_SEPARATOR);
47    preimage.extend_from_slice(&chain_id.to_le_bytes());
48    preimage.extend(consensus_codec::encode_asset_definition(definition));
49
50    AssetId(*blake3::hash(&preimage).as_bytes())
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
54pub struct AssetAmount {
55    pub asset_id: AssetId,
56    pub amount: u128,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
60pub struct Output {
61    pub owner_script_hash: Hash256,
62    pub assets: Vec<AssetAmount>,
63    pub metadata_hash: Option<Hash256>,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
67pub struct TransactionInput {
68    pub tx_id: Hash256,
69    pub index: u32,
70}
71
72#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
73pub enum TransactionKind {
74    Transfer,
75    CreateAsset {
76        definition: AssetDefinition,
77        initial_supply: u128,
78    },
79    // later: MintAsset, BurnAsset, etc.
80}
81
82#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
83pub struct TransactionCore {
84    pub kind: TransactionKind,
85    pub inputs: Vec<TransactionInput>,
86    pub outputs: Vec<Output>,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
90pub struct TransactionWitness {
91    pub inputs: Vec<Vec<u8>>, // raw script/witness data for now
92}
93
94#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95pub struct Transaction {
96    pub core: TransactionCore,
97    pub witness: TransactionWitness,
98}
99
100impl Transaction {
101    pub fn tx_id(&self) -> Hash256 {
102        self.core.tx_id()
103    }
104
105    pub fn sighash(
106        &self,
107        input_index: usize,
108        prev_output: &Output,
109        script_hash: Hash256,
110        chain_id: u32,
111        flags: SighashFlags,
112    ) -> Hash256 {
113        let mut preimage = Vec::new();
114        const DOMAIN_SEPARATOR: &[u8] = b"QCOIN_SIGHASH_V1";
115
116        preimage.extend_from_slice(DOMAIN_SEPARATOR);
117        preimage.extend_from_slice(&chain_id.to_le_bytes());
118        preimage.extend(consensus_codec::encode_tx_core(&self.core));
119        preimage.extend(consensus_codec::encode_output(prev_output));
120        preimage.extend_from_slice(&(input_index as u64).to_le_bytes());
121        preimage.extend_from_slice(&script_hash);
122        preimage.extend_from_slice(&flags.0.to_le_bytes());
123
124        *blake3::hash(&preimage).as_bytes()
125    }
126}
127
128impl TransactionCore {
129    pub fn tx_id(&self) -> Hash256 {
130        let serialized = consensus_codec::encode_tx_core(self);
131        *blake3::hash(&serialized).as_bytes()
132    }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
136pub struct SighashFlags(pub u32);
137
138pub fn create_asset_transaction(
139    issuer_script_hash: Hash256,
140    kind: AssetKind,
141    metadata_root: Hash256,
142    max_supply: Option<u128>,
143    decimals: u8,
144    initial_supply: u128,
145    destination_script_hash: Hash256,
146    chain_id: u32,
147) -> (AssetDefinition, Transaction) {
148    let definition = AssetDefinition {
149        issuer_script_hash,
150        metadata_root,
151        max_supply,
152        decimals,
153        kind,
154    };
155
156    let asset_id = derive_asset_id(&definition, chain_id);
157
158    let transaction = Transaction {
159        core: TransactionCore {
160            kind: TransactionKind::CreateAsset {
161                definition: definition.clone(),
162                initial_supply,
163            },
164            inputs: vec![],
165            outputs: vec![Output {
166                owner_script_hash: destination_script_hash,
167                assets: vec![AssetAmount {
168                    asset_id: asset_id.clone(),
169                    amount: initial_supply,
170                }],
171                metadata_hash: None,
172            }],
173        },
174        witness: TransactionWitness::default(),
175    };
176
177    (definition, transaction)
178}
179
180pub mod consensus_codec {
181    use super::{
182        AssetAmount, AssetDefinition, AssetKind, BlockHeader, Hash256, Output, TransactionCore,
183        TransactionInput, TransactionKind,
184    };
185
186    fn encode_len(len: usize, out: &mut Vec<u8>) {
187        let len: u32 = len
188            .try_into()
189            .expect("consensus encoding length should fit into u32");
190        out.extend_from_slice(&len.to_le_bytes());
191    }
192
193    pub fn encode_hash(hash: &Hash256, out: &mut Vec<u8>) {
194        out.extend_from_slice(hash);
195    }
196
197    fn encode_transaction_input(input: &TransactionInput, out: &mut Vec<u8>) {
198        encode_hash(&input.tx_id, out);
199        out.extend_from_slice(&input.index.to_le_bytes());
200    }
201
202    fn encode_asset_amount(asset: &AssetAmount, out: &mut Vec<u8>) {
203        encode_hash(&asset.asset_id.0, out);
204        out.extend_from_slice(&asset.amount.to_le_bytes());
205    }
206
207    pub fn encode_asset_definition(definition: &AssetDefinition) -> Vec<u8> {
208        let mut out = Vec::new();
209        encode_asset_definition_into(definition, &mut out);
210        out
211    }
212
213    fn encode_asset_definition_into(definition: &AssetDefinition, out: &mut Vec<u8>) {
214        encode_hash(&definition.issuer_script_hash, out);
215        out.push(match definition.kind {
216            AssetKind::Fungible => 0,
217            AssetKind::NonFungible => 1,
218            AssetKind::SemiFungible => 2,
219        });
220        encode_hash(&definition.metadata_root, out);
221        match definition.max_supply {
222            Some(max) => {
223                out.push(1);
224                out.extend_from_slice(&max.to_le_bytes());
225            }
226            None => out.push(0),
227        }
228        out.push(definition.decimals);
229    }
230
231    pub fn encode_output(output: &Output) -> Vec<u8> {
232        let mut out = Vec::new();
233        encode_output_into(output, &mut out);
234        out
235    }
236
237    pub fn encode_output_into(output: &Output, out: &mut Vec<u8>) {
238        encode_hash(&output.owner_script_hash, out);
239        encode_len(output.assets.len(), out);
240        for asset in &output.assets {
241            encode_asset_amount(asset, out);
242        }
243
244        match &output.metadata_hash {
245            Some(hash) => {
246                out.push(1);
247                encode_hash(hash, out);
248            }
249            None => out.push(0),
250        }
251    }
252
253    pub fn encode_tx_core(core: &TransactionCore) -> Vec<u8> {
254        let mut out = Vec::new();
255        encode_tx_core_into(core, &mut out);
256        out
257    }
258
259    pub fn encode_tx_core_into(core: &TransactionCore, out: &mut Vec<u8>) {
260        out.push(match core.kind {
261            TransactionKind::Transfer => 0,
262            TransactionKind::CreateAsset { .. } => 1,
263        });
264
265        encode_len(core.inputs.len(), out);
266        for input in &core.inputs {
267            encode_transaction_input(input, out);
268        }
269
270        encode_len(core.outputs.len(), out);
271        for output in &core.outputs {
272            encode_output_into(output, out);
273        }
274
275        if let TransactionKind::CreateAsset {
276            definition,
277            initial_supply,
278        } = &core.kind
279        {
280            encode_asset_definition_into(definition, out);
281            out.extend_from_slice(&initial_supply.to_le_bytes());
282        }
283    }
284
285    pub fn encode_block_header(header: &BlockHeader) -> Vec<u8> {
286        let mut out = Vec::new();
287        encode_hash(&header.parent_hash, &mut out);
288        encode_hash(&header.state_root, &mut out);
289        encode_hash(&header.tx_root, &mut out);
290        out.extend_from_slice(&header.height.to_le_bytes());
291        out.extend_from_slice(&header.timestamp.to_le_bytes());
292        out
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    fn base_transaction() -> Transaction {
301        Transaction {
302            core: TransactionCore {
303                kind: TransactionKind::Transfer,
304                inputs: vec![],
305                outputs: vec![Output {
306                    owner_script_hash: [0u8; 32],
307                    assets: vec![AssetAmount {
308                        asset_id: AssetId([2u8; 32]),
309                        amount: 10,
310                    }],
311                    metadata_hash: None,
312                }],
313            },
314            witness: TransactionWitness::default(),
315        }
316    }
317
318    #[test]
319    fn transaction_id_changes_when_payload_changes() {
320        let mut tx = base_transaction();
321        let original_id = tx.tx_id();
322
323        tx.core.outputs.push(Output {
324            owner_script_hash: [1u8; 32],
325            assets: vec![AssetAmount {
326                asset_id: AssetId([3u8; 32]),
327                amount: 1,
328            }],
329            metadata_hash: None,
330        });
331
332        let mutated_id = tx.tx_id();
333        assert_ne!(original_id, mutated_id);
334    }
335
336    #[test]
337    fn create_asset_transaction_derives_expected_asset_id_and_supply() {
338        let issuer_script_hash = [4u8; 32];
339        let metadata_root = [9u8; 32];
340        let destination_script_hash = [8u8; 32];
341        let initial_supply = 500;
342        let chain_id = 42;
343        let (definition, transaction) = create_asset_transaction(
344            issuer_script_hash,
345            AssetKind::SemiFungible,
346            metadata_root,
347            Some(1_000),
348            2,
349            initial_supply,
350            destination_script_hash,
351            chain_id,
352        );
353
354        let asset_id = derive_asset_id(&definition, chain_id);
355
356        assert!(matches!(
357            transaction.core.kind,
358            TransactionKind::CreateAsset { .. }
359        ));
360        assert!(transaction.core.inputs.is_empty());
361        assert_eq!(transaction.core.outputs.len(), 1);
362
363        assert_eq!(definition.kind, AssetKind::SemiFungible);
364        assert_eq!(definition.issuer_script_hash, issuer_script_hash);
365        assert_eq!(definition.metadata_root, metadata_root);
366        assert_eq!(definition.max_supply, Some(1_000));
367        assert_eq!(definition.decimals, 2);
368
369        let minted_output = transaction
370            .core
371            .outputs
372            .first()
373            .expect("minted output should exist");
374        assert_eq!(minted_output.owner_script_hash, destination_script_hash);
375        assert_eq!(minted_output.assets.len(), 1);
376        let minted_asset = minted_output
377            .assets
378            .first()
379            .expect("minted asset amount should be present");
380        assert_eq!(minted_asset.asset_id, asset_id);
381        assert_eq!(minted_asset.amount, initial_supply);
382        assert!(minted_output.metadata_hash.is_none());
383    }
384}