anchor

package
v0.0.0-...-01bb2a7 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

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

View Source
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

View Source
var ErrKMSUnavailable = errors.New("KMS unavailable")

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 New deprecated

func New(cfg Config) *Client

New constructs a deprecated Client. Maintains the original API surface.

Deprecated: use NewPoller + NewHederaSubmitter 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

func (*FileSigner) Sign

func (f *FileSigner) Sign(message []byte) ([]byte, error)

type FileStubSubmitter

type FileStubSubmitter struct {
	Dir string
}

func (FileStubSubmitter) Submit

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) Close

func (s *HederaSubmitter) Close()

Close releases the Hedera client.

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

func (p *Poller) Start(ctx context.Context)

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

type PostSubmitHook func(ctx context.Context, anchorID int64)

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

type Signer interface {
	PublicKey() hedera.PublicKey
	Sign(message []byte) ([]byte, error)
}

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.

func NewSignerFromEnv

func NewSignerFromEnv(ctx context.Context) (Signer, error)

NewSignerFromEnv constructs whichever Signer the operator has configured. Returns a clear error if the configuration is incomplete.

type SubmitOutcome

type SubmitOutcome struct {
	TxID       string
	AnchoredAt time.Time
}

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

func (*VaultSigner) Sign

func (v *VaultSigner) Sign(message []byte) ([]byte, error)

Sign sends message to Vault's transit sign endpoint and returns the raw 64-byte Ed25519 signature. ErrKMSUnavailable on transport or 5xx.

Jump to

Keyboard shortcuts

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