Skip to content

Commit 161be6a

Browse files
committed
Merge branch 'andrius/reward-siging-checks' into 'main'
Reward signing rewrite See merge request flarenetwork/flare-system-client!50
2 parents 8bc7613 + 4e8caa0 commit 161be6a

File tree

7 files changed

+614
-129
lines changed

7 files changed

+614
-129
lines changed

README.md

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -114,23 +114,15 @@ gas_price_multiplier = 0
114114
gas_price_fixed = 50000000000 # 50 * 1e9
115115
gas_limit = 0
116116

117-
[uptime] # uptime vote configuration - clients.enabled_uptime_voting must be set to true
118-
signing_window = 2 # (optional) how many epochs in the past wße attempt to sign uptime vote for, default: 2.
119-
120117
[rewards] # reward signing configuration - clients.enabled_reward_signing must be set to true
121-
# Local folder or URL prefix for retrieving rewards hash files.
122-
# A full path will be constructed by appending the epoch id and expected file name: <path>/<epochId>/rewards-hash.json
123-
#
124-
# Example:
125-
# - URL prefix: "https://example.com/rewards" -> https://example.com/rewards/2939/rewards-hash.json
126-
# - Folder: "./rewards" -> ./rewards/2939/rewards-hash.json
118+
# URL prefix for retrieving reward distribution data.
119+
# A full URL will be constructed by appending the epoch id and expected file name: <prefix>/<epochId>/reward-distribution-data.json
127120
#
128-
# The rewards hash file is expected to have the following structure:
129-
# {
130-
# "rewardEpochId": <epoch id>,
131-
# "noOfWeightBasedClaims": <number of weight-based claims>,
132-
# "merkleRoot": "<markle root of all claims for the epoch>"
133-
# }
134-
hash_path_prefix = ""
135-
signing_window = 2 # (optional) how many epochs in the past we attempt to sign rewards for, default: 2.
121+
# For example, if reward data for an epoch can be retrieved at https://raw.githubusercontent.com/flare-foundation/fsp-rewards/refs/heads/main/songbird/240/reward-distribution-data.json,
122+
# then the url_prefix should be set to "https://raw.githubusercontent.com/flare-foundation/fsp-rewards/refs/heads/main/songbird"
123+
url_prefix = ""
124+
min_reward = 0 # minimum acceptable claim amount in wei for the identity address of this provider, default 0.
125+
max_reward = 0 # (optional) maximum acceptable claim amount in wei for the identity address of this provider. If 0 or not set, no maximum is enforced.
126+
retries = 8 # (optional) number of retries for fetching and signing reward data, default: 8.
127+
retry_interval = "6h" # (optional) interval between retries, default: 6 hours.
136128
```

client/config/client.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ type ClientConfig struct {
3232
SubmitGas GasConfig `toml:"gas_submit"`
3333
RegisterGas GasConfig `toml:"gas_register"`
3434

35-
Uptime UptimeConfig `toml:"uptime"`
3635
Rewards RewardsConfig `toml:"rewards"`
3736
}
3837

@@ -117,13 +116,14 @@ type GasConfig struct {
117116
GasLimit int `toml:"gas_limit"`
118117
}
119118

120-
type UptimeConfig struct {
121-
SigningWindow int64 `toml:"signing_window"`
122-
}
123-
124119
type RewardsConfig struct {
125-
PathPrefix string `toml:"hash_path_prefix"`
126-
SigningWindow int64 `toml:"signing_window"`
120+
UrlPrefix string `toml:"url_prefix"`
121+
122+
MinRewardWei *big.Int `toml:"min_reward"`
123+
MaxRewardWei *big.Int `toml:"max_reward"`
124+
125+
Retries int `toml:"retries"`
126+
RetryInterval time.Duration `toml:"retry_interval"`
127127
}
128128

129129
func newConfig() *ClientConfig {
@@ -142,11 +142,9 @@ func newConfig() *ClientConfig {
142142
},
143143
SubmitGas: GasConfig{GasPriceFixed: big.NewInt(0)},
144144
RegisterGas: GasConfig{GasPriceFixed: big.NewInt(0)},
145-
Uptime: UptimeConfig{
146-
SigningWindow: 2,
147-
},
148145
Rewards: RewardsConfig{
149-
SigningWindow: 2,
146+
Retries: 8,
147+
RetryInterval: 6 * time.Hour,
150148
},
151149
}
152150
}

client/epoch/epoch_client.go

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
clientConfig "flare-tlc/client/config"
66
flarectx "flare-tlc/client/context"
7+
"flare-tlc/client/shared"
78
"flare-tlc/config"
89
"flare-tlc/logger"
910
"flare-tlc/utils/chain"
@@ -34,7 +35,6 @@ type EpochClient struct {
3435
rewardsSigningEnabled bool
3536

3637
rewardsConfig *clientConfig.RewardsConfig
37-
uptimeConfig *clientConfig.UptimeConfig
3838
}
3939

4040
func NewEpochClient(ctx flarectx.ClientContext) (*EpochClient, error) {
@@ -106,7 +106,6 @@ func NewEpochClient(ctx flarectx.ClientContext) (*EpochClient, error) {
106106
uptimeVotingEnabled: cfg.Clients.EnabledUptimeVoting,
107107
rewardsSigningEnabled: cfg.Clients.EnabledRewardSigning,
108108
rewardsConfig: &cfg.Rewards,
109-
uptimeConfig: &cfg.Uptime,
110109
}, nil
111110
}
112111

@@ -135,11 +134,11 @@ func (c *EpochClient) RunContext(ctx context.Context) error {
135134
}
136135
if c.uptimeVotingEnabled {
137136
logger.Info("Waiting for SignUptimeVoteEnabled event to start uptime vote signing")
138-
uptimeEnabledListener = c.systemsManagerClient.SignUptimeVoteEnabledListener(c.db, epoch, c.uptimeConfig.SigningWindow)
137+
uptimeEnabledListener = c.systemsManagerClient.SignUptimeVoteEnabledListener(c.db, epoch)
139138
}
140139
if c.rewardsSigningEnabled {
141140
logger.Info("Waiting for UptimeVoteSigned event to start rewards signing")
142-
uptimeSignedListener = c.systemsManagerClient.UptimeVoteSignedListener(c.db, epoch, c.rewardsConfig.SigningWindow)
141+
uptimeSignedListener = c.systemsManagerClient.UptimeVoteSignedListener(c.db, epoch)
143142
}
144143

145144
for {
@@ -156,7 +155,6 @@ func (c *EpochClient) RunContext(ctx context.Context) error {
156155
case uptimeVoteSigned := <-uptimeSignedListener:
157156
logger.Info("Uptime vote threshold reached for epoch %v, signing rewards", uptimeVoteSigned.RewardEpochId)
158157
c.signRewards(uptimeVoteSigned.RewardEpochId)
159-
160158
case <-ctx.Done():
161159
return ctx.Err()
162160
}
@@ -195,7 +193,6 @@ func (c *EpochClient) signPolicy(epochId *big.Int, policy []byte) {
195193
}
196194

197195
func (c *EpochClient) signUptimeVote(epochId *big.Int) {
198-
logger.Info("SignUptimeVoteEnabled event emitted for epoch %v, signing uptime vote", epochId)
199196
signUptimeVoteResult := <-c.systemsManagerClient.SignUptimeVote(epochId)
200197
if signUptimeVoteResult.Success {
201198
logger.Info("SignUptimeVote completed")
@@ -219,17 +216,54 @@ func (c *EpochClient) isFutureEpoch(epochId *big.Int) bool {
219216
return true
220217
}
221218

219+
// signRewards signs the reward claim merkle root for the given epoch.
220+
//
221+
// Once uptime signing is completed, data providers can start signing the reward hash for the epoch.
222+
// The end goal is that every data provider calculates reward hash by themselves and signs it. While rewarding logic
223+
// is still in flux, there is an interim solution where reward data is calculated & published centrally, and data
224+
// providers independently verify and sign the hash.
225+
//
226+
// Here signRewards first retrieves the reward claim data file from the configured URL. It verifies the provided
227+
// merkle root matches the list of claims, checks there is a reward claim for the identity address of this provider,
228+
// with reward amount within expected bounds.
229+
//
230+
// If all checks pass, it signs the merkle root and sends the signature to the SystemsManager contract.
231+
//
232+
// Since reward claim data is currently published manually, and it might take a day or so for the data to be available,
233+
// a retry mechanism is employed with a large retry interval (configurable).
222234
func (c *EpochClient) signRewards(epochId *big.Int) {
223-
logger.Info("Signing rewards for epoch %v", epochId)
224-
hash, weightClaims, err := getRewardsHash(epochId, c.rewardsConfig)
225-
if err != nil {
226-
logger.Error("error obtaining reward hash data for epoch %v, restart client to retry: %s", epochId, err)
227-
return
228-
}
229-
signingResult := <-c.systemsManagerClient.SignRewards(epochId, hash, weightClaims)
230-
if signingResult.Success {
231-
logger.Info("SignRewards completed")
232-
} else {
233-
logger.Error("SignRewards failed %s", signingResult.Message)
234-
}
235+
res := shared.ExecuteWithRetryAttempts(func(i int) (*struct{}, error) {
236+
if c.systemsManagerClient.IsRewardHashSigned(epochId) {
237+
return nil, nil
238+
}
239+
240+
logger.Info("Signing rewards for epoch %v, attempt %d", epochId, i)
241+
242+
data, err := fetchRewardData(epochId, c.rewardsConfig)
243+
if data == nil {
244+
return nil, errors.New("no reward data found")
245+
}
246+
if err != nil {
247+
return nil, errors.Wrapf(err, "unable to fetch reward data for epoch %d", epochId)
248+
}
249+
hash, weightClaims, err := verifyRewardData(epochId, c.identityAddress, data, c.rewardsConfig)
250+
if err != nil {
251+
return nil, errors.Wrapf(err, "reward data verification for epoch %d failed", epochId)
252+
}
253+
signingResult := <-c.systemsManagerClient.SignRewards(epochId, hash, weightClaims)
254+
if !signingResult.Success {
255+
return nil, errors.Errorf("unable to send reward signature")
256+
}
257+
return nil, nil
258+
}, c.rewardsConfig.Retries, c.rewardsConfig.RetryInterval)
259+
260+
// The retry loop may run four hours until the reward data is published, so we don't block for result here.
261+
go func() {
262+
status := <-res
263+
if status.Success {
264+
logger.Info("Signing rewards for epoch %v completed", epochId)
265+
} else {
266+
logger.Info("Signing rewards for epoch %v failed: %s", epochId, status.Message)
267+
}
268+
}()
235269
}

client/epoch/epoch_client_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ func (c testRegistryClient) RegisterVoter(
385385
}, 1, 0)
386386
}
387387

388-
func (c testSystemsManagerClient) SignUptimeVoteEnabledListener(db epochClientDB, epoch *utils.Epoch, i int64) <-chan *system.FlareSystemsManagerSignUptimeVoteEnabled {
388+
func (c testSystemsManagerClient) SignUptimeVoteEnabledListener(db epochClientDB, epoch *utils.Epoch) <-chan *system.FlareSystemsManagerSignUptimeVoteEnabled {
389389
return make(chan *system.FlareSystemsManagerSignUptimeVoteEnabled)
390390
}
391391

@@ -395,7 +395,7 @@ func (c testSystemsManagerClient) SignUptimeVote(b *big.Int) <-chan shared.Execu
395395
}, 1, 0)
396396
}
397397

398-
func (c testSystemsManagerClient) UptimeVoteSignedListener(db epochClientDB, epoch *utils.Epoch, window int64) <-chan *system.FlareSystemsManagerUptimeVoteSigned {
398+
func (c testSystemsManagerClient) UptimeVoteSignedListener(db epochClientDB, epoch *utils.Epoch) <-chan *system.FlareSystemsManagerUptimeVoteSigned {
399399
return make(chan *system.FlareSystemsManagerUptimeVoteSigned)
400400
}
401401

@@ -404,3 +404,7 @@ func (c testSystemsManagerClient) SignRewards(b *big.Int, hash *common.Hash, cla
404404
return nil, nil
405405
}, 1, 0)
406406
}
407+
408+
func (c testSystemsManagerClient) IsRewardHashSigned(b *big.Int) bool {
409+
return false
410+
}

0 commit comments

Comments
 (0)