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

stacks-network / stacks-core / 25903914664-1

15 May 2026 06:28AM UTC coverage: 47.122% (-38.8%) from 85.959%
25903914664-1

Pull #7199

github

94e391
web-flow
Merge 109f2828c into 1c7b8e6ac
Pull Request #7199: Feat: L1 and L2 early unlocks, updating signer

103343 of 219309 relevant lines covered (47.12%)

12880462.62 hits per line

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

25.75
/stackslib/src/util_lib/signed_structured_data.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2026 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17
use clarity::vm::types::TupleData;
18
use clarity::vm::{ClarityName, Value};
19
use stacks_common::types::chainstate::StacksPrivateKey;
20
use stacks_common::types::PrivateKey;
21
use stacks_common::util::hash::Sha256Sum;
22
use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PrivateKey};
23

24
use crate::chainstate::stacks::address::PoxAddress;
25

26
/// Message prefix for signed structured data. "SIP018" in ascii
27
pub const STRUCTURED_DATA_PREFIX: [u8; 6] = [0x53, 0x49, 0x50, 0x30, 0x31, 0x38];
28

29
pub fn structured_data_hash(value: Value) -> Sha256Sum {
151,566✔
30
    let mut bytes = vec![];
151,566✔
31
    value.serialize_write(&mut bytes).unwrap();
151,566✔
32
    Sha256Sum::from_data(bytes.as_slice())
151,566✔
33
}
151,566✔
34

35
/// Generate a message hash for signing structured Clarity data.
36
/// Reference [SIP018](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md) for more information.
37
pub fn structured_data_message_hash(structured_data: Value, domain: Value) -> Sha256Sum {
75,783✔
38
    let message = [
75,783✔
39
        STRUCTURED_DATA_PREFIX.as_ref(),
75,783✔
40
        structured_data_hash(domain).as_bytes(),
75,783✔
41
        structured_data_hash(structured_data).as_bytes(),
75,783✔
42
    ]
75,783✔
43
    .concat();
75,783✔
44

45
    Sha256Sum::from_data(&message)
75,783✔
46
}
75,783✔
47

48
/// Sign structured Clarity data with a given private key.
49
/// Reference [SIP018](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md) for more information.
50
pub fn sign_structured_data(
×
51
    structured_data: Value,
×
52
    domain: Value,
×
53
    private_key: &Secp256k1PrivateKey,
×
54
) -> Result<MessageSignature, &str> {
×
55
    let msg_hash = structured_data_message_hash(structured_data, domain);
×
56
    private_key.sign(msg_hash.as_bytes())
×
57
}
×
58

59
// Helper function to generate domain for structured data hash
60
pub fn make_structured_data_domain(name: &str, version: &str, chain_id: u32) -> Value {
75,783✔
61
    Value::Tuple(
75,783✔
62
        TupleData::from_data(vec![
75,783✔
63
            (
75,783✔
64
                ClarityName::from_literal("name"),
75,783✔
65
                Value::string_ascii_from_bytes(name.into()).unwrap(),
75,783✔
66
            ),
75,783✔
67
            (
75,783✔
68
                ClarityName::from_literal("version"),
75,783✔
69
                Value::string_ascii_from_bytes(version.into()).unwrap(),
75,783✔
70
            ),
75,783✔
71
            (
75,783✔
72
                ClarityName::from_literal("chain-id"),
75,783✔
73
                Value::UInt(chain_id.into()),
75,783✔
74
            ),
75,783✔
75
        ])
75,783✔
76
        .unwrap(),
75,783✔
77
    )
75,783✔
78
}
75,783✔
79

80
pub mod pox4 {
81
    use clarity::vm::ClarityName;
82

83
    use super::{
84
        make_structured_data_domain, structured_data_message_hash, MessageSignature, PoxAddress,
85
        PrivateKey, Sha256Sum, StacksPrivateKey, TupleData, Value,
86
    };
87
    define_named_enum!(Pox4SignatureTopic {
88
        StackStx("stack-stx"),
89
        AggregationCommit("agg-commit"),
90
        AggregationIncrease("agg-increase"),
91
        StackExtend("stack-extend"),
92
        StackIncrease("stack-increase"),
93
    });
94

95
    pub fn make_pox_4_signed_data_domain(chain_id: u32) -> Value {
8,148✔
96
        make_structured_data_domain("pox-4-signer", "1.0.0", chain_id)
8,148✔
97
    }
8,148✔
98

99
    #[cfg_attr(test, mutants::skip)]
100
    pub fn make_pox_4_signer_key_message_hash(
8,148✔
101
        pox_addr: &PoxAddress,
8,148✔
102
        reward_cycle: u128,
8,148✔
103
        topic: &Pox4SignatureTopic,
8,148✔
104
        chain_id: u32,
8,148✔
105
        period: u128,
8,148✔
106
        max_amount: u128,
8,148✔
107
        auth_id: u128,
8,148✔
108
    ) -> Sha256Sum {
8,148✔
109
        let domain_tuple = make_pox_4_signed_data_domain(chain_id);
8,148✔
110
        let data_tuple = Value::Tuple(
8,148✔
111
            TupleData::from_data(vec![
8,148✔
112
                (
8,148✔
113
                    ClarityName::from_literal("pox-addr"),
8,148✔
114
                    pox_addr
8,148✔
115
                        .clone()
8,148✔
116
                        .as_clarity_tuple()
8,148✔
117
                        .expect("Error creating signature hash - invalid PoX Address")
8,148✔
118
                        .into(),
8,148✔
119
                ),
8,148✔
120
                (
8,148✔
121
                    ClarityName::from_literal("reward-cycle"),
8,148✔
122
                    Value::UInt(reward_cycle),
8,148✔
123
                ),
8,148✔
124
                (ClarityName::from_literal("period"), Value::UInt(period)),
8,148✔
125
                (
8,148✔
126
                    ClarityName::from_literal("topic"),
8,148✔
127
                    Value::string_ascii_from_bytes(topic.get_name_str().into()).unwrap(),
8,148✔
128
                ),
8,148✔
129
                (ClarityName::from_literal("auth-id"), Value::UInt(auth_id)),
8,148✔
130
                (
8,148✔
131
                    ClarityName::from_literal("max-amount"),
8,148✔
132
                    Value::UInt(max_amount),
8,148✔
133
                ),
8,148✔
134
            ])
8,148✔
135
            .expect("Error creating signature hash"),
8,148✔
136
        );
8,148✔
137
        structured_data_message_hash(data_tuple, domain_tuple)
8,148✔
138
    }
8,148✔
139

140
    impl Into<Pox4SignatureTopic> for &'static str {
141
        #[cfg_attr(test, mutants::skip)]
142
        fn into(self) -> Pox4SignatureTopic {
×
143
            match self {
×
144
                "stack-stx" => Pox4SignatureTopic::StackStx,
×
145
                "agg-commit" => Pox4SignatureTopic::AggregationCommit,
×
146
                "stack-extend" => Pox4SignatureTopic::StackExtend,
×
147
                "stack-increase" => Pox4SignatureTopic::StackIncrease,
×
148
                _ => panic!("Invalid pox-4 signature topic"),
×
149
            }
150
        }
×
151
    }
152

153
    #[cfg_attr(test, mutants::skip)]
154
    pub fn make_pox_4_signer_key_signature(
8,148✔
155
        pox_addr: &PoxAddress,
8,148✔
156
        signer_key: &StacksPrivateKey,
8,148✔
157
        reward_cycle: u128,
8,148✔
158
        topic: &Pox4SignatureTopic,
8,148✔
159
        chain_id: u32,
8,148✔
160
        period: u128,
8,148✔
161
        max_amount: u128,
8,148✔
162
        auth_id: u128,
8,148✔
163
    ) -> Result<MessageSignature, &'static str> {
8,148✔
164
        let msg_hash = make_pox_4_signer_key_message_hash(
8,148✔
165
            pox_addr,
8,148✔
166
            reward_cycle,
8,148✔
167
            topic,
8,148✔
168
            chain_id,
8,148✔
169
            period,
8,148✔
170
            max_amount,
8,148✔
171
            auth_id,
8,148✔
172
        );
173
        signer_key.sign(msg_hash.as_bytes())
8,148✔
174
    }
8,148✔
175

176
    #[cfg(test)]
177
    mod tests {
178
        use clarity::vm::clarity::{ClarityConnection, TransactionConnection};
179
        use clarity::vm::costs::LimitedCostTracker;
180
        use clarity::vm::types::PrincipalData;
181
        use clarity::vm::ClarityVersion;
182
        use stacks_common::address::AddressHashMode;
183
        use stacks_common::consts::CHAIN_ID_TESTNET;
184
        use stacks_common::types::chainstate::StacksAddress;
185
        use stacks_common::util::hash::to_hex;
186
        use stacks_common::util::secp256k1::Secp256k1PublicKey;
187

188
        use super::*;
189
        use crate::chainstate::stacks::boot::contract_tests::ClarityTestSim;
190
        use crate::chainstate::stacks::boot::{POX_4_CODE, POX_4_NAME};
191
        use crate::util_lib::boot::boot_code_id;
192

193
        fn call_get_signer_message_hash(
×
194
            sim: &mut ClarityTestSim,
×
195
            pox_addr: &PoxAddress,
×
196
            reward_cycle: u128,
×
197
            topic: &Pox4SignatureTopic,
×
198
            lock_period: u128,
×
199
            sender: &PrincipalData,
×
200
            max_amount: u128,
×
201
            auth_id: u128,
×
202
        ) -> Vec<u8> {
×
203
            let pox_contract_id = boot_code_id(POX_4_NAME, false);
×
204
            sim.execute_next_block_as_conn(|conn| {
×
205
                let result = conn.with_readonly_clarity_env(
×
206
                    false,
207
                    CHAIN_ID_TESTNET,
208
                    sender.clone(),
×
209
                    None,
×
210
                    LimitedCostTracker::new_free(),
×
211
                    |exec_state, invoke_ctx| {
×
212
                        let program = format!(
×
213
                            "(get-signer-key-message-hash {} u{} \"{}\" u{} u{} u{})",
214
                            Value::Tuple(pox_addr.clone().as_clarity_tuple().unwrap()), //p
×
215
                            reward_cycle,
216
                            topic.get_name_str(),
×
217
                            lock_period,
218
                            max_amount,
219
                            auth_id,
220
                        );
221
                        exec_state.eval_read_only(invoke_ctx, &pox_contract_id, &program)
×
222
                    },
×
223
                );
224
                result
×
225
                    .expect("FATAL: failed to execute contract call")
×
226
                    .expect_buff(32)
×
227
                    .expect("FATAL: expected buff result")
×
228
            })
×
229
        }
×
230

231
        #[test]
232
        fn test_make_pox_4_message_hash() {
×
233
            let mut sim = ClarityTestSim::new();
×
234
            sim.epoch_bounds = vec![0, 1, 2];
×
235

236
            // Test setup
237
            sim.execute_next_block(|_env| {});
×
238
            sim.execute_next_block(|_env| {});
×
239
            sim.execute_next_block(|_env| {});
×
240

241
            let body = &*POX_4_CODE;
×
242
            let pox_contract_id = boot_code_id(POX_4_NAME, false);
×
243

244
            sim.execute_next_block_as_conn(|conn| {
×
245
                conn.as_transaction(|clarity_db| {
×
246
                    let clarity_version = ClarityVersion::Clarity2;
×
247
                    let (ast, analysis) = clarity_db
×
248
                        .analyze_smart_contract(&pox_contract_id, clarity_version, body)
×
249
                        .unwrap();
×
250
                    clarity_db
×
251
                        .initialize_smart_contract(
×
252
                            &pox_contract_id,
×
253
                            clarity_version,
×
254
                            &ast,
×
255
                            body,
×
256
                            None,
×
257
                            |_, _| None,
258
                            None,
×
259
                        )
260
                        .unwrap();
×
261
                    clarity_db
×
262
                        .save_analysis(&pox_contract_id, &analysis)
×
263
                        .expect("FATAL: failed to store contract analysis");
×
264
                });
×
265
            });
×
266

267
            let pubkey = Secp256k1PublicKey::new();
×
268
            let stacks_addr = StacksAddress::p2pkh(false, &pubkey);
×
269
            let pubkey = Secp256k1PublicKey::new();
×
270
            let principal = PrincipalData::from(stacks_addr.clone());
×
271
            let pox_addr = PoxAddress::standard_burn_address(false);
×
272
            let reward_cycle: u128 = 1;
×
273
            let topic = Pox4SignatureTopic::StackStx;
×
274
            let lock_period = 12;
×
275
            let auth_id = 111;
×
276
            let max_amount = u128::MAX;
×
277

278
            let expected_hash_vec = make_pox_4_signer_key_message_hash(
×
279
                &pox_addr,
×
280
                reward_cycle,
×
281
                &Pox4SignatureTopic::StackStx,
×
282
                CHAIN_ID_TESTNET,
283
                lock_period,
×
284
                max_amount,
×
285
                auth_id,
×
286
            );
287
            let expected_hash = expected_hash_vec.as_bytes();
×
288

289
            // Test 1: valid result
290

291
            let result = call_get_signer_message_hash(
×
292
                &mut sim,
×
293
                &pox_addr,
×
294
                reward_cycle,
×
295
                &topic,
×
296
                lock_period,
×
297
                &principal,
×
298
                max_amount,
×
299
                auth_id,
×
300
            );
301
            assert_eq!(expected_hash.clone(), result.as_slice());
×
302

303
            // Test 2: invalid pox address
304
            let other_pox_address = PoxAddress::from_legacy(
×
305
                AddressHashMode::SerializeP2PKH,
×
306
                StacksAddress::p2pkh(false, &Secp256k1PublicKey::new())
×
307
                    .destruct()
×
308
                    .1,
×
309
            );
310
            let result = call_get_signer_message_hash(
×
311
                &mut sim,
×
312
                &other_pox_address,
×
313
                reward_cycle,
×
314
                &topic,
×
315
                lock_period,
×
316
                &principal,
×
317
                max_amount,
×
318
                auth_id,
×
319
            );
320
            assert_ne!(expected_hash.clone(), result.as_slice());
×
321

322
            // Test 3: invalid reward cycle
323
            let result = call_get_signer_message_hash(
×
324
                &mut sim,
×
325
                &pox_addr,
×
326
                0,
327
                &topic,
×
328
                lock_period,
×
329
                &principal,
×
330
                max_amount,
×
331
                auth_id,
×
332
            );
333
            assert_ne!(expected_hash.clone(), result.as_slice());
×
334

335
            // Test 4: invalid topic
336
            let result = call_get_signer_message_hash(
×
337
                &mut sim,
×
338
                &pox_addr,
×
339
                reward_cycle,
×
340
                &Pox4SignatureTopic::AggregationCommit,
×
341
                lock_period,
×
342
                &principal,
×
343
                max_amount,
×
344
                auth_id,
×
345
            );
346
            assert_ne!(expected_hash.clone(), result.as_slice());
×
347

348
            // Test 5: invalid lock period
349
            let result = call_get_signer_message_hash(
×
350
                &mut sim,
×
351
                &pox_addr,
×
352
                reward_cycle,
×
353
                &topic,
×
354
                0,
355
                &principal,
×
356
                max_amount,
×
357
                auth_id,
×
358
            );
359
            assert_ne!(expected_hash.clone(), result.as_slice());
×
360

361
            // Test 5: invalid max amount
362
            let result = call_get_signer_message_hash(
×
363
                &mut sim,
×
364
                &pox_addr,
×
365
                reward_cycle,
×
366
                &topic,
×
367
                lock_period,
×
368
                &principal,
×
369
                1010101,
370
                auth_id,
×
371
            );
372
            assert_ne!(expected_hash.clone(), result.as_slice());
×
373

374
            // Test 6: invalid auth id
375
            let result = call_get_signer_message_hash(
×
376
                &mut sim,
×
377
                &pox_addr,
×
378
                reward_cycle,
×
379
                &topic,
×
380
                lock_period,
×
381
                &principal,
×
382
                max_amount,
×
383
                10101,
384
            );
385
            assert_ne!(expected_hash.clone(), result.as_slice());
×
386
        }
×
387

388
        #[test]
389
        /// Fixture message hash to test against in other libraries
390
        fn test_sig_hash_fixture() {
×
391
            let fixture = "ec5b88aa81a96a6983c26cdba537a13d253425348ffc0ba6b07130869b025a2d";
×
392
            let pox_addr = PoxAddress::standard_burn_address(false);
×
393
            let pubkey_hex = "0206952cd8813a64f7b97144c984015490a8f9c5778e8f928fbc8aa6cbf02f48e6";
×
394
            let pubkey = Secp256k1PublicKey::from_hex(pubkey_hex).unwrap();
×
395
            let reward_cycle: u128 = 1;
×
396
            let lock_period = 12;
×
397
            let auth_id = 111;
×
398
            let max_amount = u128::MAX;
×
399

400
            let message_hash = make_pox_4_signer_key_message_hash(
×
401
                &pox_addr,
×
402
                reward_cycle,
×
403
                &Pox4SignatureTopic::StackStx,
×
404
                CHAIN_ID_TESTNET,
405
                lock_period,
×
406
                max_amount,
×
407
                auth_id,
×
408
            );
409

410
            assert_eq!(to_hex(message_hash.as_bytes()), fixture);
×
411
        }
×
412
    }
413
}
414

415
pub mod pox5 {
416
    use clarity::vm::types::PrincipalData;
417
    use clarity::vm::ClarityName;
418

419
    use super::{
420
        make_structured_data_domain, structured_data_message_hash, MessageSignature, PrivateKey,
421
        Secp256k1PrivateKey, Sha256Sum, TupleData, Value,
422
    };
423

424
    pub fn make_pox_5_signed_data_domain(chain_id: u32) -> Value {
×
425
        make_structured_data_domain("pox-5-signer", "1.0.0", chain_id)
×
426
    }
×
427

428
    /// Compute the hash of the `grant-authorization` message that is signed
429
    /// by a signer key when authorizing a `signer-manager` contract to
430
    /// register the corresponding signer via pox-5's `grant-signer-key`.
431
    pub fn make_pox_5_signer_grant_message_hash(
×
432
        signer_manager: &PrincipalData,
×
433
        auth_id: u128,
×
434
        chain_id: u32,
×
435
    ) -> Sha256Sum {
×
436
        let domain_tuple = make_pox_5_signed_data_domain(chain_id);
×
437
        let data_tuple = Value::Tuple(
×
438
            TupleData::from_data(vec![
×
439
                (
×
440
                    ClarityName::from_literal("topic"),
×
441
                    Value::string_ascii_from_bytes("grant-authorization".into()).unwrap(),
×
442
                ),
×
443
                (
×
444
                    ClarityName::from_literal("signer-manager"),
×
445
                    Value::Principal(signer_manager.clone()),
×
446
                ),
×
447
                (ClarityName::from_literal("auth-id"), Value::UInt(auth_id)),
×
448
            ])
×
449
            .expect("Error creating signature hash"),
×
450
        );
×
451
        structured_data_message_hash(data_tuple, domain_tuple)
×
452
    }
×
453

454
    /// Sign a pox-5 `grant-authorization` message with `signer_key`.
455
    pub fn make_pox_5_signer_grant_signature(
×
456
        signer_manager: &PrincipalData,
×
457
        auth_id: u128,
×
458
        chain_id: u32,
×
459
        signer_key: &Secp256k1PrivateKey,
×
460
    ) -> Result<MessageSignature, &'static str> {
×
461
        let msg_hash = make_pox_5_signer_grant_message_hash(signer_manager, auth_id, chain_id);
×
462
        signer_key.sign(msg_hash.as_bytes())
×
463
    }
×
464
}
465

466
#[cfg(test)]
467
mod test {
468
    use clarity::vm::types::{TupleData, Value};
469
    use stacks_common::consts::CHAIN_ID_MAINNET;
470
    use stacks_common::util::hash::to_hex;
471

472
    use super::*;
473

474
    /// [SIP18 test vectors](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md)
475
    #[test]
476
    fn test_sip18_ref_structured_data_hash() {
×
477
        let value = Value::string_ascii_from_bytes("Hello World".into()).unwrap();
×
478
        let msg_hash = structured_data_hash(value);
×
479
        assert_eq!(
×
480
            to_hex(msg_hash.as_bytes()),
×
481
            "5297eef9765c466d945ad1cb2c81b30b9fed6c165575dc9226e9edf78b8cd9e8"
482
        )
483
    }
×
484

485
    /// [SIP18 test vectors](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md)
486
    #[test]
487
    fn test_sip18_ref_message_hashing() {
×
488
        let domain = Value::Tuple(
×
489
            TupleData::from_data(vec![
×
490
                (
×
491
                    ClarityName::from_literal("name"),
×
492
                    Value::string_ascii_from_bytes("Test App".into()).unwrap(),
×
493
                ),
×
494
                (
×
495
                    ClarityName::from_literal("version"),
×
496
                    Value::string_ascii_from_bytes("1.0.0".into()).unwrap(),
×
497
                ),
×
498
                (
×
499
                    ClarityName::from_literal("chain-id"),
×
500
                    Value::UInt(CHAIN_ID_MAINNET.into()),
×
501
                ),
×
502
            ])
×
503
            .unwrap(),
×
504
        );
×
505
        let data = Value::string_ascii_from_bytes("Hello World".into()).unwrap();
×
506

507
        let msg_hash = structured_data_message_hash(data, domain);
×
508

509
        assert_eq!(
×
510
            to_hex(msg_hash.as_bytes()),
×
511
            "1bfdab6d4158313ce34073fbb8d6b0fc32c154d439def12247a0f44bb2225259"
512
        );
513
    }
×
514

515
    /// [SIP18 test vectors](https://github.com/stacksgov/sips/blob/main/sips/sip-018/sip-018-signed-structured-data.md)
516
    #[test]
517
    fn test_sip18_ref_signing() {
×
518
        let key = Secp256k1PrivateKey::from_hex(
×
519
            "753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601",
×
520
        )
521
        .unwrap();
×
522
        let domain = Value::Tuple(
×
523
            TupleData::from_data(vec![
×
524
                (
×
525
                    ClarityName::from_literal("name"),
×
526
                    Value::string_ascii_from_bytes("Test App".into()).unwrap(),
×
527
                ),
×
528
                (
×
529
                    ClarityName::from_literal("version"),
×
530
                    Value::string_ascii_from_bytes("1.0.0".into()).unwrap(),
×
531
                ),
×
532
                (
×
533
                    ClarityName::from_literal("chain-id"),
×
534
                    Value::UInt(CHAIN_ID_MAINNET.into()),
×
535
                ),
×
536
            ])
×
537
            .unwrap(),
×
538
        );
×
539
        let data = Value::string_ascii_from_bytes("Hello World".into()).unwrap();
×
540
        let signature =
×
541
            sign_structured_data(data, domain, &key).expect("Failed to sign structured data");
×
542

543
        let signature_rsv = signature.to_rsv();
×
544

545
        assert_eq!(to_hex(signature_rsv.as_slice()), "8b94e45701d857c9f1d1d70e8b2ca076045dae4920fb0160be0642a68cd78de072ab527b5c5277a593baeb2a8b657c216b99f7abb5d14af35b4bf12ba6460ba401");
×
546
    }
×
547

548
    #[test]
549
    fn test_prefix_bytes() {
×
550
        let hex = to_hex(STRUCTURED_DATA_PREFIX.as_ref());
×
551
        assert_eq!(hex, "534950303138");
×
552
    }
×
553
}
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