Skip to main content

ark/
connectors.rs

1
2
3use std::iter;
4use std::borrow::Cow;
5
6use bitcoin::{
7	Address, Amount, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Weight, Witness
8};
9use bitcoin::secp256k1::{Keypair, PublicKey};
10use bitcoin::sighash::{self, SighashCache, TapSighashType};
11use bitcoin_ext::{fee, KeypairExt, P2TR_DUST};
12
13use crate::SECP;
14
15/// The output index of the connector chain continuation in the connector tx.
16///
17/// In the last item of the chain, it is a connector output along with
18/// output at index 1.
19pub const CONNECTOR_TX_CHAIN_VOUT: u32 = 0;
20/// The output index of the connector output in the connector tx.
21pub const CONNECTOR_TX_CONNECTOR_VOUT: u32 = 1;
22
23/// The weight of each connector tx.
24const TX_WEIGHT: Weight = Weight::from_vb_unchecked(167);
25
26/// The witness weight of a connector input.
27pub const INPUT_WEIGHT: Weight = Weight::from_wu(66);
28
29
30/// Construct a tx that breaks up a single connector output into N connectors
31pub fn construct_multi_connector_tx(
32	prev: OutPoint,
33	nb_outputs: usize,
34	connector_spk: &Script,
35) -> Transaction {
36	assert_ne!(nb_outputs, 0);
37	Transaction {
38		version: bitcoin::transaction::Version(3),
39		lock_time: bitcoin::absolute::LockTime::ZERO,
40		input: vec![TxIn {
41			previous_output: prev,
42			script_sig: ScriptBuf::new(),
43			sequence: Sequence::MAX,
44			witness: Witness::new(),
45		}],
46		output: (0..nb_outputs)
47			.map(|_| TxOut { script_pubkey: connector_spk.to_owned(), value: P2TR_DUST })
48			.chain([fee::fee_anchor()])
49			.collect(),
50	}
51}
52
53/// A chain of connector outputs.
54///
55/// Each connector is a p2tr keyspend output for the provided key.
56/// Each connector has the p2tr dust value.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ConnectorChain {
59	/// The total number of connectors in the connector chain.
60	len: usize,
61
62	/// The scriptPubkey used by all connector outputs.
63	spk: ScriptBuf,
64
65	/// The prevout from where the chain starts.
66	///
67	/// This should be an output of the round transaction.
68	utxo: OutPoint,
69}
70
71impl ConnectorChain {
72	/// The total size in vbytes of the connector tree.
73	pub fn total_weight(len: usize) -> Weight {
74		assert_ne!(len, 0);
75		// len >= 1 just asserted; usize/u64 multiplication of bounded chain length.
76		#[allow(clippy::arithmetic_side_effects)]
77		let w = (len - 1) as u64 * TX_WEIGHT;
78		w
79	}
80
81	/// The budget needed for a chain of length `len` to pay for
82	/// one dust for the connector output per tx
83	pub fn required_budget(len: usize) -> Amount {
84		assert_ne!(len, 0);
85
86		// Each tx of the chain will hold one output to continue the
87		// chain + one output for the connector + one fee anchor output
88		// So the required budget is 1 dust per connector
89		P2TR_DUST * len as u64
90	}
91
92	/// Create the scriptPubkey to create a connector chain using the given publick key.
93	pub fn output_script(pubkey: PublicKey) -> ScriptBuf {
94		ScriptBuf::new_p2tr(&SECP, pubkey.x_only_public_key().0, None)
95	}
96
97	/// Create the address to create a connector chain using the given publick key.
98	pub fn address(network: Network, pubkey: PublicKey) -> Address {
99		Address::from_script(&Self::output_script(pubkey), network).unwrap()
100	}
101
102	/// Create a connector output.
103	pub fn output(len: usize, pubkey: PublicKey) -> TxOut {
104		TxOut {
105			script_pubkey: Self::output_script(pubkey),
106			value: Self::required_budget(len),
107		}
108	}
109
110	/// Create a new connector tree.
111	///
112	/// Before calling this method, a utxo should be created with a scriptPubkey
113	/// as specified by [ConnectorChain::output_script] or [ConnectorChain::address].
114	/// The amount in this output is expected to be exaclty equal to
115	/// [ConnectorChain::required_budget] for the given length.
116	pub fn new(len: usize, utxo: OutPoint, pubkey: PublicKey) -> ConnectorChain {
117		assert_ne!(len, 0);
118		let spk = Self::output_script(pubkey);
119
120		ConnectorChain { len, spk, utxo }
121	}
122
123	pub fn len(&self) -> usize {
124		self.len
125	}
126
127	// idx < self.len enforced by caller iterator, self.len > 0 by constructor.
128	#[allow(clippy::arithmetic_side_effects)]
129	fn tx(&self, prev: OutPoint, idx: usize) -> Transaction {
130		Transaction {
131			version: bitcoin::transaction::Version(3),
132			lock_time: bitcoin::absolute::LockTime::ZERO,
133			input: vec![TxIn {
134				previous_output: prev,
135				script_sig: ScriptBuf::new(),
136				sequence: Sequence::MAX,
137				witness: Witness::new(),
138			}],
139			output: vec![
140				// this is the continuation of the chain
141				// (or a connector output if the last tx)
142				TxOut {
143					script_pubkey: self.spk.to_owned(),
144					value: ConnectorChain::required_budget(self.len - idx - 1),
145				},
146				// this is the connector output
147				// (or the second one if the last tx)
148				TxOut {
149					script_pubkey: self.spk.to_owned(),
150					value: P2TR_DUST,
151				},
152				// this is the fee anchor output
153				fee::fee_anchor(),
154			],
155		}
156	}
157
158	/// NB we expect the output key here, not the internal key
159	// idx < self.len enforced by caller iterator, self.len > 0 by constructor.
160	#[allow(clippy::arithmetic_side_effects)]
161	fn sign_tx(&self, tx: &mut Transaction, idx: usize, keypair: &Keypair) {
162		let prevout = TxOut {
163			script_pubkey: self.spk.to_owned(),
164			value: ConnectorChain::required_budget(self.len - idx),
165		};
166		let mut shc = SighashCache::new(&*tx);
167		let sighash = shc.taproot_key_spend_signature_hash(
168			0, &sighash::Prevouts::All(&[prevout]), TapSighashType::Default,
169		).expect("sighash error");
170		let sig = SECP.sign_schnorr_with_aux_rand(&sighash.into(), &keypair, &rand::random());
171		tx.input[0].witness = Witness::from_slice(&[&sig[..]]);
172	}
173
174	/// Iterator over the signed transactions in this chain.
175	///
176	/// We expect the internal key here, not the output key.
177	pub fn iter_signed_txs(
178		&self,
179		sign_key: &Keypair,
180	) -> Result<ConnectorTxIter<'_>, InvalidSigningKeyError> {
181		if self.spk == ConnectorChain::output_script(sign_key.public_key()) {
182			Ok(ConnectorTxIter {
183				chain: Cow::Borrowed(self),
184				sign_key: Some(sign_key.for_keyspend_only(&*SECP)),
185				prev: self.utxo,
186				idx: 0,
187			})
188		} else {
189			Err(InvalidSigningKeyError)
190		}
191	}
192
193	/// Iterator over the transactions in this chain.
194	pub fn iter_unsigned_txs(&self) -> ConnectorTxIter<'_> {
195		ConnectorTxIter {
196			chain: Cow::Borrowed(self),
197			sign_key: None,
198			prev: self.utxo,
199			idx: 0,
200		}
201	}
202
203	/// Iterator over the connector outpoints and unsigned txs in this chain.
204	pub fn connectors(&self) -> ConnectorIter<'_> {
205		ConnectorIter {
206			txs: self.iter_unsigned_txs(),
207			maybe_last: Some((self.utxo, None)),
208		}
209	}
210
211	/// Iterator over the connector outpoints and signed txs in this chain.
212	///
213	/// We expect the internal key here, not the output key.
214	pub fn connectors_signed(
215		&self,
216		sign_key: &Keypair,
217	) -> Result<ConnectorIter<'_>, InvalidSigningKeyError> {
218		Ok(ConnectorIter {
219			txs: self.iter_signed_txs(sign_key)?,
220			maybe_last: Some((self.utxo, None)),
221		})
222	}
223}
224
225/// An iterator over transactions in a [ConnectorChain].
226///
227/// See [ConnectorChain::iter_unsigned_txs] and
228/// [ConnectorChain::iter_signed_txs] for more info.
229pub struct ConnectorTxIter<'a> {
230	chain: Cow<'a, ConnectorChain>,
231	sign_key: Option<Keypair>,
232
233	prev: OutPoint,
234	idx: usize,
235}
236
237impl<'a> ConnectorTxIter<'a> {
238	/// Upgrade this iterator to a signing iterator.
239	pub fn signing(&mut self, sign_key: Keypair) {
240		self.sign_key = Some(sign_key);
241	}
242
243	/// Convert into owned iterator.
244	pub fn into_owned(self) -> ConnectorTxIter<'static> {
245		ConnectorTxIter {
246			chain: Cow::Owned(self.chain.into_owned()),
247			sign_key: self.sign_key,
248			prev: self.prev,
249			idx: self.idx,
250		}
251	}
252}
253
254impl<'a> iter::Iterator for ConnectorTxIter<'a> {
255	type Item = Transaction;
256
257	// chain.len > 0 by constructor; idx bounded by chain.len.
258	#[allow(clippy::arithmetic_side_effects)]
259	fn next(&mut self) -> Option<Self::Item> {
260		if self.idx >= self.chain.len - 1 {
261			return None;
262		}
263
264		let mut ret = self.chain.tx(self.prev, self.idx);
265		if let Some(ref keypair) = self.sign_key {
266			self.chain.sign_tx(&mut ret, self.idx, keypair);
267		}
268
269		self.idx += 1;
270		self.prev = OutPoint::new(ret.compute_txid(), CONNECTOR_TX_CHAIN_VOUT);
271		Some(ret)
272	}
273
274	fn size_hint(&self) -> (usize, Option<usize>) {
275		let len = self.chain.len.saturating_sub(1).saturating_sub(self.idx);
276		(len, Some(len))
277	}
278}
279
280/// An iterator over the connectors in a [ConnectorChain].
281///
282/// See [ConnectorChain::connectors] and [ConnectorChain::connectors_signed]
283/// for more info.
284pub struct ConnectorIter<'a> {
285	txs: ConnectorTxIter<'a>,
286	// On all intermediate txs, only the second output is a connector and
287	// the first output continues the chain. On the very last tx, both
288	// outputs are connectors. We will keep this variable updated to contain
289	// the first output of the last tx we say, so that we can return it once
290	// the tx iterator does no longer yield new txs (hence we reached the
291	// last tx).
292	maybe_last: Option<<Self as Iterator>::Item>,
293}
294
295impl<'a> ConnectorIter<'a> {
296	/// Upgrade this iterator to a signing iterator.
297	pub fn signing(&mut self, sign_key: Keypair) {
298		self.txs.signing(sign_key)
299	}
300
301	/// Convert into owned iterator.
302	pub fn into_owned(self) -> ConnectorIter<'static> {
303		ConnectorIter {
304			txs: self.txs.into_owned(),
305			maybe_last: self.maybe_last,
306		}
307	}
308}
309
310impl<'a> iter::Iterator for ConnectorIter<'a> {
311	type Item = (OutPoint, Option<Transaction>);
312
313	fn next(&mut self) -> Option<Self::Item> {
314		if self.maybe_last.is_none() {
315			return None;
316		}
317
318		if let Some(tx) = self.txs.next() {
319			let txid = tx.compute_txid();
320			self.maybe_last = Some((OutPoint::new(txid, CONNECTOR_TX_CHAIN_VOUT), Some(tx.clone())));
321			Some((OutPoint::new(txid, CONNECTOR_TX_CONNECTOR_VOUT), Some(tx)))
322		} else {
323			Some(self.maybe_last.take().expect("broken"))
324		}
325	}
326
327	fn size_hint(&self) -> (usize, Option<usize>) {
328		let len = self.txs.size_hint().0.saturating_add(1);
329		(len, Some(len))
330	}
331}
332
333impl<'a> iter::ExactSizeIterator for ConnectorTxIter<'a> {}
334impl<'a> iter::FusedIterator for ConnectorTxIter<'a> {}
335
336
337/// The signing key passed into [ConnectorChain::iter_signed_txs] or
338/// [ConnectorChain::connectors_signed] is incorrect.
339#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
340#[error("signing key doesn't match connector chain")]
341pub struct InvalidSigningKeyError;
342
343
344#[cfg(test)]
345mod test {
346	use bitcoin::Txid;
347	use bitcoin::hashes::Hash;
348	use bitcoin_ext::TransactionExt;
349	use crate::test_util::verify_tx;
350	use super::*;
351
352	#[test]
353	fn test_budget() {
354		let key = Keypair::new(&SECP, &mut bitcoin::secp256k1::rand::thread_rng());
355		let utxo = OutPoint::new(Txid::all_zeros(), 3);
356
357		let chain = ConnectorChain::new(1, utxo, key.public_key());
358		assert_eq!(chain.connectors().count(), 1);
359		assert_eq!(chain.iter_unsigned_txs().count(), 0);
360		assert_eq!(chain.connectors().next().unwrap().0, utxo);
361
362		let chain = ConnectorChain::new(2, utxo, key.public_key());
363		assert_eq!(chain.connectors().count(), 2);
364		assert_eq!(chain.iter_unsigned_txs().count(), 1);
365		assert_eq!(chain.iter_signed_txs(&key).unwrap().count(), 1);
366		let tx = chain.iter_signed_txs(&key).unwrap().next().unwrap();
367		assert_eq!(TX_WEIGHT, tx.weight());
368		assert_eq!(tx.output_value(), ConnectorChain::required_budget(2));
369
370		let chain = ConnectorChain::new(3, utxo, key.public_key());
371		assert_eq!(chain.connectors().count(), 3);
372		assert_eq!(chain.iter_unsigned_txs().count(), 2);
373		let mut txs = chain.iter_signed_txs(&key).unwrap();
374		let tx = txs.next().unwrap();
375		assert_eq!(TX_WEIGHT, tx.weight());
376		assert_eq!(tx.output_value(), ConnectorChain::required_budget(3));
377		let tx = txs.next().unwrap();
378		assert_eq!(TX_WEIGHT, tx.weight());
379		assert_eq!(tx.output_value(), ConnectorChain::required_budget(2));
380		assert!(txs.next().is_none());
381
382		let chain = ConnectorChain::new(100, utxo, key.public_key());
383		assert_eq!(chain.connectors().count(), 100);
384		assert_eq!(chain.iter_unsigned_txs().count(), 99);
385		assert_eq!(chain.iter_signed_txs(&key).unwrap().count(), 99);
386		let tx = chain.iter_signed_txs(&key).unwrap().next().unwrap();
387		assert_eq!(TX_WEIGHT, tx.weight());
388		assert_eq!(tx.output_value(), ConnectorChain::required_budget(100));
389		for tx in chain.iter_signed_txs(&key).unwrap() {
390			assert_eq!(tx.weight(), TX_WEIGHT);
391			assert_eq!(tx.input[0].witness.size(), INPUT_WEIGHT.to_wu() as usize);
392		}
393		let weight = chain.iter_signed_txs(&key).unwrap().map(|t| t.weight()).sum::<Weight>();
394		assert_eq!(weight, ConnectorChain::total_weight(100));
395		chain.iter_unsigned_txs().for_each(|t| assert_eq!(t.output[1].value, P2TR_DUST));
396		assert_eq!(P2TR_DUST, chain.iter_unsigned_txs().last().unwrap().output[0].value);
397	}
398
399	#[test]
400	fn test_signatures() {
401		let key = Keypair::new(&SECP, &mut bitcoin::secp256k1::rand::thread_rng());
402		let utxo = OutPoint::new(Txid::all_zeros(), 3);
403		let spk = ConnectorChain::output_script(key.public_key());
404
405		let chain = ConnectorChain::new(10, utxo, key.public_key());
406		for (i, tx) in chain.iter_signed_txs(&key).unwrap().enumerate() {
407			let amount = ConnectorChain::required_budget(chain.len - i);
408			let input = TxOut {
409				script_pubkey: spk.clone(),
410				value: amount,
411			};
412			verify_tx(&[input], 0, &tx).expect(&format!("invalid connector tx idx {}", i));
413		}
414	}
415}