reconcile

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package reconcile is the kit's reconciliation engine (T063; FR-046–FR-048, data model §8/§8a):

  • the ledger-based address formula — expected balance = Σ ChainTransferLedger inflows − Σ outflows (including FEE_DEDUCTION fee outflows), computed from the immutable ledger only and therefore independent of customer-credit workflow status: below-minimum, review-parked, fee-funding and internal movements are all in the ledger and can never produce false discrepancies;
  • per-transaction reconciliation (chain truth re-derived and diffed against the persisted ledger rows for that transaction);
  • hot-wallet comparison including pending-signed / broadcast-unconfirmed in-flight work (business.go);
  • a separate business-layer section reconciling customer-credit and sweep/withdrawal workflow totals against the ledger (business.go);
  • schedule kinds — near-real-time / hourly wallet / daily full-address / manual (schedule.go);
  • the FR-044 two-node disagreement monitor (disagreement.go).

Every non-zero difference produces a ReconciliationEntry carrying all FR-048 fields (address, expected, observed, difference, earliest suspected height, related tx hashes, recommended rescan height), increments sovren_reconciliation_discrepancies_total, and logs the alert payload verbatim.

Index

Constants

View Source
const (
	DefaultNearRealTimeInterval = time.Minute
	DefaultWalletInterval       = time.Hour
	DefaultFullAddressInterval  = 24 * time.Hour
)

Defaults for the Schedule zero values.

View Source
const DefaultDisagreementInterval = 30 * time.Second

DefaultDisagreementInterval matches the adapter.yaml nodes.disagreement.check_interval default.

View Source
const DefaultHeightTolerance = 5

DefaultHeightTolerance matches the adapter.yaml nodes.disagreement.height_divergence_threshold default.

Variables

This section is empty.

Functions

This section is empty.

Types

type BusinessSection

type BusinessSection struct {
	ChainID     string
	GeneratedAt time.Time

	// LedgerExternalDepositTotal: Σ successful EXTERNAL_DEPOSIT IN rows
	// across watched addresses — every real external inflow, credited or not.
	LedgerExternalDepositTotal sdkmath.Int
	// CreditedDepositTotal: Σ deposit records that reached CREDITED
	// (including SWEEP_PENDING / SWEPT afterwards).
	CreditedDepositTotal sdkmath.Int
	// UncreditedExternalTotal = ledger − credited. Below-minimum, review-
	// parked, awaiting-confirmation and suspended inflows live here; it is
	// never negative in a healthy system.
	UncreditedExternalTotal sdkmath.Int

	// LedgerWithdrawalOutflowTotal: Σ successful WITHDRAWAL OUT rows.
	LedgerWithdrawalOutflowTotal sdkmath.Int
	// WorkflowWithdrawalTotal: Σ CONFIRMED withdrawal record amounts.
	WorkflowWithdrawalTotal sdkmath.Int
	// InFlightWithdrawalTotal: Σ BROADCAST/INCLUDED amounts (may already be
	// in the ledger ahead of workflow confirmation, or vice versa).
	InFlightWithdrawalTotal sdkmath.Int

	// LedgerSweepInflowTotal: Σ successful SWEEP IN rows (counted on the
	// receiving side only, so one sweep is one amount).
	LedgerSweepInflowTotal sdkmath.Int
	// WorkflowSweepTotal: Σ CONFIRMED sweep job amounts.
	WorkflowSweepTotal sdkmath.Int

	// Findings are human-readable inconsistencies. An impossible state
	// (workflow total exceeding what the ledger can support) also increments
	// the discrepancy counter.
	Findings []string
}

BusinessSection reconciles the customer-credit and sweep/withdrawal workflow layers against the ledger's EXTERNAL_DEPOSIT / SWEEP / WITHDRAWAL entries (data model §8 — "a separate business-layer section"). It is returned and logged alongside address reports, not persisted in the entry-based report record.

type ChainView

type ChainView interface {
	Balance(ctx context.Context, addr, denom string) (sdkmath.Int, error)
	Tx(ctx context.Context, hash string) (*client.TxInfo, error)
	LatestBlock(ctx context.Context) (*client.Block, error)
}

ChainView is the read-only chain subset the reconciler needs. client.Client implementations and the failover wrapper satisfy it.

type Comparer

type Comparer interface {
	Compare(ctx context.Context, req client.CompareRequest) (*client.CompareResult, error)
}

Comparer runs the FR-044 two-node checks. *client.Failover satisfies it.

type Config

type Config struct {
	ChainID string
}

Config configures a Reconciler.

type DisagreementConfig

type DisagreementConfig struct {
	ChainID string
	// Interval between checks; DefaultDisagreementInterval when zero.
	Interval time.Duration
	// HeightTolerance is the allowed latest-height gap (blocks);
	// DefaultHeightTolerance when zero.
	HeightTolerance int64
	// SequenceAddress optionally adds an account-sequence check (hot wallet).
	SequenceAddress string
	// BalanceAddress optionally adds a balance check (hot wallet).
	BalanceAddress string
	// CompareHashAtCheckpoint adds a block-hash check at the scanner
	// checkpoint height (both nodes must agree on scanned history).
	CompareHashAtCheckpoint bool
}

DisagreementConfig configures a Monitor.

type HotWalletReport

type HotWalletReport struct {
	ChainID string
	Address string

	// LedgerExpected is the §8 ledger formula result.
	LedgerExpected sdkmath.Int
	// Observed is the live on-chain balance.
	Observed sdkmath.Int
	// Difference is Observed − LedgerExpected.
	Difference sdkmath.Int

	// PendingSignedOutflow: withdrawals SIGNED but not yet broadcast from
	// this wallet, plus signed-not-broadcast sweeps sourced here
	// (amount + known fee). Not yet on chain — never explains drift, listed
	// for the operator's forward view.
	PendingSignedOutflow sdkmath.Int
	// BroadcastUnconfirmedOutflow: withdrawals BROADCAST/INCLUDED and sweeps
	// BROADCAST sourced from this wallet (amount + known fee). These may
	// already be on chain ahead of the scanner — negative drift up to this
	// value is explained.
	BroadcastUnconfirmedOutflow sdkmath.Int
	// BroadcastUnconfirmedInflow: sweeps BROADCAST targeting this wallet.
	// Positive drift up to this value is explained.
	BroadcastUnconfirmedInflow sdkmath.Int

	// ConfirmedWithdrawalOutflow / ConfirmedSweepInflow are the ledger's
	// settled WITHDRAWAL (OUT) and SWEEP (IN) totals for this wallet.
	ConfirmedWithdrawalOutflow sdkmath.Int
	ConfirmedSweepInflow       sdkmath.Int

	// Explained is true when Difference is zero or within the in-flight
	// window: [−BroadcastUnconfirmedOutflow, +BroadcastUnconfirmedInflow].
	Explained   bool
	GeneratedAt time.Time
}

HotWalletReport is the hot-wallet comparison: on-chain balance vs the ledger-based expectation, with the in-flight (pending-signed / broadcast-unconfirmed) work that legitimately explains transient drift.

type LedgerPosition

type LedgerPosition struct {
	// Expected is Σ inflows − Σ outflows − Σ fee deductions.
	Expected sdkmath.Int
	// EarliestHeight is the lowest block height among the address's
	// counted ledger rows (0 when the address has no ledger activity).
	EarliestHeight uint64
	// LatestHeight is the highest counted block height.
	LatestHeight uint64
	// RelatedTxHashes are the distinct tx hashes touched (newest last,
	// capped at maxRelatedTxHashes).
	RelatedTxHashes []string
	// Rows is the number of counted ledger rows (fee outflows excluded).
	Rows int
}

LedgerPosition summarizes the ledger walk backing one expected-balance computation.

type Monitor

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

Monitor is the periodic disagreement checker.

func NewMonitor

func NewMonitor(cmp Comparer, store storage.Store, cfg DisagreementConfig, opts ...MonitorOption) (*Monitor, error)

NewMonitor builds a Monitor.

func (*Monitor) Check

func (m *Monitor) Check(ctx context.Context) (*client.CompareResult, error)

Check runs one comparison pass: mismatches open ChainReviewConditions (deduplicated per trigger), full agreement advances the auto-resolve streak, and the open-conditions gauge is refreshed either way.

func (*Monitor) Run

func (m *Monitor) Run(ctx context.Context) error

Run checks until ctx is done. Check errors are logged and retried on the next tick.

type MonitorOption

type MonitorOption func(*Monitor)

MonitorOption configures a Monitor.

func WithMonitorLogger

func WithMonitorLogger(l *slog.Logger) MonitorOption

WithMonitorLogger replaces the default logger.

func WithMonitorMetrics

func WithMonitorMetrics(s *metrics.Set) MonitorOption

WithMonitorMetrics attaches the adapter metric set.

func WithMonitorNow

func WithMonitorNow(now func() time.Time) MonitorOption

WithMonitorNow injects the clock (tests).

type Option

type Option func(*Reconciler)

Option configures a Reconciler.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger replaces the default logger.

func WithMetrics

func WithMetrics(s *metrics.Set) Option

WithMetrics attaches the adapter metric set (discrepancy counter + gauges).

func WithNow

func WithNow(now func() time.Time) Option

WithNow injects the clock (tests).

type Reconciler

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

Reconciler computes reconciliation reports from the store's ledger and the live chain.

func New

func New(store storage.Store, chain ChainView, cfg Config, opts ...Option) (*Reconciler, error)

New builds a Reconciler. chain may be nil only for ledger-only operations (ExpectedBalance); every comparison against live state requires it.

func (*Reconciler) Business

func (r *Reconciler) Business(ctx context.Context) (BusinessSection, error)

Business computes the business-layer section.

func (*Reconciler) ExpectedBalance

func (r *Reconciler) ExpectedBalance(ctx context.Context, address string) (LedgerPosition, error)

ExpectedBalance computes the ledger-based expected balance for one address (data model §8): every ledger row regardless of classification — including UNATTRIBUTED_REVIEW and block-scoped rows — counts, because the ledger records on-chain truth, not workflow opinion. Rows from failed transactions (tx_code != 0) moved no funds and are excluded; the fee those transactions still paid is captured by the FEE_DEDUCTION table and subtracted here.

func (*Reconciler) HotWallet

func (r *Reconciler) HotWallet(ctx context.Context, address string) (HotWalletReport, error)

HotWallet builds the comparison for one wallet address.

func (*Reconciler) NearRealTimePass

func (r *Reconciler) NearRealTimePass(ctx context.Context, cursors map[string]int64) error

NearRealTimePass reconciles every transaction newly appended to the ledger since the previous pass (per-address AfterID cursors, in-memory — a restart simply restarts the window; the hourly/daily balance formulas cover anything a restart skipped). Exported for the admin service and tests.

func (*Reconciler) ReconcileAddress

func (r *Reconciler) ReconcileAddress(ctx context.Context, address string) (storage.ReconciliationEntry, error)

ReconcileAddress compares the ledger-based expected balance against the live chain balance for one address and returns the FR-048 entry.

func (*Reconciler) ReconcileAddressReport

func (r *Reconciler) ReconcileAddressReport(ctx context.Context, address string) (storage.ReconciliationReport, error)

ReconcileAddressReport reconciles one address now and persists a MANUAL report (admin POST /v1/reconcile/address).

func (*Reconciler) ReconcileTx

ReconcileTx re-derives one transaction's watched-address movements from chain truth and diffs them against the persisted ledger rows (the TX_NEAR_REAL_TIME / manual per-tx check). kind must be TX_NEAR_REAL_TIME or MANUAL. A transaction the chain does not know is reported without entries (nothing on-chain to reconcile against; the ledger cannot be queried by bare hash) and logged.

func (*Reconciler) Run

Run generates one reconciliation report for the given kind:

WALLET_HOURLY  — operational wallets only (hot / cold / fee)
ADDRESS_DAILY  — every active watched address
MANUAL         — every active watched address, operator-triggered

(TX_NEAR_REAL_TIME reports come from ReconcileTx.) The report is persisted; every non-zero difference emits the FR-048 alert.

func (*Reconciler) RunSchedules

func (r *Reconciler) RunSchedules(ctx context.Context, sched Schedule) error

RunSchedules drives the three periodic kinds until ctx is done. Pass errors are logged and retried on the next tick; nothing here is fatal — reconciliation is an observer, never a mutator of chain-facing state.

type Schedule

type Schedule struct {
	NearRealTimeInterval time.Duration // default 1m
	WalletInterval       time.Duration // default 1h (adapter.yaml reconciler.wallet_interval)
	FullAddressInterval  time.Duration // default 24h (reconciler.full_address_interval)
}

Schedule carries the three periodic cadences. Zero values take the adapter.yaml contract defaults.

Jump to

Keyboard shortcuts

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