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 }
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>>, }
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}