Skip to content

Commit 99beca2

Browse files
committed
add generate_attestations task
1 parent fdfeff1 commit 99beca2

File tree

6 files changed

+1003
-0
lines changed

6 files changed

+1003
-0
lines changed

pkg/coordinator/clients/consensus/rpc/beaconapi.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ func (bc *BeaconClient) getJSON(ctx context.Context, requrl string, returnValue
117117
}
118118

119119
func (bc *BeaconClient) postJSON(ctx context.Context, requrl string, postData, returnValue interface{}) error {
120+
return bc.postJSONWithHeaders(ctx, requrl, postData, returnValue, nil)
121+
}
122+
123+
func (bc *BeaconClient) postJSONWithHeaders(ctx context.Context, requrl string, postData, returnValue interface{}, extraHeaders map[string]string) error {
120124
logurl := getRedactedURL(requrl)
121125

122126
postDataBytes, err := json.Marshal(postData)
@@ -137,6 +141,10 @@ func (bc *BeaconClient) postJSON(ctx context.Context, requrl string, postData, r
137141
req.Header.Set(headerKey, headerVal)
138142
}
139143

144+
for headerKey, headerVal := range extraHeaders {
145+
req.Header.Set(headerKey, headerVal)
146+
}
147+
140148
client := &nethttp.Client{Timeout: time.Second * 300}
141149

142150
resp, err := client.Do(req)
@@ -497,6 +505,42 @@ func (bc *BeaconClient) SubmitProposerSlashing(ctx context.Context, slashing *ph
497505
return nil
498506
}
499507

508+
type apiAttestationData struct {
509+
Data *phase0.AttestationData `json:"data"`
510+
}
511+
512+
func (bc *BeaconClient) GetAttestationData(ctx context.Context, slot uint64, committeeIndex uint64) (*phase0.AttestationData, error) {
513+
var attestationData apiAttestationData
514+
515+
err := bc.getJSON(ctx, fmt.Sprintf("%s/eth/v1/validator/attestation_data?slot=%d&committee_index=%d", bc.endpoint, slot, committeeIndex), &attestationData)
516+
if err != nil {
517+
return nil, fmt.Errorf("error retrieving attestation data: %v", err)
518+
}
519+
520+
return attestationData.Data, nil
521+
}
522+
523+
// SingleAttestation represents the Electra single attestation format for the v2 API.
524+
type SingleAttestation struct {
525+
CommitteeIndex uint64 `json:"committee_index,string"`
526+
AttesterIndex uint64 `json:"attester_index,string"`
527+
Data *phase0.AttestationData `json:"data"`
528+
Signature string `json:"signature"`
529+
}
530+
531+
func (bc *BeaconClient) SubmitAttestations(ctx context.Context, attestations []*SingleAttestation) error {
532+
headers := map[string]string{
533+
"Eth-Consensus-Version": "electra",
534+
}
535+
536+
err := bc.postJSONWithHeaders(ctx, fmt.Sprintf("%s/eth/v2/beacon/pool/attestations", bc.endpoint), attestations, nil, headers)
537+
if err != nil {
538+
return err
539+
}
540+
541+
return nil
542+
}
543+
500544
type NodeIdentity struct {
501545
PeerID string `json:"peer_id"`
502546
ENR string `json:"enr"`
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
## `generate_attestations` Task
2+
3+
### Description
4+
The `generate_attestations` task is designed to generate valid attestations for a specified range of validator keys and submit them to the network. This task fetches attester duties for the configured validators, retrieves attestation data from the beacon node, signs the attestations with the validator private keys, and submits them via the beacon API.
5+
6+
The task supports advanced configuration options for testing various attestation scenarios, including attesting for previous epochs, using delayed head blocks, and randomizing late head offsets per attestation.
7+
8+
### Configuration Parameters
9+
10+
- **`mnemonic`**:\
11+
A mnemonic phrase used for generating the validators' private keys. The keys are derived using the standard BIP39/BIP44 path (`m/12381/3600/{index}/0/0`).
12+
13+
- **`startIndex`**:\
14+
The starting index within the mnemonic from which to begin generating validator keys. This sets the initial point for key derivation.
15+
16+
- **`indexCount`**:\
17+
The number of validator keys to generate from the mnemonic, determining how many validators will be used for attestation generation.
18+
19+
- **`limitTotal`**:\
20+
The total limit on the number of attestations that the task will generate. The task will stop after reaching this limit.
21+
22+
- **`limitEpochs`**:\
23+
The total number of epochs to generate attestations for. The task will stop after processing this many epochs.
24+
25+
- **`clientPattern`**:\
26+
A regex pattern for selecting specific client endpoints for fetching attestation data and submitting attestations. If left empty, any available endpoint will be used.
27+
28+
- **`excludeClientPattern`**:\
29+
A regex pattern to exclude certain client endpoints from being used. This parameter adds a layer of control by allowing the exclusion of specific clients.
30+
31+
### Advanced Settings
32+
33+
- **`lastEpochAttestations`**:\
34+
When set to `true`, the task will generate attestations for the previous epoch's duties instead of the current epoch. This is useful for testing late attestation scenarios. Attestations are sent one slot at a time (each wallclock slot sends attestations for the corresponding slot in the previous epoch).
35+
36+
- **`sendAllLastEpoch`**:\
37+
When set to `true`, instead of sending attestations slot-by-slot, all attestations for the previous epoch are sent at once at each epoch boundary. This is useful for bulk testing of late attestations. Requires `lastEpochAttestations` to be implicitly treated as true.
38+
39+
- **`lateHead`**:\
40+
Offsets the beacon block root in the attestation by the specified number of blocks. For example, setting `lateHead: 5` will use the block root from 5 blocks before the current head. This simulates validators with a delayed view of the chain. Positive values go back (older blocks), negative values go forward.
41+
42+
- **`randomLateHead`**:\
43+
Specifies a range for randomizing the late head offset per attestation in `"min:max"` or `"min-max"` format. For example, `randomLateHead: "1-5"` will apply a random late head offset between 1 and 5 blocks (inclusive). By default, each attestation gets its own random offset. Use `lateHeadClusterSize` to group attestations with the same offset.
44+
45+
- **`lateHeadClusterSize`**:\
46+
Controls how many attestations share the same random late head offset. Default is `1` (each attestation gets its own random offset). Setting this to a higher value groups attestations together with the same late head value. For example, `lateHeadClusterSize: 10` means every 10 attestations will share the same random offset.
47+
48+
49+
### Defaults
50+
51+
Default settings for the `generate_attestations` task:
52+
53+
```yaml
54+
- name: generate_attestations
55+
config:
56+
mnemonic: ""
57+
startIndex: 0
58+
indexCount: 0
59+
limitTotal: 0
60+
limitEpochs: 0
61+
clientPattern: ""
62+
excludeClientPattern: ""
63+
lastEpochAttestations: false
64+
sendAllLastEpoch: false
65+
lateHead: 0
66+
randomLateHead: ""
67+
lateHeadClusterSize: 1
68+
```
69+
70+
### Example Usage
71+
72+
Basic usage to generate attestations for 100 validators over 5 epochs:
73+
74+
```yaml
75+
- name: generate_attestations
76+
config:
77+
mnemonic: "your mnemonic phrase here"
78+
startIndex: 0
79+
indexCount: 100
80+
limitEpochs: 5
81+
```
82+
83+
Advanced usage with late head for testing delayed attestations:
84+
85+
```yaml
86+
- name: generate_attestations
87+
config:
88+
mnemonic: "your mnemonic phrase here"
89+
startIndex: 0
90+
indexCount: 50
91+
limitTotal: 1000
92+
clientPattern: "lighthouse.*"
93+
lastEpochAttestations: true
94+
lateHead: 3
95+
```
96+
97+
Bulk send all previous epoch attestations at once with random late head:
98+
99+
```yaml
100+
- name: generate_attestations
101+
config:
102+
mnemonic: "your mnemonic phrase here"
103+
startIndex: 0
104+
indexCount: 100
105+
limitEpochs: 3
106+
sendAllLastEpoch: true
107+
randomLateHead: "1:10"
108+
```
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package generateattestations
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
type Config struct {
11+
// Key configuration
12+
Mnemonic string `yaml:"mnemonic" json:"mnemonic"`
13+
StartIndex int `yaml:"startIndex" json:"startIndex"`
14+
IndexCount int `yaml:"indexCount" json:"indexCount"`
15+
16+
// Limit configuration
17+
LimitTotal int `yaml:"limitTotal" json:"limitTotal"`
18+
LimitEpochs int `yaml:"limitEpochs" json:"limitEpochs"`
19+
20+
// Client selection
21+
ClientPattern string `yaml:"clientPattern" json:"clientPattern"`
22+
ExcludeClientPattern string `yaml:"excludeClientPattern" json:"excludeClientPattern"`
23+
24+
// Advanced settings
25+
LastEpochAttestations bool `yaml:"lastEpochAttestations" json:"lastEpochAttestations"`
26+
SendAllLastEpoch bool `yaml:"sendAllLastEpoch" json:"sendAllLastEpoch"`
27+
LateHead int `yaml:"lateHead" json:"lateHead"`
28+
RandomLateHead string `yaml:"randomLateHead" json:"randomLateHead"`
29+
LateHeadClusterSize int `yaml:"lateHeadClusterSize" json:"lateHeadClusterSize"`
30+
}
31+
32+
// ParseRandomLateHead parses the RandomLateHead string in "min:max" or "min-max" format.
33+
// Returns min, max values and whether random late head is enabled.
34+
func (c *Config) ParseRandomLateHead() (min, max int, enabled bool, err error) {
35+
if c.RandomLateHead == "" {
36+
return 0, 0, false, nil
37+
}
38+
39+
// Try colon separator first, then dash
40+
var parts []string
41+
if strings.Contains(c.RandomLateHead, ":") {
42+
parts = strings.Split(c.RandomLateHead, ":")
43+
} else if strings.Contains(c.RandomLateHead, "-") {
44+
parts = strings.Split(c.RandomLateHead, "-")
45+
}
46+
47+
if len(parts) != 2 {
48+
return 0, 0, false, fmt.Errorf("randomLateHead must be in 'min:max' or 'min-max' format, got: %s", c.RandomLateHead)
49+
}
50+
51+
min, err = strconv.Atoi(strings.TrimSpace(parts[0]))
52+
if err != nil {
53+
return 0, 0, false, fmt.Errorf("invalid min value in randomLateHead: %w", err)
54+
}
55+
56+
max, err = strconv.Atoi(strings.TrimSpace(parts[1]))
57+
if err != nil {
58+
return 0, 0, false, fmt.Errorf("invalid max value in randomLateHead: %w", err)
59+
}
60+
61+
if min > max {
62+
return 0, 0, false, fmt.Errorf("min (%d) cannot be greater than max (%d) in randomLateHead", min, max)
63+
}
64+
65+
return min, max, true, nil
66+
}
67+
68+
func DefaultConfig() Config {
69+
return Config{}
70+
}
71+
72+
func (c *Config) Validate() error {
73+
if c.LimitTotal == 0 && c.LimitEpochs == 0 {
74+
return errors.New("either limitTotal or limitEpochs must be set")
75+
}
76+
77+
if c.Mnemonic == "" {
78+
return errors.New("mnemonic must be set")
79+
}
80+
81+
if c.IndexCount == 0 {
82+
return errors.New("indexCount must be set")
83+
}
84+
85+
if c.RandomLateHead != "" {
86+
if _, _, _, err := c.ParseRandomLateHead(); err != nil {
87+
return err
88+
}
89+
}
90+
91+
return nil
92+
}

0 commit comments

Comments
 (0)