hal_simplicity/actions/
tx.rs

1use std::convert::TryInto;
2
3use elements::bitcoin::{self, secp256k1};
4use elements::encode::{deserialize, serialize};
5use elements::hashes::Hash;
6use elements::secp256k1_zkp::{
7	Generator, PedersenCommitment, PublicKey, RangeProof, SurjectionProof, Tweak,
8};
9use elements::{
10	confidential, AssetIssuance, OutPoint, Script, Transaction, TxIn, TxInWitness, TxOut,
11	TxOutWitness,
12};
13
14use crate::confidential::{
15	ConfidentialAssetInfo, ConfidentialNonceInfo, ConfidentialType, ConfidentialValueInfo,
16};
17use crate::tx::{
18	AssetIssuanceInfo, InputInfo, InputScriptInfo, InputWitnessInfo, OutputInfo, OutputScriptInfo,
19	OutputWitnessInfo, PeginDataInfo, PegoutDataInfo, TransactionInfo,
20};
21use crate::Network;
22
23#[derive(Debug, thiserror::Error)]
24pub enum TxError {
25	#[error("invalid JSON provided: {0}")]
26	JsonParse(serde_json::Error),
27
28	#[error("failed to decode raw transaction hex: {0}")]
29	TxHex(hex::FromHexError),
30
31	#[error("invalid tx format: {0}")]
32	TxDeserialize(elements::encode::Error),
33
34	#[error("field \"{field}\" is required.")]
35	MissingField {
36		field: String,
37	},
38
39	#[error("invalid prevout format: {0}")]
40	PrevoutParse(bitcoin::blockdata::transaction::ParseOutPointError),
41
42	#[error("txid field given without vout field")]
43	MissingVout,
44
45	#[error("conflicting prevout information")]
46	ConflictingPrevout,
47
48	#[error("no previous output provided")]
49	NoPrevout,
50
51	#[error("invalid confidential commitment: {0}")]
52	ConfidentialCommitment(elements::secp256k1_zkp::Error),
53
54	#[error("invalid confidential publicKey: {0}")]
55	ConfidentialCommitmentPublicKey(secp256k1::Error),
56
57	#[error("wrong size of nonce field")]
58	NonceSize,
59
60	#[error("invalid size of asset_entropy")]
61	AssetEntropySize,
62
63	#[error("invalid asset_blinding_nonce: {0}")]
64	AssetBlindingNonce(elements::secp256k1_zkp::Error),
65
66	#[error("decoding script assembly is not yet supported")]
67	AsmNotSupported,
68
69	#[error("no scriptSig info provided")]
70	NoScriptSig,
71
72	#[error("no scriptPubKey info provided")]
73	NoScriptPubKey,
74
75	#[error("invalid outpoint in pegin_data: {0}")]
76	PeginOutpoint(bitcoin::blockdata::transaction::ParseOutPointError),
77
78	#[error("outpoint in pegin_data does not correspond to input value")]
79	PeginOutpointMismatch,
80
81	#[error("asset in pegin_data should be explicit")]
82	PeginAssetNotExplicit,
83
84	#[error("invalid rangeproof: {0}")]
85	RangeProof(elements::secp256k1_zkp::Error),
86
87	#[error("invalid sequence: {0}")]
88	Sequence(core::num::TryFromIntError),
89
90	#[error("addresses for different networks are used in the output scripts")]
91	MixedNetworks,
92
93	#[error("invalid surjection proof: {0}")]
94	SurjectionProof(elements::secp256k1_zkp::Error),
95
96	#[error("value in pegout_data does not correspond to output value")]
97	PegoutValueMismatch,
98
99	#[error("explicit value is required for pegout data")]
100	PegoutValueNotExplicit,
101
102	#[error("asset in pegout_data does not correspond to output value")]
103	PegoutAssetMismatch,
104}
105
106/// Check both ways to specify the outpoint and return error if conflicting.
107fn outpoint_from_input_info(input: &InputInfo) -> Result<OutPoint, TxError> {
108	let op1: Option<OutPoint> =
109		input.prevout.as_ref().map(|op| op.parse().map_err(TxError::PrevoutParse)).transpose()?;
110	let op2 = match input.txid {
111		Some(txid) => match input.vout {
112			Some(vout) => Some(OutPoint {
113				txid,
114				vout,
115			}),
116			None => return Err(TxError::MissingVout),
117		},
118		None => None,
119	};
120
121	match (op1, op2) {
122		(Some(op1), Some(op2)) => {
123			if op1 != op2 {
124				return Err(TxError::ConflictingPrevout);
125			}
126			Ok(op1)
127		}
128		(Some(op), None) => Ok(op),
129		(None, Some(op)) => Ok(op),
130		(None, None) => Err(TxError::NoPrevout),
131	}
132}
133
134fn bytes_32(bytes: &[u8]) -> Option<[u8; 32]> {
135	if bytes.len() != 32 {
136		None
137	} else {
138		let mut array = [0; 32];
139		for (x, y) in bytes.iter().zip(array.iter_mut()) {
140			*y = *x;
141		}
142		Some(array)
143	}
144}
145
146fn create_confidential_value(info: ConfidentialValueInfo) -> Result<confidential::Value, TxError> {
147	match info.type_ {
148		ConfidentialType::Null => Ok(confidential::Value::Null),
149		ConfidentialType::Explicit => {
150			Ok(confidential::Value::Explicit(info.value.ok_or_else(|| TxError::MissingField {
151				field: "value".to_string(),
152			})?))
153		}
154		ConfidentialType::Confidential => {
155			let commitment_data = info.commitment.ok_or_else(|| TxError::MissingField {
156				field: "commitment".to_string(),
157			})?;
158			let comm = PedersenCommitment::from_slice(&commitment_data.0[..])
159				.map_err(TxError::ConfidentialCommitment)?;
160			Ok(confidential::Value::Confidential(comm))
161		}
162	}
163}
164
165fn create_confidential_asset(info: ConfidentialAssetInfo) -> Result<confidential::Asset, TxError> {
166	match info.type_ {
167		ConfidentialType::Null => Ok(confidential::Asset::Null),
168		ConfidentialType::Explicit => {
169			Ok(confidential::Asset::Explicit(info.asset.ok_or_else(|| TxError::MissingField {
170				field: "asset".to_string(),
171			})?))
172		}
173		ConfidentialType::Confidential => {
174			let commitment_data = info.commitment.ok_or_else(|| TxError::MissingField {
175				field: "commitment".to_string(),
176			})?;
177			let gen = Generator::from_slice(&commitment_data.0[..])
178				.map_err(TxError::ConfidentialCommitment)?;
179			Ok(confidential::Asset::Confidential(gen))
180		}
181	}
182}
183
184fn create_confidential_nonce(info: ConfidentialNonceInfo) -> Result<confidential::Nonce, TxError> {
185	match info.type_ {
186		ConfidentialType::Null => Ok(confidential::Nonce::Null),
187		ConfidentialType::Explicit => {
188			let nonce = info.nonce.ok_or_else(|| TxError::MissingField {
189				field: "nonce".to_string(),
190			})?;
191			let bytes = bytes_32(&nonce.0[..]).ok_or(TxError::NonceSize)?;
192			Ok(confidential::Nonce::Explicit(bytes))
193		}
194		ConfidentialType::Confidential => {
195			let commitment_data = info.commitment.ok_or_else(|| TxError::MissingField {
196				field: "commitment".to_string(),
197			})?;
198			let pubkey = PublicKey::from_slice(&commitment_data.0[..])
199				.map_err(TxError::ConfidentialCommitmentPublicKey)?;
200			Ok(confidential::Nonce::Confidential(pubkey))
201		}
202	}
203}
204
205fn create_asset_issuance(info: AssetIssuanceInfo) -> Result<AssetIssuance, TxError> {
206	let asset_blinding_nonce_data =
207		info.asset_blinding_nonce.ok_or_else(|| TxError::MissingField {
208			field: "asset_blinding_nonce".to_string(),
209		})?;
210	let asset_blinding_nonce =
211		Tweak::from_slice(&asset_blinding_nonce_data.0[..]).map_err(TxError::AssetBlindingNonce)?;
212
213	let asset_entropy_data = info.asset_entropy.ok_or_else(|| TxError::MissingField {
214		field: "asset_entropy".to_string(),
215	})?;
216	let asset_entropy = bytes_32(&asset_entropy_data.0[..]).ok_or(TxError::AssetEntropySize)?;
217
218	let amount_info = info.amount.ok_or_else(|| TxError::MissingField {
219		field: "amount".to_string(),
220	})?;
221	let amount = create_confidential_value(amount_info)?;
222
223	let inflation_keys_info = info.inflation_keys.ok_or_else(|| TxError::MissingField {
224		field: "inflation_keys".to_string(),
225	})?;
226	let inflation_keys = create_confidential_value(inflation_keys_info)?;
227
228	Ok(AssetIssuance {
229		asset_blinding_nonce,
230		asset_entropy,
231		amount,
232		inflation_keys,
233	})
234}
235
236fn create_script_sig(ss: InputScriptInfo) -> Result<Script, TxError> {
237	if let Some(hex) = ss.hex {
238		Ok(hex.0.into())
239	} else if ss.asm.is_some() {
240		Err(TxError::AsmNotSupported)
241	} else {
242		Err(TxError::NoScriptSig)
243	}
244}
245
246fn create_pegin_witness(
247	pd: PeginDataInfo,
248	prevout: bitcoin::OutPoint,
249) -> Result<Vec<Vec<u8>>, TxError> {
250	let parsed_outpoint = pd.outpoint.parse().map_err(TxError::PeginOutpoint)?;
251	if prevout != parsed_outpoint {
252		return Err(TxError::PeginOutpointMismatch);
253	}
254
255	let asset = match create_confidential_asset(pd.asset)? {
256		confidential::Asset::Explicit(asset) => asset,
257		_ => return Err(TxError::PeginAssetNotExplicit),
258	};
259	Ok(vec![
260		serialize(&pd.value),
261		serialize(&asset),
262		pd.genesis_hash.to_byte_array().to_vec(),
263		serialize(&pd.claim_script.0),
264		serialize(&pd.mainchain_tx_hex.0),
265		serialize(&pd.merkle_proof.0),
266	])
267}
268
269fn convert_outpoint_to_btc(p: elements::OutPoint) -> bitcoin::OutPoint {
270	bitcoin::OutPoint {
271		txid: bitcoin::Txid::from_byte_array(p.txid.to_byte_array()),
272		vout: p.vout,
273	}
274}
275
276fn create_input_witness(
277	info: Option<InputWitnessInfo>,
278	pd: Option<PeginDataInfo>,
279	prevout: OutPoint,
280) -> Result<TxInWitness, TxError> {
281	let pegin_witness =
282		if let Some(info_wit) = info.as_ref().and_then(|info| info.pegin_witness.as_ref()) {
283			info_wit.iter().map(|h| h.clone().0).collect()
284		} else if let Some(pd) = pd {
285			create_pegin_witness(pd, convert_outpoint_to_btc(prevout))?
286		} else {
287			Default::default()
288		};
289
290	if let Some(wi) = info {
291		let amount_rangeproof = wi
292			.amount_rangeproof
293			.map(|b| RangeProof::from_slice(&b.0).map_err(TxError::RangeProof).map(Box::new))
294			.transpose()?;
295		let inflation_keys_rangeproof = wi
296			.inflation_keys_rangeproof
297			.map(|b| RangeProof::from_slice(&b.0).map_err(TxError::RangeProof).map(Box::new))
298			.transpose()?;
299
300		Ok(TxInWitness {
301			amount_rangeproof,
302			inflation_keys_rangeproof,
303			script_witness: match wi.script_witness {
304				Some(ref w) => w.iter().map(|h| h.clone().0).collect(),
305				None => Vec::new(),
306			},
307			pegin_witness,
308		})
309	} else {
310		Ok(TxInWitness {
311			pegin_witness,
312			..Default::default()
313		})
314	}
315}
316
317fn create_input(input: InputInfo) -> Result<TxIn, TxError> {
318	let has_issuance = input.has_issuance.unwrap_or(input.asset_issuance.is_some());
319	let is_pegin = input.is_pegin.unwrap_or(input.pegin_data.is_some());
320	let prevout = outpoint_from_input_info(&input)?;
321
322	let script_sig = input.script_sig.map(create_script_sig).transpose()?.unwrap_or_default();
323
324	let sequence = elements::Sequence::from_height(
325		input.sequence.unwrap_or_default().try_into().map_err(TxError::Sequence)?,
326	);
327
328	let asset_issuance = if has_issuance {
329		input.asset_issuance.map(create_asset_issuance).transpose()?.unwrap_or_default()
330	} else {
331		Default::default()
332	};
333
334	let witness = create_input_witness(input.witness, input.pegin_data, prevout)?;
335
336	Ok(TxIn {
337		previous_output: prevout,
338		script_sig,
339		sequence,
340		is_pegin,
341		asset_issuance,
342		witness,
343	})
344}
345
346fn create_script_pubkey(
347	spk: OutputScriptInfo,
348	used_network: &mut Option<Network>,
349) -> Result<Script, TxError> {
350	if let Some(hex) = spk.hex {
351		//TODO(stevenroose) do script sanity check to avoid blackhole?
352		Ok(hex.0.into())
353	} else if spk.asm.is_some() {
354		Err(TxError::AsmNotSupported)
355	} else if let Some(address) = spk.address {
356		// Error if another network had already been used.
357		if let Some(network) = Network::from_params(address.params) {
358			if used_network.replace(network).unwrap_or(network) != network {
359				return Err(TxError::MixedNetworks);
360			}
361		}
362		Ok(address.script_pubkey())
363	} else {
364		Err(TxError::NoScriptPubKey)
365	}
366}
367
368fn create_bitcoin_script_pubkey(
369	spk: hal::tx::OutputScriptInfo,
370) -> Result<bitcoin::ScriptBuf, TxError> {
371	if let Some(hex) = spk.hex {
372		//TODO(stevenroose) do script sanity check to avoid blackhole?
373		Ok(hex.0.into())
374	} else if spk.asm.is_some() {
375		Err(TxError::AsmNotSupported)
376	} else if let Some(address) = spk.address {
377		Ok(address.assume_checked().script_pubkey())
378	} else {
379		Err(TxError::NoScriptPubKey)
380	}
381}
382
383fn create_output_witness(w: OutputWitnessInfo) -> Result<TxOutWitness, TxError> {
384	let surjection_proof = w
385		.surjection_proof
386		.map(|b| {
387			SurjectionProof::from_slice(&b.0[..]).map_err(TxError::SurjectionProof).map(Box::new)
388		})
389		.transpose()?;
390	let rangeproof = w
391		.rangeproof
392		.map(|b| RangeProof::from_slice(&b.0[..]).map_err(TxError::RangeProof).map(Box::new))
393		.transpose()?;
394
395	Ok(TxOutWitness {
396		surjection_proof,
397		rangeproof,
398	})
399}
400
401fn create_script_pubkey_from_pegout_data(pd: PegoutDataInfo) -> Result<Script, TxError> {
402	let script_pubkey = create_bitcoin_script_pubkey(pd.script_pub_key)?;
403	let mut builder = elements::script::Builder::new()
404		.push_opcode(elements::opcodes::all::OP_RETURN)
405		.push_slice(&pd.genesis_hash.to_byte_array())
406		.push_slice(script_pubkey.as_bytes());
407	for d in pd.extra_data {
408		builder = builder.push_slice(&d.0);
409	}
410	Ok(builder.into_script())
411}
412
413fn create_output(output: OutputInfo) -> Result<TxOut, TxError> {
414	// Keep track of which network has been used in addresses and error if two different networks
415	// are used.
416	let mut used_network = None;
417	let value_info = output.value.ok_or_else(|| TxError::MissingField {
418		field: "value".to_string(),
419	})?;
420	let value = create_confidential_value(value_info)?;
421
422	let asset_info = output.asset.ok_or_else(|| TxError::MissingField {
423		field: "asset".to_string(),
424	})?;
425	let asset = create_confidential_asset(asset_info)?;
426
427	let nonce = output
428		.nonce
429		.map(create_confidential_nonce)
430		.transpose()?
431		.unwrap_or(confidential::Nonce::Null);
432
433	let script_pubkey = if let Some(spk) = output.script_pub_key {
434		create_script_pubkey(spk, &mut used_network)?
435	} else if let Some(pd) = output.pegout_data {
436		match value {
437			confidential::Value::Explicit(v) => {
438				if v != pd.value {
439					return Err(TxError::PegoutValueMismatch);
440				}
441			}
442			_ => return Err(TxError::PegoutValueNotExplicit),
443		}
444		let pd_asset = create_confidential_asset(pd.asset.clone())?;
445		if asset != pd_asset {
446			return Err(TxError::PegoutAssetMismatch);
447		}
448		create_script_pubkey_from_pegout_data(pd)?
449	} else {
450		Default::default()
451	};
452
453	let witness = output.witness.map(create_output_witness).transpose()?.unwrap_or_default();
454
455	Ok(TxOut {
456		asset,
457		value,
458		nonce,
459		script_pubkey,
460		witness,
461	})
462}
463
464/// Create a transaction from transaction info.
465pub fn tx_create(info: TransactionInfo) -> Result<Transaction, TxError> {
466	let version = info.version.ok_or_else(|| TxError::MissingField {
467		field: "version".to_string(),
468	})?;
469	let lock_time = info.locktime.ok_or_else(|| TxError::MissingField {
470		field: "locktime".to_string(),
471	})?;
472
473	let inputs = info
474		.inputs
475		.ok_or_else(|| TxError::MissingField {
476			field: "inputs".to_string(),
477		})?
478		.into_iter()
479		.map(create_input)
480		.collect::<Result<Vec<_>, _>>()?;
481
482	let outputs = info
483		.outputs
484		.ok_or_else(|| TxError::MissingField {
485			field: "outputs".to_string(),
486		})?
487		.into_iter()
488		.map(create_output)
489		.collect::<Result<Vec<_>, _>>()?;
490
491	Ok(Transaction {
492		version,
493		lock_time,
494		input: inputs,
495		output: outputs,
496	})
497}
498
499/// Decode a raw transaction and return transaction info.
500pub fn tx_decode(raw_tx_hex: &str, network: Network) -> Result<TransactionInfo, TxError> {
501	use crate::GetInfo;
502
503	let raw_tx = hex::decode(raw_tx_hex).map_err(TxError::TxHex)?;
504	let tx: Transaction = deserialize(&raw_tx).map_err(TxError::TxDeserialize)?;
505
506	Ok(tx.get_info(network))
507}