Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions contracts/sysio.chalg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ OPP envelope dispute resolution and slash-execution contract.
## Responsibility

- Resolves conflicting OPP outpost envelopes via a Tier-1 node-owner vote when the automatic
consensus rules in `sysio.msgch` cannot (a 3+-way split with no majority for one (outpost, epoch))
consensus rules in `sysio.msgch` see two or more versions with no strict majority after the epoch
boundary for one (outpost, epoch), including when an otherwise eligible operator was silent
- Pauses epoch advancement while a dispute is open and releases it on resolution
- Dispatches the winning envelope (via `sysio.msgch::resolvedisp`) once a checksum wins
- Executes slashing of operators through `sysio.opreg` -- the single slashing chokepoint that holds
Expand All @@ -29,16 +30,17 @@ OPP envelope dispute resolution and slash-execution contract.

## Dispute-vote flow

1. **Open**: `sysio.msgch::evalcons` sees the active batch operators deliver 3+ distinct envelope
versions for one (outpost, epoch) with no majority, and calls `opendispute` inline. The dispute
records the candidate checksums, snapshots the active ROA generation's Tier-1 electorate and
fixed quorum, and pauses `sysio.epoch`.
1. **Open**: `sysio.msgch::evalcons` calls `opendispute` inline for a post-boundary no-majority
split with at least two versions, regardless of whether every eligible operator delivered. The
dispute records the candidate checksums, snapshots the active ROA generation's Tier-1 electorate
and fixed quorum, and pauses `sysio.epoch`.
2. **Vote**: owners in the dispute's frozen Tier-1 electorate call `votedispute` with one of the
candidate checksums. Later ROA registrations cannot join an in-flight dispute. One vote per owner.
3. **Tally**: anyone cranks `chkdispute`. With `N` equal to the snapshotted electorate size and
fixed `Q = floor(N/2)+1`, a checksum reaching `Q` votes wins at any time (fast path); after the 24h
deadline the bar relaxes to a quorum of cast votes (`cast >= Q`) plus a strict majority of cast
(`2*votes > cast`). No plurality / tie-break -- an undecided tally keeps waiting for votes.
(`2*votes > cast`). No plurality / tie-break -- an undecided tally remains open and keeps the
epoch paused until Tier-1 supplies a resolvable vote.
4. **Resolve**: the winning checksum is recorded and dispatched via `sysio.msgch::resolvedisp`.
`sysio.epoch` is unpaused when the final open dispute resolves. The next
`sysio.epoch::advance` then slashes every operator that delivered a non-canonical checksum for
Expand Down
22 changes: 15 additions & 7 deletions contracts/sysio.chalg/include/sysio.chalg/sysio.chalg.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@

namespace sysio {

namespace chalg_limits {
/// Minimum number of distinct envelope versions required to make a consensus split
/// adjudicable by the Tier-1 dispute vote.
inline constexpr uint32_t minimum_dispute_candidate_versions = 2;
} // namespace chalg_limits

class [[sysio::contract("sysio.chalg")]] chalg : public contract {
public:
using contract::contract;
Expand Down Expand Up @@ -42,9 +48,11 @@ namespace sysio {
// OPP envelope dispute vote (Tier-1 node-owner resolution)
// -----------------------------------------------------------------------

/// Open an OPP envelope dispute. Called inline by `sysio.msgch::evalcons` when the active
/// batch operators delivered 3+ distinct envelope versions for one (outpost, epoch) with no
/// majority. Records the candidate checksums, snapshots the Tier-1 electorate (the Tier-1
/// Open an OPP envelope dispute. Called inline by `sysio.msgch::evalcons` for a post-boundary
/// no-majority split with at least `chalg_limits::minimum_dispute_candidate_versions` distinct
/// versions. msgch owns the consensus boundary and strict-majority checks because it alone has
/// the live eligible group and delivery tally.
/// Records the candidate checksums, snapshots the Tier-1 electorate (the Tier-1
/// rows of `sysio.roa::nodeowners` for the current network generation) together with its
/// quorum, and pauses epoch advancement until a Tier-1 node-owner vote resolves the
/// canonical envelope. Rejects opening when no Tier-1 node owner is registered: an
Expand Down Expand Up @@ -212,10 +220,10 @@ namespace sysio {
SYSLIB_SERIALIZE(dispute_key, (id))
};

/// OPP envelope dispute. Opened on a 3+-way no-majority split for one (outpost, epoch);
/// resolved by a Tier-1 node-owner vote on the canonical checksum. The row is retained after
/// resolution as the audit record (and as the guard that prevents re-opening the same
/// (outpost, epoch) dispute).
/// OPP envelope dispute. Opened for a post-boundary no-majority split with at least two
/// versions for one (outpost, epoch); resolved by a Tier-1 node-owner vote on the canonical
/// checksum. The row is retained after resolution as the audit record (and as the guard that
/// prevents re-opening the same (outpost, epoch) dispute).
struct [[sysio::table("disputes")]] dispute_entry {
uint64_t id;
uint64_t chain_code; ///< outpost slug_name value
Expand Down
8 changes: 6 additions & 2 deletions contracts/sysio.chalg/src/sysio.chalg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ constexpr name ram_payer = "sysio"_n;

namespace {

/// Rejection text for a dispute without enough competing envelope versions to adjudicate.
constexpr const char* DISPUTE_REQUIRES_TWO_CANDIDATES =
"a dispute requires at least two candidate envelope versions";

/// WIRE asset symbol for the challenge-bond escrow + payouts (9 decimals — mirrors
/// `sysio.reserv`'s WIRE_SYMBOL; deliberately NOT opreg's CORE_SYM).
constexpr sysio::symbol WIRE_SYMBOL{"WIRE", 9};
Expand Down Expand Up @@ -244,8 +248,8 @@ void chalg::opendispute(uint64_t chain_code,
uint32_t epoch_index,
std::vector<dispute_candidate> candidates) {
require_auth(MSGCH_ACCOUNT);
check(candidates.size() >= 3,
"a dispute requires at least 3 candidate envelope versions");
check(candidates.size() >= chalg_limits::minimum_dispute_candidate_versions,
DISPUTE_REQUIRES_TWO_CANDIDATES);

disputes_t disputes(get_self());

Expand Down
Binary file modified contracts/sysio.chalg/sysio.chalg.wasm
Binary file not shown.
28 changes: 17 additions & 11 deletions contracts/sysio.msgch/src/sysio.msgch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ constexpr size_t ENVELOPE_BASELINE_BYTES = 512;
constexpr const char* UIC_DISPATCH_REJECTED_LOG_PREFIX =
"UIC_DISPATCH_REJECTED";

/// Diagnostic for a split with fewer competing versions than a Tier-1 vote can adjudicate.
constexpr const char* DISPUTE_TOO_FEW_CANDIDATES_LOG =
"msgch::maybe_open_dispute: no dispute for (chain=%llu, epoch=%u): "
"%u distinct version(s), a vote needs >=%u\n";

uint32_t current_epoch_index() {
epoch::epochstate_t tbl(EPOCH_ACCOUNT);
return tbl.exists() ? tbl.get().current_epoch_index : 0;
Expand Down Expand Up @@ -1175,10 +1180,10 @@ void dispatch_attestation(name self, uint64_t attestation_id,
return true;
}

/// Evaluate the dispute trigger and, if met, open a Tier-1 dispute vote via sysio.chalg. Trigger:
/// the epoch boundary has passed, 3+ distinct envelope versions exist, and no version holds a
/// majority of the operator group. A majority — even within a 3+-way split — resolves without a
/// vote, so it is not a trigger; a sub-3-way or pre-boundary split just waits for more deliveries.
/// Evaluate the dispute trigger and, if met, open a Tier-1 dispute vote via sysio.chalg. A
/// post-boundary no-majority split with at least two versions is anomalous enough to require Tier-1
/// adjudication, including when an eligible operator was silent. A strict majority always resolves
/// without a vote; a one-version or pre-boundary split waits for more deliveries.
void maybe_open_dispute(name self, uint64_t chain_code, uint32_t epoch_index,
uint32_t group_size,
const std::vector<checksum256>& seen_checksums,
Expand All @@ -1187,9 +1192,10 @@ void maybe_open_dispute(name self, uint64_t chain_code, uint32_t epoch_index,
// OPP silent-return diagnostics: each branch below silently declines to open a
// dispute. Logged (visible under --contracts-console) so "the dispute never
// opened" is greppable instead of a black hole.
if (seen_checksums.size() < 3) {
sysio::print_f("msgch::maybe_open_dispute: no dispute for (chain=%llu, epoch=%u): %u distinct version(s), a vote needs >=3\n",
chain_code, epoch_index, (uint32_t)seen_checksums.size());
if (seen_checksums.size() < chalg_limits::minimum_dispute_candidate_versions) {
sysio::print_f(DISPUTE_TOO_FEW_CANDIDATES_LOG,
chain_code, epoch_index, (uint32_t)seen_checksums.size(),
chalg_limits::minimum_dispute_candidate_versions);
return;
}

Expand Down Expand Up @@ -1440,8 +1446,8 @@ void msgch::evalcons(uint64_t chain_code, uint32_t epoch_index) {
};

// Group envelopes by checksum, tracking the operators that delivered each version (CDT-compatible
// parallel vectors). The per-version operator lists become the dispute candidates on a 3+-way
// split.
// parallel vectors). The per-version operator lists become the dispute candidates on a terminal
// two-way or an existing multi-version split.
std::vector<checksum256> seen_checksums;
std::vector<uint32_t> checksum_counts;
std::vector<std::vector<char>> checksum_data;
Expand Down Expand Up @@ -1498,8 +1504,8 @@ void msgch::evalcons(uint64_t chain_code, uint32_t epoch_index) {
}

if (!consensus_reached) {
// No automatic consensus. On a 3+-way no-majority split past the epoch boundary, open a
// Tier-1 dispute vote; a smaller or pre-boundary split just waits for more deliveries.
// No automatic consensus. A two-or-more-version no-majority split past the epoch boundary
// opens a Tier-1 dispute vote; a one-version or pre-boundary split waits for more deliveries.
maybe_open_dispute(get_self(), chain_code, epoch_index, group_size,
seen_checksums, checksum_counts, checksum_operators);
return;
Expand Down
Binary file modified contracts/sysio.msgch/sysio.msgch.wasm
Binary file not shown.
18 changes: 13 additions & 5 deletions contracts/tests/sysio.dispute_tests.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// Contract tests for the OPP envelope dispute vote (sysio.chalg dispute-vote flow).
///
/// Covers the new chalg actions in isolation and against a minimally-bootstrapped OPP stack:
/// * opendispute -- auth (sysio.msgch), >=3 candidates, no duplicate (outpost,epoch), pauses
/// * opendispute -- auth (sysio.msgch), >=2 candidates, no duplicate (outpost,epoch), pauses
/// epoch, snapshots the Tier-1 electorate + quorum from sysio.roa::nodeowners
/// (rejecting an empty electorate)
/// * votedispute -- electorate-snapshot eligibility (the Tier-1 set frozen at open; later
Expand Down Expand Up @@ -478,14 +478,22 @@ BOOST_FIXTURE_TEST_CASE(opendispute_requires_msgch_auth, sysio_dispute_tester) {
opendispute(eth_code(), current_epoch(), cands, /*signer=*/"voter1"_n));
} FC_LOG_AND_RETHROW() }

BOOST_FIXTURE_TEST_CASE(opendispute_requires_three_candidates, sysio_dispute_tester) { try {
/// A two-version tie has no automatic majority, so chalg must accept it as an adjudicable dispute.
BOOST_FIXTURE_TEST_CASE(opendispute_accepts_two_candidates, sysio_dispute_tester) { try {
std::vector<fc::variant> two{
candidate(fc::sha256::hash(std::string("a")), {BATCHOP}),
candidate(fc::sha256::hash(std::string("b")), {"voter1"_n}),
};
BOOST_REQUIRE_EQUAL(
error("assertion failure with message: a dispute requires at least 3 candidate envelope versions"),
opendispute(eth_code(), current_epoch(), two));
BOOST_REQUIRE_EQUAL(success(), opendispute(eth_code(), current_epoch(), two));
BOOST_REQUIRE_EQUAL(two.size(), get_dispute(1)["candidates"].get_array().size());
} FC_LOG_AND_RETHROW() }

/// One envelope version has no competing candidate, so chalg must retain the two-version floor.
BOOST_FIXTURE_TEST_CASE(opendispute_rejects_one_candidate, sysio_dispute_tester) { try {
std::vector<fc::variant> one{
candidate(fc::sha256::hash(std::string("a")), {BATCHOP}),
};
BOOST_REQUIRE(opendispute(eth_code(), current_epoch(), one) != success());
} FC_LOG_AND_RETHROW() }

BOOST_FIXTURE_TEST_CASE(opendispute_rejects_duplicate, sysio_dispute_tester) { try {
Expand Down
Loading
Loading