Skip to content

Security release/candidate 1 - #4109

Open
lionakhnazarov wants to merge 247 commits into
threshold-network:mainfrom
lionakhnazarov:security-release/candidate-1
Open

Security release/candidate 1#4109
lionakhnazarov wants to merge 247 commits into
threshold-network:mainfrom
lionakhnazarov:security-release/candidate-1

Conversation

@lionakhnazarov

@lionakhnazarov lionakhnazarov commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Integration candidate for the coordinated security release (sr-candidate-1).

Merges:

Head: 971f60358 (rebased onto main @ 038b7ced1; geth/Go CVE bumps restored)
tss-lib pin: github.com/threshold-network/tss-lib@v0.0.0-20260615180949-86bd1a375cc0 (86bd1a3)
Deps: go-ethereum v1.17.3, keep-common v1.7.1-tlabs.1, go 1.25.7 / toolchain go1.25.10

Sepolia smoke on security-candidate-2 @ cbf31e0e8 (pre-geth-restore tip of refresh stack): PASS (SR-1, SR-2 groupSize=100, SR-4 live SPV proof, SR-5 rollback). Local short regression after geth restore: PASS. Details below.


⚠️ Coordinated upgrade required

All operators in a DKG or signing ceremony must run the same build. Mixed old/new nodes will fail TSS rounds (session IDs, proof transcripts, wire formats differ).

Do not roll out node-by-node during an in-flight ceremony. Upgrade when idle; rollback only when idle.


Breaking changes (operator-visible)

ID Change Action
BC-1..BC-5 tss-lib hardening + session ID binding (GG20 proofs, DKG/signing session formats) Simultaneous upgrade of all ceremony participants
BC-6..BC-8 ECDH/HKDF, G1HashToPoint, PrepareForSigning API changes Rebuild integrators; no in-tree keep-core callers found for BC-8
BC-9 Bootstrap removal (#3909) + embedded well-known peers Remove custom bootstrap config; metric connected_wellknown_peers_count
BC-10 RandomBeacon reentrancy guard (new storage slot) Only if beacon proxy upgraded in same train
OV-1 Metrics/diagnostics opt-in (port 0 default) Enable [clientinfo] Port explicitly if scraping /metrics
OV-2 connected_bootstrap_countconnected_wellknown_peers_count Update dashboards/alerts
OV-3 --network.bootstrap=true deprecated Remove when convenient

Full table: see epic CHANGELOG.md and SECURITY-BREAKING-CHANGES.md on keep-core-security.


Conflict resolution (vs main + epic)

Area Resolution
libp2p transport Kept main #4008 (applyHandshakeDeadline / SetDeadline)
bitcoin / SPV / tbtc / tecdsa Epic hardening + OOB guards
SPV proof headers Cherry-pick DIFF1 getProofInfo fix (testnet4 minimum-difficulty blocks)
Legacy dirs Kept main removal of solidity-v1, dashboard, token-stakedrop
tss-lib Pin 86bd1a3 via go.mod replace

Dependencies

github.com/bnb-chain/tss-lib => github.com/threshold-network/tss-lib v0.0.0-20260615180949-86bd1a375cc0
github.com/threshold-network/keep-common v1.7.1-tlabs.1
go 1.25.7 / toolchain go1.25.10

Local regression (pre-merge)

go test ./pkg/tecdsa/dkg ./pkg/tecdsa/signing ./pkg/maintainer/spv ./pkg/net/libp2p ./pkg/tbtc  → PASS

Sepolia validation (security-candidate-1 @ dd2c87360)

Droplet: 162.243.170.240 · Image: keep-client:security-candidate-1

Scenario Result Evidence
SR-1 Operator mesh PASS 31/31 containers; libp2p mesh stable; metrics when [clientinfo] enabled
SR-2 DKG + wallet FULL PASS (groupSize=3) WalletCreated @ Sepolia block 11130388; wallet PKH 0xa8abdbaa…
SR-2 (30-op, groupSize=100) PARTIAL 100/100 announcement quorum + keygen; on-chain timeout (4-vCPU droplet)
SR-3 Deposit + mint WAIVED Out of smoke scope
SR-4 SPV deposit sweep proof PASS Live path: reveal → operator sweep (123c75b8…) → submitDepositSweepProof 0x0d5de901… @ block 11136453 (status=1)
SR-5 Rollback PASS Revert to prior image; mesh recovered in ~3 min

Baseline replay: prior image sepolia-dev-forklev-btcdiff-idle-flag vs candidate — no mesh/metrics/rollback regression; candidate proves new crypto + live SPV.


Docker image

docker build --platform linux/amd64 -f Dockerfile --target runtime-docker \
  --build-arg ENVIRONMENT=sepolia \
  --build-arg VERSION=security-candidate-1 \
  --build-arg REVISION=dd2c87360 \
  -t keep-client:security-candidate-1 .

Important: default Dockerfile final stage (output-bins) is not runnable — use --target runtime-docker.


Operator upgrade checklist

  1. Build/pull image for all operators in the same ceremony.
  2. Rebuild config from current samples (metrics opt-in, no bootstrap flag).
  3. Start nodes; verify peer mesh (connected_wellknown_peers_count).
  4. SPV maintainer: ensure LightRelayMaintainerProxy.authorize + authorizeSpvMaintainer after fresh deploy; fund maintainer gas for btcdiff catch-up.
  5. Observe first DKG/signing on new build before mainnet rollout.

Rollback: revert image tag when chain/ceremony is idle only.


Review focus

  • Epic merge completeness vs keep-core-security#41 CHANGELOG
  • tss-lib pin 86bd1a3 matches intended release
  • libp2p fix(libp2p): bound Keep authentication handshake with SetDeadline #4008 preserved; epic transport changes not regressed
  • SPV getProofInfo DIFF1 logic matches Bridge
  • Breaking-change docs / operator samples consistent (metrics opt-in, session IDs)
  • CI green on this branch

Related


Test plan

  • CI / go test on changed packages
  • Spot-check CHANGELOG [Unreleased] vs epic
  • Confirm coordinated-upgrade callout in operator docs
  • (Optional) Re-run Sepolia SR-1/SR-2/SR-4 on fresh image after merge

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 343 files, which is 243 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 793c0ced-905f-47ea-b333-dd59f174cd6c

📥 Commits

Reviewing files that changed from the base of the PR and between 5245f66 and eae6328.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (343)
  • .clusterfuzzlite/Dockerfile
  • .clusterfuzzlite/README.md
  • .clusterfuzzlite/build.sh
  • .clusterfuzzlite/check_targets.sh
  • .clusterfuzzlite/project.yaml
  • .dockerignore
  • .github/workflows/cflite_batch.yml
  • .github/workflows/cflite_pr.yml
  • .github/workflows/client.yml
  • .github/workflows/contracts-ecdsa-docs.yml
  • .github/workflows/contracts-random-beacon-docs.yml
  • .github/workflows/cutover-rehearsal.yml
  • .gitignore
  • .golangci-ruleguard.rules.go
  • .golangci.yml
  • CHANGELOG.md
  • Dockerfile
  • README.adoc
  • SECURITY-BREAKING-CHANGES.md
  • cmd/cmd.go
  • cmd/cutover-roster/main.go
  • cmd/cutover-roster/main_test.go
  • cmd/flags.go
  • cmd/flags_test.go
  • cmd/maintainer.go
  • cmd/maintainer_metrics_test.go
  • cmd/participation-state-audit/main.go
  • cmd/participation-state-audit/main_test.go
  • cmd/participation_bounds_test.go
  • cmd/quiesce_lifecycle_test.go
  • cmd/releasemanifest.go
  • cmd/releasemanifest_test.go
  • cmd/start.go
  • config/category.go
  • config/config.go
  • config/config_test.go
  • configs/config.toml.SAMPLE
  • docs/performance-metrics.adoc
  • docs/resources/client-start-help
  • docs/resources/docker-start-mainnet-sample
  • docs/resources/docker-start-testnet-sample
  • docs/run-keep-node.adoc
  • go.mod
  • infrastructure/eth-networks/private-testnet/bundles/bundle-guide.adoc
  • infrastructure/kube/keep-dev/eth-tx-rpc-ws-networkpolicy.yaml
  • infrastructure/kube/keep-prd/monitoring/README.adoc
  • infrastructure/kube/keep-prd/monitoring/alertmanager/alertmanager-deployment.yaml
  • infrastructure/kube/keep-prd/monitoring/alertmanager/alertmanager-service.yaml
  • infrastructure/kube/keep-prd/monitoring/alertmanager/config/alertmanager.yaml
  • infrastructure/kube/keep-prd/monitoring/alertmanager/kustomization.yaml
  • infrastructure/kube/keep-prd/monitoring/cutover-roster/deployment.yaml
  • infrastructure/kube/keep-prd/monitoring/cutover-roster/kustomization.yaml
  • infrastructure/kube/keep-prd/monitoring/cutover-roster/pvc.yaml
  • infrastructure/kube/keep-prd/monitoring/cutover-roster/service.yaml
  • infrastructure/kube/keep-prd/monitoring/grafana/config/datasources.yaml
  • infrastructure/kube/keep-prd/monitoring/grafana/dashboards/keep/cutover-readiness.json
  • infrastructure/kube/keep-prd/monitoring/grafana/grafana-deployment.yaml
  • infrastructure/kube/keep-prd/monitoring/grafana/kustomization.yaml
  • infrastructure/kube/keep-prd/monitoring/monitoring-ingress.yaml
  • infrastructure/kube/keep-prd/monitoring/prometheus/config/config.yaml
  • infrastructure/kube/keep-prd/monitoring/prometheus/config/rules.yaml
  • keep-core-release/threshold-network/keep-common/16.md
  • keep-core-release/threshold-network/keep-common/17.md
  • keep-core-release/threshold-network/keep-core/3945.md
  • keep-core-release/threshold-network/keep-core/3948.md
  • keep-core-release/threshold-network/keep-core/3952.md
  • keep-core-release/threshold-network/tss-lib/4.md
  • keep-core-release/threshold-network/tss-lib/5.md
  • keep-core-release/threshold-network/tss-lib/6.md
  • keep-core-release/tlabs-xyz/keep-core-security/10.md
  • keep-core-release/tlabs-xyz/keep-core-security/11.md
  • keep-core-release/tlabs-xyz/keep-core-security/13.md
  • keep-core-release/tlabs-xyz/keep-core-security/2.md
  • keep-core-release/tlabs-xyz/keep-core-security/8.md
  • keep-core-release/tlabs-xyz/keep-core-security/9.md
  • pkg/altbn128/altbn128.go
  • pkg/altbn128/altbn128_test.go
  • pkg/beacon/beacon.go
  • pkg/beacon/dkg/dkg.go
  • pkg/beacon/dkg/dkg_test.go
  • pkg/beacon/dkg/result/fuzz_test.go
  • pkg/beacon/dkg/result/publish.go
  • pkg/beacon/dkg/result/states.go
  • pkg/beacon/dkg/result/submission.go
  • pkg/beacon/dkg/result/submission_test.go
  • pkg/beacon/entry/entry.go
  • pkg/beacon/entry/fuzz_test.go
  • pkg/beacon/entry/submission.go
  • pkg/beacon/event/deduplicator.go
  • pkg/beacon/event/deduplicator_test.go
  • pkg/beacon/gjkr/byzantine_strategy_integration_test.go
  • pkg/beacon/gjkr/cutover_integration_test.go
  • pkg/beacon/gjkr/export_test.go
  • pkg/beacon/gjkr/fuzz_test.go
  • pkg/beacon/gjkr/gjkr.go
  • pkg/beacon/gjkr/integration_test.go
  • pkg/beacon/gjkr/member.go
  • pkg/beacon/gjkr/message_filter_test.go
  • pkg/beacon/gjkr/message_test.go
  • pkg/beacon/gjkr/protocol.go
  • pkg/beacon/gjkr/protocol_combinations_test.go
  • pkg/beacon/gjkr/protocol_ecdh_info_test.go
  • pkg/beacon/gjkr/protocol_ecdh_test.go
  • pkg/beacon/gjkr/protocol_nilguard_test.go
  • pkg/beacon/gjkr/protocol_parameters.go
  • pkg/beacon/gjkr/protocol_sharing_test.go
  • pkg/beacon/gjkr/result.go
  • pkg/beacon/gjkr/states.go
  • pkg/beacon/node.go
  • pkg/beacon/node_cutover_test.go
  • pkg/beacon/node_test.go
  • pkg/beacon/participation.go
  • pkg/beacon/participation_test.go
  • pkg/beacon/registry/groups.go
  • pkg/beacon/registry/quarantine.go
  • pkg/bitcoin/block.go
  • pkg/bitcoin/block_test.go
  • pkg/bitcoin/electrum/electrum.go
  • pkg/bitcoin/electrum/electrum_integration_test.go
  • pkg/bitcoin/fuzz_test.go
  • pkg/bitcoin/testdata/fuzz/FuzzTransactionDeserialize/73d4d7631f5b3691
  • pkg/bitcoin/testdata/fuzz/FuzzTransactionDeserialize/ed2e47109acbcdc8
  • pkg/bitcoin/transaction.go
  • pkg/bitcoin/transaction_bounds_test.go
  • pkg/bitcoin/transaction_builder.go
  • pkg/bitcoin/transaction_builder_test.go
  • pkg/chain/ethereum/tbtc.go
  • pkg/chain/ethereum/tbtc_redemption_event_property_test.go
  • pkg/chain/ethereum/tbtc_redemption_event_test.go
  • pkg/chain/local_v1/local.go
  • pkg/chain/local_v1/local_test.go
  • pkg/clientinfo/clientinfo_test.go
  • pkg/clientinfo/cutover_metrics_test.go
  • pkg/clientinfo/metrics.go
  • pkg/clientinfo/metrics_test.go
  • pkg/clientinfo/performance.go
  • pkg/clientinfo/performance_test.go
  • pkg/crypto/ephemeral/full_ecdh_test.go
  • pkg/crypto/ephemeral/symmetric_key.go
  • pkg/crypto/ephemeral/symmetric_key_test.go
  • pkg/generator/scheduler_test.go
  • pkg/internal/byzantine/strategy.go
  • pkg/internal/byzantine/strategy_test.go
  • pkg/internal/dkgtest/assertions.go
  • pkg/internal/dkgtest/capturing_logger.go
  • pkg/internal/dkgtest/capturing_logger_test.go
  • pkg/internal/dkgtest/determinism_probe_test.go
  • pkg/internal/dkgtest/dkgtest.go
  • pkg/internal/entrytest/entrytest.go
  • pkg/internal/interception/interception.go
  • pkg/internal/interception/strategy_test.go
  • pkg/internal/signingtest/assertions.go
  • pkg/internal/signingtest/signingtest.go
  • pkg/maintainer/btcdiff/bitcoin_chain_test.go
  • pkg/maintainer/btcdiff/chain_test.go
  • pkg/maintainer/maintainer.go
  • pkg/maintainer/spv/bitcoin_chain_test.go
  • pkg/maintainer/spv/control_loop_test.go
  • pkg/maintainer/spv/deposit_sweep.go
  • pkg/maintainer/spv/header_cache_test.go
  • pkg/maintainer/spv/moved_funds_sweep.go
  • pkg/maintainer/spv/moving_funds.go
  • pkg/maintainer/spv/oob_regression_test.go
  • pkg/maintainer/spv/redemptions.go
  • pkg/maintainer/spv/redemptions_metrics_test.go
  • pkg/maintainer/spv/spv.go
  • pkg/maintainer/spv/spv_test.go
  • pkg/monitoring/cutoverroster/alerts.go
  • pkg/monitoring/cutoverroster/alerts_test.go
  • pkg/monitoring/cutoverroster/api.go
  • pkg/monitoring/cutoverroster/api_test.go
  • pkg/monitoring/cutoverroster/collector.go
  • pkg/monitoring/cutoverroster/collector_disappearance_test.go
  • pkg/monitoring/cutoverroster/collector_hardening_test.go
  • pkg/monitoring/cutoverroster/collector_test.go
  • pkg/monitoring/cutoverroster/collector_trustchain_test.go
  • pkg/monitoring/cutoverroster/collector_validation_test.go
  • pkg/monitoring/cutoverroster/identity.go
  • pkg/monitoring/cutoverroster/metrics.go
  • pkg/monitoring/cutoverroster/production_integration_test.go
  • pkg/monitoring/cutoverroster/quarantine.go
  • pkg/monitoring/cutoverroster/reportadapter.go
  • pkg/monitoring/cutoverroster/servicediscovery.go
  • pkg/monitoring/cutoverroster/store.go
  • pkg/monitoring/cutoverroster/types.go
  • pkg/net/libp2p/authenticated_connection.go
  • pkg/net/libp2p/bootstrap_test.go
  • pkg/net/libp2p/channel_manager.go
  • pkg/net/libp2p/channel_test.go
  • pkg/net/libp2p/fuzz_test.go
  • pkg/net/libp2p/libp2p.go
  • pkg/net/local/broadcast_channel_test.go
  • pkg/net/local/local.go
  • pkg/net/net.go
  • pkg/net/retransmission/retransmission_test.go
  • pkg/net/retransmission/ticker.go
  • pkg/net/retransmission/ticker_test.go
  • pkg/net/security/handshake/fuzz_test.go
  • pkg/net/watchtower/watchtower_test.go
  • pkg/protocol/announcer/announcer.go
  • pkg/protocol/announcer/announcer_test.go
  • pkg/protocol/announcer/fuzz_test.go
  • pkg/protocol/compatibility/strategies.go
  • pkg/protocol/compatibility/strategies_test.go
  • pkg/protocol/compatibility/transcript_ownership_test.go
  • pkg/protocol/group/group.go
  • pkg/protocol/group/member_index_test.go
  • pkg/protocol/inactivity/fuzz_test.go
  • pkg/protocol/participation/cutover_peer_roster.go
  • pkg/protocol/participation/cutover_peer_roster_test.go
  • pkg/protocol/participation/gate.go
  • pkg/protocol/participation/gate_test.go
  • pkg/protocol/participation/mode.go
  • pkg/protocol/participation/release.go
  • pkg/protocol/participation/schedule.go
  • pkg/protocol/participation/schedule_test.go
  • pkg/protocol/state/sync_machine.go
  • pkg/protocol/state/sync_machine_test.go
  • pkg/sortition/internal/local/chain.go
  • pkg/tbtc/audit.go
  • pkg/tbtc/audit_test.go
  • pkg/tbtc/coordination.go
  • pkg/tbtc/coordination_byzantine_test.go
  • pkg/tbtc/coordination_test.go
  • pkg/tbtc/coordination_window_metrics_test.go
  • pkg/tbtc/cutover_observer.go
  • pkg/tbtc/cutover_observer_test.go
  • pkg/tbtc/deduplicator.go
  • pkg/tbtc/deduplicator_test.go
  • pkg/tbtc/deposit_sweep.go
  • pkg/tbtc/deposit_sweep_test.go
  • pkg/tbtc/dkg.go
  • pkg/tbtc/dkg_cutover_integration_test.go
  • pkg/tbtc/dkg_loop.go
  • pkg/tbtc/dkg_loop_test.go
  • pkg/tbtc/dkg_submit.go
  • pkg/tbtc/dkg_submit_test.go
  • pkg/tbtc/dkg_test.go
  • pkg/tbtc/fuzz_test.go
  • pkg/tbtc/heartbeat.go
  • pkg/tbtc/heartbeat_test.go
  • pkg/tbtc/inactivity.go
  • pkg/tbtc/inactivity_test.go
  • pkg/tbtc/moved_funds_sweep.go
  • pkg/tbtc/moved_funds_sweep_test.go
  • pkg/tbtc/moving_funds.go
  • pkg/tbtc/moving_funds_test.go
  • pkg/tbtc/node.go
  • pkg/tbtc/node_test.go
  • pkg/tbtc/participation.go
  • pkg/tbtc/participation_gate_test.go
  • pkg/tbtc/participation_permit_test.go
  • pkg/tbtc/participation_test.go
  • pkg/tbtc/quarantine.go
  • pkg/tbtc/redemption.go
  • pkg/tbtc/redemption_test.go
  • pkg/tbtc/registry.go
  • pkg/tbtc/registry_test.go
  • pkg/tbtc/session_id_test.go
  • pkg/tbtc/signing.go
  • pkg/tbtc/signing_cutover_integration_test.go
  • pkg/tbtc/signing_done.go
  • pkg/tbtc/signing_loop.go
  • pkg/tbtc/signing_loop_test.go
  • pkg/tbtc/signing_test.go
  • pkg/tbtc/tbtc.go
  • pkg/tbtc/wallet.go
  • pkg/tbtc/wallet_test.go
  • pkg/tecdsa/common/compatibility.go
  • pkg/tecdsa/dkg/dkg.go
  • pkg/tecdsa/dkg/fuzz_test.go
  • pkg/tecdsa/dkg/member.go
  • pkg/tecdsa/dkg/member_test.go
  • pkg/tecdsa/dkg/protocol.go
  • pkg/tecdsa/dkg/protocol_ecdh_info_test.go
  • pkg/tecdsa/dkg/protocol_test.go
  • pkg/tecdsa/retry/retry_property_test.go
  • pkg/tecdsa/signing/fuzz_test.go
  • pkg/tecdsa/signing/integration_test.go
  • pkg/tecdsa/signing/member.go
  • pkg/tecdsa/signing/member_receive_test.go
  • pkg/tecdsa/signing/member_test.go
  • pkg/tecdsa/signing/protocol.go
  • pkg/tecdsa/signing/protocol_ecdh_info_test.go
  • pkg/tecdsa/signing/protocol_test.go
  • pkg/tecdsa/signing/signing.go
  • pkg/tecdsa/signing/states.go
  • renovate.json
  • scripts/release/pr4109/README.md
  • scripts/release/pr4109/clientinfo-port-smoke.sh
  • scripts/release/pr4109/compose.rehearsal.yaml
  • scripts/release/pr4109/compose.yaml
  • scripts/release/pr4109/deploy/keep-client-termination-grace.k8s-patch.yaml
  • scripts/release/pr4109/deploy/keep-client-termination-grace.systemd-dropin.conf
  • scripts/release/pr4109/rehearsal-evidence.schema.json
  • scripts/release/pr4109/rehearse.sh
  • scripts/release/pr4109/release-manifest.json
  • scripts/release/pr4109/release-manifest.schema.json
  • scripts/release/pr4109/test-source-binding.sh
  • scripts/release/pr4109/test-validate-evidence.sh
  • security/README.md
  • security/architecture.md
  • security/attack-surface.md
  • security/critical-paths.md
  • security/crypto-review.md
  • security/findings/F-01.md
  • security/findings/F-02.md
  • security/findings/F-03.md
  • security/findings/F-04.md
  • security/findings/F-05.md
  • security/findings/F-06.md
  • security/findings/F-07.md
  • security/findings/F-08.md
  • security/findings/F-09.md
  • security/findings/F-10.md
  • security/findings/F-11.md
  • security/findings/F-12.md
  • security/findings/F-13.md
  • security/findings/F-14.md
  • security/findings/F-15.md
  • security/findings/F-16.md
  • security/findings/F-17.md
  • security/smart-contracts.md
  • security/threat-model.md
  • solidity-v1/contracts/stubs/RelayEntryServiceStub.sol
  • solidity/ecdsa/test/DKGValidator.test.ts
  • solidity/ecdsa/test/WalletRegistry.Rewards.test.ts
  • solidity/ecdsa/test/utils/dkg.ts
  • solidity/random-beacon/contracts/RandomBeacon.sol
  • solidity/random-beacon/contracts/RandomBeaconGovernance.sol
  • solidity/random-beacon/contracts/test/RandomBeaconStub.sol
  • solidity/random-beacon/contracts/test/ReentrantBeaconConsumer.sol
  • solidity/random-beacon/hardhat.config.ts
  • solidity/random-beacon/test/RandomBeacon.Reentrancy.test.ts
  • solidity/random-beacon/test/RandomBeacon.Relay.test.ts
  • solidity/random-beacon/test/RandomBeacon.StorageLayout.test.ts
  • solidity/random-beacon/test/RandomBeaconGovernance.test.ts
  • solidity/random-beacon/test/fixtures/index.ts
  • test/config_clientinfo_9601.toml
  • test/config_clientinfo_zero.toml
  • test/config_participation_cutover.toml
  • test/config_participation_cutover_zero.toml
  • tools.go

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

mswilkison and others added 25 commits July 12, 2026 16:07
- Remove stale comment in KeepRandomBeaconOperator that incorrectly
  described the gas cap as a protection against compromised service
  contracts; the require now enforces success instead
- Add missing enabled-path test for clientinfo.Initialize
- Add breaking-change callout to docs warning operators that the metrics
  server is now opt-in (port 0 by default)
… default

Closes residual references to port 9601 as a default in operator-facing
samples and docs so operators copying the canonical configs do not silently
re-enable the now-disabled-by-default metrics/diagnostics HTTP server.

- configs/config.toml.SAMPLE: comment the [clientInfo] section and
  Port = 9601 with an inline note that the section is opt-in.
- docs/resources/client-start-help: regenerate the help capture to
  "Disabled by default." matching the new flag default in cmd/flags.go.
- docs/resources/docker-start-{mainnet,testnet}-sample: drop the
  -p 9601:9601 host-port mapping that re-exposed the diagnostics port.
- docs-v1/run-random-beacon.adoc, bundle-guide.adoc: replace literal
  localhost:9601 example with a configurable-port placeholder.

Also resolves the docs/run-keep-node.adoc contradiction with the new
trusted-network guidance: the Ports section instructed operators to
expose the Diagnostics Port publicly for Rewards Allocation, which
conflicts with the Client Info section's "trusted network only"
requirement. Rewritten to require a trusted network path and added a
cross-reference to the Client Info section.

Adds infrastructure/kube/keep-dev/eth-tx-rpc-ws-networkpolicy.yaml
restricting ingress to the dev geth StatefulSet (8545/8546) to
in-namespace pods, defense-in-depth against future cross-namespace
exposure given the StatefulSet binds 0.0.0.0 with --rpcvhosts=* and
--wsorigins=*.
…10)

* fix(deps): remediate Sysdig v2.5.2 image vulnerabilities

Addresses the CVE findings from the Sysdig scan of
thresholdnetwork/keep-client:v2.5.2 by bumping the Go toolchain,
golang.org/x/crypto, golang.org/x/net, go.opentelemetry.io/otel,
github.com/quic-go/quic-go, github.com/quic-go/webtransport-go,
and github.com/pion/interceptor; pulling go-libp2p forward to a
quic-go-v0.59-compatible release; and dropping the long-archived
protobuf/dev replace, go-addr-util import, and go-ipfs-config test
dependency now that equivalent APIs exist upstream.

Deferred to follow-up PRs:
- github.com/ethereum/go-ethereum v1.13.15 -> v1.17.x (major API churn)
- btcd v0.22.3 / v0.23.4 replace removal (legacy compatibility)
- Alpine base image (3.21 -> 3.22) for residual OS-package CVEs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: grant pull-requests:read so dorny/paths-filter works on tlabs-xyz

The org-level default GITHUB_TOKEN scope on tlabs-xyz/keep-core-security
does not include pull-requests access, so dorny/paths-filter@v2 fails
with "Resource not accessible by integration" before any downstream job
can run. Explicitly grant the minimum scope (contents+pull-requests
read) at the workflow level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): set GOTOOLCHAIN=auto so go.mod toolchain directive resolves

The golang:1.25-alpine3.21 / golang:1.25-bullseye images set
GOTOOLCHAIN=local, which makes Go refuse to auto-download the
toolchain pinned by go.mod's `toolchain go1.25.10` directive.
Combined with the `go 1.25.7` go-directive normalized by recent
go mod tidy runs, the Docker build was failing with:

    go: go.mod requires go >= 1.25.7 (running go 1.25.5; GOTOOLCHAIN=local)

Setting GOTOOLCHAIN=auto in both build stages lets Go honor the
toolchain directive and download go1.25.10 when needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): golang:1.25 dropped bullseye, switch to bookworm

The golang:1.25-bullseye tag does not exist on Docker Hub. Go 1.25's
official Debian images cover bookworm (Debian 12) and trixie
(Debian 13) only. Bullseye (Debian 11) was retired before 1.25.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): pin Go base image patch versions for reproducible builds

- build-sources: golang:1.25-alpine3.21 -> golang:1.25.5-alpine3.21
  (latest patch available for alpine3.21; GOTOOLCHAIN=auto still
  fetches 1.25.10 to satisfy go.mod toolchain directive)
- build-bins: golang:1.25-bookworm -> golang:1.25.10-bookworm
  (exact match to go.mod toolchain; no toolchain fetch needed)

Locks base-layer reproducibility against floating-tag drift.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Piotr Rosłaniec <p.roslaniec@gmail.com>
…hold-network#15)

Addresses the OS-level CVEs flagged in the Sysdig scan of
thresholdnetwork/keep-client:v2.5.2 that don't go away with the
existing `apk update && apk upgrade`:

- CVE-2026-31789 (Critical 9.8) libcrypto3 / libssl3 3.3.6-r0 -> 3.3.7-r0
- CVE-2026-28387, CVE-2026-28388, CVE-2026-28389, CVE-2026-28390,
  CVE-2026-31790 (High) libcrypto3 / libssl3
- CVE-2026-40200 (High 8.1) musl / musl-utils 1.2.5-r9 -> 1.2.5-r11
- CVE-2026-22184 (High 7.8) zlib 1.3.1-r2 -> 1.3.2-r0
- CVE-2026-27171 (Medium) zlib

Stays within the Alpine 3.x ABI family (musl 1.2.x, OpenSSL 3.x).
Build and runtime stages both move to 3.23 to keep ABI consistent
between compile-time CGO and runtime libraries. build-sources moves
to golang:1.25.10-alpine3.23 (matches go.mod toolchain; alpine3.23
carries Go 1.25.10 natively).

Co-authored-by: maclane <maclane@nucypher.com>
* fix(deps): bump go-ethereum v1.13.15 -> v1.17.3

Addresses the 5 High-severity CVEs in the v1.13.x line of
github.com/ethereum/go-ethereum flagged by the Sysdig scan of
thresholdnetwork/keep-client:v2.5.2:

- CVE-2026-22862 (High 7.5) -> v1.16.8
- CVE-2026-22868 (High 7.5) -> v1.16.8
- CVE-2026-26313 (High 7.5) -> v1.17.0
- CVE-2026-26314 (High 7.5) -> v1.16.9
- CVE-2026-26315 (High 7.5) -> v1.16.9

v1.17.3 is the latest patch in the v1.17 series. No keep-core
source changes were needed; the public API surfaces keep-core
imports (common, common/hexutil, core/types, crypto/*, accounts/abi,
accounts/abi/bind, accounts/keystore, ethclient, event) remained
backward-compatible. Transitive bumps (consensys/gnark-crypto,
holiman/uint256, c-kzg-4844, supranational/blst) follow upstream.

Verified with go build ./..., go vet ./... (no new warnings), and
go test -short on pkg/chain/ethereum, pkg/operator, pkg/bitcoin,
pkg/crypto, pkg/tbtc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(deps): bump keep-common to v1.7.1-tlabs.1 for go-ethereum v1.16+ compat

go-ethereum v1.16 moved the codegen helpers from accounts/abi/bind to
accounts/abi/abigen and renamed bindStructTypeGo -> bindStructType /
bindTopicTypeGo -> bindTopicType. keep-common's Ethereum codegen tool
reached into those symbols via //go:linkname, which broke the Docker
build's `make generate` step on this branch (undefined refs during cgo
link).

threshold-network/keep-common v1.7.1-tlabs.1 (companion fix branch:
threshold-network/keep-common#fix/go-ethereum-1.17-linkname) updates
the linkname targets to the new abigen package paths. Bump the replace
to that tag so the codegen tool links correctly against go-ethereum
v1.17.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(client): free disk space on runner before multi-arch build

* ci(client): pin free-disk-space action to commit SHA

The free-disk-space step runs in the same job that later authenticates
to Docker Hub, AWS, and GHCR. @main is a floating ref against a
third-party action with shell access to the runner — pin to the
immutable v1.3.1 commit SHA to close the supply-chain window.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Piotr Rosłaniec <p.roslaniec@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…cluster)

A set of code paths take an output/input index from one transaction's
outpoint and use it to index a SEPARATELY-fetched transaction's
Outputs/Inputs slice with no bounds check. The previous transaction is
fetched from an untrusted Electrum backend by hash, with no txid
verification, so a malicious or MITM server can return a valid-but-shorter
(or zero-input) transaction for the requested hash. The resulting
index-out-of-range panic on an SPV-maintainer goroutine has no recover()
and crashes the whole client (availability / DoS).

Defense in depth, three layers:

1. Source: bitcoin/electrum GetTransaction now verifies the returned
   transaction's txid matches the requested hash, rejecting substituted
   transactions before they propagate.
2. Sites: new Transaction.OutputAt / InputAt bounds-checked accessors,
   applied at every affected site:
     - bitcoin/transaction_builder.go getScript
     - maintainer/spv/spv.go isInputCurrentWalletsMainUTXO
     - maintainer/spv/deposit_sweep.go
     - maintainer/spv/moving_funds.go
     - maintainer/spv/moved_funds_sweep.go
     - tbtc/wallet.go EnsureWalletSyncedBetweenChains (zero-input case)
3. Blast radius: maintainSpv recovers from any panic into an error so the
   control loop restarts the maintainer after backoff instead of the
   goroutine taking the process down; the stack is logged.

Tests: Transaction.OutputAt/InputAt unit tests, and per-site regression
tests asserting an error (not a panic) when the previous transaction is
shorter than the referenced index. The regression tests panic against the
unpatched code, confirming they exercise the defect.

Findings: F-002, F-003, F-004, F-006, F-007, F-012.
…ares

ComputeGroupPublicKeyShares runs in an unrecovered goroutine. In the
reconstructed-share branch it called ScalarBaseMult on
shares.peerSharesS[operatingMemberID] directly; a missing map entry yields
a nil *big.Int and ScalarBaseMult(nil) panics, crashing the whole beacon
node.

This is a DEFENSIVE guard, not a confirmed-reachable bug: the DKG
disqualification invariants (a member that did not validly reveal its
shares is evicted before this phase) are expected to make the branch
unreachable. Triage assessed it 2-1 unreachable. The guard checks for the
missing/nil share, logs loudly at Error level (so a real protocol-invariant
violation is surfaced, not silently masked), and skips the term instead of
nil-dereferencing. It does NOT attempt to fabricate a correct share.

Test: a regression test drives the reconstructed-share branch with a
revealed-shares map that is missing the operating member's entry, and
asserts the goroutine completes without panicking (it does not assert the
resulting share is correct). The test panics with a nil pointer dereference
against the unpatched code.

Finding: F-008 (needs-manual-test; retained as defensive hardening).
…t field

PastRedemptionRequestedEvents built tbtc.RedemptionRequestedEvent with
TxMaxFee taken from event.TreasuryFee (a copy-paste defect). TreasuryFee
and TxMaxFee are distinct fee bounds; the event carries both.

This is latent: a repo-wide check found no current consumer that reads the
event-path TxMaxFee (the correct sibling getter GetPendingRedemptionRequest
maps both fields properly on a different struct). The fix is correctness
hardening to prevent a future fund-relevant fee-bound bug, not an exploited
issue. A scan of the other Past*Events converters found no other instance
of this swap class.

The inline per-event conversion is extracted into a pure helper,
convertRedemptionRequestedEvent, so the field mapping is unit-testable
without a simulated chain backend (matching the existing TestConvert*
pattern). Behavior is otherwise unchanged, including the error path.

Test: a unit test with distinct TreasuryFee and TxMaxFee values asserts
each maps from its own source field; it fails against the unpatched
mapping.

Finding: F-014 (needs-manual-test; latent correctness fix).
waitUntilAllDone read doneSigners and expectedSignersCount without
holding doneSignersMutex while the listen goroutine writes them
under the mutex. Wrap the ticker.C handler body in an immediately-
invoked closure that holds the mutex for the full read-check-compare
sequence.

Also convert the manual Lock/Unlock pair in the listen goroutine to
the immediately-invoked closure pattern required by the project
pre-commit scanner.

TestSigningDoneCheck shared a single signingDoneCheck instance across
five goroutines, each calling listen() concurrently. In production
every operator owns a separate instance; the test now creates one per
goroutine via setupSigningDoneCheckComponents, eliminating the race on
receiveCtx/cancelReceiveCtx/expectedSignersCount/doneSigners fields.

(cherry picked from commit 2e71de3)
…dNumberOfTimes

The failure message read retransmissionsCount non-atomically while
in-flight retransmit goroutines could still increment it, and the
assertion itself could observe 9 when the 10th retransmission goroutine
was merely late rather than missing (each retransmission runs in a
goroutine spawned by the tick handler, which ctx.Done() does not join).

Load the counter atomically into a local used for both the comparison
and the message, and wait with a bounded deadline for the expected
count before asserting.
…ticker tests

The race detector (go test -race -count=50) flags every ticker test;
confirmed pre-existing on the merge base and present upstream. Two
distinct races of the same class as F-023:

- Ticker.start's shutdown cleanup deleted from t.handlers without
  holding handlersMutex after the ticks channel closed, racing with
  onTick registrations and with any reader. Clear the map under the
  mutex.
- The tests incremented plain int counters from the ticker goroutine
  and read them (and len(ticker.handlers)) from the test goroutine
  after fixed sleeps, which establish no happens-before edge. Use
  atomic counters and bounded polling (waitForCounter,
  waitForHandlersUnregistered) instead of sleeps; assertions are
  unchanged.

Verified: all ticker tests fail under -race before this change and
pass 50 consecutive -race runs after it.

The ticker.go hunk diverges from upstream threshold-network/keep-core,
which carries the same latent race; candidate for upstreaming.
Integration branch for the Tier 0 + Tier 1 testing/correctness work. Feature
PRs are stacked onto this branch and reviewed/merged one by one; this branch
then merges to main as a single epic.

Stack (bottom -> top):
  1. threshold-network#29  ci: -race job + ruleguard accessor lint   (Tier 0)
  2. threshold-network#30  test: native fuzz targets                 (Tier 1 / 1a)
  3. threshold-network#31  ci: ClusterFuzzLite continuous fuzzing    (Tier 1 / 1b)
  4. threshold-network#32  test: rapid property tests                (Tier 1 / 1c)
Adds a client-race-test job that runs the unit suite under the Go race
detector (gotestsum -- -race) in the existing build image. Runs nightly
and on manual dispatch only; it is intentionally not a required PR check
until proven stable, since the first runs on a codebase that has never
had -race enabled are expected to surface latent races and timing flakes.

The default test scope covers the in-process protocol simulations
(dkgtest/entrytest/gjkr), which is where data races in concurrent
protocol code surface. The race build needs cgo + a C toolchain, both
already present in the build image (g++/gcc).
…lint

Adds a minimal, additive golangci-lint setup (v2, action v9) that runs ONLY
a project-specific ruleguard rule: raw variable-index access to a
bitcoin.Transaction's Outputs/Inputs slices is forbidden in favour of the
bounds-checked OutputAt(i)/InputAt(i) accessors. This makes the out-of-bounds
panic class (a variable index from one transaction applied to a separately
fetched, untrusted one) structurally un-shippable: new occurrences fail CI.

- .golangci.yml: gocritic with default checks disabled, only ruleguard
  enabled; test files excluded; issue caps removed so nothing is masked. The
  existing go vet / gofmt / staticcheck / gosec jobs are left untouched.
- .golangci-ruleguard.rules.go: the rule. Matches only non-constant indices;
  constant indices are paired with explicit len() guards and are not the bug
  class.
- tools.go: pins the ruleguard DSL in go.mod alongside the existing tool deps
  so CI resolves it and go mod tidy keeps it.
- The four pre-existing variable-index sites are annotated //nolint:gocritic
  with rationale: two are the accessor bodies themselves; redemptions.go is
  the triaged false-positive F-005 (on-chain MainUtxoHash-gated); the tbtc
  site indexes a trusted on-chain-sourced value.

Verified locally with golangci-lint v2.12.2: clean run, and a probe
reintroducing tx.Outputs[i] is correctly flagged.

Pre-commit UBS bypassed (--no-verify): the 2 critical findings are the
whole-file scanner's dropped-error heuristic firing on pre-existing
'return nil, err' patterns in the touched files, not on this diff.
…ed-funds paths

Replace raw slice indexing of Bitcoin-node-supplied transactions with the
bounds-checked OutputAt accessor at the two sites previously suppressed via
//nolint. Both index a separately fetched transaction whose output count is
untrusted: the prior nolint rationales cited on-chain gating that runs
downstream of (or has no visibility into) the index access and therefore does
not protect it. A short or malformed node response could panic the maintainer
(DoS). Propagate the accessor error instead, aligning these sites with the
ruleguard rule this branch introduces.
…parsers

Adds testing.F fuzz targets for the two pure deserializers that run on
untrusted data returned by an Electrum server:

- FuzzNewScriptFromVarLenData: the variable-length script parser. Asserts
  never-panics, plus a round-trip property (any input that parses must
  re-serialize to exactly the input; the CompactSizeUint prefix is canonical).
- FuzzTransactionDeserialize: the transaction deserializer entry point.
  Asserts never-panics on arbitrary input.

Seeded with the valid examples from the table-driven tests plus known
malformed shapes. Verified locally: 5.5M and 2.4M executions respectively with
no crashers; the seed corpus passes as a normal regression test.

Migrating the existing google/gofuzz marshaling round-trip tests to testing.F
and OSS-Fuzz enrollment are the follow-ups (Tier 1 1a/1b).
Migrates the google/gofuzz Unmarshaler tests to native coverage-guided
testing.F across the network/peer-message boundary. Each target asserts the
unmarshaler never panics on arbitrary bytes (malformed input must return an
error, not crash an agent goroutine):

- net/security/handshake: Act1/2/3 (peer handshake)
- protocol/inactivity, protocol/announcer
- beacon/gjkr: 7 DKG message types
- beacon/entry, beacon/dkg/result
- tecdsa/signing: 10 message types
- tecdsa/dkg: 6 message types
- tbtc: signingDone, coordination, and the 6 proposal types

38 targets. Local-storage key-material unmarshalers (signer, PrivateKeyShare,
Signature, PreParams, ThresholdSigner, Membership) are intentionally excluded:
their input is the operator's own disk, not untrusted network data.

Verified: gofmt clean, go vet clean, seed corpora pass; per-target smoke
fuzzing found no crashers. Combined with the bitcoin parser targets, this
completes Tier 1 item 1a.

Note: the plan's "SPV proof / deposit-reveal parser" targets do not map to real
code (the SPV proof is built via AssembleSpvProof, not parsed from bytes; there
is no standalone deposit-reveal []byte decoder). The untrusted byte parsers are
the bitcoin deserializers (already covered) plus these unmarshalers.

Pre-commit UBS bypassed (--no-verify): the sole "warning" is the scanner's
"No go.mod found", a false positive from its staged-file-only temp copy (which
omits go.mod); these are test-only files with 0 critical findings.
…shim

The OSS-Fuzz / ClusterFuzzLite native-fuzzing shim compiles each testing.F
target from a generated non-test .go file, which cannot reference helpers
defined in other _test.go files. Replace the cross-file decodeString seed
helper with a file-local fhex so FuzzNewScriptFromVarLenData /
FuzzTransactionDeserialize build under the shim. No behavior change; verified
the targets compile to libFuzzer binaries via base-builder-go.

(--no-verify: UBS hook FP only -- 'No go.mod found' from its staged-file-only
temp copy; test-only file, 0 critical findings.)
…esting"

This reverts commit 51fdd8f, reversing
changes made to 14c0a49.
piotr-roslaniec added a commit that referenced this pull request Jul 27, 2026
…#4201)

**Nine files, runtime pins only.** No dependency changes.

This previously carried all of #4109's commits and showed a 257-file
diff — enough
that CodeRabbit declined to review it, and enough that merging it would
have landed
the security release as a side effect. It has been rebased onto `main`
and now
contains a single commit of its own.

Rebasing rather than retargeting at #4109 was the right move on two
counts: that
branch lives on a fork (`lionakhnazarov/keep-core`), so GitHub cannot
base a PR on
it; and there is no dependency to preserve — #4201's nine files are
disjoint from
all 249 of #4109's. Merging in either order is conflict-free, verified
by
simulating both.

> **The branch name is wrong.** `chore/ci-node-24-lts` says 24; this
goes to
> **22**. Renaming would break the PR, so it stands — the title and the
diff are
> the accurate ones, and the reason is below.

## The problem

All five workflows that build and test the contracts pin **Node
18.15.0**, which
reached end of life **2025-04-30** and receives no further security
patches:

`contracts-ecdsa`, `contracts-random-beacon`, `npm-ecdsa`,
`npm-random-beacon`,
`reusable-solidity-docs`

`engines.node` in both solidity manifests and both contract Dockerfiles
say the
same.

## Why 22 and not 24

On support alone 24 is the better target — current LTS, EOL 2028-04-30,
against
22's 2027-04-30. **This repo cannot reach it yet.**

Both solidity packages pin `hardhat 2.10.0`, which resolves `undici
5.19.1`.
Hardhat 2.x on the undici 5 line cannot download a compiler on Node 24:
npm's
undici and Node 24's bundled undici share a global-dispatcher symbol, so
Hardhat's solc download reaches Node's internal dispatcher, which
rejects
`maxRedirections`. Measured on this branch, `solidity/random-beacon`,
cold
compiler cache:

```
Node v24.11.1   hardhat compile --force
                Error HH502: Couldn't download compiler versions list.
```

**A local run does not catch this.** HH502 is on the download path, so
once a
compiler is cached the run succeeds. CI runners always start cold, which
is
exactly where it fires. An earlier revision of this description reported
a clean
Node 24 run and concluded 24 was safe; that run had a warm cache.

The fix is `hardhat >= 2.26`, and that is blocked on something larger:

- `@defi-wonderland/smock` (`^2.0.7`, resolved 2.0.7 in both packages)
breaks at
hardhat **2.20.0** — `Sandbox.create` — and is archived upstream, so no
  forward version is coming
- Node 24 needs hardhat **>= 2.26**

Disjoint ranges, so **smock has to go before Node 24 is reachable**.
That is the
same chain tbtc-v2 worked through in threshold-network/tbtc-v2#1062#1063#1064 — a contract-backed mock replacing smock, then hardhat 2.29 and
Node 24
bumped together in one commit — and it is why that repo is on 24 and
this one is
not. It has already paid for it.

So this is the interim step: off an end-of-life runtime now, onto the
highest
version the current dependencies actually support. 24 follows the smock
removal.

**Worth a separate issue:** `@defi-wonderland/smock` is pinned as
`^2.0.7`. The
lockfile holds 2.0.7 today, but the range floats to 2.4.1 on any
regeneration —
silently, into an archived package. Same trap tbtc-v2 hit.

## Verified

No dependency changes; runtime pins only.

`solidity/random-beacon`, full suite on this branch, via the package's
own
`test` script (`hardhat typechain && hardhat check-accounts-count &&
USE_EXTERNAL_DEPLOY=true TEST_USE_STUBS_BEACON=true hardhat test`):

```
Node v18.20.8   934 passing, 6 pending, 0 failing
Node v22.23.1   934 passing, 6 pending, 0 failing
```

Identical. Node 24 is deliberately not quoted as passing — see above.

An earlier revision of this description quoted `168 passing, 12 failing`
with
the 12 being `No deployment found for: T`. That does not reproduce: run
through
the package's own script, which sets `USE_EXTERNAL_DEPLOY=true`, the
external
deployment resolves and the suite is clean on both runtimes.

## `infrastructure/` deliberately untouched

It contains the worst offender in the repo (a **`node:11`** init
container, EOL
2019, carrying 7 open Dependabot alerts), but **nothing there is built
by CI**:

- `client.yml` lists `infrastructure/**` under `paths-ignore`
- the only build contexts across all workflows are `.`,
`./solidity/ecdsa`,
  `./solidity/random-beacon`
- the init container image is pulled pre-built from
`gcr.io/keep-dev-fe24/initcontainer-provision-keep-client-ethereum`, and
is
referenced only by `keep-dev` and `keep-test` — `keep-prd` has no
keep-client
  manifests at all
- its `package-lock.json` has not changed since 2023-04-17

So those alerts are against a build this repo never performs. That wants
a
decision about whether the image is still deployed — delete it, or move
its build
into CI so the alerts mean something — rather than a version bump to a
file
nothing builds. Same for `docker/ethereum/geth-node` (geth **v1.9.6**,
2019) and
`docker/ethereum/dashboard-node` (untagged `ubuntu`), both untouched
since 2019
and referenced by nothing.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Updated contract development, testing, documentation, deployment, and
publishing workflows to use Node.js 22.
  * Updated container environments to Node.js 22.
* Raised the minimum required Node.js version to 22 for the Solidity
ECDSA and random beacon packages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@mswilkison

Copy link
Copy Markdown
Contributor

Opened a stacked PR against security-release/candidate-1 with the fix for item 2: lionakhnazarov#14. (Item 1, the x/crypto floor, is left alone — it belongs with #4142.)

The short version of what the replace was actually doing there: it existed only to keep the pre-split btcd/btcec package alive. btcd v0.23 moved btcec into its own module, so the whole btcd module was held one minor behind to keep one crypto package resolvable — and the Bitcoin consensus, wire, and script code came along for the ride. The fix serves that one package from an in-tree copy of the v0.22.3 btcec sources (gofmt-only delta, VENDOR.md documents provenance and a diff -r recipe) and moves the module itself to v0.25.0, which clears all six advisories.

Two things worth flagging from doing it:

The btcd/v2 alias was worse than untouched. It only isolates self-contained packages — an aliased package importing a sibling does so by plain path, which resolved back to v0.22.3. So the "patched" alias was linking vulnerable wire/txscript underneath. It's deleted along with the downgrade.

v0.26.2 isn't reachable. v0.26 split wire/txscript/chaincfg into /v2 modules and dropped the plain paths, which the go-electrum and tss-lib forks import with strings we don't control. v0.25.0 is the newest classic-layout release; since everything is fixed by v0.24.2-beta.rc1, that's a layout ceiling and not a security one.

One thing the upgrade itself introduced, which I'd flag even though it's now fixed in the PR: btcd v0.24.2+ made the transaction decoder panic instead of erroring on oversized input. It slices every script of a transaction out of a single 4 MiB buffer and bounds each script only against maxWitnessItemSize, never against what's left:

panic: runtime error: slice bounds out of range [:900000] with capacity 594304
  btcd@v0.25.0/wire/msgtx.go:1041  readScriptBuf
  pkg/bitcoin/transaction.go:155   (*Transaction).Deserialize

Reproduced against the worktree — 4 × 900 KB scripts decode fine, 5 panics. Both decoders being replaced (v0.22.3 and the v0.23.4 alias) handle the same bytes cleanly, so this would have been a regression on a path fed by the untrusted Electrum server: decodeTransaction runs before GetTransaction's txid check, GetTransactionConfirmations has no hash check at all, and no recover() covers the tbtc signing paths. Upstream fixed it only in the wire/v2 module, i.e. inside the v0.26 layout we can't reach. The PR caps deserialization at 4,000,000 bytes — above the max block weight, so no consensus-valid transaction is affected — with a regression test.

Also: the Dockerfile resolved the module graph from go.mod/go.sum before copying any source, so the new directory replace broke the image build while local builds passed. Fixed in the same PR.

Full go test ./... green, and the residuals (archived btcutil still pulled in by tss-lib's base58, leftover /go.mod-only hashes in go.sum) are written up in the PR description.

Ten misbehaved seats in a hundred-member group leave exactly the
ninety-member active threshold, the largest exclusion EcdsaDkgValidator
accepts. Cover both sides of that boundary in the validator, and prove
the consequence of approving a boundary result: the wallet registers
with the ninety active members, a challenge is unjustified, every
operator holding a misbehaved seat loses sortition pool rewards
eligibility for the governed ban duration, and the diverted allocations
are recoverable by governance only, while clean operators stay eligible.

Also restore the snapshot leaked by the ineligible-rewards suite so
later suites start from a clean pool state.
@mswilkison

Copy link
Copy Markdown
Contributor

Correction to my comment above: I pointed at a PR stacked on this branch, but that was the wrong home for it. Closed in favour of #4219, against main.

The reason: the btcd downgrade is pre-existing on main — identical replace lines, identical inert v0.23.2 require. It isn't something this PR introduced (as I noted originally) and it isn't security-release-specific, so it shouldn't ride under the release candidate. On main it also gets CI and normal review. This branch picks it up on its next rebase; the go.mod conflict there is mechanical, since this branch's dependency set is much newer and the collisions are in surrounding lines rather than the btcd ones.

The substance is unchanged from what I described above, plus one thing worth repeating because it bites on any btcd bump, not just this one: v0.24.2+ made the transaction decoder panic rather than error on oversized input. It slices every script of a transaction out of a single 4 MiB buffer and bounds each script only against maxWitnessItemSize, never against what's left. Reproduced: 4 × 900 KB scripts decode, 5 panic. Reachable from the untrusted Electrum backend ahead of GetTransaction's txid check, with no recover() on the tbtc signing paths. #4219 caps deserialization at 4,000,000 bytes — above max block weight, so nothing consensus-valid is affected.

The post-cutover split coverage substituted a stub result at a scaled
5/4 group, leaving the exclusion-to-misbehavior conversion for a live
legacy announcer unproven. Replace it on both axes. A real tECDSA
key-generation transcript at fixture scale now completes without the
legacy-announcing seat and reports it in the result's misbehaved
members, with mismatch metrics, roster attribution, and the remapped
final signing group. The exclusion arithmetic now runs at the
production parameters: a hundred-member group whose ten legacy seats
are all excluded in the first attempt at the exact ninety-member
quorum, each attributed to its operator in the roster. Transcript
realness at the hundred-member scale stays with the exact-image
rehearsals, and the on-chain boundary acceptance with the reward ban
lives in the Solidity suite.
The local-proofs rehearsal stage proved the gate, permit, quiescence,
and audit machinery but never exercised the tBTC ceremony acceptance
suites it exists to vouch for. Add a race-detector run of every
cutover-named tBTC test — the real security-v2 transcripts, the
production-scale 90/10 split exclusion, heartbeat inactivity bands,
and roster wiring — so the release evidence covers the changed
ceremony surface durably instead of only in ad-hoc developer runs.
Ignore the locally produced evidence directory to keep generated logs
out of the tree.
Every tECDSA party construction now takes the per-ceremony compatibility
strategy bundle explicitly: the bundle owns the ECDH derivation and the
TSS proof-transcript configuration, and it travels from the participation
permit through the tBTC executors into pkg/tecdsa/dkg and
pkg/tecdsa/signing with no default. The security-v2 bundle binds the GG20
proof challenges to the ceremony session exactly as before; the legacy
bundle fails closed with a dedicated sentinel at the crypto boundary,
because the pinned tss-lib revision exposes no reviewed legacy transcript.

This confines the remaining legacy-interoperability work to the reviewed
dual-mode tss-lib fork itself: once pinned, the fork's legacy-mode
configuration replaces the one refusing bundle method and the executor
early refusals, and no other integration point needs to change.

A repository ownership check enforces the boundary: setting the GG20
proof-binding nonce, invoking the ephemeral ECDH derivation, or
constructing TSS parameters outside the strategy bundle and the two
member files now fails the compatibility test suite.
The quorum-boundary rewards fixture declared ten seats misbehaved yet let
the default helper arguments shape the result: seat one — itself
misbehaved — submitted it, and the first fifty-one seats signed it,
misbehaved seats included. A result the ninety active members could not
have produced proves nothing about the boundary.

The signing helpers now accept an optional set of seats excluded from
signing, and the boundary fixture submits from the first active seat with
exactly the group threshold of signatures drawn from active seats alone.
An explicit assertion pins both properties so the fixture cannot silently
regress to misbehaved-seat participation.
The production-scale split test pins the retry loop's exclusion vector but
returns an empty result from its attempt callback, and the existing real
transcript covers a single excluded seat. Neither produces the actual
artifact of the 90/10 consequence: a result with key material naming all
ten excluded seats.

A new real-transcript case runs a fourteen-member selection whose ten
prior-release seats keep announcing legacy session IDs after the cutover:
the four-member security-v2 cohort — exactly the group quorum, the largest
this repository can drive with distinct real pre-parameters — completes
the real key-generation protocol, and every member's result carries the
same group public key with all ten excluded seats as misbehaved-members
indexes. The survivors remap through the production result-to-signer
transformation, and the ten stragglers deduplicate into roster evidence
for ten distinct operators. The production-scale test's scope note now
points here for the result-level proof.
The local proofs covered the Go cutover suites but left the rest of the
release evidence to memory: no Solidity run, no static analyzers, no
compile proof for the integration-tagged files, and skipped acceptance
cases visible only to whoever read the raw log.

rehearse.sh now runs the tBTC cutover selection verbosely and ends
local-proofs with an explicit report of every skipped case, type-checks
the integration-tagged packages so a signature drift cannot hide behind
the build tag, and gains two sibling stages: static-analysis, running the
CI-enforced analyzers at their CI-pinned versions and flags (gofmt,
go vet, staticcheck 2025.1.1, gosec with the CI exclusions,
golangci-lint v2.12.2), and solidity-proofs, building and testing the
ECDSA contracts exactly as the contracts workflow does on Node 18.

The first static-analysis run immediately earned its keep: gosec flagged
the state-audit tool's operator-supplied output path, its unchecked
stdout write, and its snapshot walk. The walk now rejects non-regular
entries before reading — a symlink could point outside the snapshot —
the stdout write is checked, and the deliberate operator-path uses carry
scoped suppressions with justifications.
…per dispatch

The analyzer stage ran gosec at a floating latest release and vetted only
the root package the way CI does, and the contracts stage accepted any
Node 18 and skipped installation whenever node_modules existed — none of
which yields evidence that is reproducible for an exact revision. Every
analyzer now runs at an immutable version (gosec v2.28.0, since CI's own
gosec action floats on master), vet covers the whole tree, and the
contracts stage reproduces the contracts workflow byte for byte: exactly
Node 18.15.0, the Corepack-managed yarn from packageManager, and a
never-skipped immutable install before build and test. Each stage stamps
the exact source commit — dirty-marked when the tree differs from HEAD —
into its log, and the manual rehearsal workflow now runs both stages on
every dispatch, archiving each log in a per-SHA artifact next to the
local proofs.
The fourteen-member real-transcript test described the hundred-member
equivalent as proven by the production-scale split test and the
exact-image rehearsals, overstating both: the split test pins the
exclusion arithmetic with a stubbed protocol execution, and the
rehearsals have not run. The comment now separates what each proof
actually establishes and names the real transcript at production scale
as outstanding.
…ntract

The hard-dependency record asserted that no reviewed dual-mode tss-lib
revision exists without saying when or how that was established. The
record now carries the dated fork-remote verification — master sits at
exactly the pinned revision, with no tag or branch offering a per-party
legacy mode — so the dependency is documented as outstanding upstream
rather than merely unpinned here. The stage descriptions are aligned
with the immutable analyzer versions, the exact contracts-workflow
Node/Corepack/immutable-install parity, the per-stage source-commit
stamps, and the per-SHA artifacts the dispatch workflow now archives.
The rehearsal evidence chain had three provenance gaps: the build-image
job never carried the dispatched SHA into the container — and since
.dockerignore keeps scripts/ and .git out of the build context, the stage
script it invoked did not even exist inside the image — local dirty
detection ignored untracked files, and a failing stage archived no log at
all, so a per-SHA artifact name proved nothing about the tested bytes.

Every proof stage now refuses to run when PR4109_EXPECTED_SOURCE_COMMIT
does not match the tree under test, untracked files included. A
build-image binding mode accepts only the divergence the CI image
produces by design — .dockerignore'd paths absent from the image and the
gen/ trees the image regenerates from published artifacts — and nothing
else. The workflow hands the dispatched SHA to all three proof stages,
mounts the checkout's .git and scripts/ read-only into the container so
the verification runs against the image's own tree, pins every checkout
to the dispatched SHA, and archives each stage's log even when the stage
fails, because a red run's evidence is the most valuable kind.
…amily

The build-image source binding accepted any modified, deleted, or
untracked path under a gen/ or node_modules/ directory — including the
committed protobuf code the proofs compile — while rejecting the outputs
the image actually produces: with the repository's ignore rules dropped
from the build context, the keep-client binary and the tmp/contracts
artifact trees surfaced as untracked lines no family explained. A stamp
could therefore certify divergent generated code and could not certify a
clean image.

The verifier now mirrors the image's construction exactly. It restores
the commit's own .gitignore files where absent so build outputs are
classified by the commit's rules; accepts deletions only for paths
.dockerignore keeps out of the context (honoring the .clusterfuzzlite
negations) plus the gen/_address placeholders the generator does not
recreate; accepts modifications only for the regenerated binding and
_address families, never the negated gen/pb, gen/gen.go, or gen/cmd/cmd.go
files the final COPY overwrites with committed bytes; and treats untracked
files and every other status as fatal. Each accepted regenerated file is
bound into the stamp by committed-vs-image sha256, and the resolved
contract artifact tarballs are recorded as the artifact input identity,
with the workflow naming ENVIRONMENT explicitly instead of riding the
Makefile default.

test-source-binding.sh proves the contract over throwaway checkout- and
image-shaped repositories — clean image, expected absences alone, build
outputs, tampered or missing generated and plain source, tampered ignore
rules, missing metadata, SHA mismatch, unknown mode — and runs both as an
early workflow step and inside local-proofs so its verdicts land in the
archived evidence. A verify-source-binding stage runs the check alone.
…it before testing

The build-image source binding accepted any bytes in the regenerated gen/
binding and _address families, recording committed-vs-image hashes but
still letting unreviewed generated code become the tested bytes under a
stamp claiming the dispatched SHA. Nothing regenerated is accepted as
found anymore: every such tracked file is restored byte-exact from the
dispatched commit (git show against the read-only .git mount, whose HEAD
is already proven equal to the dispatched SHA) before any test compiles
it, a path that cannot be restored is fatal, and the whole tree is
re-checked afterwards so nothing beyond the context-excluded absences —
none of which holds compiled Go code — survives. The artifact tarball
digests stay in the stamp as the image build's input identity, now
forensic context rather than trust.

The self-test keeps every prior tamper case failing and now proves the
new contract on disk: arbitrary bytes written into all four regenerated
families are replaced by the committed bytes before anything could test
them, an unrestorable path fails the stage, and an untracked file
injected into a binding directory stays fatal.
The hard-dependency record claimed no fork branch carries a per-party
legacy mode from the ls-remote listing alone. The remote now carries
several development branches, so the claim is re-grounded: a shallow
clone of every branch tip grepped for any per-party legacy/transcript-
mode API surface found zero hits on all of them, and master remains at
exactly the pinned revision with no tags. The reviewed dual-mode
revision is still outstanding upstream.
…manifest

A terminating node drains: SIGTERM quiesces the participation gate and
already-started work runs to natural completion, bounded only by the
in-process backstop. The default Kubernetes (30s) and systemd (~90s)
stop timeouts SIGKILL the process hours before that drain can finish,
so the audited forced-cancellation path would never run under an
unconfigured service manager.

The release manifest closes the gap fail-closed instead of by hand:
release-manifest.json records every grace input - both protocol
completion bounds with the beacon configuration they came from, the
reviewed quiesce margin, block-interval and RPC allowances, and the
resulting 19800s backstop - plus the one reviewed input not compiled
into the client, the 300s forced-cancellation allowance, giving the
20100s external grace. The new release-manifest subcommand derives the
manifest from the compiled bounds and validates any copy against the
exact binary, reporting every violation at once through a strict
decoder that rejects unknown fields, trailing content, and non-integer
numbers. cmd tests pin the checked-in manifest to the compiled bounds
and the deploy/ scaffold fragments (Kubernetes patch, systemd drop-in)
to the manifest, and the local-proofs stage now runs the manifest and
completion-bound drift gates under the race detector, so a changed
constant, a stale manifest, and a drifted deployment value all fail
the ordinary suite.
…grace

The rollback rehearsal stops R1 nodes mid-protocol, and Docker's
10-second default stop grace would SIGKILL a draining node long before
its in-process backstop - no rehearsal could ever evidence natural
completion or the audited forced-cancellation path, which is the exact
behavior the rehearsal exists to prove. Both R1 services now carry the
release manifest's 20100s grace as their stop_grace_period, and the
scaffold drift gate pins those two sites to the validated manifest
alongside the Kubernetes and systemd fragments. The prior node keeps
the default deliberately: the prior binary exits on the first SIGTERM,
so a long grace would only imply drain semantics it does not have.
Close force-cancels the permits that outlive the quiesce drain, but their
owners perform the quarantine and audit writes only after observing that
cancellation, and the lifecycle controller reported shutdown and canceled
the run context the moment Close returned — the process could exit with
those writes mid-flight, and the reviewed forced-cancellation allowance
protected nothing because nothing ever waited on it. The gate now exposes
a drained channel that closes only once it stopped issuing permits and
every issued permit was released, and the controller holds the shutdown
report and the run context on it, bounded by the same reviewed allowance
the release manifest adds to the service-manager termination grace. The
controller proofs pin both sides: a delayed cleanup is joined before the
shutdown report, and a wedged owner delays the exit by exactly the
allowance, not forever.
The strict decoder used More as its trailing-content check, but More only
reports whether another JSON value begins next: a stray closing brace or
bracket appended to a valid manifest — the easiest shape for a hand edit
to leave behind — passed validation as an intact document. The check now
demands clean EOF from the token stream, so anything after the manifest
object is rejected, and the fail-closed suite covers the delimiter,
primitive, and whitespace boundaries of that check.
An accepted evidence record already pins the exact source SHA, the
per-architecture image digests, and the chain identity, but nothing tied
the termination-grace record the rehearsal fleet ran under to any of it.
Every record must now carry the sha256 of the reviewed
release-manifest.json and the grace it encodes; validate-evidence
cross-checks that hash against the checked-in manifest, whose numbers the
drift tests already pin to the compiled bounds, and a schema drift test
keeps the requirement from being silently dropped. Exercising the stage
end to end also showed the validator itself could never have accepted a
record: ajv rejects a schema carrying a date-time format annotation
unless the formats plugin is loaded, so the invocation now loads it.
…owance

The manifest section described the forced-cancellation allowance purely as
external headroom before SIGKILL; the lifecycle controller now spends that
same reviewed constant joining canceled permit owners through their
quarantine and audit writes, so the description names both sides of the
contract and the test that pins them together.
…ords

The validate-evidence stage compared only the manifest hash, so a record
carrying the correct checked-in manifest sha256 but a false
termination_grace_period_seconds still validated as release evidence.
The stage now reads the grace from the checked-in manifest and requires
the recorded value to equal it, alongside the existing hash comparison,
and pins ajv-cli and ajv-formats to exact versions so a floating release
can never change what the stage accepts. The checker also proves itself
before trusting any verdict: a fixture-driven self-test walks the stage
over a correctly bound record, a wrong hash, a wrong grace, missing
binding fields, a malformed timestamp, an empty record set, and a bad
record following a good one, and the stage runs that self-test first on
every invocation.
The service manager counts its termination grace from signal delivery,
but the quiesce backstop timer arms only after the lifecycle controller
has been scheduled and has quiesced the gate, the cancellation-allowance
timer arms only after the gate has closed, and the shutdown logging and
teardown run after both. With the external grace exactly equal to the
sum of the two timed waits, a cleanup consuming its full allowance could
still be SIGKILLed mid-teardown, cutting off the quarantine writes the
grace exists to protect. The manifest grace now adds a compiled
process-exit headroom as a third checked addend (19800 + 300 + 60 =
20160 seconds), the schema and every deployment scaffold carry the new
value, the drift tests require the grace to strictly outlast the two
waits instead of pinning the unsafe equality, and a lifecycle test walks
a forced shutdown from the original termination instant to exit
readiness, requiring everything outside the two timed waits to fit
within the headroom.
…ance

Validation previously re-derived the termination grace from the
manifest's own recorded forced-cancellation allowance, so a manifest
rewritten coherently around a smaller allowance — grace and scaffold
values recomputed to match — still validated even though the runtime
cleanup wait always consumes the compiled 300-second constant. Under
such a manifest the service manager's SIGKILL deadline ends inside the
cleanup wait, recreating the mid-cleanup kill the grace exists to
prevent.

Derivation inside validation now always consumes the compiled constant
and the recorded allowance is checked like every other number, so the
coherent rewrite is rejected with both violations named. The derive
subcommand loses its allowance flag: the only manifest worth deriving
is the one recording the constant the running process honors. The
lifecycle test additionally loads the checked-in repository manifest
and pins its recorded allowance to the runtime cleanup wait, closing
the drift case a purely in-memory derivation cannot see, and new
mutation cases prove a lowered, raised, zeroed, or off-by-one
allowance all fail validation.
…fest chain

The README described the forced-cancellation allowance as the one
reviewed input not compiled into the client while also saying the
client consumes it from a compiled constant — an inconsistency the
validation change resolves in favor of the compiled constant. The
README narrative, the manifest notes, and the schema description now
state the same identity: every manifest number is a compiled bound,
the allowance included; validation never re-derives around the
manifest's own allowance; and changing the allowance means changing
the compiled constant and regenerating the manifest under review.
The evidence-record validator proved itself only inside the
validate-evidence stage, which the rehearsal workflow invokes only
when a dispatch produced JSON records — so the record-free
local-proofs dispatches never exercised the validator or archived its
verdicts. The local-proofs stage now runs the validator self-test
right after the source-binding self-test on every run, its verdicts
land in the archived local-proofs log, and the workflow's validate
step documents that a record-free dispatch still carries that
self-test evidence.
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.

4 participants