For authenticated remote connections, configure sslMode: "VerifyFull" using the
PostgreSQL TLS policy and migration guide. Legacy tls: true
encrypts traffic but does not authenticate the server certificate with Npgsql 9.
Miningcore's database contains financial accounting state. Use the procedure matching the task; do not improvise SQL from another section during an incident.
| Task | Section |
|---|---|
| Create a database | New installation |
| Back up or restore | Back up and restore |
| Apply release migrations | Upgrade an existing database |
| Recover after disk exhaustion | Recover after disk exhaustion |
| Import a recovery journal | Inspect and import a recovery journal |
| Reconcile a fatal recovery latch | Reconcile fatal share-recovery state |
| Recover payout ownership | Recover payout-manager ownership safely |
| Reconcile a wallet submission | Reconcile a Bitcoin-family payout |
| Inspect routine accounting state | Routine inspection |
| Partition a large shares table | Advanced share-table partitioning |
Start with Troubleshooting when the failing boundary is not yet known.
This guide expands the beginner database steps in the root README. Commands assume PostgreSQL is on
the local Linux host; adjust host names and access controls for a private database server.
Fresh-install and partitioning commands use the active prebuilt-release path under
/opt/miningcore/migrations/. Upgrade commands are the deliberate exception: they use the verified
candidate's immutable versioned directory so the old active release cannot supply stale SQL. A
source-build operator should substitute the checkout's
src/Miningcore/Persistence/Postgres/Scripts/ directory while preserving each filename and the
documented migration order.
| Task | Section |
|---|---|
| Create a new database | New installation |
| Back up and prove a restore | Back up and restore |
| Upgrade an existing schema | Upgrade an existing database |
| Recover a full filesystem or share journal | Recover after disk exhaustion |
| Reconcile a fatal share-accounting incident | Fatal share-recovery state |
| Release a stale payout owner safely | Payout-manager ownership |
| Inspect the live database | Routine inspection |
| Partition a large shares table | Advanced partitioning |
Use a currently supported PostgreSQL release. PostgreSQL 15 or newer is a sensible baseline for a new pool even if the schema remains technically compatible with older versions. The PPS arithmetic schema contract is tested on PostgreSQL 15, 16, 17 and 18. Validate a future PostgreSQL major with the same compatibility tests before upgrading.
sudo -u postgres psqlCREATE ROLE miningcore WITH LOGIN ENCRYPTED PASSWORD 'CHANGE_ME_TO_A_STRONG_PASSWORD';
CREATE DATABASE miningcore OWNER miningcore;
\qImport the complete current schema from the installed migrations:
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
--single-transaction \
-f /opt/miningcore/migrations/createdb.sqlFor a source-only installation from the repository checkout, use the reviewed source schema
instead of the prebuilt /opt/miningcore path:
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
--single-transaction -f - \
< src/Miningcore/Persistence/Postgres/Scripts/createdb.sqlRun either command from the layout it documents, never both. createdb.sql is only for a new,
empty database. -f - makes psql read stdin as file input, so SQL errors include input line
numbers and --single-transaction follows the documented file-input form. The invoking shell opens
the redirected checkout file before sudo starts psql, so the postgres account does not need
permission to traverse the developer's home directory. Use the release upgrade procedure for an
existing database.
Confirm the application login works:
psql -h 127.0.0.1 -U miningcore -d miningcore -c "SELECT current_user, current_database();"Before starting Miningcore or importing shares, consider
Advanced share-table partitioning. It is optional, but can
improve query and maintenance isolation for a large multipool deployment. Immediately after initial
schema creation is the simplest time to enable it because the shares table is still empty.
Small and single-pool installations can keep the standard table. Partitioning is not a requirement for correctness, and the appendix must not be run automatically against a populated live database.
The current createdb.sql already creates the storage and indexes used by the miner dashboard's
bestShare and bestSessionShare API fields. No separate best-share table or running aggregate is
required:
shares.actualdifficultystores the achieved difficulty used for the best-share comparison.shares.sessionidassociates a share with the worker's logical mining session.minerstats.sessionididentifies the current sessions represented by the latest statistics sample.
Miningcore calculates lifetime Best Share as the maximum actualdifficulty for the miner. Best
Session Share is the maximum for the session IDs in the miner's current statistics sample. The same
values are calculated per worker. Because they are derived from retained share history, deleting or
archiving old share rows can reduce the reported lifetime Best Share.
Existing databases created from an older schema should verify these columns before deploying the
current binary. Do not rerun createdb.sql over an existing database.
Use pg_hba.conf, PostgreSQL TLS and a host firewall when Miningcore and PostgreSQL are on different
machines. Do not expose port 5432 to the internet.
Create a compressed logical backup before every schema change:
sudo -u postgres pg_dump -Fc -d miningcore > miningcore-$(date +%F).dump
pg_restore --list miningcore-$(date +%F).dump > /dev/nullThe local administrator form includes partitions and other objects created by an administrative
role even when the runtime miningcore role cannot lock those objects directly. For a remote
database, use a dedicated backup role with read and lock access to every schema object instead of a
superuser.
Restore into an empty database during a tested recovery exercise:
createdb -h 127.0.0.1 -U postgres -O miningcore miningcore_restore_test
pg_restore -h 127.0.0.1 -U postgres -d miningcore_restore_test --no-owner miningcore-YYYY-MM-DD.dumpA backup is not proven until it has been restored and checked.
Important
The current release series changes the payout safety contract for every coin family. The payout-manager ownership migration is mandatory before starting any node with payment processing enabled, not only an LTC/DOGE node. Treat this as a breaking upgrade and schedule a maintenance window.
First extract and verify the selected candidate release without changing /opt/miningcore, following
the release upgrade procedure. Stop Miningcore block writers,
share-relay senders, receivers and recorders, recovery importers and payout managers on every node
that uses the database, take a verified backup, then point this shell at that exact immutable
candidate and apply the migrations with ON_ERROR_STOP:
export MININGCORE_VERSION=v0.3.0
export MININGCORE_UBUNTU=26.04 # use 22.04 with the compatibility archive
export MININGCORE_CANDIDATE_DIR="/opt/miningcore-${MININGCORE_VERSION}-linux-x64-ubuntu-${MININGCORE_UBUNTU}"
test -d "$MININGCORE_CANDIDATE_DIR/migrations"
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
-f "$MININGCORE_CANDIDATE_DIR/migrations/add_auxpow_block_idempotency.sql"
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
-f "$MININGCORE_CANDIDATE_DIR/migrations/add_payout_manager_ownership.sql"
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
-f "$MININGCORE_CANDIDATE_DIR/migrations/add_share_accounting.sql"Choose the candidate directory that exactly matches the verified version and host target. Never run
upgrade migrations through the stable /opt/miningcore symlink: it must continue pointing to the
old release until every required candidate migration succeeds.
The payout ownership migration assigns its tables to the database owner. The share-accounting
migration uses the existing shares table owner as the application role. Its cumulative
add_share_accounting.sql includes add_pps_arithmetic_version.sql, whose transition table
and guard/activation functions remain owned by the migration administrator. That boundary grants
the credit-table owner SELECT only, including when the database has a different owner.
Confirm the application role matches persistence.postgres.user and inspect the owners before
restarting Miningcore. See the PPS arithmetic rollout for activation:
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore -c "
SELECT current_database() AS database,
pg_get_userbyid(datdba) AS database_owner
FROM pg_database
WHERE datname = current_database();
SELECT schemaname, tablename, tableowner
FROM pg_tables
WHERE tablename IN (
'share_recovery_imports',
'payment_batches',
'payout_manager_ownership',
'share_accounting_groups',
'share_accounting_prune_state',
'pps_share_credits',
'pps_credit_remainders',
'pps_arithmetic_transitions'
)
ORDER BY schemaname, tablename;"For payout/recovery tables, if the database owner is not the configured application role,
correct the ownership or grant that application role the required privileges before startup.
Keep pps_arithmetic_transitions and its functions owned by the administrator; runtime roles
need only SELECT on that table. Do not make the Miningcore runtime role a PostgreSQL superuser
or grant it permission to assume a superuser role. Membership that permits SET ROLE to a
superuser grants full administrative access even when the login itself is marked NOSUPERUSER.
For activation logins with narrower authority, follow the tested
dedicated PPS boundary owner procedure
after each migration; the schema migration itself still runs as the database administrator.
The payout ownership migration is required wherever payment processing is enabled and for recorder or
recovery-only deployments using the -rs importer. The AuxPoW and share-accounting migrations are
required before enabling pooled LTC/DOGE merged mining or direct Bitcoin-family PPS, including a PPS
share-relay sender: it relays ordinary accounting evidence but still persists accepted block
candidates synchronously and owns an emergency candidate journal. Share accounting is not required
by an unchanged SOLO/SOLO topology. All scripts stop instead of guessing when incompatible or duplicate state
requires manual review. Recovery mode validates the share_recovery_imports table, its required columns and its
immediate filehash primary key before scanning the journal, so a missing or stale migration fails
early with an actionable message.
The direct-SOLO migration is required before running a canonical Bitcoin SOLO pool under
v0.3.0, where soloCoinbasePayout defaults to true. An operator retaining the older custodial
flow may instead set soloCoinbasePayout: false before upgrading. The migration adds immutable
settlement evidence to blocks; historical rows remain null and continue through their original
custodial lifecycle. Startup checks the exact types and validated settlement/submission constraint,
dedicated candidate index, ordered reconciliation index, prepared-submission replay index and
statement-scoped update guards before direct work begins. The same row is a durable submission
outbox containing the exact serialized block; prepared and
submitted-uncertain entries are replayed idempotently rather than inferred from audit metadata.
See the Bitcoin direct-SOLO guide.
add_share_accounting.sql is additive and transactional. Do not attempt a live rollback by dropping
its tables or columns: they can contain PPS liabilities and replay evidence that are not reconstructible
from blocks. To roll back the application, stop every writer and payout manager, preserve the current
database, and restore the verified pre-migration backup into an isolated replacement database. Reconcile
balances and any payments created after that backup before directing miners or wallets to the older
version.
The migration also seeds the required share_accounting_prune_state singleton and recreates the
composite (created, accountingid) pruning index. Startup requires that index to be a two-key,
ascending B-tree with the standard timestamptz and UUID operator classes; a same-named BRIN,
descending, expression, partial or covering index is not an equivalent contract. It also verifies
the singleton row before accepting pooled/PPS financial work. Reapplying the migration repairs a
missing singleton or stale same-named pruning index while Miningcore is stopped. Reapplication drops
and rebuilds this index, so on a large accounting table allow enough maintenance time for the rebuild
instead of rerunning the migration speculatively during a short service window.
The migration also adds a database check requiring accounting ID, role and reward basis to be either all absent or all valid, plus a foreign key from each identified share to its durable group manifest. This protects the pair even if a custom writer bypasses Miningcore's application checks.
Merged-mining startup verifies its partial unique indexes. Payout processing uses PostgreSQL ownership and an idempotent payment ledger. Only a clean shutdown clears the durable owner token. After a crash:
- Prove the previous process is no longer running.
- Reconcile wallet history for a payout that may have been submitted before its response was lost.
- Follow the guarded ownership recovery procedure only after that reconciliation.
- Start exactly one payout manager for the pool/database set.
Automatic hot-standby payout takeover is intentionally unsupported.
A full filesystem can stop PostgreSQL and coin daemons before Miningcore itself fails. Miningcore may then show PostgreSQL connection refusals, daemon RPC errors and systemd restart throttling. Treat those as downstream symptoms: restore storage and dependencies before restarting the pool.
First stop the restart loop and prove which filesystem is full. Check inodes as well as bytes, and
inspect the filesystems that actually contain PostgreSQL data, daemon data and Miningcore logs rather
than assuming they all reside under /home:
sudo systemctl stop miningcore
sudo systemctl reset-failed miningcore
pgrep -af 'Miningcore|Miningcore.dll' || true
df -hT
df -i
findmnt -T /var/lib/postgresql
sudo du -xhd1 /var /home 2>/dev/null | sort -h
sudo journalctl --disk-usage
sudo lsof +L1Free space only by removing or moving data whose purpose and recovery consequences are understood.
Do not delete postmaster.pid, PostgreSQL WAL files or anything under /var/lib/postgresql merely
to reclaim space. An open deleted file shown by lsof +L1 continues consuming space until its owning
process closes it; investigate that service rather than deleting unrelated database files. Confirm
the affected filesystem now has both free blocks and free inodes before continuing.
Recover PostgreSQL before Miningcore. Ubuntu installations can expose both the umbrella service and a versioned cluster, so inspect the actual cluster name before choosing a start command:
pg_lsclusters
sudo systemctl status postgresql --no-pager -l
sudo systemctl start postgresql
# If the cluster remains down, substitute the values shown by pg_lsclusters.
sudo pg_ctlcluster REPLACE_WITH_VERSION REPLACE_WITH_CLUSTER start
pg_isready -h 127.0.0.1 -p 5432
psql -h 127.0.0.1 -U REPLACE_WITH_MININGCORE_ROLE \
-d REPLACE_WITH_DATABASE -c "SELECT now();"If PostgreSQL still fails, read the service and kernel evidence before changing its data directory:
sudo journalctl -u postgresql -u 'postgresql@*' -b --no-pager -n 200
findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS -T /var/lib/postgresql
sudo journalctl -k -b --no-pager -n 200Stop and investigate filesystem or hardware errors when the mount is read-only or the kernel reports I/O faults. Restore from a verified backup if PostgreSQL reports unrecoverable corruption; do not attempt speculative WAL or control-file deletion.
Next recover every daemon required by the enabled pools and verify its RPC readiness. A daemon can accept a process start while still returning an initial-sync or loading-state RPC error, so wait for its normal blockchain-information call to succeed before starting Miningcore. Use the service names, CLI clients, RPC ports and authentication from the deployment rather than copying a coin-specific example blindly.
If the incident produced Share Recorder Policy Fallback, Fatal share-recovery fallback failure
or recovery-journal errors, reconcile the journal before restarting Miningcore. Use the absolute
journal path reported when the Share Recorder came online.
A normal fallback email proves only that its batch was force-flushed. It does not prove every earlier batch succeeded or reveal shares that reached neither durable target. Review the complete outage window for successful and fatal fallback messages.
A dual-target failure exits with status 74 and creates a hashed fatal latch below the service state directory. The fatal log and alert give its exact path. The supplied systemd unit does not restart status 74, and normal startup remains blocked while the latch exists or cannot be read safely.
Stop Miningcore before inspecting or importing an active journal. Record its metadata and checksum, then copy the original as incident evidence:
sudo systemctl stop miningcore
sudo stat -- REPLACE_WITH_ABSOLUTE_RECOVERY_FILE
sudo sha256sum -- REPLACE_WITH_ABSOLUTE_RECOVERY_FILE
sudo cp --preserve=all -- REPLACE_WITH_ABSOLUTE_RECOVERY_FILE \
REPLACE_WITH_EVIDENCE_COPYDo not edit, truncate or import a corrupt original in place. Treat any of the following as possible partial-write or replay evidence:
- a missing final newline;
- invalid JSON;
- a sequence, previous-frame link, count or hash mismatch; or
- a nested, unclosed or otherwise malformed frame.
Older unframed journals prove only their final newline boundary, not that the last multi-record batch was complete. Retain the original and create a separate input containing only complete, operator-reviewed records. Do not guess missing fields or add braces merely to make a fragment parse.
Ordinary share loss can affect proportional and PPLNS reward accounting. Do not dismiss an incomplete record merely because it is not a block candidate.
Use the reviewed source only after PostgreSQL is healthy:
cd REPLACE_WITH_MININGCORE_INSTALL_DIRECTORY
./Miningcore -c /etc/miningcore/config.json \
-rs REPLACE_WITH_REVIEWED_RECOVERY_FILEThe one-shot importer validates only configuration it consumes. At cluster level that allowlist is
logging, persistence, pools, shareRecoveryFile, shareRecoveryStateDirectory and optional
coinTemplates; malformed or duplicate live-only top-level settings are discarded while the file
is streamed. Recovery retains only the consumed logging.level and
logging.enableConsoleColors settings and validates their types; stale file-only logging settings
are discarded. An absent, null or wholly malformed logging section is replaced with default
informational, non-coloured console logging so emergency progress remains visible. Recovery returns
after its ownership and PostgreSQL preflights and does not initialize mining, hashing or native
solver runtimes.
Keep a non-empty pools array with unique, non-empty pool IDs and configure a complete
persistence.postgres endpoint for the target database. Pools may all remain disabled during the
import. Prefer the minimal set of pool IDs actually present in the reviewed journal: partition
preflight checks every configured ID, including an extra historical pool that has no record in this
source. Configured pool IDs are an explicit import allowlist: every journal record must match one
exactly. An unknown or mistyped record ID fails before a pending marker, transaction or manifest is
created. Add an intentional historical pool ID to the recovery configuration only after inspecting
the retained journal.
After a committed import, crash-resume retirement revalidates the marker, manifest, record count and content hash without requiring those historical IDs to remain in the current configuration; it cannot replay the already-committed data. Committed cleanup likewise does not require current AuxPoW indexes because it never replays a block. Fresh or unproven AuxPoW imports still require those indexes before Miningcore publishes a pending marker or opens the import transaction.
The configured recovery path and state directory still identify active journal
ownership, terminal anchors and interrupted retirement markers, even when -rs names a reviewed
copy.
Live mining settings do not need to be repaired before an emergency import. After ambiguity checks,
recovery rebuilds every pool object from its required id and optional string coin metadata. All
other pool fields are discarded, including cluster instance identity, enabled state, wallet and
daemon settings, API/Stratum listeners, payout and banning policy, reward recipients, timing values
and extension data. Normal startup restores strict validation for live configuration, so correct it
before restarting the pool.
Recovery checks a partition for every configured pool ID even though all sanitized pools are
disabled. Once the complete journal has passed integrity and semantic validation, Miningcore also
requires add_auxpow_block_idempotency.sql when an unpersisted block candidate uses
bitcoin-direct, bitcoin-coinbase-direct, auxpow, auxpow-claim, merged-parent, or merged-parent-uncertain. That requirement comes from the
recovery evidence itself rather than discarded live merged-mining settings, and it is checked before
the import transaction begins.
Coin definitions are notification enrichment, not an import prerequisite. Recovery retains valid custom template paths and attempts to load and assign templates to every configured pool, including disabled pools. A missing or malformed path, missing coin metadata, or undefined coin logs a warning and import continues; a recovered block-found notification may be skipped when no template could be assigned. Database persistence and journal integrity failures remain fatal.
Before opening its PostgreSQL transaction, the importer validates:
- every versioned frame's markers, sequence and previous-frame link;
- the declared record count, record SHA-256 and deterministic frame digest;
- the complete terminal anchor when importing the configured chained-v2 recovery journal;
- the source's stable regular-file identity; and
- every record's pool ID against the configured recovery pool allowlist.
Frame hashes normalise line endings to \n, so they protect logical records rather than the original
physical newline encoding. Verified frame-comment lines are skipped while records are deserialized.
The importer then commits the records and their share_recovery_imports manifest atomically. Its
durable source-retirement marker advances through these phases:
CommittedArchiveDurableAnchorRetirementAuthorisedAnchorRetired
The source is atomically renamed with an .imported-* suffix and its directory is synchronized.
The terminal anchor is retired only after durable authorization, and the marker is removed and
synchronized last. Normal startup and fallback appends remain blocked while that marker exists.
If recovery reports an archive, directory-sync or anchor-retirement failure:
- Do not edit, replace or grow the source.
- Rerun the same recovery command with the same configuration and exact source path.
- Retain the source, marker and terminal anchor if Miningcore reports changed or unavailable proof.
Before a committed marker can retire the source or anchor, Miningcore uses a fresh PostgreSQL connection to confirm the exact manifest hash, filename and record count. It then revalidates the complete chain, terminal anchor, semantic hash, record count and file identity. This allows retirement to resume without inserting committed records again.
- Symlink and hard-link aliases of the configured journal are rejected.
- A stable parent-directory symlink is supported, but later retargeting fails closed.
- Final-component symlinks, FIFOs and other non-regular sources are rejected before database access.
- Retain the imported archive and confirm its manifest row and record count.
- Reconcile the origin and existing manifest of journals from old working directories or deployments.
Manifests identify complete semantic files, not individual shares. Importing A and later importing
an overlapping A+B source can duplicate A; never combine or import overlapping recovery sets.
An Uncertain PostgreSQL share commit incident is deliberately different from an ordinary recovery
journal. Miningcore cannot prove whether PostgreSQL committed, so it does not append those shares
to the importable journal.
The status-74 latch names an exact-share sidecar and its expected SHA-256. The sidecar contains one
shareJsonBase64= record per line for read-only reconciliation. Verify its hash, decode the records
and compare them with PostgreSQL. Do not copy them into a journal or replay them unless reconciliation
proves they are absent.
A detailState=hash-pending latch remains authoritative even if its sidecar is absent or partial.
Earlier v2 incidents may call the equivalent state incomplete. Preserve the fixed latch, every
immutable .incident file, every sidecar and temporary evidence until each record has a conclusive
disposition.
Use the same configuration as the service so Miningcore resolves the correct journal and state directory:
cd REPLACE_WITH_MININGCORE_INSTALL_DIRECTORY
./Miningcore -c /etc/miningcore/config.json --verify-share-recovery-state
echo "exit=$?"The verifier checks:
- every path-scoped incident's metadata, sequence and previous-digest chain;
- the fixed latch's expected incident count and chain tip;
- each sidecar's SHA-256 and decoded record count; and
- stable file identity and length while the restrictive no-follow handle remains open.
Sidecar records are limited to 1,048,576 characters. The smaller .fatal, .incident and
.acknowledged files use strict UTF-8, a cumulative 64-KiB raw-byte limit and a 16-KiB per-line limit.
Concurrent modification, path replacement, malformed data and memory exhaustion all fail
verification instead of being ignored.
The command returns status 74 when evidence is missing, malformed, hash-pending or incomplete. A completed verified sidecar whose latch still records the immediately preceding hash-pending state is the one recoverable transition; startup or acknowledgement completes that transition under the mutation lock but still requires operator reconciliation.
Important
Successful verification proves only that the evidence is structurally complete. It does not prove the uncertain shares were reconciled against PostgreSQL. The verifier never edits, imports or deletes recovery files.
Recovery-journal frame chains detect duplicated, missing and reordered middle frames. Fatal
incidents use the separate sequence and previous-digest chain verified in the preceding step. New
journal writes also store their final sequence and digest under
shareRecoveryStateDirectory/share-recovery-terminal. Preserve this anchor with the journal during
inspection, backup and restore; startup and import reject a shorter otherwise-valid prefix.
Legacy/v1 history cannot gain retroactive protection. Retain the checksum captured before repair or import and compare it with backups or monitoring when earlier truncation is plausible. Miningcore must successfully enumerate the exact state directory before treating a marker as absent. A directory, symlink, malformed or inaccessible marker remains unreconciled and blocks startup with status 74.
If the configured active journal is corrupt and a separate reviewed copy was imported, preserve the
original evidence but remove it from the live journal path before clearing the latch. Prefer an
atomic rename on the same filesystem, recording the before/after path and checksum. Alternatively,
move it to a protected evidence directory on that filesystem or configure shareRecoveryFile to a
fresh empty path. Never replace the live path with repaired content silently:
sudo sha256sum -- REPLACE_WITH_ABSOLUTE_RECOVERY_FILE
sudo mv -- REPLACE_WITH_ABSOLUTE_RECOVERY_FILE \
REPLACE_WITH_ABSOLUTE_RECOVERY_FILE.corrupt-evidence-YYYYMMDDTHHMMSSZ
sudo sha256sum -- \
REPLACE_WITH_ABSOLUTE_RECOVERY_FILE.corrupt-evidence-YYYYMMDDTHHMMSSZContinue only after all of these are complete:
- the reviewed import and its manifest/count are verified;
- the corrupt active path has a recorded disposition; and
- every uncertain sidecar record has been reconciled against PostgreSQL.
Use the same configuration as the service. Do not manually delete the latch, incident metadata, sidecars or journal evidence:
cd REPLACE_WITH_MININGCORE_INSTALL_DIRECTORY
./Miningcore -c /etc/miningcore/config.json --verify-share-recovery-state
echo "verify_exit=$?"
./Miningcore -c /etc/miningcore/config.json --acknowledge-share-recovery-state
echo "acknowledge_exit=$?"Both commands return status 74 when evidence is incomplete or unsafe. Acknowledgement:
- repeats structural verification;
- force-durably publishes an immutable
.acknowledgedanchor for the retained chain; - removes the active
.fatallatch; and - synchronizes the state directory.
This process is idempotent if interrupted. It does not delete evidence or perform database reconciliation for the operator. Startup accepts the acknowledged chain only while its latest anchor covers every retained incident and every referenced sidecar still passes complete hash, framing and record-count validation. Later incidents extend from that tip and require another reconciliation and acknowledgement.
Prerelease v2-only sets use a v4 legacy-set anchor that preserves the original evidence. Do not rename or rewrite those files.
Startup inspection, incident publication and acknowledgement share a cross-process .mutation.lock
in the recovery-state directory. Leave it in place. If Miningcore or another recovery command owns
the lock, stop the competing process or wait; never bypass the lock or edit the state by hand.
Finally start Miningcore and inspect its complete startup, API health and pool state:
sudo systemctl reset-failed miningcore
sudo systemctl start miningcore
sudo systemctl status miningcore --no-pager -l
sudo journalctl -u miningcore -b --no-pager -n 200If the database session disappeared during the outage, the durable payout-manager ownership marker may correctly remain set even though PostgreSQL is healthy again. Do not clear it merely to make the service start. Reconcile any possibly submitted wallet transaction, prove the old process is dead, then follow Recover payout-manager ownership safely.
After recovery, verify a fresh PostgreSQL backup, retain the incident logs, and add monitoring for filesystem bytes, inodes, the configured recovery-journal filesystem, PostgreSQL readiness, daemon RPC readiness and the Miningcore service. Miningcore's native file rotation bounds its own configured file targets, but database, daemon, journal and reverse-proxy storage still require separate policy.
Miningcore holds both a PostgreSQL session advisory lock and one durable ownership row for the complete payout-manager lifetime. The advisory lock rejects a second live manager. The durable row continues to block replacement after a process, host or database-session loss, because an operator may need to reconcile a wallet submission whose response was lost.
Stop Miningcore before stopping or restarting its PostgreSQL server during planned maintenance. A clean Miningcore shutdown clears the durable row; stopping PostgreSQL first destroys the guard session and deliberately leaves the row owned. When PostgreSQL is local, the safe order is:
sudo systemctl stop miningcore
sudo systemctl is-active miningcore
pgrep -af 'Miningcore|Miningcore.dll' || true
sudo systemctl stop postgresqlUse the reverse dependency order at startup: PostgreSQL first, then Miningcore. Adapt the service name when the host runs a versioned PostgreSQL unit, a container or a remote database.
If startup reports a payout-ownership conflict, stop the automatic restart loop before diagnosing it. Repeated restarts cannot clear the marker:
sudo systemctl stop miningcore
sudo systemctl reset-failed miningcore
pgrep -af 'Miningcore|Miningcore.dll' || trueRun the following read-only queries against the exact database and role selected by
persistence.postgres. Miningcore does not configure a PostgreSQL schema explicitly; relation
resolution follows the application role's effective search_path. Prefer connecting as that role,
then record the resolved schema before inspecting any ownership data:
psql -h REPLACE_WITH_HOST -U REPLACE_WITH_MININGCORE_ROLE \
-d REPLACE_WITH_DATABASE -x -c "
SELECT current_database(),
current_user,
current_schema(),
current_setting('search_path');
SELECT namespace.nspname AS resolved_schema,
relation.relname AS resolved_relation
FROM pg_class relation
JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace
WHERE relation.oid = to_regclass('payout_manager_ownership');"Stop if the relation does not resolve or resolves to an unexpected schema. Replace
REPLACE_WITH_SCHEMA in this separate inspection command with the discovered resolved_schema:
psql -h REPLACE_WITH_HOST -U REPLACE_WITH_MININGCORE_ROLE \
-d REPLACE_WITH_DATABASE -x -c "
SELECT id, generation, owner_id, owner_host, owner_process_id,
acquired, released
FROM REPLACE_WITH_SCHEMA.payout_manager_ownership
WHERE id = 1;"Use the schema identifier exactly as PostgreSQL reports it. Double-quote mixed-case or otherwise
non-standard identifiers. Within the double-quoted psql -c shell command shown above, escape those
quotes as \"PoolData\".payout_manager_ownership. If an administrative role is required later for
the guarded update, keep using this explicitly inspected schema; do not rely on the administrator's
different search_path.
The companion advisory lock uses the stable two-key identity 19779, 5259609. Inspect it without
trying to terminate its PostgreSQL backend:
psql -h REPLACE_WITH_HOST -U REPLACE_WITH_MININGCORE_ROLE \
-d REPLACE_WITH_DATABASE -x -c "
SELECT activity.pid AS postgres_backend_pid,
activity.usename,
activity.application_name,
activity.client_addr,
activity.backend_start
FROM pg_locks lock
JOIN pg_stat_activity activity ON activity.pid = lock.pid
WHERE lock.locktype = 'advisory'
AND lock.database = (
SELECT oid FROM pg_database WHERE datname = current_database()
)
AND lock.classid = 19779::oid
AND lock.objid = 5259609::oid
AND lock.objsubid = 2
AND lock.granted;"- A returned advisory-lock row means a live database session still owns payout processing. Locate the recorded host and process, confirm it is the expected Miningcore instance, and stop it cleanly. Never clear the durable row underneath it.
- No advisory-lock row plus a populated
owner_idmeans the database session is gone but the durable marker remains. Confirm the process is absent on the recorded host and on every node configured for the same database. - An empty
owner_idmeans this explicitly inspected schema has no durable owner. If startup still reports a conflict, verify the database, application role and schema again.
Use the recorded acquisition time and the known stop or database-failure time to define a log window with a safety margin. A process ID is supporting evidence only: numeric PIDs can be reused, including across boots, and do not identify a container's host journal records reliably.
For a systemd deployment, select the unit and bounded time window together:
sudo journalctl \
-u miningcore.service \
--since 'REPLACE_WITH_ACQUIRED_TIME_MINUS_MARGIN' \
--until 'REPLACE_WITH_STOP_OR_DATABASE_FAILURE_TIME_PLUS_MARGIN' \
--no-pager -o short-iso-preciseAlso constrain the search to the relevant boot (journalctl --list-boots and -b) or
_SYSTEMD_INVOCATION_ID when that identity is available. Add _PID=... only after the unit,
boot or invocation, and time boundaries are established; never treat _PID alone as exact process
identity.
For a Docker deployment, first identify the exact container and logging driver, then use the same bounded window:
docker inspect --format '{{.Id}} {{.Name}} {{.HostConfig.LogConfig.Type}}' \
REPLACE_WITH_CONTAINER
docker logs \
--since 'REPLACE_WITH_ACQUIRED_TIME_MINUS_MARGIN' \
--until 'REPLACE_WITH_STOP_OR_DATABASE_FAILURE_TIME_PLUS_MARGIN' \
REPLACE_WITH_CONTAINERWhen Docker uses the journald logging driver, query the recorded CONTAINER_ID_FULL or verified
CONTAINER_NAME plus the time window instead. A host PID is not the container identity.
sudo journalctl \
CONTAINER_ID_FULL=REPLACE_WITH_FULL_CONTAINER_ID \
--since 'REPLACE_WITH_ACQUIRED_TIME_MINUS_MARGIN' \
--until 'REPLACE_WITH_STOP_OR_DATABASE_FAILURE_TIME_PLUS_MARGIN' \
--no-pager -o short-iso-preciseMiningcore may write payout-cycle evidence to logging.logFile instead of the console. If console
logging was disabled, or a file path was configured, inspect that file and its rotated files over
the same time window. An empty systemd or Docker log is not proof that no wallet submission occurred
until every configured logging destination and the correct service/container invocation have been
checked.
Review every payout cycle after ownership was acquired. If the log only reports No balances over configured minimum payout, no wallet submission began. If it processed payable balances, reported
an unknown wallet outcome, lost transport after submission, or stopped during payment persistence,
reconcile the daemon or wallet transaction history with Miningcore's payments and
payment_batches records before proceeding. Do not repair balances or payment rows with ad-hoc SQL.
Use the bounded log window established above. The following read-only query lists transaction IDs
that Miningcore received from the wallet and durably persisted during that window. Replace the
schema and timestamps with the values already inspected; keep the explicit schema even when it is
public:
SELECT batch.poolid,
batch.transactionconfirmationdata AS transaction_id,
batch.created AS batch_created,
COUNT(payment.id) AS recipient_count,
COALESCE(SUM(payment.amount), 0) AS recorded_amount
FROM REPLACE_WITH_SCHEMA.payment_batches AS batch
LEFT JOIN REPLACE_WITH_SCHEMA.payments AS payment
ON payment.poolid = batch.poolid
AND payment.transactionconfirmationdata = batch.transactionconfirmationdata
WHERE batch.created >= TIMESTAMPTZ 'REPLACE_WITH_WINDOW_START'
AND batch.created <= TIMESTAMPTZ 'REPLACE_WITH_WINDOW_END'
GROUP BY batch.poolid, batch.transactionconfirmationdata, batch.created
ORDER BY batch.created, batch.poolid, batch.transactionconfirmationdata;Inspect the recorded recipients for one candidate transaction without changing accounting data:
SELECT poolid,
address,
amount,
transactionconfirmationdata AS transaction_id,
created
FROM REPLACE_WITH_SCHEMA.payments
WHERE poolid = 'REPLACE_WITH_POOL_ID'
AND transactionconfirmationdata = 'REPLACE_WITH_TRANSACTION_ID'
ORDER BY id;A payment_batches row, any public payments rows and the corresponding balance resets are committed
together. Miningcore deliberately omits active, positive-percentage rewardRecipients from public
payment history, so a batch with no matching payments rows can be valid when every balance
represented by that transaction belonged to an active reward recipient. A zero-percent entry is
inactive and remains visible in payment history if the same address earns a miner payout. This is
especially relevant to a per-recipient payout path in which one wallet transaction pays only one
reward recipient.
Treat a zero-public-payment batch as requiring reconciliation, not as corruption by itself. Inspect
the positive-percentage rewardRecipients configuration that was active at payout time, the
transaction's wallet outputs, the bounded Miningcore log and nearby balance_changes rows whose
usage is
Balance reset after payment. Current configuration alone is insufficient if reward recipients
changed after the payout, and timestamp proximity is supporting evidence rather than a transaction-ID
link. If any represented wallet recipient was not an active, positive-percentage reward recipient,
or the evidence is incomplete, stop and investigate before releasing ownership.
SELECT poolid,
address,
amount,
usage,
created
FROM REPLACE_WITH_SCHEMA.balance_changes
WHERE poolid = 'REPLACE_WITH_POOL_ID'
AND usage = 'Balance reset after payment'
AND created >= TIMESTAMPTZ 'REPLACE_WITH_BATCH_TIME_MINUS_MARGIN'
AND created <= TIMESTAMPTZ 'REPLACE_WITH_BATCH_TIME_PLUS_MARGIN'
ORDER BY created, id;No matching batch does not prove that the wallet submitted nothing. When a transport failure loses the wallet response, Miningcore may never receive the transaction ID and therefore cannot persist it; use the unknown-ID procedure below.
For a known transaction ID, query the node mempool and the exact wallet that Miningcore uses. These
commands are read-only. Supply the same RPC authentication, network and wallet-selection arguments
as the production daemon; omit -rpcwallet only when that daemon has no named wallet:
TXID='REPLACE_WITH_TRANSACTION_ID'
bitcoin-cli getmempoolentry "$TXID"
bitcoin-cli -rpcwallet='REPLACE_WITH_WALLET' gettransaction "$TXID" trueInterpret the two results together:
- A valid
getmempoolentryresult means the node accepted the transaction. Anunbroadcast: truefield only means that no peer has acknowledged initial relay yet; it is not a wallet-only payout. getmempoolentryreturning transaction-not-found whilegettransactionreports positive confirmations means the payout is mined and completed.- Transaction-not-found plus zero confirmations means the wallet knows the transaction but the
node has not accepted or mined it. Inspect
walletconflicts, the raw transaction and daemon configuration before deciding whether to rebroadcast it. - Negative confirmations or a non-empty conflicting-wallet transaction require conflict reconciliation. Do not clear ownership merely because the original transaction is absent.
For an unknown transaction ID, search the production wallet over the same bounded UTC window.
Increase the listtransactions count when the wallet has more than 1,000 recent entries. The jq
filter is optional but makes sent transactions easier to group by ID:
FROM_EPOCH=$(date --utc --date='REPLACE_WITH_WINDOW_START' +%s)
TO_EPOCH=$(date --utc --date='REPLACE_WITH_WINDOW_END' +%s)
bitcoin-cli -rpcwallet='REPLACE_WITH_WALLET' \
listtransactions '*' 1000 0 true |
jq --argjson from "$FROM_EPOCH" --argjson to "$TO_EPOCH" '
[.[]
| select(.category == "send")
| select(.time >= $from and .time <= $to)]
| group_by(.txid)
| map({
transaction_id: .[0].txid,
time: .[0].time,
confirmations: .[0].confirmations,
wallet_conflicts: .[0].walletconflicts,
recipients: map({address, amount, fee})
})'Compare candidate recipients and amounts with the bounded Miningcore logs and the unpaid balances
that triggered the cycle. Wallet send amounts are normally negative, fees may appear on only one
entry, and a single sendmany transaction can have several recipient entries. Run the known-ID
queries for every candidate. If the initial search finds nothing, expand the wallet-history range
and inspect daemon or RPC-proxy logs; absence from the first page is not proof of no submission.
If a known transaction is wallet-only, fully signed, conflict-free and confirmed to be the exact persisted payout, rebroadcasting the same raw transaction preserves its transaction ID. Inspect its decoded outputs before taking the mutating step:
RAW_TX=$(bitcoin-cli -rpcwallet='REPLACE_WITH_WALLET' \
gettransaction "$TXID" true | jq -er '.hex')
bitcoin-cli decoderawtransaction "$RAW_TX"
# After verifying every output and correcting the cause of failed relay:
bitcoin-cli sendrawtransaction "$RAW_TX"Never run sendmany or sendtoaddress as a substitute for rebroadcasting a known payout: those
commands create a new financial operation. If the original transaction cannot safely be broadcast,
document and complete an operator-approved compensation procedure before releasing ownership. Do
not alter Miningcore balances, payments or batch rows ad hoc.
After proving that every previous payout manager is dead and completing any required wallet
reconciliation, release only the owner token and generation that were inspected. Run this block in
psql after replacing the explicitly inspected schema, owner token and generation placeholders.
The transaction-level advisory lock prevents a new payout manager from racing the release, and the
expected token/generation prevents an operator from clearing a newer owner accidentally:
BEGIN;
DO $recovery$
DECLARE
released_rows integer;
BEGIN
IF NOT pg_try_advisory_xact_lock(19779, 5259609) THEN
RAISE EXCEPTION 'A payout manager still holds the advisory lock';
END IF;
UPDATE REPLACE_WITH_SCHEMA.payout_manager_ownership
SET owner_id = NULL,
owner_host = NULL,
owner_process_id = NULL,
released = now()
WHERE id = 1
AND generation = REPLACE_WITH_INSPECTED_GENERATION
AND owner_id = 'REPLACE_WITH_INSPECTED_OWNER_UUID'::uuid;
GET DIAGNOSTICS released_rows = ROW_COUNT;
IF released_rows <> 1 THEN
RAISE EXCEPTION 'The inspected payout-manager owner changed; release aborted';
END IF;
END
$recovery$;
COMMIT;Start exactly one intended payout manager and confirm that it acquires the next ownership generation, remains active through a complete payout interval, and starts every expected pool:
sudo systemctl start miningcore
sudo systemctl status miningcore --no-pager
sudo journalctl -u miningcore --since '10 minutes ago' --no-pagerIf mining and share recording must resume before wallet reconciliation is complete, pause the payout-manager pipeline without violating the deployment's startup validation:
- On a node without enabled LTC-DOGE merged mining, or on a share-relay sender with the top-level
shareRelayconfigured, set the top-levelpaymentProcessing.enabledvalue tofalse. - On a direct pool or share-relay receiver/recorder with LTC-DOGE merged mining enabled (no
top-level
shareRelay), keep the top-levelpaymentProcessing.enabledvaluetrueand temporarily setpaymentProcessing.enabledtofalseinside every enabled pool. Direct and receiver nodes require the cluster-level switch for merged-block reconciliation validation, while disabling all pool-level switches preventsPayoutManagerfrom being registered.
For the merged-mining case, preserve every other payment setting and change only each pool's
enabled value:
{
"paymentProcessing": {
"enabled": true
},
"pools": [
{
"id": "ltc",
"paymentProcessing": {
"enabled": false
}
},
{
"id": "doge",
"paymentProcessing": {
"enabled": false
}
}
]
}Validate the complete JSON before starting Miningcore. This emergency configuration pauses the entire payout-manager pipeline: pending-block classification, uncertain merged-block reconciliation, maturity processing, balance credit and wallet payouts. Newly persisted block candidates remain in PostgreSQL and are processed after the original configuration is restored. It does not clear or supersede the durable marker. Keep this mode temporary, inspect the pending block backlog before re-enabling the pipeline, and restore the original configuration only after ownership recovery is complete and exactly one designated node is ready to acquire it.
psql -h 127.0.0.1 -U miningcore -d miningcore\dt
SELECT poolid, blockheight, status, type, created
FROM blocks
ORDER BY created DESC
LIMIT 20;
SELECT poolid, address, amount, created
FROM payments
ORDER BY created DESC
LIMIT 20;
SELECT poolid, count(*) AS credits,
sum(calculatedamount) AS exact_pps_liability,
sum(creditedamount) AS posted_balance_amount
FROM pps_share_credits
GROUP BY poolid
ORDER BY poolid;
SELECT poolid, count(*) AS projected_shares,
count(DISTINCT accountingid) AS accounting_groups
FROM shares
WHERE accountingid IS NOT NULL
GROUP BY poolid
ORDER BY poolid;
SELECT count(*) AS groups_with_excess_projection_rows
FROM share_accounting_groups groups
WHERE (SELECT count(*) FROM shares
WHERE shares.accountingid = groups.accountingid)
> groups.projectioncount;The accounting group and PPS credit tables are replay evidence and are not ordinary share retention tables. Settled PROP/PPLNS/PPS share rows may be deleted by payout cleanup, so a healthy group may retain any subset of its original projections as the two pools cross independent payout boundaries. Miningcore authenticates every remaining projection against the original payload and accepts the subset on a retry. The group receipt proves the original projections were committed in one transaction; an excess or conflicting row still fails closed. Do not delete these records with an independent shares-retention job. Miningcore retires them only after the configured replay horizon has passed and rejects older evidence before it can create another liability.
Use these queries for inspection only. Never repair balances or payments with ad-hoc SQL.
paymentProcessing.shareAccountingRetentionDays defaults to 30 days. It must exceed the longest
supported relay outage, recovery-journal retention and incident-response window. Once the horizon
passes, Miningcore rejects the old envelope and its payout-manager maintenance pass prunes the
corresponding pps_share_credits, PPS balance_changes, and orphaned
share_accounting_groups after an additional one-day safety margin. A transaction-locked
share_accounting_prune_state keyset cursor advances the bounded group scan past still-referenced
PROP/PPLNS or long-retention rows and wraps after reaching the expiry tail. Pinned groups therefore
remain protected without permanently starving later eligible receipts. Registration also enforces the
replay cutoff inside the accounting transaction: an expired new ID is rejected, while a retained
receipt can still prove an already committed replay. Statistical-share and evidence deletes are
index-supported and limited by paymentProcessing.shareAccountingPruneBatchSize per pool/table and
payout cycle. The default is 50,000 rows and startup permits 1,000 through 100,000; a warning means an
unexamined expiry window remains and later cycles will continue scanning it. Referenced rows at
the scanned tail do not produce a false backlog warning. One maintenance transaction can process the
configured limit for each PPS pool and each global evidence table, so multiply the possible work by
the number of PPS pools before increasing the setting. An oversized multi-pool transaction can hold
back vacuum progress even though every individual delete is bounded. Per-recipient
pps_credit_remainders remain because they carry exact
sub-unit value and grow with recipients, not shares. PPS statistical rows use the separate per-pool
ppsShareRetentionDays setting (default 7) and are pruned even when the pool finds blocks; block
settlement never shortens that statistical window.
Approximate row creation at 20 accepted accounting envelopes per second is:
| Evidence | Rows/day | Rows at 30 days |
|---|---|---|
| Accounting groups | 1,728,000 | 51,840,000 |
| PPS credits, one PPS projection | 1,728,000 | 51,840,000 |
| PPS balance changes, upper bound | 1,728,000 | 51,840,000 |
| PPS credits/changes, two PPS projections | 3,456,000 each | 103,680,000 each |
Actual disk cost depends on addresses, indexes, PostgreSQL settings and vacuum state. Measure it:
SELECT relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
n_live_tup
FROM pg_stat_user_tables
WHERE relname IN ('share_accounting_groups', 'share_accounting_prune_state',
'pps_share_credits', 'pps_credit_remainders',
'balance_changes', 'shares')
ORDER BY pg_total_relation_size(relid) DESC;At the default 600-second payout interval, a 50,000-row batch can retire 7.2 million rows per table per day. That exceeds the table-specific 3.456-million-row upper bound in the two-PPS-projection example above. For another interval or workload, choose at least:
peak rows created per second in the busiest pruned table × paymentProcessing.interval
and retain operational headroom for catch-up after downtime. Keep the value bounded rather than turning retention into one unbounded transaction. A persistent backlog warning means creation is outpacing the selected capacity or referenced rows are still being swept; inspect table counts and the cursor before increasing the batch.
If policy requires audit retention beyond the live replay horizon, stop Miningcore at a planned boundary, take and verify the normal custom-format database backup, and export the expiring rows before restarting. Record the UTC cutoff and archive checksum with the backup:
cutoff='2026-08-01T00:00:00Z'
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
--csv -c "SELECT * FROM share_accounting_groups WHERE created <= '$cutoff' ORDER BY created, accountingid" \
> share-accounting-groups.csv
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
--csv -c "SELECT * FROM pps_share_credits WHERE created <= '$cutoff' ORDER BY created, accountingid, poolid" \
> pps-share-credits.csv
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
--csv -c "SELECT * FROM balance_changes WHERE usage='PPS share credit' AND created <= '$cutoff' ORDER BY created, id" \
> pps-balance-changes.csv
sha256sum share-accounting-groups.csv pps-share-credits.csv \
pps-balance-changes.csv > share-accounting-archive.sha256
sha256sum --check share-accounting-archive.sha256Keep the files protected like wallet/accounting data. Do not manually delete the live rows; allow Miningcore's ordered maintenance transaction to do so after the same cutoff becomes eligible.
The optional packaged /opt/miningcore/migrations/createdb_postgresql_11_appendix.sql converts
shares to a list-partitioned layout. This can improve a large multipool cluster because most
Miningcore queries are scoped to one pool. It is not needed for a first installation or a small
pool.
Caution
The appendix deletes and rebuilds shares. Stop every recorder and recovery importer first.
Take a verified, restorable backup and practise the complete procedure on a restored database.
Do not run it against a live production database with active writers.
Create both the normal full backup described above and a focused shares archive:
sudo -u postgres pg_dump -Fc -d miningcore -t public.shares \
> shares-before-partition.dump
pg_restore --list shares-before-partition.dump > /dev/nullRecord the count for every pool so the restored result can be compared:
sudo -u postgres psql -d miningcore -c "
SELECT poolid, count(*)
FROM public.shares
GROUP BY poolid
ORDER BY poolid;"With Miningcore stopped:
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcore \
-f /opt/miningcore/migrations/createdb_postgresql_11_appendix.sqlThe script is transactional, but a successful run intentionally leaves the new parent empty and without child partitions.
This step is mandatory. The text in FOR VALUES IN (...) must exactly match the pool's id in
config.json. The child table name is an ordinary PostgreSQL identifier and may replace hyphens
with underscores:
sudo -u postgres psql -v ON_ERROR_STOP=1 -d miningcoreSET ROLE miningcore;
CREATE TABLE public.shares_btc1_solo
PARTITION OF public.shares
FOR VALUES IN ('btc1-solo');
CREATE TABLE public.shares_ltc1_solo
PARTITION OF public.shares
FOR VALUES IN ('ltc1-solo');
CREATE TABLE public.shares_doge1_solo
PARTITION OF public.shares
FOR VALUES IN ('doge1-solo');
RESET ROLE;Use your actual enabled pool IDs, not the examples. Create a partition before enabling any new pool later. Merged mining now creates one ordinary auxiliary projection for every attributed proof, so the auxiliary pool always needs a partition even when its direct Stratum listener is disabled.
Miningcore now checks this during startup on direct recorder nodes, share-relay receivers and recovery imports. Normal startup checks enabled pool IDs; recovery checks every configured recovery pool ID because enabled state is deliberately discarded. A missing partition fails before Stratum opens or recovery data is imported. Sender-only share-relay nodes skip this local check because their ordinary shares are recorded elsewhere.
A PostgreSQL DEFAULT partition is also routable and therefore passes the preflight, but dedicated
per-pool partitions preserve the performance and operational isolation this layout is intended to
provide.
Restore the saved rows only after every required partition exists:
sudo -u postgres pg_restore --exit-on-error --data-only \
--table=public.shares -d miningcore shares-before-partition.dumpList the partition bounds and owners:
sudo -u postgres psql -d miningcore -c "
SELECT child.relname AS partition_name,
pg_get_userbyid(child.relowner) AS owner,
pg_get_expr(child.relpartbound, child.oid) AS partition_bound
FROM pg_inherits
JOIN pg_class parent ON parent.oid = inhparent
JOIN pg_class child ON child.oid = inhrelid
WHERE parent.oid = 'public.shares'::regclass
ORDER BY child.relname;"Repeat the per-pool count query from step 1 and compare every result before restarting Miningcore. Keep the backup until normal share persistence and statistics have been observed successfully.