Skip to content

fix(pool): make pool leave work and keep the Blox config in step with the chain - #251

Merged
ehsan6sha merged 6 commits into
mainfrom
fix/pool-leave
Oct 8, 2026
Merged

ehsan6sha merged 6 commits into
mainfrom
fix/pool-leave

Conversation

@ehsan6sha

Copy link
Copy Markdown
Member

Why

Leaving a pool never worked on the Blox: HandlePoolLeave gated its cleanup on an eth_call of removeMemberPeerId with a placeholder selector (0x12345678) that reverts on both SKALE and Base, so every leave answered 500 and poolName/chainName were never cleared. The device can't remove itself on-chain anyway — the app sends removeMemberPeerId from the member wallet (fxblox-web 0.0.3).

What changes

Leave handler (fula-pool-leave): clears pool + chain from config.yaml only after a strict on-chain check (RPC errors are errors) that the device's cluster peer is neither a member of nor waiting to join the pool, then restarts the fula services (existing .command_restart_fula → docker compose restart) so ipfs-cluster stops following the old pool.

Situation Answer
left on-chain 202, config cleared, services restarted
still member / pending (re-checked 3× for RPC lag) 409, config kept
configured for another pool or another chain 409, no RPC
nothing configured 202, no change, no restart
chain unreadable / no cluster peer id 503, config kept
config write failed 500, no restart
pool host (--poolHost) 409

Reconcile (background, after startup discovery, then every 6 h; never on pool hosts): records the pool the Blox is a member of, and later clears it only if the Blox was seen as a member by this firmware and the chain no longer lists it (e.g. it left from the app while offline). A configured pool never seen as a membership (e.g. a join whose on-chain part never landed) is left alone — so the update changes nothing for devices as they run today.

Safety rails: two definitive reads 30 s apart; config re-read before clearing; at most one reconcile clear (and restart) per 24 h, recorded before clearing (no record → no clear); restart only after the clear is verified; config.yaml accessors in cmd/blox serialised with a mutex; kill switch: while /internal/.disable_pool_reconcile exists (host: /home/pi/.internal/.disable_pool_reconcile) reconcile does nothing. Real selectors for removeMemberPeerId (0x3d71233a) and joinRequests (0xccc9fc03).

Checks done

  1. No loop: reconcile restarts are capped at one per 24 h by a marker written before the clear; no clear without the marker; no restart without a verified clear; after a clear the pool is 0 so neither path fires again. Nothing here reboots the host (only docker compose restart), so the 5-boots factory reset can't be reached; go-fula.sh's OTA md5 marker logic is untouched.
  2. Running devices: unaffected unless they were seen as a pool member by this firmware and then left. Legacy configs with empty chainName, unconfirmed pools, pool hosts and offline/slow networks are all left alone (errors keep the config).
  3. OTA pickup: no go.mod / config schema / startup-flag changes; blox, wap, initipfs, initipfscluster build for linux/arm64 with Go 1.25 (the image's toolchain). Rollout is the existing path: fula-ota release builds go-fula:release from go-fula main → Watchtower → go-fula.sh one OTA reboot. No fula-ota change needed (.command_restart_fula exists since fula-ota v3.1.6, OTA marker since v3.0.0).

Tests: new unit tests for the handler, reconcile, loop guards and ABI helpers pass with -race -shuffle=on; build-tagged livechain tests (go test -tags livechain -run TestLive ./blockchain/) pass read-only against the real SKALE and Base contracts.

CI note: unit and All are already red on main before this PR — TestRealContractIntegration and TestPoolDiscoveryWithMockServer always fail, TestBlockchainHealthCheck / TestFxBlockchainCreation flake (port collisions), and 6 files already fail gofmt. This PR adds no new failures (same results on main and the branch over 5 shuffled race runs; gofmt list unchanged).

Known limits: a Blox whose owner leaves on-chain before the Blox has ever run this firmware is never confirmed as a member, so it isn't auto-cleared (as today). The mobile app sends the Blox leave before its on-chain tx, so mobile users get a 409 (previously 500) until the app reorders.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QaKz9HeXaNBjrchbbGimFx

ehsan6sha and others added 6 commits October 5, 2026 01:19
HandlePoolLeave always answered 500 and never cleaned up: it gated the
cleanup on an eth_call of removeMemberPeerId with a placeholder
selector (0x12345678) that reverts on both chains, using the caller's
peer id. The device can't remove itself on-chain anyway — the app sends
removeMemberPeerId from the member wallet.

- HandlePoolLeave now checks the chain (strictly: RPC errors are errors)
  that the device's cluster peer is neither a member of nor waiting to
  join the pool, re-checking briefly for RPC lag, then clears pool and
  chain from the config and answers 202. 409 if still listed or the
  device is configured for another pool; 503 if the chain can't be
  read. Config is never cleared on a guess.
- ReconcilePoolConfig (run at startup and on the 6 h loop) clears a
  configured pool the chain says the Blox has left — e.g. it left from
  the app while offline — except within 24 h of a join from this device
  (join-time marker), since joins write the config before the chain
  lists the Blox.
- After the config is cleared, signal the existing fula restart marker
  so ipfs-cluster re-runs its init and stops following the old pool.
- Real selectors for removeMemberPeerId and joinRequests; tests for the
  handler, reconcile and ABI helpers, plus build-tagged (livechain)
  read-only tests against the real SKALE and Base contracts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QaKz9HeXaNBjrchbbGimFx
…gured

- A leave for pool N on chain B must not clear a config of pool N on
  chain A (pool ids repeat across chains; some Bloxes are in pool 1 on
  both SKALE and Base): answer 409 "Chain mismatch".
- With no pool configured (e.g. a reconcile already cleared it), answer
  202 straight away, without reading the chain or restarting services.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QaKz9HeXaNBjrchbbGimFx
- Run the startup reconcile in the background 2 min after start (off
  the startup path, after the network settles), and never on pool
  hosts (--poolHost), whose own pool is not a chain-listed membership.
- Only signal the fula services restart after the pool is confirmed
  gone from the config: if the config write failed, a restart would
  find the pool again and loop. Leave answers 500 in that case.
- Don't write Blox fields from the background reconcile; the restart
  reloads them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QaKz9HeXaNBjrchbbGimFx
- At most one automatic (reconcile) clear — and services restart — per
  24 h, recorded in /internal/.tmp/pool_reconcile_cleared_at.tmp before
  clearing; if that record can't be written, nothing is cleared.
- Act only on two definitive "not listed" answers 30 s apart, so a
  single lagging RPC node can't clear a member's pool.
- clearPoolConfig checks the config write errors itself instead of
  trusting a re-read that falls back to "0" when config.yaml can't be
  read.
- Don't write in-memory topic/chain names from the leave / reconcile
  goroutines (other request goroutines read them; the restart reloads
  them).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QaKz9HeXaNBjrchbbGimFx
So that the update changes nothing for devices as they run today:

- ReconcilePoolConfig only clears a configured pool this firmware has
  itself seen the Blox be a member of (recorded in
  /internal/.tmp/pool_member_confirmed.tmp as "<chain>:<pool>") and the
  chain now says it isn't. A configured pool never seen as a membership
  — e.g. a join whose on-chain part never landed — is left alone, as
  before. Replaces the 24 h join-time grace marker.
- Re-read the config just before clearing and abort if it changed (a
  join or rejoin landing during the 30 s check is not wiped).
- Start the reconcile goroutine only after startup pool discovery, and
  serialise the config.yaml accessors in cmd/blox with a mutex, so
  concurrent read-modify-writes can't mix fields.
- Kill switch: while /internal/.disable_pool_reconcile exists,
  reconcile does nothing.
- Pool hosts (--poolHost) refuse pool leave requests (409) and never
  reconcile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QaKz9HeXaNBjrchbbGimFx
@ehsan6sha
ehsan6sha merged commit 051bb4c into main Oct 8, 2026
3 of 9 checks passed
@ehsan6sha
ehsan6sha deleted the fix/pool-leave branch October 8, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant