Skip to content

Commit 457aa4a

Browse files
committed
*: feerecipient cmd, wip
1 parent 41440a3 commit 457aa4a

10 files changed

Lines changed: 1109 additions & 6 deletions

app/obolapi/feerecipient.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
2+
3+
package obolapi
4+
5+
import (
6+
"context"
7+
"encoding/hex"
8+
"encoding/json"
9+
"net/url"
10+
"strconv"
11+
"strings"
12+
13+
"github.com/obolnetwork/charon/app/errors"
14+
)
15+
16+
const (
17+
submitPartialFeeRecipientTmpl = "/fee_recipient/partial/" + lockHashPath + "/" + shareIndexPath
18+
fetchPartialFeeRecipientTmpl = "/fee_recipient/" + lockHashPath + "/" + valPubkeyPath
19+
)
20+
21+
// submitPartialFeeRecipientURL returns the partial fee recipient Obol API URL for a given lock hash.
22+
func submitPartialFeeRecipientURL(lockHash string, shareIndex uint64) string {
23+
return strings.NewReplacer(
24+
lockHashPath,
25+
lockHash,
26+
shareIndexPath,
27+
strconv.FormatUint(shareIndex, 10),
28+
).Replace(submitPartialFeeRecipientTmpl)
29+
}
30+
31+
// fetchPartialFeeRecipientURL returns the partial fee recipient Obol API URL for a given validator public key.
32+
func fetchPartialFeeRecipientURL(valPubkey, lockHash string) string {
33+
return strings.NewReplacer(
34+
valPubkeyPath,
35+
valPubkey,
36+
lockHashPath,
37+
lockHash,
38+
).Replace(fetchPartialFeeRecipientTmpl)
39+
}
40+
41+
// PostPartialFeeRecipients POSTs partial fee recipient registrations to the Obol API.
42+
// It respects the timeout specified in the Client instance.
43+
func (c Client) PostPartialFeeRecipients(ctx context.Context, lockHash []byte, shareIndex uint64, partialRegs []PartialRegistration) error {
44+
lockHashStr := "0x" + hex.EncodeToString(lockHash)
45+
46+
path := submitPartialFeeRecipientURL(lockHashStr, shareIndex)
47+
48+
u, err := url.ParseRequestURI(c.baseURL)
49+
if err != nil {
50+
return errors.Wrap(err, "bad Obol API url")
51+
}
52+
53+
u.Path = path
54+
55+
req := PartialFeeRecipientRequest{PartialRegistrations: partialRegs}
56+
57+
data, err := json.Marshal(req)
58+
if err != nil {
59+
return errors.Wrap(err, "json marshal error")
60+
}
61+
62+
ctx, cancel := context.WithTimeout(ctx, c.reqTimeout)
63+
defer cancel()
64+
65+
err = httpPost(ctx, u, data, nil)
66+
if err != nil {
67+
return errors.Wrap(err, "http Obol API POST request")
68+
}
69+
70+
return nil
71+
}
72+
73+
// GetPartialFeeRecipients fetches partial fee recipient registrations from the Obol API.
74+
// It respects the timeout specified in the Client instance.
75+
func (c Client) GetPartialFeeRecipients(ctx context.Context, valPubkey string, lockHash []byte, _ int) (PartialFeeRecipientResponse, error) {
76+
path := fetchPartialFeeRecipientURL(valPubkey, "0x"+hex.EncodeToString(lockHash))
77+
78+
u, err := url.ParseRequestURI(c.baseURL)
79+
if err != nil {
80+
return PartialFeeRecipientResponse{}, errors.Wrap(err, "bad Obol API url")
81+
}
82+
83+
u.Path = path
84+
85+
ctx, cancel := context.WithTimeout(ctx, c.reqTimeout)
86+
defer cancel()
87+
88+
respBody, err := httpGet(ctx, u, map[string]string{})
89+
if err != nil {
90+
return PartialFeeRecipientResponse{}, errors.Wrap(err, "http Obol API GET request")
91+
}
92+
93+
defer respBody.Close()
94+
95+
var resp PartialFeeRecipientResponse
96+
if err := json.NewDecoder(respBody).Decode(&resp); err != nil {
97+
return PartialFeeRecipientResponse{}, errors.Wrap(err, "unmarshal response")
98+
}
99+
100+
if len(resp.Partials) == 0 {
101+
return PartialFeeRecipientResponse{}, ErrNoValue
102+
}
103+
104+
return resp, nil
105+
}

app/obolapi/feerecipient_model.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
2+
3+
package obolapi
4+
5+
import (
6+
eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
7+
8+
"github.com/obolnetwork/charon/tbls"
9+
)
10+
11+
// PartialRegistration represents a partial builder registration with a partial BLS signature.
12+
type PartialRegistration struct {
13+
Message *eth2v1.ValidatorRegistration `json:"message"`
14+
Signature tbls.Signature `json:"signature"`
15+
}
16+
17+
// PartialFeeRecipientRequest represents the request body for posting partial fee recipient registrations.
18+
type PartialFeeRecipientRequest struct {
19+
PartialRegistrations []PartialRegistration `json:"partial_registrations"`
20+
}
21+
22+
// PartialFeeRecipientResponsePartial represents a single partial registration in the response.
23+
type PartialFeeRecipientResponsePartial struct {
24+
ShareIdx int `json:"share_index"`
25+
Message *eth2v1.ValidatorRegistration `json:"message"`
26+
Signature []byte `json:"signature"`
27+
}
28+
29+
// PartialFeeRecipientResponse represents the response body when fetching partial fee recipient registrations.
30+
type PartialFeeRecipientResponse struct {
31+
Partials []PartialFeeRecipientResponsePartial `json:"partial_registrations"`
32+
}

cmd/cmd.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ func New() *cobra.Command {
7474
newDepositSignCmd(runDepositSign),
7575
newDepositFetchCmd(runDepositFetch),
7676
),
77+
newFeeRecipientCmd(
78+
newFeeRecipientSignCmd(runFeeRecipientSign),
79+
newFeeRecipientFetchCmd(runFeeRecipientFetch),
80+
),
7781
newUnsafeCmd(newRunCmd(app.Run, true)),
7882
)
7983
}

cmd/feerecipient.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
2+
3+
package cmd
4+
5+
import (
6+
"time"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/obolnetwork/charon/app/log"
11+
)
12+
13+
type feerecipientConfig struct {
14+
ValidatorPublicKeys []string
15+
PrivateKeyPath string
16+
LockFilePath string
17+
ValidatorKeysDir string
18+
PublishAddress string
19+
PublishTimeout time.Duration
20+
Log log.Config
21+
}
22+
23+
func newFeeRecipientCmd(cmds ...*cobra.Command) *cobra.Command {
24+
root := &cobra.Command{
25+
Use: "feerecipient",
26+
Short: "Sign and fetch updated fee recipient registrations.",
27+
Long: "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.",
28+
}
29+
30+
root.AddCommand(cmds...)
31+
32+
return root
33+
}
34+
35+
func bindFeeRecipientFlags(cmd *cobra.Command, config *feerecipientConfig) {
36+
cmd.Flags().StringSliceVar(&config.ValidatorPublicKeys, "validator-public-keys", []string{}, "Comma-separated list of validator public keys to update (required for the sign subcommand).")
37+
cmd.Flags().StringVar(&config.PrivateKeyPath, privateKeyPath.String(), ".charon/charon-enr-private-key", "Path to the charon enr private key file.")
38+
cmd.Flags().StringVar(&config.ValidatorKeysDir, validatorKeysDir.String(), ".charon/validator_keys", "Path to the directory containing the validator private key share files and passwords.")
39+
cmd.Flags().StringVar(&config.LockFilePath, lockFilePath.String(), ".charon/cluster-lock.json", "Path to the cluster lock file defining the distributed validator cluster.")
40+
cmd.Flags().StringVar(&config.PublishAddress, publishAddress.String(), "https://api.obol.tech/v1", "The URL of the remote API.")
41+
cmd.Flags().DurationVar(&config.PublishTimeout, publishTimeout.String(), 5*time.Minute, "Timeout for publishing to the publish-address API.")
42+
}

0 commit comments

Comments
 (0)