helper

package
v0.5.21 Latest Latest
Warning

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

Go to latest
Published: Apr 26, 2026 License: MIT Imports: 26 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 ErrInvalidSubmitAt = errors.New("invalid submit_at")

ErrInvalidSubmitAt is returned when submit_at is in the past or after vote end time.

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 CaptureErr added in v0.4.2

func CaptureErr(err error, tags map[string]string)

CaptureErr delegates to the shared sentry package.

func FlushSentry added in v0.4.2

func FlushSentry()

FlushSentry delegates to the shared sentry package.

func InitSentry added in v0.4.2

func InitSentry(dsn, release, serverName string, logger log.Logger) error

InitSentry delegates to the shared sentry package.

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,
	getShareNullifier ShareNullifierCheckerGetter,
	logger log.Logger,
)

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

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 admin server hasn't seen the bonding tx yet), 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 /shielded-vote/v1/shares when set (checked via X-Helper-Token).
	APIToken string `mapstructure:"api_token"`

	// ExposeQueueStatus enables the benchmark-only GET /shielded-vote/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"`

	// 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. Defaults to 1317 — the
	// standard Cosmos SDK [api] address port that svoted serves. Setting
	// this to a port where no server listens causes every share to fail
	// broadcast silently (`connection refused` warnings in the log while
	// the helper keeps accepting and proving new shares), which surfaces
	// to voters as zero-value tallies with no user-visible error.
	ChainAPIPort int `mapstructure:"chain_api_port"`

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

	// AdminURL is the base URL of the admin server for registration and
	// heartbeat (e.g. "https://admin.example.com"). When empty, heartbeat
	// is disabled.
	AdminURL string `mapstructure:"admin_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"`

	// SentryDSN is the Sentry DSN for error tracking. When empty, Sentry is
	// disabled. Can also be set via the SENTRY_DSN environment variable at
	// runtime (app.toml takes precedence if set).
	SentryDSN string `mapstructure:"sentry_dsn"`
}

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
	ShareNullifierChecker ShareNullifierChecker
	Logger                log.Logger
}

Helper manages the share processing pipeline lifecycle.

func New

func New(cfg Config, tree TreeReader, prover ProofGenerator, roundFetcher RoundInfoFetcher, isRoundActive RoundStatusChecker, vcHash VCHashFunc, shareNF ShareNullifierChecker, 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)
  • isRoundActive: checks if a round is still ACTIVE (nil = skip check)
  • 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,
	isRoundActive RoundStatusChecker,
) *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
	// encC1, encC2: 32-byte compressed Pallas points of the revealed share
	// Returns proof bytes, nullifier (32 bytes), tree root (32 bytes).
	GenerateShareRevealProof(
		merklePath []byte,
		shareComms [16][32]byte,
		primaryBlind [32]byte,
		encC1 [32]byte,
		encC2 [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 {
	AdminURL        string // Admin server base URL (e.g. "https://admin.example.com")
	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 vote_end_time (unix seconds) for the given round ID (hex).

type RoundStatusChecker added in v0.3.0

type RoundStatusChecker func(roundID string) (isActive bool, err error)

RoundStatusChecker returns true if the round is still accepting shares (i.e., status == ACTIVE). Used by the processor to skip shares for rounds that have transitioned to TALLYING or beyond, avoiding wasted ZKP computation.

type ShareNullifierChecker added in v0.3.0

type ShareNullifierChecker func(roundIDHex string, shareNullifier []byte) (bool, error)

ShareNullifierChecker reports whether a share nullifier is recorded on-chain for the given voting round (hex-encoded 32-byte round id). Used by the share-status endpoint to confirm MsgRevealShare acceptance without exposing a public chain query API to wallets.

type ShareNullifierCheckerGetter added in v0.3.0

type ShareNullifierCheckerGetter func() ShareNullifierChecker

ShareNullifierCheckerGetter resolves the checker at request time (nil when helper disabled).

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
	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
	SubmitAt     uint64             `json:"submit_at"`     // unix seconds; 0 = immediate (last-moment)
}

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; submit_at timestamps from the client control when each share is released for proof generation.

func NewShareStore

func NewShareStore(dbPath string, 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 {
	// SetRoundID selects the voting round for subsequent tree lookups.
	SetRoundID(roundID []byte)

	// 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. Commitment trees are scoped per voting round; callers must call SetRoundID before any tree lookup to select the correct round.

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