ark/
lib.rs

1
2pub extern crate bitcoin;
3
4#[macro_use] extern crate serde;
5#[macro_use] extern crate lazy_static;
6
7#[macro_use] mod util;
8
9pub mod address;
10pub mod arkoor;
11pub mod connectors;
12pub mod encode;
13pub mod error;
14pub mod forfeit;
15pub mod lightning;
16pub mod musig;
17pub mod board;
18pub mod rounds;
19pub mod tree;
20pub mod vtxo;
21pub mod integration;
22
23pub use crate::address::Address;
24pub use crate::encode::{ProtocolEncoding, WriteExt, ReadExt, ProtocolDecodingError};
25pub use crate::vtxo::{Vtxo, VtxoId, VtxoPolicy};
26
27#[cfg(test)]
28mod napkin;
29#[cfg(any(test, feature = "test-util"))]
30pub mod test;
31
32
33use std::time::Duration;
34
35use bitcoin::{Amount, FeeRate, Network, Script, ScriptBuf, TxOut, Weight};
36use bitcoin::secp256k1::{self, schnorr, PublicKey};
37
38use bitcoin_ext::{
39	TxOutExt, P2PKH_DUST_VB, P2SH_DUST_VB, P2TR_DUST_VB, P2WPKH_DUST_VB, P2WSH_DUST_VB,
40};
41
42lazy_static! {
43	/// Global secp context.
44	pub static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct ArkInfo {
49	pub network: Network,
50	pub server_pubkey: PublicKey,
51	pub round_interval: Duration,
52	pub nb_round_nonces: usize,
53	pub vtxo_exit_delta: u16,
54	pub vtxo_expiry_delta: u16,
55	pub htlc_expiry_delta: u16,
56	pub max_vtxo_amount: Option<Amount>,
57	pub max_arkoor_depth: u16,
58	pub required_board_confirmations: usize,
59}
60
61/// Input of a round
62#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
63pub struct VtxoIdInput {
64	pub vtxo_id: VtxoId,
65	/// A schnorr signature over a message containing a static prefix,
66	/// a random challenge generated by the server and the VTXO's id.
67	/// See [`rounds::VtxoOwnershipChallenge`].
68	///
69	/// Should be produced using VTXO's private key
70	pub ownership_proof: schnorr::Signature,
71}
72
73/// Request for the creation of an vtxo.
74#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
75pub struct VtxoRequest {
76	pub amount: Amount,
77	#[serde(with = "crate::encode::serde")]
78	pub policy: VtxoPolicy,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub struct SignedVtxoRequest {
83	/// The actual VTXO request.
84	pub vtxo: VtxoRequest,
85	/// The public key used by the client to cosign the transaction tree
86	/// The client SHOULD forget this key after signing it
87	pub cosign_pubkey: Option<PublicKey>,
88}
89
90
91#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
92#[error("invalid offboard request: {0}")]
93pub struct InvalidOffboardRequestError(&'static str);
94
95
96#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
97pub struct OffboardRequest {
98	pub script_pubkey: ScriptBuf,
99	#[serde(rename = "amount_sat", with = "bitcoin::amount::serde::as_sat")]
100	pub amount: Amount,
101}
102
103impl OffboardRequest {
104	/// Calculate the fee we have to charge for adding an output
105	/// with the given scriptPubkey to a transaction.
106	///
107	/// Returns an error if the output type is non-standard.
108	pub fn calculate_fee(
109		script_pubkey: &Script,
110		fee_rate: FeeRate,
111	) -> Result<Amount, InvalidOffboardRequestError> {
112		// NB We calculate the required extra fee as the "dust" fee for the given feerate.
113		// We take Bitcoin's dust amounts, which are calculated at 3 sat/vb, but then
114		// calculated for the given feerate. For more on dust, see:
115		// https://bitcoin.stackexchange.com/questions/10986/what-is-meant-by-bitcoin-dust
116
117		let vb = if script_pubkey.is_p2pkh() {
118			P2PKH_DUST_VB
119		} else if script_pubkey.is_p2sh() {
120			P2SH_DUST_VB
121		} else if script_pubkey.is_p2wpkh() {
122			P2WPKH_DUST_VB
123		} else if script_pubkey.is_p2wsh() {
124			P2WSH_DUST_VB
125		} else if script_pubkey.is_p2tr() {
126			P2TR_DUST_VB
127		} else if script_pubkey.is_op_return() {
128			if script_pubkey.len() > 83 {
129				return Err(InvalidOffboardRequestError("OP_RETURN over 83 bytes"));
130			} else {
131				bitcoin::consensus::encode::VarInt(script_pubkey.len() as u64).size() as u64
132					+ script_pubkey.len() as u64
133					+ 8  // output amount
134					// the input data (scriptSig and witness length fields included)
135					+ 36 // input prevout
136					+ 4  // sequence
137					+ 1  // 0 length scriptsig
138					+ 1  // 0 length witness
139			}
140		} else {
141			return Err(InvalidOffboardRequestError("non-standard scriptPubkey"));
142		};
143		Ok(fee_rate * Weight::from_vb(vb).expect("no overflow"))
144	}
145
146	/// Validate that the offboard has a valid script.
147	pub fn validate(&self) -> Result<(), InvalidOffboardRequestError> {
148		if self.to_txout().is_standard() {
149			Ok(())
150		} else {
151			Err(InvalidOffboardRequestError("non-standard output"))
152		}
153	}
154
155	/// Convert into a tx output.
156	pub fn to_txout(&self) -> TxOut {
157		TxOut {
158			script_pubkey: self.script_pubkey.clone(),
159			value: self.amount,
160		}
161	}
162
163	/// Returns the fee charged for the user to make this offboard given the fee rate.
164	pub fn fee(&self, fee_rate: FeeRate) -> Result<Amount, InvalidOffboardRequestError> {
165		Ok(Self::calculate_fee(&self.script_pubkey, fee_rate)?)
166	}
167}
168
169pub mod scripts {
170	use bitcoin::{opcodes, ScriptBuf, TapSighash, TapTweakHash, Transaction};
171	use bitcoin::hashes::{sha256, ripemd160, Hash};
172	use bitcoin::secp256k1::{schnorr, PublicKey, XOnlyPublicKey};
173
174	use bitcoin_ext::{BlockHeight, TAPROOT_KEYSPEND_WEIGHT};
175
176	use crate::musig;
177
178	/// Create a tapscript that is a checksig and a relative timelock.
179	pub fn delayed_sign(delay_blocks: u16, pubkey: XOnlyPublicKey) -> ScriptBuf {
180		let csv = bitcoin::Sequence::from_height(delay_blocks);
181		bitcoin::Script::builder()
182			.push_int(csv.to_consensus_u32() as i64)
183			.push_opcode(opcodes::all::OP_CSV)
184			.push_opcode(opcodes::all::OP_DROP)
185			.push_x_only_key(&pubkey)
186			.push_opcode(opcodes::all::OP_CHECKSIG)
187			.into_script()
188	}
189
190	/// Create a tapscript that is a checksig and an absolute timelock.
191	pub fn timelock_sign(timelock_height: BlockHeight, pubkey: XOnlyPublicKey) -> ScriptBuf {
192		let lt = bitcoin::absolute::LockTime::from_height(timelock_height).unwrap();
193		bitcoin::Script::builder()
194			.push_int(lt.to_consensus_u32() as i64)
195			.push_opcode(opcodes::all::OP_CLTV)
196			.push_opcode(opcodes::all::OP_DROP)
197			.push_x_only_key(&pubkey)
198			.push_opcode(opcodes::all::OP_CHECKSIG)
199			.into_script()
200	}
201
202	/// Create a tapscript
203	pub fn delay_timelock_sign(delay_blocks: u16, timelock_height: u32, pubkey: XOnlyPublicKey) -> ScriptBuf {
204		let csv = bitcoin::Sequence::from_height(delay_blocks);
205		let lt = bitcoin::absolute::LockTime::from_height(timelock_height).unwrap();
206		bitcoin::Script::builder()
207			.push_int(lt.to_consensus_u32().try_into().unwrap())
208			.push_opcode(opcodes::all::OP_CLTV)
209			.push_opcode(opcodes::all::OP_DROP)
210			.push_int(csv.to_consensus_u32().try_into().unwrap())
211			.push_opcode(opcodes::all::OP_CSV)
212			.push_opcode(opcodes::all::OP_DROP)
213			.push_x_only_key(&pubkey)
214			.push_opcode(opcodes::all::OP_CHECKSIG)
215			.into_script()
216	}
217
218	pub fn hash_and_sign(hash: sha256::Hash, pubkey: XOnlyPublicKey) -> ScriptBuf {
219		let hash_160 = ripemd160::Hash::hash(&hash[..]);
220
221		bitcoin::Script::builder()
222			.push_opcode(opcodes::all::OP_HASH160)
223			.push_slice(hash_160.as_byte_array())
224			.push_opcode(opcodes::all::OP_EQUALVERIFY)
225			.push_x_only_key(&pubkey)
226			.push_opcode(opcodes::all::OP_CHECKSIG)
227			.into_script()
228	}
229
230	pub fn hash_delay_sign(hash: sha256::Hash, delay_blocks: u16, pubkey: XOnlyPublicKey) -> ScriptBuf {
231		let hash_160 = ripemd160::Hash::hash(&hash[..]);
232		let csv = bitcoin::Sequence::from_height(delay_blocks);
233
234		bitcoin::Script::builder()
235			.push_int(csv.to_consensus_u32().try_into().unwrap())
236			.push_opcode(opcodes::all::OP_CSV)
237			.push_opcode(opcodes::all::OP_DROP)
238			.push_opcode(opcodes::all::OP_HASH160)
239			.push_slice(hash_160.as_byte_array())
240			.push_opcode(opcodes::all::OP_EQUALVERIFY)
241			.push_x_only_key(&pubkey)
242			.push_opcode(opcodes::all::OP_CHECKSIG)
243			.into_script()
244	}
245
246	/// Fill in the signatures into the unsigned transaction.
247	///
248	/// Panics if the nb of inputs and signatures doesn't match or if some input
249	/// witnesses are not empty.
250	pub fn fill_taproot_sigs(tx: &mut Transaction, sigs: &[schnorr::Signature]) {
251		assert_eq!(tx.input.len(), sigs.len());
252		for (input, sig) in tx.input.iter_mut().zip(sigs.iter()) {
253			assert!(input.witness.is_empty());
254			input.witness.push(&sig[..]);
255			debug_assert_eq!(TAPROOT_KEYSPEND_WEIGHT, input.witness.size());
256		}
257	}
258
259	/// Verify a partial signature from either of the two parties cosigning a tx.
260	pub fn verify_partial_sig(
261		sighash: TapSighash,
262		tweak: TapTweakHash,
263		signer: (PublicKey, &musig::PublicNonce),
264		other: (PublicKey, &musig::PublicNonce),
265		partial_signature: &musig::PartialSignature,
266	) -> bool {
267		let agg_nonce = musig::nonce_agg(&[&signer.1, &other.1]);
268		let agg_pk = musig::tweaked_key_agg([signer.0, other.0], tweak.to_byte_array()).0;
269
270		let session = musig::Session::new(&agg_pk, agg_nonce, &sighash.to_byte_array());
271		session.partial_verify(
272			&agg_pk, partial_signature, signer.1, musig::pubkey_to(signer.0),
273		)
274	}
275}