Repository: rust-bitcoin/rust-bitcoin
Pull request #4200: Remove a bunch of `try_into().expect()`
Author: login: Kixunil
Labels: name: Correctness
description: Improves the chance that the code will be correct - either our or consumer (via well-designed API)
name: C-bitcoin
description: PRs modifying the bitcoin crate
name: C-internals
description: PRs modifying the internals crate
name: C-primitives
name: C-base58
description: PRs modifying the base58 crate
Created: 2025-03-05T19:34:45Z | Merged: 2025-03-21T12:45:39Z

Previously we've used `try_into().expect()` because const generics wer
unavailable. Then they become available but we didn't realize we could
already convert a bunch of code to not use panicking conversions. But we
can (and could for a while).

This adds an extension trait for arrays to provide basic non-panicking
operations retutning arrays, so they can be composed with other
functions accepting arrays without any conversions. It also refactors a
bunch of code to use the non-panicking constructs but it's certainly not
all of it. That could be done later. This just aims at removing the
ugliest offenders and demonstrate the usefulness of this approach.


Diff:
diff --git a/base58/src/lib.rs b/base58/src/lib.rs
index 312e9c5644..13db9fd1f1 100644
--- a/base58/src/lib.rs
+++ b/base58/src/lib.rs
@@ -20,6 +20,7 @@
 // Exclude lints we don't think are valuable.
 #![allow(clippy::needless_question_mark)] // https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
 #![allow(clippy::manual_range_contains)] // More readable than clippy's format.
+#![allow(clippy::incompatible_msrv)] // Has FPs and we're testing it which is more reliable anyway.
 
 extern crate alloc;
 
@@ -40,7 +41,10 @@ use core::fmt;
 pub use std::{string::String, vec::Vec};
 
 use hashes::sha256d;
+use internals::array::ArrayExt;
 use internals::array_vec::ArrayVec;
+#[allow(unused)] // MSRV polyfill
+use internals::slice::SliceExt;
 
 use crate::error::{IncorrectChecksumError, TooShortError};
 
@@ -109,15 +113,9 @@ pub fn decode(data: &str) -> Result<Vec<u8>, InvalidCharacterError> {
 /// Decodes a base58check-encoded string into a byte vector verifying the checksum.
 pub fn decode_check(data: &str) -> Result<Vec<u8>, Error> {
     let mut ret: Vec<u8> = decode(data)?;
-    if ret.len() < 4 {
-        return Err(TooShortError { length: ret.len() }.into());
-    }
-    let check_start = ret.len() - 4;
+    let (remaining, &data_check) = ret.split_last_chunk::<4>().ok_or(TooShortError { length: ret.len() })?;
 
-    let hash_check = sha256d::Hash::hash(&ret[..check_start]).as_byte_array()[..4]
-        .try_into()
-        .expect("4 byte slice");
-    let data_check = ret[check_start..].try_into().expect("4 byte slice");
+    let hash_check = *sha256d::Hash::hash(remaining).as_byte_array().sub_array::<0, 4>();
 
     let expected = u32::from_le_bytes(hash_check);
     let actual = u32::from_le_bytes(data_check);
@@ -126,7 +124,7 @@ pub fn decode_check(data: &str) -> Result<Vec<u8>, Error> {
         return Err(IncorrectChecksumError { incorrect: actual, expected }.into());
     }
 
-    ret.truncate(check_start);
+    ret.truncate(remaining.len());
     Ok(ret)
 }
 
diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs
index 36b6a553c4..93bc290d87 100644
--- a/bitcoin/src/address/mod.rs
+++ b/bitcoin/src/address/mod.rs
@@ -49,6 +49,7 @@ use core::str::FromStr;
 use bech32::primitives::gf32::Fe32;
 use bech32::primitives::hrp::Hrp;
 use hashes::{hash160, HashEngine};
+use internals::array::ArrayExt;
 use secp256k1::{Secp256k1, Verification, XOnlyPublicKey};
 
 use crate::address::script_pubkey::ScriptBufExt as _;
@@ -902,12 +903,9 @@ impl Address<NetworkUnchecked> {
             return Err(LegacyAddressTooLongError { length: s.len() }.into());
         }
         let data = base58::decode_check(s)?;
-        if data.len() != 21 {
-            return Err(InvalidBase58PayloadLengthError { length: s.len() }.into());
-        }
+        let data: &[u8; 21] = (&*data).try_into().map_err(|_| InvalidBase58PayloadLengthError { length: s.len() })?;
 
-        let (prefix, data) = data.split_first().expect("length checked above");
-        let data: [u8; 20] = data.try_into().expect("length checked above");
+        let (prefix, &data) = data.split_first();
 
         let inner = match *prefix {
             PUBKEY_ADDRESS_PREFIX_MAIN => {
diff --git a/bitcoin/src/address/script_pubkey.rs b/bitcoin/src/address/script_pubkey.rs
index 7c048680e9..b89008ca33 100644
--- a/bitcoin/src/address/script_pubkey.rs
+++ b/bitcoin/src/address/script_pubkey.rs
@@ -2,6 +2,7 @@
 
 //! Bitcoin scriptPubkey script extensions.
 
+use internals::array::ArrayExt;
 use secp256k1::{Secp256k1, Verification};
 
 use crate::internal_macros::define_extension_trait;
@@ -99,14 +100,16 @@ define_extension_trait! {
     pub(crate) trait ScriptExtPrivate impl for Script {
         /// Returns the bytes of the (possibly invalid) public key if this script is P2PK.
         fn p2pk_pubkey_bytes(&self) -> Option<&[u8]> {
-            match self.len() {
-                67 if self.as_bytes()[0] == OP_PUSHBYTES_65.to_u8()
-                    && self.as_bytes()[66] == OP_CHECKSIG.to_u8() =>
-                    Some(&self.as_bytes()[1..66]),
-                35 if self.as_bytes()[0] == OP_PUSHBYTES_33.to_u8()
-                    && self.as_bytes()[34] == OP_CHECKSIG.to_u8() =>
-                    Some(&self.as_bytes()[1..34]),
-                _ => None,
+            if let Ok(bytes) = <&[u8; 67]>::try_from(self.as_bytes()) {
+                let (&first, bytes) = bytes.split_first::<66>();
+                let (&last, pubkey) = bytes.split_last::<65>();
+                (first == OP_PUSHBYTES_65.to_u8() && last == OP_CHECKSIG.to_u8()).then_some(pubkey)
+            } else if let Ok(bytes) = <&[u8; 35]>::try_from(self.as_bytes()) {
+                let (&first, bytes) = bytes.split_first::<34>();
+                let (&last, pubkey) = bytes.split_last::<33>();
+                (first == OP_PUSHBYTES_33.to_u8() && last == OP_CHECKSIG.to_u8()).then_some(pubkey)
+            } else {
+                None
             }
         }
     }
diff --git a/bitcoin/src/bip152.rs b/bitcoin/src/bip152.rs
index 979dabc5e5..bb25066d18 100644
--- a/bitcoin/src/bip152.rs
+++ b/bitcoin/src/bip152.rs
@@ -10,7 +10,7 @@ use core::{convert, fmt, mem};
 use std::error;
 
 use hashes::{sha256, siphash24};
-use internals::ToU64 as _;
+use internals::{ToU64 as _, array::ArrayExt as _};
 use io::{BufRead, Write};
 
 use crate::consensus::encode::{self, Decodable, Encodable, ReadExt, WriteExt};
@@ -117,8 +117,8 @@ impl ShortId {
         // 2. Running SipHash-2-4 with the input being the transaction ID and the keys (k0/k1)
         // set to the first two little-endian 64-bit integers from the above hash, respectively.
         (
-            u64::from_le_bytes(h.as_byte_array()[0..8].try_into().expect("8 byte slice")),
-            u64::from_le_bytes(h.as_byte_array()[8..16].try_into().expect("8 byte slice")),
+            u64::from_le_bytes(*h.as_byte_array().sub_array::<0, 8>()),
+            u64::from_le_bytes(*h.as_byte_array().sub_array::<8, 8>()),
         )
     }
 
diff --git a/bitcoin/src/bip158.rs b/bitcoin/src/bip158.rs
index a0d57c1a87..abdde2a9c2 100644
--- a/bitcoin/src/bip158.rs
+++ b/bitcoin/src/bip158.rs
@@ -42,7 +42,7 @@ use core::convert::Infallible;
 use core::fmt;
 
 use hashes::{sha256d, siphash24, HashEngine as _};
-use internals::{write_err, ToU64 as _};
+use internals::{write_err, ToU64 as _, array::ArrayExt as _};
 use io::{BufRead, Write};
 
 use crate::block::{Block, BlockHash, Checked};
@@ -195,8 +195,8 @@ impl<'a, W: Write> BlockFilterWriter<'a, W> {
     /// Constructs a new [`BlockFilterWriter`] from `block`.
     pub fn new(writer: &'a mut W, block: &'a Block<Checked>) -> BlockFilterWriter<'a, W> {
         let block_hash_as_int = block.block_hash().to_byte_array();
-        let k0 = u64::from_le_bytes(block_hash_as_int[0..8].try_into().expect("8 byte slice"));
-        let k1 = u64::from_le_bytes(block_hash_as_int[8..16].try_into().expect("8 byte slice"));
+        let k0 = u64::from_le_bytes(*block_hash_as_int.sub_array::<0, 8>());
+        let k1 = u64::from_le_bytes(*block_hash_as_int.sub_array::<8, 8>());
         let writer = GcsFilterWriter::new(writer, k0, k1, M, P);
         BlockFilterWriter { block, writer }
     }
@@ -250,8 +250,8 @@ impl BlockFilterReader {
     /// Constructs a new [`BlockFilterReader`] from `block_hash`.
     pub fn new(block_hash: BlockHash) -> BlockFilterReader {
         let block_hash_as_int = block_hash.to_byte_array();
-        let k0 = u64::from_le_bytes(block_hash_as_int[0..8].try_into().expect("8 byte slice"));
-        let k1 = u64::from_le_bytes(block_hash_as_int[8..16].try_into().expect("8 byte slice"));
+        let k0 = u64::from_le_bytes(*block_hash_as_int.sub_array::<0, 8>());
+        let k1 = u64::from_le_bytes(*block_hash_as_int.sub_array::<8, 8>());
         BlockFilterReader { reader: GcsFilterReader::new(k0, k1, M, P) }
     }
 
diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs
index 382d855543..73d74ffc82 100644
--- a/bitcoin/src/bip32.rs
+++ b/bitcoin/src/bip32.rs
@@ -10,7 +10,8 @@ use core::ops::Index;
 use core::str::FromStr;
 use core::{fmt, slice};
 
-use hashes::{hash160, hash_newtype, sha512, HashEngine, Hmac, HmacEngine};
+use hashes::{hash160, hash_newtype, sha512, Hash, HashEngine, Hmac, HmacEngine};
+use internals::array::ArrayExt;
 use internals::write_err;
 use secp256k1::{Secp256k1, XOnlyPublicKey};
 
@@ -44,7 +45,7 @@ impl_array_newtype_stringify!(ChainCode, 32);
 
 impl ChainCode {
     fn from_hmac(hmac: Hmac<sha512::Hash>) -> Self {
-        hmac.as_ref()[32..].try_into().expect("half of hmac is guaranteed to be 32 bytes")
+        ChainCode(*hmac.as_byte_array().split_array::<32, 32>().1)
     }
 }
 
@@ -608,7 +609,7 @@ impl Xpriv {
             parent_fingerprint: Default::default(),
             child_number: ChildNumber::ZERO_NORMAL,
             private_key: secp256k1::SecretKey::from_byte_array(
-                &hmac.as_ref()[..32].try_into().expect("Slice should be exactly 32 bytes"),
+                hmac.as_byte_array().split_array::<32, 32>().0,
             )?,
             chain_code: ChainCode::from_hmac(hmac),
         })
@@ -682,7 +683,7 @@ impl Xpriv {
         engine.input(&u32::from(i).to_be_bytes());
         let hmac: Hmac<sha512::Hash> = engine.finalize();
         let sk = secp256k1::SecretKey::from_byte_array(
-            &hmac.as_ref()[..32].try_into().expect("statistically impossible to hit"),
+            hmac.as_byte_array().split_array::<32, 32>().0,
         )
         .expect("statistically impossible to hit");
         let tweaked =
@@ -700,49 +701,33 @@ impl Xpriv {
 
     /// Decoding extended private key from binary data according to BIP 32
     pub fn decode(data: &[u8]) -> Result<Xpriv, Error> {
-        if data.len() != 78 {
-            return Err(Error::WrongExtendedKeyLength(data.len()));
-        }
+        let Common {
+            network,
+            depth,
+            parent_fingerprint,
+            child_number,
+            chain_code,
+            key,
+        } = Common::decode(data)?;
 
-        let network = if data.starts_with(&VERSION_BYTES_MAINNET_PRIVATE) {
-            NetworkKind::Main
-        } else if data.starts_with(&VERSION_BYTES_TESTNETS_PRIVATE) {
-            NetworkKind::Test
-        } else {
-            let (b0, b1, b2, b3) = (data[0], data[1], data[2], data[3]);
-            return Err(Error::UnknownVersion([b0, b1, b2, b3]));
+        let network = match network {
+            VERSION_BYTES_MAINNET_PRIVATE => NetworkKind::Main,
+            VERSION_BYTES_TESTNETS_PRIVATE => NetworkKind::Test,
+            unknown => return Err(Error::UnknownVersion(unknown)),
         };
 
-        // Check that byte 45 is 0 as required by BIP-32 for private keys
-        if data[45] != 0 {
+        let (&zero, private_key) = key.split_first();
+        if zero != 0 {
             return Err(Error::InvalidPrivateKeyPrefix);
         }
 
-        // For master keys (depth=0), both parent fingerprint and child number must be zero
-        let depth = data[4];
-        if depth == 0 {
-            if !data[5..9].iter().all(|&b| b == 0) {
-                return Err(Error::NonZeroParentFingerprintForMasterKey);
-            }
-
-            if !data[9..13].iter().all(|&b| b == 0) {
-                return Err(Error::NonZeroChildNumberForMasterKey);
-            }
-        }
-
         Ok(Xpriv {
             network,
-            depth: data[4],
-            parent_fingerprint: data[5..9]
-                .try_into()
-                .expect("9 - 5 == 4, which is the Fingerprint length"),
-            child_number: u32::from_be_bytes(data[9..13].try_into().expect("4 byte slice")).into(),
-            chain_code: data[13..45]
-                .try_into()
-                .expect("45 - 13 == 32, which is the ChainCode length"),
-            private_key: secp256k1::SecretKey::from_byte_array(
-                &data[46..78].try_into().expect("Slice should be exactly 32 bytes"),
-            )?,
+            depth,
+            parent_fingerprint,
+            child_number,
+            chain_code,
+            private_key: secp256k1::SecretKey::from_byte_array(private_key)?,
         })
     }
 
@@ -769,7 +754,7 @@ impl Xpriv {
 
     /// Returns the first four bytes of the identifier
     pub fn fingerprint<C: secp256k1::Signing>(&self, secp: &Secp256k1<C>) -> Fingerprint {
-        self.identifier(secp).as_byte_array()[0..4].try_into().expect("4 is the fingerprint length")
+        self.identifier(secp).as_byte_array().sub_array::<0, 4>().into()
     }
 }
 
@@ -849,7 +834,7 @@ impl Xpub {
 
                 let hmac = engine.finalize();
                 let private_key = secp256k1::SecretKey::from_byte_array(
-                    &hmac.as_ref()[..32].try_into().expect("Slice should be exactly 32 bytes"),
+                    hmac.as_byte_array().split_array::<32, 32>().0
                 )?;
                 let chain_code = ChainCode::from_hmac(hmac);
                 Ok((private_key, chain_code))
@@ -878,30 +863,28 @@ impl Xpub {
 
     /// Decoding extended public key from binary data according to BIP 32
     pub fn decode(data: &[u8]) -> Result<Xpub, Error> {
-        if data.len() != 78 {
-            return Err(Error::WrongExtendedKeyLength(data.len()));
-        }
+        let Common {
+            network,
+            depth,
+            parent_fingerprint,
+            child_number,
+            chain_code,
+            key,
+        } = Common::decode(data)?;
 
-        let network = if data.starts_with(&VERSION_BYTES_MAINNET_PUBLIC) {
-            NetworkKind::Main
-        } else if data.starts_with(&VERSION_BYTES_TESTNETS_PUBLIC) {
-            NetworkKind::Test
-        } else {
-            let (b0, b1, b2, b3) = (data[0], data[1], data[2], data[3]);
-            return Err(Error::UnknownVersion([b0, b1, b2, b3]));
+        let network = match network {
+            VERSION_BYTES_MAINNET_PUBLIC => NetworkKind::Main,
+            VERSION_BYTES_TESTNETS_PUBLIC => NetworkKind::Test,
+            unknown => return Err(Error::UnknownVersion(unknown)),
         };
 
         Ok(Xpub {
             network,
-            depth: data[4],
-            parent_fingerprint: data[5..9]
-                .try_into()
-                .expect("9 - 5 == 4, which is the Fingerprint length"),
-            child_number: u32::from_be_bytes(data[9..13].try_into().expect("4 byte slice")).into(),
-            chain_code: data[13..45]
-                .try_into()
-                .expect("45 - 13 == 32, which is the ChainCode length"),
-            public_key: secp256k1::PublicKey::from_slice(&data[45..78])?,
+            depth,
+            parent_fingerprint,
+            child_number,
+            chain_code,
+            public_key: secp256k1::PublicKey::from_slice(&key)?,
         })
     }
 
@@ -927,7 +910,7 @@ impl Xpub {
 
     /// Returns the first four bytes of the identifier
     pub fn fingerprint(&self) -> Fingerprint {
-        self.identifier().as_byte_array()[0..4].try_into().expect("4 is the fingerprint length")
+        self.identifier().as_byte_array().sub_array::<0, 4>().into()
     }
 }
 
@@ -1004,6 +987,48 @@ impl fmt::Display for InvalidBase58PayloadLengthError {
 #[cfg(feature = "std")]
 impl std::error::Error for InvalidBase58PayloadLengthError {}
 
+// Helps unify decoding
+struct Common {
+    network: [u8; 4],
+    depth: u8,
+    parent_fingerprint: Fingerprint,
+    child_number: ChildNumber,
+    chain_code: ChainCode,
+    // public key (compressed) or 0 byte followed by a private key 
+    key: [u8; 33],
+}
+
+impl Common {
+    fn decode(data: &[u8]) -> Result<Self, Error> {
+        let data: &[u8; 78] = data.try_into().map_err(|_| Error::WrongExtendedKeyLength(data.len()))?;
+
+        let (&network, data) = data.split_array::<4, 74>();
+        let (&depth, data) = data.split_first::<73>();
+        let (&parent_fingerprint, data) = data.split_array::<4, 69>();
+        let (&child_number, data) = data.split_array::<4, 65>();
+        let (&chain_code, &key) = data.split_array::<32, 33>();
+
+        if depth == 0 {
+            if parent_fingerprint != [0u8; 4] {
+                return Err(Error::NonZeroParentFingerprintForMasterKey);
+            }
+
+            if child_number != [0u8; 4] {
+                return Err(Error::NonZeroChildNumberForMasterKey);
+            }
+        }
+
+        Ok(Common {
+            network,
+            depth,
+            parent_fingerprint: parent_fingerprint.into(),
+            child_number: u32::from_be_bytes(child_number).into(),
+            chain_code: chain_code.into(),
+            key,
+        })
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use hex::test_hex_unwrap as hex;
@@ -1385,4 +1410,33 @@ mod tests {
         let xpriv_str = "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fENZ3QzxW";
         xpriv_str.parse::<Xpriv>().unwrap();
     }
+
+    #[test]
+    fn official_vectors_5() {
+        let invalid_keys = [
+            "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm",
+            "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH",
+            "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn",
+            "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ",
+            "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4",
+            "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J",
+            "xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNTuLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv",
+            "xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgiT91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ",
+            "xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF42acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN",
+            "xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJaNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8",
+            "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4",
+            "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9",
+            "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx",
+            "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G",
+            "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY",
+            "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL",
+        ];
+        for key in invalid_keys {
+            if key.starts_with("xpub") {
+                key.parse::<Xpub>().unwrap_err();
+            } else {
+                key.parse::<Xpriv>().unwrap_err();
+            }
+        }
+    }
 }
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index 862ed1fc79..1c4dc32ae8 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -12,6 +12,7 @@ use core::str::FromStr;
 
 use hashes::hash160;
 use hex::{FromHex, HexToArrayError};
+use internals::array::ArrayExt;
 use internals::array_vec::ArrayVec;
 use internals::{impl_to_hex_from_lower_hex, write_err};
 use io::{Read, Write};
@@ -505,20 +506,20 @@ impl PrivateKey {
     pub fn from_wif(wif: &str) -> Result<PrivateKey, FromWifError> {
         let data = base58::decode_check(wif)?;
 
-        let compressed = match data.len() {
-            33 => false,
-            34 => {
-                if data[33] != 1 {
-                    return Err(InvalidWifCompressionFlagError { invalid: data[33] }.into());
-                }
-                true
-            }
-            length => {
-                return Err(InvalidBase58PayloadLengthError { length }.into());
+        let (compressed, data) = if let Ok(data) = <&[u8; 33]>::try_from(&*data) {
+            (false, data)
+        } else if let Ok(data) = <&[u8; 34]>::try_from(&*data) {
+            let (compressed_flag, data) = data.split_last::<33>();
+            if *compressed_flag != 1 {
+                return Err(InvalidWifCompressionFlagError { invalid: *compressed_flag }.into());
             }
+            (true, data)
+        } else {
+            return Err(InvalidBase58PayloadLengthError { length: data.len() }.into());
         };
 
-        let network = match data[0] {
+        let (network, key) = data.split_first();
+        let network = match *network {
             128 => NetworkKind::Main,
             239 => NetworkKind::Test,
             invalid => {
@@ -529,9 +530,7 @@ impl PrivateKey {
         Ok(PrivateKey {
             compressed,
             network,
-            inner: secp256k1::SecretKey::from_byte_array(
-                &data[1..33].try_into().expect("Slice should be exactly 32 bytes"),
-            )?,
+            inner: secp256k1::SecretKey::from_byte_array(key)?,
         })
     }
 }
diff --git a/bitcoin/src/crypto/taproot.rs b/bitcoin/src/crypto/taproot.rs
index 7a19f579ff..fe66028b87 100644
--- a/bitcoin/src/crypto/taproot.rs
+++ b/bitcoin/src/crypto/taproot.rs
@@ -10,6 +10,7 @@ use core::fmt;
 #[cfg(feature = "arbitrary")]
 use arbitrary::{Arbitrary, Unstructured};
 use internals::write_err;
+use internals::array::ArrayExt;
 use io::Write;
 
 use crate::prelude::Vec;
@@ -29,23 +30,17 @@ pub struct Signature {
 impl Signature {
     /// Deserializes the signature from a slice.
     pub fn from_slice(sl: &[u8]) -> Result<Self, SigFromSliceError> {
-        match sl.len() {
-            64 => {
-                // default type
-                let signature = secp256k1::schnorr::Signature::from_byte_array(
-                    sl[0..64].try_into().expect("Slice should be exactly 64 bytes"),
-                );
-                Ok(Signature { signature, sighash_type: TapSighashType::Default })
-            }
-            65 => {
-                let (sighash_type, signature) = sl.split_last().expect("slice len checked == 65");
-                let sighash_type = TapSighashType::from_consensus_u8(*sighash_type)?;
-                let signature = secp256k1::schnorr::Signature::from_byte_array(
-                    signature[0..64].try_into().expect("Slice should be exactly 64 bytes"),
-                );
-                Ok(Signature { signature, sighash_type })
-            }
-            len => Err(SigFromSliceError::InvalidSignatureSize(len)),
+        if let Ok(signature) = <[u8; 64]>::try_from(sl) {
+            // default type
+            let signature = secp256k1::schnorr::Signature::from_byte_array(signature);
+            Ok(Signature { signature, sighash_type: TapSighashType::Default })
+        } else if let Ok(signature) = <[u8; 65]>::try_from(sl) {
+            let (sighash_type, signature) = signature.split_last();
+            let sighash_type = TapSighashType::from_consensus_u8(*sighash_type)?;
+            let signature = secp256k1::schnorr::Signature::from_byte_array(*signature);
+            Ok(Signature { signature, sighash_type })
+        } else {
+            Err(SigFromSliceError::InvalidSignatureSize(sl.len()))
         }
     }
 
@@ -144,9 +139,7 @@ impl<'a> Arbitrary<'a> for Signature {
         let arbitrary_bytes: [u8; secp256k1::constants::SCHNORR_SIGNATURE_SIZE] = u.arbitrary()?;
 
         Ok(Signature {
-            signature: secp256k1::schnorr::Signature::from_byte_array(
-                arbitrary_bytes[0..64].try_into().expect("Slice should be exactly 64 bytes"),
-            ),
+            signature: secp256k1::schnorr::Signature::from_byte_array(arbitrary_bytes),
             sighash_type: TapSighashType::arbitrary(u)?,
         })
     }
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 545604397b..ed14a8bad0 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -38,6 +38,7 @@
 // Exclude lints we don't think are valuable.
 #![allow(clippy::needless_question_mark)] // https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
 #![allow(clippy::manual_range_contains)] // More readable than clippy's format.
+#![allow(clippy::incompatible_msrv)] // Has FPs and we're testing it which is more reliable anyway.
 
 // We only support machines with index size of 4 bytes or more.
 //
diff --git a/bitcoin/src/psbt/serialize.rs b/bitcoin/src/psbt/serialize.rs
index 34086afab0..4c2b0f4815 100644
--- a/bitcoin/src/psbt/serialize.rs
+++ b/bitcoin/src/psbt/serialize.rs
@@ -7,6 +7,8 @@
 
 use hashes::{hash160, ripemd160, sha256, sha256d};
 use internals::compact_size;
+#[allow(unused)] // MSRV polyfill
+use internals::slice::SliceExt;
 use secp256k1::XOnlyPublicKey;
 
 use super::map::{Input, Map, Output, PsbtSighashType};
@@ -214,14 +216,12 @@ impl Serialize for KeySource {
 
 impl Deserialize for KeySource {
     fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
-        if bytes.len() < 4 {
-            return Err(io::Error::from(io::ErrorKind::UnexpectedEof).into());
-        }
+        let (fingerprint, mut d) = bytes.split_first_chunk::<4>()
+            .ok_or(io::Error::from(io::ErrorKind::UnexpectedEof))?;
 
-        let fprint: Fingerprint = bytes[0..4].try_into().expect("4 is the fingerprint length");
+        let fprint: Fingerprint = fingerprint.into();
         let mut dpath: Vec<ChildNumber> = Default::default();
 
-        let mut d = &bytes[4..];
         while !d.is_empty() {
             match u32::consensus_decode(&mut d) {
                 Ok(index) => dpath.push(index.into()),
diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs
index a3358964ae..b14264f249 100644
--- a/bitcoin/src/taproot/mod.rs
+++ b/bitcoin/src/taproot/mod.rs
@@ -13,7 +13,11 @@ use core::fmt;
 use core::iter::FusedIterator;
 
 use hashes::{hash_newtype, sha256t, sha256t_tag, HashEngine};
+use internals::array::ArrayExt;
 use internals::{impl_to_hex_from_lower_hex, write_err};
+#[allow(unused)] // MSRV polyfill
+use internals::slice::SliceExt;
+
 use io::Write;
 use secp256k1::{Scalar, Secp256k1};
 
@@ -1162,22 +1166,20 @@ impl ControlBlock {
     /// - [`TaprootError::InvalidInternalKey`] if internal key is invalid (first 32 bytes after the parity byte).
     /// - [`TaprootError::InvalidMerkleTreeDepth`] if Merkle tree is too deep (more than 128 levels).
     pub fn decode(sl: &[u8]) -> Result<ControlBlock, TaprootError> {
-        if sl.len() < TAPROOT_CONTROL_BASE_SIZE
-            || (sl.len() - TAPROOT_CONTROL_BASE_SIZE) % TAPROOT_CONTROL_NODE_SIZE != 0
-        {
-            return Err(InvalidControlBlockSizeError(sl.len()).into());
-        }
-        let output_key_parity = match sl[0] & 1 {
+        let (base, merkle_branch) = sl.split_first_chunk::<TAPROOT_CONTROL_BASE_SIZE>()
+            .ok_or(InvalidControlBlockSizeError(sl.len()))?;
+
+        let (&first, internal_key) = base.split_first();
+
+        let output_key_parity = match first & 1 {
             0 => secp256k1::Parity::Even,
             _ => secp256k1::Parity::Odd,
         };
 
-        let leaf_version = LeafVersion::from_consensus(sl[0] & TAPROOT_LEAF_MASK)?;
-        let internal_key = UntweakedPublicKey::from_byte_array(
-            &sl[1..TAPROOT_CONTROL_BASE_SIZE].try_into().expect("Slice should be exactly 32 bytes"),
-        )
-        .map_err(TaprootError::InvalidInternalKey)?;
-        let merkle_branch = TaprootMerkleBranchBuf::decode(&sl[TAPROOT_CONTROL_BASE_SIZE..])?;
+        let leaf_version = LeafVersion::from_consensus(first & TAPROOT_LEAF_MASK)?;
+        let internal_key = UntweakedPublicKey::from_byte_array(internal_key)
+            .map_err(TaprootError::InvalidInternalKey)?;
+        let merkle_branch = TaprootMerkleBranchBuf::decode(merkle_branch)?;
         Ok(ControlBlock { leaf_version, output_key_parity, internal_key, merkle_branch })
     }
 }
diff --git a/internals/src/array.rs b/internals/src/array.rs
new file mode 100644
index 0000000000..becbf6b64c
--- /dev/null
+++ b/internals/src/array.rs
@@ -0,0 +1,105 @@
+//! Contains extensions related to arrays.
+
+/// Extension trait for arrays.
+pub trait ArrayExt {
+    /// The item type the array is storing.
+    type Item;
+
+    /// Just like the slicing operation, this returns an array `LEN` items long at position
+    /// `OFFSET`.
+    ///
+    /// The correctness of this operation is compile-time checked.
+    ///
+    /// Note that unlike slicing where the second number is the end index, here the second number
+    /// is array length!
+    fn sub_array<const OFFSET: usize, const LEN: usize>(&self) -> &[Self::Item; LEN];
+
+    /// Returns an item at given statically-known index.
+    ///
+    /// This is just like normal indexing except the check happens at compile time.
+    fn get_static<const INDEX: usize>(&self) -> &Self::Item {
+        &self.sub_array::<INDEX, 1>()[0]
+    }
+
+    /// Returns the first item in an array.
+    ///
+    /// Fails to compile if the array is empty.
+    ///
+    /// Note that this method's name is intentionally shadowing the `std`'s `first` method which
+    /// returns `Option`. The rationale is that given the known length of the array, we always know
+    /// that this will not return `None` so trying to keep the `std` method around is pointless.
+    /// Importing the trait will also cause compile failures - that's also intentional to expose
+    /// the places where useless checks are made.
+    fn first(&self) -> &Self::Item {
+        self.get_static::<0>()
+    }
+
+    /// Splits the array into two, non-overlaping smaller arrays covering the entire range.
+    ///
+    /// This is almost equivalent to just calling [`sub_array`](Self::sub_array) twice, except it also
+    /// checks that the arrays don't overlap and that they cover the full range. This is very useful
+    /// for demonstrating correctness, especially when chained. Using this technique even revealed
+    /// a bug in the past. ([#4195](https://github.com/rust-bitcoin/rust-bitcoin/issues/4195))
+    fn split_array<const LEFT: usize, const RIGHT: usize>(&self) -> (&[Self::Item; LEFT], &[Self::Item; RIGHT]);
+
+    /// Splits the array into the first element and the remaining, one element shorter, array.
+    ///
+    /// Fails to compile if the array is empty.
+    ///
+    /// Note that this method's name is intentionally shadowing the `std`'s `split_first` method which
+    /// returns `Option`. The rationale is that given the known length of the array, we always know
+    /// that this will not return `None` so trying to keep the `std` method around is pointless.
+    /// Importing the trait will also cause compile failures - that's also intentional to expose
+    /// the places where useless checks are made.
+    fn split_first<const RIGHT: usize>(&self) -> (&Self::Item, &[Self::Item; RIGHT]) {
+        let (first, remaining) = self.split_array::<1, RIGHT>();
+        (&first[0], remaining)
+    }
+
+    /// Splits the array into the last element and the remaining, one element shorter, array.
+    ///
+    /// Fails to compile if the array is empty.
+    ///
+    /// Note that this method's name is intentionally shadowing the `std`'s `split_last` method which
+    /// returns `Option`. The rationale is that given the known length of the array, we always know
+    /// that this will not return `None` so trying to keep the `std` method around is pointless.
+    /// Importing the trait will also cause compile failures - that's also intentional to expose
+    /// the places where useless checks are made.
+    ///
+    /// The returned tuple is also reversed just as `std` for consistency and simpler diffs when
+    /// migrating.
+    fn split_last<const LEFT: usize>(&self) -> (&Self::Item, &[Self::Item; LEFT]) {
+        let (remaining, last) = self.split_array::<LEFT, 1>();
+        (&last[0], remaining)
+    }
+}
+
+impl<const N: usize, T> ArrayExt for [T; N] {
+    type Item = T;
+
+    fn sub_array<const OFFSET: usize, const LEN: usize>(&self) -> &[Self::Item; LEN] {
+        #[allow(clippy::let_unit_value)]
+        let _ = Hack::<N, OFFSET, LEN>::IS_VALID_RANGE;
+
+        self[OFFSET..(OFFSET + LEN)].try_into().expect("this is also compiler-checked above")
+    }
+
+    fn split_array<const LEFT: usize, const RIGHT: usize>(&self) -> (&[Self::Item; LEFT], &[Self::Item; RIGHT]) {
+        #[allow(clippy::let_unit_value)]
+        let _ = Hack2::<N, LEFT, RIGHT>::IS_FULL_RANGE;
+
+        (self.sub_array::<0, LEFT>(), self.sub_array::<LEFT, RIGHT>())
+    }
+}
+
+struct Hack<const N: usize, const OFFSET: usize, const LEN: usize>;
+
+impl<const N: usize, const OFFSET: usize, const LEN: usize> Hack<N, OFFSET, LEN> {
+    const IS_VALID_RANGE: () = assert!(OFFSET + LEN <= N);
+}
+
+struct Hack2<const N: usize, const LEFT: usize, const RIGHT: usize>;
+
+impl<const N: usize, const LEFT: usize, const RIGHT: usize> Hack2<N, LEFT, RIGHT> {
+    const IS_FULL_RANGE: () = assert!(LEFT + RIGHT == N);
+}
diff --git a/internals/src/lib.rs b/internals/src/lib.rs
index bed67bd2f1..c2e2ce7080 100644
--- a/internals/src/lib.rs
+++ b/internals/src/lib.rs
@@ -35,6 +35,7 @@ pub mod rust_version {
     include!(concat!(env!("OUT_DIR"), "/rust_version.rs"));
 }
 
+pub mod array;
 pub mod array_vec;
 pub mod compact_size;
 pub mod const_tools;
diff --git a/internals/src/slice.rs b/internals/src/slice.rs
index 4c32126d06..b703238e45 100644
--- a/internals/src/slice.rs
+++ b/internals/src/slice.rs
@@ -26,6 +26,25 @@ pub trait SliceExt {
     fn bitcoin_as_chunks_mut<const N: usize>(
         &mut self,
     ) -> (&mut [[Self::Item; N]], &mut [Self::Item]);
+
+    /// Tries to access a sub-array of length `ARRAY_LEN` at the specified `offset`.
+    ///
+    /// Returns `None` in case of out-of-bounds access.
+    fn get_array<const ARRAY_LEN: usize>(&self, offset: usize) -> Option<&[Self::Item; ARRAY_LEN]>;
+
+    /// Splits the slice into an array and remainder if it's long enough.
+    ///
+    /// Returns `None` if the slice is shorter than `ARRAY_LEN`
+    #[allow(clippy::type_complexity)] // it's not really complex and redefining would make it
+                                      // harder to understand
+    fn split_first_chunk<const ARRAY_LEN: usize>(&self) -> Option<(&[Self::Item; ARRAY_LEN], &[Self::Item])>;
+
+    /// Splits the slice into a remainder and an array if it's long enough.
+    ///
+    /// Returns `None` if the slice is shorter than `ARRAY_LEN`
+    #[allow(clippy::type_complexity)] // it's not really complex and redefining would make it
+                                      // harder to understand
+    fn split_last_chunk<const ARRAY_LEN: usize>(&self) -> Option<(&[Self::Item], &[Self::Item; ARRAY_LEN])>;
 }
 
 impl<T> SliceExt for [T] {
@@ -69,6 +88,27 @@ impl<T> SliceExt for [T] {
         };
         (left, right)
     }
+
+    fn get_array<const ARRAY_LEN: usize>(&self, offset: usize) -> Option<&[Self::Item; ARRAY_LEN]> {
+        self.get(offset..(offset + ARRAY_LEN))
+            .map(|slice| slice.try_into().expect("the arguments to `get` evaluate to the same length the return type uses"))
+    }
+
+    fn split_first_chunk<const ARRAY_LEN: usize>(&self) -> Option<(&[Self::Item; ARRAY_LEN], &[Self::Item])> {
+        if self.len() < ARRAY_LEN {
+            return None;
+        }
+        let (first, remainder) = self.split_at(ARRAY_LEN);
+        Some((first.try_into().expect("we're passing `ARRAY_LEN` to `split_at` above"), remainder))
+    }
+
+    fn split_last_chunk<const ARRAY_LEN: usize>(&self) -> Option<(&[Self::Item], &[Self::Item; ARRAY_LEN])> {
+        if self.len() < ARRAY_LEN {
+            return None;
+        }
+        let (remainder, last) = self.split_at(self.len() - ARRAY_LEN);
+        Some((remainder, last.try_into().expect("we're passing `self.len() - ARRAY_LEN` to `split_at` above")))
+    }
 }
 
 struct Hack<const N: usize>;
diff --git a/primitives/src/witness.rs b/primitives/src/witness.rs
index bf2a8f9f8c..101c37caab 100644
--- a/primitives/src/witness.rs
+++ b/primitives/src/witness.rs
@@ -12,6 +12,7 @@ use arbitrary::{Arbitrary, Unstructured};
 use hex::DisplayHex;
 use internals::compact_size;
 use internals::wrap_debug::WrapDebug;
+use internals::slice::SliceExt;
 
 use crate::prelude::Vec;
 
@@ -234,12 +235,7 @@ fn encode_cursor(bytes: &mut [u8], start_of_indices: usize, index: usize, value:
 #[inline]
 fn decode_cursor(bytes: &[u8], start_of_indices: usize, index: usize) -> Option<usize> {
     let start = start_of_indices + index * 4;
-    let end = start + 4;
-    if end > bytes.len() {
-        None
-    } else {
-        Some(u32::from_ne_bytes(bytes[start..end].try_into().expect("is u32 size")) as usize)
-    }
+    bytes.get_array::<4>(start).map(|index_bytes| u32::from_ne_bytes(*index_bytes) as usize)
 }
 
 /// Debug implementation that displays the witness as a structured output containing:


Reviews:
user: login: tcharding
body: ACK 6bd2749b2ac1b50755af000a5176c52d409744df
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-17T00:50:47Z
user: login: apoelstra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T13:55:40Z
user: login: apoelstra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T13:56:39Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T14:09:52Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T14:10:54Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:32:40Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T18:14:19Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T18:22:45Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T18:30:30Z
user: login: yancyribbens
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T18:58:47Z
user: login: Kixunil
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T19:15:00Z
user: login: apoelstra
body: ACK 474977f3146c793d32e59f470083a572de02c697; successfully ran local tests
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-18T01:30:39Z
user: login: tcharding
body: ACK 474977f3146c793d32e59f470083a572de02c697
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-18T02:45:22Z
user: login: tcharding
body: ACK c1de14215351a277141589f0dff7ee8e42a0067d
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-19T03:25:18Z
user: login: apoelstra
body: ACK c1de14215351a277141589f0dff7ee8e42a0067d; successfully ran local tests
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-19T06:11:48Z
user: login: tcharding
body: ACK 437562e71cbb1e18179df3f5d01d2cc86961a150
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T21:13:18Z
user: login: apoelstra
body: ACK 437562e71cbb1e18179df3f5d01d2cc86961a150; successfully ran local tests
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T00:33:46Z

Review comments:
diff_hunk: @@ -44,7 +45,7 @@ impl_array_newtype_stringify!(ChainCode, 32);
 
 impl ChainCode {
     fn from_hmac(hmac: Hmac<sha512::Hash>) -> Self {
-        hmac.as_ref()[32..].try_into().expect("half of hmac is guaranteed to be 32 bytes")
+        ChainCode(*hmac.as_byte_array().sub_array::<32, 32>())
path: bitcoin/src/bip32.rs
user: login: apoelstra
body: In b04b2b8c58cc7770ed7ecf8507e4e380563976d8:

I would prefer to use `.split_array::<32, 32>().1` to emphasize that the hmac has exactly 64 bytes and you're taking exactly the second half of it. (Similar everywhere you are using `sub_array::<0, 32>` or `sub_array::<32, 32>`.)
created_at: 2025-03-17T13:55:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,104 @@
+//! Contains extensions related to arrays.
+
+/// Extension trait for arrays.
+pub trait ArrayExt {
+    /// The item type the array is storing.
+    type Item;
+
+    /// Just like the slicing operation, this returns an array `LEN` items long at position
+    /// `OFFSET`.
+    ///
+    /// The correctness of this operation is compile-time checked.
+    ///
+    /// Note that unlike slicing where the second number is the end index, here the second number
+    /// is array length!
+    fn sub_array<const OFFSET: usize, const LEN: usize>(&self) -> &[Self::Item; LEN];
+
+    /// Returns an item at given statically-known index.
+    ///
+    /// This is just like normal indexing except the check happens at compile time.
+    fn get_static<const INDEX: usize>(&self) -> &Self::Item {
+        &self.sub_array::<INDEX, 1>()[0]
+    }
+
+    /// Returns the first item in an array.
+    ///
+    /// Fails to compile if the array is empty.
+    ///
+    /// Note that this method's name is intentionally shadowing the `std`'s `first` method which
+    /// returns `Option`. The rationale is that given the known length of the array, we always know
+    /// that this will not return `None` so trying to keep the `std` method around is pointless.
+    /// Importing the trait will also cause compile failures - that's also intentional to expose
+    /// the places where useless checks are made.
+    fn first(&self) -> &Self::Item {
+        self.get_static::<0>()
+    }
+
+    /// Splits the array into two, non-overlaping smaller arrays covering the entire range.
+    ///
+    /// This is almost equivalent to just calling [`sub_array`](Self::sub_array) twice, except it also
+    /// checks that the arrays don't overlap and that they cover the full range. This is very useful
+    /// for demonstrating correctness, especially when chained. It also already found a bug.
path: internals/src/array.rs
user: login: apoelstra
body: In b04b2b8c58cc7770ed7ecf8507e4e380563976d8:

Heh, nice, though if you need to edit this commit you can remove "already found a bug" since it won't be meaningful to future readers of the code. You can move it to the commit message if you like.
created_at: 2025-03-17T13:56:39Z
author_association: MEMBER
diff_hunk: @@ -44,7 +45,7 @@ impl_array_newtype_stringify!(ChainCode, 32);
 
 impl ChainCode {
     fn from_hmac(hmac: Hmac<sha512::Hash>) -> Self {
-        hmac.as_ref()[32..].try_into().expect("half of hmac is guaranteed to be 32 bytes")
+        ChainCode(*hmac.as_byte_array().sub_array::<32, 32>())
path: bitcoin/src/bip32.rs
user: login: Kixunil
body: Oh, yes, good idea.
created_at: 2025-03-17T14:09:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,104 @@
+//! Contains extensions related to arrays.
+
+/// Extension trait for arrays.
+pub trait ArrayExt {
+    /// The item type the array is storing.
+    type Item;
+
+    /// Just like the slicing operation, this returns an array `LEN` items long at position
+    /// `OFFSET`.
+    ///
+    /// The correctness of this operation is compile-time checked.
+    ///
+    /// Note that unlike slicing where the second number is the end index, here the second number
+    /// is array length!
+    fn sub_array<const OFFSET: usize, const LEN: usize>(&self) -> &[Self::Item; LEN];
+
+    /// Returns an item at given statically-known index.
+    ///
+    /// This is just like normal indexing except the check happens at compile time.
+    fn get_static<const INDEX: usize>(&self) -> &Self::Item {
+        &self.sub_array::<INDEX, 1>()[0]
+    }
+
+    /// Returns the first item in an array.
+    ///
+    /// Fails to compile if the array is empty.
+    ///
+    /// Note that this method's name is intentionally shadowing the `std`'s `first` method which
+    /// returns `Option`. The rationale is that given the known length of the array, we always know
+    /// that this will not return `None` so trying to keep the `std` method around is pointless.
+    /// Importing the trait will also cause compile failures - that's also intentional to expose
+    /// the places where useless checks are made.
+    fn first(&self) -> &Self::Item {
+        self.get_static::<0>()
+    }
+
+    /// Splits the array into two, non-overlaping smaller arrays covering the entire range.
+    ///
+    /// This is almost equivalent to just calling [`sub_array`](Self::sub_array) twice, except it also
+    /// checks that the arrays don't overlap and that they cover the full range. This is very useful
+    /// for demonstrating correctness, especially when chained. It also already found a bug.
path: internals/src/array.rs
user: login: Kixunil
body: I think I'll reword it a bit.
created_at: 2025-03-17T14:10:54Z
author_association: COLLABORATOR
diff_hunk: @@ -24,6 +24,25 @@ pub trait SliceExt {
     /// let fail = slice.as_chunks_mut::<0>();
     /// ```
     fn bitcoin_as_chunks_mut<const N: usize>(&mut self) -> (&mut [[Self::Item; N]], &mut [Self::Item]);
+
+    /// Tries to access a sub-array of length `ARRAY_LEN` at the specified `offset`.
+    ///
+    /// Returns `None` in case of out-of-bounds access.
path: internals/src/slice.rs
user: login: yancyribbens
body: Why not return an Error type with useful information instead of just `None`?  Although that can be done later not needed with this PR.
created_at: 2025-03-17T15:32:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,6 +24,25 @@ pub trait SliceExt {
     /// let fail = slice.as_chunks_mut::<0>();
     /// ```
     fn bitcoin_as_chunks_mut<const N: usize>(&mut self) -> (&mut [[Self::Item; N]], &mut [Self::Item]);
+
+    /// Tries to access a sub-array of length `ARRAY_LEN` at the specified `offset`.
+    ///
+    /// Returns `None` in case of out-of-bounds access.
path: internals/src/slice.rs
user: login: Kixunil
body: Because these are low-level primitives which don't have much information about the error, so `ok_or` is likely better but also this one is currently only used to implement APIs consistent with `std` which return `Option` so any such information would be a waste.
created_at: 2025-03-17T18:14:19Z
author_association: COLLABORATOR
diff_hunk: @@ -24,6 +24,25 @@ pub trait SliceExt {
     /// let fail = slice.as_chunks_mut::<0>();
     /// ```
     fn bitcoin_as_chunks_mut<const N: usize>(&mut self) -> (&mut [[Self::Item; N]], &mut [Self::Item]);
+
+    /// Tries to access a sub-array of length `ARRAY_LEN` at the specified `offset`.
+    ///
+    /// Returns `None` in case of out-of-bounds access.
path: internals/src/slice.rs
user: login: yancyribbens
body: I see.  So besides mirroring std, since there is only one specific error then it's not informative to return why it error-ed it sounds like.  Thanks, just looking to understand better the rational for when to return None and when to return an Error.
created_at: 2025-03-17T18:22:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,6 +24,25 @@ pub trait SliceExt {
     /// let fail = slice.as_chunks_mut::<0>();
     /// ```
     fn bitcoin_as_chunks_mut<const N: usize>(&mut self) -> (&mut [[Self::Item; N]], &mut [Self::Item]);
+
+    /// Tries to access a sub-array of length `ARRAY_LEN` at the specified `offset`.
+    ///
+    /// Returns `None` in case of out-of-bounds access.
path: internals/src/slice.rs
user: login: Kixunil
body: One is in iterator - trying to go past the end means end of iteration. The other is `get` method which is similar enough to `get` on slices to warrant matching API. `std` has `Option` there not `Result`. While returning indices in error types is somewhat convenient in erroneous cases using `get` is usually an operation that's handled by the caller.
created_at: 2025-03-17T18:30:30Z
author_association: COLLABORATOR
diff_hunk: @@ -58,6 +77,27 @@ impl<T> SliceExt for [T] {
         let left = unsafe { core::slice::from_raw_parts_mut(left.as_mut_ptr().cast::<[Self::Item; N]>(), chunks_count) };
         (left, right)
     }
+
+    fn get_array<const ARRAY_LEN: usize>(&self, offset: usize) -> Option<&[Self::Item; ARRAY_LEN]> {
path: internals/src/slice.rs
user: login: yancyribbens
body: > One is in iterator - trying to go past the end means end of iteration. The other is get method which is similar enough to get on slices to warrant matching API. std has Option there not Result. While returning indices in error types is somewhat convenient in erroneous cases using get is usually an operation that's handled by the caller.

I see, you're talking about when methods impl the trait.  I guess doc comments here aren't need since it's implied by the trait.

created_at: 2025-03-17T18:58:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +77,27 @@ impl<T> SliceExt for [T] {
         let left = unsafe { core::slice::from_raw_parts_mut(left.as_mut_ptr().cast::<[Self::Item; N]>(), chunks_count) };
         (left, right)
     }
+
+    fn get_array<const ARRAY_LEN: usize>(&self, offset: usize) -> Option<&[Self::Item; ARRAY_LEN]> {
path: internals/src/slice.rs
user: login: Kixunil
body: I was talking about where these methods are used.
created_at: 2025-03-17T19:15:00Z
author_association: COLLABORATOR

PR comments:
user: login: coveralls
created_at: 2025-03-05T19:37:05Z
body: ## Pull Request Test Coverage Report for [Build 13977619809](https://coveralls.io/builds/72842155)


### Details

* **161** of **189**   **(85.19%)**  changed or added relevant lines in **13** files are covered.
* **4** unchanged lines in **2** files lost coverage.
* Overall coverage increased (+**0.06%**) to **83.173%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [bitcoin/src/crypto/key.rs](https://coveralls.io/builds/72842155/source?filename=bitcoin%2Fsrc%2Fcrypto%2Fkey.rs#L518) | 10 | 11 | 90.91%
| [bitcoin/src/bip32.rs](https://coveralls.io/builds/72842155/source?filename=bitcoin%2Fsrc%2Fbip32.rs#L877) | 90 | 92 | 97.83%
| [bitcoin/src/crypto/taproot.rs](https://coveralls.io/builds/72842155/source?filename=bitcoin%2Fsrc%2Fcrypto%2Ftaproot.rs#L38) | 5 | 10 | 50.0%
| [internals/src/array.rs](https://coveralls.io/builds/72842155/source?filename=internals%2Fsrc%2Farray.rs#L20) | 20 | 26 | 76.92%
| [internals/src/slice.rs](https://coveralls.io/builds/72842155/source?filename=internals%2Fsrc%2Fslice.rs#L97) | 4 | 18 | 22.22%
<!-- | **Total:** | **161** | **189** | **85.19%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [bitcoin/src/taproot/mod.rs](https://coveralls.io/builds/72842155/source?filename=bitcoin%2Fsrc%2Ftaproot%2Fmod.rs#L1526) | 1 | 77.27% |
| [bitcoin/src/internal_macros.rs](https://coveralls.io/builds/72842155/source?filename=bitcoin%2Fsrc%2Finternal_macros.rs#L351) | 3 | 61.24% |
<!-- | **Total:** | **4** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/72842155/badge)](https://coveralls.io/builds/72842155) |
| :-- | --: |
| Change from base [Build 13977478725](https://coveralls.io/builds/72842012): |  0.06% |
| Covered Lines: | 22030 |
| Relevant Lines: | 26487 |

---
##### 💛  - [Coveralls](https://coveralls.io)

author_association: NONE
user: login: apoelstra
created_at: 2025-03-05T20:21:14Z
body: oooo. Concept ACK.
author_association: MEMBER
user: login: Kixunil
created_at: 2025-03-05T20:32:38Z
body: Forgot to say the warning is intentional for now - I think I'll fix that bug first before merging this. (Or anyone else can tonight, I don't think I'll do it today.)
author_association: COLLABORATOR
user: login: tcharding
created_at: 2025-03-05T23:01:22Z
body: Nice. Here are two more tests I wrote while understanding the extension trait.

```rust
    #[test]
    fn five_to_two() {
        let slice = [1, 2, 3, 4, 5];
        let (left, right) = slice.as_chunks::<2>();
        assert_eq!(left, &[[1, 2], [3, 4]]);
        assert_eq!(right, &[5]);

        let (first, rest) = slice.split_first_chunk::<2>().unwrap();
        assert_eq!(first, &[1, 2]);
        assert_eq!(rest, &[3, 4, 5]);

        let (rest, last) = slice.split_last_chunk::<2>().unwrap();
        assert_eq!(rest, &[1, 2, 3]);
        assert_eq!(last, &[4, 5]);
    }

    #[test]
    fn get_array() {
        let slice = [1, 2, 3, 4, 5];

        let chunk = slice.get_array::<2>(0).unwrap();
        assert_eq!(chunk, &[1, 2]);

        // offset is an index of `T` not an index of chunks.
        let chunk = slice.get_array::<2>(1).unwrap();
        assert_eq!(chunk, &[2, 3]);

        let chunk = slice.get_array::<2>(3).unwrap();
        assert_eq!(chunk, &[4, 5]);

        assert!(slice.get_array::<2>(4).is_none());
    }
```

At first I thought `offset` was a chunk index not an index of `T`s.
author_association: MEMBER
user: login: jirijakes
created_at: 2025-03-06T00:47:56Z
body: Nice!

Pity rust-analyzer does not report error:

![image](https://github.com/user-attachments/assets/78b15efe-739b-4673-9780-7b1f0ae88be6)

author_association: CONTRIBUTOR
user: login: Kixunil
created_at: 2025-03-06T20:11:14Z
body: @jirijakes Yeah, I've seen someone mentioned that postmonomorphization errors only show up with `cargo build`, not `cargo check` and presumable RA uses check (very reasonable) and postmono error is the only feasible option right now (that is still compile time).
author_association: COLLABORATOR
user: login: jirijakes
created_at: 2025-03-06T23:24:22Z
body: @Kixunil, you're right. There is a related issue: https://github.com/rust-lang/rust/issues/99682.

So, `cargo check` will not report these Hacky errors, only `cargo build` will. Rust-analyzer can be configured to use `build` if needed but I don't think it makes much sense.
author_association: CONTRIBUTOR
user: login: Kixunil
created_at: 2025-03-17T06:21:48Z
body: @jirijakes FTR I've seen a proposal to make `check` also evaluate these consts which would be much cleaner solution but something that requires work on the compiler side.
author_association: COLLABORATOR
user: login: Kixunil
created_at: 2025-03-17T14:12:13Z
body: @apoelstra do you want me to redo my PR or do it in a followup to save Tobin's ACK?
author_association: COLLABORATOR
user: login: apoelstra
created_at: 2025-03-17T14:25:55Z
body: @Kixunil can you force-push the PR? I think it'll be OK -- I should be able to ACK it and Tobin should be online before I sign off so I can start merging it.
author_association: MEMBER
user: login: Kixunil
created_at: 2025-03-17T15:14:58Z
body: I need to run now (some unexpected stuff came up) but I think I'll be able to do it by the end of the day.
author_association: COLLABORATOR
user: login: apoelstra
created_at: 2025-03-18T12:30:11Z
body: Sorry, this needs another rebase. Process failure on my part -- I closed this tab because I was waiting for Tobin's ACK, and then I merged #4242 because it didn't conflict with any open tabs.
author_association: MEMBER
user: login: Kixunil
created_at: 2025-03-18T14:51:33Z
body: Crap, these ridiculous conflicts are driving me crazy. I was thinking for a while of having a version control system that would store the entire source as a tree (AST + files + directories), not text and make formatting completely machine-generated. It'd make this kind of reformatting impossible (but still settable client-side; obviously, rustfmt would need some more tweaks) and many changes would become trivially mergable - e.g. `use` items added to the same line.

Out of curiosity, does the idea sound appealing to you? Of course I'm not going to implement this any time soon, at least not in usable form, I'm just wondering if I'm the only one bothered by it. :)
author_association: COLLABORATOR
user: login: apoelstra
created_at: 2025-03-18T15:20:47Z
body: > Out of curiosity, does the idea sound appealing to you?

Yes, though I *suspect* you can get 95% of the way there with custom git merge strategies. And the last 5% probably is not worth the switching cost of learning a new VCS.

In Bitcoin Core there is a notion of a "scripted diff" where if your diff is expressible in shell (e.g. `cargo +nightly fmt` and somehow we decide on compiler pins) then you can put that shell script into your commit message and CI will verify that the commit is exactly the result of the script. This *dramatically* simplifies review for stuff that is basically a `sed -i` across the codebase, but it could also be used as a git merge strategy, where if git sees a conflict between a scripted diff and a normal one, it attempts to apply the normal one and then re-run the script.

Relatedly, it would also be nice to have a git diff driver that collapsed out scripted stuff so you don't have to wade through a bajillion renames to find the one real change..
author_association: MEMBER
user: login: Kixunil
created_at: 2025-03-18T15:56:16Z
body: > I _suspect_ you can get 95% of the way there with custom git merge strategies.

Some amount yes but I did a bit of digging into it and I haven't found any documentation on how to implement them (just merge drivers) and the one merge **driver** I made for merging `use` is pretty cool but for some reason `git` refuses to use it with cherry-picks. Also pijul as some cool ideas that I think would work even better with trees.

If we could just use non-nightly rustfmt it might be also worth applying it as a filter. Scripted diffs are nice but I'd love to see some tool that can do stuff like "rename this Rust variable".

Anyway, this all is just entirely theoretical.
author_association: COLLABORATOR
user: login: apoelstra
created_at: 2025-03-20T19:12:20Z
body: Needs rebase after #4248.
author_association: MEMBER
user: login: Kixunil
created_at: 2025-03-20T19:21:16Z
body: Damn, this is exactly the kind of merge conflict that should be resolvable in a smarter version control... Anyway, rebased.
author_association: COLLABORATOR
user: login: apoelstra
created_at: 2025-03-20T19:24:14Z
body: Yeah. Super frustrating that we gotta manually re-apply renames like this.

Queued for re-ACK and I'll also queue for merge since the range-diff from what Tobin ACKed is nearly trivial. (Though he may show up and sneak a new ACK in before my scripts are done running :)).
author_association: MEMBER