test(dashmate): live state sync e2e — join tooling, churn, re-sync and fallback coverage - #4530
test(dashmate): live state sync e2e — join tooling, churn, re-sync and fallback coverage#4530PastaPastaPasta wants to merge 54 commits into
Conversation
…ate sync Adds a minimal, platform-versioned subset of the Platform state that will be written into the replicated GroveDB state (Misc tree) so state-synced nodes can reconstruct the full Platform state, which is otherwise only persisted to non-replicated aux storage. Unlike the earlier prototype, fee versions of previous epochs are persisted faithfully by version number, and unknown-at-store-time block fields (app hash, block id hash, signature) are Options instead of zero-filled placeholders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds platform.drive.tenderdash.stateSync (enabled, retries, chunkRequestTimeout, fetchersCount) and platform.drive.abci.stateSync.snapshots (enabled, frequencySeconds, maxCount). Tenderdash 1.7 minimums are encoded in the schema: chunk request timeout of at least 5s, 1-64 fetchers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base config enables consuming (tenderdash stateSync) and serving (drive snapshots every 600s, keeping 6). Serving is always on in Tenderdash and a node with local state ignores the consume flag, so the default is safe for existing nodes. The local preset disables both: a local network genesis starts every node from scratch, so there is no populated peer to sync from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Templates the statesync section from config: enable, retries, chunk-request-timeout and fetchers. use-p2p is hardcoded to true because the RPC state provider needs two reachable RPC servers while dashmate publishes the Tenderdash RPC on loopback only, unproxied and without TLS. Drops the trust-height/trust-hash/trust-period keys removed in Tenderdash 1.7. Routes ListSnapshots and LoadSnapshotChunk to the drive gRPC app alongside CheckTx and bounds their concurrency; OfferSnapshot and ApplySnapshotChunk stay on the consensus socket. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maps the state sync snapshot config to the SNAPSHOTS_ENABLED, SNAPSHOTS_FREQUENCY_SECONDS and MAX_NUM_SNAPSHOTS envs drive-abci consumes. Checkpoints are written to the default CHECKPOINTS_PATH under DB_PATH, which is already inside the drive_abci_data volume, so no new volume is needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keyed at 4.2.0-dev.6, above the 4.2.0-dev.5 the package is at, so the runner picks it up and dev-build stamped configs cross it. Options are pulled from the default config matching each config's name or group, which gives the local preset its disables and everything else the base defaults. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tree Persists the reduced platform state under Misc/reduced_saved_state inside the replicated grovedb state (unlike the full platform state, which lives in non-replicated aux storage). fetch returns Ok(None) when the key is absent, so callers can distinguish pre-activation snapshots. Adds the DriveError::Snapshot variant and the platform_state method version fields for the new methods. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When drive snapshots are enabled, the doctor adds a conservative 10GB to the required free disk space and says so in the problem message. Checkpoints hard-link unchanged data, so a small fixed headroom is enough. Configs collected by an older dashmate have no state sync options and skip the headroom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a State Sync section to the tenderdash config doc (consume side, P2P-only rationale, self-disable semantics) and a State Sync Snapshots section to the drive-abci doc (serve side, checkpoint location and cost). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces for-of loops over test fixtures with forEach to satisfy no-loop-func, and drops an unused catch binding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bing Adds PLATFORM_V15 (drive-abci method versions v11: run_block_proposal 1, consensus_params_update 2), the DriveAbciStateSyncVersions substructure carrying the grovedb state sync wire protocol version on every platform version, and the reduced-platform-state storage method version slots on DriveAbciPlatformStateStorageMethodVersions. Pure plumbing: no behavior changes outside version selection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… before root hash v1 (gated on drive-abci method versions v11 / protocol v15) is a copy of v0 with validator_set_update moved above the root-hash computation and the reduced platform state written into the replicated state immediately before the root hash, so the stored state carries the post-rotation next validator set and is covered by the block's app hash. Adds the store/fetch_reduced_platform_state execution wrappers and the PlatformState::to_reduced_platform_state conversion (fee versions persisted faithfully by number). A test proves rotation outcomes are unchanged by the reorder: validator_set_update only mutates in-memory block state and reads neither the app hash nor grovedb. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to v15 transition_to_version_15 stores the reduced platform state built from the last committed platform state under Misc/reduced_saved_state during the v15 activation block, so the key exists in the replicated state from the fork block onward and every snapshot taken at or after activation is restorable. run_block_proposal v1 overwrites it later in the same block with the state of the block being processed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on pattern Judges disk problem severity against the base minimum so enabling snapshots widens when a problem is raised but never downgrades a HIGH shortage to MEDIUM. Accepts fractional minute and hour chunk request timeouts down to 0.1 (all at least 6s, above the 5s Tenderdash floor). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stry Adds StateSyncAbciConfig (env contract: SNAPSHOTS_ENABLED, SNAPSHOTS_FREQUENCY_SECONDS, MAX_NUM_SNAPSHOTS, CHECKPOINTS_PATH) which, when enabled, overrides the platform-version-driven checkpoint frequency, retention and directory. list_snapshots and load_snapshot_chunk (on both the tenderdash socket app and the gRPC CheckTx app) serve snapshots directly from drive.checkpoints: only checkpoints containing the reduced platform state are offered (pre-v15 checkpoints are unrestorable), requested wire versions are validated against a single supported-set const, chunk ids are size-capped before decoding (dashpay#3773), and served checkpoints are pinned via the existing Arc refcount so pruning cannot delete them mid-transfer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…unk handlers Adds the StateSyncApplication trait and a snapshot fetching session (grovedb sync session plus the wire version taken from the offered snapshot) on the Consensus and Full ABCI apps. offer_snapshot validates the offered version against the single supported-set const (REJECT_FORMAT otherwise), wipes grovedb, and answers Accept on both the fresh-session and the replace-with-newer-height paths. apply_snapshot_chunk caps chunk and chunk-id sizes before any decode (dashpay#3773), answers RETRY with the failed chunk in refetch_chunks (banning the sender) instead of killing the session when grovedb rejects a chunk, and on completion commits the session, verifies grovedb, reconstructs the platform state (stub until the next commit) and checks the restored root hash against the snapshot app hash. The completion log fires once per transfer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fter snapshot restore reconstruct_platform_state reads the reduced platform state out of the restored grovedb, restores scalar fields and fee versions faithfully by version number, re-derives masternode lists, identities and quorums from Core via update_core_info with start_from_scratch=true (idempotent re-derivation, proven by the caller's root-hash equality check), restores the recorded validator set order, and advances the state to the snapshot block via update_state_cache so the info handler reports the snapshot height and app hash across restarts. update_core_info now passes is_init_chain through to update_quorum_info (its only effect is skipping the same-core-height short-circuit, required for init chain and reconstruction; the normal block path is unchanged), and update_masternode_list's early return is likewise guarded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… v15 activation The first block of protocol v15 additionally emits EvidenceParams (max age 15000 blocks / 20 days, max bytes 1 MiB) per issue dashpay#2512, in named constants. A review-flag comment notes that 15000 blocks (~1 day at 6s blocks) vs 20 days look inconsistent, since evidence expires at the earlier bound, and must be confirmed before release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A source chain runs past several checkpoints via the strategy harness with snapshot serving enabled, and a fresh target restores its newest snapshot through the real offer/load/apply chunk loop (modeled on grovedb's run_sync driver) with mocked Core RPC. Findings baked into the tests: grovedb wire v1 at the pinned rev cannot faithfully restore sum trees (root hash reproduces but recomputation diverges - latent corruption that the strict post-restore verify_grovedb correctly refuses), pinned by a minimal tripwire reproducer plus an active test asserting the refusal; the full happy-path test is ignored until the grovedb pin gains the fixed wire version. The reconstruction path itself is fully validated by an active test running it against the source's own grove: it is byte-idempotent (root hash unchanged by the masternode identity re-derivation) and reproduces the complete platform state including validator set order, masternode lists and fee versions, satisfying the info handler. A tampered chunk yields RETRY with a refetch and sender ban; since grovedb drops a chunk id from its pending set before processing, a refetch it can no longer honor yields RETRY_SNAPSHOT, and offer_snapshot now accepts same-height re-offers so Tenderdash snapshot restarts work. Pre-v15 snapshots are not offered and cannot be restored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…config) into feat/state-sync-e2e
…constructed state Review follow-up: reconstruct_platform_state now commits the update_core_info re-derivation before update_state_cache publishes the in-memory state, so a commit failure propagates without the info handler ever reporting a snapshot height grovedb never persisted. Aux writes (not part of the root hash) commit in their own transaction afterwards. Also documents that the RetrySnapshot string-match fallback is safe if grovedb's error wording changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a DI-registered setupLocalJoinNodeTask that creates a platform-enabled full node config (no masternode registration) from a running local group's config file: next port offset, fresh Tenderdash node key, Core sync through the group seed, group sporks, and stateSync.enabled=true so the fresh node bootstraps from a snapshot. Tenderdash mesh wiring (chain id, persistent peers, validator quorum type) is extracted from configureTenderdashTask into a shared wireLocalTenderdashNode helper rather than duplicated. Exposed as a task instead of a group join CLI command to keep new surface minimal for its only consumer, the state sync e2e test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings up a three validator local network with snapshot serving enabled at the minimum 60s frequency, waits for a Drive checkpoint beyond genesis, then creates and starts a state-sync-enabled join node and asserts its Tenderdash reaches catching_up=false with earliest_block_height > 1 (proof it restored a snapshot instead of replaying), that all its services run, and that DAPI serves a system contract from the restored state. No initial protocol version plumbing is needed: local networks put no app_version into the Tenderdash genesis, so drive-abci starts the chain at PlatformVersion::desired() (latest, >= v15) and snapshots are restorable from genesis on. Documented in the spec header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-ups: clear errors when the group lacks a seed or platform-enabled configs, peers advertised at their own external IP, hardcoded local_join name instead of a single-value ctx knob, and stronger unit assertions plus error-path coverage. The e2e now also moves every remaining host-published port (core zmq, drive metrics/tokio/grovedb, tenderdash metrics, gateway metrics/admin, rate limiter metrics, quorum list API) off the defaults so it can run beside another local network; the first live run failed on the seed's zmq port already being bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The local join task hardcoded the config name and derived its host port offset from the group size, so a second joiner would overwrite the first one's config and collide on every published port. Take both as options with the previous values as defaults, letting a test stand up several joiners against one network. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…or e2e A state sync test against a chain of empty blocks proves little: the restored state is almost entirely defaults, so a joiner can look healthy having restored nothing. These helpers let an e2e spec put real state on a local network and then re-read it from one specific node. Seeding goes through js-dash-sdk against a wallet funded by dashmate's own wallet mint task, so the suite's isolated home dir and per-run ports are honoured. Verification goes through the WASM SDK's proved paths, which check a GroveDB proof and the quorum signature over the root hash rather than trusting the response. Every address is derived from the dashmate Config of the node being addressed, which is what lets one client target the validators and another the joiner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…state sync e2e The spec proved a joiner bootstraps from a snapshot, but against a chain of empty blocks and without ever asking the joiner what state it restored. It now seeds identities, a DPNS name, two data contracts (one carrying a rankedCountable index, whose ordered secondary trees exist only from protocol v14) and documents before the snapshot, then re-reads all of it from the joined node with proofs. Three scenarios are added around that: a second joiner whose serving validator is restarted mid-sync, a joined node whose platform data is wiped and must sync again, and a joiner pointed at a network with snapshot serving disabled, which must fall back to block sync and replay from genesis rather than hang. State sync counters are polled from both Tenderdash RPC and DAPI getStatus during each join and recorded in a run report, along with a log excerpt of the sync lifecycle. They are recorded rather than asserted because a sync that finishes between two polls legitimately leaves no observation. The scenarios share one network deliberately: each bring-up costs many minutes and the later ones only need a config change. They must run in order, since the fallback scenario disables snapshot serving for good. Subnet and port ranges are now overridable so a run can avoid an orphaned docker network from an earlier one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ision No behaviour change — this only makes an existing landmine visible. FEE_VERSION2, which protocol versions 9 and later actually run with, declares fee_version_number 1, the same number FEE_VERSION1 declares, and is absent from FEE_VERSIONS. FeeVersion::get resolves numbers through that list, so FeeVersion::get(1) can only ever return FEE_VERSION1 — never FEE_VERSION2 — even though the two differ in data_contract_registration. That makes every number-only round trip of a fee version silently lossy, and there are two: PlatformStateForSavingV1 stores previous_fee_versions as (epoch index -> number), so a node that RESTARTS rehydrates previous epochs' fees as FEE_VERSION1; ReducedPlatformStateV0 does the same, so a node that STATE-SYNCS gets the substitution without even restarting. It is latent rather than a live fork only because previous_fee_versions is consulted solely to price storage refunds and the two constants have identical storage fees. It becomes a consensus fork the moment a future FeeVersion changes a storage or processing fee without taking a distinct number. Documents the rule — every FeeVersion constant must have a unique fee_version_number and be listed in FEE_VERSIONS at the index its number implies — and adds fee_version_numbers_are_unique_and_resolvable to enforce it. The test is #[ignore]d because it fails today; running it with --ignored reproduces the defect. Un-ignore it as part of giving FEE_VERSION2 its own number, which is protocol-visible and needs a migration rather than an in-place edit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng on sentinel cleanup *** THIS COMMIT CHANGES FEATURE CODE, NOT TESTS — please review it on its own. *** Three findings from an independent review of the preceding fix. 1. The wipe did not clear drive.checkpoints. That registry is populated by Drive::open and is what list_snapshots serves to peers — it is not a value cache that merely goes stale. Left in place across a wipe, a node that discarded a chain kept advertising snapshots of it, so a peer could state-sync from state this node no longer had. Now cleared, with the entries marked for deletion first so their directories are removed rather than leaking on disk. Regression test: a_wiped_node_stops_serving_snapshots_of_the_discarded_chain. 2. Clearing the sentinel at the two points where the node is ALREADY self-consistent — the end of a completed restore, and the end of init_chain — propagated I/O errors, so a failed remove_file turned a fully successful restore or a working genesis into a hard ABCI error. Now best-effort with a loud error log: the cost of not removing it is one unnecessary wipe-and-resync on a later restart, which is bounded and safe, unlike failing the operation. 3. commit_session's own failure still returned an ABCI exception rather than going through the recovery path. grovedb only makes the session durable once its internal root-hash check passes, so nothing is committed on that error — but the database is still WIPED from the offer, so the node must not be left as it is, and an exception stalls Tenderdash's snapshot ladder where REJECT_SNAPSHOT keeps it moving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maintainer ruling: the original state sync never shipped, so grovedb updates its replication protocol in place and stays at version 1 - there is no v2. The supported-set constant and the offered-snapshot validation remain so any future incompatible protocol change fails fast on both sides; comments now say exactly that instead of describing a version bump that will not happen. No behavior changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A joining full node could never state sync. Its Core reached the network tip and finished masternode sync, but reported no ChainLock, so drive-abci sat in wait_for_core_to_sync forever, Tenderdash never completed its ABCI handshake, and the snapshot offer was never made. The cause is spork propagation, not state sync: local setup activates SPORK_19_CHAINLOCKS_ENABLED once on the seed while every node of the group is already connected, and a Core that finishes its masternode sync afterwards does not go back for it. On the joiner SPORK_19 stayed at its far-future default, so it treated ChainLocks as disabled and ignored the CLSIGs the rest of the network was enforcing. Confirmed live: pushing SPORK_19 to a stuck joiner produced a chain lock within two blocks and drive-abci started immediately. The joiner's config already carries the group's spork key, so it can sign the same sporks setup applies. Also extracts getRunningCoreService, which both this and the minting path need. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…allback The suite joined the new node as soon as any checkpoint above genesis existed, which on a freshly started network meant a snapshot at height 2 on a four-block chain. Tenderdash verifies the light block at the snapshot height before accepting an offer and has nothing to verify against that early, so it abandoned discovery and block synced — the suite then failed on earliest_block_height without saying why. It now waits for the chain to run ten blocks past the newest checkpoint, and when a joiner block syncs anyway it records both sides: the joiner's logs and container states, the serving validator's snapshot lines, and the checkpoints that existed at that moment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he real re-pin Every grovedb crate is pinned by rev to dashpay/grovedb 6c882c3, which carries a restore defect for sum trees: applying a state sync snapshot chunk fails GroveDB verification, so a joining node can never complete a state sync. Confirmed live on this branch before the patch — apply_snapshot_chunk grovedb verification failed with 2 incorrect hashes, first paths: [60, 50/05]. dashpay/grovedb#840 (feat/state-sync-v2 on the PastaPastaPasta fork, head 10a63e1) fixes it. Its restore wire version is 1, inside drive-abci's supported set, so nothing about protocol compatibility moves with it. A workspace [patch] silently overrides every manifest's rev and a branch reference is not reproducible, so this must be removed once the fork is merged and the crates are re-pinned by rev. It exists only so the state sync e2e can be exercised against the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k_height The suite called a joiner "block synced" whenever earliest_block_height was 1, which is not what that field means. After a successful restore Tenderdash backfills light blocks backwards from the snapshot height to fill its evidence window, and on a chain a few dozen blocks long that backfill reaches genesis. A node that provably restored a snapshot therefore reports 1, exactly like one that replayed every block. Observed live: drive-abci logged state_sync completed height=28 while Tenderdash logged backfill down to height 4 and below, and the suite failed the run as a block sync. The restore is now read from drive-abci's own log, which block execution cannot produce. The join and repeatability scenarios assert a restore happened above genesis, the fallback scenario asserts none happened at all, and earliest_block_height is recorded with the backfill caveat rather than asserted on. The parser strips the ANSI colouring tracing puts between the field name and its value, verified against the captured log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The restore check read a container's whole log, so the repeatability scenario — wipe the joined node's platform data and make it sync again — would have been satisfied by the first join's restore line. The wipe does not necessarily replace the container, and the helper takes the last match, so a node that silently failed to re-sync would still have passed on the earlier line. That is the one regression the scenario exists to catch. Each start now records a boot time and the check reads only from there, via a new since option on DockerCompose#logs that forwards docker compose's own --since. A failure to read the logs throws instead of reporting "no restore", so a docker hiccup cannot masquerade as proof for the fallback scenario, which asserts on absence. The height is also matched anywhere on the line rather than immediately after the message, so adding a tracing field cannot quietly turn a restore into a non-restore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getStatus was read as getStateSync()/getChain(), which do not exist — the response object exposes getStateSyncStatus() and getChainStatus(), and the chunk-count getters are getSnapshotChunkCount/getBackfilledBlockTotal. Every mid-sync poll therefore failed with "response.getStateSync is not a function" and the run recorded zero DAPI observations, which looked like an unreachable node but was a wrong accessor. DAPI is reachable: the response arrived, only the read of it was wrong. Readiness no longer goes through dashmate's waitForNodeToBeReadyTask either. That task hardcodes no-ssl while the local preset obtains a self-signed certificate and saveCertificateTask turns platform.gateway.ssl.enabled on, so a plain HTTP request to the gateway can never succeed — and it retries forever with no deadline, which hangs the suite rather than failing it. Readiness is now a bounded poll over the same TLS address the rest of the suite uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The regtest localhost workaround rewrote EVERY live address to 127.0.0.1:2443+i*100 (the stock local gateway ports), including addresses the caller configured explicitly. A local network that moves its ports (the dashmate e2e suites do, to run next to other networks) had every request silently redirected to whatever squats the stock ports on the machine - on a shared dev box, a completely different network. Only rewrite addresses that carry a non-loopback (docker-internal) host, which is the case the workaround exists for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xt prefetch Discovery only feeds the no-explicit-addresses path of withTrustedContext, but a failure made the whole prefetch unusable - and it fails routinely on local networks, where the quorum sidecar's per-masternode version checks reject the gateway's self-signed TLS and report no eligible masternodes. Degrade to a warning and an empty discovered list; SDKs constructed with explicit addresses are unaffected, and the quorum data proof verification needs is fetched before this point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
InitChain passes PlatformVersion::first() as the original version so Tenderdash learns the real app version, but that fiction also makes a chain STARTING on protocol v15+ look like it just crossed to v15 - and consensus_params_update_v2 then emits the 15000-block evidence window meant for chains upgrading with pre-state-sync genesis documents (dashpay#2512), silently overriding the evidence params of the genesis document being initialized. At genesis the operator's genesis document is authoritative; strip the evidence section from the InitChain update so it stays in force. Mid-chain crossings to v15 keep the override. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…roken step Funding: after sendToAddress, assert the transaction entered the Core mempool and verify it confirmed on chain before waiting on the wallet, so a broadcast or mining failure surfaces immediately instead of minutes later as a generic wallet-sync timeout that points every investigation at DAPI. The final timeout message now states the chain-side facts. Proof verifier: raise the per-request deadline to span the 30s local block interval (never set waitTimeoutMs - it routes through tokio::time::timeout, whose std::time::Instant is unimplemented on wasm32 and panics the module), raise the js-dash-sdk client deadline for CheckTx-heavy broadcasts, and retry the EvoSDK connect instead of caching one warm-up rejection forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stock 100000-block evidence window makes Tenderdash backfill light blocks from the snapshot to genesis on any local-sized chain, so earliest_block_height always landed at 1 and the user-visible property of state sync - a truncated history starting at the backfill floor - was never exercised. Shrink the window in the network genesis (10 blocks, 10s), have join nodes inherit the group genesis wholesale so their Tenderdash agrees about the chain, and assert on the first join that earliest_block_height sits above genesis at snapshot height minus the window (observed live: restored at 32, earliest 22), with the block-synced fallback joiner asserting the contrast (full history from genesis). The drive-abci restore log stays as the per-boot proof of the restore itself. The checkpoint gate now demands tip headroom above the oldest acceptable checkpoint rather than the newest: snapshots appear every other block here and drive-abci prunes old checkpoints, so the tip can never outrun the newest by the old margin. Also adds a keep-network knob (DASHMATE_E2E_STATE_SYNC_KEEP_NETWORK) for post-mortems, since teardown otherwise destroys the only diagnosable evidence, plus serving-side log capture when seeding produces nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eight precondition Review follow-up: a masternode discovery failure stays fatal on public networks, where it signals a genuine outage of the trusted endpoint, and is only degraded to a warning on regtest where the sidecar's version checks rejecting self-signed TLS is the normal case. The truncation scenario also asserts the restored snapshot height meets the minimum its bounds math assumes, so a Tenderdash fallback to an older snapshot fails by name instead of as a baffling bounds mismatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Update: the suite now proves the truncated-history property for real, and seeding is fully green (commits through
🤖 Posted autonomously by Claude on behalf of pasta. |
|
⛔ Final review complete — 2 blocking finding(s) (commit 380adfd) |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Sol-only technical fallback
The state-sync harness adds valuable live coverage, but this exact head still has four blocking issues: a temporary mutable GroveDB override, regression tests inverted against that override, restored state published before final acceptance, and checkpoint paths that cannot be reloaded after restart. Two e2e assertions should also be tightened so the churn scenario proves state sync and the seeding wallet avoids scanning irrelevant history. Source: claude-opus-4-6 (general and security reviewers), gpt-5.3-codex (review checkpoint), claude-sonnet-4-6 (final verifier).
One or more required Phase-1 GLM Flash lanes remained technically unusable after the bounded exact-model retry. Their evidence was discarded as authoritative, and the complete selected role cohort was rerun fresh on exact gpt-5.6-sol before this fresh Sol verifier produced the final decision. No additional Phase-2 reviewer pass ran.
Review provenance
- Phase 1 GLM evidence: technically unusable after bounded retry; discarded from the decision
- GLM failure attempts:
codex-general-3dd75fc719c644d9b944136ac4309648(failed),codex-general-ea10c050eb4442f09629979d7552c970(failed),codex-security-auditor-c6987bff19974f9ba7a8d2318a1f4b79(failed),codex-security-auditor-8870d871774a4236a799867fbaba279b(failed) - Sol-only fallback reasons:
launch_transport_or_nonzero_exit,launch_transport_or_nonzero_exit - Sol-only fallback reviewers:
gpt-5.6-sol— general (completed),gpt-5.6-sol— security-auditor (completed) - Fresh verifier (Sol):
gpt-5.6-sol— final-verifier - Additional Phase 2 pass: not run; the Sol-only fallback is final
🔴 4 blocking | 🟡 2 suggestion(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `Cargo.toml`:
- [BLOCKING] Cargo.toml:154-162: Replace the mutable GroveDB fork override before merge
The workspace patch replaces every manifest-pinned GroveDB crate with a branch on a contributor-owned fork. Cargo.lock currently records commit 10a63e1e2dab39b6cc59af8253dc42e187f071c6, but lockfile regeneration or an update follows the branch head, making consensus-critical storage and proof code mutable and dependent on a personal fork. The PR description and in-file comment explicitly identify this override as temporary and release-blocking. Remove the patch and update the package pins and lockfile to the immutable reviewed revision in dashpay/grovedb after the fix merges.
In `packages/rs-drive-abci/tests/sum_tree_sync_probe.rs`:
- [BLOCKING] packages/rs-drive-abci/tests/sum_tree_sync_probe.rs:110-124: Remove the inverted GroveDB defect tripwires after applying the fix
This active test requires verify_grovedb to report the restored sum tree as corrupt, while the workspace override resolves to the GroveDB change intended to make that restore verify cleanly. The same stale assumption remains in state_sync_transfer_detects_sum_tree_restore_defect, and the successful run_state_sync_between_two_platforms test remains ignored. With the fixed dependency, the two defect tests fail precisely when restoration works. Remove the obsolete tripwires and enable the successful two-platform restore test as part of the immutable dependency update.
In `packages/rs-drive-abci/src/execution/platform_events/state_sync/reconstruct_platform_state/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/execution/platform_events/state_sync/reconstruct_platform_state/mod.rs:188-203: Publish restored platform state only after the snapshot is accepted
update_state_cache_v0 writes the new PlatformState into self.state before the auxiliary transaction is committed. An auxiliary commit failure therefore publishes the snapshot height and app hash before apply_snapshot_chunk rejects the restore and wipes GroveDB. The caller also performs its final root-hash equality check only after this method returns, so a root mismatch follows the same path. The process is then left with snapshot-height in-memory state over an empty database; Info or a block-sync fallback can observe or execute against that mismatch. Commit the persisted state and complete the final root verification before publishing it, or reset the in-memory state whenever reject_restored_snapshot wipes the restore.
In `packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs:45-49: Reload checkpoints from the configured checkpoint path
Checkpoint creation honors StateSyncAbciConfig.checkpoints_path, but Drive::open still calls load_current_checkpoints(db_path), which scans only <db_path>/checkpoints. Platform::open_with_client likewise reads platform_state.bin from the hardcoded default directory. After a restart with CHECKPOINTS_PATH set elsewhere, the checkpoint registry is empty: existing snapshots are no longer listed or pruned, their platform states are not loaded, and their directories accumulate. Pass the resolved checkpoint directory through Drive and Platform startup, or remove the unsupported path option.
In `packages/dashmate/test/e2e/localNetworkStateSync.spec.js`:
- [SUGGESTION] packages/dashmate/test/e2e/localNetworkStateSync.spec.js:862-869: Require the churn scenario to interrupt an active state sync
When waitForStateSyncActivity returns undefined, the test records that the run is inconclusive but continues. It later records rather than asserts churnRestoreHeight, so the scenario passes if the joiner had already completed or falls back to block sync. That does not prove the behavior named by the test or claimed by the PR. Require a positive mid-restore observation before restarting validators and require a restore height after synchronization; retry or skip the scenario explicitly if the transfer finishes too quickly to observe.
- [SUGGESTION] packages/dashmate/test/e2e/localNetworkStateSync.spec.js:576-582: Pass the computed Core height to the seeding wallet
The test obtains coreHeight specifically to avoid scanning blocks that predate the new wallet, but createClient is called without skipSyncBeforeHeight. createClient only sets skipSynchronizationBeforeHeight when that option is supplied, so the fresh wallet scans thousands of irrelevant local-network setup blocks and can consume the funding timeout. Pass the already-computed height as documented by the helper.
| [patch."https://github.com/dashpay/grovedb"] | ||
| grovedb = { git = "https://github.com/PastaPastaPasta/grovedb", branch = "feat/state-sync-v2" } | ||
| grovedb-costs = { git = "https://github.com/PastaPastaPasta/grovedb", branch = "feat/state-sync-v2" } | ||
| grovedb-path = { git = "https://github.com/PastaPastaPasta/grovedb", branch = "feat/state-sync-v2" } | ||
| grovedb-storage = { git = "https://github.com/PastaPastaPasta/grovedb", branch = "feat/state-sync-v2" } | ||
| grovedb-version = { git = "https://github.com/PastaPastaPasta/grovedb", branch = "feat/state-sync-v2" } | ||
| grovedb-epoch-based-storage-flags = { git = "https://github.com/PastaPastaPasta/grovedb", branch = "feat/state-sync-v2" } | ||
| grovedb-query = { git = "https://github.com/PastaPastaPasta/grovedb", branch = "feat/state-sync-v2" } | ||
| grovedb-commitment-tree = { git = "https://github.com/PastaPastaPasta/grovedb", branch = "feat/state-sync-v2" } |
There was a problem hiding this comment.
🔴 Blocking: Replace the mutable GroveDB fork override before merge
The workspace patch replaces every manifest-pinned GroveDB crate with a branch on a contributor-owned fork. Cargo.lock currently records commit 10a63e1e2dab39b6cc59af8253dc42e187f071c6, but lockfile regeneration or an update follows the branch head, making consensus-critical storage and proof code mutable and dependent on a personal fork. The PR description and in-file comment explicitly identify this override as temporary and release-blocking. Remove the patch and update the package pins and lockfile to the immutable reviewed revision in dashpay/grovedb after the fix merges.
source: ['claude']
| // ...but recomputing the restored sum tree exposes the latent corruption. | ||
| let issues = target | ||
| .verify_grovedb(None, true, false, grove_version) | ||
| .unwrap(); | ||
| let paths: Vec<String> = issues | ||
| .keys() | ||
| .map(|path| path.iter().map(hex::encode).collect::<Vec<_>>().join("/")) | ||
| .collect(); | ||
| assert_eq!( | ||
| paths, | ||
| vec!["73".to_string()], // hex of b"s", the sum tree | ||
| "expected exactly the sum tree to fail verification — if no issues are \ | ||
| reported, grovedb has been fixed: delete this tripwire and un-ignore \ | ||
| run_state_sync_between_two_platforms" | ||
| ); |
There was a problem hiding this comment.
🔴 Blocking: Remove the inverted GroveDB defect tripwires after applying the fix
This active test requires verify_grovedb to report the restored sum tree as corrupt, while the workspace override resolves to the GroveDB change intended to make that restore verify cleanly. The same stale assumption remains in state_sync_transfer_detects_sum_tree_restore_defect, and the successful run_state_sync_between_two_platforms test remains ignored. With the fixed dependency, the two defect tests fail precisely when restoration works. Remove the obsolete tripwires and enable the successful two-platform restore test as part of the immutable dependency update.
source: ['claude']
There was a problem hiding this comment.
Resolved in 7a815ec — Remove the inverted GroveDB defect tripwires after applying the fix no longer present.
Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.
| self.update_state_cache( | ||
| current_block_info, | ||
| platform_state, | ||
| &aux_transaction, | ||
| state_platform_version, | ||
| )?; | ||
| self.drive | ||
| .grove | ||
| .commit_transaction(aux_transaction) | ||
| .unwrap() | ||
| .map_err(|e| { | ||
| AbciError::StateSyncInternalError(format!( | ||
| "reconstruct_platform_state unable to commit aux transaction: {}", | ||
| e | ||
| )) | ||
| })?; |
There was a problem hiding this comment.
🔴 Blocking: Publish restored platform state only after the snapshot is accepted
update_state_cache_v0 writes the new PlatformState into self.state before the auxiliary transaction is committed. An auxiliary commit failure therefore publishes the snapshot height and app hash before apply_snapshot_chunk rejects the restore and wipes GroveDB. The caller also performs its final root-hash equality check only after this method returns, so a root mismatch follows the same path. The process is then left with snapshot-height in-memory state over an empty database; Info or a block-sync fallback can observe or execute against that mismatch. Commit the persisted state and complete the final root verification before publishing it, or reset the in-memory state whenever reject_restored_snapshot wipes the restore.
source: ['claude']
There was a problem hiding this comment.
Resolved in this update — Publish restored platform state only after the snapshot is accepted no longer present.
Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.
| // Build the checkpoint path: <checkpoints_path>/<block_height> | ||
| // (defaults to db_path/checkpoints) | ||
| let checkpoint_path = state_sync_config | ||
| .resolved_checkpoints_path(&self.config.db_path) | ||
| .join(block_height.to_string()); |
There was a problem hiding this comment.
🔴 Blocking: Reload checkpoints from the configured checkpoint path
Checkpoint creation honors StateSyncAbciConfig.checkpoints_path, but Drive::open still calls load_current_checkpoints(db_path), which scans only <db_path>/checkpoints. Platform::open_with_client likewise reads platform_state.bin from the hardcoded default directory. After a restart with CHECKPOINTS_PATH set elsewhere, the checkpoint registry is empty: existing snapshots are no longer listed or pruned, their platform states are not loaded, and their directories accumulate. Pass the resolved checkpoint directory through Drive and Platform startup, or remove the unsupported path option.
source: ['claude']
There was a problem hiding this comment.
Resolved in this update — Reload checkpoints from the configured checkpoint path no longer present.
Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.
| const activity = await waitForStateSyncActivity(churnConfig); | ||
|
|
||
| if (activity) { | ||
| record(`churn joiner is mid-restore: ${JSON.stringify(activity)}`); | ||
| } else { | ||
| record('INCONCLUSIVE: the churn joiner never reported an in-progress restore,' | ||
| + ' so the restart below did not interrupt a chunk transfer'); | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: Require the churn scenario to interrupt an active state sync
When waitForStateSyncActivity returns undefined, the test records that the run is inconclusive but continues. It later records rather than asserts churnRestoreHeight, so the scenario passes if the joiner had already completed or falls back to block sync. That does not prove the behavior named by the test or claimed by the PR. Require a positive mid-restore observation before restarting validators and require a restore height after synchronization; retry or skip the scenario explicitly if the transfer finishes too quickly to observe.
source: ['claude']
| // Start the wallet's transaction scan at the tip: everything below it | ||
| // predates the key and only costs time to walk. | ||
| const coreHeight = await getCoreHeight(coreService); | ||
|
|
||
| record(`core height before funding: ${coreHeight}`); | ||
|
|
||
| const client = createClient(validatorConfig, seedConfig); |
There was a problem hiding this comment.
🟡 Suggestion: Pass the computed Core height to the seeding wallet
The test obtains coreHeight specifically to avoid scanning blocks that predate the new wallet, but createClient is called without skipSyncBeforeHeight. createClient only sets skipSynchronizationBeforeHeight when that option is supplied, so the fresh wallet scans thousands of irrelevant local-network setup blocks and can consume the funding timeout. Pass the already-computed height as documented by the helper.
| // Start the wallet's transaction scan at the tip: everything below it | |
| // predates the key and only costs time to walk. | |
| const coreHeight = await getCoreHeight(coreService); | |
| record(`core height before funding: ${coreHeight}`); | |
| const client = createClient(validatorConfig, seedConfig); | |
| const client = createClient(validatorConfig, seedConfig, { | |
| skipSyncBeforeHeight: coreHeight, | |
| }); |
source: ['claude']
There was a problem hiding this comment.
Resolved in 380adfd — Pass the computed Core height to the seeding wallet no longer present.
Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.
With the temporary root Cargo.toml patch to dashpay/grovedb#840 in place, the two tripwires that assert the sum-tree restore DEFECT is present fail by design (verify_grovedb reports zero issues), and the full two-platform state sync test they gate can run. Ignore the two defect-present pins with QA-branch markers and un-ignore run_state_sync_between_two_platforms so the suite runs green against the patched dependency. Every change here is reverted by the same commit that drops the patch section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ight The seed-state scenario computes the pre-funding core height precisely so the fresh wallet does not walk the local network's setup blocks, but never passed it to createClient, so skipSynchronizationBeforeHeight was never set and the wallet scanned the whole chain against the funding timeout. Pass the height through, as the helper documents. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Response to the automated review (findings at
Dashmate unit suite 841 passing; no production code changed, so no live e2e rerun was warranted. 🤖 Posted autonomously by Claude on behalf of pasta. |
|
Thanks for the detailed reconciliation. I’ve queued an exact-head automated follow-up review for |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Sol-only technical fallback
The latest commits fix the prior GroveDB tripwire and wallet scan-height findings, but the PR still has two in-scope blockers: the acknowledged mutable GroveDB branch override and a 30-minute CI job limit that leaves no room for the reported 29-minute state-sync suite plus setup. The churn scenario also remains weaker than its stated coverage, while two technically valid Drive-ABCI defects are intentionally deferred to the explicitly stacked #4520 review scope.
Source: reviewer 1: gpt-5.6-sol (agent: sol-fallback-reviewer, role: general); final verifier: gpt-5.6-sol (agent: sol-verifier, role: final-verifier)
One or more required Phase-1 GLM Flash lanes remained technically unusable after the bounded exact-model retry. Their evidence was discarded as authoritative, and the complete selected role cohort was rerun fresh on exact gpt-5.6-sol before this fresh Sol verifier produced the final decision. No additional Phase-2 reviewer pass ran.
Review provenance
- Phase 1 GLM evidence: technically unusable after bounded retry; discarded from the decision
- GLM failure attempts:
codex-general-948023c279654fd79e799c5ff31bbaed(failed),codex-general-08b1af2ae8a4455aab4bfc421c387ab3(failed) - Sol-only fallback reasons:
launch_transport_or_nonzero_exit - Sol-only fallback reviewers:
gpt-5.6-sol— general (completed); agentsol-fallback-reviewer - Fresh verifier (Sol):
gpt-5.6-sol— final-verifier; agentsol-verifier - Additional Phase 2 pass: not run; the Sol-only fallback is final
🔴 1 blocking
2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `.github/workflows/tests.yml`:
- [BLOCKING] .github/workflows/tests.yml:533-535: Give the state-sync E2E job enough time to finish
This new matrix entry invokes `.github/workflows/tests-dashmate.yml`, where the entire reusable job has `timeout-minutes: 30`. The reported state-sync suite already takes approximately 29 minutes, while that job must also perform checkout, dependency setup, artifact downloads, image pulls, cache or volume restoration, and other preparation; the reusable workflow notes that cold image pulls alone can consume much of 15 minutes. The suite's 120-minute Mocha timeout cannot help because GitHub cancels the enclosing job first. Add a per-matrix timeout input, raise the reusable timeout, split the suite, or reduce its runtime enough to leave meaningful setup and execution margin.
In `Cargo.toml`:
- [BLOCKING] Cargo.toml:154-162: Replace the mutable GroveDB fork override before merge
(existing thread: https://github.com/dashpay/platform/pull/4530#discussion_r3890036724)
The workspace still overrides every manifest-pinned GroveDB crate with the moving `feat/state-sync-v2` branch on a contributor-owned fork. Although `Cargo.lock` currently resolves commit `10a63e1e2dab39b6cc59af8253dc42e187f071c6`, regenerating or updating the lockfile follows the branch head, so consensus-critical storage and proof behavior is not tied to an immutable reviewed revision. The surrounding comment and PR description both identify this as temporary and release-blocking. Remove the patch and update the normal package pins and lockfile to the immutable dashpay/grovedb revision containing the restore fix.
In `packages/dashmate/test/e2e/localNetworkStateSync.spec.js`:
- [SUGGESTION] packages/dashmate/test/e2e/localNetworkStateSync.spec.js:864-915: Require the churn scenario to interrupt an active state sync
(existing thread: https://github.com/dashpay/platform/pull/4530#discussion_r3890036735)
The scenario still continues when `waitForStateSyncActivity` returns no observation, explicitly records that result as inconclusive, and later accepts an undefined restore height as a block-sync fallback. It can therefore pass after restarting validators even if snapshot transfer already completed, never started, or was abandoned in favor of block sync. That proves eventual synchronization after validator restarts, but not the serving-side state-sync churn behavior claimed by the scenario. Require a positive mid-restore observation and a restore height, or explicitly retry or skip when transfer completes too quickly to observe.
| - name: Local network state sync | ||
| test-pattern: test/e2e/localNetworkStateSync.spec.js | ||
| restore_local_network_data: true |
There was a problem hiding this comment.
🔴 Blocking: Give the state-sync E2E job enough time to finish
This new matrix entry invokes .github/workflows/tests-dashmate.yml, where the entire reusable job has timeout-minutes: 30. The reported state-sync suite already takes approximately 29 minutes, while that job must also perform checkout, dependency setup, artifact downloads, image pulls, cache or volume restoration, and other preparation; the reusable workflow notes that cold image pulls alone can consume much of 15 minutes. The suite's 120-minute Mocha timeout cannot help because GitHub cancels the enclosing job first. Add a per-matrix timeout input, raise the reusable timeout, split the suite, or reduce its runtime enough to leave meaningful setup and execution margin.
source: ['claude']
|
Follow-up on the exact-head review:
The PR head therefore remains |
Issue being fixed or feature implemented
Live end-to-end test coverage for Platform state sync: a fresh node joins a running local network and bootstraps from a snapshot instead of replaying the chain — plus churn, re-sync, and fallback scenarios.
Stacked integration PR: this branch contains #4520 (drive-abci state sync) and #4521 (dashmate config) via merges, plus the e2e harness commits on top — review only the
test(dashmate)/feat(dashmate)commits here. It also carries a clearly-marked TEMPORARY workspace[patch]pointing grovedb at dashpay/grovedb#840 (1a005de6b2) so the suite runs against the fixed restore; that commit must be dropped and replaced by a normal rev pin bump once #840 merges (a branch reference is not reproducible).What was done?
mode: fullplatform node wired into a running group (chain_id, persistent-peer mesh, port offsets, fresh node key), withstateSync.enabled.localNetworkStateSync.spec.js: 15 scenarios — snapshot creation beyond genesis, the join via state sync, proof-verified state served by the joined node, dashmate health, sync-lifecycle log capture, serving-validator churn mid-join, re-sync after a platform-data wipe, block-sync fallback when no validator serves snapshots, and ops checks. CI matrix entry included.rankedCountablecontract, documents) — currently pending in the suite due to a defect in the harness's own Core funding step — a follow-up investigation proved the DAPI streaming path itself works correctly (subscriptions, BIP37 filtering with positive/negative controls, historical replay, and live ZMQ delivery all verified; the harness'ssendToAddresspayments never actually reached the chain). Verification falls back to proof-verifying the DPNS genesis contract out of the joined node, so the post-sync check is never vacuous.earliest_block_height > 1is NOT evidence of state sync — Tenderdash backfills light blocks to genesis on short chains; the suite proves the restore from drive-abci's own logs instead.SPORK_19_CHAINLOCKS_ENABLED, so it never sees a ChainLock and drive-abci waits forever — the join task now activates local sporks on the joiner. Without this, no state-sync e2e could ever have passed locally or in CI.no-sslagainst a TLS-enabled gateway and retried forever).How Has This Been Tested?
Full live run against images built from this branch (drive-abci with #4520 fixes + grovedb #840):
Highlights: joiner restored a snapshot at height 24 and served proof-verified state; a joiner completed while all three validators restarted mid-join; a platform-data wipe led to a fresh snapshot restore (height 31), not a replay; with snapshot serving disabled the joiner block-synced from genesis with no restore and still served state. No
info.rsapp-hash panic across four joiner boots — the #4520 sentinel fix holds live.Known ops gap surfaced: rs-dapi reported every
getStatusStateSync field as zero even on a freshly-restored node. Root-caused and addressed in #4532 (rs-dapi/js-dapi-client side); the underlying cause is that tenderdash never wires itsStateSyncMetricerinto the /status RPC, being fixed separately in tenderdash.Breaking Changes
None (test infrastructure + join tooling; the temporary grovedb patch is release-blocking by design until the re-pin).
Checklist:
🤖 Generated with Claude Code