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 pub static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct ArkInfo {
51 pub network: Network,
53 pub server_pubkey: PublicKey,
55 pub mailbox_pubkey: PublicKey,
57 pub round_interval: Duration,
59 pub nb_round_nonces: usize,
61 pub vtxo_exit_delta: BlockDelta,
63 pub vtxo_expiry_delta: BlockDelta,
65 pub htlc_send_expiry_delta: BlockDelta,
67 pub htlc_expiry_delta: BlockDelta,
69 pub max_vtxo_amount: Option<Amount>,
71 pub required_board_confirmations: usize,
73 pub max_user_invoice_cltv_delta: u16,
76 pub min_board_amount: Amount,
78
79 pub offboard_feerate: FeeRate,
81 pub ln_receive_anti_dos_required: bool,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
89pub struct VtxoIdInput {
90 pub vtxo_id: VtxoId,
91 pub ownership_proof: schnorr::Signature,
97}
98
99#[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 pub vtxo: VtxoRequest,
111 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 pub fn calculate_fee(
136 script_pubkey: &Script,
137 fee_rate: FeeRate,
138 ) -> Result<Amount, InvalidOffboardRequestError> {
139 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 + 36 + 4 + 1 + 1 }
167 } else {
168 return Err(InvalidOffboardRequestError("non-standard scriptPubkey"));
169 };
170 Ok(fee_rate * Weight::from_vb(vb).expect("no overflow"))
171 }
172
173 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 pub fn to_txout(&self) -> TxOut {
184 TxOut {
185 script_pubkey: self.script_pubkey.clone(),
186 value: self.amount,
187 }
188 }
189
190 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 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 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 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 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 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}