Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 106 additions & 1 deletion api/v1_claim_rewards.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"math"
"math/rand/v2"
"net/http"
"net/url"
Expand Down Expand Up @@ -217,7 +218,6 @@ func fetchAttestations(
hasAntiAbuseOracleAttestation bool,
minVotes int,
) ([]SenderAttestation, error) {

// Shuffle the validators
shuffled := slices.Clone(allValidators)
rand.Shuffle(len(shuffled), func(i, j int) {
Expand Down Expand Up @@ -348,6 +348,111 @@ func fetchAttestations(
return attestations, nil
}

// Builds submit/evaluate transactions and sends them in batches of at most 2
// to ensure we stay under the transaction size limit.
func sendRewardClaimTransactionsBatched(
ctx context.Context,
rewardManagerClient *reward_manager.RewardManagerClient,
transactionSender *spl.TransactionSender,
rewardClaim RewardClaim,
attestations []SenderAttestation,
sourceTokenAccount solana.PublicKey,
tokenDecimals int,
) ([]solana.Signature, error) {
firstTx := solana.NewTransactionBuilder()
secondTx := solana.NewTransactionBuilder()

feePayer, err := transactionSender.GetFeePayer()
if err != nil {
return nil, err
}
firstTx.SetFeePayer(feePayer.PublicKey())
secondTx.SetFeePayer(feePayer.PublicKey())

for i, attestation := range attestations {
instructionIndex := uint8((i % 2) * 2)
submitAttestationSecpInstruction := secp256k1.NewSecp256k1Instruction(
attestation.EthAddress,
attestation.Message,
attestation.Signature,
instructionIndex,
).Build()
submitAttestationInstruction, err := reward_manager.NewSubmitAttestationInstruction(
rewardClaim.RewardID,
rewardClaim.Specifier,
attestation.EthAddress,
rewardManagerClient.GetProgramStateAccount(),
feePayer.PublicKey(),
)
if err != nil {
return nil, fmt.Errorf("failed to build submitAttestation instruction: %w", err)
}
if i > 1 {
secondTx.AddInstruction(submitAttestationSecpInstruction)
secondTx.AddInstruction(submitAttestationInstruction.Build())
} else {
firstTx.AddInstruction(submitAttestationSecpInstruction)
firstTx.AddInstruction(submitAttestationInstruction.Build())
}
}

lookupTable, err := rewardManagerClient.GetLookupTable(ctx)
if err != nil {
return nil, err
}
addressLookupTables := map[solana.PublicKey]solana.PublicKeySlice{
rewardManagerClient.GetLookupTableAccount(): lookupTable.Addresses,
}

txSignatures := make([]solana.Signature, 0)
evaluateAttestationInstruction, err := reward_manager.NewEvaluateAttestationInstruction(
rewardClaim.RewardID,
rewardClaim.Specifier,
common.HexToAddress(rewardClaim.RecipientEthAddress),
rewardClaim.Amount*uint64(math.Pow10(tokenDecimals)), // Convert to token wei
common.HexToAddress(rewardClaim.ClaimAuthority),
rewardManagerClient.GetProgramStateAccount(),
sourceTokenAccount,
rewardClaim.UserBank,
feePayer.PublicKey(),
)
if err != nil {
return nil, fmt.Errorf("failed to build evaluateAttestation instruction: %w", err)
}

secondTx.AddInstruction(evaluateAttestationInstruction.Build())

firstTx.WithOpt(solana.TransactionAddressTables(addressLookupTables))
err = transactionSender.AddPriorityFees(ctx, firstTx, spl.AddPriorityFeesParams{Percentile: 99, Multiplier: 1})
if err != nil {
return txSignatures, err
}

secondTx.WithOpt(solana.TransactionAddressTables(addressLookupTables))
err = transactionSender.AddPriorityFees(ctx, secondTx, spl.AddPriorityFeesParams{Percentile: 99, Multiplier: 1})
if err != nil {
return txSignatures, err
}

if len(attestations) != 0 {
sig, err := transactionSender.SendTransactionWithRetries(ctx, firstTx, rpc.CommitmentConfirmed, rpc.TransactionOpts{})
if err != nil {
return txSignatures, err
}
txSignatures = append(txSignatures, *sig)
}

sig2, err := transactionSender.SendTransactionWithRetries(ctx, secondTx, rpc.CommitmentConfirmed, rpc.TransactionOpts{
SkipPreflight: true,
})
if err != nil {
return txSignatures, err
}

txSignatures = append(txSignatures, *sig2)
return txSignatures, nil
}

// Builds a Solana transaction to claim a reward from the attestations and sends it with retries.
func sendRewardClaimTransactions(
ctx context.Context,
Expand Down
86 changes: 76 additions & 10 deletions api/v1_coins_post_redeem.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -103,20 +104,27 @@ func (app *ApiServer) v1CoinsPostRedeem(c *fiber.Ctx) error {
// Get reward manager state for the given mint
// and init a new client for it
var rewardStateAddress string
var authorityAddress string
var minVotes int
var tokenSourceAddress string
err = app.pool.QueryRow(c.Context(), `SELECT reward_manager_state, token_source, min_votes FROM sol_reward_manager_inits WHERE mint = @mint`, pgx.NamedArgs{
err = app.pool.QueryRow(c.Context(), `SELECT reward_manager_state, authority, min_votes FROM sol_reward_manager_inits WHERE mint = @mint`, pgx.NamedArgs{
"mint": mintString,
}).Scan(&rewardStateAddress, &tokenSourceAddress, &minVotes)
}).Scan(&rewardStateAddress, &authorityAddress, &minVotes)
if err != nil {
return fmt.Errorf("failed to get reward manager state: %w", err)
}
rewardManagerPubkey, err := solana.PublicKeyFromBase58(rewardStateAddress)
if err != nil {
return fmt.Errorf("failed to get init pubkey for reward manager state address: %w", err)
}

authorityPubkey, err := solana.PublicKeyFromBase58(authorityAddress)
if err != nil {
return fmt.Errorf("failed to init pubkey for token source address: %w", err)
return fmt.Errorf("failed to get authority pubkey: %w", err)
}

tokenSourcePubkey, _, err := solana.FindAssociatedTokenAddress(authorityPubkey, mint)
if err != nil {
return fmt.Errorf("failed to get token source pubkey: %w", err)
}

// Ensure ProgramID is set correctly for PDA derivation
Expand Down Expand Up @@ -225,8 +233,8 @@ func (app *ApiServer) v1CoinsPostRedeem(c *fiber.Ctx) error {
rewardClaim := oap_rewards.RewardClaim{
RecipientEthAddress: userWalletAddress,
Amount: amount,
Decimals: coinDecimals,
RewardID: redeemCode,
RewardAddress: rewardAddress,
Specifier: specifier,
ClaimAuthority: claimAuthorityPublicKey,
}
Expand All @@ -248,11 +256,67 @@ func (app *ApiServer) v1CoinsPostRedeem(c *fiber.Ctx) error {
claimMessage, err := rewardClaim.Compile()

attestations := make([]SenderAttestation, 0)
// TODO: Add retries to this loop, maybe the whole thing, but def each attestation attempt
for _, validator := range app.validators.GetNodes() {
if len(attestations) >= minVotes {

attestationsData, err := rewardManagerClient.GetSubmittedAttestations(c.Context(), rewardClaim)
if err != nil {
// If not found, then it's empty. Use default values for the purpose
// of getting an empty list of messages
if err.Error() != "not found" {
return fmt.Errorf("failed to get submitted attestations: %w", err)
}
attestationsData = &reward_manager.AttestationsAccountData{}
}

hasClaimAuthorityAttestation := false
existingValidatorOwners := make([]string, 0)
for _, attestation := range attestationsData.Messages {
if attestation.Claim.ClaimAuthority != "" {
existingValidatorOwners = append(existingValidatorOwners, attestation.OperatorEthAddress)
} else {
hasClaimAuthorityAttestation = true
}
}

if !hasClaimAuthorityAttestation {
claimAuthorityRewardClaim := oap_rewards.RewardClaim{
RecipientEthAddress: userWalletAddress,
Amount: amount,
Decimals: coinDecimals,
RewardID: redeemCode,
Specifier: specifier,
}
message, err := claimAuthorityRewardClaim.Compile()
if err != nil {
return fmt.Errorf("failed to compile claim authority reward claim: %w", err)
}

claimAuthoritySignature, err := oap_rewards.SignClaim(claimAuthorityRewardClaim, claimAuthorityKey)
signatureBytes, err := hex.DecodeString(strings.TrimPrefix(claimAuthoritySignature, "0x"))

if err != nil {
return fmt.Errorf("failed to sign claim with claim authority: %w", err)
}

attestation := SenderAttestation{
EthAddress: common.HexToAddress(claimAuthorityPublicKey),
Message: message,
Signature: signatureBytes,
}

attestations = append(attestations, attestation)
}

neededAttestations := (minVotes + 1) - len(attestationsData.Messages)

for _, validator := range config.Cfg.ArtistCoinRewardsStaticSenders {
if len(attestations) >= neededAttestations {
break
}
if slices.ContainsFunc(existingValidatorOwners, func(s string) bool {
return strings.EqualFold(s, validator.OwnerWallet)
}) {
continue
}
oap := sdk.NewOpenAudioSDK(validator.Endpoint)
oap.SetPrivKey(claimAuthorityKey)

Expand Down Expand Up @@ -288,7 +352,7 @@ func (app *ApiServer) v1CoinsPostRedeem(c *fiber.Ctx) error {
attestations = append(attestations, attestation)
}

if len(attestations) < minVotes {
if len(attestations) < neededAttestations {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "failed to get enough attestations",
})
Expand All @@ -297,12 +361,14 @@ func (app *ApiServer) v1CoinsPostRedeem(c *fiber.Ctx) error {
// #endregion Build Claim
// #region Send Tx
// Build and send solana transactions
signatures, err := sendRewardClaimTransactions(
signatures, err := sendRewardClaimTransactionsBatched(
c.Context(),
rewardManagerClient,
app.transactionSender,
decoratedRewardClaim,
attestations,
tokenSourcePubkey,
int(coinDecimals),
)

if err != nil {
Expand Down
99 changes: 67 additions & 32 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,39 @@ import (
)

type Config struct {
Env string
Git string
LogLevel string
ZapLevel zapcore.Level
ReadDbUrl string
ReadDbReplicas []string
WriteDbUrl string
RunMigrations bool
EsUrl string
Nodes []Node
StoreAllNodes []string
DeadNodes []string
VerifierAddress string
DelegatePrivateKey string
AxiomToken string
AxiomDataset string
NetworkTakeRate float64
SolanaConfig SolanaConfig
AntiAbuseOracles []string
Rewards []rewards.Reward
AudiusdURL string
ChainId string
BirdeyeToken string
SolanaIndexerWorkers int
SolanaIndexerRetryInterval time.Duration
CommsMessagePush bool
AudiusdChainID uint
AudiusdEntityManagerAddress string
AudiusAppUrl string
ContentNodeMonitor bool
RewardCodeAuthorizedKeys []string
LaunchpadDeterministicSecret string
Env string
Git string
LogLevel string
ZapLevel zapcore.Level
ReadDbUrl string
ReadDbReplicas []string
WriteDbUrl string
RunMigrations bool
EsUrl string
Nodes []Node
ArtistCoinRewardsStaticSenders []Node
StoreAllNodes []string
DeadNodes []string
VerifierAddress string
DelegatePrivateKey string
AxiomToken string
AxiomDataset string
NetworkTakeRate float64
SolanaConfig SolanaConfig
AntiAbuseOracles []string
Rewards []rewards.Reward
AudiusdURL string
ChainId string
BirdeyeToken string
SolanaIndexerWorkers int
SolanaIndexerRetryInterval time.Duration
CommsMessagePush bool
AudiusdChainID uint
AudiusdEntityManagerAddress string
AudiusAppUrl string
ContentNodeMonitor bool
RewardCodeAuthorizedKeys []string
LaunchpadDeterministicSecret string
}

var Cfg = Config{
Expand Down Expand Up @@ -120,6 +121,23 @@ func init() {
Cfg.AudiusAppUrl = "https://staging.audius.co"
Cfg.RewardCodeAuthorizedKeys = []string{"9XeZbswbSSUU4AHVArQbTQjAEjAPhVweGU5cogBVkvh4", "GrWNH9qfwrvoCEoTm65hmnSh4z3CD96SfhtfQY6ZKUfY"}
Cfg.VerifierAddress = "0xbbbb93A6B3A1D6fDd27909729b95CCB0cc9002C0"
Cfg.ArtistCoinRewardsStaticSenders = []Node{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nitpick but maybe for sanity put this in the nodes.go file

{
DelegateOwnerWallet: "0x140eD283b33be2145ed7d9d15f1fE7bF1E0B2Ac3",
Endpoint: "https://creatornode9.staging.audius.co",
OwnerWallet: "0x140eD283b33be2145ed7d9d15f1fE7bF1E0B2Ac3",
},
{
DelegateOwnerWallet: "0x4c88d2c0f4c4586b41621aD6e98882ae904B98f6",
Endpoint: "https://creatornode11.staging.audius.co",
OwnerWallet: "0x4c88d2c0f4c4586b41621aD6e98882ae904B98f6",
},
{
DelegateOwnerWallet: "0x6b52969934076318863243fb92E9C4b3A08267b5",
Endpoint: "https://creatornode12.staging.audius.co",
OwnerWallet: "0x6b52969934076318863243fb92E9C4b3A08267b5",
},
}
case "prod":
fallthrough
case "production":
Expand All @@ -142,6 +160,23 @@ func init() {
Cfg.AudiusAppUrl = "https://audius.co"
Cfg.RewardCodeAuthorizedKeys = []string{"DDT15s6MMNxE4jkyGN46wNYqrgLWofT6WAvWtjYYrCUq"}
Cfg.VerifierAddress = "0xbeef8E42e8B5964fDD2b7ca8efA0d9aef38AA996"
Cfg.ArtistCoinRewardsStaticSenders = []Node{
{
DelegateOwnerWallet: "0xc8d0C29B6d540295e8fc8ac72456F2f4D41088c8",
Endpoint: "https://creatornode.audius.co",
OwnerWallet: "0xe5b256d302ea2f4e04B8F3bfD8695aDe147aB68d",
},
{
DelegateOwnerWallet: "0x159200F84c2cF000b3A014cD4D8244500CCc36ca",
Endpoint: "https://audius-cn1.tikilabs.com",
OwnerWallet: "0xe4882D9A38A2A1fc652996719AF0fb15CB968d0a",
},
Comment on lines +169 to +173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we pick bragi or one of the culturestake nodes? this node is offline @raymondjacobson @schottra

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no we cannot

{
DelegateOwnerWallet: "0x422541273087beC833c57D3c15B9e17F919bFB1F",
Endpoint: "https://dn2.monophonic.digital",
OwnerWallet: "0x6470Daf3bd32f5014512bCdF0D02232f5640a5BD",
},
}
default:
log.Fatalf("Unknown environment: %s", env)
}
Expand Down