• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 21153500862

19 Jan 2026 10:49PM UTC coverage: 82.793% (-0.6%) from 83.357%
21153500862

Pull #1232

github

web-flow
Merge c7444e8de into 93d4888c6
Pull Request #1232: Unified payjoin service

291 of 413 new or added lines in 12 files covered. (70.46%)

41 existing lines in 3 files now uncovered.

10133 of 12239 relevant lines covered (82.79%)

430.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.4
/payjoin-test-utils/src/lib.rs
1
use std::result::Result;
2
use std::str::FromStr;
3
use std::sync::Arc;
4
use std::time::Duration;
5

6
use axum_server::tls_rustls::RustlsConfig;
7
use bitcoin::{Amount, Psbt};
8
pub use corepc_node; // re-export for convenience
9
use corepc_node::AddressType;
10
use http::StatusCode;
11
use ohttp::hpke::{Aead, Kdf, Kem};
12
use ohttp::{KeyId, SymmetricSuite};
13
use once_cell::sync::{Lazy, OnceCell};
14
use payjoin::io::{fetch_ohttp_keys_with_cert, Error as IOError};
15
use payjoin::OhttpKeys;
16
use rcgen::Certificate;
17
use reqwest::{Client, ClientBuilder};
18
use rustls::pki_types::CertificateDer;
19
use rustls::RootCertStore;
20
use tempfile::tempdir;
21
use tokio::task::JoinHandle;
22
use tracing::Level;
23
use tracing_subscriber::{EnvFilter, FmtSubscriber};
24

25
pub type BoxError = Box<dyn std::error::Error + 'static>;
26
pub type BoxSendSyncError = Box<dyn std::error::Error + Send + Sync>;
27

28
pub use payjoin::persist::test_utils::InMemoryTestPersister;
29
pub use payjoin::persist::SessionPersister;
30

31
static INIT_TRACING: OnceCell<()> = OnceCell::new();
32

33
pub fn init_tracing() {
17✔
34
    INIT_TRACING.get_or_init(|| {
17✔
35
        let subscriber = FmtSubscriber::builder()
2✔
36
            .with_env_filter(EnvFilter::from_default_env())
2✔
37
            .with_test_writer()
2✔
38
            .finish();
2✔
39

40
        tracing::subscriber::set_global_default(subscriber)
2✔
41
            .expect("failed to set global default subscriber");
2✔
42
    });
2✔
43
}
17✔
44

45
pub struct TestServices {
46
    cert: Certificate,
47
    directory: (u16, Option<JoinHandle<Result<(), BoxSendSyncError>>>),
48
    ohttp_relay: (u16, Option<JoinHandle<Result<(), BoxSendSyncError>>>),
49
    http_agent: Arc<Client>,
50
}
51

52
impl TestServices {
53
    pub async fn initialize() -> Result<Self, BoxSendSyncError> {
10✔
54
        // TODO add a UUID, and cleanup guard to delete after on successful run
55
        let cert = local_cert_key();
10✔
56
        let cert_der = cert.cert.der().to_vec();
10✔
57
        let key_der = cert.signing_key.serialize_der();
10✔
58
        let cert_key = (cert_der.clone(), key_der);
10✔
59

60
        let mut root_store = RootCertStore::empty();
10✔
61
        root_store.add(CertificateDer::from(cert.cert.der().to_vec())).unwrap();
10✔
62

63
        let directory = init_directory(cert_key, root_store.clone()).await?;
10✔
64
        let ohttp_relay = init_ohttp_relay(root_store).await?;
10✔
65

66
        let http_agent: Arc<Client> = Arc::new(http_agent(cert_der)?);
10✔
67

68
        Ok(Self {
10✔
69
            cert: cert.cert,
10✔
70
            directory: (directory.0, Some(directory.1)),
10✔
71
            ohttp_relay: (ohttp_relay.0, Some(ohttp_relay.1)),
10✔
72
            http_agent,
10✔
73
        })
10✔
74
    }
10✔
75

76
    pub fn cert(&self) -> Vec<u8> { self.cert.der().to_vec() }
10✔
77

78
    pub fn directory_url(&self) -> String { format!("https://localhost:{}", self.directory.0) }
27✔
79

80
    pub fn take_directory_handle(&mut self) -> JoinHandle<Result<(), BoxSendSyncError>> {
9✔
81
        self.directory.1.take().expect("directory handle not found")
9✔
82
    }
9✔
83

84
    pub fn ohttp_relay_url(&self) -> String { format!("http://localhost:{}", self.ohttp_relay.0) }
49✔
85

86
    pub fn ohttp_gateway_url(&self) -> String {
×
87
        format!("{}/.well-known/ohttp-gateway", self.directory_url())
×
88
    }
×
89

90
    pub fn take_ohttp_relay_handle(&mut self) -> JoinHandle<Result<(), BoxSendSyncError>> {
8✔
91
        self.ohttp_relay.1.take().expect("ohttp relay handle not found")
8✔
92
    }
8✔
93

94
    pub fn http_agent(&self) -> Arc<Client> { self.http_agent.clone() }
27✔
95

96
    pub async fn wait_for_services_ready(&self) -> Result<(), &'static str> {
10✔
97
        wait_for_service_ready(&self.ohttp_relay_url(), self.http_agent()).await?;
10✔
98
        wait_for_service_ready(&self.directory_url(), self.http_agent()).await?;
10✔
99
        Ok(())
10✔
100
    }
10✔
101

102
    pub async fn fetch_ohttp_keys(&self) -> Result<OhttpKeys, IOError> {
8✔
103
        fetch_ohttp_keys_with_cert(
8✔
104
            self.ohttp_relay_url().as_str(),
8✔
105
            self.directory_url().as_str(),
8✔
106
            self.cert(),
8✔
107
        )
8✔
108
        .await
8✔
109
    }
8✔
110
}
111

112
pub async fn init_directory(
10✔
113
    local_cert_key: (Vec<u8>, Vec<u8>),
10✔
114
    root_store: RootCertStore,
10✔
115
) -> std::result::Result<
10✔
116
    (u16, tokio::task::JoinHandle<std::result::Result<(), BoxSendSyncError>>),
10✔
117
    BoxSendSyncError,
10✔
118
> {
10✔
119
    let tempdir = tempdir()?;
10✔
120
    let config = payjoin_service::config::Config {
10✔
121
        port: 0, // let OS assign a free port
10✔
122
        storage_dir: tempdir.path().to_path_buf(),
10✔
123
        timeout: Duration::from_secs(2),
10✔
124
    };
10✔
125

126
    let tls_config = RustlsConfig::from_der(vec![local_cert_key.0], local_cert_key.1).await?;
10✔
127

128
    let (port, handle) = payjoin_service::serve_manual_tls(config, Some(tls_config), root_store)
10✔
129
        .await
10✔
130
        .map_err(|e| e.to_string())?;
10✔
131

132
    let handle = tokio::spawn(async move {
10✔
133
        let _tempdir = tempdir; // keep the tempdir until the directory shuts down
10✔
134
        handle.await.map_err(|e| e.to_string())?.map_err(|e| e.to_string().into())
10✔
135
    });
×
136

137
    Ok((port, handle))
10✔
138
}
10✔
139

140
async fn init_ohttp_relay(
10✔
141
    root_store: RootCertStore,
10✔
142
) -> std::result::Result<
10✔
143
    (u16, tokio::task::JoinHandle<std::result::Result<(), BoxSendSyncError>>),
10✔
144
    BoxSendSyncError,
10✔
145
> {
10✔
146
    let tempdir = tempdir()?;
10✔
147
    let config = payjoin_service::config::Config {
10✔
148
        port: 0, // let OS assign a free port
10✔
149
        storage_dir: tempdir.path().to_path_buf(),
10✔
150
        timeout: Duration::from_secs(2),
10✔
151
    };
10✔
152

153
    let (port, handle) = payjoin_service::serve_manual_tls(config, None, root_store)
10✔
154
        .await
10✔
155
        .map_err(|e| e.to_string())?;
10✔
156

157
    let handle = tokio::spawn(async move {
10✔
158
        let _tempdir = tempdir; // keep the tempdir until the relay shuts down
10✔
159
        handle.await.map_err(|e| e.to_string())?.map_err(|e| e.to_string().into())
10✔
NEW
160
    });
×
161

162
    Ok((port, handle))
10✔
163
}
10✔
164

165
/// generate or get a DER encoded localhost cert and key.
166
pub fn local_cert_key() -> rcgen::CertifiedKey<rcgen::KeyPair> {
15✔
167
    rcgen::generate_simple_self_signed(vec!["0.0.0.0".to_string(), "localhost".to_string()])
15✔
168
        .expect("Failed to generate cert")
15✔
169
}
15✔
170

171
pub fn init_bitcoind() -> Result<corepc_node::Node, BoxError> {
19✔
172
    let bitcoind_exe = corepc_node::exe_path()?;
19✔
173
    let mut conf = corepc_node::Conf::default();
19✔
174
    conf.view_stdout = tracing::enabled!(target: "corepc", Level::TRACE);
19✔
175
    // conf.args.push("-txindex");
176
    let bitcoind = corepc_node::Node::with_conf(bitcoind_exe, &conf)?;
19✔
177
    Ok(bitcoind)
19✔
178
}
19✔
179

180
pub fn init_bitcoind_sender_receiver(
19✔
181
    sender_address_type: Option<AddressType>,
19✔
182
    receiver_address_type: Option<AddressType>,
19✔
183
) -> Result<(corepc_node::Node, corepc_node::Client, corepc_node::Client), BoxError> {
19✔
184
    let bitcoind = init_bitcoind()?;
19✔
185
    let mut wallets = create_and_fund_wallets(
19✔
186
        &bitcoind,
19✔
187
        vec![("receiver", receiver_address_type), ("sender", sender_address_type)],
19✔
188
    )?;
×
189
    let receiver = wallets.pop().expect("receiver to exist");
19✔
190
    let sender = wallets.pop().expect("sender to exist");
19✔
191

192
    Ok((bitcoind, receiver, sender))
19✔
193
}
19✔
194

195
fn create_and_fund_wallets<W: AsRef<str>>(
19✔
196
    bitcoind: &corepc_node::Node,
19✔
197
    wallets: Vec<(W, Option<AddressType>)>,
19✔
198
) -> Result<Vec<corepc_node::Client>, BoxError> {
19✔
199
    let mut funded_wallets = vec![];
19✔
200
    let funding_wallet = bitcoind.create_wallet("funding_wallet")?;
19✔
201
    let funding_address = funding_wallet.new_address()?;
19✔
202
    // 100 blocks would work here, we add a extra block to cover fees between transfers
203
    bitcoind.client.generate_to_address(101 + wallets.len(), &funding_address)?;
19✔
204
    for (wallet_name, address_type) in wallets {
57✔
205
        let wallet = bitcoind.create_wallet(wallet_name)?;
38✔
206
        let address = wallet.get_new_address(None, address_type)?.into_model()?.0.assume_checked();
38✔
207
        funding_wallet.send_to_address(&address, Amount::from_btc(50.0)?)?;
38✔
208
        funded_wallets.push(wallet);
38✔
209
    }
210
    // Mine the block which funds the different wallets
211
    bitcoind.client.generate_to_address(1, &funding_address)?;
19✔
212

213
    for wallet in funded_wallets.iter() {
38✔
214
        let balances = wallet.get_balances()?.into_model()?;
38✔
215
        assert_eq!(
38✔
216
            balances.mine.trusted,
217
            Amount::from_btc(50.0)?,
38✔
218
            "wallet doesn't have expected amount of bitcoin"
×
219
        );
220
    }
221

222
    Ok(funded_wallets)
19✔
223
}
19✔
224

225
pub fn http_agent(cert_der: Vec<u8>) -> Result<Client, BoxSendSyncError> {
12✔
226
    Ok(http_agent_builder(cert_der).build()?)
12✔
227
}
12✔
228

229
pub fn init_bitcoind_multi_sender_single_reciever(
×
230
    number_of_senders: usize,
×
231
) -> Result<(corepc_node::Node, Vec<corepc_node::Client>, corepc_node::Client), BoxError> {
×
232
    let bitcoind = init_bitcoind()?;
×
233
    let wallets_to_create =
×
234
        (0..number_of_senders + 1).map(|i| (format!("sender_{i}"), None)).collect::<Vec<_>>();
×
235
    let mut wallets = create_and_fund_wallets(&bitcoind, wallets_to_create)?;
×
236
    let receiver = wallets.pop().expect("receiver to exist");
×
237
    let senders = wallets;
×
238

239
    Ok((bitcoind, senders, receiver))
×
240
}
×
241

242
fn http_agent_builder(cert_der: Vec<u8>) -> ClientBuilder {
12✔
243
    ClientBuilder::new().http1_only().use_rustls_tls().add_root_certificate(
12✔
244
        reqwest::tls::Certificate::from_der(cert_der.as_slice())
12✔
245
            .expect("cert_der should be a valid DER-encoded certificate"),
12✔
246
    )
247
}
12✔
248

249
const TESTS_TIMEOUT: Duration = Duration::from_secs(20);
250
const WAIT_SERVICE_INTERVAL: Duration = Duration::from_secs(3);
251

252
pub async fn wait_for_service_ready(
23✔
253
    service_url: &str,
23✔
254
    agent: Arc<Client>,
23✔
255
) -> Result<(), &'static str> {
23✔
256
    let health_url = format!("{}/health", service_url.trim_end_matches("/"));
23✔
257
    let start = std::time::Instant::now();
23✔
258

259
    while start.elapsed() < TESTS_TIMEOUT {
23✔
260
        let request_result =
23✔
261
            agent.get(health_url.clone()).send().await.map_err(|_| "Bad request")?;
23✔
262
        match request_result.status() {
23✔
263
            StatusCode::OK => return Ok(()),
23✔
264
            StatusCode::NOT_FOUND => return Err("Endpoint not found"),
×
265
            _ => std::thread::sleep(WAIT_SERVICE_INTERVAL),
×
266
        }
267
    }
268

269
    Err("Timeout waiting for service to be ready")
×
270
}
23✔
271

272
pub static EXAMPLE_URL: &str = "https://example.com";
273

274
pub const KEY_ID: KeyId = 1;
275
pub const KEM: Kem = Kem::K256Sha256;
276
pub const SYMMETRIC: &[SymmetricSuite] =
277
    &[ohttp::SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
278

279
// https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-span_idtestvectorsspanTest_vectors
280
// | InputScriptType | Original PSBT Fee rate | maxadditionalfeecontribution | additionalfeeoutputindex|
281
// |-----------------|-----------------------|------------------------------|-------------------------|
282
// | P2SH-P2WPKH     |  2 sat/vbyte          | 0.00000182                   | 0                       |
283

284
pub const QUERY_PARAMS: &str = "maxadditionalfeecontribution=182&additionalfeeoutputindex=0";
285

286
/// From the BIP-78 test vector
287
pub const ORIGINAL_PSBT: &str = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
288

289
/// From the BIP-174 test vector
290
pub const INVALID_PSBT: &str = "AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA&#61;&#61;";
291

292
/// From the BIP-78 test vector
293
pub const PAYJOIN_PROPOSAL: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcAAQEggIQeAAAAAAAXqRTI8sv5ymFHLIjkZNRrNXSEXZHY1YcBBxcWABRfgGZV5ZJMkgTC1RvlOU9L+e2iEAEIawJHMEQCIGe7e0DfJaVPRYEKWxddL2Pr0G37BoKz0lyNa02O2/tWAiB7ZVgBoF4s8MHocYWWmo4Q1cyV2wl7MX0azlqa8NBENAEhAmXWPPW0G3yE3HajBOb7gO7iKzHSmZ0o0w0iONowcV+tAAAA";
294

295
/// From the BIP-78 test vector
296
pub const PAYJOIN_PROPOSAL_WITH_SENDER_INFO: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA=";
297

298
/// Input contribution for the receiver, from the BIP78 test vector
299
pub const RECEIVER_INPUT_CONTRIBUTION: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==";
300

301
pub static PARSED_ORIGINAL_PSBT: Lazy<Psbt> =
302
    Lazy::new(|| Psbt::from_str(ORIGINAL_PSBT).expect("known psbt should parse"));
1✔
303

304
pub static PARSED_PAYJOIN_PROPOSAL: Lazy<Psbt> =
305
    Lazy::new(|| Psbt::from_str(PAYJOIN_PROPOSAL).expect("known psbt should parse"));
1✔
306

307
pub static PARSED_PAYJOIN_PROPOSAL_WITH_SENDER_INFO: Lazy<Psbt> = Lazy::new(|| {
1✔
308
    Psbt::from_str(PAYJOIN_PROPOSAL_WITH_SENDER_INFO).expect("known psbt should parse")
1✔
309
});
1✔
310

311
pub const MULTIPARTY_ORIGINAL_PSBT_ONE: &str = "cHNidP8BAMMCAAAAA9cTcqYjSnJdteUn3Re12vFuVb5xPypYheHze74WBW8EAAAAAAD+////6JHS6JgYdav7tr+Q5bWk95C2aA4AudqZnjeRF0hx33gBAAAAAP7///+d/1mRjJ6snVe9o0EfMM8bIExdlwfaig1j/JnUupqIWwAAAAAA/v///wJO4wtUAgAAABYAFGAEc6cWf7z5a97Xxulg+S7JZyg/kvEFKgEAAAAWABQexWFsGEdXxYNTJLJ1qBCO0nXELwAAAAAAAQCaAgAAAAKa/zFrWjicRDNVCehGj/2jdXGKuZLMzLNUo+uj7PBW8wAAAAAA/v///4HkRlmU9FA3sHckkCQMjGrabNpgAw37XaqUItNFjR5rAAAAAAD+////AgDyBSoBAAAAFgAUGPI/E9xCHmaL6DIsjkaD8LX2mpLg6QUqAQAAABYAFH6MyygysrAmk/nunEFuGGDLf244aAAAAAEBHwDyBSoBAAAAFgAUGPI/E9xCHmaL6DIsjkaD8LX2mpIBCGsCRzBEAiBLJUhV7tja6FdELXDcB6Q3Gd+BMBpkjdHpj3DLnbL6AAIgJmFl3kpWBzkO8yDDl73BMMalSlAOQvfY+hqFRos5DhQBIQLfeq7CCftNEUHcRLZniJOqcgKZEqPc1A40BnEiZHj3FQABAJoCAAAAAp3/WZGMnqydV72jQR8wzxsgTF2XB9qKDWP8mdS6mohbAQAAAAD+////1xNypiNKcl215SfdF7Xa8W5VvnE/KliF4fN7vhYFbwQBAAAAAP7///8CoNkFKgEAAAAWABQchAe9uxzqT1EjLx4jgx9u1Mn6QADyBSoBAAAAFgAUye8yXWX0MvouXYhAFb06xX5kADpoAAAAAQEfAPIFKgEAAAAWABTJ7zJdZfQy+i5diEAVvTrFfmQAOgEIawJHMEQCIF7ihY/YtUPUTOaEdJbg0/HiwKunK398BI67/LknPGqMAiBHBXmL6gTP8PxEGeWswk6T0tCI2Gvwq1zh+wd7h8QCWAEhApM0w2WFlw0eg64Zp3PeyRmxl/7WGHUED8Ul/aX1FiTBAAAAAA==";
312

313
pub const MULTIPARTY_ORIGINAL_PSBT_TWO: &str = "cHNidP8BAMMCAAAAA9cTcqYjSnJdteUn3Re12vFuVb5xPypYheHze74WBW8EAAAAAAD+////6JHS6JgYdav7tr+Q5bWk95C2aA4AudqZnjeRF0hx33gBAAAAAP7///+d/1mRjJ6snVe9o0EfMM8bIExdlwfaig1j/JnUupqIWwAAAAAA/v///wJO4wtUAgAAABYAFGAEc6cWf7z5a97Xxulg+S7JZyg/kvEFKgEAAAAWABQexWFsGEdXxYNTJLJ1qBCO0nXELwAAAAAAAAEAmgIAAAACnf9ZkYyerJ1XvaNBHzDPGyBMXZcH2ooNY/yZ1LqaiFsBAAAAAP7////XE3KmI0pyXbXlJ90XtdrxblW+cT8qWIXh83u+FgVvBAEAAAAA/v///wKg2QUqAQAAABYAFByEB727HOpPUSMvHiODH27UyfpAAPIFKgEAAAAWABTJ7zJdZfQy+i5diEAVvTrFfmQAOmgAAAABAR8A8gUqAQAAABYAFMnvMl1l9DL6Ll2IQBW9OsV+ZAA6AQhrAkcwRAIgXuKFj9i1Q9RM5oR0luDT8eLAq6crf3wEjrv8uSc8aowCIEcFeYvqBM/w/EQZ5azCTpPS0IjYa/CrXOH7B3uHxAJYASECkzTDZYWXDR6Drhmnc97JGbGX/tYYdQQPxSX9pfUWJMEAAQCaAgAAAAIKOB8lY4eoEupDnxviz0/nAuR2biNFKbdkvckiW5ioPAAAAAAA/v///w0g/mj67592vy29xhnZMGeVtEXN1jD4lU/SMZM8oStqAAAAAAD+////AgDyBSoBAAAAFgAUC3r4YzVSpsWp3knMxbgWIx2R36/g6QUqAQAAABYAFMGlw3hwcx1b+KQGWIfOUxzwrwWkaAAAAAEBHwDyBSoBAAAAFgAUC3r4YzVSpsWp3knMxbgWIx2R368BCGsCRzBEAiA//JH+jonzbzqnKI0Uti16iJcdsXI+6Zu0IAZKlOq6AwIgP0XawCCH73uXKilFqSXQQQrBvmi/Sx44D/A+/MQ/mJYBIQIekOyEpJKpFQd7eHuY6Vt4Qlf0+00Wp529I23hl/EpcQAAAA==";
314

315
/// sha256d of the empty string
316
#[rustfmt::skip]
317
pub const DUMMY32: [u8; 32] = [
318
    0x5d, 0xf6, 0xe0, 0xe2, 0x76, 0x13, 0x59, 0xd3,
319
    0x0a, 0x82, 0x75, 0x05, 0x8e, 0x29, 0x9f, 0xcc,
320
    0x03, 0x81, 0x53, 0x45, 0x45, 0xf5, 0x5c, 0xf4,
321
    0x3e, 0x41, 0x98, 0x3f, 0x5d, 0x4c, 0x94, 0x56,
322
];
323
/// hash160 of the empty string
324
#[rustfmt::skip]
325
pub const DUMMY20: [u8; 20] = [
326
    0xb4, 0x72, 0xa2, 0x66, 0xd0, 0xbd, 0x89, 0xc1, 0x37, 0x06,
327
    0xa4, 0x13, 0x2c, 0xcf, 0xb1, 0x6f, 0x7c, 0x3b, 0x9f, 0xcb,
328
];
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc