Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure absence of panics on scripts >4GB and other consensus-invalid data #59

Merged
merged 4 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion consensus/src/coding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ use crate::{
TxIn, TxOut, TxVer, Txid, Vout, Witness, WitnessScript, LIB_NAME_BITCOIN,
};

/// Bitcoin consensus allows arrays which length is encoded as VarInt to grow up
/// to 64-bit values. However, at the same time no consensus rule allows any
/// block data structure to exceed 2^32 bytes (4GB), and any change to that rule
/// will be a hardfork. So for practical reasons we are safe to restrict the
/// maximum size here with just 32 bits.
pub type VarIntArray<T> = Confined<Vec<T>, 0, U32>;

/// A variable-length unsigned integer.
Expand Down Expand Up @@ -95,7 +100,7 @@ impl AsRef<[u8]> for ByteStr {
}

impl From<Vec<u8>> for ByteStr {
fn from(value: Vec<u8>) -> Self { Self(Confined::try_from(value).expect("u64 >= usize")) }
fn from(value: Vec<u8>) -> Self { Self(Confined::try_from(value).expect("u32 >= usize")) }
}

impl ByteStr {
Expand Down
2 changes: 0 additions & 2 deletions consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Version 0.11.0:
// TODO: Ensure script length control doesn't panic for data structures > 4GB
// Version 1.0:
// TODO: Complete block data type implementation
// TODO: Complete OpCode enumeration
Expand Down
110 changes: 83 additions & 27 deletions consensus/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use amplify::confinement;
use amplify::confinement::Confined;
use commit_verify::{DigestExt, Ripemd160};

use crate::opcodes::*;
use crate::{VarInt, VarIntArray, LIB_NAME_BITCOIN};
Expand Down Expand Up @@ -116,14 +116,35 @@ pub enum OpCode {
derive(Serialize, Deserialize),
serde(crate = "serde_crate", transparent)
)]
pub struct SigScript(
#[from]
#[from(Vec<u8>)]
ScriptBytes,
);
pub struct SigScript(ScriptBytes);

impl TryFrom<Vec<u8>> for SigScript {
type Error = confinement::Error;
fn try_from(script_bytes: Vec<u8>) -> Result<Self, Self::Error> {
ScriptBytes::try_from(script_bytes).map(Self)
}
}

impl SigScript {
#[inline]
pub fn empty() -> Self { SigScript::default() }

#[inline]
pub fn new() -> Self { Self::default() }

#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self(ScriptBytes::from(Confined::with_capacity(capacity)))
}

/// Constructs script object assuming the script length is less than 4GB.
/// Panics otherwise.
#[inline]
pub fn from_unsafe(script_bytes: Vec<u8>) -> Self {
Self(ScriptBytes::from_unsafe(script_bytes))
}

#[inline]
pub fn as_script_bytes(&self) -> &ScriptBytes { &self.0 }
}

Expand All @@ -137,19 +158,31 @@ impl SigScript {
derive(Serialize, Deserialize),
serde(crate = "serde_crate", transparent)
)]
pub struct ScriptPubkey(
#[from]
#[from(Vec<u8>)]
ScriptBytes,
);
pub struct ScriptPubkey(ScriptBytes);

impl TryFrom<Vec<u8>> for ScriptPubkey {
type Error = confinement::Error;
fn try_from(script_bytes: Vec<u8>) -> Result<Self, Self::Error> {
ScriptBytes::try_from(script_bytes).map(Self)
}
}

impl ScriptPubkey {
#[inline]
pub fn new() -> Self { Self::default() }

#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self(ScriptBytes::from(Confined::with_capacity(capacity)))
}

/// Constructs script object assuming the script length is less than 4GB.
/// Panics otherwise.
#[inline]
pub fn from_unsafe(script_bytes: Vec<u8>) -> Self {
Self(ScriptBytes::from_unsafe(script_bytes))
}

pub fn p2pkh(hash: impl Into<[u8; 20]>) -> Self {
let mut script = Self::with_capacity(25);
script.push_opcode(OpCode::Dup);
Expand Down Expand Up @@ -195,11 +228,14 @@ impl ScriptPubkey {
self.0[22] == OP_EQUAL
}

#[inline]
pub fn is_op_return(&self) -> bool { self[0] == OpCode::Return as u8 }

/// Adds a single opcode to the script.
#[inline]
pub fn push_opcode(&mut self, op_code: OpCode) { self.0.push(op_code as u8) }

#[inline]
pub fn as_script_bytes(&self) -> &ScriptBytes { &self.0 }
}

Expand All @@ -213,43 +249,61 @@ impl ScriptPubkey {
derive(Serialize, Deserialize),
serde(crate = "serde_crate", transparent)
)]
pub struct RedeemScript(
#[from]
#[from(Vec<u8>)]
ScriptBytes,
);
pub struct RedeemScript(ScriptBytes);

impl TryFrom<Vec<u8>> for RedeemScript {
type Error = confinement::Error;
fn try_from(script_bytes: Vec<u8>) -> Result<Self, Self::Error> {
ScriptBytes::try_from(script_bytes).map(Self)
}
}

impl RedeemScript {
#[inline]
pub fn new() -> Self { Self::default() }

#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self(ScriptBytes::from(Confined::with_capacity(capacity)))
}

/// Constructs script object assuming the script length is less than 4GB.
/// Panics otherwise.
#[inline]
pub fn from_unsafe(script_bytes: Vec<u8>) -> Self {
Self(ScriptBytes::from_unsafe(script_bytes))
}

/// Adds a single opcode to the script.
#[inline]
pub fn push_opcode(&mut self, op_code: OpCode) { self.0.push(op_code as u8); }

pub fn to_script_pubkey(&self) -> ScriptPubkey {
let mut engine = Ripemd160::default();
engine.input_raw(self.as_slice());
ScriptPubkey::p2sh(engine.finish())
}

#[inline]
pub fn as_script_bytes(&self) -> &ScriptBytes { &self.0 }
}

#[derive(Wrapper, WrapperMut, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default, Debug, From)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_BITCOIN)]
#[wrapper(Deref, AsSlice, Hex)]
#[wrapper_mut(DerefMut, AsSliceMut)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_BITCOIN)]
pub struct ScriptBytes(VarIntArray<u8>);

impl From<Vec<u8>> for ScriptBytes {
fn from(value: Vec<u8>) -> Self { Self(Confined::try_from(value).expect("u64 >= usize")) }
impl TryFrom<Vec<u8>> for ScriptBytes {
type Error = confinement::Error;
fn try_from(script_bytes: Vec<u8>) -> Result<Self, Self::Error> {
Confined::try_from(script_bytes).map(Self)
}
}

impl ScriptBytes {
/// Constructs script object assuming the script length is less than 4GB.
/// Panics otherwise.
#[inline]
pub fn from_unsafe(script_bytes: Vec<u8>) -> Self {
Self(Confined::try_from(script_bytes).expect("script exceeding 4GB"))
}

/// Adds instructions to push some arbitrary data onto the stack.
///
/// ## Panics
Expand Down Expand Up @@ -341,7 +395,9 @@ mod _serde {
Self::from_hex(&string).map_err(|_| D::Error::custom("wrong hex data"))
})
} else {
Vec::<u8>::deserialize(deserializer).map(ScriptBytes::from)
let bytes = Vec::<u8>::deserialize(deserializer)?;
ScriptBytes::try_from(bytes)
.map_err(|_| D::Error::custom("invalid script length exceeding 4GB"))
}
}
}
Expand Down
53 changes: 35 additions & 18 deletions consensus/src/segwit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
use std::vec;

use amplify::confinement::Confined;
use amplify::{Bytes32StrRev, Wrapper};
use amplify::{confinement, Bytes32StrRev, Wrapper};

use crate::opcodes::*;
use crate::{
OpCode, RedeemScript, ScriptBytes, ScriptPubkey, VarIntArray, WScriptHash, LIB_NAME_BITCOIN,
ByteStr, OpCode, RedeemScript, ScriptBytes, ScriptPubkey, VarIntArray, WScriptHash,
LIB_NAME_BITCOIN,
};

#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display, Error)]
Expand Down Expand Up @@ -313,20 +314,33 @@ impl ScriptPubkey {
derive(Serialize, Deserialize),
serde(crate = "serde_crate", transparent)
)]
pub struct WitnessScript(
#[from]
#[from(Vec<u8>)]
ScriptBytes,
);
pub struct WitnessScript(ScriptBytes);

impl TryFrom<Vec<u8>> for WitnessScript {
type Error = confinement::Error;
fn try_from(script_bytes: Vec<u8>) -> Result<Self, Self::Error> {
ScriptBytes::try_from(script_bytes).map(Self)
}
}

impl WitnessScript {
#[inline]
pub fn new() -> Self { Self::default() }

#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self(ScriptBytes::from(Confined::with_capacity(capacity)))
}

/// Constructs script object assuming the script length is less than 4GB.
/// Panics otherwise.
#[inline]
pub fn from_unsafe(script_bytes: Vec<u8>) -> Self {
Self(ScriptBytes::from_unsafe(script_bytes))
}

/// Adds a single opcode to the script.
#[inline]
pub fn push_opcode(&mut self, op_code: OpCode) { self.0.push(op_code as u8); }

pub fn to_redeem_script(&self) -> RedeemScript {
Expand All @@ -336,10 +350,12 @@ impl WitnessScript {

pub fn to_script_pubkey(&self) -> ScriptPubkey { ScriptPubkey::p2wsh(WScriptHash::from(self)) }

#[inline]
pub fn as_script_bytes(&self) -> &ScriptBytes { &self.0 }
}

#[derive(Wrapper, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, From)]
#[wrapper(BorrowSlice, Index, RangeOps, Debug, Hex, Display, FromStr)]
#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_BITCOIN)]
#[derive(CommitEncode)]
Expand All @@ -349,7 +365,6 @@ impl WitnessScript {
derive(Serialize, Deserialize),
serde(crate = "serde_crate", transparent)
)]
#[wrapper(BorrowSlice, Index, RangeOps, Debug, Hex, Display, FromStr)]
pub struct Wtxid(
#[from]
#[from([u8; 32])]
Expand All @@ -360,32 +375,35 @@ pub struct Wtxid(
#[wrapper(Deref, Index, RangeOps)]
#[derive(StrictType, StrictEncode, StrictDecode)]
#[strict_type(lib = LIB_NAME_BITCOIN)]
pub struct Witness(VarIntArray<VarIntArray<u8>>);
pub struct Witness(VarIntArray<ByteStr>);

impl IntoIterator for Witness {
type Item = VarIntArray<u8>;
type IntoIter = vec::IntoIter<VarIntArray<u8>>;
type Item = ByteStr;
type IntoIter = vec::IntoIter<ByteStr>;

fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
}

impl Witness {
#[inline]
pub fn new() -> Self { default!() }

#[inline]
pub fn elements(&self) -> impl Iterator<Item = &'_ [u8]> {
self.0.iter().map(|el| el.as_slice())
}

pub fn from_consensus_stack(witness: impl IntoIterator<Item = Vec<u8>>) -> Witness {
let iter = witness.into_iter().map(|vec| {
VarIntArray::try_from(vec).expect("witness stack element length exceeds 2^64 bytes")
ByteStr::try_from(vec).expect("witness stack element length exceeds 2^32 bytes")
});
let stack =
VarIntArray::try_from_iter(iter).expect("witness stack size exceeds 2^64 bytes");
VarIntArray::try_from_iter(iter).expect("witness stack size exceeds 2^32 elements");
Witness(stack)
}

pub(crate) fn as_var_int_array(&self) -> &VarIntArray<VarIntArray<u8>> { &self.0 }
#[inline]
pub(crate) fn as_var_int_array(&self) -> &VarIntArray<ByteStr> { &self.0 }
}

#[cfg(feature = "serde")]
Expand All @@ -395,14 +413,13 @@ mod _serde {
use serde_crate::{Deserializer, Serializer};

use super::*;
use crate::ScriptBytes;

impl Serialize for Witness {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
let mut ser = serializer.serialize_seq(Some(self.len()))?;
for el in &self.0 {
ser.serialize_element(&ScriptBytes::from(el.to_inner()))?;
ser.serialize_element(&el)?;
}
ser.end()
}
Expand All @@ -411,8 +428,8 @@ mod _serde {
impl<'de> Deserialize<'de> for Witness {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de> {
let data = Vec::<ScriptBytes>::deserialize(deserializer)?;
Ok(Witness::from_consensus_stack(data.into_iter().map(ScriptBytes::into_vec)))
let data = Vec::<ByteStr>::deserialize(deserializer)?;
Ok(Witness::from_consensus_stack(data.into_iter().map(ByteStr::into_vec)))
}
}
}
6 changes: 3 additions & 3 deletions consensus/src/stl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ use crate::{

#[deprecated(since = "0.10.8", note = "use LIB_ID_BP_TX instead")]
pub const LIB_ID_BITCOIN: &str =
"urn:ubideco:stl:6GgF7biXPVNcus2FfQj2pQuRzau11rXApMQLfCZhojgi#money-pardon-parody";
"urn:ubideco:stl:HX2UBak8vPsTokug1DGMDvTpzns3xUdwZ7QJdyt4qBA9#speed-atlanta-trilogy";
pub const LIB_ID_BP_TX: &str =
"urn:ubideco:stl:6GgF7biXPVNcus2FfQj2pQuRzau11rXApMQLfCZhojgi#money-pardon-parody";
"urn:ubideco:stl:HX2UBak8vPsTokug1DGMDvTpzns3xUdwZ7QJdyt4qBA9#speed-atlanta-trilogy";
pub const LIB_ID_BP_CONSENSUS: &str =
"urn:ubideco:stl:4AXTqXq8jUDs244XbhvErdsG82Y8r9PiaPBPAmD5y9fQ#cheese-provide-morph";
"urn:ubideco:stl:8nFxDqDPmHD15hQf1npGQbDfpRX4Y9hXoo7VfarMJX11#urgent-viva-ninja";

#[deprecated(since = "0.10.8", note = "use _bp_tx_stl instead")]
fn _bitcoin_stl() -> Result<TypeLib, CompileError> { _bp_tx_stl() }
Expand Down
Loading