use core::num::NonZeroU32;
use crate::consensus::BlockHeight;
use crate::value::{COIN, Zatoshis};
const DENOMINATION_RADIX: u64 = 10;
const ONE_TWO_FIVE_DESCENDING: [u64; 3] = [5, 2, 1];
pub const DENOM_CAP: Zatoshis = Zatoshis::const_from_u64(10_000 * COIN);
pub const MAX_RESIDUAL_VALUE: Zatoshis = Zatoshis::const_from_u64(COIN / 100);
pub const PREP_TX_ACTIONS: usize = 16;
pub const PREP_DELAY_MEAN: NonZeroU32 = NonZeroU32::new(16).expect("16 is nonzero");
pub const PREP_DELAY_CAP: NonZeroU32 = NonZeroU32::new(96).expect("96 is nonzero");
#[deprecated(note = "DO NOT USE;
`zcash_protocol::zip318::DELAY_CAP_RATIO`. ZIP 318 no longer relates each
delay cap to its mean by a shared ratio; use `TRANSFER_DELAY_CAP` and
`PREP_DELAY_CAP` directly.")]
pub const DELAY_CAP_RATIO: NonZeroU32 = NonZeroU32::new(4).expect("4 is nonzero");
pub const TRANSFER_DELAY_MEAN: NonZeroU32 = NonZeroU32::new(66).expect("66 is nonzero");
pub const TRANSFER_DELAY_CAP: NonZeroU32 = NonZeroU32::new(576).expect("576 is nonzero");
pub const ANCHOR_AGE_CAP: u32 = 4;
pub const EXPIRY_MODULUS: u32 = 34_560;
pub const EXPIRY_WINDOW: u32 = 2 * EXPIRY_MODULUS;
pub const CROSSING_SOURCE_ACTIONS: usize = 2;
pub const CROSSING_DESTINATION_ACTIONS: usize = 1;
pub fn largest_one_two_five(hi: u64, floor: u64) -> u64 {
if hi < floor {
return 0;
}
let mut pow = floor;
while pow.checked_mul(DENOMINATION_RADIX).is_some_and(|p| p <= hi) {
pow *= DENOMINATION_RADIX;
}
for multiple in ONE_TWO_FIVE_DESCENDING {
if let Some(v) = pow.checked_mul(multiple)
&& v <= hi
{
return v;
}
}
pow
}
fn is_canonical_within(value: Zatoshis, min: Zatoshis, max: Zatoshis) -> bool {
if value < min || value > max {
return false;
}
let mut n = u64::from(value);
while n.is_multiple_of(DENOMINATION_RADIX) {
n /= DENOMINATION_RADIX;
}
ONE_TWO_FIVE_DESCENDING.contains(&n)
}
pub fn is_canonical_denomination(value: Zatoshis) -> bool {
is_canonical_within(value, MAX_RESIDUAL_VALUE, DENOM_CAP)
}
pub fn expiry_height(target_height: BlockHeight) -> BlockHeight {
let h = u32::from(target_height);
BlockHeight::from_u32(h - (h % EXPIRY_MODULUS)) + EXPIRY_WINDOW
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AnchorBucketInterval(NonZeroU32);
impl AnchorBucketInterval {
pub const ZIP_318: Self = Self(NonZeroU32::new(144).expect("144 is nonzero"));
pub const fn custom(blocks: NonZeroU32) -> Self {
Self(blocks)
}
pub fn block_count(&self) -> NonZeroU32 {
self.0
}
pub fn is_boundary(&self, height: BlockHeight) -> bool {
u32::from(height) % self.0 == 0
}
pub fn boundary_at_or_below(&self, height: BlockHeight) -> BlockHeight {
let h = u32::from(height);
BlockHeight::from_u32(h - (h % self.0))
}
pub fn boundary_at_or_above(&self, height: BlockHeight) -> BlockHeight {
let h = u32::from(height);
let r = h % self.0;
BlockHeight::from_u32(if r == 0 {
h
} else {
h.saturating_add(self.0.get() - r)
})
}
}
impl Default for AnchorBucketInterval {
fn default() -> Self {
Self::ZIP_318
}
}
pub trait PoolMigrationConstants {
fn anchor_bucket_interval(&self) -> AnchorBucketInterval {
AnchorBucketInterval::ZIP_318
}
fn denomination_cap(&self) -> Zatoshis {
DENOM_CAP
}
fn max_residual_value(&self) -> Zatoshis {
MAX_RESIDUAL_VALUE
}
fn preparation_tx_actions(&self) -> usize {
PREP_TX_ACTIONS
}
fn transfer_delay(&self) -> (NonZeroU32, NonZeroU32) {
(TRANSFER_DELAY_MEAN, TRANSFER_DELAY_CAP)
}
fn preparation_delay(&self) -> (NonZeroU32, NonZeroU32) {
(PREP_DELAY_MEAN, PREP_DELAY_CAP)
}
fn anchor_age_cap(&self) -> u32 {
ANCHOR_AGE_CAP
}
fn expiry_window(&self) -> (u32, u32) {
(EXPIRY_MODULUS, EXPIRY_WINDOW)
}
fn is_canonical_denomination(&self, value: Zatoshis) -> bool {
is_canonical_within(value, self.max_residual_value(), self.denomination_cap())
}
fn canonical_expiry(&self, reference: BlockHeight) -> BlockHeight {
let (modulus, window) = self.expiry_window();
let h = u32::from(reference);
BlockHeight::from_u32(h - (h % modulus)) + window
}
fn is_canonical_expiry(&self, expiry: BlockHeight, reference: BlockHeight) -> bool {
expiry == self.canonical_expiry(reference)
}
fn is_canonical_expiry_value(&self, expiry: BlockHeight) -> bool {
let (modulus, window) = self.expiry_window();
let expiry = u32::from(expiry);
expiry >= window && expiry % modulus == 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Zip318TxKind {
Preparation,
Transfer,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Zip318Classification {
Conforms(Zip318TxKind),
Nonconforming,
Unknown,
}
impl Zip318Classification {
pub fn to_code(&self) -> i64 {
match self {
Self::Unknown => 0,
Self::Nonconforming => 1,
Self::Conforms(Zip318TxKind::Preparation) => 2,
Self::Conforms(Zip318TxKind::Transfer) => 3,
}
}
pub fn from_code(code: i64) -> Self {
match code {
1 => Self::Nonconforming,
2 => Self::Conforms(Zip318TxKind::Preparation),
3 => Self::Conforms(Zip318TxKind::Transfer),
_ => Self::Unknown,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Zip318Evidence {
source_actions: Option<usize>,
destination_actions: Option<usize>,
other_bundles_present: Option<bool>,
source_is_send_to_self: Option<bool>,
sole_destination_value: Option<Zatoshis>,
expiry_is_canonical: Option<bool>,
anchor_on_grid: Option<bool>,
fee_is_canonical: Option<bool>,
}
impl Zip318Evidence {
pub fn with_source_actions(mut self, source_actions: Option<usize>) -> Self {
self.source_actions = source_actions;
self
}
pub fn with_destination_actions(mut self, destination_actions: Option<usize>) -> Self {
self.destination_actions = destination_actions;
self
}
pub fn with_other_bundles_present(mut self, other_bundles_present: Option<bool>) -> Self {
self.other_bundles_present = other_bundles_present;
self
}
pub fn with_source_is_send_to_self(mut self, source_is_send_to_self: Option<bool>) -> Self {
self.source_is_send_to_self = source_is_send_to_self;
self
}
pub fn with_sole_destination_value(mut self, sole_destination_value: Option<Zatoshis>) -> Self {
self.sole_destination_value = sole_destination_value;
self
}
pub fn with_expiry_is_canonical(mut self, expiry_is_canonical: Option<bool>) -> Self {
self.expiry_is_canonical = expiry_is_canonical;
self
}
pub fn with_anchor_on_grid(mut self, anchor_on_grid: Option<bool>) -> Self {
self.anchor_on_grid = anchor_on_grid;
self
}
pub fn with_fee_is_canonical(mut self, fee_is_canonical: Option<bool>) -> Self {
self.fee_is_canonical = fee_is_canonical;
self
}
pub fn source_actions(&self) -> Option<usize> {
self.source_actions
}
pub fn destination_actions(&self) -> Option<usize> {
self.destination_actions
}
pub fn other_bundles_present(&self) -> Option<bool> {
self.other_bundles_present
}
pub fn source_is_send_to_self(&self) -> Option<bool> {
self.source_is_send_to_self
}
pub fn sole_destination_value(&self) -> Option<Zatoshis> {
self.sole_destination_value
}
pub fn expiry_is_canonical(&self) -> Option<bool> {
self.expiry_is_canonical
}
pub fn anchor_on_grid(&self) -> Option<bool> {
self.anchor_on_grid
}
pub fn fee_is_canonical(&self) -> Option<bool> {
self.fee_is_canonical
}
}
pub fn classify<C>(evidence: &Zip318Evidence, constants: &C) -> Zip318Classification
where
C: PoolMigrationConstants + ?Sized,
{
if evidence.anchor_on_grid == Some(false) || evidence.fee_is_canonical == Some(false) {
return Zip318Classification::Nonconforming;
}
let (
Some(source_actions),
Some(destination_actions),
Some(other_bundles_present),
Some(expiry_is_canonical),
) = (
evidence.source_actions,
evidence.destination_actions,
evidence.other_bundles_present,
evidence.expiry_is_canonical,
)
else {
return Zip318Classification::Unknown;
};
if other_bundles_present || !expiry_is_canonical {
return Zip318Classification::Nonconforming;
}
match destination_actions {
0 => classify_preparation(evidence, constants, source_actions),
CROSSING_DESTINATION_ACTIONS => classify_crossing(evidence, constants, source_actions),
_ => Zip318Classification::Nonconforming,
}
}
fn classify_preparation<C>(
evidence: &Zip318Evidence,
constants: &C,
source_actions: usize,
) -> Zip318Classification
where
C: PoolMigrationConstants + ?Sized,
{
if source_actions != constants.preparation_tx_actions() {
return Zip318Classification::Nonconforming;
}
match evidence.source_is_send_to_self {
None => Zip318Classification::Unknown,
Some(false) => Zip318Classification::Nonconforming,
Some(true) => Zip318Classification::Conforms(Zip318TxKind::Preparation),
}
}
fn classify_crossing<C>(
evidence: &Zip318Evidence,
constants: &C,
source_actions: usize,
) -> Zip318Classification
where
C: PoolMigrationConstants + ?Sized,
{
if source_actions != CROSSING_SOURCE_ACTIONS {
return Zip318Classification::Nonconforming;
}
let Some(value) = evidence.sole_destination_value else {
return Zip318Classification::Unknown;
};
if !constants.is_canonical_denomination(value) {
return Zip318Classification::Nonconforming;
}
Zip318Classification::Conforms(Zip318TxKind::Transfer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_denominations_are_the_one_two_five_series_in_range() {
for zat in [
1_000_000u64,
2_000_000,
5_000_000,
10_000_000,
100_000_000,
200_000_000,
1_000_000_000_000,
] {
assert!(
is_canonical_denomination(Zatoshis::const_from_u64(zat)),
"{zat} should be canonical"
);
}
for zat in [1u64, 2, 5, 100_000, 500_000] {
assert!(
!is_canonical_denomination(Zatoshis::const_from_u64(zat)),
"{zat} is below the minimum denomination"
);
}
assert!(!is_canonical_denomination(Zatoshis::const_from_u64(
2_000_000_000_000
)));
for zat in [3_000_000u64, 1_000_001, 999_999_999] {
assert!(
!is_canonical_denomination(Zatoshis::const_from_u64(zat)),
"{zat} is off the series"
);
}
assert!(!is_canonical_denomination(Zatoshis::ZERO));
}
#[test]
fn largest_at_or_below_picks_the_greatest_series_member() {
assert_eq!(largest_one_two_five(0, 1), 0);
assert_eq!(largest_one_two_five(4, 1), 2);
assert_eq!(largest_one_two_five(9, 1), 5);
assert_eq!(largest_one_two_five(10, 1), 10);
assert_eq!(largest_one_two_five(123_456, 1), 100_000);
assert_eq!(largest_one_two_five(999_999, 1_000_000), 0);
}
#[test]
fn zip_318_bounds_are_themselves_canonical() {
assert!(is_canonical_denomination(MAX_RESIDUAL_VALUE));
assert!(is_canonical_denomination(DENOM_CAP));
}
#[test]
fn zip_318_interval_is_144_blocks() {
assert_eq!(AnchorBucketInterval::ZIP_318.block_count().get(), 144);
assert_eq!(
AnchorBucketInterval::default(),
AnchorBucketInterval::ZIP_318
);
}
#[test]
fn boundaries_round_to_multiples_of_the_interval() {
let i = AnchorBucketInterval::ZIP_318;
for (height, below, above) in [
(0u32, 0u32, 0u32),
(1, 0, 144),
(143, 0, 144),
(144, 144, 144),
(145, 144, 288),
(2_000_000, 1_999_872, 2_000_016),
] {
let h = BlockHeight::from_u32(height);
assert_eq!(
u32::from(i.boundary_at_or_below(h)),
below,
"below({height})"
);
assert_eq!(
u32::from(i.boundary_at_or_above(h)),
above,
"above({height})"
);
}
assert!(i.is_boundary(BlockHeight::from_u32(288)));
assert!(!i.is_boundary(BlockHeight::from_u32(289)));
}
#[test]
fn expiry_is_shared_across_a_modulus_period() {
let period_start = 3 * EXPIRY_MODULUS;
let expected = BlockHeight::from_u32(period_start + EXPIRY_WINDOW);
for offset in [0u32, 1, EXPIRY_MODULUS / 2, EXPIRY_MODULUS - 1] {
let h = BlockHeight::from_u32(period_start + offset);
assert_eq!(
expiry_height(h),
expected,
"offset {offset} must share the expiry"
);
assert!(expiry_height(h) > h);
assert!(u32::from(expiry_height(h)) - u32::from(h) <= EXPIRY_WINDOW);
}
let next = BlockHeight::from_u32(period_start + EXPIRY_MODULUS);
assert_eq!(
expiry_height(next),
BlockHeight::from_u32(period_start + EXPIRY_MODULUS + EXPIRY_WINDOW)
);
}
#[test]
fn scheduling_constants_carry_the_zip_318_values() {
assert_eq!(PREP_TX_ACTIONS, 16);
assert_eq!(PREP_DELAY_MEAN.get(), 16);
assert_eq!(PREP_DELAY_CAP.get(), 96);
assert_eq!(TRANSFER_DELAY_MEAN.get(), 66);
assert_eq!(TRANSFER_DELAY_CAP.get(), 576);
assert_eq!(ANCHOR_AGE_CAP, 4);
assert_eq!(EXPIRY_MODULUS, 34_560);
assert_eq!(EXPIRY_WINDOW, 2 * EXPIRY_MODULUS);
}
#[test]
fn defaults_are_the_zip_318_values() {
#[derive(Clone)]
struct Specified;
impl PoolMigrationConstants for Specified {}
assert_eq!(
Specified.anchor_bucket_interval(),
AnchorBucketInterval::ZIP_318
);
assert_eq!(Specified.denomination_cap(), DENOM_CAP);
assert_eq!(Specified.max_residual_value(), MAX_RESIDUAL_VALUE);
assert_eq!(Specified.preparation_tx_actions(), PREP_TX_ACTIONS);
assert_eq!(
Specified.transfer_delay(),
(TRANSFER_DELAY_MEAN, TRANSFER_DELAY_CAP)
);
assert_eq!(
Specified.preparation_delay(),
(PREP_DELAY_MEAN, PREP_DELAY_CAP)
);
assert_eq!(Specified.anchor_age_cap(), ANCHOR_AGE_CAP);
assert_eq!(Specified.expiry_window(), (EXPIRY_MODULUS, EXPIRY_WINDOW));
}
#[test]
fn an_implementor_may_override_only_the_interval() {
#[derive(Clone)]
struct ShortGrid;
impl PoolMigrationConstants for ShortGrid {
fn anchor_bucket_interval(&self) -> AnchorBucketInterval {
AnchorBucketInterval::custom(NonZeroU32::new(12).expect("12 is nonzero"))
}
}
assert_eq!(ShortGrid.anchor_bucket_interval().block_count().get(), 12);
assert_eq!(ShortGrid.denomination_cap(), DENOM_CAP);
assert_eq!(ShortGrid.preparation_tx_actions(), PREP_TX_ACTIONS);
}
#[test]
fn overridden_bounds_narrow_the_canonical_set() {
#[derive(Clone)]
struct SmallCap;
impl PoolMigrationConstants for SmallCap {
fn denomination_cap(&self) -> Zatoshis {
Zatoshis::const_from_u64(COIN)
}
}
let two_zec = Zatoshis::const_from_u64(2 * COIN);
assert!(is_canonical_denomination(two_zec));
assert!(!SmallCap.is_canonical_denomination(two_zec));
assert!(SmallCap.is_canonical_denomination(Zatoshis::const_from_u64(COIN)));
}
#[derive(Clone)]
struct Zip318Params;
impl PoolMigrationConstants for Zip318Params {}
#[test]
fn canonical_expiry_matches_the_specified_window() {
for height in [0u32, 1, 144, 34_559, 34_560, 2_000_000] {
let h = BlockHeight::from_u32(height);
assert_eq!(Zip318Params.canonical_expiry(h), expiry_height(h));
assert!(Zip318Params.is_canonical_expiry(expiry_height(h), h));
assert!(!Zip318Params.is_canonical_expiry(h + 40, h));
}
}
#[test]
fn canonical_expiry_honours_an_overridden_window() {
#[derive(Clone)]
struct ShortWindow;
impl PoolMigrationConstants for ShortWindow {
fn expiry_window(&self) -> (u32, u32) {
(100, 200)
}
}
let h = BlockHeight::from_u32(250);
assert_eq!(ShortWindow.canonical_expiry(h), BlockHeight::from_u32(400));
assert!(ShortWindow.is_canonical_expiry(BlockHeight::from_u32(400), h));
assert!(!ShortWindow.is_canonical_expiry(expiry_height(h), h));
}
#[test]
fn the_height_independent_expiry_test_admits_canonical_expiries() {
for height in [0u32, 1, 144, 34_559, 34_560, 2_000_000] {
let h = BlockHeight::from_u32(height);
assert!(
Zip318Params.is_canonical_expiry_value(expiry_height(h)),
"the canonical expiry for {height} must be admitted without a reference height"
);
assert!(
!Zip318Params.is_canonical_expiry_value(h + 40),
"an ordinary expiry for {height} must be rejected"
);
}
let other_period = expiry_height(BlockHeight::from_u32(0));
let mined_much_later = BlockHeight::from_u32(10 * EXPIRY_MODULUS);
assert!(Zip318Params.is_canonical_expiry_value(other_period));
assert!(!Zip318Params.is_canonical_expiry(other_period, mined_much_later));
}
#[test]
fn the_classification_encoding_round_trips() {
for classification in [
Zip318Classification::Unknown,
Zip318Classification::Nonconforming,
Zip318Classification::Conforms(Zip318TxKind::Preparation),
Zip318Classification::Conforms(Zip318TxKind::Transfer),
] {
assert_eq!(
Zip318Classification::from_code(classification.to_code()),
classification
);
}
assert_eq!(Zip318Classification::Unknown.to_code(), 0);
assert_eq!(
Zip318Classification::from_code(0),
Zip318Classification::Unknown
);
assert_ne!(
Zip318Classification::Unknown.to_code(),
Zip318Classification::Nonconforming.to_code(),
"never classified must not encode as classified-and-refused"
);
for unrecognised in [4, 99, -1] {
assert_eq!(
Zip318Classification::from_code(unrecognised),
Zip318Classification::Unknown
);
}
}
fn prep_evidence() -> Zip318Evidence {
Zip318Evidence::default()
.with_source_actions(Some(PREP_TX_ACTIONS))
.with_destination_actions(Some(0))
.with_other_bundles_present(Some(false))
.with_source_is_send_to_self(Some(true))
.with_expiry_is_canonical(Some(true))
.with_anchor_on_grid(Some(true))
.with_fee_is_canonical(Some(true))
}
fn crossing_evidence(value: Zatoshis) -> Zip318Evidence {
Zip318Evidence::default()
.with_source_actions(Some(CROSSING_SOURCE_ACTIONS))
.with_destination_actions(Some(CROSSING_DESTINATION_ACTIONS))
.with_other_bundles_present(Some(false))
.with_sole_destination_value(Some(value))
.with_expiry_is_canonical(Some(true))
.with_anchor_on_grid(Some(true))
.with_fee_is_canonical(Some(true))
}
#[test]
fn no_evidence_is_unknown() {
assert_eq!(
classify(&Zip318Evidence::default(), &Zip318Params),
Zip318Classification::Unknown
);
}
#[test]
fn a_canonical_preparation_conforms() {
assert_eq!(
classify(&prep_evidence(), &Zip318Params),
Zip318Classification::Conforms(Zip318TxKind::Preparation)
);
}
#[test]
fn a_canonical_crossing_conforms() {
assert_eq!(
classify(
&crossing_evidence(Zatoshis::const_from_u64(COIN)),
&Zip318Params
),
Zip318Classification::Conforms(Zip318TxKind::Transfer)
);
}
#[test]
fn each_clause_refutes_on_its_own() {
let cases: [(&str, Zip318Evidence); 5] = [
(
"an ordinary split has a different action count",
prep_evidence().with_source_actions(Some(PREP_TX_ACTIONS + 1)),
),
(
"no ZIP 318 transaction carries another bundle",
prep_evidence().with_other_bundles_present(Some(true)),
),
(
"an ordinary expiry is not the rolling one",
prep_evidence().with_expiry_is_canonical(Some(false)),
),
(
"a preparation output paying elsewhere is not a send-to-self",
prep_evidence().with_source_is_send_to_self(Some(false)),
),
(
"a crossing of an off-series amount joins no anonymity set",
crossing_evidence(Zatoshis::const_from_u64(3 * COIN)),
),
];
for (why, evidence) in cases {
assert_eq!(
classify(&evidence, &Zip318Params),
Zip318Classification::Nonconforming,
"{why}"
);
}
}
#[test]
fn a_coincidental_expiry_alone_does_not_conform() {
let evidence = prep_evidence()
.with_other_bundles_present(Some(true))
.with_expiry_is_canonical(Some(true));
assert_eq!(
classify(&evidence, &Zip318Params),
Zip318Classification::Nonconforming
);
}
#[test]
fn confirmatory_clauses_refute_but_do_not_block() {
for knock_out in [
prep_evidence().with_anchor_on_grid(Some(false)),
prep_evidence().with_fee_is_canonical(Some(false)),
] {
assert_eq!(
classify(&knock_out, &Zip318Params),
Zip318Classification::Nonconforming
);
}
let unanswerable = prep_evidence()
.with_anchor_on_grid(None)
.with_fee_is_canonical(None);
assert_eq!(
classify(&unanswerable, &Zip318Params),
Zip318Classification::Conforms(Zip318TxKind::Preparation)
);
}
#[test]
fn an_unanswered_required_clause_is_unknown_not_a_refutation() {
let knock_outs = [
prep_evidence().with_source_actions(None),
prep_evidence().with_destination_actions(None),
prep_evidence().with_other_bundles_present(None),
prep_evidence().with_expiry_is_canonical(None),
prep_evidence().with_source_is_send_to_self(None),
];
for evidence in knock_outs {
assert_eq!(
classify(&evidence, &Zip318Params),
Zip318Classification::Unknown
);
}
let no_output =
crossing_evidence(Zatoshis::const_from_u64(COIN)).with_sole_destination_value(None);
assert_eq!(
classify(&no_output, &Zip318Params),
Zip318Classification::Unknown
);
}
#[test]
fn classification_is_monotone_in_the_evidence() {
for full in [
prep_evidence(),
crossing_evidence(Zatoshis::const_from_u64(COIN)),
prep_evidence().with_other_bundles_present(Some(true)),
] {
let chain = [
Zip318Evidence::default(),
Zip318Evidence::default()
.with_source_actions(full.source_actions())
.with_destination_actions(full.destination_actions()),
full.with_sole_destination_value(None)
.with_expiry_is_canonical(None),
full,
];
let mut decided: Option<Zip318Classification> = None;
for evidence in chain {
match classify(&evidence, &Zip318Params) {
Zip318Classification::Unknown => assert!(
decided.is_none(),
"evidence grew and a decision reverted to Unknown"
),
settled => {
if let Some(earlier) = decided {
assert_eq!(earlier, settled, "evidence grew and the decision changed");
}
decided = Some(settled);
}
}
}
}
}
}