diff --git a/consensus/src/coding.rs b/consensus/src/coding.rs index 46b191ab..33cbf880 100644 --- a/consensus/src/coding.rs +++ b/consensus/src/coding.rs @@ -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 = Confined, 0, U32>; /// A variable-length unsigned integer. @@ -95,7 +100,7 @@ impl AsRef<[u8]> for ByteStr { } impl From> for ByteStr { - fn from(value: Vec) -> Self { Self(Confined::try_from(value).expect("u64 >= usize")) } + fn from(value: Vec) -> Self { Self(Confined::try_from(value).expect("u32 >= usize")) } } impl ByteStr { diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index 7d5fe62a..e273dc7b 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -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 diff --git a/consensus/src/script.rs b/consensus/src/script.rs index 7648d8fa..df9f568a 100644 --- a/consensus/src/script.rs +++ b/consensus/src/script.rs @@ -1,4 +1,4 @@ -// Bitcoin protocol consensus library. +// Bitcoin protocol primitives library. // // SPDX-License-Identifier: Apache-2.0 // @@ -19,8 +19,9 @@ // 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 amplify::hex::{FromHex, ToHex}; use crate::opcodes::*; use crate::{VarInt, VarIntArray, LIB_NAME_BITCOIN}; @@ -116,14 +117,35 @@ pub enum OpCode { derive(Serialize, Deserialize), serde(crate = "serde_crate", transparent) )] -pub struct SigScript( - #[from] - #[from(Vec)] - ScriptBytes, -); +pub struct SigScript(ScriptBytes); + +impl TryFrom> for SigScript { + type Error = confinement::Error; + fn try_from(script_bytes: Vec) -> Result { + 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) -> Self { + Self(ScriptBytes::from_unsafe(script_bytes)) + } + + #[inline] pub fn as_script_bytes(&self) -> &ScriptBytes { &self.0 } } @@ -137,19 +159,31 @@ impl SigScript { derive(Serialize, Deserialize), serde(crate = "serde_crate", transparent) )] -pub struct ScriptPubkey( - #[from] - #[from(Vec)] - ScriptBytes, -); +pub struct ScriptPubkey(ScriptBytes); + +impl TryFrom> for ScriptPubkey { + type Error = confinement::Error; + fn try_from(script_bytes: Vec) -> Result { + 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) -> 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); @@ -195,11 +229,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 } } @@ -213,43 +250,61 @@ impl ScriptPubkey { derive(Serialize, Deserialize), serde(crate = "serde_crate", transparent) )] -pub struct RedeemScript( - #[from] - #[from(Vec)] - ScriptBytes, -); +pub struct RedeemScript(ScriptBytes); + +impl TryFrom> for RedeemScript { + type Error = confinement::Error; + fn try_from(script_bytes: Vec) -> Result { + 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) -> 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); -impl From> for ScriptBytes { - fn from(value: Vec) -> Self { Self(Confined::try_from(value).expect("u64 >= usize")) } +impl TryFrom> for ScriptBytes { + type Error = confinement::Error; + fn try_from(script_bytes: Vec) -> Result { + 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) -> Self { + Self(Confined::try_from(script_bytes).expect("script exceeding 4GB")) + } + /// Adds instructions to push some arbitrary data onto the stack. /// /// ## Panics @@ -315,7 +370,6 @@ impl ScriptBytes { #[cfg(feature = "serde")] mod _serde { - use amplify::hex::{FromHex, ToHex}; use serde::{Deserialize, Serialize}; use serde_crate::de::Error; use serde_crate::{Deserializer, Serializer}; @@ -341,42 +395,10 @@ mod _serde { Self::from_hex(&string).map_err(|_| D::Error::custom("wrong hex data")) }) } else { - Vec::::deserialize(deserializer).map(ScriptBytes::from) + let bytes = Vec::::deserialize(deserializer)?; + ScriptBytes::try_from(bytes) + .map_err(|_| D::Error::custom("invalid script length exceeding 4GB")) } } } } - -#[cfg(test)] -mod test { - use amplify::hex::ToHex; - - use super::*; - - #[test] - fn script_index() { - let mut script = ScriptPubkey::op_return(&[0u8; 40]); - assert_eq!(script[0], OP_RETURN); - assert_eq!(&script[..2], &[OP_RETURN, OP_PUSHBYTES_40]); - assert_eq!(&script[40..], &[0u8, 0u8]); - assert_eq!(&script[2..4], &[0u8, 0u8]); - assert_eq!(&script[2..=3], &[0u8, 0u8]); - - script[0] = 0xFF; - script[..2].copy_from_slice(&[0xFF, 0xFF]); - script[40..].copy_from_slice(&[0xFF, 0xFF]); - script[2..4].copy_from_slice(&[0xFF, 0xFF]); - script[2..=3].copy_from_slice(&[0xFF, 0xFF]); - - assert_eq!(script[0], 0xFF); - assert_eq!(&script[..2], &[0xFF, 0xFF]); - assert_eq!(&script[40..], &[0xFF, 0xFF]); - assert_eq!(&script[2..4], &[0xFF, 0xFF]); - assert_eq!(&script[2..=3], &[0xFF, 0xFF]); - - assert_eq!( - &script.to_hex(), - "ffffffff000000000000000000000000000000000000000000000000000000000000000000000000ffff" - ); - } -} diff --git a/consensus/src/segwit.rs b/consensus/src/segwit.rs index b4b134a9..d2fdda7a 100644 --- a/consensus/src/segwit.rs +++ b/consensus/src/segwit.rs @@ -1,4 +1,4 @@ -// Bitcoin protocol consensus library. +// Bitcoin protocol primitives library. // // SPDX-License-Identifier: Apache-2.0 // @@ -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)] @@ -313,20 +314,33 @@ impl ScriptPubkey { derive(Serialize, Deserialize), serde(crate = "serde_crate", transparent) )] -pub struct WitnessScript( - #[from] - #[from(Vec)] - ScriptBytes, -); +pub struct WitnessScript(ScriptBytes); + +impl TryFrom> for WitnessScript { + type Error = confinement::Error; + fn try_from(script_bytes: Vec) -> Result { + 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) -> 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 { @@ -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)] @@ -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])] @@ -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>); +pub struct Witness(VarIntArray); impl IntoIterator for Witness { - type Item = VarIntArray; - type IntoIter = vec::IntoIter>; + type Item = ByteStr; + type IntoIter = vec::IntoIter; 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 { self.0.iter().map(|el| el.as_slice()) } pub fn from_consensus_stack(witness: impl IntoIterator>) -> 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> { &self.0 } + #[inline] + pub(crate) fn as_var_int_array(&self) -> &VarIntArray { &self.0 } } #[cfg(feature = "serde")] @@ -395,14 +413,13 @@ mod _serde { use serde_crate::{Deserializer, Serializer}; use super::*; - use crate::ScriptBytes; impl Serialize for Witness { fn serialize(&self, serializer: S) -> Result 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() } @@ -411,8 +428,8 @@ mod _serde { impl<'de> Deserialize<'de> for Witness { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { - let data = Vec::::deserialize(deserializer)?; - Ok(Witness::from_consensus_stack(data.into_iter().map(ScriptBytes::into_vec))) + let data = Vec::::deserialize(deserializer)?; + Ok(Witness::from_consensus_stack(data.into_iter().map(ByteStr::into_vec))) } } } diff --git a/consensus/src/taproot.rs b/consensus/src/taproot.rs index f44fddf1..f627978c 100644 --- a/consensus/src/taproot.rs +++ b/consensus/src/taproot.rs @@ -564,7 +564,7 @@ impl LeafScript { pub fn with_bytes(version: LeafVer, script: Vec) -> Result { Ok(LeafScript { version, - script: ScriptBytes::from(script), + script: ScriptBytes::try_from(script)?, }) } #[inline] @@ -618,16 +618,34 @@ pub enum TapCode { pub struct TapScript(ScriptBytes); // TODO: impl Display/FromStr for TapScript providing correct opcodes +impl TryFrom> for TapScript { + type Error = confinement::Error; + fn try_from(script_bytes: Vec) -> Result { + ScriptBytes::try_from(script_bytes).map(Self) + } +} + impl TapScript { + #[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) -> Self { + Self(ScriptBytes::from_unsafe(script_bytes)) + } + /// Adds a single opcode to the script. + #[inline] pub fn push_opcode(&mut self, op_code: TapCode) { self.0.push(op_code as u8); } + #[inline] pub fn as_script_bytes(&self) -> &ScriptBytes { &self.0 } }