diff --git a/api/solana/programs/reward-manager/EvaluateAttestations.go b/api/solana/programs/reward-manager/EvaluateAttestations.go deleted file mode 100644 index 24da4ef3..00000000 --- a/api/solana/programs/reward-manager/EvaluateAttestations.go +++ /dev/null @@ -1,3 +0,0 @@ -package reward_manager - -type EvaluateAttestation struct{} diff --git a/api/solana/programs/reward-manager/ChangeManagerAccount.go b/api/solana/programs/reward_manager/ChangeManagerAccount.go similarity index 100% rename from api/solana/programs/reward-manager/ChangeManagerAccount.go rename to api/solana/programs/reward_manager/ChangeManagerAccount.go diff --git a/api/solana/programs/reward-manager/CreateSender.go b/api/solana/programs/reward_manager/CreateSender.go similarity index 100% rename from api/solana/programs/reward-manager/CreateSender.go rename to api/solana/programs/reward_manager/CreateSender.go diff --git a/api/solana/programs/reward-manager/CreateSenderPublic.go b/api/solana/programs/reward_manager/CreateSenderPublic.go similarity index 100% rename from api/solana/programs/reward-manager/CreateSenderPublic.go rename to api/solana/programs/reward_manager/CreateSenderPublic.go diff --git a/api/solana/programs/reward-manager/DeleteSender.go b/api/solana/programs/reward_manager/DeleteSender.go similarity index 100% rename from api/solana/programs/reward-manager/DeleteSender.go rename to api/solana/programs/reward_manager/DeleteSender.go diff --git a/api/solana/programs/reward-manager/DeleteSenderPublic.go b/api/solana/programs/reward_manager/DeleteSenderPublic.go similarity index 100% rename from api/solana/programs/reward-manager/DeleteSenderPublic.go rename to api/solana/programs/reward_manager/DeleteSenderPublic.go diff --git a/api/solana/programs/reward_manager/EvaluateAttestations.go b/api/solana/programs/reward_manager/EvaluateAttestations.go new file mode 100644 index 00000000..044651fd --- /dev/null +++ b/api/solana/programs/reward_manager/EvaluateAttestations.go @@ -0,0 +1,185 @@ +package reward_manager + +import ( + "encoding/hex" + "strings" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" +) + +type EvaluateAttestation struct { + // Instruction Data + Amount uint64 + DisbursementID string + ReceipientEthAddress string + + // Used for derivations + RewardManagerState solana.PublicKey `bin:"-" borsh_skip:"true"` + Payer solana.PublicKey `bin:"-" borsh_skip:"true"` + DestinationUserBank solana.PublicKey `bin:"-" borsh_skip:"true"` + TokenSource solana.PublicKey `bin:"-" borsh_skip:"true"` + AntiAbuseOracleEthAddress string `bin:"-" borsh_skip:"true"` + + // Accounts + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewEvaluateAttestationInstructionBuilder() *EvaluateAttestation { + data := &EvaluateAttestation{} + return data +} + +func (inst *EvaluateAttestation) SetDisbursementID(challengedId string, specifier string) *EvaluateAttestation { + inst.DisbursementID = challengedId + ":" + specifier + return inst +} + +func (inst *EvaluateAttestation) SetRecipientEthAddress(recipientEthAddress string) *EvaluateAttestation { + inst.ReceipientEthAddress = recipientEthAddress + return inst +} + +func (inst *EvaluateAttestation) SetAmount(amount uint64) *EvaluateAttestation { + inst.Amount = amount + return inst +} + +func (inst *EvaluateAttestation) SetAntiAbuseOracleEthAddress(antiAbuseOracleAddress string) *EvaluateAttestation { + inst.AntiAbuseOracleEthAddress = antiAbuseOracleAddress + return inst +} + +func (inst *EvaluateAttestation) SetRewardManagerState(state solana.PublicKey) *EvaluateAttestation { + inst.RewardManagerState = state + return inst +} + +func (inst *EvaluateAttestation) SetTokenSource(tokenSource solana.PublicKey) *EvaluateAttestation { + inst.TokenSource = tokenSource + return inst +} + +func (inst *EvaluateAttestation) SetDestinationUserBank(userBank solana.PublicKey) *EvaluateAttestation { + inst.DestinationUserBank = userBank + return inst +} + +func (inst *EvaluateAttestation) SetPayer(payer solana.PublicKey) *EvaluateAttestation { + inst.Payer = payer + return inst +} + +func (inst EvaluateAttestation) Build() *Instruction { + authority, _, _ := deriveAuthority(ProgramID, inst.RewardManagerState) + attestations, _, _ := deriveAttestations(ProgramID, authority, inst.DisbursementID) + disbursement, _, _ := deriveDisbursement(ProgramID, authority, inst.DisbursementID) + antiAbuseOracle, _, _ := deriveSender(ProgramID, authority, inst.AntiAbuseOracleEthAddress) + + inst.AccountMetaSlice = []*solana.AccountMeta{ + { + PublicKey: attestations, + IsSigner: false, + IsWritable: true, + }, + { + PublicKey: inst.RewardManagerState, + IsSigner: false, + IsWritable: false, + }, + { + PublicKey: authority, + IsSigner: false, + IsWritable: false, + }, + { + PublicKey: inst.TokenSource, + IsSigner: false, + IsWritable: true, + }, + { + PublicKey: inst.DestinationUserBank, + IsSigner: false, + IsWritable: true, + }, + { + PublicKey: disbursement, + IsSigner: false, + IsWritable: true, + }, + { + PublicKey: antiAbuseOracle, + IsSigner: false, + IsWritable: false, + }, + { + PublicKey: inst.Payer, + IsSigner: true, + IsWritable: true, + }, + { + PublicKey: solana.SysVarRentPubkey, + IsSigner: false, + IsWritable: false, + }, + { + PublicKey: solana.TokenProgramID, + IsSigner: false, + IsWritable: false, + }, + { + PublicKey: solana.SystemProgramID, + IsSigner: false, + IsWritable: false, + }, + } + + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint8(Instruction_EvaluateAttestations), + }} +} + +func (inst EvaluateAttestation) MarshalWithEncoder(encoder *bin.Encoder) error { + err := encoder.Encode(inst.Amount) + if err != nil { + return err + } + + err = encoder.Encode(inst.DisbursementID) + if err != nil { + return err + } + + address, err := hex.DecodeString(strings.TrimPrefix(inst.ReceipientEthAddress, "0x")) + if err != nil { + return err + } + return encoder.WriteBytes(address, false) +} + +func (inst *EvaluateAttestation) UnmarshalWithDecoder(decoder *bin.Decoder) error { + return decoder.Decode(&inst) +} + +func NewEvaluateAttestationInstruction( + challengeId string, + specifier string, + recipientEthAddress string, + amount uint64, + antiAbuseOracleAddress string, + rewardManagerState solana.PublicKey, + tokenSource solana.PublicKey, + destinationUserBank solana.PublicKey, + payer solana.PublicKey, +) *EvaluateAttestation { + return NewEvaluateAttestationInstructionBuilder(). + SetRewardManagerState(rewardManagerState). + SetDisbursementID(challengeId, specifier). + SetRecipientEthAddress(recipientEthAddress). + SetAmount(amount). + SetAntiAbuseOracleEthAddress(antiAbuseOracleAddress). + SetTokenSource(tokenSource). + SetDestinationUserBank(destinationUserBank). + SetPayer(payer) +} diff --git a/api/solana/programs/reward_manager/EvaluateAttestations_test.go b/api/solana/programs/reward_manager/EvaluateAttestations_test.go new file mode 100644 index 00000000..99d21827 --- /dev/null +++ b/api/solana/programs/reward_manager/EvaluateAttestations_test.go @@ -0,0 +1,63 @@ +package reward_manager_test + +import ( + "encoding/hex" + "testing" + + "bridgerton.audius.co/api/solana/programs/reward_manager" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +func TestEvaluateAttestationsInstruction(t *testing.T) { + // Test data + challengeId := "ft" + specifier := "37364e80" + recipientEthAddress := "0x3f6d9fcf0d4466dd5886e3b1def017adfb7916b4" + amount := uint64(200000000) + antiAbuseOracleEthAddress := "0x00b6462e955dA5841b6D9e1E2529B830F00f31Bf" + + // Expected Accounts + // From successful stage transaction (signature 26gT9HVMhzBDzsKcsiKREYmGcXuZhjAJpCVUu9WFNhVMyKje8SdApYc4ev3HrumZB4LEXLUaPnKyriBPLmtzwrWp) + rewardState := solana.MustPublicKeyFromBase58("GaiG9LDYHfZGqeNaoGRzFEnLiwUT7WiC6sA6FDJX9ZPq") + expectedAuthority := solana.MustPublicKeyFromBase58("6mpecd6bJCpH8oDwwjqPzTPU6QacnwW3cR9pAwEwkYJa") + tokenSource := solana.MustPublicKeyFromBase58("HJQj8P47BdA7ugjQEn45LaESYrxhiZDygmukt8iumFZJ") + destinationUserBank := solana.MustPublicKeyFromBase58("Cjv8dvVfWU8wUYAR82T5oZ4nHLB6EyGNvpPBzw3r76Qy") + expectedDisbursement := solana.MustPublicKeyFromBase58("3qQfuDEBWEmxRo5G4J2a4eYUVf9u1LWzLgRPndiwew2w") + expectedOracle := solana.MustPublicKeyFromBase58("FNz5mur7EFh1LyH5HDaKyWVx7vcfGK6gRizEpDqMfgGk") + payer := solana.MustPublicKeyFromBase58("E3CfijtAJwBSHfwFEViAUd3xp7c8TBxwC1eXn1Fgxp8h") + + // Expected Data (from same tx) + expectedData, err := hex.DecodeString("0700c2eb0b000000000b00000066743a33373336346538303f6d9fcf0d4466dd5886e3b1def017adfb7916b4") + require.NoError(t, err) + + // Use stage program ID + stageProgramId := solana.MustPublicKeyFromBase58("CDpzvz7DfgbF95jSSCHLX3ERkugyfgn9Fw8ypNZ1hfXp") + reward_manager.SetProgramID(stageProgramId) + + inst := reward_manager.NewEvaluateAttestationInstruction( + challengeId, + specifier, + recipientEthAddress, + amount, + antiAbuseOracleEthAddress, + rewardState, + tokenSource, + destinationUserBank, + payer, + ).Build() + + require.Equal(t, stageProgramId, inst.ProgramID()) + require.Len(t, inst.Accounts(), 11) + require.Equal(t, rewardState.String(), inst.Accounts()[1].PublicKey.String()) + require.Equal(t, expectedAuthority.String(), inst.Accounts()[2].PublicKey.String()) + require.Equal(t, tokenSource.String(), inst.Accounts()[3].PublicKey.String()) + require.Equal(t, destinationUserBank.String(), inst.Accounts()[4].PublicKey.String()) + require.Equal(t, expectedDisbursement.String(), inst.Accounts()[5].PublicKey.String()) + require.Equal(t, expectedOracle.String(), inst.Accounts()[6].PublicKey.String()) + require.Equal(t, payer.String(), inst.Accounts()[7].PublicKey.String()) + + data, err := inst.Data() + require.NoError(t, err) + require.Equal(t, expectedData, data) +} diff --git a/api/solana/programs/reward-manager/Init.go b/api/solana/programs/reward_manager/Init.go similarity index 100% rename from api/solana/programs/reward-manager/Init.go rename to api/solana/programs/reward_manager/Init.go diff --git a/api/solana/programs/reward-manager/SubmitAttestation.go b/api/solana/programs/reward_manager/SubmitAttestation.go similarity index 66% rename from api/solana/programs/reward-manager/SubmitAttestation.go rename to api/solana/programs/reward_manager/SubmitAttestation.go index 1fa0ca31..af3ddfed 100644 --- a/api/solana/programs/reward-manager/SubmitAttestation.go +++ b/api/solana/programs/reward_manager/SubmitAttestation.go @@ -1,24 +1,20 @@ package reward_manager import ( - "encoding/hex" - bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" ) -const ( - SenderSeedPrefix = "S_" - EthAddressByteLength = 20 - AttestationsSeedPrefix = "V_" -) - type SubmitAttestation struct { - DisbursementID string + // Instruction Data + DisbursementID string + + // Used for derivations SenderEthAddress string `bin:"-" borsh_skip:"true"` RewardManagerState solana.PublicKey `bin:"-" borsh_skip:"true"` Payer solana.PublicKey `bin:"-" borsh_skip:"true"` + // Accounts solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` } @@ -47,34 +43,6 @@ func (inst *SubmitAttestation) SetPayer(payer solana.PublicKey) *SubmitAttestati return inst } -func deriveAuthority(programId solana.PublicKey, state solana.PublicKey) (solana.PublicKey, uint8, error) { - seeds := make([][]byte, 1) - seeds[0] = state.Bytes()[0:32] - return solana.FindProgramAddress(seeds, programId) -} - -func deriveSender(programId solana.PublicKey, authority solana.PublicKey, ethAddress string) (solana.PublicKey, uint8, error) { - - senderSeedPrefix := []byte(SenderSeedPrefix) - // Remove 0x and decode hex - decodedEthAddress, err := hex.DecodeString(ethAddress[2:]) - if err != nil { - return solana.PublicKey{}, 0, err - } - // Pad the eth address if necessary w/ leading 0 - senderSeed := make([]byte, len(senderSeedPrefix)+EthAddressByteLength) - copy(senderSeed, senderSeedPrefix) - copy(senderSeed[len(senderSeed)-len(decodedEthAddress):], decodedEthAddress) - return solana.FindProgramAddress([][]byte{authority.Bytes()[0:32], senderSeed}, programId) -} - -func deriveAttestations(programId solana.PublicKey, authority solana.PublicKey, disbursementId string) (solana.PublicKey, uint8, error) { - attestationsSeed := make([]byte, len(AttestationsSeedPrefix)+len(disbursementId)) - copy(attestationsSeed, []byte(AttestationsSeedPrefix)) - copy(attestationsSeed[len([]byte(AttestationsSeedPrefix)):], disbursementId) - return solana.FindProgramAddress([][]byte{authority.Bytes()[0:32], attestationsSeed}, programId) -} - func (inst SubmitAttestation) Build() *Instruction { authority, _, _ := deriveAuthority(ProgramID, inst.RewardManagerState) sender, _, _ := deriveSender(ProgramID, authority, inst.SenderEthAddress) diff --git a/api/solana/programs/reward-manager/instruction.go b/api/solana/programs/reward_manager/instruction.go similarity index 60% rename from api/solana/programs/reward-manager/instruction.go rename to api/solana/programs/reward_manager/instruction.go index 81869fd7..fb980e69 100644 --- a/api/solana/programs/reward-manager/instruction.go +++ b/api/solana/programs/reward_manager/instruction.go @@ -2,6 +2,7 @@ package reward_manager import ( "bytes" + "encoding/hex" "fmt" bin "github.com/gagliardetto/binary" @@ -9,6 +10,13 @@ import ( ag_text "github.com/gagliardetto/solana-go/text" ) +const ( + SenderSeedPrefix = "S_" + EthAddressByteLength = 20 + AttestationsSeedPrefix = "V_" + DisbursementSeedPrefix = "T_" +) + var ProgramID = solana.MustPublicKeyFromBase58("CDpzvz7DfgbF95jSSCHLX3ERkugyfgn9Fw8ypNZ1hfXp") func SetProgramID(pubkey solana.PublicKey) { @@ -76,7 +84,7 @@ var InstructionImplDef = bin.NewVariantDefinition( Name: "SubmitAttestation", Type: (*SubmitAttestation)(nil), }, { - Name: "EvaluateAttestation", Type: (*SubmitAttestation)(nil), + Name: "EvaluateAttestation", Type: (*EvaluateAttestation)(nil), }, }, ) @@ -118,3 +126,37 @@ func DecodeInstruction(accounts []*solana.AccountMeta, data []byte) (*Instructio } return inst, nil } + +func deriveAuthority(programId solana.PublicKey, state solana.PublicKey) (solana.PublicKey, uint8, error) { + seeds := make([][]byte, 1) + seeds[0] = state.Bytes()[0:32] + return solana.FindProgramAddress(seeds, programId) +} + +func deriveSender(programId solana.PublicKey, authority solana.PublicKey, ethAddress string) (solana.PublicKey, uint8, error) { + senderSeedPrefix := []byte(SenderSeedPrefix) + // Remove 0x and decode hex + decodedEthAddress, err := hex.DecodeString(ethAddress[2:]) + if err != nil { + return solana.PublicKey{}, 0, err + } + // Pad the eth address if necessary w/ leading 0 + senderSeed := make([]byte, len(senderSeedPrefix)+EthAddressByteLength) + copy(senderSeed, senderSeedPrefix) + copy(senderSeed[len(senderSeed)-len(decodedEthAddress):], decodedEthAddress) + return solana.FindProgramAddress([][]byte{authority.Bytes()[0:32], senderSeed}, programId) +} + +func deriveAttestations(programId solana.PublicKey, authority solana.PublicKey, disbursementId string) (solana.PublicKey, uint8, error) { + attestationsSeed := make([]byte, len(AttestationsSeedPrefix)+len(disbursementId)) + copy(attestationsSeed, []byte(AttestationsSeedPrefix)) + copy(attestationsSeed[len([]byte(AttestationsSeedPrefix)):], disbursementId) + return solana.FindProgramAddress([][]byte{authority.Bytes()[0:32], attestationsSeed}, programId) +} + +func deriveDisbursement(programId solana.PublicKey, authority solana.PublicKey, disbursementId string) (solana.PublicKey, uint8, error) { + disbursementSeed := make([]byte, len(DisbursementSeedPrefix)+len(disbursementId)) + copy(disbursementSeed, []byte(DisbursementSeedPrefix)) + copy(disbursementSeed[len([]byte(DisbursementSeedPrefix)):], disbursementId) + return solana.FindProgramAddress([][]byte{authority.Bytes()[0:32], disbursementSeed}, programId) +} diff --git a/api/solana/programs/secp256k1/Secp256k1Instruction.go b/api/solana/programs/secp256k1/Secp256k1Instruction.go index d178f495..64a54ad8 100644 --- a/api/solana/programs/secp256k1/Secp256k1Instruction.go +++ b/api/solana/programs/secp256k1/Secp256k1Instruction.go @@ -128,7 +128,7 @@ func (obj *Secp256k1Instruction) UnmarshalWithDecoder(decoder *ag_binary.Decoder if err != nil { return err } - signature, err := decoder.ReadBytes(int(offsets.MessageDataOffset) - int(offsets.SignatureOffset) - 1) + signature, err := decoder.ReadBytes(int(offsets.MessageDataOffset) - int(offsets.SignatureOffset)) if err != nil { return err } diff --git a/api/solana/programs/secp256k1/Secp256k1Instruction_test.go b/api/solana/programs/secp256k1/Secp256k1Instruction_test.go new file mode 100644 index 00000000..b71f812f --- /dev/null +++ b/api/solana/programs/secp256k1/Secp256k1Instruction_test.go @@ -0,0 +1,79 @@ +package secp256k1_test + +import ( + "encoding/hex" + "testing" + + "bridgerton.audius.co/api/solana/programs/secp256k1" + "github.com/ethereum/go-ethereum/crypto" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +func TestSecp256k1Instruction(t *testing.T) { + // Expected results + ethAddress, err := hex.DecodeString("8fcfa10bd3808570987dbb5b1ef4ab74400fbfda") + require.NoError(t, err) + message, err := hex.DecodeString("68d5397bb16195ea47091010f3abb8fc6b5cdfa65f00e1f505000000005f623a33383639383d3e3530373431303135335f00b6462e955da5841b6d9e1e2529b830f00f31bf") + require.NoError(t, err) + signature, err := hex.DecodeString("f89b2e6f97f95f1306b468b10b1a18df9569b07d9d7b81b241d6fc99d9ec782e4e449f5c3c63836ed52c9344d3de5c3133fead711e421af545822f09bd78cb3900") + require.NoError(t, err) + + // Test data (taken from Secp256k1Program.test.ts) + expectedData, _ := hex.DecodeString("012000000c000061004500008fcfa10bd3808570987dbb5b1ef4ab74400fbfdaf89b2e6f97f95f1306b468b10b1a18df9569b07d9d7b81b241d6fc99d9ec782e4e449f5c3c63836ed52c9344d3de5c3133fead711e421af545822f09bd78cb390068d5397bb16195ea47091010f3abb8fc6b5cdfa65f00e1f505000000005f623a33383639383d3e3530373431303135335f00b6462e955da5841b6d9e1e2529b830f00f31bf") + + ix := secp256k1.NewSecp256k1Instruction(ethAddress, message, signature, 0).Build() + data, err := ix.Data() + + require.Equal(t, solana.Secp256k1ProgramID, ix.ProgramID()) + require.Len(t, ix.Accounts(), 0) + require.NoError(t, err) + require.Equal(t, expectedData, data) +} + +func TestUnmarshal(t *testing.T) { + // Expected results + ethAddress, err := hex.DecodeString("00b6462e955da5841b6d9e1e2529b830f00f31bf") + require.NoError(t, err) + message, err := hex.DecodeString("00ab2f814a75e9bb778ccbb998a028bb9b8a1ce1bc5f0065cd1d000000005f623a39636537613a3261633834376538") + require.NoError(t, err) + signature, err := hex.DecodeString("559ec22babe96e7d9ed0b40fd908a8da7049209cce80da05c46b4ed4b2ac996a16962d4d9b910f0bc536ee4bfe254a52cda3612f4e505ebacf8b0ea8869f6d4400") + require.NoError(t, err) + instrIndex := uint8(0) + + // Test data + data, err := hex.DecodeString("012000000c000061002f000000b6462e955da5841b6d9e1e2529b830f00f31bf559ec22babe96e7d9ed0b40fd908a8da7049209cce80da05c46b4ed4b2ac996a16962d4d9b910f0bc536ee4bfe254a52cda3612f4e505ebacf8b0ea8869f6d440000ab2f814a75e9bb778ccbb998a028bb9b8a1ce1bc5f0065cd1d000000005f623a39636537613a3261633834376538") + require.NoError(t, err) + + ix := secp256k1.NewSecp256k1InstructionBuilder() + decoder := bin.NewBorshDecoder(data) + ix.UnmarshalWithDecoder(decoder) + + require.Len(t, ix.SignatureDatas, 1) + require.Equal(t, ethAddress, ix.SignatureDatas[0].EthAddress) + require.Equal(t, message, ix.SignatureDatas[0].Message) + require.Equal(t, signature, ix.SignatureDatas[0].Signature) + require.Equal(t, instrIndex, ix.SignatureDatas[0].InstructionIndex) +} + +func TestUnmarshalVerifySignature(t *testing.T) { + // Test data + data, err := hex.DecodeString("012000000c0000610029000000b6462e955da5841b6d9e1e2529b830f00f31bf0b9e26079eabfde8da3ed9e3aa1d9a18d272bf50a11bc45ada709c9570b7b7825630f512be7673ff92cbdb5494b7e7890365d0eed04f46fc14b197dbf2f5529e0177afbe5f6e6d0b95e6b35ba205df8fbbf26f1d1f5f00c2eb0b000000005f66743a3162616561663731") + require.NoError(t, err) + + ix := secp256k1.NewSecp256k1InstructionBuilder() + decoder := bin.NewBorshDecoder(data) + ix.UnmarshalWithDecoder(decoder) + + hash := crypto.Keccak256(ix.SignatureDatas[0].Message) + recoveredWallet, err := crypto.SigToPub(hash, ix.SignatureDatas[0].Signature) + + require.NoError(t, err) + require.Equal( + t, + ix.SignatureDatas[0].EthAddress, + crypto.PubkeyToAddress(*recoveredWallet).Bytes(), + "signature recovers to declared signer eth address", + ) +}