Skip to content

*: feerecipient command#4341

Open
pinebit wants to merge 14 commits intomainfrom
pinebit/feerecipient-cmd
Open

*: feerecipient command#4341
pinebit wants to merge 14 commits intomainfrom
pinebit/feerecipient-cmd

Conversation

@pinebit
Copy link
Collaborator

@pinebit pinebit commented Feb 26, 2026

This PR introduces new CLI commands: charon feerecipient sign|fetch. The fetch command eventually writes the overrides file, but it is NOT used yet in charon runtime (another PR will address this).

$ charon feerecipient --help
Sign and fetch updated builder registration messages with new fee recipients using a remote API, enabling the modification of fee recipient addresses without cluster restart.

Usage:
  charon feerecipient [command]

Available Commands:
  fetch       Fetch aggregated builder registrations.
  sign        Sign partial builder registration messages.
$ charon feerecipient fetch --help
Fetches builder registration messages from a remote API and aggregates those with quorum, writing fully signed registrations to a local JSON file.

Usage:
  charon feerecipient fetch [flags]

Flags:
  -h, --help                            Help for fetch
      --lock-file string                Path to the cluster lock file defining the distributed validator cluster. (default ".charon/cluster-lock.json")
      --overrides-file string           Path to the builder registrations overrides file. (default ".charon/builder_registrations_overrides.json")
      --publish-address string          The URL of the remote API. (default "https://api.obol.tech/v1")
      --publish-timeout duration        Timeout for accessing the remote API. (default 5m0s)
      --validator-public-keys strings   Optional comma-separated list of validator public keys to fetch builder registrations for.
$ charon feerecipient sign --help
Signs new partial builder registration messages with updated fee recipients and publishes them to a remote API.

Usage:
  charon feerecipient sign [flags]

Flags:
      --fee-recipient string            [REQUIRED] New fee recipient address to be applied to all specified validators.
      --gas-limit uint                  Optional gas limit override for builder registrations. If not set, the existing gas limit from the cluster lock or overrides file is used.
  -h, --help                            Help for sign
      --lock-file string                Path to the cluster lock file defining the distributed validator cluster. (default ".charon/cluster-lock.json")
      --overrides-file string           Path to the builder registrations overrides file. (default ".charon/builder_registrations_overrides.json")
      --private-key-file string         Path to the charon enr private key file. (default ".charon/charon-enr-private-key")
      --publish-address string          The URL of the remote API. (default "https://api.obol.tech/v1")
      --publish-timeout duration        Timeout for accessing the remote API. (default 5m0s)
      --timestamp int                   Optional Unix timestamp for the builder registration message. When set, all operators can sign independently with the same timestamp. If not set, the current time is used for new registrations.
      --validator-keys-dir string       Path to the directory containing the validator private key share files and passwords. (default ".charon/validator_keys")
      --validator-public-keys strings   [REQUIRED] Comma-separated list of validator public keys to sign builder registrations for.

Testing

  • Unit tests
  • Manually tested typical flows against a non-prod dev environment
  • Stress tested with ai-generated python scripts, no race conditions observed

category: feature
ticket: #4293

@pinebit pinebit force-pushed the pinebit/feerecipient-cmd branch from a2a4531 to 457aa4a Compare March 6, 2026 09:06
@codecov
Copy link

codecov bot commented Mar 6, 2026

Codecov Report

❌ Patch coverage is 53.91121% with 218 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.35%. Comparing base (aea4746) to head (9f1f938).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
cmd/feerecipientsign.go 63.90% 58 Missing and 29 partials ⚠️
app/obolapi/feerecipient.go 0.00% 61 Missing ⚠️
cmd/feerecipientfetch.go 71.65% 23 Missing and 13 partials ⚠️
app/obolapi/feerecipient_model.go 0.00% 30 Missing ⚠️
cmd/cmd.go 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4341      +/-   ##
==========================================
- Coverage   56.67%   56.35%   -0.33%     
==========================================
  Files         237      242       +5     
  Lines       31681    32162     +481     
==========================================
+ Hits        17956    18125     +169     
- Misses      11438    11703     +265     
- Partials     2287     2334      +47     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@pinebit pinebit force-pushed the pinebit/feerecipient-cmd branch from 5be4c8f to c03b378 Compare March 12, 2026 07:27
@pinebit pinebit changed the title *: feerecipient cmd, wip *: feerecipient command Mar 12, 2026
@pinebit pinebit requested a review from OisinKyne March 12, 2026 12:15
@pinebit pinebit marked this pull request as ready for review March 12, 2026 12:17
Comment on lines +234 to +241
// filterPubkeysByStatus fetches the current registration groups for each pubkey from the remote
// API and returns only those that need signing, each paired with the timestamp and gas limit to
// use for signing. Validators with a quorum-complete registration for the requested fee recipient
// are skipped. In-progress (non-quorum) registrations with a mismatched fee recipient cause an error.
// For validators with a matching in-progress registration, the existing timestamp and gas limit are
// adopted so all operators sign the identical message. For unknown validators, now() and the
// gas limit from the config override or cluster lock are used.
func filterPubkeysByStatus(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I like this logic here. The only downside I find is that it may break if >=2 nodes start it at more or less the same time. Which can be the case to be fair, in clusters with single operator. Any idea how we can prevent that from breaking?

Copy link
Collaborator Author

@pinebit pinebit Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was observed during my stress test. I think the right flow should be single operator submitting the first partial, then others can submit in parallel. Otherwise we need to introduce the --timeout flag, that takes precedence over now() when specified?

Copy link
Collaborator

@KaloyanTanev KaloyanTanev Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I have in mind is mainly for the single operator clusters. It's not unlikely they will execute the N commands for the N charon nodes almost immediately one after the other.

I think optional --timeout flag is good approach here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added optional --timestamp flag (by mistake we were says --timeout, but it is timestamp).

log.Info(ctx, "Validators with partial builder registrations", z.Int("count", len(cats.Incomplete)))

for _, pubkey := range cats.Incomplete {
log.Info(ctx, " Incomplete", z.Str("pubkey", pubkey), z.Int("partial_signatures", maxPartialSigs[pubkey]))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT this maxPartialSigs[pubkey] equals the amount of par sigs received, right? I think it would be useful to have also which indices have submitted them. This way operators know which peer hasn't submitted yet and act upon it.

I think we can fetch that by matching the Pubkey to the fields in the lock distributed_validators.public_shares. This is an ordered list of the partial pubkeys of each operator.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made it rendering as "submitted_indices": "[1 2]"

@sonarqubecloud
Copy link

@OisinKyne
Copy link
Contributor

Feature idea:
After a lot of edits, it might be hard to tell what the freshest / latest fee recipient is for a key / the whole cluster is. We might want a charon feerecipient get <pubkey>|all or charon feerecipient list, or something of the sort that lists summaries for one/all keys in the cluster. checking across the lock, their local overrides file, and best-effort our API, (and even some relays to confirm they see the same thing?) wdyt?

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.

3 participants