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

ergoplatform / sigma-rust / 15667516698

15 Jun 2025 09:22PM UTC coverage: 77.672% (-0.6%) from 78.291%
15667516698

Pull #832

github

web-flow
Merge 39c40f47b into 6f12ef8f2
Pull Request #832: Soft Fork support

298 of 462 new or added lines in 72 files covered. (64.5%)

53 existing lines in 22 files now uncovered.

11765 of 15147 relevant lines covered (77.67%)

2.91 hits per line

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

96.63
/ergotree-interpreter/src/sigma_protocol/unproven_tree.rs
1
//! Unproven tree types
2

3
use super::dht_protocol::FirstDhTupleProverMessage;
4
use super::proof_tree::ConjectureType;
5
use super::proof_tree::ProofTree;
6
use super::proof_tree::ProofTreeConjecture;
7
use super::proof_tree::ProofTreeKind;
8
use super::prover::ProverError;
9
use super::wscalar::Wscalar;
10
use super::{dlog_protocol::FirstDlogProverMessage, Challenge, FirstProverMessage};
11
use crate::sigma_protocol::proof_tree::ProofTreeLeaf;
12
use crate::sigma_protocol::SOUNDNESS_BYTES;
13
use alloc::vec::Vec;
14
use bounded_vec::BoundedVecOutOfBounds;
15
use ergotree_ir::sigma_protocol::sigma_boolean::cand::Cand;
16
use ergotree_ir::sigma_protocol::sigma_boolean::cor::Cor;
17
use ergotree_ir::sigma_protocol::sigma_boolean::cthreshold::Cthreshold;
18
use ergotree_ir::sigma_protocol::sigma_boolean::ProveDhTuple;
19
use ergotree_ir::sigma_protocol::sigma_boolean::ProveDlog;
20
use ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean;
21
use ergotree_ir::sigma_protocol::sigma_boolean::SigmaConjectureItems;
22
use ergotree_ir::sigma_protocol::sigma_boolean::SigmaProofOfKnowledgeTree;
23
use gf2_192::gf2_192poly::Gf2_192Poly;
24

25
extern crate derive_more;
26
use derive_more::From;
27

28
/// Unproven trees
29
#[derive(PartialEq, Debug, Clone, From)]
30
pub(crate) enum UnprovenTree {
31
    UnprovenLeaf(UnprovenLeaf),
32
    UnprovenConjecture(UnprovenConjecture),
33
}
34

35
impl UnprovenTree {
36
    /// Is real or simulated
37
    pub(crate) fn is_real(&self) -> bool {
2✔
38
        !self.simulated()
2✔
39
    }
40

41
    pub(crate) fn simulated(&self) -> bool {
2✔
42
        match self {
2✔
43
            UnprovenTree::UnprovenLeaf(ul) => ul.simulated(),
2✔
44
            UnprovenTree::UnprovenConjecture(uc) => uc.simulated(),
2✔
45
        }
46
    }
47

48
    pub(crate) fn with_position(self, updated: NodePosition) -> Self {
2✔
49
        match self {
2✔
50
            UnprovenTree::UnprovenLeaf(ul) => ul.with_position(updated).into(),
2✔
51
            UnprovenTree::UnprovenConjecture(uc) => uc.with_position(updated).into(),
2✔
52
        }
53
    }
54

55
    pub(crate) fn with_challenge(self, challenge: Challenge) -> Self {
2✔
56
        match self {
2✔
57
            UnprovenTree::UnprovenLeaf(ul) => ul.with_challenge(challenge).into(),
2✔
58
            UnprovenTree::UnprovenConjecture(uc) => uc.with_challenge(challenge).into(),
2✔
59
        }
60
    }
61

62
    pub(crate) fn with_simulated(self, simulated: bool) -> Self {
2✔
63
        match self {
2✔
64
            UnprovenTree::UnprovenLeaf(ul) => ul.with_simulated(simulated).into(),
2✔
65
            UnprovenTree::UnprovenConjecture(uc) => uc.with_simulated(simulated).into(),
1✔
66
        }
67
    }
68

69
    pub(crate) fn as_tree_kind(&self) -> ProofTreeKind {
2✔
70
        match self {
2✔
71
            UnprovenTree::UnprovenLeaf(ul) => ProofTreeKind::Leaf(ul),
2✔
72
            UnprovenTree::UnprovenConjecture(uc) => ProofTreeKind::Conjecture(uc),
2✔
73
        }
74
    }
75

76
    pub(crate) fn challenge(&self) -> Option<Challenge> {
2✔
77
        match self {
2✔
78
            UnprovenTree::UnprovenLeaf(ul) => ul.challenge(),
2✔
79
            UnprovenTree::UnprovenConjecture(uc) => uc.challenge(),
2✔
80
        }
81
    }
82

83
    pub(crate) fn position(&self) -> &NodePosition {
1✔
84
        match self {
2✔
85
            UnprovenTree::UnprovenLeaf(ul) => ul.position(),
1✔
86
            UnprovenTree::UnprovenConjecture(uc) => uc.position(),
×
87
        }
88
    }
89
}
90

91
impl From<UnprovenSchnorr> for UnprovenTree {
92
    fn from(v: UnprovenSchnorr) -> Self {
2✔
93
        UnprovenTree::UnprovenLeaf(v.into())
2✔
94
    }
95
}
96

97
impl From<CandUnproven> for UnprovenTree {
98
    fn from(v: CandUnproven) -> Self {
2✔
99
        UnprovenTree::UnprovenConjecture(v.into())
2✔
100
    }
101
}
102

103
impl From<CorUnproven> for UnprovenTree {
104
    fn from(v: CorUnproven) -> Self {
2✔
105
        UnprovenTree::UnprovenConjecture(v.into())
2✔
106
    }
107
}
108

109
impl From<UnprovenDhTuple> for UnprovenTree {
110
    fn from(v: UnprovenDhTuple) -> Self {
2✔
111
        UnprovenTree::UnprovenLeaf(v.into())
2✔
112
    }
113
}
114

115
impl From<CthresholdUnproven> for UnprovenTree {
116
    fn from(v: CthresholdUnproven) -> Self {
2✔
117
        UnprovenTree::UnprovenConjecture(v.into())
2✔
118
    }
119
}
120

121
/// Unproven leaf types
122
#[derive(PartialEq, Debug, Clone, From)]
123
pub(crate) enum UnprovenLeaf {
124
    /// Unproven Schnorr
125
    UnprovenSchnorr(UnprovenSchnorr),
126
    UnprovenDhTuple(UnprovenDhTuple),
127
}
128

129
impl UnprovenLeaf {
130
    pub(crate) fn with_position(self, updated: NodePosition) -> Self {
2✔
131
        match self {
2✔
132
            UnprovenLeaf::UnprovenSchnorr(us) => us.with_position(updated).into(),
2✔
133
            UnprovenLeaf::UnprovenDhTuple(ut) => ut.with_position(updated).into(),
2✔
134
        }
135
    }
136

137
    pub(crate) fn with_challenge(self, challenge: Challenge) -> Self {
2✔
138
        match self {
2✔
139
            UnprovenLeaf::UnprovenSchnorr(us) => us.with_challenge(challenge).into(),
2✔
140
            UnprovenLeaf::UnprovenDhTuple(ut) => ut.with_challenge(challenge).into(),
2✔
141
        }
142
    }
143

144
    pub(crate) fn with_simulated(self, simulated: bool) -> Self {
2✔
145
        match self {
2✔
146
            UnprovenLeaf::UnprovenSchnorr(us) => us.with_simulated(simulated).into(),
2✔
147
            UnprovenLeaf::UnprovenDhTuple(ut) => ut.with_simulated(simulated).into(),
2✔
148
        }
149
    }
150

151
    pub(crate) fn is_real(&self) -> bool {
2✔
152
        match self {
2✔
153
            UnprovenLeaf::UnprovenSchnorr(us) => us.is_real(),
2✔
154
            UnprovenLeaf::UnprovenDhTuple(ut) => ut.is_real(),
2✔
155
        }
156
    }
157

158
    pub(crate) fn challenge(&self) -> Option<Challenge> {
2✔
159
        match self {
2✔
160
            UnprovenLeaf::UnprovenSchnorr(us) => us.challenge_opt.clone(),
2✔
161
            UnprovenLeaf::UnprovenDhTuple(ut) => ut.challenge_opt.clone(),
2✔
162
        }
163
    }
164

165
    pub(crate) fn position(&self) -> &NodePosition {
1✔
166
        match self {
2✔
167
            UnprovenLeaf::UnprovenSchnorr(us) => &us.position,
1✔
168
            UnprovenLeaf::UnprovenDhTuple(ut) => &ut.position,
1✔
169
        }
170
    }
171

172
    pub(crate) fn simulated(&self) -> bool {
2✔
173
        match self {
2✔
174
            UnprovenLeaf::UnprovenSchnorr(us) => us.simulated,
2✔
175
            UnprovenLeaf::UnprovenDhTuple(udht) => udht.simulated,
2✔
176
        }
177
    }
178
}
179

180
impl ProofTreeLeaf for UnprovenLeaf {
181
    fn proposition(&self) -> SigmaBoolean {
2✔
182
        match self {
2✔
183
            UnprovenLeaf::UnprovenSchnorr(us) => SigmaBoolean::ProofOfKnowledge(
184
                SigmaProofOfKnowledgeTree::ProveDlog(us.proposition.clone()),
2✔
185
            ),
186
            UnprovenLeaf::UnprovenDhTuple(udht) => SigmaBoolean::ProofOfKnowledge(
187
                SigmaProofOfKnowledgeTree::ProveDhTuple(udht.proposition.clone()),
2✔
188
            ),
189
        }
190
    }
191

192
    fn commitment_opt(&self) -> Option<FirstProverMessage> {
2✔
193
        match self {
2✔
194
            UnprovenLeaf::UnprovenSchnorr(us) => us.commitment_opt.clone().map(Into::into),
2✔
195
            UnprovenLeaf::UnprovenDhTuple(udht) => udht.commitment_opt.clone().map(Into::into),
2✔
196
        }
197
    }
198
}
199

200
#[derive(PartialEq, Debug, Clone, From)]
201
#[allow(clippy::enum_variant_names)]
202
pub(crate) enum UnprovenConjecture {
203
    CandUnproven(CandUnproven),
204
    CorUnproven(CorUnproven),
205
    CthresholdUnproven(CthresholdUnproven),
206
}
207

208
impl UnprovenConjecture {
209
    pub(crate) fn children(&self) -> &[ProofTree] {
2✔
210
        match self {
4✔
211
            UnprovenConjecture::CandUnproven(cand) => &cand.children,
2✔
212
            UnprovenConjecture::CorUnproven(cor) => &cor.children,
2✔
213
            UnprovenConjecture::CthresholdUnproven(ct) => ct.children.as_slice(),
2✔
214
        }
215
    }
216

217
    pub(crate) fn with_children(
2✔
218
        self,
219
        children: Vec<ProofTree>,
220
    ) -> Result<Self, BoundedVecOutOfBounds> {
221
        Ok(match self {
4✔
UNCOV
222
            UnprovenConjecture::CandUnproven(cand) => cand.with_children(children).into(),
×
223
            UnprovenConjecture::CorUnproven(cor) => cor.with_children(children).into(),
2✔
224
            UnprovenConjecture::CthresholdUnproven(ct) => {
2✔
225
                ct.with_children(children.try_into()?).into()
2✔
226
            }
227
        })
228
    }
229

230
    pub(crate) fn position(&self) -> &NodePosition {
2✔
231
        match self {
4✔
232
            UnprovenConjecture::CandUnproven(cand) => &cand.position,
2✔
233
            UnprovenConjecture::CorUnproven(cor) => &cor.position,
2✔
234
            UnprovenConjecture::CthresholdUnproven(ct) => &ct.position,
2✔
235
        }
236
    }
237

238
    fn challenge(&self) -> Option<Challenge> {
2✔
239
        match self {
2✔
240
            UnprovenConjecture::CandUnproven(cand) => cand.challenge_opt.clone(),
2✔
241
            UnprovenConjecture::CorUnproven(cor) => cor.challenge_opt.clone(),
1✔
242
            UnprovenConjecture::CthresholdUnproven(ct) => ct.challenge_opt.clone(),
1✔
243
        }
244
    }
245

246
    fn with_position(self, updated: NodePosition) -> Self {
2✔
247
        match self {
2✔
248
            UnprovenConjecture::CandUnproven(cand) => cand.with_position(updated).into(),
2✔
249
            UnprovenConjecture::CorUnproven(cor) => cor.with_position(updated).into(),
2✔
250
            UnprovenConjecture::CthresholdUnproven(ct) => ct.with_position(updated).into(),
1✔
251
        }
252
    }
253

254
    fn with_challenge(self, challenge: Challenge) -> Self {
2✔
255
        match self {
2✔
256
            UnprovenConjecture::CandUnproven(cand) => cand.with_challenge(challenge).into(),
2✔
257
            UnprovenConjecture::CorUnproven(cor) => cor.with_challenge(challenge).into(),
2✔
258
            UnprovenConjecture::CthresholdUnproven(ct) => ct.with_challenge(challenge).into(),
2✔
259
        }
260
    }
261

262
    fn with_simulated(self, simulated: bool) -> Self {
1✔
263
        match self {
1✔
264
            UnprovenConjecture::CandUnproven(cand) => cand.with_simulated(simulated).into(),
1✔
265
            UnprovenConjecture::CorUnproven(cor) => cor.with_simulated(simulated).into(),
×
266
            UnprovenConjecture::CthresholdUnproven(ct) => ct.with_simulated(simulated).into(),
×
267
        }
268
    }
269

270
    fn simulated(&self) -> bool {
2✔
271
        match self {
2✔
272
            UnprovenConjecture::CandUnproven(au) => au.simulated,
2✔
273
            UnprovenConjecture::CorUnproven(ou) => ou.simulated,
2✔
274
            UnprovenConjecture::CthresholdUnproven(ct) => ct.simulated,
2✔
275
        }
276
    }
277

278
    pub(crate) fn is_real(&self) -> bool {
2✔
279
        !self.simulated()
2✔
280
    }
281
}
282

283
impl ProofTreeConjecture for UnprovenConjecture {
284
    fn conjecture_type(&self) -> ConjectureType {
2✔
285
        match self {
2✔
286
            UnprovenConjecture::CandUnproven(_) => ConjectureType::And,
2✔
287
            UnprovenConjecture::CorUnproven(_) => ConjectureType::Or,
2✔
288
            UnprovenConjecture::CthresholdUnproven(_) => ConjectureType::Threshold,
2✔
289
        }
290
    }
291

292
    fn children(&self) -> Vec<ProofTree> {
2✔
293
        match self {
2✔
294
            UnprovenConjecture::CandUnproven(cand) => cand.children.clone(),
2✔
295
            UnprovenConjecture::CorUnproven(cor) => cor.children.clone(),
2✔
296
            UnprovenConjecture::CthresholdUnproven(ct) => ct.children.clone().into(),
2✔
297
        }
298
    }
299
}
300

301
#[derive(PartialEq, Debug, Clone)]
302
pub(crate) struct UnprovenSchnorr {
303
    pub(crate) proposition: ProveDlog,
304
    pub(crate) commitment_opt: Option<FirstDlogProverMessage>,
305
    pub(crate) randomness_opt: Option<Wscalar>,
306
    pub(crate) challenge_opt: Option<Challenge>,
307
    pub(crate) simulated: bool,
308
    pub(crate) position: NodePosition,
309
}
310

311
impl UnprovenSchnorr {
312
    fn with_position(self, updated: NodePosition) -> Self {
2✔
313
        UnprovenSchnorr {
314
            position: updated,
315
            ..self
316
        }
317
    }
318

319
    fn with_challenge(self, challenge: Challenge) -> Self {
2✔
320
        UnprovenSchnorr {
321
            challenge_opt: Some(challenge),
2✔
322
            ..self
323
        }
324
    }
325

326
    fn with_simulated(self, simulated: bool) -> Self {
2✔
327
        UnprovenSchnorr { simulated, ..self }
328
    }
329

330
    pub(crate) fn is_real(&self) -> bool {
2✔
331
        !self.simulated
2✔
332
    }
333
}
334

335
/// Unproven DhTuple
336
#[derive(PartialEq, Eq, Debug, Clone)]
337
pub struct UnprovenDhTuple {
338
    /// Proposition
339
    pub proposition: ProveDhTuple,
340
    /// Commitment
341
    pub commitment_opt: Option<FirstDhTupleProverMessage>,
342
    /// Randomness
343
    pub randomness_opt: Option<Wscalar>,
344
    /// Challenge
345
    pub challenge_opt: Option<Challenge>,
346
    /// Simulated or not
347
    pub simulated: bool,
348
    /// Position in tree
349
    pub position: NodePosition,
350
}
351

352
impl UnprovenDhTuple {
353
    fn with_position(self, updated: NodePosition) -> Self {
2✔
354
        UnprovenDhTuple {
355
            position: updated,
356
            ..self
357
        }
358
    }
359

360
    fn with_challenge(self, challenge: Challenge) -> Self {
2✔
361
        UnprovenDhTuple {
362
            challenge_opt: Some(challenge),
2✔
363
            ..self
364
        }
365
    }
366

367
    fn with_simulated(self, simulated: bool) -> Self {
2✔
368
        UnprovenDhTuple { simulated, ..self }
369
    }
370

371
    /// Set Commitment
372
    pub fn with_commitment(self, commitment: FirstDhTupleProverMessage) -> Self {
1✔
373
        Self {
374
            commitment_opt: Some(commitment),
1✔
375
            ..self
376
        }
377
    }
378

379
    pub(crate) fn is_real(&self) -> bool {
2✔
380
        !self.simulated
2✔
381
    }
382
}
383

384
/// Data type which encodes position of a node in a tree.
385
///
386
/// Position is encoded like following (the example provided is for CTHRESHOLD(2, Seq(pk1, pk2, pk3 && pk4)) :
387
///
388
/// r#"
389
///            0
390
///          / | \
391
///         /  |  \
392
///       0-0 0-1 0-2
393
///               /|
394
///              / |
395
///             /  |
396
///            /   |
397
///          0-2-0 0-2-1
398
/// "#;
399
///
400
/// So a hint associated with pk1 has a position "0-0", pk4 - "0-2-1" .
401
///
402
/// Please note that "0" prefix is for a crypto tree. There are several kinds of trees during evaluation.
403
/// Initial mixed tree (ergoTree) would have another prefix.
404
#[derive(PartialEq, Eq, Debug, Clone)]
405
#[cfg_attr(
406
    feature = "json",
407
    derive(serde::Serialize, serde::Deserialize),
408
    serde(
409
        try_from = "crate::json::hint::NodePositionJson",
410
        into = "crate::json::hint::NodePositionJson"
411
    )
412
)]
413
#[cfg_attr(feature = "arbitrary", derive(proptest_derive::Arbitrary))]
414
pub struct NodePosition {
415
    /// positions from root (inclusive) in top-down order
416
    #[cfg_attr(
417
        feature = "arbitrary",
418
        proptest(strategy = "proptest::collection::vec(0..10usize, 1..3)")
419
    )]
420
    pub positions: Vec<usize>,
421
}
422

423
impl NodePosition {
424
    /// Prefix of crypto tree root
425
    pub fn crypto_tree_prefix() -> Self {
2✔
426
        NodePosition { positions: vec![0] }
2✔
427
    }
428

429
    /// Child position
430
    pub fn child(&self, child_idx: usize) -> NodePosition {
2✔
431
        let mut positions = self.positions.clone();
2✔
432
        positions.push(child_idx);
2✔
433
        NodePosition { positions }
434
    }
435
}
436

437
#[derive(PartialEq, Debug, Clone)]
438
pub(crate) struct CandUnproven {
439
    pub(crate) proposition: Cand,
440
    pub(crate) challenge_opt: Option<Challenge>,
441
    pub(crate) simulated: bool,
442
    pub(crate) children: Vec<ProofTree>,
443
    pub(crate) position: NodePosition,
444
}
445

446
impl CandUnproven {
447
    pub(crate) fn is_real(&self) -> bool {
2✔
448
        !self.simulated
2✔
449
    }
450

451
    fn with_position(self, updated: NodePosition) -> Self {
2✔
452
        CandUnproven {
453
            position: updated,
454
            ..self
455
        }
456
    }
457

458
    fn with_challenge(self, challenge: Challenge) -> Self {
2✔
459
        CandUnproven {
460
            challenge_opt: Some(challenge),
2✔
461
            ..self
462
        }
463
    }
464

465
    fn with_simulated(self, simulated: bool) -> Self {
1✔
466
        Self { simulated, ..self }
467
    }
468

469
    pub(crate) fn with_children(self, children: Vec<ProofTree>) -> Self {
2✔
470
        CandUnproven { children, ..self }
471
    }
472
}
473

474
#[derive(PartialEq, Debug, Clone)]
475
pub(crate) struct CorUnproven {
476
    pub(crate) proposition: Cor,
477
    pub(crate) challenge_opt: Option<Challenge>,
478
    pub(crate) simulated: bool,
479
    pub(crate) children: Vec<ProofTree>,
480
    pub(crate) position: NodePosition,
481
}
482

483
impl CorUnproven {
484
    pub(crate) fn is_real(&self) -> bool {
2✔
485
        !self.simulated
2✔
486
    }
487

488
    fn with_position(self, updated: NodePosition) -> Self {
2✔
489
        Self {
490
            position: updated,
491
            ..self
492
        }
493
    }
494

495
    fn with_challenge(self, challenge: Challenge) -> Self {
2✔
496
        Self {
497
            challenge_opt: Some(challenge),
2✔
498
            ..self
499
        }
500
    }
501

502
    fn with_simulated(self, simulated: bool) -> Self {
×
503
        Self { simulated, ..self }
504
    }
505

506
    pub(crate) fn with_children(self, children: Vec<ProofTree>) -> Self {
2✔
507
        Self { children, ..self }
508
    }
509
}
510

511
#[derive(PartialEq, Debug, Clone)]
512
pub(crate) struct CthresholdUnproven {
513
    pub(crate) proposition: Cthreshold,
514
    pub(crate) k: u8,
515
    pub(crate) children: SigmaConjectureItems<ProofTree>,
516
    polinomial_opt: Option<Gf2_192Poly>,
517
    pub(crate) challenge_opt: Option<Challenge>,
518
    pub(crate) simulated: bool,
519
    pub(crate) position: NodePosition,
520
}
521

522
impl CthresholdUnproven {
523
    pub(crate) fn new(
2✔
524
        proposition: Cthreshold,
525
        k: u8,
526
        children: SigmaConjectureItems<ProofTree>,
527
        challenge_opt: Option<Challenge>,
528
        simulated: bool,
529
        position: NodePosition,
530
    ) -> Self {
531
        Self {
532
            proposition,
533
            k,
534
            children,
535
            polinomial_opt: None,
536
            challenge_opt,
537
            simulated,
538
            position,
539
        }
540
    }
541

542
    pub(crate) fn with_children(self, children: SigmaConjectureItems<ProofTree>) -> Self {
2✔
543
        Self { children, ..self }
544
    }
545

546
    #[allow(clippy::panic)]
547
    pub(crate) fn with_polynomial(self, q: Gf2_192Poly) -> Result<Self, ProverError> {
2✔
548
        let bytes = q.to_bytes();
2✔
549
        if bytes.len() == (self.proposition.children.len() - self.k as usize) * SOUNDNESS_BYTES {
5✔
550
            Ok(Self {
2✔
551
                polinomial_opt: Some(q),
3✔
552
                ..self
553
            })
554
        } else {
555
            Err(ProverError::Unexpected(
×
556
                "Invalid polynomial length in CthresholdUnproven (children.len() - k) * SOUNDNESS_BYTES != polynomial.len()"
557
            ))
558
        }
559
    }
560

561
    fn with_position(self, updated: NodePosition) -> Self {
1✔
562
        Self {
563
            position: updated,
564
            ..self
565
        }
566
    }
567

568
    fn with_challenge(self, challenge: Challenge) -> Self {
2✔
569
        Self {
570
            challenge_opt: Some(challenge),
2✔
571
            ..self
572
        }
573
    }
574

575
    pub(crate) fn with_simulated(self, simulated: bool) -> Self {
2✔
576
        Self { simulated, ..self }
577
    }
578

579
    pub(crate) fn is_real(&self) -> bool {
2✔
580
        !self.simulated
2✔
581
    }
582

583
    pub(crate) fn polinomial_opt(&self) -> Option<Gf2_192Poly> {
2✔
584
        self.polinomial_opt.clone()
3✔
585
    }
586
}
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

© 2025 Coveralls, Inc