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