-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfabfile.py
328 lines (274 loc) · 12 KB
/
fabfile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
import json
import os
from pathlib import Path
from pprint import pprint
import time
from fabric.api import task
import requests
from web3 import Web3
from web3.exceptions import TransactionNotFound
from web3.gas_strategies.time_based import medium_gas_price_strategy
KEEP_RANDOM_BEACON_OPERATOR_JSON_FILE = Path(__file__).parent / 'KeepRandomBeaconOperator.json' # noqa: E501
with KEEP_RANDOM_BEACON_OPERATOR_JSON_FILE.open() as fp:
random_beacon_artifact = json.load(fp)
KEEP_RANDOM_BEACON_OPERATOR_ABI = random_beacon_artifact['abi']
KEEP_RANDOM_BEACON_OPERATOR_ADDRESS = random_beacon_artifact['networks']["1"]["address"] # noqa: E501
KEEP_RANDOM_BEACON_OPERATOR_STATISTICS_JSON_FILE = Path(__file__).parent / 'KeepRandomBeaconOperatorStatistics.json' # noqa: E501
with KEEP_RANDOM_BEACON_OPERATOR_STATISTICS_JSON_FILE.open() as fp:
random_beacon_statistics_artifact = json.load(fp)
KEEP_RANDOM_BEACON_OPERATOR_STATISTICS_ABI = random_beacon_statistics_artifact['abi'] # noqa: E501
KEEP_RANDOM_BEACON_OPERATOR_STATISTICS_ADDRESS = random_beacon_statistics_artifact['networks']["1"]["address"] # noqa: E501
ECDSA_REWARDS_DISTRIBUTOR_JSON_FILE = Path(__file__).parent / 'ECDSARewardsDistributor.json' # noqa: E501
with ECDSA_REWARDS_DISTRIBUTOR_JSON_FILE.open() as fp:
ecdsa_rewards_artifact = json.load(fp)
ECDSA_REWARDS_DISTRIBUTOR_ABI = ecdsa_rewards_artifact['abi']
ECDSA_REWARDS_DISTRIBUTOR_ADDRESS = ecdsa_rewards_artifact['networks']["1"]["address"] # noqa: E501
DAPPNODE_HTTP_RPC_URL = 'http://fullnode.dappnode:8545/'
HTTP_RPC_URL = os.environ.get('HTTP_RPC_URL', DAPPNODE_HTTP_RPC_URL)
PROVIDER = Web3.HTTPProvider(HTTP_RPC_URL)
W3 = Web3(PROVIDER)
W3.eth.setGasPriceStrategy(medium_gas_price_strategy)
ATTEMPTS = 150
# the estimated gas limit could be off, so add 10,000 gas to the limit
GAS_LIMIT_FUDGE_FACTOR = 10000
DRY_RUN = bool(os.environ.get('DRY_RUN', False))
KEEP_RANDOM_BEACON_OPERATOR_CONTRACT = W3.eth.contract(
abi=KEEP_RANDOM_BEACON_OPERATOR_ABI,
address=KEEP_RANDOM_BEACON_OPERATOR_ADDRESS
)
KEEP_RANDOM_BEACON_OPERATOR_CONTRACT_INIT_BLOCK = 10834116
KEEP_RANDOM_BEACON_OPERATOR_STATISTICS_CONTRACT = W3.eth.contract(
abi=KEEP_RANDOM_BEACON_OPERATOR_STATISTICS_ABI,
address=KEEP_RANDOM_BEACON_OPERATOR_STATISTICS_ADDRESS
)
ECDSA_REWARDS_DISTRIBUTOR_CONTRACT = W3.eth.contract(
abi=ECDSA_REWARDS_DISTRIBUTOR_ABI,
address=ECDSA_REWARDS_DISTRIBUTOR_ADDRESS
)
# see: https://github.com/keep-network/keep-core/blob/master/solidity/dashboard/src/rewards-allocation/rewards.json # noqa: E501
REWARDS_DATA_PATH = Path(__file__).parent / 'artifacts/rewards-data.json'
if REWARDS_DATA_PATH.exists():
with REWARDS_DATA_PATH.open() as fp:
REWARDS_DATA = json.load(fp)
else:
REWARDS_DATA = {}
class Group:
def __init__(self, group_pub_key, group_index=None):
self.index = group_index
self.pub_key = group_pub_key
self.operator_functions = KEEP_RANDOM_BEACON_OPERATOR_CONTRACT.functions # noqa: E501
self.statistics_functions = KEEP_RANDOM_BEACON_OPERATOR_STATISTICS_CONTRACT.functions # noqa: E501
@property
def size(self):
return self.operator_functions.groupSize().call()
@property
def members(self):
function = self.operator_functions.getGroupMembers
return function(self.pub_key).call()
@property
def rewards(self):
function = self.operator_functions.getGroupMemberRewards # noqa:E501
return function(self.pub_key).call()
@property
def stale(self):
return self.operator_functions.isStaleGroup(self.pub_key).call()
def operator_groups(operator):
events = KEEP_RANDOM_BEACON_OPERATOR_CONTRACT.events.DkgResultSubmittedEvent.getLogs( # noqa: E501
fromBlock=KEEP_RANDOM_BEACON_OPERATOR_CONTRACT_INIT_BLOCK
)
groups = {}
for group_index, event in enumerate(events):
group = Group(event.args.groupPubKey, group_index)
if operator in group.members:
groups[group_index] = group
return groups
# TODO: change terminology here? rewards mean KEEP
@task
def list_all_beacon_earnings(operator):
"operator_address -> [earnings]"
operator = Web3.toChecksumAddress(operator)
groups = {}
for group_index, group in operator_groups(operator).items():
groups[group_index] = {
'pubkey': group.pub_key,
'earnings': group.rewards / 10**18,
}
pprint(groups)
@task
def list_unclaimed_beacon_earnings(operator):
operator = Web3.toChecksumAddress(operator)
groups = {}
for group_index, group in operator_groups(operator).items():
has_withdrawn = KEEP_RANDOM_BEACON_OPERATOR_CONTRACT.functions.hasWithdrawnRewards( # noqa: E501
operator, group_index
).call()
if group.stale and not has_withdrawn:
groups[group_index] = {
'pubkey': group.pub_key,
'earnings': group.rewards / 10**18,
'group_index': group_index,
}
pprint(groups)
def load_private_key():
if 'PRIVATE_KEY' in os.environ:
skey = os.environ['PRIVATE_KEY']
else:
try:
path = Path(os.environ['PRIVATE_KEY_FILE'])
except KeyError:
raise RuntimeError(
"PRIVATE_KEY_FILE environment variable key not found.\n"
"Set it to the path to the account json file.")
try:
passphrase = os.environ['PRIVATE_KEY_PASSPHRASE']
except KeyError:
raise RuntimeError(
"PRIVATE_KEY_PASSPHRASE environment variable not found\n"
"Set set it to the account's passphrase")
with path.open() as fp:
encrypted_key = fp.read()
skey = W3.eth.account.decrypt(encrypted_key, passphrase)
return skey
def load_claimer_contract():
try:
keep_benefits_address = os.environ['KEEP_CLAIMER_CONTRACT_ADDRESS'] # noqa: E501
except KeyError:
msg = "Error: Must have KEEP_CLAIMER_CONTRACT_ADDRESS environment variable set" # noqa: E501
raise RuntimeError(msg)
# this was created when you initially compiled & deployed the contract
keep_benefits_json_file = Path(__file__).parent / 'keep-benefits/build/contracts/BulkClaimer.json' # noqa: E501
with keep_benefits_json_file.open() as fp:
keep_benefits_abi = json.load(fp)['abi']
contract = W3.eth.contract(
abi=keep_benefits_abi,
address=keep_benefits_address
)
return contract
# TODO: make this *operator
@task
def claim_beacon_earnings(operator):
"first find epochs with unclaimed income, then claim them in bulk"
skey = load_private_key()
account = W3.eth.account.from_key(skey)
benefits_contract = load_claimer_contract()
operator = Web3.toChecksumAddress(operator)
group_indicies = []
for group_index, group in operator_groups(operator).items():
has_withdrawn = KEEP_RANDOM_BEACON_OPERATOR_CONTRACT.functions.hasWithdrawnRewards( # noqa: E501
operator, group_index
).call()
if group.stale and not has_withdrawn:
group_indicies.append(group_index)
print(f'group indicies: {group_indicies}')
contract_call = benefits_contract.functions.claimBeaconEarnings(group_indicies, operator) # noqa: E501
gas_limit = contract_call.estimateGas()
gas_price = W3.eth.generateGasPrice()
tx = contract_call.buildTransaction({
'chainId': W3.eth.chainId,
'gas': gas_limit,
'gasPrice': gas_price,
'nonce': W3.eth.getTransactionCount(account.address)
})
signed_tx = W3.eth.account.sign_transaction(tx, account.key)
tx_hash = W3.eth.sendRawTransaction(signed_tx.rawTransaction)
print(tx_hash)
@task
def get_latest_rewards_data():
"get latest rewards data from keep-network/keep-core github repo"
url = "https://raw.githubusercontent.com/keep-network/keep-core/master/solidity/dashboard/src/rewards-allocation/rewards.json" # noqa: E501
resp = requests.get(url)
resp.raise_for_status()
with REWARDS_DATA_PATH.open('w') as fp:
fp.write(resp.text)
@task
def list_claimed_ecdsa_rewards(operator):
operator = Web3.toChecksumAddress(operator)
events = ECDSA_REWARDS_DISTRIBUTOR_CONTRACT.events.RewardsClaimed.getLogs(
fromBlock=11432833,
argument_filters={'operator': operator}
)
pprint(events)
@task
def list_unclaimed_ecdsa_rewards(operator):
operator = Web3.toChecksumAddress(operator)
events = ECDSA_REWARDS_DISTRIBUTOR_CONTRACT.events.RewardsClaimed.getLogs(
fromBlock=11432833,
argument_filters={'operator': operator}
)
claimed_rewards = set()
for event in events:
# NOTE: this is not totally foolproof
# just very unlikely to have colliding index + amount
claimed_rewards.add((event.args.index, event.args.amount))
for merkle_root, info in REWARDS_DATA.items():
if 'claims' in info:
for claim in info['claims']:
if claim == operator:
amount = eval(info['claims'][claim]['amount'])
index = info['claims'][claim]['index']
if (index, amount) not in claimed_rewards:
print(info['claims'][claim])
@task
def list_all_ecdsa_rewards(operator):
operator = Web3.toChecksumAddress(operator)
for merkle_root, info in REWARDS_DATA.items():
if 'claims' in info:
for claim in info['claims']:
if claim == operator:
print(info['claims'][claim])
def perform_tx(tx, account):
"""internal"""
signed_tx = W3.eth.account.sign_transaction(tx, account.key)
if not DRY_RUN:
tx_hash = W3.eth.sendRawTransaction(signed_tx.rawTransaction)
for i in range(ATTEMPTS + 1):
time.sleep(i)
try:
receipt = W3.eth.getTransactionReceipt(tx_hash)
except TransactionNotFound:
print(f'waiting for {tx_hash.hex()} to be mined')
continue
break
assert receipt['status'] != 0, f'{tx_hash.hex()} failed'
print(f'{tx_hash.hex()} performed successfully')
else:
print('Skipping performing transaction')
@task
def claim_ecdsa_rewards(operator):
operator = Web3.toChecksumAddress(operator)
skey = os.environ['ETHEREUM_PRIVATE_KEY']
account = W3.eth.account.from_key(skey)
events = ECDSA_REWARDS_DISTRIBUTOR_CONTRACT.events.RewardsClaimed.getLogs(
fromBlock=11432833,
argument_filters={'operator': operator}
)
claimed_rewards = set()
for event in events:
# NOTE: this is not totally foolproof
# just very unlikely to have colliding index + amount
claimed_rewards.add((event.args.index, event.args.amount))
for merkle_root, info in REWARDS_DATA.items():
if 'claims' in info:
for claim, data in info['claims'].items():
if claim == operator:
amount = eval(info['claims'][claim]['amount'])
index = info['claims'][claim]['index']
if (index, amount) not in claimed_rewards:
index = data['index']
merkle_proof = data['proof']
contract_call = ECDSA_REWARDS_DISTRIBUTOR_CONTRACT.functions.claim( # noqa: E501
merkle_root,
index,
operator,
amount,
merkle_proof
)
gas_limit = contract_call.estimateGas() + GAS_LIMIT_FUDGE_FACTOR # noqa: E501
gas_price = W3.eth.generateGasPrice()
tx = contract_call.buildTransaction({
'chainId': W3.eth.chainId,
'gas': gas_limit,
'gasPrice': gas_price,
'nonce': W3.eth.getTransactionCount(account.address) # noqa: E501
})
perform_tx(tx, account)