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 beforeObserveInclusionis 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 blockblock_hashat slotShas been observed on-chain. Validation: theatelier::Candidates[S]with matching hash exists; therelay::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 whattally::core::computeproduces from the upstream evidence. Duplicate drafts from other members either match (silent dedupe) or reject (integration bug — this is thetally_evidence_failuresmetric).CommitRefund. Leader-issued afterComputeAttributionhas majority agreement. Apply handler appendsAttributiontoRefunds.SubmitAttestationSignature. Each committee member signs the committed attribution with its ECDSA key; the signatures land here. Whentsignatures are present (tfrom the settlement contract’s threshold, not the Raft majority), the aggregate is appended toAttestations.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:
- Total MEV =
block's coinbase_transfer - baseline_reward. - Winning searcher gets
auction.bid * searcher_share_pct. - Each wallet whose zipnet submission landed in the block
gets
(tx_value / total_tx_value) * orderflow_share_pctof the remaining MEV. - Co-builders (Phase 2) split
cobuilder_share_pctequally. - 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
- One
Refundsentry per included slot. Enforced inCommitRefund. - Attribution is a deterministic function of the upstream evidence plus the policy. Every committee member computes the same draft.
- 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).
- Nothing tally commits can be rolled back. Once an
Attestationis 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 aRefundsrewrite.
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
AttributionPolicyenum 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 oftallywith 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
kconfirmations) is safer and trivially added — but changes the SLA tally exposes to integrators.
Cross-references
- The six organisms
- relay — upstream.
- atelier — upstream.
- offer — upstream.
- zipnet — upstream.
- cryptography.md — tally
- threat-model.md — tally
- integrators/refunds.md
- Operator runbook