@audius/sdk 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +38 -0
- package/.prettierrc.js +1 -0
- package/.python-version +1 -0
- package/Dockerfile +15 -0
- package/README.md +3 -0
- package/babel.config.js +3 -0
- package/data-contracts/ABIs/AdminUpgradeabilityProxy.json +132 -0
- package/data-contracts/ABIs/BaseAdminUpgradeabilityProxy.json +113 -0
- package/data-contracts/ABIs/BaseUpgradeabilityProxy.json +22 -0
- package/data-contracts/ABIs/DiscoveryProviderFactory.json +189 -0
- package/data-contracts/ABIs/DiscoveryProviderFactoryInterface.json +61 -0
- package/data-contracts/ABIs/DiscoveryProviderStorage.json +205 -0
- package/data-contracts/ABIs/DiscoveryProviderStorageInterface.json +65 -0
- package/data-contracts/ABIs/ECDSA.json +4 -0
- package/data-contracts/ABIs/IPLDBlacklistFactory.json +168 -0
- package/data-contracts/ABIs/Initializable.json +4 -0
- package/data-contracts/ABIs/Migrations.json +67 -0
- package/data-contracts/ABIs/OpenZeppelinUpgradesAddress.json +4 -0
- package/data-contracts/ABIs/Ownable.json +79 -0
- package/data-contracts/ABIs/PlaylistFactory.json +669 -0
- package/data-contracts/ABIs/PlaylistFactoryInterface.json +42 -0
- package/data-contracts/ABIs/PlaylistStorage.json +250 -0
- package/data-contracts/ABIs/PlaylistStorageInterface.json +129 -0
- package/data-contracts/ABIs/Proxy.json +10 -0
- package/data-contracts/ABIs/Registry.json +240 -0
- package/data-contracts/ABIs/RegistryContract.json +102 -0
- package/data-contracts/ABIs/RegistryContractInterface.json +28 -0
- package/data-contracts/ABIs/RegistryInterface.json +66 -0
- package/data-contracts/ABIs/SigningLogic.json +43 -0
- package/data-contracts/ABIs/SigningLogicInitializable.json +46 -0
- package/data-contracts/ABIs/SocialFeatureFactory.json +460 -0
- package/data-contracts/ABIs/SocialFeatureStorage.json +225 -0
- package/data-contracts/ABIs/SocialFeatureStorageInterface.json +123 -0
- package/data-contracts/ABIs/TestContract.json +135 -0
- package/data-contracts/ABIs/TestContractInterface.json +19 -0
- package/data-contracts/ABIs/TestContractWithStorage.json +165 -0
- package/data-contracts/ABIs/TestContractWithStorageInterface.json +24 -0
- package/data-contracts/ABIs/TestStorage.json +144 -0
- package/data-contracts/ABIs/TestStorageInterface.json +42 -0
- package/data-contracts/ABIs/TestUserReplicaSetManager.json +432 -0
- package/data-contracts/ABIs/TrackFactory.json +391 -0
- package/data-contracts/ABIs/TrackFactoryInterface.json +73 -0
- package/data-contracts/ABIs/TrackStorage.json +223 -0
- package/data-contracts/ABIs/TrackStorageInterface.json +121 -0
- package/data-contracts/ABIs/UpgradeabilityProxy.json +37 -0
- package/data-contracts/ABIs/UserFactory.json +657 -0
- package/data-contracts/ABIs/UserFactoryInterface.json +65 -0
- package/data-contracts/ABIs/UserLibraryFactory.json +334 -0
- package/data-contracts/ABIs/UserReplicaSetManager.json +418 -0
- package/data-contracts/ABIs/UserStorage.json +233 -0
- package/data-contracts/ABIs/UserStorageInterface.json +93 -0
- package/data-contracts/signatureSchemas.ts +1236 -0
- package/dist/core.d.ts +446 -0
- package/dist/core.js +769 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +689 -0
- package/dist/index.js +72850 -0
- package/dist/index.js.map +1 -0
- package/eth-contracts/ABIs/Address.json +4 -0
- package/eth-contracts/ABIs/AudiusAdminUpgradeabilityProxy.json +105 -0
- package/eth-contracts/ABIs/AudiusClaimDistributor.json +4968 -0
- package/eth-contracts/ABIs/AudiusToken.json +724 -0
- package/eth-contracts/ABIs/BaseUpgradeabilityProxy.json +23 -0
- package/eth-contracts/ABIs/Checkpointing.json +4 -0
- package/eth-contracts/ABIs/ClaimsManager.json +539 -0
- package/eth-contracts/ABIs/Context.json +11 -0
- package/eth-contracts/ABIs/DelegateManager.json +989 -0
- package/eth-contracts/ABIs/DelegateManagerV2.json +1049 -0
- package/eth-contracts/ABIs/DelegateManagerV2Bad.json +1049 -0
- package/eth-contracts/ABIs/ERC20.json +252 -0
- package/eth-contracts/ABIs/ERC20Burnable.json +287 -0
- package/eth-contracts/ABIs/ERC20Detailed.json +270 -0
- package/eth-contracts/ABIs/ERC20Mintable.json +364 -0
- package/eth-contracts/ABIs/ERC20Pausable.json +397 -0
- package/eth-contracts/ABIs/EthRewardsManager.json +174 -0
- package/eth-contracts/ABIs/Governance.json +938 -0
- package/eth-contracts/ABIs/GovernanceUpgraded.json +953 -0
- package/eth-contracts/ABIs/GovernanceV2.json +938 -0
- package/eth-contracts/ABIs/IERC20.json +200 -0
- package/eth-contracts/ABIs/Initializable.json +4 -0
- package/eth-contracts/ABIs/InitializableV2.json +14 -0
- package/eth-contracts/ABIs/Migrations.json +71 -0
- package/eth-contracts/ABIs/MinterRole.json +91 -0
- package/eth-contracts/ABIs/MockAccount.json +62 -0
- package/eth-contracts/ABIs/MockDelegateManager.json +55 -0
- package/eth-contracts/ABIs/MockStakingCaller.json +259 -0
- package/eth-contracts/ABIs/MockWormhole.json +106 -0
- package/eth-contracts/ABIs/OpenZeppelinUpgradesAddress.json +4 -0
- package/eth-contracts/ABIs/Ownable.json +93 -0
- package/eth-contracts/ABIs/Pausable.json +150 -0
- package/eth-contracts/ABIs/PauserRole.json +91 -0
- package/eth-contracts/ABIs/Proxy.json +10 -0
- package/eth-contracts/ABIs/Registry.json +288 -0
- package/eth-contracts/ABIs/Roles.json +4 -0
- package/eth-contracts/ABIs/SafeERC20.json +4 -0
- package/eth-contracts/ABIs/SafeMath.json +4 -0
- package/eth-contracts/ABIs/ServiceProviderFactory.json +1153 -0
- package/eth-contracts/ABIs/ServiceTypeManager.json +337 -0
- package/eth-contracts/ABIs/Staking.json +555 -0
- package/eth-contracts/ABIs/StakingUpgraded.json +570 -0
- package/eth-contracts/ABIs/TestContract.json +44 -0
- package/eth-contracts/ABIs/TrustedNotifierManager.json +265 -0
- package/eth-contracts/ABIs/Uint256Helpers.json +4 -0
- package/eth-contracts/ABIs/UpgradeabilityProxy.json +40 -0
- package/eth-contracts/ABIs/Wormhole.json +45 -0
- package/eth-contracts/ABIs/WormholeClient.json +155 -0
- package/examples/file.mp3 +0 -0
- package/examples/initAudiusLibs.js +86 -0
- package/examples/initializeVersions.js +95 -0
- package/examples/pic.jpg +0 -0
- package/initScripts/configureLocalDiscProv.js +167 -0
- package/initScripts/helpers/claim.js +43 -0
- package/initScripts/helpers/distributeTokens.js +24 -0
- package/initScripts/helpers/spRegistration.js +138 -0
- package/initScripts/helpers/utils.js +34 -0
- package/initScripts/helpers/version.js +93 -0
- package/initScripts/local.js +617 -0
- package/initScripts/mainnet.js +131 -0
- package/initScripts/manageProdRelayerWallets.js +191 -0
- package/package.json +125 -0
- package/rollup.config.js +164 -0
- package/scripts/AudiusClaimDistributor.json +4968 -0
- package/scripts/Wormhole.json +155 -0
- package/scripts/addCIDToIpldBlacklist.js +124 -0
- package/scripts/circleci-test.sh +53 -0
- package/scripts/communityRewards/transferCommunityRewardsToSolana.js +222 -0
- package/scripts/ipfs.sh +58 -0
- package/scripts/migrate_contracts.sh +25 -0
- package/scripts/reset.sh +65 -0
- package/scripts/test.sh +77 -0
- package/src/api/account.js +670 -0
- package/src/api/base.js +122 -0
- package/src/api/file.js +168 -0
- package/src/api/playlist.js +328 -0
- package/src/api/rewards.d.ts +4 -0
- package/src/api/rewards.js +682 -0
- package/src/api/serviceProvider.js +154 -0
- package/src/api/track.js +604 -0
- package/src/api/user.js +888 -0
- package/src/api/user.test.js +172 -0
- package/src/constants.ts +7 -0
- package/src/core.ts +3 -0
- package/src/index.js +6 -0
- package/src/libs.d.ts +3 -0
- package/src/libs.js +619 -0
- package/src/sanityChecks/addSecondaries.js +40 -0
- package/src/sanityChecks/assignReplicaSetIfNecessary.js +10 -0
- package/src/sanityChecks/index.d.ts +9 -0
- package/src/sanityChecks/index.js +31 -0
- package/src/sanityChecks/isCreator.js +73 -0
- package/src/sanityChecks/needsRecoveryEmail.js +20 -0
- package/src/sanityChecks/rolloverNodes.js +74 -0
- package/src/sanityChecks/sanitizeNodes.js +24 -0
- package/src/sanityChecks/syncNodes.js +28 -0
- package/src/sdk/constants.ts +10 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/oauth/Oauth.ts +265 -0
- package/src/sdk/oauth/index.ts +1 -0
- package/src/sdk/sdk.ts +102 -0
- package/src/service-selection/ServiceSelection.test.ts +320 -0
- package/src/service-selection/ServiceSelection.ts +460 -0
- package/src/service-selection/constants.ts +14 -0
- package/src/service-selection/index.ts +1 -0
- package/src/services/ABIDecoder/AudiusABIDecoder.ts +71 -0
- package/src/services/ABIDecoder/index.ts +1 -0
- package/src/services/comstock/Comstock.ts +39 -0
- package/src/services/comstock/index.ts +1 -0
- package/src/services/contracts/ContractClient.ts +227 -0
- package/src/services/contracts/GovernedContractClient.ts +53 -0
- package/src/services/contracts/ProviderSelection.ts +42 -0
- package/src/services/creatorNode/CreatorNode.ts +1065 -0
- package/src/services/creatorNode/CreatorNodeSelection.test.ts +997 -0
- package/src/services/creatorNode/CreatorNodeSelection.ts +488 -0
- package/src/services/creatorNode/constants.ts +10 -0
- package/src/services/creatorNode/index.ts +2 -0
- package/src/services/dataContracts/AudiusContracts.ts +234 -0
- package/src/services/dataContracts/IPLDBlacklistFactoryClient.ts +73 -0
- package/src/services/dataContracts/PlaylistFactoryClient.ts +370 -0
- package/src/services/dataContracts/RegistryClient.ts +95 -0
- package/src/services/dataContracts/SocialFeatureFactoryClient.ts +196 -0
- package/src/services/dataContracts/TrackFactoryClient.ts +131 -0
- package/src/services/dataContracts/UserFactoryClient.ts +351 -0
- package/src/services/dataContracts/UserLibraryFactoryClient.ts +115 -0
- package/src/services/dataContracts/UserReplicaSetManagerClient.ts +206 -0
- package/src/services/dataContracts/index.ts +1 -0
- package/src/services/discoveryProvider/DiscoveryProvider.ts +1168 -0
- package/src/services/discoveryProvider/DiscoveryProviderSelection.test.ts +536 -0
- package/src/services/discoveryProvider/DiscoveryProviderSelection.ts +383 -0
- package/src/services/discoveryProvider/constants.ts +13 -0
- package/src/services/discoveryProvider/index.ts +1 -0
- package/src/services/discoveryProvider/requests.ts +629 -0
- package/src/services/ethContracts/AudiusTokenClient.ts +163 -0
- package/src/services/ethContracts/ClaimDistributionClient.ts +45 -0
- package/src/services/ethContracts/ClaimsManagerClient.ts +102 -0
- package/src/services/ethContracts/DelegateManagerClient.ts +480 -0
- package/src/services/ethContracts/EthContracts.ts +359 -0
- package/src/services/ethContracts/EthRewardsManagerClient.ts +33 -0
- package/src/services/ethContracts/GovernanceClient.ts +451 -0
- package/src/services/ethContracts/RegistryClient.ts +33 -0
- package/src/services/ethContracts/ServiceProviderFactoryClient.ts +691 -0
- package/src/services/ethContracts/ServiceTypeManagerClient.ts +112 -0
- package/src/services/ethContracts/StakingProxyClient.ts +97 -0
- package/src/services/ethContracts/TrustedNotifierManagerClient.ts +101 -0
- package/src/services/ethContracts/WormholeClient.ts +97 -0
- package/src/services/ethContracts/index.ts +1 -0
- package/src/services/ethWeb3Manager/EthWeb3Manager.ts +239 -0
- package/src/services/ethWeb3Manager/index.ts +1 -0
- package/src/services/hedgehog/Hedgehog.ts +96 -0
- package/src/services/hedgehog/index.ts +1 -0
- package/src/services/identity/IdentityService.ts +551 -0
- package/src/services/identity/index.ts +1 -0
- package/src/services/identity/requests.ts +65 -0
- package/src/services/schemaValidator/SchemaValidator.ts +105 -0
- package/src/services/schemaValidator/index.ts +1 -0
- package/src/services/schemaValidator/schemas/trackSchema.json +267 -0
- package/src/services/schemaValidator/schemas/userSchema.json +230 -0
- package/src/services/solanaAudiusData/errors.ts +20 -0
- package/src/services/solanaAudiusData/index.ts +1189 -0
- package/src/services/solanaWeb3Manager/errors.js +101 -0
- package/src/services/solanaWeb3Manager/index.d.ts +46 -0
- package/src/services/solanaWeb3Manager/index.js +655 -0
- package/src/services/solanaWeb3Manager/padBNToUint8Array.ts +7 -0
- package/src/services/solanaWeb3Manager/rewards.js +941 -0
- package/src/services/solanaWeb3Manager/rewardsAttester.ts +1093 -0
- package/src/services/solanaWeb3Manager/tokenAccount.js +149 -0
- package/src/services/solanaWeb3Manager/transactionHandler.js +345 -0
- package/src/services/solanaWeb3Manager/transfer.js +272 -0
- package/src/services/solanaWeb3Manager/userBank.js +160 -0
- package/src/services/solanaWeb3Manager/utils.d.ts +31 -0
- package/src/services/solanaWeb3Manager/utils.js +163 -0
- package/src/services/solanaWeb3Manager/wAudio.js +28 -0
- package/src/services/solanaWeb3Manager/wAudio.test.js +30 -0
- package/src/services/web3Manager/Web3Config.ts +14 -0
- package/src/services/web3Manager/Web3Manager.ts +360 -0
- package/src/services/web3Manager/XMLHttpRequest.ts +11 -0
- package/src/services/web3Manager/index.ts +2 -0
- package/src/services/wormhole/index.js +424 -0
- package/src/types.ts +8 -0
- package/src/userStateManager.ts +53 -0
- package/src/utils/apiSigning.ts +51 -0
- package/src/utils/captcha.ts +97 -0
- package/src/utils/estimateGas.ts +64 -0
- package/src/utils/fileHasher.ts +278 -0
- package/src/utils/importContractABI.d.ts +9 -0
- package/src/utils/importContractABI.js +19 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/multiProvider.ts +72 -0
- package/src/utils/network.test.ts +127 -0
- package/src/utils/network.ts +308 -0
- package/src/utils/promiseFight.test.ts +87 -0
- package/src/utils/promiseFight.ts +36 -0
- package/src/utils/signatures.ts +139 -0
- package/src/utils/types.ts +34 -0
- package/src/utils/utils.test.ts +36 -0
- package/src/utils/utils.ts +235 -0
- package/src/utils/uuid.ts +14 -0
- package/src/web3.d.ts +9 -0
- package/src/web3.js +8 -0
- package/tests/assets/static_image.png +0 -0
- package/tests/assets/static_text.txt +1 -0
- package/tests/audiusTokenClientTest.js +37 -0
- package/tests/creatorNodeTest.js +19 -0
- package/tests/fileHasherTest.js +125 -0
- package/tests/governanceTest.js +382 -0
- package/tests/helpers.js +105 -0
- package/tests/index.js +14 -0
- package/tests/playlistClientTest.js +157 -0
- package/tests/providerSelectionTest.js +241 -0
- package/tests/registryClientTest.js +19 -0
- package/tests/rewardsAttesterTest.js +373 -0
- package/tests/serviceTypeManagerClientTest.js +33 -0
- package/tests/socialFeatureClientTest.js +79 -0
- package/tests/stakingTest.js +302 -0
- package/tests/trackClientTest.js +86 -0
- package/tests/userClientTest.js +121 -0
- package/tsconfig.json +10 -0
- package/types/@audius-hedgehog/index.d.ts +39 -0
- package/types/abi-decoder/index.d.ts +41 -0
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
import { SubmitAndEvaluateError } from '../../api/rewards'
|
|
2
|
+
import type { ServiceWithEndpoint } from '../../utils'
|
|
3
|
+
import { Utils } from '../../utils/utils'
|
|
4
|
+
|
|
5
|
+
const { decodeHashId } = Utils
|
|
6
|
+
|
|
7
|
+
// `BaseRewardsReporter` is intended to be subclassed, and provides
|
|
8
|
+
// "reporting" functionality to RewardsAttester (i.e. posts to Slack if something notable happens)
|
|
9
|
+
class BaseRewardsReporter {
|
|
10
|
+
async reportSuccess(_: {
|
|
11
|
+
userId: number
|
|
12
|
+
challengeId: string
|
|
13
|
+
amount: number
|
|
14
|
+
specifier: string
|
|
15
|
+
}): Promise<void> {}
|
|
16
|
+
|
|
17
|
+
async reportRetry(_: {
|
|
18
|
+
userId: number
|
|
19
|
+
challengeId: string
|
|
20
|
+
amount: number
|
|
21
|
+
error: string
|
|
22
|
+
phase: string
|
|
23
|
+
}): Promise<void> {}
|
|
24
|
+
|
|
25
|
+
async reportFailure(_: {
|
|
26
|
+
userId: number
|
|
27
|
+
challengeId: string
|
|
28
|
+
amount: number
|
|
29
|
+
error: string
|
|
30
|
+
phase: string
|
|
31
|
+
}): Promise<void> {}
|
|
32
|
+
|
|
33
|
+
async reportAAORejection(_: {
|
|
34
|
+
userId: number
|
|
35
|
+
challengeId: string
|
|
36
|
+
amount: number
|
|
37
|
+
error: string
|
|
38
|
+
reason: string
|
|
39
|
+
}): Promise<void> {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const MAX_DISBURSED_CACHE_SIZE = 100
|
|
43
|
+
const SOLANA_EST_SEC_PER_SLOT = 0.5
|
|
44
|
+
const POA_SEC_PER_BLOCK = 5
|
|
45
|
+
const MAX_DISCOVERY_NODE_BLOCKLIST_LEN = 10
|
|
46
|
+
|
|
47
|
+
type ATTESTER_PHASE =
|
|
48
|
+
| 'HALTED'
|
|
49
|
+
| 'SELECTING_NODES'
|
|
50
|
+
| 'REFILLING_QUEUE'
|
|
51
|
+
| 'ATTESTING'
|
|
52
|
+
| 'SLEEPING'
|
|
53
|
+
| 'RETRY_BACKOFF'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Class to encapsulate logic for calculating disbursement delay thresholds.
|
|
57
|
+
* Periodically polls Solana to get slot production rate.
|
|
58
|
+
* Caches old values (`allowedStalenessSec`) for current POA block & Solana slot to reduce RPC
|
|
59
|
+
* overhead.
|
|
60
|
+
*
|
|
61
|
+
* Exposes `getPOABlockThreshold` and `getSolanaSlotThreshold`
|
|
62
|
+
*
|
|
63
|
+
* @class ThresholdCalculator
|
|
64
|
+
*/
|
|
65
|
+
export class AttestationDelayCalculator {
|
|
66
|
+
libs: any
|
|
67
|
+
solanaSecPerSlot: number
|
|
68
|
+
runBehindSec: number
|
|
69
|
+
lastSolanaThreshold: { threshold: number; time: number } | null
|
|
70
|
+
lastPOAThreshold: { threshold: number; time: number } | null
|
|
71
|
+
allowedStalenessSec: number
|
|
72
|
+
solanaPollingInterval: number
|
|
73
|
+
logger: any
|
|
74
|
+
intervalHandle: NodeJS.Timer | null
|
|
75
|
+
|
|
76
|
+
constructor({
|
|
77
|
+
libs,
|
|
78
|
+
runBehindSec,
|
|
79
|
+
allowedStalenessSec,
|
|
80
|
+
solanaPollingInterval = 30,
|
|
81
|
+
logger = console
|
|
82
|
+
}: {
|
|
83
|
+
libs: any
|
|
84
|
+
runBehindSec: number
|
|
85
|
+
allowedStalenessSec: number
|
|
86
|
+
solanaPollingInterval?: number
|
|
87
|
+
logger: any
|
|
88
|
+
}) {
|
|
89
|
+
this.libs = libs
|
|
90
|
+
this.solanaSecPerSlot = SOLANA_EST_SEC_PER_SLOT
|
|
91
|
+
this.runBehindSec = runBehindSec
|
|
92
|
+
this.lastSolanaThreshold = null
|
|
93
|
+
this.lastPOAThreshold = null
|
|
94
|
+
this.allowedStalenessSec = allowedStalenessSec
|
|
95
|
+
this.solanaPollingInterval = solanaPollingInterval
|
|
96
|
+
this.logger = logger
|
|
97
|
+
this.intervalHandle = null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async start() {
|
|
101
|
+
// Begin Solana slot rate polling
|
|
102
|
+
let oldSlot = await this.libs.solanaWeb3Manager.getSlot()
|
|
103
|
+
// eslint-disable-next-line
|
|
104
|
+
this.intervalHandle = setInterval(async () => {
|
|
105
|
+
const newSlot = await this.libs.solanaWeb3Manager.getSlot()
|
|
106
|
+
const diff = this.solanaPollingInterval / (newSlot - oldSlot)
|
|
107
|
+
this.solanaSecPerSlot = diff
|
|
108
|
+
this.logger.info(`Setting Solana seconds per slot to ${diff}`)
|
|
109
|
+
oldSlot = newSlot
|
|
110
|
+
}, this.solanaPollingInterval * 1000)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
stop() {
|
|
114
|
+
if (this.intervalHandle) {
|
|
115
|
+
clearInterval(this.intervalHandle)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async getPOABlockThreshold() {
|
|
120
|
+
// Use cached value if possible
|
|
121
|
+
if (
|
|
122
|
+
this.lastPOAThreshold &&
|
|
123
|
+
(Date.now() - this.lastPOAThreshold.time) / 1000 <
|
|
124
|
+
this.allowedStalenessSec
|
|
125
|
+
) {
|
|
126
|
+
return this.lastPOAThreshold.threshold
|
|
127
|
+
}
|
|
128
|
+
const currentBlock = await this.libs.web3Manager
|
|
129
|
+
.getWeb3()
|
|
130
|
+
.eth.getBlockNumber()
|
|
131
|
+
const threshold = currentBlock - this.runBehindSec / POA_SEC_PER_BLOCK
|
|
132
|
+
this.lastPOAThreshold = {
|
|
133
|
+
threshold,
|
|
134
|
+
time: Date.now()
|
|
135
|
+
}
|
|
136
|
+
return threshold
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async getSolanaSlotThreshold() {
|
|
140
|
+
// Use cached value if possible
|
|
141
|
+
if (
|
|
142
|
+
this.lastSolanaThreshold &&
|
|
143
|
+
(Date.now() - this.lastSolanaThreshold.time) / 1000 <
|
|
144
|
+
this.allowedStalenessSec
|
|
145
|
+
) {
|
|
146
|
+
return this.lastSolanaThreshold.threshold
|
|
147
|
+
}
|
|
148
|
+
const currentSlot = await this.libs.solanaWeb3Manager.getSlot()
|
|
149
|
+
const threshold = currentSlot - this.runBehindSec / this.solanaSecPerSlot
|
|
150
|
+
this.lastSolanaThreshold = {
|
|
151
|
+
threshold,
|
|
152
|
+
time: Date.now()
|
|
153
|
+
}
|
|
154
|
+
return threshold
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type ConstructorArgs = {
|
|
159
|
+
libs: any
|
|
160
|
+
startingBlock: number
|
|
161
|
+
offset: number
|
|
162
|
+
parallelization: number
|
|
163
|
+
logger?: any
|
|
164
|
+
quorumSize: number
|
|
165
|
+
aaoEndpoint: string
|
|
166
|
+
aaoAddress: string
|
|
167
|
+
updateValues: ({
|
|
168
|
+
startingBlock,
|
|
169
|
+
offset,
|
|
170
|
+
successCount
|
|
171
|
+
}: {
|
|
172
|
+
startingBlock: number
|
|
173
|
+
offset: number
|
|
174
|
+
successCount: number
|
|
175
|
+
}) => void
|
|
176
|
+
getStartingBlockOverride: () => Promise<number | null> | number | null
|
|
177
|
+
maxRetries: number
|
|
178
|
+
reporter?: BaseRewardsReporter
|
|
179
|
+
challengeIdsDenyList: string[]
|
|
180
|
+
endpoints?: string[]
|
|
181
|
+
runBehindSec?: number
|
|
182
|
+
isSolanaChallenge?: (challenge: string) => boolean
|
|
183
|
+
feePayerOverride: string | null
|
|
184
|
+
maxAggregationAttempts?: number
|
|
185
|
+
updateStateCallback?: (state: AttesterState) => Promise<void>
|
|
186
|
+
maxCooldownMsec?: number
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type Challenge = {
|
|
190
|
+
challengeId: string
|
|
191
|
+
userId: string
|
|
192
|
+
specifier: string
|
|
193
|
+
amount: number
|
|
194
|
+
handle: string
|
|
195
|
+
wallet: string
|
|
196
|
+
completedBlocknumber: number
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type AttestationResult = Challenge & {
|
|
200
|
+
error?: string
|
|
201
|
+
phase?: string
|
|
202
|
+
nodesToReselect?: string[] | null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
type DiscoveryNodeChallenge = {
|
|
206
|
+
challenge_id: string
|
|
207
|
+
user_id: string
|
|
208
|
+
specifier: string
|
|
209
|
+
amount: number
|
|
210
|
+
handle: string
|
|
211
|
+
wallet: string
|
|
212
|
+
completed_blocknumber: number
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
type AttesterState = {
|
|
216
|
+
phase: ATTESTER_PHASE
|
|
217
|
+
lastSuccessChallengeTime: number | null
|
|
218
|
+
lastChallengeTime: number | null
|
|
219
|
+
lastActionTime: number
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* `RewardsAttester` is responsible for repeatedly attesting for completed rewards.
|
|
224
|
+
*
|
|
225
|
+
* **Implementation**
|
|
226
|
+
*
|
|
227
|
+
* `RewardsAttester` attempts to attest for `parallelization` rewards in parallel.
|
|
228
|
+
* It won't move onto the next batch of rewards until every reward in that batch has
|
|
229
|
+
* either succeeded or failed attestation. It retries errors that might be due to DN
|
|
230
|
+
* timing issues, and skips AAO errors and some Solana program errors.
|
|
231
|
+
*
|
|
232
|
+
* Internally, state is tracked with two variables: `offset` and `startingBlock`.
|
|
233
|
+
* `startingBlock` represents which block it start requesting attestations from, while `offset` determines
|
|
234
|
+
* where within those results we offset. AAO rejected rewards
|
|
235
|
+
* are never cleared from the DN rewards queue, so we have to move past them either with `offset` or `startingBlock`.
|
|
236
|
+
* `RewardsAttester` accepts callbacks (`updateValues`) for a client to persist these values periodically.
|
|
237
|
+
*
|
|
238
|
+
* RewardsAttester will fetch a single large list of undisbursed rewards (`undisbursedQueue`), and
|
|
239
|
+
* process that entire list before fetching new undisbursed rewards. It also maintains a list of
|
|
240
|
+
* recently processed rewards, and filters those out when re-fetching new undisbursed rewards.
|
|
241
|
+
*/
|
|
242
|
+
export class RewardsAttester {
|
|
243
|
+
private startingBlock: number
|
|
244
|
+
private offset: number
|
|
245
|
+
// Stores a set of identifiers representing
|
|
246
|
+
// recently disbursed challenges.
|
|
247
|
+
// Stored as an array to make it simpler to prune
|
|
248
|
+
// old entries
|
|
249
|
+
private recentlyDisbursedQueue: string[]
|
|
250
|
+
private _shouldStop: boolean
|
|
251
|
+
private endpoints: string[]
|
|
252
|
+
// Stores a queue of undisbursed challenges
|
|
253
|
+
private undisbursedQueue: Challenge[]
|
|
254
|
+
private attesterState: AttesterState
|
|
255
|
+
private parallelization: number
|
|
256
|
+
private aaoEndpoint: string
|
|
257
|
+
private aaoAddress: string
|
|
258
|
+
private endpointPool: Set<string>
|
|
259
|
+
private challengeIdsDenyList: Set<string>
|
|
260
|
+
private discoveryNodeBlocklist: string[]
|
|
261
|
+
|
|
262
|
+
private readonly libs: any
|
|
263
|
+
private readonly logger: Console
|
|
264
|
+
private readonly quorumSize: number
|
|
265
|
+
private readonly reporter: BaseRewardsReporter
|
|
266
|
+
private readonly maxRetries: number
|
|
267
|
+
private readonly maxAggregationAttempts: number
|
|
268
|
+
private readonly updateValues: (args: {
|
|
269
|
+
startingBlock: number
|
|
270
|
+
offset: number
|
|
271
|
+
successCount: number
|
|
272
|
+
}) => void
|
|
273
|
+
|
|
274
|
+
// How long wait wait before retrying
|
|
275
|
+
private readonly cooldownMsec: number
|
|
276
|
+
// How much we increase the cooldown between attempts:
|
|
277
|
+
// coolDown = min(cooldownMsec * backoffExponent ^ retryCount, maxCooldownMsec)
|
|
278
|
+
private readonly backoffExponent: number
|
|
279
|
+
// Maximum time to wait before retrying
|
|
280
|
+
private readonly maxCooldownMsec: number
|
|
281
|
+
// Maximum number of retries before moving on
|
|
282
|
+
// Get override starting block for manually setting indexing start
|
|
283
|
+
private readonly getStartingBlockOverride: () =>
|
|
284
|
+
| Promise<number | null>
|
|
285
|
+
| number
|
|
286
|
+
| null
|
|
287
|
+
|
|
288
|
+
private readonly feePayerOverride: string | null
|
|
289
|
+
|
|
290
|
+
// Calculate delay
|
|
291
|
+
private readonly delayCalculator: AttestationDelayCalculator
|
|
292
|
+
private readonly isSolanaChallenge: (challenge: string) => boolean
|
|
293
|
+
private readonly _updateStateCallback: (state: AttesterState) => Promise<void>
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Creates an instance of RewardsAttester.
|
|
297
|
+
* @memberof RewardsAttester
|
|
298
|
+
*/
|
|
299
|
+
constructor({
|
|
300
|
+
libs,
|
|
301
|
+
startingBlock,
|
|
302
|
+
offset,
|
|
303
|
+
parallelization,
|
|
304
|
+
logger = console,
|
|
305
|
+
quorumSize,
|
|
306
|
+
aaoEndpoint,
|
|
307
|
+
aaoAddress,
|
|
308
|
+
updateValues = () => {},
|
|
309
|
+
getStartingBlockOverride = () => null,
|
|
310
|
+
maxRetries = 5,
|
|
311
|
+
reporter = new BaseRewardsReporter(),
|
|
312
|
+
challengeIdsDenyList = [],
|
|
313
|
+
endpoints = [],
|
|
314
|
+
runBehindSec = 0,
|
|
315
|
+
isSolanaChallenge = (_) => true,
|
|
316
|
+
feePayerOverride = null,
|
|
317
|
+
maxAggregationAttempts = 20,
|
|
318
|
+
updateStateCallback = async (_) => {},
|
|
319
|
+
maxCooldownMsec = 15000
|
|
320
|
+
}: ConstructorArgs) {
|
|
321
|
+
this.libs = libs
|
|
322
|
+
this.logger = logger
|
|
323
|
+
this.parallelization = parallelization
|
|
324
|
+
this.startingBlock = startingBlock
|
|
325
|
+
this.offset = offset
|
|
326
|
+
this.quorumSize = quorumSize
|
|
327
|
+
this.aaoEndpoint = aaoEndpoint
|
|
328
|
+
this.aaoAddress = aaoAddress
|
|
329
|
+
this.reporter = reporter
|
|
330
|
+
this.endpoints = endpoints
|
|
331
|
+
this.endpointPool = new Set(endpoints)
|
|
332
|
+
this.maxRetries = maxRetries
|
|
333
|
+
this.maxAggregationAttempts = maxAggregationAttempts
|
|
334
|
+
this.updateValues = updateValues
|
|
335
|
+
this.challengeIdsDenyList = new Set(...challengeIdsDenyList)
|
|
336
|
+
this.undisbursedQueue = []
|
|
337
|
+
this.recentlyDisbursedQueue = []
|
|
338
|
+
this.cooldownMsec = 2000
|
|
339
|
+
this.backoffExponent = 1.8
|
|
340
|
+
this.maxCooldownMsec = maxCooldownMsec
|
|
341
|
+
this.getStartingBlockOverride = getStartingBlockOverride
|
|
342
|
+
this.feePayerOverride = feePayerOverride
|
|
343
|
+
this.attesterState = {
|
|
344
|
+
phase: 'HALTED',
|
|
345
|
+
lastSuccessChallengeTime: null,
|
|
346
|
+
lastChallengeTime: null,
|
|
347
|
+
lastActionTime: Date.now()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Calculate delay
|
|
351
|
+
this.delayCalculator = new AttestationDelayCalculator({
|
|
352
|
+
libs,
|
|
353
|
+
runBehindSec,
|
|
354
|
+
logger,
|
|
355
|
+
allowedStalenessSec: 5
|
|
356
|
+
})
|
|
357
|
+
this.isSolanaChallenge = isSolanaChallenge
|
|
358
|
+
|
|
359
|
+
this._performSingleAttestation = this._performSingleAttestation.bind(this)
|
|
360
|
+
this._disbursementToKey = this._disbursementToKey.bind(this)
|
|
361
|
+
this._shouldStop = false
|
|
362
|
+
this._updateStateCallback = updateStateCallback
|
|
363
|
+
this.discoveryNodeBlocklist = []
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Begin attestation loop. Entry point for identity attestations
|
|
368
|
+
*
|
|
369
|
+
* @memberof RewardsAttester
|
|
370
|
+
*/
|
|
371
|
+
async start() {
|
|
372
|
+
this.logger.info(`Starting attester with:
|
|
373
|
+
quorum size: ${this.quorumSize}, \
|
|
374
|
+
parallelization: ${this.parallelization} \
|
|
375
|
+
AAO endpoint: ${this.aaoEndpoint} \
|
|
376
|
+
AAO address: ${this.aaoAddress} \
|
|
377
|
+
endpoints: ${this.endpoints}
|
|
378
|
+
`)
|
|
379
|
+
|
|
380
|
+
// If a list of endpoints was not specified,
|
|
381
|
+
// set the pool to the entire list of discovery providers.
|
|
382
|
+
// This overrides any configured whitelist for the service selector.
|
|
383
|
+
if (this.endpointPool.size === 0) {
|
|
384
|
+
const pool =
|
|
385
|
+
await this.libs.discoveryProvider.serviceSelector.getServices()
|
|
386
|
+
this.endpointPool = new Set(pool)
|
|
387
|
+
}
|
|
388
|
+
await this._selectDiscoveryNodes()
|
|
389
|
+
await this.delayCalculator.start()
|
|
390
|
+
|
|
391
|
+
while (!this._shouldStop) {
|
|
392
|
+
try {
|
|
393
|
+
await this._awaitFeePayerBalance()
|
|
394
|
+
await this._checkForStartingBlockOverride()
|
|
395
|
+
|
|
396
|
+
// Refill queue if necessary, returning early if error
|
|
397
|
+
const { error } = await this._refillQueueIfNecessary()
|
|
398
|
+
if (error) {
|
|
399
|
+
this.logger.error(`Got error trying to refill challenges: [${error}]`)
|
|
400
|
+
throw new Error(error)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// If queue is still empty, sleep and return
|
|
404
|
+
if (!this.undisbursedQueue.length) {
|
|
405
|
+
this.logger.info('No undisbursed challenges. Sleeping...')
|
|
406
|
+
await this._updatePhase('SLEEPING')
|
|
407
|
+
await this._delay(1000)
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Get undisbursed rewards
|
|
412
|
+
const toAttest = this.undisbursedQueue.splice(0, this.parallelization)
|
|
413
|
+
|
|
414
|
+
// Attest for batch in parallel
|
|
415
|
+
const { highestBlock, offset, successCount } =
|
|
416
|
+
await this._attestInParallel(toAttest)
|
|
417
|
+
|
|
418
|
+
// Set state
|
|
419
|
+
// Set offset:
|
|
420
|
+
// - If same startingBlock as before, add offset
|
|
421
|
+
// - If new startingBlock, set offset
|
|
422
|
+
if (highestBlock && this.startingBlock === highestBlock - 1) {
|
|
423
|
+
this.offset += offset
|
|
424
|
+
} else {
|
|
425
|
+
this.offset = offset
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.logger.info(
|
|
429
|
+
`Updating values: startingBlock: ${this.startingBlock}, offset: ${this.offset}`
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
this.startingBlock = highestBlock
|
|
433
|
+
? highestBlock - 1
|
|
434
|
+
: this.startingBlock
|
|
435
|
+
|
|
436
|
+
// Set the recently disbursed set
|
|
437
|
+
this._addRecentlyDisbursed(toAttest)
|
|
438
|
+
|
|
439
|
+
// run the `updateValues` callback
|
|
440
|
+
await this.updateValues({
|
|
441
|
+
startingBlock: this.startingBlock,
|
|
442
|
+
offset: this.offset,
|
|
443
|
+
successCount
|
|
444
|
+
})
|
|
445
|
+
} catch (e) {
|
|
446
|
+
this.logger.error(`Got error: ${e}, sleeping`)
|
|
447
|
+
await this._delay(1000)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this._shouldStop = false
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async stop() {
|
|
455
|
+
this._shouldStop = true
|
|
456
|
+
this.delayCalculator.stop()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Called from the client to attest challenges
|
|
461
|
+
*/
|
|
462
|
+
async processChallenges(challenges: Challenge[]) {
|
|
463
|
+
await this._selectDiscoveryNodes()
|
|
464
|
+
const toProcess = [...challenges]
|
|
465
|
+
while (toProcess.length) {
|
|
466
|
+
try {
|
|
467
|
+
this.logger.info(`Processing ${toProcess.length} challenges`)
|
|
468
|
+
const toAttest = toProcess.splice(0, this.parallelization)
|
|
469
|
+
const { accumulatedErrors: errors } = await this._attestInParallel(
|
|
470
|
+
toAttest
|
|
471
|
+
)
|
|
472
|
+
if (errors?.length) {
|
|
473
|
+
this.logger.error(
|
|
474
|
+
`Got errors in processChallenges: ${JSON.stringify(errors)}`
|
|
475
|
+
)
|
|
476
|
+
return { errors }
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
this.logger.error(`Got error: ${e}, sleeping`)
|
|
480
|
+
await this._delay(1000)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return {}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Updates attester config
|
|
488
|
+
*
|
|
489
|
+
* @memberof RewardsAttester
|
|
490
|
+
*/
|
|
491
|
+
updateConfig({
|
|
492
|
+
aaoEndpoint,
|
|
493
|
+
aaoAddress,
|
|
494
|
+
endpoints,
|
|
495
|
+
challengeIdsDenyList,
|
|
496
|
+
parallelization
|
|
497
|
+
}: {
|
|
498
|
+
aaoEndpoint: string
|
|
499
|
+
aaoAddress: string
|
|
500
|
+
endpoints: string[]
|
|
501
|
+
challengeIdsDenyList: string[]
|
|
502
|
+
parallelization: number
|
|
503
|
+
}) {
|
|
504
|
+
this.logger.info(
|
|
505
|
+
`Updating attester with config aaoEndpoint: ${aaoEndpoint}, aaoAddress: ${aaoAddress}, endpoints: ${endpoints}, challengeIdsDenyList: ${challengeIdsDenyList}, parallelization: ${parallelization}`
|
|
506
|
+
)
|
|
507
|
+
this.aaoEndpoint = aaoEndpoint || this.aaoEndpoint
|
|
508
|
+
this.aaoAddress = aaoAddress || this.aaoAddress
|
|
509
|
+
this.endpoints = endpoints || this.endpoints
|
|
510
|
+
this.challengeIdsDenyList = challengeIdsDenyList
|
|
511
|
+
? new Set(...challengeIdsDenyList)
|
|
512
|
+
: this.challengeIdsDenyList
|
|
513
|
+
this.parallelization = parallelization || this.parallelization
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Sleeps until the feePayer has a usable Sol balance.
|
|
518
|
+
*
|
|
519
|
+
* @memberof RewardsAttester
|
|
520
|
+
*/
|
|
521
|
+
async _awaitFeePayerBalance() {
|
|
522
|
+
const getHasBalance = async () =>
|
|
523
|
+
this.libs.solanaWeb3Manager.hasBalance({
|
|
524
|
+
publicKey: this.libs.solanaWeb3Manager.feePayerKey
|
|
525
|
+
})
|
|
526
|
+
while (!(await getHasBalance())) {
|
|
527
|
+
this.logger.warn('No usable balance. Waiting...')
|
|
528
|
+
await this._delay(2000)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Returns the override feePayer if set, otherwise a random fee payer from among the list of existing fee payers.
|
|
534
|
+
*
|
|
535
|
+
* @memberof RewardsAttester
|
|
536
|
+
*/
|
|
537
|
+
_getFeePayer() {
|
|
538
|
+
if (this.feePayerOverride) {
|
|
539
|
+
return this.feePayerOverride
|
|
540
|
+
}
|
|
541
|
+
const feePayerKeypairs =
|
|
542
|
+
this.libs.solanaWeb3Manager.solanaWeb3Config.feePayerKeypairs
|
|
543
|
+
if (feePayerKeypairs?.length) {
|
|
544
|
+
const randomFeePayerIndex = Math.floor(
|
|
545
|
+
Math.random() * feePayerKeypairs.length
|
|
546
|
+
)
|
|
547
|
+
return feePayerKeypairs[randomFeePayerIndex].publicKey
|
|
548
|
+
}
|
|
549
|
+
return null
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Escape hatch for manually setting starting block.
|
|
554
|
+
*
|
|
555
|
+
* @memberof RewardsAttester
|
|
556
|
+
*/
|
|
557
|
+
async _checkForStartingBlockOverride() {
|
|
558
|
+
const override = await this.getStartingBlockOverride()
|
|
559
|
+
// Careful with 0...
|
|
560
|
+
if (override === null || override === undefined) return
|
|
561
|
+
this.logger.info(
|
|
562
|
+
`Setting starting block override: ${override}, emptying recent disbursed queue`
|
|
563
|
+
)
|
|
564
|
+
this.startingBlock = override
|
|
565
|
+
this.offset = 0
|
|
566
|
+
this.recentlyDisbursedQueue = []
|
|
567
|
+
this.undisbursedQueue = []
|
|
568
|
+
this.discoveryNodeBlocklist = []
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Main method to attest for a bucket of challenges in parallel.
|
|
573
|
+
*
|
|
574
|
+
* Algorithm:
|
|
575
|
+
* - Gets `this.parallelization` undisbursed challenges from the queue, refilling it from DN if necessary.
|
|
576
|
+
* - Call `_performSingleAttestation` on those in parallel.
|
|
577
|
+
* - For challenges that failed, either keep retrying or discard them, depending on the error.
|
|
578
|
+
* - Set offset and startingBlock
|
|
579
|
+
*
|
|
580
|
+
* @memberof RewardsAttester
|
|
581
|
+
*/
|
|
582
|
+
async _attestInParallel(toAttest: Challenge[]) {
|
|
583
|
+
this.logger.info(
|
|
584
|
+
`Attesting in parallel with startingBlock: ${this.startingBlock}, offset: ${this.offset}, parallelization: ${this.parallelization}`
|
|
585
|
+
)
|
|
586
|
+
await this._updatePhase('ATTESTING')
|
|
587
|
+
// Get the highest block number, ignoring Solana based challenges (i.e. listens) which have a significantly higher
|
|
588
|
+
// slot and throw off this calculation.
|
|
589
|
+
// TODO: [AUD-1217] we should handle this in a less hacky way, possibly by
|
|
590
|
+
// attesting for Solana + POA challenges separately.
|
|
591
|
+
const poaAttestations = toAttest.filter(
|
|
592
|
+
({ challengeId }) => !this.isSolanaChallenge(challengeId)
|
|
593
|
+
)
|
|
594
|
+
const highestBlock = poaAttestations.length
|
|
595
|
+
? Math.max(...poaAttestations.map((e) => e.completedBlocknumber))
|
|
596
|
+
: null
|
|
597
|
+
|
|
598
|
+
let retryCount = 0
|
|
599
|
+
let successful: AttestationResult[] = []
|
|
600
|
+
let noRetry: AttestationResult[] = []
|
|
601
|
+
let needsAttestation: AttestationResult[] = toAttest
|
|
602
|
+
let shouldReselect = false
|
|
603
|
+
let accumulatedErrors: AttestationResult[] = []
|
|
604
|
+
let successCount = 0
|
|
605
|
+
let offset = 0
|
|
606
|
+
let failingNodes: string[] = []
|
|
607
|
+
|
|
608
|
+
do {
|
|
609
|
+
// Attempt to attest in a single sweep
|
|
610
|
+
await this._updatePhase('ATTESTING')
|
|
611
|
+
if (retryCount !== 0) {
|
|
612
|
+
await this._backoff(retryCount)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
this.logger.info(
|
|
616
|
+
`Attestation attempt ${retryCount + 1}, max ${this.maxRetries}`
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
if (shouldReselect) {
|
|
620
|
+
await this._selectDiscoveryNodes()
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const results = await Promise.all(
|
|
624
|
+
needsAttestation.map(this._performSingleAttestation)
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
// "Process" the results of attestation into noRetry and needsAttestation errors,
|
|
628
|
+
// as well as a flag that indicates whether we should reselect.
|
|
629
|
+
;({
|
|
630
|
+
successful,
|
|
631
|
+
noRetry,
|
|
632
|
+
needsRetry: needsAttestation,
|
|
633
|
+
shouldReselect,
|
|
634
|
+
failingNodes
|
|
635
|
+
} = await this._processResponses(
|
|
636
|
+
results,
|
|
637
|
+
retryCount === this.maxRetries - 1
|
|
638
|
+
))
|
|
639
|
+
|
|
640
|
+
// Add failing nodes to the blocklist, trimming out oldest nodes if necessary
|
|
641
|
+
if (failingNodes?.length) {
|
|
642
|
+
const existing = new Set(this.discoveryNodeBlocklist)
|
|
643
|
+
failingNodes.forEach((n) => {
|
|
644
|
+
if (!existing.has(n)) {
|
|
645
|
+
this.discoveryNodeBlocklist.push(n)
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
this.discoveryNodeBlocklist = this.discoveryNodeBlocklist.slice(
|
|
649
|
+
-1 * MAX_DISCOVERY_NODE_BLOCKLIST_LEN
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
successCount += successful.length
|
|
654
|
+
accumulatedErrors = [...accumulatedErrors, ...noRetry]
|
|
655
|
+
|
|
656
|
+
// Increment offset by the # of errors we're not retrying that have the max block #.
|
|
657
|
+
//
|
|
658
|
+
// Note: any successfully completed rewards will eventually be flushed from the
|
|
659
|
+
// disbursable queue on DN, but ignored rewards will stay stuck in that list, so we
|
|
660
|
+
// have to move past them with offset if they're not already moved past with `startingBlock`.
|
|
661
|
+
offset += noRetry.filter(
|
|
662
|
+
({ completedBlocknumber }) => completedBlocknumber === highestBlock
|
|
663
|
+
).length
|
|
664
|
+
|
|
665
|
+
retryCount++
|
|
666
|
+
} while (needsAttestation.length && retryCount < this.maxRetries)
|
|
667
|
+
|
|
668
|
+
if (retryCount === this.maxRetries) {
|
|
669
|
+
this.logger.error(`Gave up with ${retryCount} retries`)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
accumulatedErrors,
|
|
674
|
+
highestBlock,
|
|
675
|
+
offset,
|
|
676
|
+
successCount
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Attempts to attest for a single challenge.
|
|
682
|
+
*
|
|
683
|
+
* @memberof RewardsAttester
|
|
684
|
+
*/
|
|
685
|
+
async _performSingleAttestation({
|
|
686
|
+
challengeId,
|
|
687
|
+
userId,
|
|
688
|
+
specifier,
|
|
689
|
+
amount,
|
|
690
|
+
handle,
|
|
691
|
+
wallet,
|
|
692
|
+
completedBlocknumber
|
|
693
|
+
}: Challenge): Promise<AttestationResult> {
|
|
694
|
+
this.logger.info(
|
|
695
|
+
`Attempting to attest for userId [${decodeHashId(
|
|
696
|
+
userId
|
|
697
|
+
)}], challengeId: [${challengeId}], quorum size: [${this.quorumSize}]}`
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
const { success, error, phase, nodesToReselect } =
|
|
701
|
+
await this.libs.Rewards.submitAndEvaluate({
|
|
702
|
+
challengeId,
|
|
703
|
+
encodedUserId: userId,
|
|
704
|
+
handle,
|
|
705
|
+
recipientEthAddress: wallet,
|
|
706
|
+
specifier,
|
|
707
|
+
oracleEthAddress: this.aaoAddress,
|
|
708
|
+
amount,
|
|
709
|
+
quorumSize: this.quorumSize,
|
|
710
|
+
AAOEndpoint: this.aaoEndpoint,
|
|
711
|
+
endpoints: this.endpoints,
|
|
712
|
+
logger: this.logger,
|
|
713
|
+
feePayerOverride: this._getFeePayer(),
|
|
714
|
+
maxAggregationAttempts: this.maxAggregationAttempts
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
if (success) {
|
|
718
|
+
this.logger.info(
|
|
719
|
+
`Successfully attestested for challenge [${challengeId}] for user [${decodeHashId(
|
|
720
|
+
userId
|
|
721
|
+
)}], amount [${amount}]!`
|
|
722
|
+
)
|
|
723
|
+
return {
|
|
724
|
+
challengeId,
|
|
725
|
+
userId,
|
|
726
|
+
specifier,
|
|
727
|
+
amount,
|
|
728
|
+
handle,
|
|
729
|
+
wallet,
|
|
730
|
+
completedBlocknumber,
|
|
731
|
+
nodesToReselect: null
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Handle error path
|
|
736
|
+
this.logger.error(
|
|
737
|
+
`Failed to attest for challenge [${challengeId}] for user [${decodeHashId(
|
|
738
|
+
userId
|
|
739
|
+
)}], amount [${amount}], oracle: [${
|
|
740
|
+
this.aaoAddress
|
|
741
|
+
}] at phase: [${phase}] with error [${error}]`
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
challengeId,
|
|
746
|
+
userId,
|
|
747
|
+
specifier,
|
|
748
|
+
amount,
|
|
749
|
+
handle,
|
|
750
|
+
wallet,
|
|
751
|
+
completedBlocknumber,
|
|
752
|
+
error,
|
|
753
|
+
phase,
|
|
754
|
+
nodesToReselect
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async _selectDiscoveryNodes() {
|
|
759
|
+
await this._updatePhase('SELECTING_NODES')
|
|
760
|
+
this.logger.info(
|
|
761
|
+
`Selecting discovery nodes with blocklist ${JSON.stringify(
|
|
762
|
+
this.discoveryNodeBlocklist
|
|
763
|
+
)}`
|
|
764
|
+
)
|
|
765
|
+
const startTime = Date.now()
|
|
766
|
+
let endpoints: ServiceWithEndpoint[] =
|
|
767
|
+
(await this.libs.discoveryProvider.serviceSelector.findAll({
|
|
768
|
+
verbose: true,
|
|
769
|
+
whitelist: this.endpointPool.size > 0 ? this.endpointPool : null
|
|
770
|
+
})) ?? []
|
|
771
|
+
// Filter out blocklisted nodes
|
|
772
|
+
const blockSet = new Set(this.discoveryNodeBlocklist)
|
|
773
|
+
endpoints = [...endpoints].filter((e) => !blockSet.has(e.endpoint))
|
|
774
|
+
|
|
775
|
+
this.endpoints =
|
|
776
|
+
await this.libs.Rewards.ServiceProvider.getUniquelyOwnedDiscoveryNodes(
|
|
777
|
+
this.quorumSize,
|
|
778
|
+
endpoints
|
|
779
|
+
)
|
|
780
|
+
this.logger.info(
|
|
781
|
+
`Selected new discovery nodes in ${
|
|
782
|
+
(Date.now() - startTime) / 1000
|
|
783
|
+
} seconds: [${this.endpoints}]`
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Fetches new undisbursed rewards and inserts them into the undisbursedQueue
|
|
789
|
+
* if the queue is currently empty.
|
|
790
|
+
*
|
|
791
|
+
* @memberof RewardsAttester
|
|
792
|
+
*/
|
|
793
|
+
async _refillQueueIfNecessary() {
|
|
794
|
+
if (this.undisbursedQueue.length) return {}
|
|
795
|
+
|
|
796
|
+
this.logger.info(
|
|
797
|
+
`Refilling queue with startingBlock: ${this.startingBlock}, offset: ${
|
|
798
|
+
this.offset
|
|
799
|
+
}, recently disbursed: ${JSON.stringify(this.recentlyDisbursedQueue)}`
|
|
800
|
+
)
|
|
801
|
+
await this._updatePhase('REFILLING_QUEUE')
|
|
802
|
+
const {
|
|
803
|
+
success: disbursable,
|
|
804
|
+
error
|
|
805
|
+
}: { success: DiscoveryNodeChallenge[]; error: null } =
|
|
806
|
+
await this.libs.Rewards.getUndisbursedChallenges({
|
|
807
|
+
offset: this.offset,
|
|
808
|
+
completedBlockNumber: this.startingBlock,
|
|
809
|
+
logger: this.logger
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
if (error) {
|
|
813
|
+
return { error }
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (disbursable?.length) {
|
|
817
|
+
this.logger.info(
|
|
818
|
+
`Got challenges: ${disbursable.map(
|
|
819
|
+
(
|
|
820
|
+
{ challenge_id, user_id, specifier } // eslint-disable-line
|
|
821
|
+
) => `${challenge_id}-${user_id}-${specifier}`
|
|
822
|
+
)}`
|
|
823
|
+
) // eslint-disable-line
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Map to camelCase, and filter out
|
|
827
|
+
// any challenges in the denylist or recently disbursed set
|
|
828
|
+
this.undisbursedQueue = disbursable
|
|
829
|
+
.map(
|
|
830
|
+
({
|
|
831
|
+
challenge_id, // eslint-disable-line
|
|
832
|
+
user_id, // eslint-disable-line
|
|
833
|
+
specifier,
|
|
834
|
+
amount,
|
|
835
|
+
handle,
|
|
836
|
+
wallet,
|
|
837
|
+
completed_blocknumber // eslint-disable-line
|
|
838
|
+
}) => ({
|
|
839
|
+
challengeId: challenge_id,
|
|
840
|
+
userId: user_id,
|
|
841
|
+
specifier,
|
|
842
|
+
amount,
|
|
843
|
+
handle,
|
|
844
|
+
wallet,
|
|
845
|
+
completedBlocknumber: completed_blocknumber
|
|
846
|
+
})
|
|
847
|
+
)
|
|
848
|
+
.filter(
|
|
849
|
+
(d) =>
|
|
850
|
+
!(
|
|
851
|
+
this.challengeIdsDenyList.has(d.challengeId) ||
|
|
852
|
+
new Set(this.recentlyDisbursedQueue).has(this._disbursementToKey(d))
|
|
853
|
+
)
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
// Filter out recently disbursed challenges
|
|
857
|
+
if (this.undisbursedQueue.length) {
|
|
858
|
+
this.undisbursedQueue = await this._filterRecentlyCompleted(
|
|
859
|
+
this.undisbursedQueue
|
|
860
|
+
)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
this.logger.info(
|
|
864
|
+
`Got ${disbursable.length} undisbursed challenges${
|
|
865
|
+
this.undisbursedQueue.length !== disbursable.length
|
|
866
|
+
? `, filtered out [${
|
|
867
|
+
disbursable.length - this.undisbursedQueue.length
|
|
868
|
+
}] challenges.`
|
|
869
|
+
: '.'
|
|
870
|
+
}`
|
|
871
|
+
)
|
|
872
|
+
return {}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Processes responses from `_performSingleAttestation`,
|
|
877
|
+
* bucketing errors into those that need retry and those that should be skipped.
|
|
878
|
+
*
|
|
879
|
+
* @memberof RewardsAttester
|
|
880
|
+
*/
|
|
881
|
+
async _processResponses(
|
|
882
|
+
responses: AttestationResult[],
|
|
883
|
+
isFinalAttempt: boolean
|
|
884
|
+
): Promise<{
|
|
885
|
+
successful: AttestationResult[]
|
|
886
|
+
noRetry: AttestationResult[]
|
|
887
|
+
needsRetry: AttestationResult[]
|
|
888
|
+
shouldReselect: boolean
|
|
889
|
+
failingNodes: string[]
|
|
890
|
+
}> {
|
|
891
|
+
const errors = SubmitAndEvaluateError
|
|
892
|
+
const AAO_ERRORS = new Set([
|
|
893
|
+
errors.HCAPTCHA,
|
|
894
|
+
errors.COGNITO_FLOW,
|
|
895
|
+
errors.AAO_ATTESTATION_REJECTION,
|
|
896
|
+
errors.AAO_ATTESTATION_UNKNOWN_RESPONSE
|
|
897
|
+
])
|
|
898
|
+
// Account for errors from DN aggregation + Solana program
|
|
899
|
+
// CHALLENGE_INCOMPLETE and MISSING_CHALLENGES are already handled in the `submitAndEvaluate` flow -
|
|
900
|
+
// safe to assume those won't work if we see them at this point.
|
|
901
|
+
const NEEDS_RESELECT_ERRORS = new Set([
|
|
902
|
+
errors.INSUFFICIENT_DISCOVERY_NODE_COUNT,
|
|
903
|
+
errors.CHALLENGE_INCOMPLETE,
|
|
904
|
+
errors.MISSING_CHALLENGES
|
|
905
|
+
])
|
|
906
|
+
const ALREADY_COMPLETE_ERRORS = new Set([
|
|
907
|
+
errors.ALREADY_DISBURSED,
|
|
908
|
+
errors.ALREADY_SENT
|
|
909
|
+
])
|
|
910
|
+
|
|
911
|
+
const noRetry: AttestationResult[] = []
|
|
912
|
+
const successful: AttestationResult[] = []
|
|
913
|
+
// Filter our successful responses
|
|
914
|
+
const allErrors = responses.filter((res) => {
|
|
915
|
+
if (!res.error) {
|
|
916
|
+
successful.push(res)
|
|
917
|
+
this.reporter.reportSuccess({
|
|
918
|
+
userId: decodeHashId(res.userId) ?? -1,
|
|
919
|
+
challengeId: res.challengeId,
|
|
920
|
+
amount: res.amount,
|
|
921
|
+
specifier: res.specifier
|
|
922
|
+
})
|
|
923
|
+
return false
|
|
924
|
+
}
|
|
925
|
+
return true
|
|
926
|
+
}) as Array<AttestationResult & { error: string; phase: string }>
|
|
927
|
+
|
|
928
|
+
// Filter out responses that are already disbursed
|
|
929
|
+
const stillIncomplete = allErrors.filter(
|
|
930
|
+
({ error }) => !ALREADY_COMPLETE_ERRORS.has(error)
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
// Filter to errors needing retry
|
|
934
|
+
const needsRetry = stillIncomplete.filter((res) => {
|
|
935
|
+
const report = {
|
|
936
|
+
userId: decodeHashId(res.userId) ?? -1,
|
|
937
|
+
challengeId: res.challengeId,
|
|
938
|
+
amount: res.amount,
|
|
939
|
+
error: res.error,
|
|
940
|
+
phase: res.phase,
|
|
941
|
+
specifier: res.specifier,
|
|
942
|
+
reason: 'unknown'
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function getIsAAOError(err?: string): err is string {
|
|
946
|
+
return !!err && AAO_ERRORS.has(err)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const { error } = res
|
|
950
|
+
const isAAOError = getIsAAOError(error)
|
|
951
|
+
// Filter out and handle unretryable AAO errors
|
|
952
|
+
if (isAAOError) {
|
|
953
|
+
noRetry.push(res)
|
|
954
|
+
const errorType = {
|
|
955
|
+
[errors.HCAPTCHA]: 'hcaptcha',
|
|
956
|
+
[errors.COGNITO_FLOW]: 'cognito',
|
|
957
|
+
[errors.AAO_ATTESTATION_REJECTION]: 'rejection',
|
|
958
|
+
[errors.AAO_ATTESTATION_UNKNOWN_RESPONSE]: 'unknown'
|
|
959
|
+
// Some hacky typing here because we haen't typed the imported error type yet
|
|
960
|
+
}[error] as unknown as 'hcaptcha' | 'cognito' | 'rejection' | 'unknown'
|
|
961
|
+
report.reason = errorType
|
|
962
|
+
this.reporter.reportAAORejection(report)
|
|
963
|
+
} else if (isFinalAttempt) {
|
|
964
|
+
// Final attempt at retries,
|
|
965
|
+
// should be classified as noRetry
|
|
966
|
+
// and reported as a failure
|
|
967
|
+
noRetry.push(res)
|
|
968
|
+
this.reporter.reportFailure(report)
|
|
969
|
+
} else {
|
|
970
|
+
// Otherwise, retry it
|
|
971
|
+
this.reporter.reportRetry(report)
|
|
972
|
+
}
|
|
973
|
+
return !isAAOError && !isFinalAttempt
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
if (needsRetry.length) {
|
|
977
|
+
this.logger.info(
|
|
978
|
+
`Handling errors: ${JSON.stringify(
|
|
979
|
+
needsRetry.map(({ error, phase }) => ({ error, phase }))
|
|
980
|
+
)}`
|
|
981
|
+
)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Reselect if necessary
|
|
985
|
+
const shouldReselect = needsRetry.some(({ error }) =>
|
|
986
|
+
NEEDS_RESELECT_ERRORS.has(error)
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
let failingNodes: string[] = []
|
|
990
|
+
if (shouldReselect) {
|
|
991
|
+
failingNodes = [
|
|
992
|
+
...needsRetry.reduce((acc, cur) => {
|
|
993
|
+
if (cur.nodesToReselect) {
|
|
994
|
+
cur.nodesToReselect?.forEach((n) => acc.add(n))
|
|
995
|
+
}
|
|
996
|
+
return acc
|
|
997
|
+
}, new Set<string>())
|
|
998
|
+
]
|
|
999
|
+
this.logger.info(`Failing nodes: ${JSON.stringify(failingNodes)}`)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Update state
|
|
1003
|
+
const now = Date.now()
|
|
1004
|
+
let update: {
|
|
1005
|
+
lastChallengeTime: number
|
|
1006
|
+
lastSuccessChallengeTime?: number
|
|
1007
|
+
} = {
|
|
1008
|
+
lastChallengeTime: now
|
|
1009
|
+
}
|
|
1010
|
+
if (successful.length) {
|
|
1011
|
+
update = {
|
|
1012
|
+
...update,
|
|
1013
|
+
lastSuccessChallengeTime: now
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
await this._updateState(update)
|
|
1017
|
+
return {
|
|
1018
|
+
successful,
|
|
1019
|
+
noRetry,
|
|
1020
|
+
needsRetry,
|
|
1021
|
+
shouldReselect,
|
|
1022
|
+
failingNodes
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
_disbursementToKey({ challengeId, userId, specifier }: Challenge) {
|
|
1027
|
+
return `${challengeId}_${userId}_${specifier}`
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async _backoff(retryCount: number) {
|
|
1031
|
+
const backoff = Math.min(
|
|
1032
|
+
this.cooldownMsec * Math.pow(this.backoffExponent, retryCount),
|
|
1033
|
+
this.maxCooldownMsec
|
|
1034
|
+
)
|
|
1035
|
+
this.logger.info(`Waiting [${backoff}] msec`)
|
|
1036
|
+
await this._updatePhase('RETRY_BACKOFF')
|
|
1037
|
+
return await this._delay(backoff)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async _delay(waitTime: number): Promise<void> {
|
|
1041
|
+
return await new Promise((resolve) => setTimeout(resolve, waitTime))
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
_addRecentlyDisbursed(challenges: Challenge[]) {
|
|
1045
|
+
const ids = challenges.map(this._disbursementToKey)
|
|
1046
|
+
this.recentlyDisbursedQueue.push(...ids)
|
|
1047
|
+
if (this.recentlyDisbursedQueue.length > MAX_DISBURSED_CACHE_SIZE) {
|
|
1048
|
+
this.recentlyDisbursedQueue.splice(
|
|
1049
|
+
0,
|
|
1050
|
+
this.recentlyDisbursedQueue.length - MAX_DISBURSED_CACHE_SIZE
|
|
1051
|
+
)
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async _filterRecentlyCompleted(challenges: Challenge[]) {
|
|
1056
|
+
const [poaThreshold, solanaThreshold] = await Promise.all([
|
|
1057
|
+
this.delayCalculator.getPOABlockThreshold(),
|
|
1058
|
+
this.delayCalculator.getSolanaSlotThreshold()
|
|
1059
|
+
])
|
|
1060
|
+
|
|
1061
|
+
this.logger.info(
|
|
1062
|
+
`Filtering with POA threshold: ${poaThreshold}, Solana threshold: ${solanaThreshold}`
|
|
1063
|
+
)
|
|
1064
|
+
const res = challenges.filter(
|
|
1065
|
+
(c) =>
|
|
1066
|
+
c.completedBlocknumber <=
|
|
1067
|
+
(this.isSolanaChallenge(c.challengeId) ? solanaThreshold : poaThreshold)
|
|
1068
|
+
)
|
|
1069
|
+
if (res.length < challenges.length) {
|
|
1070
|
+
this.logger.info(
|
|
1071
|
+
`Filtered out ${challenges.length - res.length} recent challenges`
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
return res
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async _updateState(newState: Partial<AttesterState>) {
|
|
1078
|
+
try {
|
|
1079
|
+
this.attesterState = {
|
|
1080
|
+
...this.attesterState,
|
|
1081
|
+
...newState,
|
|
1082
|
+
lastActionTime: Date.now()
|
|
1083
|
+
}
|
|
1084
|
+
await this._updateStateCallback(this.attesterState)
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
this.logger.error(`Got error updating state: ${e}`)
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async _updatePhase(phase: ATTESTER_PHASE) {
|
|
1091
|
+
await this._updateState({ phase })
|
|
1092
|
+
}
|
|
1093
|
+
}
|