helper

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 17, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package helper implements the share processing pipeline that receives encrypted voting shares from wallets, applies random delays for temporal unlinkability, generates ZKP #3 proofs, and submits MsgRevealShare to the chain.

This package runs inside the svoted binary, reading commitment tree leaves directly from the vote keeper's KV store.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidCommitment = fmt.Errorf("invalid vote commitment")

ErrInvalidCommitment is returned when the share's recomputed vote commitment hash does not match the on-chain leaf at the claimed tree position.

View Source
var ErrUnknownRound = errors.New("unknown voting round")

ErrUnknownRound is returned when a share references a round that does not exist on-chain. Callers can check for this with errors.Is to distinguish it from transient failures.

Functions

func IsDuplicateNullifier

func IsDuplicateNullifier(code uint32) bool

IsDuplicateNullifier returns true if the chain rejection code matches ErrDuplicateNullifier, meaning the share was already revealed by another helper. In quorum mode multiple helpers process the same share; the first to submit wins and the rest receive this benign rejection.

func RegisterRoutes

func RegisterRoutes(router *mux.Router, store *ShareStore, logger log.Logger)

RegisterRoutes registers helper server HTTP routes on the given mux router.

func RegisterRoutesWithGetters

func RegisterRoutesWithGetters(
	router *mux.Router,
	getStore func() *ShareStore,
	getAPIToken func() string,
	getExposeQueueStatus func() bool,
	getTree func() TreeReader,
	getVCHash func() VCHashFunc,
	logger log.Logger,
)

RegisterRoutesWithGetters registers helper routes using runtime getters for store, API token, tree reader, and VC hash function.

func RegisterRoutesWithStoreGetter

func RegisterRoutesWithStoreGetter(router *mux.Router, getStore func() *ShareStore, logger log.Logger)

RegisterRoutesWithStoreGetter registers helper server HTTP routes on the given mux router, resolving the store at request time. This allows routes to be mounted before the helper is fully initialized.

func RunPulse

func RunPulse(ctx context.Context, cfg PulseConfig)

RunPulse registers with register-validator on startup (to ensure approved-servers is populated), then sends a heartbeat pulse to server-heartbeat every 2 hours until ctx is cancelled.

If the initial registration returns "pending" (e.g. the chain API wasn't reachable yet on Vercel's side), registration is retried on each tick until it succeeds, then the loop switches to heartbeat-only mode.

Types

type BroadcastResult

type BroadcastResult struct {
	TxHash string `json:"tx_hash"`
	Code   uint32 `json:"code"`
	Log    string `json:"log"`
}

BroadcastResult is the chain's response to a transaction broadcast.

type ChainSubmitter

type ChainSubmitter struct {
	// contains filtered or unexported fields
}

ChainSubmitter submits MsgRevealShare transactions to the chain's REST API.

func NewChainSubmitter

func NewChainSubmitter(baseURL string) *ChainSubmitter

NewChainSubmitter creates a submitter targeting the given base URL.

func (*ChainSubmitter) FetchVoteRound

func (c *ChainSubmitter) FetchVoteRound(roundID string) (uint64, error)

FetchVoteRound queries the chain REST API for a vote round's metadata and returns the vote_end_time (unix seconds).

func (*ChainSubmitter) SubmitRevealShare

func (c *ChainSubmitter) SubmitRevealShare(msg *MsgRevealShareJSON) (*BroadcastResult, error)

SubmitRevealShare POSTs a MsgRevealShare to the chain endpoint.

type Config

type Config struct {
	// Disable turns off the helper server entirely.
	Disable bool `mapstructure:"disable"`

	// APIToken protects POST /api/v1/shares when set (checked via X-Helper-Token).
	APIToken string `mapstructure:"api_token"`

	// ExposeQueueStatus enables the benchmark-only GET /api/v1/queue-status
	// endpoint. Keep this off by default to avoid exposing per-round queue
	// counts to observers.
	ExposeQueueStatus bool `mapstructure:"expose_queue_status"`

	// DBPath is the path to the SQLite database file. Use ":memory:" for testing.
	DBPath string `mapstructure:"db_path"`

	// MinDelay is the minimum delay floor (seconds). No share will be
	// submitted sooner than this after receipt, preventing near-zero
	// exponential samples from making shares trivially linkable.
	// Default: 90 (3 × default ProcessInterval).
	MinDelay int `mapstructure:"min_delay"`

	// ProcessInterval is how often to check for shares ready to submit (seconds).
	ProcessInterval int `mapstructure:"process_interval"`

	// ChainAPIPort is the port of the chain's REST API (localhost).
	// Used for submitting MsgRevealShare via POST.
	ChainAPIPort int `mapstructure:"chain_api_port"`

	// MaxConcurrentProofs limits concurrent proof generation goroutines.
	MaxConcurrentProofs int `mapstructure:"max_concurrent_proofs"`

	// PulseURL is the base URL for the server heartbeat endpoint
	// (e.g. "https://shielded-vote.vercel.app"). When empty, heartbeat is disabled.
	PulseURL string `mapstructure:"pulse_url"`

	// HelperURL is this server's own public URL as seen by clients
	// (e.g. "https://1-2-3-4.sslip.io"). When empty, heartbeat is disabled.
	HelperURL string `mapstructure:"helper_url"`
}

Config holds the helper server configuration, read from app.toml helper.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the default helper configuration.

type EncryptedShareWire

type EncryptedShareWire struct {
	C1         string `json:"c1"`          // base64, 32 bytes
	C2         string `json:"c2"`          // base64, 32 bytes
	ShareIndex uint32 `json:"share_index"` // 0..15
}

EncryptedShareWire is the wire format for an encrypted ElGamal share component.

type EnqueueResult

type EnqueueResult int

EnqueueResult reports how an enqueue attempt was handled.

const (
	EnqueueInserted EnqueueResult = iota
	EnqueueDuplicate
	EnqueueConflict
)

type Helper

type Helper struct {
	Store             *ShareStore
	Processor         *Processor
	APIToken          string
	ExposeQueueStatus bool
	VCHash            VCHashFunc
	Logger            log.Logger
}

Helper manages the share processing pipeline lifecycle.

func New

func New(cfg Config, tree TreeReader, prover ProofGenerator, roundFetcher RoundInfoFetcher, vcHash VCHashFunc, homeDir string, logger log.Logger) (*Helper, error)

New creates a new Helper from the given configuration.

Parameters:

  • cfg: helper configuration (from app.toml helper section)
  • tree: accesses the commitment tree (status + merkle paths + leaf reads) from the keeper's KV store
  • prover: generates ZKP #3 proofs (real FFI or mock)
  • roundFetcher: queries the chain for round metadata (direct keeper access)
  • vcHash: computes vote commitment Poseidon hash for ingress validation
  • homeDir: the chain's home directory (for default DB path)
  • logger: module logger

func (*Helper) Close

func (h *Helper) Close() error

Close shuts down the helper and releases resources.

func (*Helper) RegisterRoutes

func (h *Helper) RegisterRoutes(router *mux.Router)

RegisterRoutes registers the helper's HTTP routes on the given router.

func (*Helper) Start

func (h *Helper) Start(ctx context.Context) error

Start launches the background processor in the given context. It blocks until the context is cancelled.

func (*Helper) Tree

func (h *Helper) Tree() TreeReader

Tree returns the tree reader used by the processor.

type MsgRevealShareJSON

type MsgRevealShareJSON struct {
	ShareNullifier           string `json:"share_nullifier"` // base64, 32 bytes
	EncShare                 string `json:"enc_share"`       // base64, 64 bytes (C1||C2)
	ProposalID               uint32 `json:"proposal_id"`
	VoteDecision             uint32 `json:"vote_decision"`
	Proof                    string `json:"proof"`         // base64
	VoteRoundID              string `json:"vote_round_id"` // base64, 32 bytes
	VoteCommTreeAnchorHeight uint64 `json:"vote_comm_tree_anchor_height"`
}

MsgRevealShareJSON is the JSON payload for submitting MsgRevealShare to the chain's REST API. Byte fields are base64-encoded (Go's default for []byte).

type Processor

type Processor struct {
	// contains filtered or unexported fields
}

Processor is the background share processing loop. It checks the share queue at Poisson-distributed intervals (exponential inter-arrival times) for shares whose delay has elapsed, generates Merkle paths and ZKP #3 proofs, and submits MsgRevealShare to the chain. The random timing prevents an observer from correlating submission patterns with share readiness.

func NewProcessor

func NewProcessor(
	store *ShareStore,
	tree TreeReader,
	prover ProofGenerator,
	submitter *ChainSubmitter,
	logger log.Logger,
	meanInterval time.Duration,
	maxConcurrent int,
) *Processor

NewProcessor creates a new share processor.

func (*Processor) Run

func (p *Processor) Run(ctx context.Context) error

Run starts the processing loop. Blocks until ctx is cancelled. Wake-up intervals follow an exponential distribution so that share submissions form a Poisson process, preventing timing correlation. Each cycle also purges share data for rounds whose voting window has ended.

type ProofGenerator

type ProofGenerator interface {
	// GenerateShareRevealProof generates a share reveal proof.
	// merklePath: 772-byte serialized Merkle path (from votetree.TreeHandle.Path)
	// shareComms: 16 × 32-byte per-share Poseidon commitments
	// primaryBlind: 32-byte blind factor for the revealed share
	// encC1X, encC2X: 32-byte x-coordinates of the revealed share
	// Returns proof bytes, nullifier (32 bytes), tree root (32 bytes).
	GenerateShareRevealProof(
		merklePath []byte,
		shareComms [16][32]byte,
		primaryBlind [32]byte,
		encC1X [32]byte,
		encC2X [32]byte,
		shareIndex uint32,
		proposalID, voteDecision uint32,
		roundID [32]byte,
	) (proof []byte, nullifier [32]byte, treeRoot [32]byte, err error)
}

ProofGenerator abstracts ZKP #3 proof generation for testing.

type PulseConfig

type PulseConfig struct {
	PulseURL        string // Vercel base URL (e.g. "https://shielded-vote.vercel.app")
	HelperURL       string // Own public URL (e.g. "https://1-2-3-4.sslip.io")
	OperatorAddress string // Bech32 operator address derived from validator key
	Moniker         string // Node moniker from CometBFT config
	Sign            SignFn
	Logger          log.Logger
}

PulseConfig holds the parameters needed for the heartbeat loop.

type QueueStatus

type QueueStatus struct {
	Total     int `json:"total"`
	Pending   int `json:"pending"`
	Submitted int `json:"submitted"`
	Failed    int `json:"failed"`
}

QueueStatus holds per-round queue statistics.

type QueuedShare

type QueuedShare struct {
	Payload     SharePayload
	State       ShareState
	Attempts    int
	VoteEndTime uint64 // unix seconds; 0 if unknown
}

QueuedShare is a share payload with processing metadata.

type RoundInfoFetcher

type RoundInfoFetcher func(roundID string) (voteEndTime uint64, err error)

RoundInfoFetcher queries the chain for vote round metadata. Returns the vote_end_time (unix seconds) for the given round ID (hex).

type SharePayload

type SharePayload struct {
	SharesHash   string             `json:"shares_hash"`   // base64, 32 bytes
	ProposalID   uint32             `json:"proposal_id"`   // proposal being voted on
	VoteDecision uint32             `json:"vote_decision"` // 0=support, 1=oppose, 2=skip
	EncShare     EncryptedShareWire `json:"enc_share"`     // the share to relay
	ShareIndex   uint32             `json:"share_index"`   // redundant with enc_share.share_index
	TreePosition uint64             `json:"tree_position"` // VC leaf index
	VoteRoundID  string             `json:"vote_round_id"` // hex, 32 bytes
	ShareComms   []string           `json:"share_comms"`   // base64, 16 × 32-byte Poseidon commitments
	PrimaryBlind string             `json:"primary_blind"` // base64, 32 bytes
}

SharePayload is the wire format sent by wallets to the helper server.

type ShareState

type ShareState int

ShareState represents the processing state of a queued share.

const (
	ShareStateReceived  ShareState = 0 // waiting for delay to elapse
	ShareStateWitnessed ShareState = 1 // ready for proof generation
	ShareStateSubmitted ShareState = 2 // submitted to chain
	ShareStateFailed    ShareState = 3 // permanently failed
)

type ShareStore

type ShareStore struct {
	// contains filtered or unexported fields
}

ShareStore is a SQLite-backed share queue with ephemeral in-memory scheduling. Payload data and processing state are persisted; scheduling delays (which provide temporal unlinkability) are kept only in memory — on recovery, shares get fresh random delays per spec.

func NewShareStore

func NewShareStore(dbPath string, minDelay time.Duration, fetcher RoundInfoFetcher) (*ShareStore, error)

NewShareStore opens (or creates) a SQLite database and runs migrations.

func (*ShareStore) Close

func (s *ShareStore) Close() error

Close closes the database connection.

func (*ShareStore) Enqueue

func (s *ShareStore) Enqueue(payload SharePayload) (EnqueueResult, error)

Enqueue adds a share payload with a uniform random submission delay, capped at the vote end time for the round.

Returns:

  • EnqueueInserted when a new row was inserted and scheduled.
  • EnqueueDuplicate when an identical payload already exists.
  • EnqueueConflict when an entry exists for (round_id, share_index) but with different payload content.

func (*ShareStore) MarkFailed

func (s *ShareStore) MarkFailed(roundID string, shareIndex, proposalID uint32, treePosition uint64)

MarkFailed marks a share processing attempt as failed, with retry or permanent failure after max attempts.

func (*ShareStore) MarkSubmitted

func (s *ShareStore) MarkSubmitted(roundID string, shareIndex, proposalID uint32, treePosition uint64)

MarkSubmitted marks a share as successfully submitted to the chain.

func (*ShareStore) PurgeExpiredRounds

func (s *ShareStore) PurgeExpiredRounds() int64

PurgeExpiredRounds deletes all share data for rounds whose vote_end_time has passed, and removes the corresponding entries from the in-memory schedule and round cache. Returns the number of rows deleted.

func (*ShareStore) Status

func (s *ShareStore) Status() map[string]QueueStatus

Status returns per-round queue statistics.

func (*ShareStore) TakeReady

func (s *ShareStore) TakeReady() []QueuedShare

TakeReady returns all shares past their scheduled submission time that are in Received state, transitioning them to Witnessed.

type SignFn

type SignFn func(payload string) (signature, pubKey string, err error)

SignFn signs an arbitrary string payload using ADR-036 and returns the base64-encoded signature and compressed public key.

type TreeReader

type TreeReader interface {
	// GetTreeStatus returns lightweight tree statistics (leaf count + anchor height).
	GetTreeStatus() (TreeStatus, error)

	// MerklePath returns the 772-byte serialized Poseidon Merkle authentication
	// path for the leaf at position, anchored to the checkpoint at anchorHeight.
	// anchorHeight must correspond to a checkpoint that exists in the persistent
	// tree (i.e. a block height at which Checkpoint was called by EndBlocker).
	MerklePath(position uint64, anchorHeight uint32) ([]byte, error)

	// LeafAt returns the raw 32-byte vote commitment stored at the given tree
	// position, or nil if no leaf exists at that index.
	LeafAt(position uint64) ([]byte, error)
}

TreeReader abstracts commitment tree access from the keeper.

type TreeStatus

type TreeStatus struct {
	LeafCount    uint64 `json:"leaf_count"`
	AnchorHeight uint64 `json:"anchor_height"`
}

TreeStatus holds lightweight commitment tree statistics.

type VCHashFunc

type VCHashFunc func(roundID, sharesHash [32]byte, proposalID, voteDecision uint32) ([32]byte, error)

VCHashFunc computes the vote commitment Poseidon hash. The implementation is provided by the votecommitment CGo package but abstracted here so the helper package doesn't depend on the Rust FFI library directly.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL