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