Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

tally — refund accounting

audience: contributors

Proposed source: crates/tally/.

The refund-accounting organism. Watches the chain for inclusion of the lattice’s winning blocks, joins the upstream organisms’ public commit logs to compute attribution, commits one Refunds entry per included block, and publishes ECDSA Attestations that integrators present to an on-chain settlement contract.

Tally is the last non-reversible step in the lattice pipeline; everything upstream of it can fail or degrade gracefully, tally just doesn’t commit for that slot.

Crate layout

  • tally::proto — wire types for attributions, attestations, on-chain evidence. No I/O.
  • tally::core — pure attribution computation: given a winning block and all upstream commits, who gets what.
  • tally::chain — chain-RPC watcher. One module per supported chain backend.
  • tally::node — mosaik integration. TallyMachine, role event loops, attestation signing.

Public facade:

  • tally::Tally::<A>::read(&network, &Config) -> Reader<A> — read-side for integrators claiming refunds.
  • tally::Tally::<A>::attestations(&network, &Config) -> Attestations — presentable attestations for on-chain settlement.
  • tally::Config — pins committee pubkeys, settlement contract address, chain RPC.

Public surface

Refunds collection

declare::collection! {
    pub Refunds<A> = Vec<Attribution<A>>,
    derive_id: TALLY_ROOT.derive("refunds"),
    consumer require_ticket: LATTICE_READ_TICKET,
    writer   require_ticket: TallyMember,
}

pub struct Attribution<A: AttributionDatum> {
    pub slot:         u64,
    pub block_hash:   [u8; 32],
    pub recipients:   Vec<Recipient>,
    pub evidence:     Evidence,
    pub committed_at: UnixSecs,
}

pub struct Recipient {
    pub addr:   [u8; 20],
    pub amount: u128,
    pub kind:   RecipientKind,
}

pub enum RecipientKind {
    OrderflowProvider { submission: SubmissionRef },
    BidWinner         { bid:        BidRef        },
    CoBuilder         { member:     BlsPub        },
    Proposer          { share:      ProposerShare },
}

pub struct Evidence {
    pub zipnet_broadcasts:  Vec<BroadcastRef>,
    pub unseal_pool:        Vec<UnsealedRef>,
    pub offer_outcome:      OutcomeRef,
    pub atelier_candidate:  CandidateRef,
    pub relay_accepted:     AcceptedRef,
    pub on_chain_inclusion: OnChainRef,
}

Attestations collection

declare::collection! {
    pub Attestations = Vec<Attestation>,
    derive_id: TALLY_ROOT.derive("attestations"),
    consumer require_ticket: Open,   // world-readable
    writer   require_ticket: TallyMember,
}

pub struct Attestation {
    pub slot:        u64,
    pub block_hash:  [u8; 32],
    pub recipient:   [u8; 20],
    pub amount:      u128,
    pub kind_digest: [u8; 32],
    pub signatures:  Vec<(TallyMemberId, Secp256k1Sig)>,
}

Attestations are deliberately world-readable: on-chain settlement contracts verify signatures against published tally member pubkeys, and the attestation’s recipient is already public information (it is the payout address).

Internal plumbing

Derived private network (TALLY_ROOT.derive("private")):

  • ChainWatchGossip — per-member observation of on-chain inclusion, reconciled before ObserveInclusion is proposed.

State machine

pub enum Command {
    ObserveInclusion(InclusionReport),
    ComputeAttribution(AttributionDraft),
    CommitRefund(CommitRefund),
    SubmitAttestationSignature(AttestationShare),
    MarkDone(u64),
}

pub enum Query {
    RefundFor(u64),
    AttestationsFor(u64),
    PendingInclusions,
    ChainHeadLag,
}

Apply semantics

  • ObserveInclusion. Committee member reports that block block_hash at slot S has been observed on-chain. Validation: the atelier::Candidates[S] with matching hash exists; the relay::AcceptedHeaders[S] references that candidate. First-report-per-slot wins.
  • ComputeAttribution. Any committee member computes the deterministic attribution for an observed slot and proposes it. Validation: the draft matches what tally::core::compute produces from the upstream evidence. Duplicate drafts from other members either match (silent dedupe) or reject (integration bug — this is the tally_evidence_failures metric).
  • CommitRefund. Leader-issued after ComputeAttribution has majority agreement. Apply handler appends Attribution to Refunds.
  • SubmitAttestationSignature. Each committee member signs the committed attribution with its ECDSA key; the signatures land here. When t signatures are present (t from the settlement contract’s threshold, not the Raft majority), the aggregate is appended to Attestations.
  • MarkDone. GC after a slot is past the settlement contract’s claim window.

Attribution algorithm (tally::core::compute)

Deterministic function signature:

pub fn compute(
    block:        &Candidate<B>,
    auction:      &CommittedOutcome,
    unsealed:     &UnsealedRound,
    broadcasts:   &Broadcasts,
    inclusion:    &OnChainRef,
    policy:       &AttributionPolicy,
) -> Attribution<A> { ... }

AttributionPolicy is a per-lattice parameter that folds into the tally fingerprint. The reference policy (AttributionPolicy::Default) is:

  1. Total MEV = block's coinbase_transfer - baseline_reward.
  2. Winning searcher gets auction.bid * searcher_share_pct.
  3. Each wallet whose zipnet submission landed in the block gets (tx_value / total_tx_value) * orderflow_share_pct of the remaining MEV.
  4. Co-builders (Phase 2) split cobuilder_share_pct equally.
  5. The proposer gets whatever is left (via the on-chain coinbase transfer; no explicit recipient in Refunds).

Shares are pinned in the policy and fold into the signature.

Invariants

  1. One Refunds entry per included slot. Enforced in CommitRefund.
  2. Attribution is a deterministic function of the upstream evidence plus the policy. Every committee member computes the same draft.
  3. Attestations are idempotent under committee rotation. An attestation issued by committee set C_k is still valid after C_k retires, as long as the settlement contract’s pubkey list includes C_k’s keys (rotation is a pubkey-set extension, not a replacement).
  4. Nothing tally commits can be rolled back. Once an Attestation is in the collection, it is meant to land on-chain; post-commit corrections happen via the settlement contract’s dispute mechanism if any, not via a Refunds rewrite.

Signature versioning

fn signature(&self) -> UniqueId {
    let tag = format!(
        "tally.v{WIRE_VERSION}.policy={}.settlement={:x}.chain_backend={}.committee={}",
        self.config.policy.tag(),
        self.config.settlement_addr,
        self.config.chain_backend.tag(),
        self.config.committee_size,
    );
    UniqueId::from(tag.as_str())
}

Changing the attribution policy shares is a fingerprint change. Changing the settlement contract address is a fingerprint change. Swapping chain backends (e.g. from a full-node RPC to an indexer) is a fingerprint change.

Chain backend

tally::chain abstracts the inclusion watcher:

pub trait ChainBackend: Send + Sync {
    async fn head(&self) -> Result<(u64, [u8; 32])>;
    async fn block(&self, slot: u64) -> Result<Option<BlockInfo>>;
    async fn coinbase_transfer(&self, block: &BlockInfo) -> Result<u128>;
    fn tag(&self) -> &'static str;
}

Implementations:

  • L1FullNode — Ethereum full-node JSON-RPC.
  • L2Rollup — op-node / op-geth dual-source reader.
  • Archival — historical indexer for slow back-fill.

A lattice pinning chain_backend: L1FullNode and pointing at an archival indexer is a configuration mismatch that the fingerprint catches at deploy time.

ACL composition

impl Config {
    pub fn member_validator(&self) -> Validators {
        Validators::stacked().with(JwtIssuer::from(self.operator_jwt_key))
    }
}

Tally has no TDX requirement in v1. The settlement contract is the ground truth; mis-attestations cannot be paid out. Integrators who distrust tally’s committee can compute attribution themselves from the upstream organisms.

Trust shape

Majority-honest for Refunds and Attestations integrity; settlement contract is the ultimate arbiter. See threat-model.md — tally.

Open questions

  • Settlement contract interface standardization. Different lattices running different chains will have different contract conventions. Do we ship a reference ABI? Open; the v1 target is one contract per lattice, bespoke to the chain.
  • Refund policy extensibility. The AttributionPolicy enum covers a small set of reference splits. Custom policies (e.g. per-bundle cap, per-wallet throttle) need a more expressive policy DSL. Not speced; a lattice with custom needs ships a fork of tally with its own policy variant.
  • Cross-lattice attribution. When a bundle spans two lattices (cross-chain backrun), whose tally attributes? v1 answer: each tally attributes the local slice; searchers integrate across lattices in their own agent. The bridge organism shape in cross-chain.md — Shape 3 is the right answer for tighter coupling.
  • Reorg handling. If the block we attributed reorgs out, the attestations we emitted are invalid. v1 commits optimistically and relies on the settlement contract’s finality check. A delayed commit (after k confirmations) is safer and trivially added — but changes the SLA tally exposes to integrators.

Cross-references