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 pub static ref SECP: secp256k1::Secp256k1<secp256k1::All> = secp256k1::Secp256k1::new();
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct ArkInfo {
50 pub network: Network,
52 pub server_pubkey: PublicKey,
54 pub round_interval: Duration,
56 pub nb_round_nonces: usize,
58 pub vtxo_exit_delta: BlockDelta,
60 pub vtxo_expiry_delta: BlockDelta,
62 pub htlc_send_expiry_delta: BlockDelta,
64 pub htlc_expiry_delta: BlockDelta,
66 pub max_vtxo_amount: Option<Amount>,
68 pub max_arkoor_depth: u16,
70 pub required_board_confirmations: usize,
72 pub max_user_invoice_cltv_delta: u16,
75 pub min_board_amount: Amount,
77
78 pub offboard_feerate: FeeRate,
80 pub ln_receive_anti_dos_required: bool,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
88pub struct VtxoIdInput {
89 pub vtxo_id: VtxoId,
90 pub ownership_proof: schnorr::Signature,
96}
97
98#[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 pub vtxo: VtxoRequest,
110 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 pub fn calculate_fee(
135 script_pubkey: &Script,
136 fee_rate: FeeRate,
137 ) -> Result<Amount, InvalidOffboardRequestError> {
138 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 + 36 + 4 + 1 + 1 }
166 } else {
167 return Err(InvalidOffboardRequestError("non-standard scriptPubkey"));
168 };
169 Ok(fee_rate * Weight::from_vb(vb).expect("no overflow"))
170 }
171
172 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 pub fn to_txout(&self) -> TxOut {
183 TxOut {
184 script_pubkey: self.script_pubkey.clone(),
185 value: self.amount,
186 }
187 }
188
189 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 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 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 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 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 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}