Repository: rust-bitcoin/rust-bitcoin
Pull request #4092 (2025-02-20 23:38:13)

Add `Timestamp` newtype
Bitcoin block headers have a timestamp. Currently we are using a `u32`. While this functions correctly it gives the compiler no chance to enforce type safety.

Add a `Timestamp` newtype that is a thin wrapper around a `u32`. Document it and test the API surface in `api.rs`.


Patch:
diff --git a/bitcoin/src/bip152.rs b/bitcoin/src/bip152.rs
index f57fe3e1..c7b69526 100644
--- a/bitcoin/src/bip152.rs
+++ b/bitcoin/src/bip152.rs
@@ -413,8 +413,8 @@ mod test {
     use crate::merkle_tree::TxMerkleNode;
     use crate::transaction::OutPointExt;
     use crate::{
-        transaction, Amount, BlockChecked, CompactTarget, OutPoint, ScriptBuf, Sequence, TxIn,
-        TxOut, Txid, Witness,
+        transaction, Amount, BlockChecked, CompactTarget, OutPoint, ScriptBuf, Sequence, Timestamp,
+        TxIn, TxOut, Txid, Witness,
     };
 
     fn dummy_tx(nonce: &[u8]) -> Transaction {
@@ -437,7 +437,7 @@ fn dummy_block() -> Block<BlockChecked> {
             version: block::Version::ONE,
             prev_blockhash: BlockHash::from_byte_array([0x99; 32]),
             merkle_root: TxMerkleNode::from_byte_array([0x77; 32]),
-            time: 2,
+            time: Timestamp::from_u32(2),
             bits: CompactTarget::from_consensus(3),
             nonce: 4,
         };
diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs
index 43ac31ad..dfd039e5 100644
--- a/bitcoin/src/blockdata/block.rs
+++ b/bitcoin/src/blockdata/block.rs
@@ -13,6 +13,7 @@
 use hashes::{sha256d, HashEngine};
 use internals::{compact_size, ToU64};
 use io::{BufRead, Write};
+use units::Timestamp;
 
 use super::Weight;
 use crate::consensus::encode::WriteExt as _;
@@ -85,6 +86,18 @@ fn consensus_decode<R: BufRead + ?Sized>(r: &mut R) -> Result<Self, encode::Erro
     }
 }
 
+impl Encodable for Timestamp {
+    fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
+        self.to_u32().consensus_encode(w)
+    }
+}
+
+impl Decodable for Timestamp {
+    fn consensus_decode<R: BufRead + ?Sized>(r: &mut R) -> Result<Self, encode::Error> {
+        Decodable::consensus_decode(r).map(Timestamp::from_u32)
+    }
+}
+
 /// Extension functionality for the [`Block<Unchecked>`] type.
 pub trait BlockUncheckedExt: sealed::Sealed {
     /// Validates (or checks) a block.
@@ -596,7 +609,7 @@ fn block() {
             block::compute_merkle_root(&transactions).unwrap()
         );
         assert_eq!(serialize(&real_decode.header().merkle_root), merkle);
-        assert_eq!(real_decode.header().time, 1231965655);
+        assert_eq!(real_decode.header().time, Timestamp::from_u32(1231965655));
         assert_eq!(real_decode.header().bits, CompactTarget::from_consensus(486604799));
         assert_eq!(real_decode.header().nonce, 2067413810);
         assert_eq!(real_decode.header().work(), work);
@@ -647,7 +660,7 @@ fn segwit_block() {
             real_decode.header().merkle_root,
             block::compute_merkle_root(&transactions).unwrap()
         );
-        assert_eq!(real_decode.header().time, 1472004949);
+        assert_eq!(real_decode.header().time, Timestamp::from_u32(1472004949));
         assert_eq!(real_decode.header().bits, CompactTarget::from_consensus(0x1a06d450));
         assert_eq!(real_decode.header().nonce, 1879759182);
         assert_eq!(real_decode.header().work(), work);
diff --git a/bitcoin/src/blockdata/constants.rs b/bitcoin/src/blockdata/constants.rs
index 4b7e5148..2c5cb4e1 100644
--- a/bitcoin/src/blockdata/constants.rs
+++ b/bitcoin/src/blockdata/constants.rs
@@ -16,7 +16,7 @@
 use crate::pow::CompactTarget;
 use crate::transaction::{self, OutPoint, Transaction, TxIn, TxOut};
 use crate::witness::Witness;
-use crate::{script, Amount, BlockHash, Sequence, TestnetVersion};
+use crate::{script, Amount, BlockHash, Sequence, TestnetVersion, Timestamp};
 
 /// How many seconds between blocks we expect on average.
 pub const TARGET_BLOCK_SPACING: u32 = 600;
@@ -132,7 +132,7 @@ pub fn genesis_block(params: impl AsRef<Params>) -> Block<Checked> {
                 version: block::Version::ONE,
                 prev_blockhash: BlockHash::GENESIS_PREVIOUS_BLOCK_HASH,
                 merkle_root,
-                time: 1231006505,
+                time: Timestamp::from_u32(1231006505),
                 bits: CompactTarget::from_consensus(0x1d00ffff),
                 nonce: 2083236893,
             },
@@ -144,7 +144,7 @@ pub fn genesis_block(params: impl AsRef<Params>) -> Block<Checked> {
                 version: block::Version::ONE,
                 prev_blockhash: BlockHash::GENESIS_PREVIOUS_BLOCK_HASH,
                 merkle_root,
-                time: 1296688602,
+                time: Timestamp::from_u32(1296688602),
                 bits: CompactTarget::from_consensus(0x1d00ffff),
                 nonce: 414098458,
             },
@@ -156,7 +156,7 @@ pub fn genesis_block(params: impl AsRef<Params>) -> Block<Checked> {
                 version: block::Version::ONE,
                 prev_blockhash: BlockHash::GENESIS_PREVIOUS_BLOCK_HASH,
                 merkle_root,
-                time: 1714777860,
+                time: Timestamp::from_u32(1714777860),
                 bits: CompactTarget::from_consensus(0x1d00ffff),
                 nonce: 393743547,
             },
@@ -168,7 +168,7 @@ pub fn genesis_block(params: impl AsRef<Params>) -> Block<Checked> {
                 version: block::Version::ONE,
                 prev_blockhash: BlockHash::GENESIS_PREVIOUS_BLOCK_HASH,
                 merkle_root,
-                time: 1598918400,
+                time: Timestamp::from_u32(1598918400),
                 bits: CompactTarget::from_consensus(0x1e0377ae),
                 nonce: 52613770,
             },
@@ -180,7 +180,7 @@ pub fn genesis_block(params: impl AsRef<Params>) -> Block<Checked> {
                 version: block::Version::ONE,
                 prev_blockhash: BlockHash::GENESIS_PREVIOUS_BLOCK_HASH,
                 merkle_root,
-                time: 1296688602,
+                time: Timestamp::from_u32(1296688602),
                 bits: CompactTarget::from_consensus(0x207fffff),
                 nonce: 2,
             },
@@ -320,7 +320,7 @@ fn bitcoin_genesis_full_block() {
             "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
         );
 
-        assert_eq!(gen.header().time, 1231006505);
+        assert_eq!(gen.header().time, Timestamp::from_u32(1231006505));
         assert_eq!(gen.header().bits, CompactTarget::from_consensus(0x1d00ffff));
         assert_eq!(gen.header().nonce, 2083236893);
         assert_eq!(
@@ -338,7 +338,7 @@ fn testnet_genesis_full_block() {
             gen.header().merkle_root.to_string(),
             "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
         );
-        assert_eq!(gen.header().time, 1296688602);
+        assert_eq!(gen.header().time, Timestamp::from_u32(1296688602));
         assert_eq!(gen.header().bits, CompactTarget::from_consensus(0x1d00ffff));
         assert_eq!(gen.header().nonce, 414098458);
         assert_eq!(
@@ -356,7 +356,7 @@ fn signet_genesis_full_block() {
             gen.header().merkle_root.to_string(),
             "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
         );
-        assert_eq!(gen.header().time, 1598918400);
+        assert_eq!(gen.header().time, Timestamp::from_u32(1598918400));
         assert_eq!(gen.header().bits, CompactTarget::from_consensus(0x1e0377ae));
         assert_eq!(gen.header().nonce, 52613770);
         assert_eq!(
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index af6386a4..ab1d7c9a 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -135,6 +135,7 @@
     amount::{Amount, Denomination, SignedAmount},
     block::{BlockHeight, BlockInterval},
     fee_rate::FeeRate,
+    timestamp::{self, Timestamp},
     weight::Weight,
 };
 
diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs
index e2531927..09929f64 100644
--- a/bitcoin/src/pow.rs
+++ b/bitcoin/src/pow.rs
@@ -420,9 +420,9 @@ fn from_header_difficulty_adjustment(
             current: Header,
             params: impl AsRef<Params>,
         ) -> CompactTarget {
-            let timespan = current.time - last_epoch_boundary.time;
+            let timespan = i64::from(current.time.to_u32()) - i64::from(last_epoch_boundary.time.to_u32());
             let bits = current.bits;
-            CompactTarget::from_next_work_required(bits, timespan.into(), params)
+            CompactTarget::from_next_work_required(bits, timespan, params)
         }
     }
 }
@@ -1078,6 +1078,7 @@ pub fn u64_to_target(u: u64) -> Target { Target(U256::from(u)) }
 mod tests {
     use super::*;
     use crate::pow::test_utils::{u128_to_work, u32_to_target, u64_to_target};
+    use crate::Timestamp;
 
     impl U256 {
         fn bit_at(&self, index: usize) -> bool {
@@ -1762,7 +1763,7 @@ fn compact_target_from_upwards_difficulty_adjustment_using_headers() {
             version: Version::ONE,
             prev_blockhash: BlockHash::from_byte_array([0; 32]),
             merkle_root: TxMerkleNode::from_byte_array([0; 32]),
-            time: 1599332177,
+            time: Timestamp::from_u32(1599332177),
             bits: epoch_start.bits,
             nonce: epoch_start.nonce,
         };
@@ -1784,7 +1785,7 @@ fn compact_target_from_downwards_difficulty_adjustment_using_headers() {
             version: Version::ONE,
             prev_blockhash: BlockHash::from_byte_array([0; 32]),
             merkle_root: TxMerkleNode::from_byte_array([0; 32]),
-            time: 1599332844,
+            time: Timestamp::from_u32(1599332844),
             bits: starting_bits,
             nonce: 0,
         };
@@ -1794,7 +1795,7 @@ fn compact_target_from_downwards_difficulty_adjustment_using_headers() {
             version: Version::ONE,
             prev_blockhash: BlockHash::from_byte_array([0; 32]),
             merkle_root: TxMerkleNode::from_byte_array([0; 32]),
-            time: 1600591200,
+            time: Timestamp::from_u32(1600591200),
             bits: starting_bits,
             nonce: 0,
         };
diff --git a/primitives/src/block.rs b/primitives/src/block.rs
index b30a2c6c..5de122e5 100644
--- a/primitives/src/block.rs
+++ b/primitives/src/block.rs
@@ -14,6 +14,7 @@
 #[cfg(feature = "arbitrary")]
 use arbitrary::{Arbitrary, Unstructured};
 use hashes::{sha256d, HashEngine as _};
+use units::Timestamp;
 
 use crate::merkle_tree::TxMerkleNode;
 #[cfg(feature = "alloc")]
@@ -170,7 +171,7 @@ pub struct Header {
     /// The root hash of the Merkle tree of transactions in the block.
     pub merkle_root: TxMerkleNode,
     /// The timestamp of the block, as claimed by the miner.
-    pub time: u32,
+    pub time: Timestamp,
     /// The target value below which the blockhash must lie.
     pub bits: CompactTarget,
     /// The nonce, selected to obtain a low enough blockhash.
@@ -189,7 +190,7 @@ pub fn block_hash(&self) -> BlockHash {
         engine.input(&self.version.to_consensus().to_le_bytes());
         engine.input(self.prev_blockhash.as_byte_array());
         engine.input(self.merkle_root.as_byte_array());
-        engine.input(&self.time.to_le_bytes());
+        engine.input(&self.time.to_u32().to_le_bytes());
         engine.input(&self.bits.to_consensus().to_le_bytes());
         engine.input(&self.nonce.to_le_bytes());
 
@@ -388,7 +389,7 @@ fn header_size() {
             version: Version::ONE,
             prev_blockhash: BlockHash::from_byte_array([0x99; 32]),
             merkle_root: TxMerkleNode::from_byte_array([0x77; 32]),
-            time: 2,
+            time: Timestamp::from(2),
             bits: CompactTarget::from_consensus(3),
             nonce: 4,
         };
@@ -398,7 +399,7 @@ fn header_size() {
         let header_size = header.version.to_consensus().to_le_bytes().len()
             + header.prev_blockhash.as_byte_array().len()
             + header.merkle_root.as_byte_array().len()
-            + header.time.to_le_bytes().len()
+            + header.time.to_u32().to_le_bytes().len()
             + header.bits.to_consensus().to_le_bytes().len()
             + header.nonce.to_le_bytes().len();
 
diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs
index 5cd71506..ebc44aa5 100644
--- a/primitives/src/lib.rs
+++ b/primitives/src/lib.rs
@@ -59,6 +59,7 @@ pub mod _core {
     amount::{self, Amount, SignedAmount},
     block::{BlockHeight, BlockInterval},
     fee_rate::{self, FeeRate},
+    timestamp::{self, Timestamp},
     weight::{self, Weight},
 };
 
diff --git a/primitives/src/script/borrowed.rs b/primitives/src/script/borrowed.rs
index ece38f6e..6e68a6ef 100644
--- a/primitives/src/script/borrowed.rs
+++ b/primitives/src/script/borrowed.rs
@@ -4,9 +4,6 @@
     Bound, Index, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
 };
 
-#[cfg(feature = "arbitrary")]
-use arbitrary::{Arbitrary, Unstructured};
-
 use super::ScriptBuf;
 use crate::prelude::{Box, ToOwned, Vec};
 
@@ -57,7 +54,7 @@
 ///
 #[derive(PartialOrd, Ord, PartialEq, Eq, Hash)]
 #[repr(transparent)]
-pub struct Script([u8]);
+pub struct Script(pub(in crate::script) [u8]);
 
 impl Default for &Script {
     #[inline]
@@ -67,40 +64,39 @@ fn default() -> Self { Script::new() }
 impl ToOwned for Script {
     type Owned = ScriptBuf;
 
-    #[inline]
-    fn to_owned(&self) -> Self::Owned { ScriptBuf::from_bytes(self.to_vec()) }
+    fn to_owned(&self) -> Self::Owned { ScriptBuf(self.0.to_owned()) }
 }
 
 impl Script {
     /// Constructs a new empty script.
     #[inline]
-    pub const fn new() -> &'static Self { Self::from_bytes(&[]) }
+    pub fn new() -> &'static Script { Script::from_bytes(&[]) }
 
     /// Treat byte slice as `Script`
     #[inline]
-    pub const fn from_bytes(bytes: &[u8]) -> &Self {
+    pub fn from_bytes(bytes: &[u8]) -> &Script {
         // SAFETY: copied from `std`
         // The pointer was just created from a reference which is still alive.
         // Casting slice pointer to a transparent struct wrapping that slice is sound (same
         // layout).
-        unsafe { &*(bytes as *const [u8] as *const Self) }
+        unsafe { &*(bytes as *const [u8] as *const Script) }
     }
 
     /// Treat mutable byte slice as `Script`
     #[inline]
-    pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Self {
+    pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut Script {
         // SAFETY: copied from `std`
         // The pointer was just created from a reference which is still alive.
         // Casting slice pointer to a transparent struct wrapping that slice is sound (same
         // layout).
         // Function signature prevents callers from accessing `bytes` while the returned reference
         // is alive.
-        unsafe { &mut *(bytes as *mut [u8] as *mut Self) }
+        unsafe { &mut *(bytes as *mut [u8] as *mut Script) }
     }
 
     /// Returns the script data as a byte slice.
     #[inline]
-    pub const fn as_bytes(&self) -> &[u8] { &self.0 }
+    pub fn as_bytes(&self) -> &[u8] { &self.0 }
 
     /// Returns the script data as a mutable byte slice.
     #[inline]
@@ -108,7 +104,7 @@ pub fn as_mut_bytes(&mut self) -> &mut [u8] { &mut self.0 }
 
     /// Returns a copy of the script data.
     #[inline]
-    pub fn to_vec(&self) -> Vec<u8> { self.as_bytes().to_owned() }
+    pub fn to_vec(&self) -> Vec<u8> { self.0.to_owned() }
 
     /// Returns a copy of the script data.
     #[inline]
@@ -117,15 +113,14 @@ pub fn to_bytes(&self) -> Vec<u8> { self.to_vec() }
 
     /// Returns the length in bytes of the script.
     #[inline]
-    pub const fn len(&self) -> usize { self.as_bytes().len() }
+    pub fn len(&self) -> usize { self.0.len() }
 
     /// Returns whether the script is the empty script.
     #[inline]
-    pub const fn is_empty(&self) -> bool { self.as_bytes().is_empty() }
+    pub fn is_empty(&self) -> bool { self.0.is_empty() }
 
     /// Converts a [`Box<Script>`](Box) into a [`ScriptBuf`] without copying or allocating.
     #[must_use]
-    #[inline]
     pub fn into_script_buf(self: Box<Self>) -> ScriptBuf {
         let rw = Box::into_raw(self) as *mut [u8];
         // SAFETY: copied from `std`
@@ -133,16 +128,7 @@ pub fn into_script_buf(self: Box<Self>) -> ScriptBuf {
         // Casting a transparent struct wrapping a slice to the slice pointer is sound (same
         // layout).
         let inner = unsafe { Box::from_raw(rw) };
-        ScriptBuf::from_bytes(Vec::from(inner))
-    }
-}
-
-#[cfg(feature = "arbitrary")]
-impl<'a> Arbitrary<'a> for &'a Script {
-    #[inline]
-    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
-        let v = <&'a [u8]>::arbitrary(u)?;
-        Ok(Script::from_bytes(v))
+        ScriptBuf(Vec::from(inner))
     }
 }
 
@@ -155,7 +141,7 @@ impl Index<$type> for Script {
 
                 #[inline]
                 fn index(&self, index: $type) -> &Self::Output {
-                    Self::from_bytes(&self.as_bytes()[index])
+                    Self::from_bytes(&self.0[index])
                 }
             }
         )*
diff --git a/primitives/src/script/mod.rs b/primitives/src/script/mod.rs
index 7832ecfe..dda22402 100644
--- a/primitives/src/script/mod.rs
+++ b/primitives/src/script/mod.rs
@@ -2,12 +2,15 @@
 
 //! Bitcoin scripts.
 
+/// FIXME: Make this private.
 mod borrowed;
+/// FIXME: Make this private.
 mod owned;
 
 use core::cmp::Ordering;
 use core::convert::Infallible;
 use core::fmt;
+use core::ops::{Deref, DerefMut};
 
 use hashes::{hash160, sha256};
 use hex::DisplayHex;
@@ -33,18 +36,9 @@
 pub const MAX_WITNESS_SCRIPT_SIZE: usize = 10_000;
 
 hashes::hash_newtype! {
-    /// A 160-bit hash of Bitcoin Script bytecode.
-    ///
-    /// Note: there is another "script hash" object in bitcoin ecosystem (Electrum protocol) that
-    /// uses 256-bit hash and hashes a semantically different script. Thus, this type cannot
-    /// represent it.
+    /// A hash of Bitcoin Script bytecode.
     pub struct ScriptHash(hash160::Hash);
-
-    /// SegWit (256-bit) version of a Bitcoin Script bytecode hash.
-    ///
-    /// Note: there is another "script hash" object in bitcoin ecosystem (Electrum protocol) that
-    /// looks similar to this one also being SHA256, however, they hash semantically different
-    /// scripts and have reversed representations, so this type cannot be used for both.
+    /// SegWit version of a Bitcoin Script bytecode hash.
     pub struct WScriptHash(sha256::Hash);
 }
 
@@ -63,14 +57,12 @@ impl ScriptHash {
     /// > spend a P2SH output if the redemption script it refers to is >520 bytes in length.
     ///
     /// ref: [BIP-16](https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki#user-content-520byte_limitation_on_serialized_script_size)
-    #[inline]
     pub fn from_script(redeem_script: &Script) -> Result<Self, RedeemScriptSizeError> {
         if redeem_script.len() > MAX_REDEEM_SCRIPT_SIZE {
             return Err(RedeemScriptSizeError { size: redeem_script.len() });
         }
 
-        // We've just checked the length
-        Ok(ScriptHash::from_script_unchecked(redeem_script))
+        Ok(ScriptHash(hash160::Hash::hash(redeem_script.as_bytes())))
     }
 
     /// Constructs a new `ScriptHash` from any script irrespective of script size.
@@ -79,7 +71,6 @@ pub fn from_script(redeem_script: &Script) -> Result<Self, RedeemScriptSizeError
     /// then the output will be unspendable (see [BIP-16]).
     ///
     /// [BIP-16]: <https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki#user-content-520byte_limitation_on_serialized_script_size>
-    #[inline]
     pub fn from_script_unchecked(script: &Script) -> Self {
         ScriptHash(hash160::Hash::hash(script.as_bytes()))
     }
@@ -94,14 +85,12 @@ impl WScriptHash {
     /// > witnessScript must match the 32-byte witness program.
     ///
     /// ref: [BIP-141](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki)
-    #[inline]
     pub fn from_script(witness_script: &Script) -> Result<Self, WitnessScriptSizeError> {
         if witness_script.len() > MAX_WITNESS_SCRIPT_SIZE {
             return Err(WitnessScriptSizeError { size: witness_script.len() });
         }
 
-        // We've just checked the length
-        Ok(WScriptHash::from_script_unchecked(witness_script))
+        Ok(WScriptHash(sha256::Hash::hash(witness_script.as_bytes())))
     }
 
     /// Constructs a new `WScriptHash` from any script irrespective of script size.
@@ -110,7 +99,6 @@ pub fn from_script(witness_script: &Script) -> Result<Self, WitnessScriptSizeErr
     /// output then the output will be unspendable (see [BIP-141]).
     ///
     /// ref: [BIP-141](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki)
-    #[inline]
     pub fn from_script_unchecked(script: &Script) -> Self {
         WScriptHash(sha256::Hash::hash(script.as_bytes()))
     }
@@ -119,7 +107,6 @@ pub fn from_script_unchecked(script: &Script) -> Self {
 impl TryFrom<ScriptBuf> for ScriptHash {
     type Error = RedeemScriptSizeError;
 
-    #[inline]
     fn try_from(redeem_script: ScriptBuf) -> Result<Self, Self::Error> {
         Self::from_script(&redeem_script)
     }
@@ -128,7 +115,6 @@ fn try_from(redeem_script: ScriptBuf) -> Result<Self, Self::Error> {
 impl TryFrom<&ScriptBuf> for ScriptHash {
     type Error = RedeemScriptSizeError;
 
-    #[inline]
     fn try_from(redeem_script: &ScriptBuf) -> Result<Self, Self::Error> {
         Self::from_script(redeem_script)
     }
@@ -137,7 +123,6 @@ fn try_from(redeem_script: &ScriptBuf) -> Result<Self, Self::Error> {
 impl TryFrom<&Script> for ScriptHash {
     type Error = RedeemScriptSizeError;
 
-    #[inline]
     fn try_from(redeem_script: &Script) -> Result<Self, Self::Error> {
         Self::from_script(redeem_script)
     }
@@ -146,7 +131,6 @@ fn try_from(redeem_script: &Script) -> Result<Self, Self::Error> {
 impl TryFrom<ScriptBuf> for WScriptHash {
     type Error = WitnessScriptSizeError;
 
-    #[inline]
     fn try_from(witness_script: ScriptBuf) -> Result<Self, Self::Error> {
         Self::from_script(&witness_script)
     }
@@ -155,7 +139,6 @@ fn try_from(witness_script: ScriptBuf) -> Result<Self, Self::Error> {
 impl TryFrom<&ScriptBuf> for WScriptHash {
     type Error = WitnessScriptSizeError;
 
-    #[inline]
     fn try_from(witness_script: &ScriptBuf) -> Result<Self, Self::Error> {
         Self::from_script(witness_script)
     }
@@ -164,7 +147,6 @@ fn try_from(witness_script: &ScriptBuf) -> Result<Self, Self::Error> {
 impl TryFrom<&Script> for WScriptHash {
     type Error = WitnessScriptSizeError;
 
-    #[inline]
     fn try_from(witness_script: &Script) -> Result<Self, Self::Error> {
         Self::from_script(witness_script)
     }
@@ -178,12 +160,10 @@ pub struct RedeemScriptSizeError {
 }
 
 impl From<Infallible> for RedeemScriptSizeError {
-    #[inline]
     fn from(never: Infallible) -> Self { match never {} }
 }
 
 impl fmt::Display for RedeemScriptSizeError {
-    #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "redeem script size exceeds {} bytes: {}", MAX_REDEEM_SCRIPT_SIZE, self.size)
     }
@@ -200,12 +180,10 @@ pub struct WitnessScriptSizeError {
 }
 
 impl From<Infallible> for WitnessScriptSizeError {
-    #[inline]
     fn from(never: Infallible) -> Self { match never {} }
 }
 
 impl fmt::Display for WitnessScriptSizeError {
-    #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "witness script size exceeds {} bytes: {}", MAX_WITNESS_SCRIPT_SIZE, self.size)
     }
@@ -217,17 +195,14 @@ impl std::error::Error for WitnessScriptSizeError {}
 // We keep all the `Script` and `ScriptBuf` impls together since its easier to see side-by-side.
 
 impl From<ScriptBuf> for Box<Script> {
-    #[inline]
     fn from(v: ScriptBuf) -> Self { v.into_boxed_script() }
 }
 
 impl From<ScriptBuf> for Cow<'_, Script> {
-    #[inline]
     fn from(value: ScriptBuf) -> Self { Cow::Owned(value) }
 }
 
 impl<'a> From<Cow<'a, Script>> for ScriptBuf {
-    #[inline]
     fn from(value: Cow<'a, Script>) -> Self {
         match value {
             Cow::Owned(owned) => owned,
@@ -237,7 +212,6 @@ fn from(value: Cow<'a, Script>) -> Self {
 }
 
 impl<'a> From<Cow<'a, Script>> for Box<Script> {
-    #[inline]
     fn from(value: Cow<'a, Script>) -> Self {
         match value {
             Cow::Owned(owned) => owned.into(),
@@ -247,26 +221,22 @@ fn from(value: Cow<'a, Script>) -> Self {
 }
 
 impl<'a> From<&'a Script> for Box<Script> {
-    #[inline]
     fn from(value: &'a Script) -> Self { value.to_owned().into() }
 }
 
 impl<'a> From<&'a Script> for ScriptBuf {
-    #[inline]
     fn from(value: &'a Script) -> Self { value.to_owned() }
 }
 
 impl<'a> From<&'a Script> for Cow<'a, Script> {
-    #[inline]
     fn from(value: &'a Script) -> Self { Cow::Borrowed(value) }
 }
 
 /// Note: This will fail to compile on old Rust for targets that don't support atomics
 #[cfg(target_has_atomic = "ptr")]
 impl<'a> From<&'a Script> for Arc<Script> {
-    #[inline]
     fn from(value: &'a Script) -> Self {
-        let rw: *const [u8] = Arc::into_raw(Arc::from(value.as_bytes()));
+        let rw: *const [u8] = Arc::into_raw(Arc::from(&value.0));
         // SAFETY: copied from `std`
         // The pointer was just created from an Arc without deallocating
         // Casting a slice to a transparent struct wrapping that slice is sound (same
@@ -276,9 +246,8 @@ fn from(value: &'a Script) -> Self {
 }
 
 impl<'a> From<&'a Script> for Rc<Script> {
-    #[inline]
     fn from(value: &'a Script) -> Self {
-        let rw: *const [u8] = Rc::into_raw(Rc::from(value.as_bytes()));
+        let rw: *const [u8] = Rc::into_raw(Rc::from(&value.0));
         // SAFETY: copied from `std`
         // The pointer was just created from an Rc without deallocating
         // Casting a slice to a transparent struct wrapping that slice is sound (same
@@ -288,22 +257,19 @@ fn from(value: &'a Script) -> Self {
 }
 
 impl From<Vec<u8>> for ScriptBuf {
-    #[inline]
-    fn from(v: Vec<u8>) -> Self { ScriptBuf::from_bytes(v) }
+    fn from(v: Vec<u8>) -> Self { ScriptBuf(v) }
 }
 
 impl From<ScriptBuf> for Vec<u8> {
-    #[inline]
-    fn from(v: ScriptBuf) -> Self { v.into_bytes() }
+    fn from(v: ScriptBuf) -> Self { v.0 }
 }
 
 impl AsRef<Script> for Script {
     #[inline]
-    fn as_ref(&self) -> &Self { self }
+    fn as_ref(&self) -> &Script { self }
 }
 
 impl AsRef<Script> for ScriptBuf {
-    #[inline]
     fn as_ref(&self) -> &Script { self }
 }
 
@@ -313,17 +279,14 @@ fn as_ref(&self) -> &[u8] { self.as_bytes() }
 }
 
 impl AsRef<[u8]> for ScriptBuf {
-    #[inline]
     fn as_ref(&self) -> &[u8] { self.as_bytes() }
 }
 
 impl AsMut<Script> for Script {
-    #[inline]
-    fn as_mut(&mut self) -> &mut Self { self }
+    fn as_mut(&mut self) -> &mut Script { self }
 }
 
 impl AsMut<Script> for ScriptBuf {
-    #[inline]
     fn as_mut(&mut self) -> &mut Script { self }
 }
 
@@ -333,7 +296,6 @@ fn as_mut(&mut self) -> &mut [u8] { self.as_mut_bytes() }
 }
 
 impl AsMut<[u8]> for ScriptBuf {
-    #[inline]
     fn as_mut(&mut self) -> &mut [u8] { self.as_mut_bytes() }
 }
 
@@ -346,7 +308,6 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 }
 
 impl fmt::Debug for ScriptBuf {
-    #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self.as_script(), f) }
 }
 
@@ -429,23 +390,21 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(self.as
 }
 
 impl fmt::LowerHex for Script {
-    #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         fmt::LowerHex::fmt(&self.as_bytes().as_hex(), f)
     }
 }
 #[cfg(feature = "alloc")]
-internals::impl_to_hex_from_lower_hex!(Script, |script: &Self| script.len() * 2);
+internals::impl_to_hex_from_lower_hex!(Script, |script: &Script| script.len() * 2);
 
 impl fmt::LowerHex for ScriptBuf {
     #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(self.as_script(), f) }
 }
 #[cfg(feature = "alloc")]
-internals::impl_to_hex_from_lower_hex!(ScriptBuf, |script_buf: &Self| script_buf.len() * 2);
+internals::impl_to_hex_from_lower_hex!(ScriptBuf, |script_buf: &ScriptBuf| script_buf.len() * 2);
 
 impl fmt::UpperHex for Script {
-    #[inline]
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         fmt::UpperHex::fmt(&self.as_bytes().as_hex(), f)
     }
@@ -456,35 +415,39 @@ impl fmt::UpperHex for ScriptBuf {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(self.as_script(), f) }
 }
 
+impl Deref for ScriptBuf {
+    type Target = Script;
+
+    fn deref(&self) -> &Self::Target { Script::from_bytes(&self.0) }
+}
+
+impl DerefMut for ScriptBuf {
+    fn deref_mut(&mut self) -> &mut Self::Target { Script::from_bytes_mut(&mut self.0) }
+}
+
 impl Borrow<Script> for ScriptBuf {
-    #[inline]
     fn borrow(&self) -> &Script { self }
 }
 
 impl BorrowMut<Script> for ScriptBuf {
-    #[inline]
     fn borrow_mut(&mut self) -> &mut Script { self }
 }
 
 impl PartialEq<ScriptBuf> for Script {
-    #[inline]
     fn eq(&self, other: &ScriptBuf) -> bool { self.eq(other.as_script()) }
 }
 
 impl PartialEq<Script> for ScriptBuf {
-    #[inline]
     fn eq(&self, other: &Script) -> bool { self.as_script().eq(other) }
 }
 
 impl PartialOrd<Script> for ScriptBuf {
-    #[inline]
     fn partial_cmp(&self, other: &Script) -> Option<Ordering> {
         self.as_script().partial_cmp(other)
     }
 }
 
 impl PartialOrd<ScriptBuf> for Script {
-    #[inline]
     fn partial_cmp(&self, other: &ScriptBuf) -> Option<Ordering> {
         self.partial_cmp(other.as_script())
     }
diff --git a/primitives/src/script/owned.rs b/primitives/src/script/owned.rs
index f3c78516..29950cbf 100644
--- a/primitives/src/script/owned.rs
+++ b/primitives/src/script/owned.rs
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: CC0-1.0
 
-use core::ops::{Deref, DerefMut};
+#[cfg(doc)]
+use core::ops::Deref;
 
 #[cfg(feature = "arbitrary")]
 use arbitrary::{Arbitrary, Unstructured};
@@ -18,31 +19,27 @@
 ///
 /// [deref coercions]: https://doc.rust-lang.org/std/ops/trait.Deref.html#more-on-deref-coercion
 #[derive(Default, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
-pub struct ScriptBuf(Vec<u8>);
+pub struct ScriptBuf(pub(in crate::script) Vec<u8>);
 
 impl ScriptBuf {
     /// Constructs a new empty script.
     #[inline]
-    pub const fn new() -> Self { Self::from_bytes(Vec::new()) }
+    pub const fn new() -> Self { ScriptBuf(Vec::new()) }
 
     /// Converts byte vector into script.
     ///
     /// This method doesn't (re)allocate.
-    #[inline]
-    pub const fn from_bytes(bytes: Vec<u8>) -> Self { Self(bytes) }
+    pub fn from_bytes(bytes: Vec<u8>) -> Self { ScriptBuf(bytes) }
 
     /// Returns a reference to unsized script.
-    #[inline]
     pub fn as_script(&self) -> &Script { Script::from_bytes(&self.0) }
 
     /// Returns a mutable reference to unsized script.
-    #[inline]
     pub fn as_mut_script(&mut self) -> &mut Script { Script::from_bytes_mut(&mut self.0) }
 
     /// Converts the script into a byte vector.
     ///
     /// This method doesn't (re)allocate.
-    #[inline]
     pub fn into_bytes(self) -> Vec<u8> { self.0 }
 
     /// Converts this `ScriptBuf` into a [boxed](Box) [`Script`].
@@ -55,13 +52,12 @@ pub fn into_bytes(self) -> Vec<u8> { self.0 }
     #[inline]
     pub fn into_boxed_script(self) -> Box<Script> {
         // Copied from PathBuf::into_boxed_path
-        let rw = Box::into_raw(self.into_bytes().into_boxed_slice()) as *mut Script;
+        let rw = Box::into_raw(self.0.into_boxed_slice()) as *mut Script;
         unsafe { Box::from_raw(rw) }
     }
 
     /// Constructs a new empty script with pre-allocated capacity.
-    #[inline]
-    pub fn with_capacity(capacity: usize) -> Self { ScriptBuf::from_bytes(Vec::with_capacity(capacity)) }
+    pub fn with_capacity(capacity: usize) -> Self { ScriptBuf(Vec::with_capacity(capacity)) }
 
     /// Pre-allocates at least `additional_len` bytes if needed.
     ///
@@ -73,7 +69,6 @@ pub fn with_capacity(capacity: usize) -> Self { ScriptBuf::from_bytes(Vec::with_
     /// # Panics
     ///
     /// Panics if the new capacity exceeds `isize::MAX bytes`.
-    #[inline]
     pub fn reserve(&mut self, additional_len: usize) { self.0.reserve(additional_len); }
 
     /// Pre-allocates exactly `additional_len` bytes if needed.
@@ -89,36 +84,14 @@ pub fn with_capacity(capacity: usize) -> Self { ScriptBuf::from_bytes(Vec::with_
     /// # Panics
     ///
     /// Panics if the new capacity exceeds `isize::MAX bytes`.
-    #[inline]
     pub fn reserve_exact(&mut self, additional_len: usize) { self.0.reserve_exact(additional_len); }
-
-    /// Returns the number of **bytes** available for writing without reallocation.
-    ///
-    /// It is guaranteed that `script.capacity() >= script.len()` always holds.
-    #[inline]
-    pub fn capacity(&self) -> usize {
-        self.0.capacity()
-    }
-}
-
-impl Deref for ScriptBuf {
-    type Target = Script;
-
-    #[inline]
-    fn deref(&self) -> &Self::Target { self.as_script() }
-}
-
-impl DerefMut for ScriptBuf {
-    #[inline]
-    fn deref_mut(&mut self) -> &mut Self::Target { self.as_mut_script() }
 }
 
 #[cfg(feature = "arbitrary")]
 impl<'a> Arbitrary<'a> for ScriptBuf {
-    #[inline]
     fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
         let v = Vec::<u8>::arbitrary(u)?;
-        Ok(ScriptBuf::from_bytes(v))
+        Ok(ScriptBuf(v))
     }
 }
 
@@ -130,7 +103,7 @@ mod tests {
     fn script_buf_from_bytes() {
         let bytes = vec![1, 2, 3];
         let script = ScriptBuf::from_bytes(bytes.clone());
-        assert_eq!(script.as_bytes(), bytes);
+        assert_eq!(script.0, bytes);
     }
 
     #[test]
@@ -147,7 +120,7 @@ fn script_buf_as_mut_script() {
         let mut script = ScriptBuf::from_bytes(bytes.clone());
         let script_mut_ref = script.as_mut_script();
         script_mut_ref.as_mut_bytes()[0] = 4;
-        assert_eq!(script.as_mut_bytes(), vec![4, 2, 3]);
+        assert_eq!(script.0, vec![4, 2, 3]);
     }
 
     #[test]
@@ -169,20 +142,20 @@ fn script_buf_into_boxed_script() {
     #[test]
     fn script_buf_capacity() {
         let script = ScriptBuf::with_capacity(10);
-        assert!(script.capacity() >= 10);
+        assert!(script.0.capacity() >= 10);
     }
 
     #[test]
     fn script_buf_reserve() {
         let mut script = ScriptBuf::new();
         script.reserve(10);
-        assert!(script.capacity() >= 10);
+        assert!(script.0.capacity() >= 10);
     }
 
     #[test]
     fn script_buf_reserve_exact() {
         let mut script = ScriptBuf::new();
         script.reserve_exact(10);
-        assert!(script.capacity() >= 10);
+        assert!(script.0.capacity() >= 10);
     }
 }
diff --git a/units/src/lib.rs b/units/src/lib.rs
index 4d3be98a..a5f5434a 100644
--- a/units/src/lib.rs
+++ b/units/src/lib.rs
@@ -34,6 +34,7 @@ pub mod _core {
 pub mod fee_rate;
 pub mod locktime;
 pub mod parse;
+pub mod timestamp;
 pub mod weight;
 
 #[doc(inline)]
@@ -42,5 +43,6 @@ pub mod _core {
     amount::{Amount, SignedAmount},
     block::{BlockHeight, BlockInterval},
     fee_rate::FeeRate,
+    timestamp::Timestamp,
     weight::Weight
 };
diff --git a/units/src/timestamp.rs b/units/src/timestamp.rs
new file mode 100644
index 00000000..35e59d8b
--- /dev/null
+++ b/units/src/timestamp.rs
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! A UNIX timestamp used as the Bitcoin block time.
+//!
+//! Also known as Epoch Time - January 1, 1970.
+//!
+//! This differs from other UNIX timestamps in that we only use non-negative values. The Epoch
+//! pre-dates Bitcoin so timestamps before this are not useful for block timestamps.
+
+#[cfg(feature = "arbitrary")]
+use arbitrary::{Arbitrary, Unstructured};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+/// A Bitcoin block timestamp.
+///
+/// > Each block contains a Unix time timestamp. In addition to serving as a source of variation for
+/// > the block hash, they also make it more difficult for an adversary to manipulate the block chain.
+/// >
+/// > A timestamp is accepted as valid if it is greater than the median timestamp of previous 11
+/// > blocks, and less than the network-adjusted time + 2 hours. "Network-adjusted time" is the
+/// > median of the timestamps returned by all nodes connected to you. As a result block timestamps
+/// > are not exactly accurate, and they do not need to be. Block times are accurate only to within
+/// > an hour or two.
+///
+/// ref: <https://en.bitcoin.it/wiki/Block_timestamp>
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub struct Timestamp(u32);
+
+impl Timestamp {
+    /// Constructs a new [`Timestamp`] from an unsigned 32 bit integer value.
+    #[inline]
+    pub const fn from_u32(t: u32) -> Self { Timestamp(t) }
+
+    /// Returns the inner `u32` value.
+    #[inline]
+    pub const fn to_u32(self) -> u32 { self.0 }
+}
+
+impl From<u32> for Timestamp {
+    #[inline]
+    fn from(t: u32) -> Self { Self::from_u32(t) }
+}
+
+impl From<Timestamp> for u32 {
+    #[inline]
+    fn from(t: Timestamp) -> Self { t.to_u32() }
+}
+
+#[cfg(feature = "arbitrary")]
+impl<'a> Arbitrary<'a> for Timestamp {
+    #[inline]
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        let t: u32 = u.arbitrary()?;
+        Ok(Timestamp::from(t))
+    }
+}


Test patch:
diff --git a/units/tests/api.rs b/units/tests/api.rs
index 00ad9b08..97a20e2a 100644
--- a/units/tests/api.rs
+++ b/units/tests/api.rs
@@ -15,7 +15,7 @@
 use bitcoin_units::locktime::{absolute, relative}; // Typical usage is `absolute::Height`.
 use bitcoin_units::{
     amount, block, fee_rate, locktime, parse, weight, Amount, BlockHeight, BlockInterval, FeeRate,
-    SignedAmount, Weight,
+    SignedAmount, Timestamp, Weight,
 };
 
 /// A struct that includes all public non-error enums.
@@ -42,6 +42,7 @@ struct Structs {
     i: relative::Height,
     j: relative::Time,
     k: Weight,
+    l: Timestamp,
 }
 
 impl Structs {
@@ -58,6 +59,7 @@ fn max() -> Self {
             i: relative::Height::MAX,
             j: relative::Time::MAX,
             k: Weight::MAX,
+            l: Timestamp::from_u32(u32::MAX),
         }
     }
 }
@@ -87,6 +89,7 @@ struct CommonTraits {
     i: relative::Height,
     j: relative::Time,
     k: Weight,
+    l: Timestamp,
 }
 
 /// A struct that includes all types that implement `Default`.
@@ -138,12 +141,14 @@ struct Errors {
 
 #[test]
 fn api_can_use_modules_from_crate_root() {
-    use bitcoin_units::{amount, block, fee_rate, locktime, parse, weight};
+    use bitcoin_units::{amount, block, fee_rate, locktime, parse, timestamp, weight};
 }
 
 #[test]
 fn api_can_use_types_from_crate_root() {
-    use bitcoin_units::{Amount, BlockHeight, BlockInterval, FeeRate, SignedAmount, Weight};
+    use bitcoin_units::{
+        Amount, BlockHeight, BlockInterval, FeeRate, SignedAmount, Timestamp, Weight,
+    };
 }
 
 #[test]
@@ -183,6 +188,11 @@ fn api_can_use_all_types_from_module_parse() {
     use bitcoin_units::parse::{ParseIntError, PrefixedHexError, UnprefixedHexError};
 }
 
+#[test]
+fn api_can_use_all_types_from_module_timestamp() {
+    use bitcoin_units::timestamp::Timestamp;
+}
+
 #[test]
 fn api_can_use_all_types_from_module_weight() {
     use bitcoin_units::weight::Weight;
@@ -218,6 +228,8 @@ fn api_all_non_error_types_have_non_empty_debug() {
     assert!(!debug.is_empty());
     let debug = format!("{:?}", t.b.k);
     assert!(!debug.is_empty());
+    let debug = format!("{:?}", t.b.l);
+    assert!(!debug.is_empty());
 }
 
 #[test]
@@ -283,6 +295,7 @@ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
             i: relative::Height::arbitrary(u)?,
             j: relative::Time::arbitrary(u)?,
             k: Weight::arbitrary(u)?,
+            l: Timestamp::arbitrary(u)?,
         };
         Ok(a)
     }