Skip to content

Commit

Permalink
Feature: Instruction can read the script number
Browse files Browse the repository at this point in the history
  • Loading branch information
junderw committed Sep 20, 2023
1 parent e70836c commit 767b94e
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 7 deletions.
25 changes: 25 additions & 0 deletions bitcoin/src/blockdata/script/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,31 @@ impl<'a> Instruction<'a> {
}
}

/// Returns the number interpretted by the script parser
/// if it can be coerced into a number.
///
/// This does not require the script num to be minimal.
pub fn script_num(&self) -> Option<i64> {
match self {
Instruction::Op(op) => {
let v = op.to_u8();
match v {
// OP_PUSHNUM_1 ..= OP_PUSHNUM_16
0x51..=0x60 => Some(v as i64 - 0x50),
// OP_PUSHNUM_NEG1
0x4f => Some(-1),
_ => None,
}
}
Instruction::PushBytes(bytes) => {
match super::read_scriptint_non_minimal(bytes.as_bytes()) {
Ok(v) => Some(v),
_ => None,
}
}
}
}

/// Returns the number of bytes required to encode the instruction in script.
pub(super) fn script_serialized_len(&self) -> usize {
match self {
Expand Down
34 changes: 27 additions & 7 deletions bitcoin/src/blockdata/script/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,20 +170,19 @@ pub fn write_scriptint(out: &mut [u8; 8], n: i64) -> usize {
///
/// This code is based on the `CScriptNum` constructor in Bitcoin Core (see `script.h`).
pub fn read_scriptint(v: &[u8]) -> Result<i64, Error> {
let len = v.len();
if len > 4 {
return Err(Error::NumericOverflow);
}
let last = match v.last() {
Some(last) => last,
None => return Ok(0),
};
if v.len() > 4 {
return Err(Error::NumericOverflow);
}
// Comment and code copied from Bitcoin Core:
// https://github.com/bitcoin/bitcoin/blob/447f50e4aed9a8b1d80e1891cda85801aeb80b4e/src/script/script.h#L247-L262
// If the most-significant-byte - excluding the sign bit - is zero
// then we're not minimal. Note how this test also rejects the
// negative-zero encoding, 0x80.
if (last & 0x7f) == 0 {
if (*last & 0x7f) == 0 {
// One exception: if there's more than one byte and the most
// significant bit of the second-most-significant-byte is set
// it would conflict with the sign bit. An example of this case
Expand All @@ -194,12 +193,33 @@ pub fn read_scriptint(v: &[u8]) -> Result<i64, Error> {
}
}

Ok(scriptint_parse(v))
}

/// Decodes an integer in script format without non-minimal error.
///
/// The overflow error for slices over 4 bytes long is still there.
/// See [`read_scriptint`] for a description of some subtleties of
/// this function.
pub fn read_scriptint_non_minimal(v: &[u8]) -> Result<i64, Error> {
if v.len() == 0 {
return Ok(0);
}
if v.len() > 4 {
return Err(Error::NumericOverflow);
}

Ok(scriptint_parse(v))
}

// Caller to guarantee that `v` is not empty.
fn scriptint_parse(v: &[u8]) -> i64 {
let (mut ret, sh) = v.iter().fold((0, 0), |(acc, sh), n| (acc + ((*n as i64) << sh), sh + 8));
if v[len - 1] & 0x80 != 0 {
if v[v.len() - 1] & 0x80 != 0 {
ret &= (1 << (sh - 1)) - 1;
ret = -ret;
}
Ok(ret)
ret
}

/// Decodes a boolean.
Expand Down
55 changes: 55 additions & 0 deletions bitcoin/src/blockdata/script/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,13 @@ fn scriptint_round_trip() {
for &i in test_vectors.iter() {
assert_eq!(Ok(i), read_scriptint(&build_scriptint(i)));
assert_eq!(Ok(-i), read_scriptint(&build_scriptint(-i)));
assert_eq!(Ok(i), read_scriptint_non_minimal(&build_scriptint(i)));
assert_eq!(Ok(-i), read_scriptint_non_minimal(&build_scriptint(-i)));
}
assert!(read_scriptint(&build_scriptint(1 << 31)).is_err());
assert!(read_scriptint(&build_scriptint(-(1 << 31))).is_err());
assert!(read_scriptint_non_minimal(&build_scriptint(1 << 31)).is_err());
assert!(read_scriptint_non_minimal(&build_scriptint(-(1 << 31))).is_err());
}

#[test]
Expand All @@ -323,6 +327,11 @@ fn non_minimal_scriptints() {
assert_eq!(read_scriptint(&[0xff, 0x00]), Ok(0xff));
assert_eq!(read_scriptint(&[0x8f, 0x00, 0x00]), Err(Error::NonMinimalPush));
assert_eq!(read_scriptint(&[0x7f, 0x00]), Err(Error::NonMinimalPush));

assert_eq!(read_scriptint_non_minimal(&[0x80, 0x00]), Ok(0x80));
assert_eq!(read_scriptint_non_minimal(&[0xff, 0x00]), Ok(0xff));
assert_eq!(read_scriptint_non_minimal(&[0x8f, 0x00, 0x00]), Ok(0x8f));
assert_eq!(read_scriptint_non_minimal(&[0x7f, 0x00]), Ok(0x7f));
}

#[test]
Expand Down Expand Up @@ -769,3 +778,49 @@ fn read_scriptbool_non_zero_is_true() {
let v: Vec<u8> = vec![0x01, 0x00, 0x00, 0x80]; // With sign bit set.
assert!(read_scriptbool(&v));
}

#[test]
fn instruction_script_num_parse() {
let push_bytes = [
(PushBytesBuf::from([]), Some(0)),
(PushBytesBuf::from([0x00]), Some(0)),
(PushBytesBuf::from([0x01]), Some(1)),
// Check all the negative 1s
(PushBytesBuf::from([0x81]), Some(-1)),
(PushBytesBuf::from([0x01, 0x80]), Some(-1)),
(PushBytesBuf::from([0x01, 0x00, 0x80]), Some(-1)),
(PushBytesBuf::from([0x01, 0x00, 0x00, 0x80]), Some(-1)),
// Check all the negative 0s
(PushBytesBuf::from([0x80]), Some(0)),
(PushBytesBuf::from([0x00, 0x80]), Some(0)),
(PushBytesBuf::from([0x00, 0x00, 0x80]), Some(0)),
(PushBytesBuf::from([0x00, 0x00, 0x00, 0x80]), Some(0)),
// Too long
(PushBytesBuf::from([0x01, 0x00, 0x00, 0x00, 0x80]), None),
// Check the position of all the bytes
(PushBytesBuf::from([0xef, 0xbe, 0xad, 0x5e]), Some(0x5eadbeef)),
// Add negative
(PushBytesBuf::from([0xef, 0xbe, 0xad, 0xde]), Some(-0x5eadbeef)),
];
let ops = [
(Instruction::Op(opcodes::all::OP_PUSHDATA4), None),
(Instruction::Op(opcodes::all::OP_PUSHNUM_NEG1), Some(-1)),
(Instruction::Op(opcodes::all::OP_RESERVED), None),
(Instruction::Op(opcodes::all::OP_PUSHNUM_1), Some(1)),
(Instruction::Op(opcodes::all::OP_PUSHNUM_16), Some(16)),
(Instruction::Op(opcodes::all::OP_NOP), None),
];
for (input, expected) in &push_bytes {
assert_eq!(Instruction::PushBytes(input).script_num(), *expected);
}
for (input, expected) in &ops {
assert_eq!(input.script_num(), *expected);
}

// script_num() is predicated on OP_0/OP_FALSE (0x00)
// being treated as an empty PushBytes
assert_eq!(
Script::from_bytes(&[0x00]).instructions().next(),
Some(Ok(Instruction::PushBytes(PushBytes::empty()))),
);
}

0 comments on commit 767b94e

Please sign in to comment.