protocol/submission
Package submission implements TwoListBase — the shared anonymous state machine foundation embedded by every shyware domain state machine (shyvoting, shywire, shystore, shychat).
Import path: github.com/ShywareLLC/core/protocol/submission
TwoListBase
TwoListBase owns the LevelDB-backed canonical state, enforces the two-list invariant, manages period lifecycle, and delegates optional signed period-close attestation.
type TwoListBase struct { /* unexported */ }
func NewTwoListBase(ctx context.Context, db dbm.DB, kmsKeyID string, logger log.Logger) (*TwoListBase, error)
Every domain state machine embeds TwoListBase and delegates atomic writes, period management, and attestation to it. Domain-specific logic (validation predicates, tx routing) is layered on top.
Period lifecycle
// Open a new period (poll, mailbox, bucket, etc.)
err := b.CreatePeriod(periodID, domainType, startTime, endTime)
// Query a period
rec := b.GetPeriod(periodID) // → *PeriodRecord
// Close with optional signer attestation
closure, events, err := b.ClosePeriod(ctx, periodID, closingHeight)
// Rolling attestation checkpoint (without closing)
events, err := b.CommitRollingAttestation(ctx, periodID)
Two-list writes
// Atomic List 1 + List 2 write — the core protocol operation
err := b.SubmitToLists(periodID, submissionID, submissionData, identityHash, participantData)
// Sealed List 2 identity attribute variant (shybrowser dual-sealing)
err := b.SubmitToListsWithSealedAttrs(periodID, submissionID, submissionData, identityHash, sealedAttrs)
// Direction change — replace List 1 entry; List 2 unchanged
err := b.UpdateSubmission(periodID, oldSubmissionID, newSubmissionID, newData, identityHash)
// Bilateral withdrawal — remove from both lists atomically
err := b.WithdrawFromLists(periodID, submissionID, identityHash)
Presence and count queries
b.HasParticipant(periodID, identityHash) bool
b.HasSubmission(periodID, submissionID) bool
b.SubmissionPresence(periodID, submissionID) ([]byte, error)
l1, l2 := b.CountsForPeriod(periodID) // count-match verification
CountsForPeriod is used by domain state machines to verify |L1| == |L2| before committing a period close.
Write-only posture
b.SetWriteOnly(true)
b.IsWriteOnly() bool
When write-only is active, SubmitToLists succeeds but the submission receipt is not written to the off-chain linkage store. The participant retains only the direction-free submissionID.
Beacon
b.RecordBeacon(height int64, blockHashHex string)
window := b.BeaconWindow() // map[int64]string — recent block heights → hashes
Default voting (nonce_only) hashes decoded beacon bytes followed by decoded nonce bytes. nonce_plus_payload uses its separate canonical derivation helper. Both paths must include the beacon evidence required by their transaction validation; use the matching SDK builder rather than a nonce-text hash.
Validator management
events, err := b.RegisterValidator(pubKeyBase64 string, power int64, name string)
updates := b.GetPendingValidatorUpdates()
Commit
appHash, err := b.Commit()
Called by the ABCI app's Commit handler. Returns the new app hash.
Core types
type SubmissionRecord struct {
ID string `json:"id"`
PeriodID string `json:"period_id"`
Data json.RawMessage `json:"data"`
Height int64 `json:"height"`
}
type ParticipantRecord struct {
IdentityHash string `json:"identity_hash"`
PeriodID string `json:"period_id"`
Height int64 `json:"height"`
}
type PeriodRecord struct {
ID string `json:"id"`
DomainType string `json:"domain_type"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
Closed bool `json:"closed"`
}
type ClosureRecord struct {
PeriodID string `json:"period_id"`
TotalSubmissions int64 `json:"total_submissions"`
MerkleRoot1 string `json:"merkle_root_1"` // over submission IDs (List 1)
MerkleRoot2 string `json:"merkle_root_2"` // over identity hashes (List 2)
ClosingHeight int64 `json:"closing_height"`
PublicKey string `json:"public_key"` // KMS public key (DER)
Signature string `json:"signature"` // KMS signature
}
type AttestationCheckpoint struct {
PeriodID string `json:"period_id"`
Height int64 `json:"height"`
Signature string `json:"signature"`
}
Sentinel errors
type ErrCountMismatch struct { L1, L2 int } // |L1| ≠ |L2| at period close
type ErrDuplicateParticipant struct { Identity string } // identity already in List 2
type ErrPeriodNotFound struct { PeriodID string }
type ErrWriteOnlyPosture struct{} // receipt suppressed in write-only mode
Cover traffic and retained transactions
Claim 9 describes a transport mechanism separate from the canonical rejection predicate: every active slot emits a request; a real operation takes a dummy slot; the receiving boundary discards dummies before canonical services. Dummies are never participants and cannot satisfy a minimum release cohort.
The replacement JavaScript CoverTrafficDispatcher and Swift
CoverTrafficAdapter use a version-1 /dispatch envelope:
{"version":1,"kind":"cast|update|dummy","body":{...}}, padded with JSON
whitespace to exactly 32,768 UTF-8 bytes. The response is
{"status":HTTP_STATUS,"body":{...}}, padded to the same size. Casts,
replacements and withdrawals share this endpoint. Real requests retain their
normal authorization and state validation. A dummy exits before enrollment,
queues, recovery stores and broadcasting.
Start cover traffic on authenticated foreground entry, independently of a vote,
using client.startCoverTraffic(); stop it on background or sign-out with
client.stopCoverTraffic(). A configured client rejects submissions when its
scheduler is stopped. Swift methods are asynchronous and start can throw.
Do not enable submission_dispatch: "cover_traffic" before deploying the
receiving endpoint and wiring the consumer lifecycle. The released Populist iOS
app has user-confirmed cast/change/withdraw and results refresh. A macOS SDK probe verifies actual six-second scheduling and stop behavior; authenticated physical-device cover traces remain pending.
Fixed bodies and scheduled slots do not hide authentication-header lengths,
TLS connection behavior, response latency, other endpoints, background
suspension, or the plaintext visible to the receiving service. Authentication
middleware errors outside /dispatch may also have a different response shape.
Transport padding must survive proxies without compression to preserve sizes.
Populist's separated publication gate privately validates submitted envelopes and publishes independently sorted complete ballot and participant lists. The relay queues casts, replacements and withdrawals together, publishing each poll's pending operations on its configurable timer (ten seconds in Populist). A client slot sends one operation; a downstream batch may contain several real operations, or only one when traffic is low. Neither mechanism creates dummy participants or promises a minimum anonymity cohort.
A queued response means accepted for later processing, not ledger confirmation. Recoverable receipts change only after a confirmed batch commit. Identical update retries do not duplicate queued work; conflicting operations for the same pending participant are rejected. Invalid admission entries are isolated by index, while transient failures retain pending work. The queue is in memory, so a process restart can lose uncommitted work. Public clients cannot force an early flush in the separated deployment; the manual flush endpoint requires operator authorization.
Historical paired transactions remain in the ledger. Sorting new releases does not erase old disclosures, and singleton or successive-release inference remains a separate concern. Count-match is per poll; KMS/HSM and close signing are optional.