Documentation
¶
Overview ¶
SPDX-License-Identifier: AGPL-3.0-or-later
Package anchor is tophats's interface to Hedera Consensus Service.
Since the 2026-05-22 unification (project_feds_layer_collapsed.md + project_replay_check_design.md), tophats is the ONLY entity that talks to Hedera. It reads PENDING rows from aggregator.hedera_anchors (written by aggd's outbox-only AnchorWorker), submits the compounded_root to a Hedera HCS topic, and writes back hedera_tx_id + anchor_timestamp.
Two Submitter implementations:
- HederaSubmitter — production; uses hashgraph/hedera-sdk-go/v2
- FileStubSubmitter — local PoC; writes JSON to disk
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: AGPL-3.0-or-later
Index ¶
- Constants
- Variables
- type AnchorPayload
- type Clientdeprecated
- func New(cfg Config) *Clientdeprecated
- type Config
- type FileSigner
- type FileStubSubmitter
- type HederaSubmitter
- type LegacyPayload
- type MirrorCheckConfig
- type MirrorCheckOutcome
- type MirrorCheckWorker
- type MirrorVerifyDB
- type OutboxDB
- type Poller
- type PollerConfig
- type PostSubmitHook
- type Signer
- type SubmitOutcome
- type Submitter
- type VaultConfig
- type VaultSigner
Constants ¶
const BackoffCap = 60 * time.Second
BackoffCap is the upper bound on the per-cycle backoff delay. 60s per the wp-04 acceptance criteria ("KMS unreachable; will retry" must not spin tighter than this).
Variables ¶
ErrKMSUnavailable is returned by VaultSigner.Sign when Vault is unreachable or returns 5xx. The poller checks errors.Is to decide whether to back off vs. propagate a hard error.
Functions ¶
This section is empty.
Types ¶
type AnchorPayload ¶
type AnchorPayload struct {
SectorId string `json:"network_id"`
AnchorID int64 `json:"anchor_id"` // aggregator.hedera_anchors.anchor_id
StartBatchID int64 `json:"start_batch_id"`
EndBatchID int64 `json:"end_batch_id"`
MerkleRoot string `json:"compounded_root_hex"`
GeneratedAt time.Time `json:"generated_at"`
AnchorCID string `json:"anchor_cid,omitempty"`
}
AnchorPayload is the data structure tophats submits to Hedera. Mirrors orgnodes/int-alt/slab.AnchorPayload — eventual consolidation point.
Workplan #07: AnchorCID (when non-empty) is set as the Hedera TransactionMemo. The replay-check worker verifies the memo on the Mirror Node against tophats.anchor_cids — defence against operator- key compromise where an attacker re-submits the same anchor_id with a mutated compounded_root.
type Client
deprecated
type Client struct {
// contains filtered or unexported fields
}
Client is the deprecated wrapper kept for backward compatibility with the WebSocket+NetworkSlab code. intly wraps a FileStubSubmitter using the configured anchor directory (or /var/lib/tophats if unset).
Deprecated: use Poller + HederaSubmitter for new code.
func (*Client) SubmitAnchor
deprecated
func (c *Client) SubmitAnchor(ctx context.Context, p LegacyPayload) error
SubmitAnchor is the deprecated WebSocket-path entry point. Maps the legacy payload onto the new AnchorPayload shape and forwards to the FileStubSubmitter so anchor JSON files keep landing on disk during the transition period.
Deprecated: use Poller.Start() for new code.
type Config ¶
type Config struct {
Network string // "testnet" | "mainnet" | "previewnet"
TopicID string // e.g. "0.0.12345"
OperatorID string // e.g. "0.0.6789"
OperatorKeyPath string // FileSigner only; ignored when HEDERA_SIGNER_TYPE=vault
}
Config holds Hedera connection parameters. Loaded from env vars in main_BAK.go.
Workplan #04 (2026-05-31): OperatorKeyPath is retained for the FileSigner backward-compat path only. Production deployments set HEDERA_SIGNER_TYPE=vault and configure the Vault env vars; the file path is then unused. The Signer abstraction in signer.go decides which implementation to construct based on those env vars.
type FileSigner ¶
type FileSigner struct {
// contains filtered or unexported fields
}
func NewFileSigner ¶
func NewFileSigner(keyPath string) (*FileSigner, error)
NewFileSigner reads the operator key from the given path. Returns a signer that holds the key in process memory.
func (*FileSigner) PublicKey ¶
func (f *FileSigner) PublicKey() hedera.PublicKey
type FileStubSubmitter ¶
type FileStubSubmitter struct {
Dir string
}
func (FileStubSubmitter) Submit ¶
func (s FileStubSubmitter) Submit(ctx context.Context, payload AnchorPayload) (SubmitOutcome, error)
type HederaSubmitter ¶
type HederaSubmitter struct {
// contains filtered or unexported fields
}
func NewHederaSubmitter ¶
func NewHederaSubmitter(ctx context.Context, cfg Config) (*HederaSubmitter, error)
NewHederaSubmitter constructs a live Hedera client + wires the Signer as the SDK's operator. The Signer comes from NewSignerFromEnv (or is passed explicitly by tests via NewHederaSubmitterWithSigner) — the caller decides whether to use FileSigner or VaultSigner.
Workplan #04 contract: the private key never enters HederaSubmitter itself. The Hiero SDK's Client.SetOperatorWith(accountID, publicKey, signer TransactionSigner) accepts a callback rather than a raw key, and we wire signer.Sign as that callback. With VaultSigner the key stays in Vault for the lifetime of the process.
func NewHederaSubmitterWithSigner ¶
func NewHederaSubmitterWithSigner(cfg Config, signer Signer) (*HederaSubmitter, error)
NewHederaSubmitterWithSigner is the test seam — accepts an already-constructed Signer (typically a FileSigner backed by a fixture or a mock VaultSigner against an httptest.Server).
func (*HederaSubmitter) Submit ¶
func (s *HederaSubmitter) Submit(ctx context.Context, payload AnchorPayload) (SubmitOutcome, error)
Submit serialises the payload to JSON and posts it as an HCS topic message. Blocks until the receipt is received (synchronous; the poller handles the concurrency).
Workplan #07: when payload.AnchorCID is non-empty, set it as the Hedera TransactionMemo so the replay-check worker can verify the memo on the Mirror Node side against the value stored in tophats.anchor_cids. Empty AnchorCID falls back to the legacy no-memo behaviour for backward compatibility.
type LegacyPayload ¶
type LegacyPayload struct {
Network string `json:"network"`
StartHeight int64 `json:"start_height"`
EndHeight int64 `json:"end_height"`
RootHex string `json:"compounded_root"`
Timestamp time.Time `json:"timestamp"`
}
LegacyPayload mirrors the pre-2026-05-22 shape used by the WebSocket worker. Kept distinct from the new AnchorPayload so renaming one doesn't ripple into the other.
type MirrorCheckConfig ¶
type MirrorCheckConfig struct {
// MirrorBaseURL is the Hedera Mirror Node API base, e.g.
// "https://testnet.mirrornode.hedera.com" or the mainnet equivalent.
MirrorBaseURL string
// Interval between sweeps. Default 10 min per wp-07 plan.
Interval time.Duration
// MaxBackoff bounds the retry delay on Mirror Node 5xx / transport
// errors. Default 1 hour.
MaxBackoff time.Duration
// BatchSize caps the number of anchor_cids rows verified per
// sweep. Default 100.
BatchSize int
// HTTPClient is optional; nil falls back to default with timeout.
HTTPClient *http.Client
}
MirrorCheckConfig holds worker knobs.
type MirrorCheckOutcome ¶
type MirrorCheckOutcome struct {
AnchorID int64
HederaTxID string
StoredCID string
MirrorMemo string
Status string
Notes string
}
MirrorCheckOutcome captures one anchor's verification result. int use; the persisted form lives in tophats.replay_checks with status one of:
- OK memo matches anchor_cid
- HEDERA_MEMO_MISMATCH memo present but != anchor_cid
- HEDERA_MEMO_MISSING mirror response had no memo
- HEDERA_NOT_FOUND mirror returned 404 for the tx_id
- MIRROR_UNREACHABLE transport or 5xx
type MirrorCheckWorker ¶
type MirrorCheckWorker struct {
// contains filtered or unexported fields
}
MirrorCheckWorker queries the Mirror Node for each anchor_cids row with a known hedera_tx_id and verifies the memo matches the stored CID. Mismatches land in tophats.replay_checks with status 'HEDERA_MEMO_MISMATCH'; transport failures with 'MIRROR_UNREACHABLE'. cc110 (and any successor) reads from tophats.replay_checks to fire Catastrophic.
func NewMirrorCheckWorker ¶
func NewMirrorCheckWorker(db MirrorVerifyDB, cfg MirrorCheckConfig) (*MirrorCheckWorker, error)
NewMirrorCheckWorker constructs a worker. Returns error if MirrorBaseURL is empty.
func (*MirrorCheckWorker) Start ¶
func (w *MirrorCheckWorker) Start(ctx context.Context)
Start runs the worker loop until ctx is cancelled.
func (*MirrorCheckWorker) SweepOnce ¶
func (w *MirrorCheckWorker) SweepOnce(ctx context.Context) (int, error)
SweepOnce runs a single sweep. Returns the number of rows verified (regardless of outcome). Exposed for tests + manual triggering.
func (*MirrorCheckWorker) VerifyOne ¶
func (w *MirrorCheckWorker) VerifyOne(ctx context.Context, anchorID int64, anchorCID, hederaTxID string) MirrorCheckOutcome
VerifyOne is the exported version of verifyOne — useful for the event-triggered post-submit hook so a freshly-anchored slab is verified immediately rather than waiting for the next sweep.
type MirrorVerifyDB ¶
type MirrorVerifyDB interface {
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}
MirrorVerifyDB is the subset of pgxpool methods the worker uses against the tophats Postgres. Defined narrow so tests can stub.
type OutboxDB ¶
type OutboxDB interface {
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}
OutboxDB is the subset of *pgxpool.Pool the poller needs. Defined as an interface so tests can stub it. Method names match pgx's so a real pool satisfies the interface without an adapter.
type Poller ¶
type Poller struct {
// contains filtered or unexported fields
}
func NewPoller ¶
func NewPoller(db OutboxDB, submitter Submitter, cfg PollerConfig, broadcaster bftmempool.Broadcaster, proposerID string) *Poller
NewPoller constructs the poller. cfg.Interval defaults to 30s, BatchSize to 50, SectorId to "default" when fields are zero/empty.
When broadcaster is non-nil, the post-Hedera UPDATE flows through goldbftd consensus (ABCI dispatch handles applyConfirmAnchor); when nil the legacy direct dbpool.Exec path runs. Production deployments pass a real broadcaster + the local replica's proposer_id.
func (*Poller) SetPostSubmitHook ¶
func (p *Poller) SetPostSubmitHook(h PostSubmitHook)
SetPostSubmitHook installs a hook that's called after each successful outbox row update. Used to wire the replay-check verifier's event-triggered VerifySpecific(anchorID) from main_BAK.go without importing the replay package into anchor (which would create an orgnodes/aggd-style cross-package dependency).
func (*Poller) Start ¶
Start runs the poll loop until ctx is cancelled.
wp-04: when a poll cycle fails (Submit error — typically Vault unreachable or Hedera transport blip), the next cycle is delayed by an exponentially-increasing amount up to BackoffCap. This keeps the poller from spinning when the KMS or Hedera is down. The interval resets to p.cfg.Interval on the next successful cycle.
type PollerConfig ¶
type PollerConfig struct {
SectorId string // logical network identifier carried in the AnchorPayload
Interval time.Duration // how often to poll for PENDING rows
BatchSize int // max rows to submit per poll cycle
}
PollerConfig holds the runtime parameters for the outbox poller.
type PostSubmitHook ¶
Poller continuously reads PENDING rows from aggregator.hedera_anchors, submits each to the configured Submitter, and writes back hedera_tx_id + anchor_timestamp + status='SUCCESS' on success. Failures leave the row in PENDING for the next cycle (no auto-retry escalation in Phase 3 Session 1; replay-check worker in Session 3 owns deeper retry/escalation logic). PostSubmitHook is called after each successful Hedera submission + outbox row update. Used to trigger the event-triggered replay-check path (per project_replay_check_design.md): verify the just-anchored slab immediately rather than waiting for the daily sweep.
Implementations should not block — the poller's loop runs at the configured interval and waiting here delays subsequent submissions. Errors are logged but don't propagate.
type Signer ¶
Signer is the unified signing surface used by HederaSubmitter. Both FileSigner and VaultSigner implement it; the choice is made at startup by NewSignerFromEnv based on HEDERA_SIGNER_TYPE.
PublicKey returns the Hedera PublicKey for the operator. Cached at construction time — never fetched on the hot path.
Sign receives the raw transaction body bytes the Hiero SDK wants signed and returns the raw 64-byte Ed25519 signature. Any error causes the SDK's signer callback to return nil, which the SDK reports as a failed transaction submission.
type SubmitOutcome ¶
SubmitOutcome is what the Submitter returns on success. The poller writes these values back into aggregator.hedera_anchors.
type Submitter ¶
type Submitter interface {
Submit(ctx context.Context, payload AnchorPayload) (SubmitOutcome, error)
}
Submitter abstracts the anchoring substrate.
type VaultConfig ¶
type VaultConfig struct {
// Addr is the Vault HTTP base URL, e.g. "https://vault.example:8200".
Addr string
// Token is the Vault auth token used for both the public-key fetch
// at startup AND every Sign call. The deployer is responsible for
// supplying a short-lived token via a juju/k8s secret.
Token string
// KeyName is the name of the transit key, e.g. "hedera-operator".
KeyName string
// HTTPClient is optional; if nil a default *http.Client with a
// modest timeout is used.
HTTPClient *http.Client
}
VaultConfig holds the runtime parameters needed to construct a VaultSigner.
type VaultSigner ¶
type VaultSigner struct {
// contains filtered or unexported fields
}
func NewVaultSigner ¶
func NewVaultSigner(ctx context.Context, cfg VaultConfig) (*VaultSigner, error)
NewVaultSigner constructs a VaultSigner. Fetches the public key from Vault at startup so subsequent Sign calls don't need that round-trip. Returns ErrKMSUnavailable if Vault is unreachable at startup.
func (*VaultSigner) PublicKey ¶
func (v *VaultSigner) PublicKey() hedera.PublicKey