This commit is contained in:
2026-06-12 15:33:51 -04:00
parent 2213aab723
commit d2d2a4d596
5 changed files with 719 additions and 350 deletions

View File

@@ -0,0 +1,198 @@
<?php
/**
* cry01_attestation.php — STUB INTERFACES ONLY. Nothing in this file is
* implemented. Every function returns a "not implemented" placeholder.
*
* This file documents the planned shape of two related future features:
*
* 1. ATTESTATION — W3C Verifiable Credentials + OpenTimestamps proofs for
* records produced by cry01 (balance lookups, ballot definitions,
* ballot outcomes). Per CRY01-SPEC.md, this is part of the shared
* credential infrastructure that poll01 and future addons depend on.
*
* 2. BALLOT TALLYING — the wallet-to-wallet voting model described below.
* This is the END GOAL that g1wallet, cry01, and poll01 are jointly
* building toward. Nothing here is implemented; this file exists so
* the eventual implementation has an interface to fill in rather than
* bolting tallying logic onto whatever shape cry01_chain.php happens
* to have at the time.
*
* ============================================================================
* THE BALLOT MODEL (as designed, 2026-06-12)
* ============================================================================
*
* For a given poll (e.g. "Should the association approve budget item X?"):
*
* - Two Ğ1 wallets are provisioned for the poll: a YES wallet and a NO
* wallet. Who controls these keys and how they are provisioned is NOT
* YET DECIDED — see OPEN QUESTIONS below.
*
* - Each eligible voter (a SASE-verified participant with a Ğ1 wallet) may
* send AT MOST ONE TOKEN to either the YES or NO wallet during the
* voting period. The transaction itself — wallet-to-wallet, with a
* block timestamp — IS the vote and IS the timestamp. No separate OTS
* step is needed for individual votes.
*
* - TALLY RULE: count only the FIRST token received from each eligible
* voter's wallet (by block order). Any additional tokens from the same
* wallet, to either YES or NO, are ignored for tallying purposes. This
* is an APPLICATION-LAYER rule — nothing on-chain prevents a wallet from
* sending multiple tokens; cry01/poll01 must enforce "first token only"
* when counting.
*
* - ABSTENTION: (number of eligible voters) - (yes_count + no_count) =
* abstentions. No "spoiled ballot" state exists — a voter either sent
* exactly one token to one wallet, or did not vote.
*
* - AFTER THE VOTE: all tokens in both YES and NO wallets are sent to the
* association's wallet, to be used as the homeowners subsequently
* decide. This transfer is itself a public, timestamped on-chain event
* — closing the books on the poll.
*
* - PUBLIC BALLOT BY DESIGN: every vote is a public on-chain transaction,
* permanently linking a voter's wallet address to their choice. This is
* intentional — Illinois law and federal law do not require HOA votes
* to be secret, and public attribution provides verifiability without
* requiring trust in the board's counting.
*
* ============================================================================
* OPEN QUESTIONS (not blockers — design space the interfaces below must
* accommodate, not resolve)
* ============================================================================
*
* - YES/NO wallet provisioning: who creates these wallets, who holds the
* keys during the voting period, how is the post-vote transfer to the
* association wallet authorized/triggered (manual operator action vs.
* some automated/multisig mechanism)?
*
* - Voter eligibility registry: cry01_get_eligible_voters() needs a source
* of truth for "which Ğ1 account IDs belong to SASE-verified members of
* this association, as of the poll's start date". This likely overlaps
* with the g1wallet pubkey-registration flow (cry01's signal form
* already has a data-g1wallet-target="pubkey" field) but the
* association-membership-at-a-point-in-time registry does not exist yet.
*
* - Participation barrier: sending a token requires the voter to HOLD at
* least one Ğ1 and pay any associated transaction cost. Whether this is
* a deliberate "skin in the game" feature or an unintended barrier for
* participants who hold zero Ğ1 is a POLICY decision, not addressed
* here. cry01_get_eligible_voters() and cry01_tally_ballot() should not
* assume either answer — e.g. don't assume "no token sent" always means
* "chose not to vote" vs. "could not afford to vote", though the
* externally-visible RESULT (abstention) is the same either way.
*
* ============================================================================
* THE TRANSACTION-HISTORY DEPENDENCY
* ============================================================================
*
* cry01_get_wallet_transactions() is the hard dependency underlying
* cry01_tally_ballot(). Per docs/DUNITER-RPC-FINDINGS.md §4.3, NEITHER
* current Duniter node (light mirror or full-fast-sync) can answer "what
* transactions has account X sent/received" — both use
* --state-pruning 256, which only retains recent state, not transaction
* history. Tallying a ballot requires scanning all transactions to the
* YES/NO wallets over the poll's duration, which requires either:
*
* - an archive node (--state-pruning archive), much larger disk footprint
* than either current node, or
* - a separate indexer (e.g. a Subsquid/Squid instance, as referenced in
* Duniter's own public-RPC documentation) that processes blocks as they
* arrive and maintains a queryable transaction database
*
* This is a SEPARATE INFRASTRUCTURE PROJECT from anything in cry01_chain.php
* or cry01_substrate.php. The interface below assumes it exists; building it
* is future work.
*/
// ----------------------------------------------------------------------------
// ATTESTATION (VC + OpenTimestamps)
// ----------------------------------------------------------------------------
function cry01_attest_record($record_type, $data, $block_hash = null) {
// Produces a W3C Verifiable Credential + OpenTimestamps proof for a
// cry01 record.
//
// $record_type: one of 'lookup', 'ballot_definition', 'ballot_outcome'
// (open to extension — these are the three currently anticipated).
// $data: the record's content — shape depends on $record_type. For
// 'lookup', likely {account_id, account_info, queried_at}. For
// 'ballot_definition'/'ballot_outcome', shape TBD alongside
// cry01_tally_ballot().
// $block_hash: optional — if the record pertains to a specific on-chain
// state (e.g. a balance lookup result), the block hash it was read at,
// for reproducibility.
//
// Returns null (not implemented). When implemented, should return an
// array containing at minimum the VC document and the OTS proof/receipt
// (or a reference to where the OTS proof can be retrieved — OTS
// calendar submission is asynchronous, per CRY01-SPEC.md's
// ots_calendar_url config).
logger('cry01_attestation: cry01_attest_record not yet implemented');
return null;
}
// ----------------------------------------------------------------------------
// BALLOT TALLYING
// ----------------------------------------------------------------------------
function cry01_get_eligible_voters($association_slug, $poll_id) {
// Returns the list of Ğ1 account IDs (raw 32-byte, or hex — TBD) eligible
// to vote in the given poll for the given association.
//
// Source of truth: NOT YET DESIGNED. See "Voter eligibility registry" in
// the open questions above. Likely candidates: a snapshot of registered
// g1wallet pubkeys for SASE-verified members of $association_slug, taken
// at the poll's start time (so membership changes during voting don't
// retroactively change eligibility).
//
// Returns null (not implemented).
logger('cry01_attestation: cry01_get_eligible_voters not yet implemented');
return null;
}
function cry01_get_wallet_transactions($account_id_bytes, $since_block = null) {
// Returns all incoming transactions to the given account, optionally
// since a given block number, ordered by block number ascending.
//
// THIS IS THE HARD DEPENDENCY — see "THE TRANSACTION-HISTORY DEPENDENCY"
// above. Neither current Duniter node can answer this query. Requires
// an archive node or a separate indexer — both out of scope for cry01
// application code; this function is the integration point once that
// infrastructure exists.
//
// Expected return shape (once implemented): array of
// {from: account_id, amount: decimal_string, block_number: int, block_hash: string, timestamp: ...}
//
// Returns null (not implemented).
logger('cry01_attestation: cry01_get_wallet_transactions not yet implemented (requires archive node or indexer — see docs/DUNITER-RPC-FINDINGS.md)');
return null;
}
function cry01_tally_ballot($yes_wallet_account_id, $no_wallet_account_id, $eligible_voters, $poll_start_block = null, $poll_end_block = null) {
// Tallies a ballot using the wallet-to-wallet voting model.
//
// TALLY RULE (see module docblock): for each eligible voter, find their
// FIRST transaction (by block order, within [$poll_start_block,
// $poll_end_block] if given) to EITHER $yes_wallet_account_id or
// $no_wallet_account_id. Count that as their vote. Any subsequent
// transactions from the same voter, to either wallet, are ignored.
// Voters with no qualifying transaction are abstentions.
//
// Depends on cry01_get_wallet_transactions() for both wallets — see the
// hard dependency note above. Also depends on $eligible_voters from
// cry01_get_eligible_voters().
//
// Expected return shape (once implemented):
// {
// yes: int,
// no: int,
// abstain: int,
// total_eligible: int,
// votes: [ {voter: account_id, choice: 'yes'|'no', block_number: int}, ... ]
// }
//
// Returns null (not implemented).
logger('cry01_attestation: cry01_tally_ballot not yet implemented');
return null;
}