Skip to content

Commit 76971d8

Browse files
committed
Allow to construct ShutdownScript from OP_RETURN script
We add a new `ShutdownScript::new_op_return` constructor and extend the `is_bolt_compliant` check to enforce the new rules introduced by `option_simple_close`.
1 parent 5bb2686 commit 76971d8

File tree

1 file changed

+130
-7
lines changed

1 file changed

+130
-7
lines changed

lightning/src/ln/script.rs

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Abstractions for scripts used in the Lightning Network.
22
3+
use bitcoin::blockdata::script::Instruction;
34
use bitcoin::hashes::Hash;
4-
use bitcoin::opcodes::all::OP_PUSHBYTES_0 as SEGWIT_V0;
5-
use bitcoin::script::{Script, ScriptBuf};
5+
use bitcoin::opcodes::all::{OP_PUSHBYTES_0 as SEGWIT_V0, OP_RETURN};
6+
use bitcoin::script::{PushBytes, Script, ScriptBuf};
67
use bitcoin::secp256k1::PublicKey;
78
use bitcoin::{WPubkeyHash, WScriptHash, WitnessProgram};
89

@@ -75,6 +76,25 @@ impl ShutdownScript {
7576
Self(ShutdownScriptImpl::Bolt2(ScriptBuf::new_p2wsh(script_hash)))
7677
}
7778

79+
/// Generates an `OP_RETURN` script pubkey from the given `data` bytes.
80+
///
81+
/// This is only needed and valid for channels supporting `option_simple_close`. Please refer
82+
/// to [BOLT-2] for more information.
83+
///
84+
/// Will return [`InvalidShutdownScript`] if the given data is not [BOLT-2] compliant based on
85+
/// the given features.
86+
///
87+
/// [BOLT-2]: https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#closing-negotiation-closing_complete-and-closing_sig
88+
pub fn new_op_return<T: AsRef<PushBytes>>(
89+
data: T, features: &InitFeatures,
90+
) -> Result<Self, InvalidShutdownScript> {
91+
let script = ScriptBuf::new_op_return(data);
92+
if !is_bolt2_compliant(&script, features) {
93+
return Err(InvalidShutdownScript { script });
94+
}
95+
Ok(Self(ShutdownScriptImpl::Bolt2(script)))
96+
}
97+
7898
/// Generates a witness script pubkey from the given segwit version and program.
7999
///
80100
/// Note for version-zero witness scripts you must use [`ShutdownScript::new_p2wpkh`] or
@@ -116,10 +136,47 @@ impl ShutdownScript {
116136
/// Check if a given script is compliant with BOLT 2's shutdown script requirements for the given
117137
/// counterparty features.
118138
pub(crate) fn is_bolt2_compliant(script: &Script, features: &InitFeatures) -> bool {
139+
// BOLT2:
140+
// 1. `OP_0` `20` 20-bytes (version 0 pay to witness pubkey hash), OR
141+
// 2. `OP_0` `32` 32-bytes (version 0 pay to witness script hash), OR
119142
if script.is_p2pkh() || script.is_p2sh() || script.is_p2wpkh() || script.is_p2wsh() {
120143
true
121-
} else if features.supports_shutdown_anysegwit() {
122-
script.is_witness_program() && script.as_bytes()[0] != SEGWIT_V0.to_u8()
144+
} else if features.supports_shutdown_anysegwit() && script.is_witness_program() {
145+
// 3. if (and only if) `option_shutdown_anysegwit` is negotiated:
146+
// * `OP_1` through `OP_16` inclusive, followed by a single push of 2 to 40 bytes
147+
// (witness program versions 1 through 16)
148+
script.as_bytes()[0] != SEGWIT_V0.to_u8()
149+
} else if features.supports_simple_close() && script.is_op_return() {
150+
// 4. if (and only if) `option_simple_close` is negotiated:
151+
let mut instruction_iter = script.instructions();
152+
if let Some(Ok(Instruction::Op(opcode))) = instruction_iter.next() {
153+
// * `OP_RETURN` followed by one of:
154+
if opcode != OP_RETURN {
155+
return false;
156+
}
157+
158+
match instruction_iter.next() {
159+
Some(Ok(Instruction::PushBytes(bytes))) => {
160+
// * `6` to `75` inclusive followed by exactly that many bytes
161+
if (6..=75).contains(&bytes.len()) {
162+
return instruction_iter.next().is_none();
163+
}
164+
165+
// `rust-bitcoin` interprets `OP_PUSHDATA1` as `Instruction::PushBytes`, having
166+
// us land here in this case, too.
167+
//
168+
// * `76` followed by `76` to `80` followed by exactly that many bytes
169+
if (76..=80).contains(&bytes.len()) {
170+
return instruction_iter.next().is_none();
171+
}
172+
173+
false
174+
},
175+
_ => false,
176+
}
177+
} else {
178+
false
179+
}
123180
} else {
124181
false
125182
}
@@ -142,7 +199,7 @@ impl TryFrom<(ScriptBuf, &InitFeatures)> for ShutdownScript {
142199
type Error = InvalidShutdownScript;
143200

144201
fn try_from((script, features): (ScriptBuf, &InitFeatures)) -> Result<Self, Self::Error> {
145-
if is_bolt2_compliant(&script, features) && script.is_witness_program() {
202+
if is_bolt2_compliant(&script, features) {
146203
Ok(Self(ShutdownScriptImpl::Bolt2(script)))
147204
} else {
148205
Err(InvalidShutdownScript { script })
@@ -175,13 +232,15 @@ mod shutdown_script_tests {
175232
use super::ShutdownScript;
176233

177234
use bitcoin::opcodes;
178-
use bitcoin::script::{Builder, ScriptBuf};
235+
use bitcoin::script::{Builder, PushBytes, ScriptBuf};
179236
use bitcoin::secp256k1::Secp256k1;
180237
use bitcoin::secp256k1::{PublicKey, SecretKey};
181238
use bitcoin::{WitnessProgram, WitnessVersion};
182239

240+
use crate::ln::channelmanager::provided_init_features;
183241
use crate::prelude::*;
184242
use crate::types::features::InitFeatures;
243+
use crate::util::config::UserConfig;
185244

186245
fn pubkey() -> bitcoin::key::PublicKey {
187246
let secp_ctx = Secp256k1::signing_only();
@@ -210,6 +269,13 @@ mod shutdown_script_tests {
210269
features
211270
}
212271

272+
#[cfg(simple_close)]
273+
fn simple_close_features() -> InitFeatures {
274+
let mut features = InitFeatures::empty();
275+
features.set_simple_close_optional();
276+
features
277+
}
278+
213279
#[test]
214280
fn generates_p2wpkh_from_pubkey() {
215281
let pubkey = pubkey();
@@ -246,6 +312,43 @@ mod shutdown_script_tests {
246312
assert!(ShutdownScript::try_from(p2wsh_script).is_ok());
247313
}
248314

315+
#[cfg(simple_close)]
316+
#[test]
317+
fn generates_op_return_from_data() {
318+
let data = [6; 6];
319+
let features = provided_init_features(&UserConfig::default());
320+
let op_return_script = ScriptBuf::new_op_return(&data);
321+
let shutdown_script = ShutdownScript::new_op_return(&data, &features).unwrap();
322+
assert!(shutdown_script.is_compatible(&simple_close_features()));
323+
assert!(!shutdown_script.is_compatible(&InitFeatures::empty()));
324+
assert_eq!(shutdown_script.into_inner(), op_return_script);
325+
assert!(ShutdownScript::try_from(op_return_script).is_ok());
326+
327+
let assert_pushdata_script_compat = |len| {
328+
let mut pushdata_vec = Builder::new()
329+
.push_opcode(opcodes::all::OP_RETURN)
330+
.push_opcode(opcodes::all::OP_PUSHDATA1)
331+
.into_bytes();
332+
pushdata_vec.push(len as u8);
333+
pushdata_vec.extend_from_slice(&vec![1u8; len]);
334+
let pushdata_script = ScriptBuf::from_bytes(pushdata_vec);
335+
let pushdata_shutdown_script = ShutdownScript::try_from(pushdata_script).unwrap();
336+
assert!(pushdata_shutdown_script.is_compatible(&simple_close_features()));
337+
assert!(!pushdata_shutdown_script.is_compatible(&InitFeatures::empty()));
338+
};
339+
340+
// For `option_simple_close` we assert compatibility with `OP_PUSHDATA1` scripts for the
341+
// intended length range of 76 to 80 bytes.
342+
assert_pushdata_script_compat(76);
343+
assert_pushdata_script_compat(80);
344+
345+
// While the `option_simple_close` spec prescribes the use of `OP_PUSHBYTES_0` up to 75
346+
// bytes, we follow Postel's law and accept if our counterparty would create an
347+
// `OP_PUSHDATA1` script for less than 76 bytes of payload.
348+
assert_pushdata_script_compat(75);
349+
assert_pushdata_script_compat(6);
350+
}
351+
249352
#[test]
250353
fn generates_segwit_from_non_v0_witness_program() {
251354
let witness_program = WitnessProgram::new(WitnessVersion::V16, &[0; 40]).unwrap();
@@ -258,7 +361,27 @@ mod shutdown_script_tests {
258361

259362
#[test]
260363
fn fails_from_unsupported_script() {
261-
let op_return = ScriptBuf::new_op_return(&[0; 42]);
364+
// For `option_simple_close` we assert we fail when:
365+
//
366+
// - The first byte of the OP_RETURN data (interpreted as u8 int) is not equal to the
367+
// remaining number of bytes (i.e., `[5; 6]` would succeed here).
368+
let op_return = ScriptBuf::new_op_return(&[5; 5]);
262369
assert!(ShutdownScript::try_from(op_return).is_err());
370+
371+
// - The OP_RETURN data will fail if it's longer than 80 bytes.
372+
let mut pushdata_vec = Builder::new()
373+
.push_opcode(opcodes::all::OP_RETURN)
374+
.push_opcode(opcodes::all::OP_PUSHDATA1)
375+
.into_bytes();
376+
pushdata_vec.push(81);
377+
pushdata_vec.extend_from_slice(&[1u8; 81]);
378+
let pushdata_script = ScriptBuf::from_bytes(pushdata_vec);
379+
assert!(ShutdownScript::try_from(pushdata_script).is_err());
380+
381+
// - In `ShutdownScript::new_op_return` the OP_RETURN data is longer than 80 bytes.
382+
let features = provided_init_features(&UserConfig::default());
383+
let big_buffer = &[1u8; 81][..];
384+
let push_bytes: &PushBytes = big_buffer.try_into().unwrap();
385+
assert!(ShutdownScript::new_op_return(&push_bytes, &features).is_err());
263386
}
264387
}

0 commit comments

Comments
 (0)