Repository: rust-bitcoin/rust-bitcoin
Pull request #2585 (2024-03-13 02:53:03)

Improve array macros
Currently we have two macros used when creating array wrapper types, one is in `internals` and the other in `bitcoin::internal_macros`. It is not immediately obvious what is what and why there are two.
    
Improve the macros by:
    
- Move the inherent functions to `impl_array_newtype`
- Use `*_byte_array` for the names instead of `*_bytes`
- Re-name the other macro to match what it now does


coveralls: ## Pull Request Test Coverage Report for [Build 8258775846](https://coveralls.io/builds/66252292)


### Details

* **0** of **1**   **(0.0%)**  changed or added relevant line in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.004%**) to **84.039%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [bitcoin/src/internal_macros.rs](https://coveralls.io/builds/66252292/source?filename=bitcoin%2Fsrc%2Finternal_macros.rs#L73) | 0 | 1 | 0.0%
<!-- | **Total:** | **0** | **1** | **0.0%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/66252292/badge)](https://coveralls.io/builds/66252292) |
| :-- | --: |
| Change from base [Build 8251085872](https://coveralls.io/builds/66237344): |  -0.004% |
| Covered Lines: | 19540 |
| Relevant Lines: | 23251 |

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

apoelstra: In our hash macros we use `to_byte_array` and `from_byte_array` rather than `to_bytes`/`from_bytes` (which typically are about byte slices, not arrays).

I wonder if we should be consistent here, and break the existing `*_bytes` methods by making them slice-oriented, and then add new `*_byte_array` methods.
tcharding: I went looking to find some naming consensus on this, I found 
- https://doc.rust-lang.org/std/primitive.u32.html#method.from_be_bytes
- https://doc.rust-lang.org/std/string/struct.String.html#method.into_bytes
- https://doc.rust-lang.org/std/primitive.slice.html#method.as_bytes

I didn't manage to convince myself of any naming consensus. I have been wondering about the `*_byte_array` names in `hashes` lately, and I think they are correct because they are special in that they relate to the associated `Bytes` type so the extra info in the name is good. For the byte wrapper type I'm leaning towards thinking the more terse `to_bytes`, `as_bytes`, `from_bytes` is ok. What do you rekon? 
apoelstra: I don't have strong feelings either way, but (a) the hash types are just byte wrappers, even though we use an associated type with them (which I think we should replace with an associated length constant, now that we can do so in our MSRV). And (b) to me, `to_bytes` and `as_bytes` should be about slices. And (c) your examples support me -- you show `as_bytes` is for `&[u8]` and `to_bytes` is for `Vec<u8>` (the "owned variant of `&[u8]`). Are there any counterexamples? If not then I think this is consensus.

`from_be_bytes` is not either of the names in question, it is a special method on the fixed-size integer types.


tcharding: > which I think we should replace with an associated length constant

I've tried this and also using cons generics and neither work. I posted the a link to the SO post that taught me but the reason is that associated consts (and const generics) after some stage of compilation are defined _outside_ of the type that implements them so one cannot have functions that use them in return values e.g, `fn foo(&self) -> [u8; Self::LEN]` does not work. Trust me, it took me like 3 goes at trying it before I found that SO post - the build warning is a bit cryptic. There is a nightly feature to enable it.
tcharding: All the rest are fair points except I would have expected `from_be_byte_array`.

> I wonder if we should be consistent here, and break the existing *_bytes methods by making them slice-oriented, and then add new *_byte_array methods.

I'm not too fussed, if you still stand by this statement I'll hack it up.
apoelstra: > > which I think we should replace with an associated length constant
> 
> I've tried this and also using const generics and neither work. I posted the a link to the SO post somewhere that taught me but the reason is that associated consts (and const generics) after some stage of compilation are defined _outside_ of the type that implements them so one cannot have functions that use them in return values e.g, `fn foo(&self) -> [u8; Self::LEN]` does not work. Trust me, it took me like 3 goes at trying it before I found that SO post - the build warning is a bit cryptic. There is a nightly feature to enable it.

:( well, thanks for being the one to chase this down!

>I'm not too fussed, if you still stand by this statement I'll hack it up.

Yep, I stand by it.
tcharding: Re-done as suggested, please holla at me if the commit log is not clear enough.
tcharding: Sure, can do. 
tcharding: I've added `as_bytes` and `to_bytes`. But in doing so I dug a little deeper to try to convince myself. I'm totally happy with `as_bytes` but I'm not sure if `to_bytes` is better than `to_vec` - its not really that important because this macro is only used for a few "minor" types

- `ShortId`
- `ChainCode`
- `Fingerprint`
- `ChainHash`


Some more digging, I searched Rust src and we definitely have `as_bytes` returning a slice

```bash
ack 'fn as_bytes'

core/src/str/mod.rs
328:    pub const fn as_bytes(&self) -> &[u8] {
372:    pub unsafe fn as_bytes_mut(&mut self) -> &mut [u8] {

core/src/ascii/ascii_char.rs
571:    pub const fn as_bytes(&self) -> &[u8] {

core/src/mem/maybe_uninit.rs
1312:    pub fn as_bytes(&self) -> &[MaybeUninit<u8>] {
1343:    pub fn as_bytes_mut(&mut self) -> &mut [MaybeUninit<u8>] {

std/src/os/unix/ffi/os_str.rs
57:    fn as_bytes(&self) -> &[u8];
67:    fn as_bytes(&self) -> &[u8] {

std/src/sys_common/wtf8.rs
636:    pub fn as_bytes(&self) -> &[u8] {

backtrace/src/symbolize/mod.rs
354:    pub fn as_bytes(&self) -> &'a [u8] {

alloc/src/string.rs
1381:    pub fn as_bytes(&self) -> &[u8] {
2009:    pub fn as_bytes(&self) -> &[u8] {

alloc/src/ffi/c_str.rs
188:    pub fn as_bytes(&self) -> &[u8] {
543:    pub fn as_bytes(&self) -> &[u8] {
563:    pub fn as_bytes_with_nul(&self) -> &[u8] {

```

But `to_vec` vs `to_bytes` is not as clear

```bash
ack 'fn to_bytes'

core/src/ffi/c_str.rs
595:    pub const fn to_bytes(&self) -> &[u8] {
624:    pub const fn to_bytes_with_nul(&self) -> &[u8] {
```

```bash
ack 'fn to_vec'

proc_macro/src/bridge/buffer.rs
136:        fn to_vec(b: Buffer) -> Vec<u8> {

alloc/src/slice.rs
110:    pub fn to_vec<T: ConvertVec, A: Allocator>(s: &[T], alloc: A) -> Vec<T, A> {
116:        fn to_vec<A: Allocator>(s: &[Self], alloc: A) -> Vec<Self, A>
124:        default fn to_vec<A: Allocator>(s: &[Self], alloc: A) -> Vec<Self, A> {
161:        fn to_vec<A: Allocator>(s: &[Self], alloc: A) -> Vec<Self, A> {
412:    pub fn to_vec(&self) -> Vec<T>
436:    pub fn to_vec_in<A: Allocator>(&self, alloc: A) -> Vec<T, A>
```

tcharding: hmm, and the `to_` function has to be feature gated on "alloc" - makes me lean even more towards `to_vec`. What do you think?
tcharding: And searching this repo, we are using both `to_vec` and `to_bytes`.

```bash
❯ gg 'fn to_bytes'
bitcoin/src/blockdata/script/borrowed.rs:143:    pub fn to_bytes(&self) -> Vec<u8> { self.0.to_owned() }
bitcoin/src/crypto/key.rs:111:    pub fn to_bytes(self) -> Vec<u8> {
bitcoin/src/crypto/key.rs:320:    pub fn to_bytes(&self) -> [u8; 33] { self.0.serialize() }
bitcoin/src/crypto/key.rs:439:    pub fn to_bytes(self) -> Vec<u8> { self.inner[..].to_vec() }
bitcoin/src/internal_macros.rs:62:             pub fn to_bytes(self) -> [u8; $len] {
bitcoin/src/p2p/mod.rs:229:    pub fn to_bytes(self) -> [u8; 4] { self.0 }
~/b/g/t/r/master ❯❯❯ gg 'fn to_vec'
bitcoin/src/blockdata/witness.rs:287:    pub fn to_vec(&self) -> Vec<Vec<u8>> { self.iter().map(|s| s.to_vec()).collect() }
bitcoin/src/crypto/ecdsa.rs:60:     pub fn to_vec(self) -> Vec<u8> {
bitcoin/src/crypto/taproot.rs:50:     pub fn to_vec(self) -> Vec<u8> {
```
tcharding: `to_bytes` everywhere works for me.

Patch:
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
deleted file mode 100644
index b8896a95..00000000
--- a/.github/workflows/README.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# rust-bitcoin workflow notes
-
-We are attempting to run max 20 parallel jobs using GitHub actions (usage limit for free tier).
-
-ref: https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration
-
-The minimal/recent lock files are handled by CI (`rust.yml`).
-
-## Jobs
-
-Run from `rust.yml` unless stated otherwise. Total 21 jobs but
-`Prepare` is quick and must be run first anyway.
-
-0.  `Prepare`
-1.  `Stable - minimal`
-2.  `Stable - recent`
-3.  `Nightly - minimal`
-4.  `Nightly - recent`
-5.  `MSRV - minimal`
-6.  `MSRV - recent`
-7.  `Lint`
-8.  `Docs`
-9.  `Docsrs`
-10. `Bench`
-11. `ASAN`
-12. `WASM`
-13. `schemars`
-14. `Arch32bit`
-15. `Cross`
-16. `Embedded`
-17. `Kani`
-18. `Coveralls` - run by `coveralls.yml`
-19. `release` - run by `release.yml`
-20. `labeler` - run by `manage-pr.yml`
diff --git a/.github/workflows/cron-semi-weekly-update-nightly.yml b/.github/workflows/cron-daily-update-nightly.yml
similarity index 96%
rename from .github/workflows/cron-semi-weekly-update-nightly.yml
rename to .github/workflows/cron-daily-update-nightly.yml
index 51f7afa2..467abc07 100644
--- a/.github/workflows/cron-semi-weekly-update-nightly.yml
+++ b/.github/workflows/cron-daily-update-nightly.yml
@@ -1,7 +1,7 @@
 name: Update Nightly rustc
 on:
   schedule:
-    - cron: "0 0 * * 1,4" # runs every Monday and Thursday at 00:00
+    - cron: "0 0 * * *" # runs daily at 00:00
   workflow_dispatch: # allows manual triggering
 jobs:
   format:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 7f778e8f..da20ed1d 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -1,5 +1,4 @@
----   # rust-bitcoin CI: If you edit this file please update README.md
-on:   # yamllint disable-line rule:truthy
+on:
   push:
     branches:
       - master
@@ -30,18 +29,12 @@ jobs:
     steps:
       - name: "Checkout repo"
         uses: actions/checkout@v4
-      - name: "Checkout maintainer tools"
-        uses: actions/checkout@v4
-        with:
-          repository: rust-bitcoin/rust-bitcoin-maintainer-tools
-          rev: b2ac115
-          path: maintainer-tools
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@stable
       - name: "Set dependencies"
         run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
       - name: "Run test script"
-        run: ./maintainer-tools/ci/run_task.sh stable
+        run: ./contrib/run_task.sh stable
 
   Nightly:                      # 2 jobs, one per manifest.
     name: Test - nightly toolchain
@@ -54,12 +47,6 @@ jobs:
     steps:
       - name: "Checkout repo"
         uses: actions/checkout@v4
-      - name: "Checkout maintainer tools"
-        uses: actions/checkout@v4
-        with:
-          repository: rust-bitcoin/rust-bitcoin-maintainer-tools
-          rev: b2ac115
-          path: maintainer-tools
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@v1
         with:
@@ -67,7 +54,7 @@ jobs:
       - name: "Set dependencies"
         run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
       - name: "Run test script"
-        run: ./maintainer-tools/ci/run_task.sh nightly
+        run: ./contrib/run_task.sh nightly
 
   MSRV:                         # 2 jobs, one per manifest.
     name: Test - 1.56.1 toolchain
@@ -79,20 +66,14 @@ jobs:
     steps:
       - name: "Checkout repo"
         uses: actions/checkout@v4
-      - name: "Checkout maintainer tools"
-        uses: actions/checkout@v4
-        with:
-          repository: rust-bitcoin/rust-bitcoin-maintainer-tools
-          rev: b2ac115
-          path: maintainer-tools
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@stable
         with:
           toolchain: "1.56.1"
-      - name: "Set dependencies"
+      - name: "Copy lock file"
         run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
       - name: "Run test script"
-        run: ./maintainer-tools/ci/run_task.sh msrv
+        run: ./contrib/run_task.sh msrv
 
   Lint:
     name: Lint - nightly toolchain
@@ -105,12 +86,6 @@ jobs:
     steps:
       - name: "Checkout repo"
         uses: actions/checkout@v4
-      - name: "Checkout maintainer tools"
-        uses: actions/checkout@v4
-        with:
-          repository: rust-bitcoin/rust-bitcoin-maintainer-tools
-          rev: b2ac115
-          path: maintainer-tools
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@v1
         with:
@@ -120,7 +95,7 @@ jobs:
       - name: "Set dependencies"
         run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
       - name: "Run test script"
-        run: ./maintainer-tools/ci/run_task.sh lint
+        run: ./contrib/run_task.sh lint
 
   Docs:
     name: Docs - stable toolchain
@@ -132,18 +107,12 @@ jobs:
     steps:
       - name: "Checkout repo"
         uses: actions/checkout@v4
-      - name: "Checkout maintainer tools"
-        uses: actions/checkout@v4
-        with:
-          repository: rust-bitcoin/rust-bitcoin-maintainer-tools
-          rev: b2ac115
-          path: maintainer-tools
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@stable
-      - name: "Set dependencies"
+      - name: "Copy lock file"
         run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
       - name: "Run test script"
-        run: ./maintainer-tools/ci/run_task.sh docs
+        run: ./contrib/run_task.sh docs
 
   Docsrs:
     name: Docs - nightly toolchain
@@ -156,12 +125,6 @@ jobs:
     steps:
       - name: "Checkout repo"
         uses: actions/checkout@v4
-      - name: "Checkout maintainer tools"
-        uses: actions/checkout@v4
-        with:
-          repository: rust-bitcoin/rust-bitcoin-maintainer-tools
-          rev: b2ac115
-          path: maintainer-tools
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@v1
         with:
@@ -169,7 +132,7 @@ jobs:
       - name: "Set dependencies"
         run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
       - name: "Run test script"
-        run: ./maintainer-tools/ci/run_task.sh docsrs
+        run: ./contrib/run_task.sh docsrs
 
   Bench:
     name: Bench - nightly toolchain
@@ -182,20 +145,14 @@ jobs:
     steps:
       - name: "Checkout repo"
         uses: actions/checkout@v4
-      - name: "Checkout maintainer tools"
-        uses: actions/checkout@v4
-        with:
-          repository: rust-bitcoin/rust-bitcoin-maintainer-tools
-          rev: b2ac115
-          path: maintainer-tools
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@v1
         with:
           toolchain: ${{ needs.Prepare.outputs.nightly_version }}
-      - name: "Set dependencies"
+      - name: "Copy lock file"
         run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
       - name: "Run test script"
-        run: ./maintainer-tools/ci/run_task.sh bench
+        run: ./contrib/run_task.sh bench
 
   Arch32bit:
     name: Test 32-bit version
@@ -256,7 +213,7 @@ jobs:
       - name: "Run hashes/embedded with alloc"
         run: cd hashes/embedded && cargo run --target thumbv7m-none-eabi --features=alloc
 
-  ASAN:                         # hashes crate only.
+  ASAN: # hashes crate only.
     name: ASAN - nightly toolchain
     needs: Prepare
     runs-on: ubuntu-latest
@@ -273,12 +230,12 @@ jobs:
           toolchain: ${{ needs.Prepare.outputs.nightly_version }}
       - name: Install src
         run: rustup component add rust-src
-      - name: "Set dependencies"
+      - name: "Copy lock file"
         run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
-      - name: "Run sanitizer script"
-        run: cd ./hashes && ./contrib/sanitizer.sh
+      - name: "Run test script"
+        run: ./contrib/run_task.sh asan
 
-  WASM:                         # hashes crate only.
+  WASM: # hashes crate only.
     name: WASM - stable toolchain
     runs-on: ubuntu-latest
     strategy:
@@ -289,22 +246,25 @@ jobs:
         uses: actions/checkout@v4
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@stable
-      - name: "Run wasm script"
-        run: cd hashes && ./contrib/wasm.sh
+      - name: "Run test script"
+        run: ./contrib/run_task.sh wasm
 
-  Schemars:                     # hashes crate only.
+  Schemars: # hashes crate only.
     name: Schemars - stable toolchain
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
-      # Note we do not use the recent lock file for schemars testing.
+      matrix:
+        dep: [recent]
     steps:
       - name: "Checkout repo"
         uses: actions/checkout@v4
       - name: "Select toolchain"
         uses: dtolnay/rust-toolchain@stable
-      - name: "Run schemars test"
-        run: cd hashes/extended_tests/schemars && cargo test
+      - name: "Copy lock file"
+        run: cp Cargo-${{ matrix.dep }}.lock Cargo.lock
+      - name: "Run test script"
+        run: ./contrib/run_task.sh schemars
 
   Kani:
     name: Kani codegen - stable toolchain
diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml
deleted file mode 100644
index 8577c050..00000000
--- a/.github/workflows/shellcheck.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-name: Shellcheck
-on:
-  pull_request:
-    branches:
-      - master
-jobs:
-  shellcheck:
-    name: Shellcheck
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v4
-    - name: Run ShellCheck
-      uses: ludeeus/action-shellcheck@2.0.0
-      env:
-        SHELLCHECK_OPTS: -x # allow outside sources
diff --git a/base58/src/lib.rs b/base58/src/lib.rs
index 05544c43..9533365e 100644
--- a/base58/src/lib.rs
+++ b/base58/src/lib.rs
@@ -19,9 +19,6 @@
 #[macro_use]
 extern crate alloc;
 
-#[cfg(bench)]
-extern crate test;
-
 #[cfg(feature = "std")]
 extern crate std;
 
@@ -31,7 +28,7 @@
 
 #[cfg(not(feature = "std"))]
 pub use alloc::{string::String, vec::Vec};
-use core::{fmt, str};
+use core::{fmt, iter, slice, str};
 #[cfg(feature = "std")]
 pub use std::{string::String, vec::Vec};
 
@@ -151,7 +148,7 @@ fn format_iter<I, W>(writer: &mut W, data: I) -> Result<(), fmt::Error>
     I: Iterator<Item = u8> + Clone,
     W: fmt::Write,
 {
-    let mut ret = Vec::with_capacity(128);
+    let mut ret = SmallVec::new();
 
     let mut leading_zero_count = 0;
     let mut leading_zeroes = true;
@@ -176,7 +173,9 @@ fn format_iter<I, W>(writer: &mut W, data: I) -> Result<(), fmt::Error>
     }
 
     // ... then reverse it and convert to chars
-    ret.resize(ret.len() + leading_zero_count, 0);
+    for _ in 0..leading_zero_count {
+        ret.push(0);
+    }
 
     for ch in ret.iter().rev() {
         writer.write_char(BASE58_CHARS[*ch as usize] as char)?;
@@ -185,6 +184,37 @@ fn format_iter<I, W>(writer: &mut W, data: I) -> Result<(), fmt::Error>
     Ok(())
 }
 
+/// Vector-like object that holds the first 100 elements on the stack. If more space is needed it
+/// will be allocated on the heap.
+struct SmallVec<T> {
+    len: usize,
+    stack: [T; 100],
+    heap: Vec<T>,
+}
+
+impl<T: Default + Copy> SmallVec<T> {
+    fn new() -> SmallVec<T> { SmallVec { len: 0, stack: [T::default(); 100], heap: Vec::new() } }
+
+    fn push(&mut self, val: T) {
+        if self.len < 100 {
+            self.stack[self.len] = val;
+            self.len += 1;
+        } else {
+            self.heap.push(val);
+        }
+    }
+
+    fn iter(&self) -> iter::Chain<slice::Iter<T>, slice::Iter<T>> {
+        // If len<100 then we just append an empty vec
+        self.stack[0..self.len].iter().chain(self.heap.iter())
+    }
+
+    fn iter_mut(&mut self) -> iter::Chain<slice::IterMut<T>, slice::IterMut<T>> {
+        // If len<100 then we just append an empty vec
+        self.stack[0..self.len].iter_mut().chain(self.heap.iter_mut())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use hex::test_hex_unwrap as hex;
@@ -254,28 +284,3 @@ fn test_base58_roundtrip() {
         assert_eq!(decode_check(&encode(&[1, 2, 3])), Err(TooShortError { length: 3 }.into()));
     }
 }
-
-#[cfg(bench)]
-mod benches {
-    use test::{black_box, Bencher};
-
-    #[bench]
-    pub fn bench_encode_check_50(bh: &mut Bencher) {
-        let data: alloc::vec::Vec<_> = (0u8..50).collect();
-
-        bh.iter(|| {
-            let r = super::encode_check(&data);
-            black_box(&r);
-        });
-    }
-
-    #[bench]
-    pub fn bench_encode_check_xpub(bh: &mut Bencher) {
-        let data: alloc::vec::Vec<_> = (0u8..78).collect(); // lenght of xpub
-
-        bh.iter(|| {
-            let r = super::encode_check(&data);
-            black_box(&r);
-        });
-    }
-}
diff --git a/bitcoin/embedded/scripts/env.sh b/bitcoin/embedded/scripts/env.sh
index 798c4af8..125162b6 100644
--- a/bitcoin/embedded/scripts/env.sh
+++ b/bitcoin/embedded/scripts/env.sh
@@ -1,4 +1,2 @@
-# we don't want shebangs in env.sh, disable shellcheck warning
-# shellcheck disable=SC2148
 export RUSTFLAGS="-C link-arg=-Tlink.x"
 export CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER="qemu-system-arm -cpu cortex-m3 -machine mps2-an385 -nographic -semihosting-config enable=on,target=native -kernel"
diff --git a/bitcoin/examples/bip32.rs b/bitcoin/examples/bip32.rs
index 8b15ba52..901793ed 100644
--- a/bitcoin/examples/bip32.rs
+++ b/bitcoin/examples/bip32.rs
@@ -41,7 +41,7 @@ fn main() {
 
     // derive child xpub
     let path = DerivationPath::from_str("84h/0h/0h").unwrap();
-    let child = root.derive_priv(&secp, &path);
+    let child = root.derive_priv(&secp, &path).unwrap();
     println!("Child at {}: {}", path, child);
     let xpub = Xpub::from_priv(&secp, &child);
     println!("Public key at {}: {}", path, xpub);
diff --git a/bitcoin/examples/ecdsa-psbt.rs b/bitcoin/examples/ecdsa-psbt.rs
index 93631fcc..428461a9 100644
--- a/bitcoin/examples/ecdsa-psbt.rs
+++ b/bitcoin/examples/ecdsa-psbt.rs
@@ -116,11 +116,11 @@ fn new<C: Signing>(secp: &Secp256k1<C>, xpriv: &str) -> Result<ExportData> {
         // Hardened children require secret data to derive.
 
         let path = "84h/0h/0h".into_derivation_path()?;
-        let account_0_xpriv = master_xpriv.derive_priv(secp, &path);
+        let account_0_xpriv = master_xpriv.derive_priv(secp, &path)?;
         let account_0_xpub = Xpub::from_priv(secp, &account_0_xpriv);
 
         let path = INPUT_UTXO_DERIVATION_PATH.into_derivation_path()?;
-        let input_xpriv = master_xpriv.derive_priv(secp, &path);
+        let input_xpriv = master_xpriv.derive_priv(secp, &path)?;
         let input_xpub = Xpub::from_priv(secp, &input_xpriv);
 
         let wallet = ColdStorage { master_xpriv, master_xpub };
diff --git a/bitcoin/examples/taproot-psbt.rs b/bitcoin/examples/taproot-psbt.rs
index 1b6d59c1..b5db05ab 100644
--- a/bitcoin/examples/taproot-psbt.rs
+++ b/bitcoin/examples/taproot-psbt.rs
@@ -296,7 +296,7 @@ fn generate_bip86_key_spend_tx(
                 .get(&input.tap_internal_key.ok_or("Internal key missing in PSBT")?)
                 .ok_or("Missing taproot key origin")?;
 
-            let secret_key = master_xpriv.derive_priv(secp, &derivation_path).to_priv().inner;
+            let secret_key = master_xpriv.derive_priv(secp, &derivation_path)?.to_priv().inner;
             sign_psbt_taproot(
                 &secret_key,
                 input.tap_internal_key.unwrap(),
@@ -391,7 +391,7 @@ fn create_inheritance_funding_tx(
         // that we use an unhardened path so we can make use of xpubs.
         let derivation_path = DerivationPath::from_str(&format!("101/1/0/0/{}", self.next))?;
         let internal_keypair =
-            self.master_xpriv.derive_priv(&self.secp, &derivation_path).to_keypair(&self.secp);
+            self.master_xpriv.derive_priv(&self.secp, &derivation_path)?.to_keypair(&self.secp);
         let beneficiary_key =
             self.beneficiary_xpub.derive_pub(&self.secp, &derivation_path)?.to_x_only_pub();
 
@@ -481,7 +481,7 @@ fn refresh_tx(
                 DerivationPath::from_str(&format!("101/1/0/0/{}", self.next))?;
             let new_internal_keypair = self
                 .master_xpriv
-                .derive_priv(&self.secp, &new_derivation_path)
+                .derive_priv(&self.secp, &new_derivation_path)?
                 .to_keypair(&self.secp);
             let beneficiary_key =
                 self.beneficiary_xpub.derive_pub(&self.secp, &new_derivation_path)?.to_x_only_pub();
@@ -530,7 +530,7 @@ fn refresh_tx(
                     .get(&input.tap_internal_key.ok_or("Internal key missing in PSBT")?)
                     .ok_or("Missing taproot key origin")?;
                 let secret_key =
-                    self.master_xpriv.derive_priv(&self.secp, &derivation_path).to_priv().inner;
+                    self.master_xpriv.derive_priv(&self.secp, &derivation_path)?.to_priv().inner;
                 sign_psbt_taproot(
                     &secret_key,
                     spend_info.internal_key(),
@@ -651,7 +651,7 @@ fn spend_inheritance(
             &psbt.inputs[0].tap_key_origins.clone()
         {
             let secret_key =
-                self.master_xpriv.derive_priv(&self.secp, &derivation_path).to_priv().inner;
+                self.master_xpriv.derive_priv(&self.secp, &derivation_path)?.to_priv().inner;
             for lh in leaf_hashes {
                 let sighash_type = TapSighashType::All;
                 let hash = SighashCache::new(&unsigned_tx).taproot_script_spend_signature_hash(
diff --git a/bitcoin/src/bip152.rs b/bitcoin/src/bip152.rs
index 2d2c0fea..e68e717d 100644
--- a/bitcoin/src/bip152.rs
+++ b/bitcoin/src/bip152.rs
@@ -14,7 +14,7 @@
 use io::{BufRead, Write};
 
 use crate::consensus::encode::{self, Decodable, Encodable, VarInt};
-use crate::internal_macros::{impl_bytes_newtype, impl_consensus_encoding};
+use crate::internal_macros::{impl_array_newtype_stringify, impl_consensus_encoding};
 use crate::prelude::*;
 use crate::{block, Block, BlockHash, Transaction};
 
@@ -95,7 +95,7 @@ fn consensus_decode<R: BufRead + ?Sized>(r: &mut R) -> Result<Self, encode::Erro
 #[derive(PartialEq, Eq, Clone, Copy, Hash, Default, PartialOrd, Ord)]
 pub struct ShortId([u8; 6]);
 impl_array_newtype!(ShortId, u8, 6);
-impl_bytes_newtype!(ShortId, 6);
+impl_array_newtype_stringify!(ShortId, 6);
 
 impl ShortId {
     /// Calculate the SipHash24 keys used to calculate short IDs.
diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs
index dd685507..fda920ae 100644
--- a/bitcoin/src/bip32.rs
+++ b/bitcoin/src/bip32.rs
@@ -16,7 +16,7 @@
 use secp256k1::{Secp256k1, XOnlyPublicKey};
 
 use crate::crypto::key::{CompressedPublicKey, Keypair, PrivateKey};
-use crate::internal_macros::impl_bytes_newtype;
+use crate::internal_macros::impl_array_newtype_stringify;
 use crate::network::NetworkKind;
 use crate::prelude::*;
 
@@ -41,11 +41,11 @@
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub struct ChainCode([u8; 32]);
 impl_array_newtype!(ChainCode, u8, 32);
-impl_bytes_newtype!(ChainCode, 32);
+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")
+        hmac[32..].try_into().expect("half of hmac is guaranteed to be 32 bytes")
     }
 }
 
@@ -53,7 +53,7 @@ fn from_hmac(hmac: Hmac<sha512::Hash>) -> Self {
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
 pub struct Fingerprint([u8; 4]);
 impl_array_newtype!(Fingerprint, u8, 4);
-impl_bytes_newtype!(Fingerprint, 4);
+impl_array_newtype_stringify!(Fingerprint, 4);
 
 hash_newtype! {
     /// Extended key identifier as defined in BIP-32.
@@ -566,7 +566,7 @@ pub fn new_master(network: impl Into<NetworkKind>, seed: &[u8]) -> Result<Xpriv,
             depth: 0,
             parent_fingerprint: Default::default(),
             child_number: ChildNumber::from_normal_idx(0)?,
-            private_key: secp256k1::SecretKey::from_slice(&hmac_result.as_ref()[..32])?,
+            private_key: secp256k1::SecretKey::from_slice(&hmac_result[..32])?,
             chain_code: ChainCode::from_hmac(hmac_result),
         })
     }
@@ -583,23 +583,27 @@ pub fn to_keypair<C: secp256k1::Signing>(self, secp: &Secp256k1<C>) -> Keypair {
             .expect("BIP32 internal private key representation is broken")
     }
 
-    /// Derives an extended private key from a path.
+    /// Attempts to derive an extended private key from a path.
     ///
     /// The `path` argument can be both of type `DerivationPath` or `Vec<ChildNumber>`.
     pub fn derive_priv<C: secp256k1::Signing, P: AsRef<[ChildNumber]>>(
         &self,
         secp: &Secp256k1<C>,
         path: &P,
-    ) -> Xpriv {
+    ) -> Result<Xpriv, Error> {
         let mut sk: Xpriv = *self;
         for cnum in path.as_ref() {
-            sk = sk.ckd_priv(secp, *cnum)
+            sk = sk.ckd_priv(secp, *cnum)?;
         }
-        sk
+        Ok(sk)
     }
 
     /// Private->Private child key derivation
-    fn ckd_priv<C: secp256k1::Signing>(&self, secp: &Secp256k1<C>, i: ChildNumber) -> Xpriv {
+    fn ckd_priv<C: secp256k1::Signing>(
+        &self,
+        secp: &Secp256k1<C>,
+        i: ChildNumber,
+    ) -> Result<Xpriv, Error> {
         let mut hmac_engine: HmacEngine<sha512::Hash> = HmacEngine::new(&self.chain_code[..]);
         match i {
             ChildNumber::Normal { .. } => {
@@ -617,19 +621,19 @@ fn ckd_priv<C: secp256k1::Signing>(&self, secp: &Secp256k1<C>, i: ChildNumber) -
 
         hmac_engine.input(&u32::from(i).to_be_bytes());
         let hmac_result: Hmac<sha512::Hash> = Hmac::from_engine(hmac_engine);
-        let sk = secp256k1::SecretKey::from_slice(&hmac_result.as_ref()[..32])
+        let sk = secp256k1::SecretKey::from_slice(&hmac_result[..32])
             .expect("statistically impossible to hit");
         let tweaked =
             sk.add_tweak(&self.private_key.into()).expect("statistically impossible to hit");
 
-        Xpriv {
+        Ok(Xpriv {
             network: self.network,
             depth: self.depth + 1,
             parent_fingerprint: self.fingerprint(secp),
             child_number: i,
             private_key: tweaked,
             chain_code: ChainCode::from_hmac(hmac_result),
-        }
+        })
     }
 
     /// Decoding extended private key from binary data according to BIP 32
@@ -738,7 +742,7 @@ pub fn ckd_pub_tweak(
 
                 let hmac_result: Hmac<sha512::Hash> = Hmac::from_engine(hmac_engine);
 
-                let private_key = secp256k1::SecretKey::from_slice(&hmac_result.as_ref()[..32])?;
+                let private_key = secp256k1::SecretKey::from_slice(&hmac_result[..32])?;
                 let chain_code = ChainCode::from_hmac(hmac_result);
                 Ok((private_key, chain_code))
             }
@@ -997,7 +1001,7 @@ fn test_path<C: secp256k1::Signing + secp256k1::Verification>(
         let mut pk = Xpub::from_priv(secp, &sk);
 
         // Check derivation convenience method for Xpriv
-        assert_eq!(&sk.derive_priv(secp, &path).to_string()[..], expected_sk);
+        assert_eq!(&sk.derive_priv(secp, &path).unwrap().to_string()[..], expected_sk);
 
         // Check derivation convenience method for Xpub, should error
         // appropriately if any ChildNumber is hardened
@@ -1009,7 +1013,7 @@ fn test_path<C: secp256k1::Signing + secp256k1::Verification>(
 
         // Derive keys, checking hardened and non-hardened derivation one-by-one
         for &num in path.0.iter() {
-            sk = sk.ckd_priv(secp, num);
+            sk = sk.ckd_priv(secp, num).unwrap();
             match num {
                 Normal { .. } => {
                     let pk2 = pk.ckd_pub(secp, num).unwrap();
diff --git a/bitcoin/src/blockdata/constants.rs b/bitcoin/src/blockdata/constants.rs
index 0b01831f..57fb2419 100644
--- a/bitcoin/src/blockdata/constants.rs
+++ b/bitcoin/src/blockdata/constants.rs
@@ -18,7 +18,7 @@
 use crate::blockdata::transaction::{self, OutPoint, Sequence, Transaction, TxIn, TxOut};
 use crate::blockdata::witness::Witness;
 use crate::consensus::Params;
-use crate::internal_macros::impl_bytes_newtype;
+use crate::internal_macros::impl_array_newtype_stringify;
 use crate::network::Network;
 use crate::pow::CompactTarget;
 use crate::Amount;
@@ -141,7 +141,7 @@ pub fn genesis_block(params: impl AsRef<Params>) -> Block {
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub struct ChainHash([u8; 32]);
 impl_array_newtype!(ChainHash, u8, 32);
-impl_bytes_newtype!(ChainHash, 32);
+impl_array_newtype_stringify!(ChainHash, 32);
 
 impl ChainHash {
     // Mainnet value can be verified at https://github.com/lightning/bolts/blob/master/00-introduction.md
diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs
index 0ea93820..69d67f9b 100644
--- a/bitcoin/src/crypto/sighash.rs
+++ b/bitcoin/src/crypto/sighash.rs
@@ -61,6 +61,7 @@ fn from(hash: $ty) -> secp256k1::Message {
     /// Taproot-tagged hash with tag \"TapSighash\".
     ///
     /// This hash type is used for computing taproot signature hash."
+    #[hash_newtype(forward)]
     pub struct TapSighash(_);
 }
 
diff --git a/bitcoin/src/internal_macros.rs b/bitcoin/src/internal_macros.rs
index b8d20694..91850029 100644
--- a/bitcoin/src/internal_macros.rs
+++ b/bitcoin/src/internal_macros.rs
@@ -44,30 +44,17 @@ fn consensus_decode<R: $crate::io::BufRead + ?Sized>(
 }
 pub(crate) use impl_consensus_encoding;
 
-/// Implements several traits for byte-based newtypes.
-/// Implements:
-/// - core::fmt::LowerHex
-/// - core::fmt::UpperHex
-/// - core::fmt::Display
-/// - core::str::FromStr
-macro_rules! impl_bytes_newtype {
+/// Implements several string-ish traits for byte-based newtypes.
+///
+/// - `fmt::Display` and `str::FromStr` (using lowercase hex)
+/// - `fmt::LowerHex` and `UpperHex`
+/// - `fmt::Debug` (using `LowerHex`)
+/// - `serde::Serialize` and `Deserialize` (using lowercase hex)
+///
+/// As well as an inherent `from_hex` method.
+macro_rules! impl_array_newtype_stringify {
     ($t:ident, $len:literal) => {
         impl $t {
-            /// Returns a reference the underlying bytes.
-            #[inline]
-            pub fn as_bytes(&self) -> &[u8; $len] { &self.0 }
-
-            /// Returns the underlying bytes.
-            #[inline]
-            pub fn to_bytes(self) -> [u8; $len] {
-                // We rely on `Copy` being implemented for $t so conversion
-                // methods use the correct Rust naming conventions.
-                fn check_copy<T: Copy>() {}
-                check_copy::<$t>();
-
-                self.0
-            }
-
             /// Creates `Self` from a hex string.
             pub fn from_hex(s: &str) -> Result<Self, hex::HexToArrayError> {
                 Ok($t($crate::hex::FromHex::from_hex(s)?))
@@ -186,7 +173,7 @@ fn visit_bytes<E>(self, v: &[u8]) -> core::result::Result<Self::Value, E>
         }
     };
 }
-pub(crate) use impl_bytes_newtype;
+pub(crate) use impl_array_newtype_stringify;
 
 #[rustfmt::skip]
 macro_rules! impl_hashencode {
diff --git a/bitcoin/src/psbt/map/input.rs b/bitcoin/src/psbt/map/input.rs
index cfe77811..6c722f2f 100644
--- a/bitcoin/src/psbt/map/input.rs
+++ b/bitcoin/src/psbt/map/input.rs
@@ -136,16 +136,6 @@ pub struct Input {
 /// type can be either [`EcdsaSighashType`] or [`TapSighashType`] but it is not possible to know
 /// directly which signature hash type the user is dealing with. Therefore, the user is responsible
 /// for converting to/from [`PsbtSighashType`] from/to the desired signature hash type they need.
-///
-/// # Examples
-///
-/// ```
-/// use bitcoin::{EcdsaSighashType, TapSighashType};
-/// use bitcoin::psbt::PsbtSighashType;
-///
-/// let ecdsa_sighash_all: PsbtSighashType = EcdsaSighashType::All.into();
-/// let tap_sighash_all: PsbtSighashType = TapSighashType::All.into();
-/// ```
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
@@ -524,7 +514,7 @@ fn psbt_insert_hash_pair<H>(
             if <H as hashes::Hash>::hash(&val) != key_val {
                 return Err(psbt::Error::InvalidPreimageHashPair {
                     preimage: val.into_boxed_slice(),
-                    hash: Box::from(key_val.as_ref()),
+                    hash: Box::from(key_val.borrow()),
                     hash_type,
                 });
             }
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index 667719ec..3d428fda 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -762,7 +762,7 @@ fn get_key<C: Signing>(
             KeyRequest::Pubkey(_) => Err(GetKeyError::NotSupported),
             KeyRequest::Bip32((fingerprint, path)) => {
                 let key = if self.fingerprint(secp) == fingerprint {
-                    let k = self.derive_priv(secp, &path);
+                    let k = self.derive_priv(secp, &path)?;
                     Some(k.to_priv())
                 } else {
                     None
@@ -808,7 +808,7 @@ fn get_key<C: Signing>(
             KeyRequest::Bip32((fingerprint, path)) => {
                 for xpriv in self.iter() {
                     if xpriv.parent_fingerprint == fingerprint {
-                        let k = xpriv.derive_priv(secp, &path);
+                        let k = xpriv.derive_priv(secp, &path)?;
                         return Ok(Some(k.to_priv()));
                     }
                 }
@@ -1366,7 +1366,7 @@ fn serialize_then_deserialize_output() {
             ChildNumber::from_normal_idx(31337).unwrap(),
         ];
 
-        sk = sk.derive_priv(secp, &dpath);
+        sk = sk.derive_priv(secp, &dpath).unwrap();
 
         let pk = Xpub::from_priv(secp, &sk);
 
diff --git a/bitcoin/src/psbt/serialize.rs b/bitcoin/src/psbt/serialize.rs
index f9e0564a..06df0fc9 100644
--- a/bitcoin/src/psbt/serialize.rs
+++ b/bitcoin/src/psbt/serialize.rs
@@ -185,7 +185,7 @@ impl Serialize for KeySource {
     fn serialize(&self) -> Vec<u8> {
         let mut rv: Vec<u8> = Vec::with_capacity(key_source_len(self));
 
-        rv.append(&mut self.0.to_bytes().to_vec());
+        rv.append(&mut self.0.to_byte_array().to_vec());
 
         for cnum in self.1.into_iter() {
             rv.append(&mut serialize(&u32::from(*cnum)))
diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs
index 87318ada..60ce28ee 100644
--- a/bitcoin/src/taproot/mod.rs
+++ b/bitcoin/src/taproot/mod.rs
@@ -36,6 +36,7 @@
     /// Taproot-tagged hash with tag \"TapLeaf\".
     ///
     /// This is used for computing tapscript script spend hash.
+    #[hash_newtype(forward)]
     pub struct TapLeafHash(_);
 
     pub struct TapBranchTag = hash_str("TapBranch");
@@ -43,6 +44,7 @@
     /// Tagged hash used in taproot trees.
     ///
     /// See BIP-340 for tagging rules.
+    #[hash_newtype(forward)]
     pub struct TapNodeHash(_);
 
     pub struct TapTweakTag = hash_str("TapTweak");
@@ -50,6 +52,7 @@
     /// Taproot-tagged hash with tag \"TapTweak\".
     ///
     /// This hash type is used while computing the tweaked public key.
+    #[hash_newtype(forward)]
     pub struct TapTweakHash(_);
 }
 
diff --git a/contrib/crates.sh b/contrib/crates.sh
deleted file mode 100755
index f74787e4..00000000
--- a/contrib/crates.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-# disable verify unused vars, despite the fact that they are used when sourced
-# shellcheck disable=SC2034
-
-# Crates in this workspace to test (note "fuzz" is only built not tested).
-CRATES=("base58" "bitcoin" "fuzz" "hashes" "internals" "io" "units")
diff --git a/contrib/gen_label_config.sh b/contrib/gen_label_config.sh
index 11536bbf..089bd30d 100755
--- a/contrib/gen_label_config.sh
+++ b/contrib/gen_label_config.sh
@@ -20,7 +20,7 @@ fi
 
 excluded_crates="fuzz|dep_test"
 
-CRATES="$(cd "$SCAN_DIR" && cargo metadata --no-deps --format-version 1 | jq -j -r '.packages | map(.manifest_path | rtrimstr("/Cargo.toml") | ltrimstr("'"$PWD"'/")) | join(" ")')"
+CRATES="`cd "$SCAN_DIR" && cargo metadata --no-deps --format-version 1 | jq -j -r '.packages | map(.manifest_path | rtrimstr("/Cargo.toml") | ltrimstr("'$PWD'/")) | join(" ")'`"
 
 for crate in $CRATES;
 do
@@ -29,9 +29,7 @@ do
 		continue
 	fi
 
-	{
-		echo "C-$crate:"
-		echo "  - changed-files:"
-		echo "    - any-glob-to-any-file: $crate/**"
-	} >> "$config"
+	echo "C-$crate:" >> "$config"
+	echo "  - changed-files:" >> "$config"
+	echo "    - any-glob-to-any-file: $crate/**" >> "$config"
 done
diff --git a/contrib/get_matrix.sh b/contrib/get_matrix.sh
new file mode 100755
index 00000000..d72a865b
--- /dev/null
+++ b/contrib/get_matrix.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+. contrib/test_vars.sh
+
+crates="`cargo metadata --no-deps --format-version 1 | jq -c '.packages | map(.manifest_path | rtrimstr("/Cargo.toml") | ltrimstr("'$PWD'/"))'`"
+deps="`echo -n $DEPS | jq -R -c 'split(" ")'`"
+# debug
+echo "$crates"
+echo "$deps"
+
+echo "crates=$crates" >> $GITHUB_OUTPUT
+echo "deps=$deps" >> $GITHUB_OUTPUT
diff --git a/contrib/release.sh b/contrib/release.sh
index b30f2fb8..558ba0fb 100755
--- a/contrib/release.sh
+++ b/contrib/release.sh
@@ -28,7 +28,7 @@ main () {
 # preparation for releasing the crate.
 release_changes() {
     local crate=$1
-    git log --patch --reverse master.. -- "$crate"/Cargo.toml | grep version
+    git log --patch --reverse master.. -- $crate/Cargo.toml | grep version
 }
 
 # Do a dry run publish to crates.io using the correct package name for crate ($1).
@@ -47,4 +47,4 @@ publish_dry_run() {
 #
 # Main script.
 #
-main "$@"
+main $@
diff --git a/contrib/run_task.sh b/contrib/run_task.sh
new file mode 100755
index 00000000..424dd902
--- /dev/null
+++ b/contrib/run_task.sh
@@ -0,0 +1,341 @@
+#!/usr/bin/env bash
+#
+# We are attempting to run max 20 parallel jobs using GitHub actions (usage limit for free tier).
+#
+# ref: https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration
+#
+# The minimal/recent manifests are handled by CI (rust.yaml).
+#
+# Jobs (shell functions) that get run twice, once for each manifest:
+#
+# 1+2  stable
+# 3+4  nightly
+# 5+6  msrv
+#
+# Jobs (shell functions) that get run once:
+#
+# 7   lint
+# 8   docs
+# 9   docsrs
+# 10  bench
+# 11  asan
+# 12  wasm
+# 13  schemars
+#
+# Jobs run directly by rust.yml workflow:
+#
+# 0 Prepare
+#
+# 14  Arch32bit
+# 15  Cross
+# 16  Embedded
+# 17  Kani
+#
+# Jobs run directly from other workflows:
+#
+# 18  Coveralls - run by coveralls.yml
+# 19  release - run by release.yml
+# 20  labeler - run by manage-pr.yml
+
+set -euox pipefail
+
+REPO_DIR=$(git rev-parse --show-toplevel)
+CRATES=("bitcoin" "hashes" "internals" "io" "units" "base58")
+
+# Make all cargo invocations verbose.
+export CARGO_TERM_VERBOSE=true
+
+main() {
+    local task="$1"
+
+    check_required_commands
+
+    cargo --version
+    rustc --version
+    /usr/bin/env bash --version
+    locale
+    env
+
+    case $task in
+        # 2 jobs each for these (one for each lock file).
+	stable)
+            # Test, run examples, do feature matrix.
+            # crate/contrib/test_vars.sh is sourced in this function.
+            build_and_test
+            ;;
+
+	nightly)
+            build_and_test
+            ;;
+
+	msrv)
+            build_and_test
+            ;;
+
+        # 1 job each for these.
+        lint)
+            do_lint
+            do_dup_deps
+            ;;
+
+	docs)
+            build_docs_with_stable_toolchain
+	    ;;
+
+	docsrs)
+            build_docs_with_nightly_toolchain
+	    ;;
+
+	bench)
+	    do_bench
+	    ;;
+
+	wasm)
+            # hashes crate only.
+	    do_wasm
+	    ;;
+
+	asan)
+            # hashes crate only - hashes/contrib/test_vars.sh is sourced in this function.
+	    do_asan
+	    ;;
+
+	schemars)
+            # hashes crate only.
+	    do_schemars
+	    ;;
+
+        *)
+            err "Error: unknown task $task"
+            ;;
+    esac
+}
+
+# Build and test for each crate, done with each toolchain.
+build_and_test() {
+    # Building the fuzz crate is more-or-less just a sanity check.
+    pushd "$REPO_DIR/fuzz" > /dev/null
+    cargo --locked build
+    popd > /dev/null
+
+    for crate in "${CRATES[@]}"; do
+        pushd "$REPO_DIR/$crate" > /dev/null
+
+        # Set crate specific variables.
+        . contrib/test_vars.sh || exit 1
+
+        do_test
+        do_feature_matrix
+
+        popd > /dev/null
+    done
+}
+
+do_test() {
+    # Use the current (recent/minimal) lock file.
+    local cargo="cargo --locked"
+
+    # Defaults / sanity checks
+    $cargo build
+    $cargo test
+
+    for example in $EXAMPLES; do # EXAMPLES is set in contrib/test_vars.sh
+        name="$(echo "$example" | cut -d ':' -f 1)"
+        features="$(echo "$example" | cut -d ':' -f 2)"
+        $cargo run --example "$name" --features="$features"
+    done
+
+    if [ -e ./contrib/extra_tests.sh ];
+    then
+        ./contrib/extra_tests.sh
+    fi
+}
+
+# Each crate defines its own feature matrix test so feature combinations
+# can be better controlled.
+do_feature_matrix() {
+    local cargo="cargo --locked"
+
+    $cargo build --no-default-features
+    $cargo test --no-default-features
+
+    # All crates have a "std" feature and FEATURES_WITH_STD is set in
+    # contrib/test_vars.sh
+    loop_features "std" "$FEATURES_WITH_STD"
+
+    # All but `bitcoin` crate have an "alloc" feature, this tests it
+    # along with any other features that should work with "std".
+    if [ -n "$FEATURES_WITHOUT_STD" ]
+       then
+           loop_features "" "$FEATURES_WITHOUT_STD"
+    fi
+}
+
+# Build with each feature as well as all combinations of two features.
+#
+# Usage: loop_features "std" "this-feature that-feature other"
+loop_features() {
+    local use="$1"
+    local features="$2"
+    local cargo="cargo --locked"
+
+    # All the provided features including $use
+    $cargo build --no-default-features --features="$use $features"
+    $cargo test --no-default-features --features="$use $features"
+
+    read -r -a array <<< "$features"
+    local len="${#array[@]}"
+
+    if (( len > 1 )); then
+        for ((i = 0 ; i < len ; i++ ));
+        do
+            $cargo build --features="$use ${array[i]}"
+            $cargo test --features="$use ${array[i]}"
+
+            if (( i < len - 1 )); then
+               for ((j = i + 1 ; j < len ; j++ ));
+               do
+                   $cargo build --features="$use ${array[i]} ${array[j]}"
+                   $cargo test --features="$use ${array[i]} ${array[j]}"
+               done
+            fi
+        done
+    fi
+}
+
+# Lint the workspace.
+do_lint() {
+    need_nightly
+    local cargo="cargo --locked"
+
+    # Lint various feature combinations to try and catch mistakes in feature gating.
+    $cargo clippy --workspace --all-targets --keep-going -- -D warnings
+    $cargo clippy --workspace --all-targets --all-features --keep-going -- -D warnings
+    $cargo clippy --workspace --all-targets --no-default-features --keep-going -- -D warnings
+}
+
+# We should not have any duplicate dependencies. This catches mistakes made upgrading dependencies
+# in one crate and not in another (e.g. upgrade bitcoin_hashes in bitcoin but not in secp).
+do_dup_deps() {
+    # We can't use pipefail because these grep statements fail by design when there is no duplicate,
+    # the shell therefore won't pick up mistakes in your pipe - you are on your own.
+    set +o pipefail
+
+    duplicate_dependencies=$(
+        # Only show the actual duplicated deps, not their reverse tree, then
+        # whitelist the 'syn' crate which is duplicated but it's not our fault.
+        #
+        # Temporarily allow 2 versions of `hashes`, `internals`, and `hex` while we upgrade.
+        cargo tree  --target=all --all-features --duplicates \
+            | grep '^[0-9A-Za-z]' \
+            | grep -v 'syn' \
+            | wc -l
+                          )
+    if [ "$duplicate_dependencies" -ne 0 ]; then
+        echo "Dependency tree is broken, contains duplicates"
+        cargo tree  --target=all --all-features --duplicates
+        exit 1
+    fi
+
+    set -o pipefail
+}
+
+# Build the docs with a nightly toolchain, in unison with the function
+# below this checks that we feature guarded docs imports correctly.
+build_docs_with_nightly_toolchain() {
+    need_nightly
+    local cargo="cargo --locked"
+    RUSTDOCFLAGS="--cfg docsrs -D warnings -D rustdoc::broken-intra-doc-links" $cargo doc --all-features
+}
+
+# Build the docs with a stable toolchain, in unison with the function
+# above this checks that we feature guarded docs imports correctly.
+build_docs_with_stable_toolchain() {
+    local cargo="cargo +stable --locked"
+    RUSTDOCFLAGS="-D warnings" $cargo doc --all-features
+}
+
+# Bench only works with a non-stable toolchain (nightly, beta).
+do_bench() {
+    for crate in bitcoin hashes; do
+        pushd "$REPO_DIR/$crate" > /dev/null
+        RUSTFLAGS='--cfg=bench' cargo bench
+        popd > /dev/null
+    done
+}
+
+# This is only relevant for hashes.
+do_schemars() {
+    pushd "$REPO_DIR/hashes/extended_tests/schemars" > /dev/null
+    cargo test
+    popd > /dev/null
+}
+
+# Note we do not use the recent lock file or `--locked` when running the wasm tests.
+do_wasm() {
+    pushd "$REPO_DIR/hashes" > /dev/null
+
+    clang --version &&
+	CARGO_TARGET_DIR=wasm cargo install --force wasm-pack &&
+	printf '\n[target.wasm32-unknown-unknown.dev-dependencies]\nwasm-bindgen-test = "0.3"\n' >> Cargo.toml &&
+	printf '\n[lib]\ncrate-type = ["cdylib", "rlib"]\n' >> Cargo.toml &&
+	CC=clang-9 wasm-pack build &&
+	CC=clang-9 wasm-pack test --node;
+
+    popd > /dev/null
+}
+
+do_asan() {
+    pushd "$REPO_DIR/hashes" > /dev/null
+
+    # Set ASAN_FEATURES
+    . contrib/test_vars.sh || exit 1
+
+    cargo clean
+    CC='clang -fsanitize=address -fno-omit-frame-pointer'                                        \
+      RUSTFLAGS='-Zsanitizer=address -Clinker=clang -Cforce-frame-pointers=yes'                    \
+      ASAN_OPTIONS='detect_leaks=1 detect_invalid_pointer_pairs=1 detect_stack_use_after_return=1' \
+      cargo test --lib --no-default-features --features="$ASAN_FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
+    # There is currently a bug in the MemorySanitizer (MSAN) - disable the job for now.
+    #
+    # cargo clean
+    # CC='clang -fsanitize=memory -fno-omit-frame-pointer'                                         \
+    #   RUSTFLAGS='-Zsanitizer=memory -Zsanitizer-memory-track-origins -Cforce-frame-pointers=yes'   \
+    #   cargo test --lib --no-default-features --features="$ASAN_FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
+
+    popd > /dev/null
+}
+
+# Check all the commands we use are present in the current environment.
+check_required_commands() {
+    need_cmd cargo
+    need_cmd rustc
+    need_cmd jq
+    need_cmd cut
+    need_cmd grep
+    need_cmd wc
+}
+
+need_cmd() {
+    if ! command -v "$1" > /dev/null 2>&1
+    then err "need '$1' (command not found)"
+    fi
+}
+
+need_nightly() {
+    cargo_ver=$(cargo --version)
+    if echo "$cargo_ver" | grep -q -v nightly; then
+        err "Need a nightly compiler; have $(cargo --version)"
+    fi
+}
+
+err() {
+    echo "$1" >&2
+    exit 1
+}
+
+#
+# Main script
+#
+main "$@"
+exit 0
diff --git a/fuzz/cycle.sh b/fuzz/cycle.sh
index c642157d..b72c44da 100755
--- a/fuzz/cycle.sh
+++ b/fuzz/cycle.sh
@@ -8,8 +8,7 @@
 set -euo pipefail
 
 REPO_DIR=$(git rev-parse --show-toplevel)
-# can't find the file because of the ENV var
-# shellcheck source=/dev/null
+# shellcheck source=./fuzz-util.sh
 source "$REPO_DIR/fuzz/fuzz-util.sh"
 
 while :
diff --git a/fuzz/fuzz.sh b/fuzz/fuzz.sh
index 2632e6bc..c85b8e4e 100755
--- a/fuzz/fuzz.sh
+++ b/fuzz/fuzz.sh
@@ -3,8 +3,7 @@ set -euox pipefail
 
 REPO_DIR=$(git rev-parse --show-toplevel)
 
-# can't find the file because of the ENV var
-# shellcheck source=/dev/null
+# shellcheck source=./fuzz-util.sh
 source "$REPO_DIR/fuzz/fuzz-util.sh"
 
 # Check that input files are correct Windows file names
diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh
index 39e64697..b3b32fb9 100755
--- a/fuzz/generate-files.sh
+++ b/fuzz/generate-files.sh
@@ -4,8 +4,7 @@ set -euo pipefail
 
 REPO_DIR=$(git rev-parse --show-toplevel)
 
-# can't find the file because of the ENV var
-# shellcheck source=/dev/null
+# shellcheck source=./fuzz-util.sh
 source "$REPO_DIR/fuzz/fuzz-util.sh"
 
 # 1. Generate fuzz/Cargo.toml
diff --git a/githooks/pre-commit b/githooks/pre-commit
index d0e92ee9..5d0b9fe6 100755
--- a/githooks/pre-commit
+++ b/githooks/pre-commit
@@ -21,13 +21,11 @@ exec 1>&2
 # Cross platform projects tend to avoid non-ASCII filenames; prevent
 # them from being added to the repository. We exploit the fact that the
 # printable range starts at the space character and ends with tilde.
-# disable the check to quoting the $(...) command
-# shellcheck disable=SC2046
 if [ "$allownonascii" != "true" ] &&
 	# Note that the use of brackets around a tr range is ok here, (it's
 	# even required, for portability to Solaris 10's /usr/bin/tr), since
 	# the square bracket bytes happen to fall in the designated range.
-	test $(git diff --cached --name-only --diff-filter=A -z "$against" |
+	test $(git diff --cached --name-only --diff-filter=A -z $against |
 	  LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
 then
 	cat <<\EOF
@@ -45,9 +43,7 @@ EOF
 fi
 
 # If there are whitespace errors, print the offending file names and fail.
-git diff-index --check --cached "$against" -- || exit 1
+git diff-index --check --cached $against -- || exit 1
 
 # Check that code lints cleanly.
-# disable the check to quoting the $(...) command
-# shellcheck disable=SC2046
 cargo +$(cat ./nightly-version) clippy --workspace --all-targets --all-features -- --deny warnings || exit 1
diff --git a/hashes/CHANGELOG.md b/hashes/CHANGELOG.md
index 887776a5..ca96b020 100644
--- a/hashes/CHANGELOG.md
+++ b/hashes/CHANGELOG.md
@@ -1,20 +1,3 @@
-# unreleased
-
-* Change the default display direction of for tagged hashes to forwards. [#2707](https://github.com/rust-bitcoin/rust-bitcoin/pull/2707)
-
-  Note please this usage if you need to display backward:
-
-```rust
-  sha256t_hash_newtype! {
-    /// Test detailed explanation.
-    struct NewTypeTag = hash_str("tag");
-
-    /// A test hash.
-    #[hash_newtype(backward)]
-    struct NewTypeHash(_);
-  }
-```
-
 # 0.14.0 - 2024-03-21
 
 * Bump MSRV to Rust version 1.56.1 [#2188](https://github.com/rust-bitcoin/rust-bitcoin/pull/2188)
diff --git a/hashes/contrib/sanitizer.sh b/hashes/contrib/sanitizer.sh
deleted file mode 100755
index 5a1e56c2..00000000
--- a/hashes/contrib/sanitizer.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-#
-# Run the Address/Memory Sanitizer tests.
-
-set -euox pipefail
-
-# Run the sanitizer with these features.
-FEATURES="std io serde"
-
-cargo clean
-CC='clang -fsanitize=address -fno-omit-frame-pointer'                                        \
-  RUSTFLAGS='-Zsanitizer=address -Clinker=clang -Cforce-frame-pointers=yes'                    \
-  ASAN_OPTIONS='detect_leaks=1 detect_invalid_pointer_pairs=1 detect_stack_use_after_return=1' \
-  cargo test --lib --no-default-features --features="$FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
-
-# There is currently a bug in the MemorySanitizer (MSAN) - disable the job for now.
-#
-# cargo clean
-# CC='clang -fsanitize=memory -fno-omit-frame-pointer'                                         \
-    #   RUSTFLAGS='-Zsanitizer=memory -Zsanitizer-memory-track-origins -Cforce-frame-pointers=yes'   \
-    #   cargo test --lib --no-default-features --features="$FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
diff --git a/hashes/contrib/wasm.sh b/hashes/contrib/wasm.sh
deleted file mode 100755
index 6139e307..00000000
--- a/hashes/contrib/wasm.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-#
-# Run the WASM tests.
-
-set -euox pipefail
-
-clang --version &&
-    CARGO_TARGET_DIR=wasm cargo install --force wasm-pack &&
-    printf '\n[target.wasm32-unknown-unknown.dev-dependencies]\nwasm-bindgen-test = "0.3"\n' >> Cargo.toml &&
-    printf '\n[lib]\ncrate-type = ["cdylib", "rlib"]\n' >> Cargo.toml &&
-    CC=clang-9 wasm-pack build &&
-    CC=clang-9 wasm-pack test --node;
diff --git a/hashes/embedded/scripts/env.sh b/hashes/embedded/scripts/env.sh
index 798c4af8..7ea1e31c 100644
--- a/hashes/embedded/scripts/env.sh
+++ b/hashes/embedded/scripts/env.sh
@@ -1,4 +1,4 @@
-# we don't want shebangs in env.sh, disable shellcheck warning
-# shellcheck disable=SC2148
+#!/usr/bin/env bash
+
 export RUSTFLAGS="-C link-arg=-Tlink.x"
 export CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER="qemu-system-arm -cpu cortex-m3 -machine mps2-an385 -nographic -semihosting-config enable=on,target=native -kernel"
diff --git a/hashes/src/hmac.rs b/hashes/src/hmac.rs
index 53be1959..32b967a5 100644
--- a/hashes/src/hmac.rs
+++ b/hashes/src/hmac.rs
@@ -8,7 +8,7 @@
 //! Hash-based Message Authentication Code (HMAC).
 //!
 
-use core::{convert, fmt, str};
+use core::{borrow, fmt, ops, str};
 
 #[cfg(feature = "serde")]
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -72,10 +72,10 @@ pub fn new(key: &[u8]) -> HmacEngine<T> {
 
         if key.len() > T::Engine::BLOCK_SIZE {
             let hash = <T as Hash>::hash(key);
-            for (b_i, b_h) in ipad.iter_mut().zip(hash.as_ref()) {
+            for (b_i, b_h) in ipad.iter_mut().zip(&hash[..]) {
                 *b_i ^= *b_h;
             }
-            for (b_o, b_h) in opad.iter_mut().zip(hash.as_ref()) {
+            for (b_o, b_h) in opad.iter_mut().zip(&hash[..]) {
                 *b_o ^= *b_h;
             }
         } else {
@@ -124,8 +124,33 @@ impl<T: Hash> fmt::LowerHex for Hmac<T> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) }
 }
 
-impl<T: Hash> convert::AsRef<[u8]> for Hmac<T> {
-    fn as_ref(&self) -> &[u8] { self.0.as_ref() }
+impl<T: Hash> ops::Index<usize> for Hmac<T> {
+    type Output = u8;
+    fn index(&self, index: usize) -> &u8 { &self.0[index] }
+}
+
+impl<T: Hash> ops::Index<ops::Range<usize>> for Hmac<T> {
+    type Output = [u8];
+    fn index(&self, index: ops::Range<usize>) -> &[u8] { &self.0[index] }
+}
+
+impl<T: Hash> ops::Index<ops::RangeFrom<usize>> for Hmac<T> {
+    type Output = [u8];
+    fn index(&self, index: ops::RangeFrom<usize>) -> &[u8] { &self.0[index] }
+}
+
+impl<T: Hash> ops::Index<ops::RangeTo<usize>> for Hmac<T> {
+    type Output = [u8];
+    fn index(&self, index: ops::RangeTo<usize>) -> &[u8] { &self.0[index] }
+}
+
+impl<T: Hash> ops::Index<ops::RangeFull> for Hmac<T> {
+    type Output = [u8];
+    fn index(&self, index: ops::RangeFull) -> &[u8] { &self.0[index] }
+}
+
+impl<T: Hash> borrow::Borrow<[u8]> for Hmac<T> {
+    fn borrow(&self) -> &[u8] { &self[..] }
 }
 
 impl<T: Hash> Hash for Hmac<T> {
@@ -134,7 +159,7 @@ impl<T: Hash> Hash for Hmac<T> {
 
     fn from_engine(mut e: HmacEngine<T>) -> Hmac<T> {
         let ihash = T::from_engine(e.iengine);
-        e.oengine.input(ihash.as_ref());
+        e.oengine.input(&ihash[..]);
         let ohash = T::from_engine(e.oengine);
         Hmac(ohash)
     }
@@ -293,7 +318,7 @@ struct Test {
             let mut engine = HmacEngine::<sha256::Hash>::new(&test.key);
             engine.input(&test.input);
             let hash = Hmac::<sha256::Hash>::from_engine(engine);
-            assert_eq!(hash.as_ref(), &test.output[..]);
+            assert_eq!(&hash[..], &test.output[..]);
             assert_eq!(hash.as_byte_array(), test.output.as_slice());
         }
     }
diff --git a/hashes/src/lib.rs b/hashes/src/lib.rs
index 1aa7a6b9..3bfdbf62 100644
--- a/hashes/src/lib.rs
+++ b/hashes/src/lib.rs
@@ -123,7 +123,7 @@ pub mod _core {
 pub mod sha512_256;
 pub mod siphash24;
 
-use core::{convert, fmt, hash};
+use core::{borrow, fmt, hash, ops};
 
 pub use hmac::{Hmac, HmacEngine};
 
@@ -158,7 +158,12 @@ pub trait Hash:
     + fmt::Debug
     + fmt::Display
     + fmt::LowerHex
-    + convert::AsRef<[u8]>
+    + ops::Index<ops::RangeFull, Output = [u8]>
+    + ops::Index<ops::RangeFrom<usize>, Output = [u8]>
+    + ops::Index<ops::RangeTo<usize>, Output = [u8]>
+    + ops::Index<ops::Range<usize>, Output = [u8]>
+    + ops::Index<usize, Output = u8>
+    + borrow::Borrow<[u8]>
 {
     /// A hashing engine which bytes can be serialized into. It is expected
     /// to implement the `io::Write` trait, and to never return errors under
diff --git a/hashes/src/sha256t.rs b/hashes/src/sha256t.rs
index 593b9fd6..c98d4e61 100644
--- a/hashes/src/sha256t.rs
+++ b/hashes/src/sha256t.rs
@@ -66,7 +66,7 @@ impl<T: Tag> core::hash::Hash for Hash<T> {
     fn hash<H: core::hash::Hasher>(&self, h: &mut H) { self.0.hash(h) }
 }
 
-crate::internal_macros::hash_trait_impls!(256, false, T: Tag);
+crate::internal_macros::hash_trait_impls!(256, true, T: Tag);
 
 fn from_engine<T: Tag>(e: sha256::HashEngine) -> Hash<T> {
     use crate::Hash as _;
@@ -91,8 +91,8 @@ fn from_engine<T: Tag>(e: sha256::HashEngine) -> Hash<T> {
 ///     pub struct FooTag = hash_str("foo");
 ///
 ///     /// A foo hash.
-///     // Direction works just like the hash_newtype! macro.
-///     #[hash_newtype(backward)]
+///     // Direction works just like in case of hash_newtype! macro.
+///     #[hash_newtype(forward)]
 ///     pub struct FooHash(_);
 /// }
 /// ```
@@ -174,15 +174,6 @@ mod tests {
         108, 71, 99, 110, 96, 125, 179, 62, 234, 221, 198, 240, 201,
     ];
 
-    // The digest created by sha256 hashing `&[0]` starting with `TEST_MIDSTATE`.
-    #[cfg(feature = "alloc")]
-    const HASH_ZERO_BACKWARD: &str =
-        "29589d5122ec666ab5b4695070b6debc63881a4f85d88d93ddc90078038213ed";
-    // And the same thing, forward.
-    #[cfg(feature = "alloc")]
-    const HASH_ZERO_FORWARD: &str =
-        "ed1382037800c9dd938dd8854f1a8863bcdeb6705069b4b56a66ec22519d5829";
-
     #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
     pub struct TestHashTag;
 
@@ -194,45 +185,44 @@ fn engine() -> sha256::HashEngine {
         }
     }
 
-    // We support manually implementing `Tag` and creating a tagged hash from it.
-    #[cfg(feature = "alloc")]
-    pub type TestHash = sha256t::Hash<TestHashTag>;
-
-    #[test]
-    #[cfg(feature = "alloc")]
-    fn manually_created_sha256t_hash_type() {
-        assert_eq!(TestHash::hash(&[0]).to_string(), HASH_ZERO_FORWARD);
-    }
-
-    // We also provide a macro to create the tag and the hash type.
-    sha256t_hash_newtype! {
-        /// Test detailed explanation.
-        struct NewTypeTagBackward = raw(TEST_MIDSTATE, 64);
-
-        /// A test hash.
-        #[hash_newtype(backward)]
-        struct NewTypeHashBackward(_);
+    #[cfg(feature = "schemars")]
+    impl schemars::JsonSchema for TestHashTag {
+        fn schema_name() -> String { "Hash".to_owned() }
+
+        fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+            let mut schema: schemars::schema::SchemaObject = <String>::json_schema(gen).into();
+            schema.string = Some(Box::new(schemars::schema::StringValidation {
+                max_length: Some(64 * 2),
+                min_length: Some(64 * 2),
+                pattern: Some("[0-9a-fA-F]+".to_owned()),
+            }));
+            schema.into()
+        }
     }
 
-    #[test]
+    /// A hash tagged with `$name`.
     #[cfg(feature = "alloc")]
-    fn macro_created_sha256t_hash_type_backward() {
-        assert_eq!(NewTypeHashBackward::hash(&[0]).to_string(), HASH_ZERO_BACKWARD);
-    }
+    pub type TestHash = sha256t::Hash<TestHashTag>;
 
-    // We also provide a macro to create the tag and the hash type.
     sha256t_hash_newtype! {
         /// Test detailed explanation.
-        struct NewTypeTagForward = raw(TEST_MIDSTATE, 64);
+        struct NewTypeTag = raw(TEST_MIDSTATE, 64);
 
         /// A test hash.
-        #[hash_newtype(forward)]
-        struct NewTypeHashForward(_);
+        #[hash_newtype(backward)]
+        struct NewTypeHash(_);
     }
 
     #[test]
     #[cfg(feature = "alloc")]
-    fn macro_created_sha256t_hash_type_prints_forward() {
-        assert_eq!(NewTypeHashForward::hash(&[0]).to_string(), HASH_ZERO_FORWARD);
+    fn test_sha256t() {
+        assert_eq!(
+            TestHash::hash(&[0]).to_string(),
+            "29589d5122ec666ab5b4695070b6debc63881a4f85d88d93ddc90078038213ed"
+        );
+        assert_eq!(
+            NewTypeHash::hash(&[0]).to_string(),
+            "29589d5122ec666ab5b4695070b6debc63881a4f85d88d93ddc90078038213ed"
+        );
     }
 }
diff --git a/internals/src/macros.rs b/internals/src/macros.rs
index 7ec8e244..0003e42f 100644
--- a/internals/src/macros.rs
+++ b/internals/src/macros.rs
@@ -8,6 +8,34 @@
 macro_rules! impl_array_newtype {
     ($thing:ident, $ty:ty, $len:literal) => {
         impl $thing {
+            /// Creates `Self` by wrapping `bytes`.
+            #[inline]
+            pub fn from_byte_array(bytes: [u8; $len]) -> Self { Self(bytes) }
+
+            /// Returns a reference the underlying byte array.
+            #[inline]
+            pub fn as_byte_array(&self) -> &[u8; $len] { &self.0 }
+
+            /// Returns the underlying byte array.
+            #[inline]
+            pub fn to_byte_array(self) -> [u8; $len] {
+                // We rely on `Copy` being implemented for $thing so conversion
+                // methods use the correct Rust naming conventions.
+                fn check_copy<T: Copy>() {}
+                check_copy::<$thing>();
+
+                self.0
+            }
+
+            /// Returns a slice of the underlying bytes.
+            #[inline]
+            pub fn as_bytes(&self) -> &[u8] { &self.0 }
+
+            /// Copies the underlying bytes into a new `Vec`.
+            #[cfg(feature = "alloc")]
+            #[inline]
+            pub fn to_bytes(&self) -> alloc::vec::Vec<u8> { self.0.to_vec() }
+
             /// Converts the object to a raw pointer.
             #[inline]
             pub fn as_ptr(&self) -> *const $ty {


Test patch:
diff --git a/base58/contrib/test_vars.sh b/base58/contrib/test_vars.sh
index c456fe0d..0c25edc7 100644
--- a/base58/contrib/test_vars.sh
+++ b/base58/contrib/test_vars.sh
@@ -1,6 +1,4 @@
 #!/usr/bin/env bash
-# disable verify unused vars, despite the fact that they are used when sourced
-# shellcheck disable=SC2034
 
 # Test all these features with "std" enabled.
 FEATURES_WITH_STD=""
diff --git a/bitcoin/contrib/test_vars.sh b/bitcoin/contrib/test_vars.sh
index cab77a50..e1d02314 100644
--- a/bitcoin/contrib/test_vars.sh
+++ b/bitcoin/contrib/test_vars.sh
@@ -1,6 +1,4 @@
 #!/usr/bin/env bash
-# disable verify unused vars, despite the fact that they are used when sourced
-# shellcheck disable=SC2034
 
 # Test all these features with "std" enabled.
 FEATURES_WITH_STD="rand-std serde secp-recovery bitcoinconsensus-std base64 ordered"
diff --git a/bitcoin/tests/bip_174.rs b/bitcoin/tests/bip_174.rs
index 522360c6..fa32a0ce 100644
--- a/bitcoin/tests/bip_174.rs
+++ b/bitcoin/tests/bip_174.rs
@@ -317,7 +317,8 @@ fn parse_and_verify_keys(
 
         let path =
             derivation_path.into_derivation_path().expect("failed to convert derivation path");
-        let derived_priv = ext_priv.derive_priv(secp, &path).to_priv();
+        let derived_priv =
+            ext_priv.derive_priv(secp, &path).expect("failed to derive ext priv key").to_priv();
         assert_eq!(wif_priv, derived_priv);
         let derived_pub = derived_priv.public_key(secp);
         key_map.insert(derived_pub, derived_priv);
diff --git a/contrib/test_vars.sh b/contrib/test_vars.sh
index c490c367..3f96f89e 100644
--- a/contrib/test_vars.sh
+++ b/contrib/test_vars.sh
@@ -1,11 +1,4 @@
 #!/usr/bin/env bash
-#
-# Used by labeler.yaml
-#
-# Not to be confused with the per crate `test_vars.sh` used by
-# `rust-bitcoin-maintainer-tools-run_task.sh`.
-# disable verify unused vars, despite the fact that they are used when sourced
-# shellcheck disable=SC2034
 
-CRATES="$(cargo metadata --no-deps --format-version 1 | jq -j -r '.packages | map(.manifest_path | rtrimstr("/Cargo.toml") | ltrimstr("'"$PWD"'/")) | join(" ")')"
+CRATES="`cargo metadata --no-deps --format-version 1 | jq -j -r '.packages | map(.manifest_path | rtrimstr("/Cargo.toml") | ltrimstr("'$PWD'/")) | join(" ")'`"
 DEPS="recent minimal"
diff --git a/hashes/contrib/test_vars.sh b/hashes/contrib/test_vars.sh
index 4020401c..5e89c21e 100644
--- a/hashes/contrib/test_vars.sh
+++ b/hashes/contrib/test_vars.sh
@@ -1,6 +1,4 @@
 #!/usr/bin/env bash
-# disable verify unused vars, despite the fact that they are used when sourced
-# shellcheck disable=SC2034
 
 # Test all these features with "std" enabled.
 FEATURES_WITH_STD="io serde small-hash schemars"
@@ -8,5 +6,8 @@ FEATURES_WITH_STD="io serde small-hash schemars"
 # Test all these features without "std" enabled.
 FEATURES_WITHOUT_STD="alloc serde small-hash"
 
+# Run address sanitizer with these features.
+ASAN_FEATURES="std io serde"
+
 # Run these examples.
 EXAMPLES=""
diff --git a/internals/contrib/test_vars.sh b/internals/contrib/test_vars.sh
index c1731775..759f5afb 100644
--- a/internals/contrib/test_vars.sh
+++ b/internals/contrib/test_vars.sh
@@ -1,6 +1,4 @@
 #!/usr/bin/env bash
-# disable verify unused vars, despite the fact that they are used when sourced
-# shellcheck disable=SC2034
 
 # Test all these features with "std" enabled.
 FEATURES_WITH_STD="serde"
diff --git a/io/contrib/test_vars.sh b/io/contrib/test_vars.sh
index df392afa..0317fc3b 100644
--- a/io/contrib/test_vars.sh
+++ b/io/contrib/test_vars.sh
@@ -1,6 +1,4 @@
 #!/usr/bin/env bash
-# disable verify unused vars, despite the fact that they are used when sourced
-# shellcheck disable=SC2034
 
 # Test all these features with "std" enabled.
 FEATURES_WITH_STD=""
diff --git a/units/contrib/test_vars.sh b/units/contrib/test_vars.sh
index c1731775..759f5afb 100644
--- a/units/contrib/test_vars.sh
+++ b/units/contrib/test_vars.sh
@@ -1,6 +1,4 @@
 #!/usr/bin/env bash
-# disable verify unused vars, despite the fact that they are used when sourced
-# shellcheck disable=SC2034
 
 # Test all these features with "std" enabled.
 FEATURES_WITH_STD="serde"