Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add errors for Solana cases that precede confirmed #2544

Merged
merged 16 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
12 changes: 12 additions & 0 deletions services/payments/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,15 @@ type InsufficientAuthorizationsError struct{}
func (e *InsufficientAuthorizationsError) Error() string {
return "insufficient authorizations"
}

const (
SolanaTransactionUnknownError Error = "transaction status unknown"
SolanaTransactionNotFoundError Error = "transaction not found"
SolanaTransactionNotConfirmedError Error = "transaction not confirmed"
Sneagan marked this conversation as resolved.
Show resolved Hide resolved
)

type Error string

func (e Error) Error() string {
return string(e)
}
76 changes: 44 additions & 32 deletions services/payments/live_solana_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math"
"os"
Expand All @@ -19,7 +21,7 @@ import (
"github.com/blocto/solana-go-sdk/client"
"github.com/blocto/solana-go-sdk/common"
"github.com/blocto/solana-go-sdk/rpc"
"github.com/blocto/solana-go-sdk/types"
// "github.com/blocto/solana-go-sdk/types"
appctx "github.com/brave-intl/bat-go/libs/context"
"github.com/brave-intl/bat-go/libs/logging"
paymentLib "github.com/brave-intl/bat-go/libs/payments"
Expand All @@ -30,6 +32,15 @@ import (
must "github.com/stretchr/testify/require"
)

const (
// DEV NET CHAIN INFO
// mint = "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq"
// tokenAccountOwner = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
// MAIN NET CHAIN INFO
mint = "EPeUFDgHRxs9xxEPVaL6kfGQvCon7jmAWKVUHuux1Tpz"
tokenAccountOwner = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
)

/*
* The below tests rely on some environment variables in order to execute:
* SOLANA_SIGNING_KEY
Expand All @@ -42,22 +53,18 @@ TestLiveSolanaStateMachineATAMissing tests for correct state progression from
Initialized to Paid with a payee account that is missing the SPL-BAT ATA.
*/
func TestLiveSolanaStateMachineATAMissing(t *testing.T) {
const (
mint = "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq"
tokenAccountOwner = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
)

ctx, _ := logging.SetupLogger(context.WithValue(context.Background(), appctx.DebugLoggingCTXKey, true))

// New account for every test execution to ensure that the account does
// not already have its ATA configured.
payeeAccount := types.NewAccount()
// not already have its ATA configured. Only used on devnet
//payeeAccount := types.NewAccount()

state := paymentLib.AuthenticatedPaymentState{
Status: paymentLib.Prepared,
PaymentDetails: paymentLib.PaymentDetails{
Amount: decimal.NewFromFloat(1.4),
To: payeeAccount.PublicKey.ToBase58(),
Amount: decimal.NewFromFloat(0.01),
// Hard coded to address that we control for main net testing
To: "4Pyz7vNKfjERMyCwecRhqurLwJWbs2HLKHuxW9QakqR9",
From: os.Getenv("SOLANA_PAYER_ADDRESS"),
Custodian: "solana",
PayoutID: "4b2f22c9-f227-43b3-98d2-4a5337b65bc5",
Expand All @@ -78,7 +85,7 @@ func TestLiveSolanaStateMachineATAMissing(t *testing.T) {
)

createdAta, _, err := common.FindAssociatedTokenAddress(
payeeAccount.PublicKey,
common.PublicKeyFromString(state.PaymentDetails.To),
common.PublicKeyFromString(mint),
)
must.Nil(t, err)
Expand Down Expand Up @@ -110,18 +117,14 @@ TestLiveSolanaStateMachineATAPresent tests for correct state progression from
Initialized to Paid with a payee account that has the SPL-BAT ATA configured.
*/
func TestLiveSolanaStateMachineATAPresent(t *testing.T) {
const (
mint = "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq"
)

ctx, _ := logging.SetupLogger(context.WithValue(context.Background(), appctx.DebugLoggingCTXKey, true))

state := paymentLib.AuthenticatedPaymentState{
Status: paymentLib.Prepared,
PaymentDetails: paymentLib.PaymentDetails{
Amount: decimal.NewFromFloat(1.4),
Amount: decimal.NewFromFloat(0.01),
// Fixed To address that has the ATA configured already
To: "5g7xMFn9bk8vyZdfsr4mAfUWKWDaWxzZBH5Cb1XHftBL",
To: "4Pyz7vNKfjERMyCwecRhqurLwJWbs2HLKHuxW9QakqR9",
From: os.Getenv("SOLANA_PAYER_ADDRESS"),
Custodian: "solana",
PayoutID: "4b2f22c9-f227-43b3-98d2-4a5337b65bc5",
Expand Down Expand Up @@ -151,17 +154,13 @@ func TestLiveSolanaStateMachineATAPresent(t *testing.T) {
}

func TestLiveSolanaStateMachineDropped(t *testing.T) {
const (
mint = "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq"
)

ctx, _ := logging.SetupLogger(context.WithValue(context.Background(), appctx.DebugLoggingCTXKey, true))

state := paymentLib.AuthenticatedPaymentState{
Status: paymentLib.Prepared,
PaymentDetails: paymentLib.PaymentDetails{
Amount: decimal.NewFromFloat(1.4),
To: "5g7xMFn9bk8vyZdfsr4mAfUWKWDaWxzZBH5Cb1XHftBL",
Amount: decimal.NewFromFloat(0.01),
To: "4Pyz7vNKfjERMyCwecRhqurLwJWbs2HLKHuxW9QakqR9",
From: os.Getenv("SOLANA_PAYER_ADDRESS"),
Custodian: "solana",
PayoutID: "4b2f22c9-f227-43b3-98d2-4a5337b65bc5",
Expand Down Expand Up @@ -228,14 +227,19 @@ func checkTransactionMatchesPaymentDetails(
Commitment: rpc.CommitmentConfirmed,
})
must.Nil(t, err)
defer func() {
if err := recover(); err != nil {
t.Logf("panic occurred introspecting transaction:\n%+v\nwith error:\n%s", txn, err)
}
}()
if innerTxn, ok := txn.Result.Transaction.(map[string]interface{}); ok {
if message, ok := innerTxn["message"].(map[string]interface{}); ok {
if instructions, ok := message["instructions"].([]interface{}); ok {
if instructionOne, ok := instructions[2].(map[string]interface{}); ok {
if parsed, ok := instructionOne["parsed"].(map[string]interface{}); ok {
if info, ok := parsed["info"].(map[string]interface{}); ok {
t.Log("Verifying chain transaction mint, to, and from")
should.Equal(t, "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq", info["mint"])
should.Equal(t, mint, info["mint"])
should.Equal(t, state.PaymentDetails.To, info["wallet"])
should.Equal(t, state.PaymentDetails.From, info["source"])
} else {
Expand Down Expand Up @@ -324,9 +328,9 @@ func driveHappyPathTransitions(
should.Equal(t, idempotencyData, transaction.ExternalIdempotency)
t.Log("State is Pending")

for i := 1; i < 30; i++ {
t.Log("Checking for Paid status")
time.Sleep(100 * time.Millisecond)
for i := 1; i < 65; i++ {
t.Logf("Checking for Paid status (%d/65)\nFound status: %s\nLast error: %v", i, transaction.Status, transaction.LastError)
time.Sleep(1 * time.Second)
md, _ := json.Marshal(transaction)
mockTransitionHistory.Data.UnsafePaymentState = md
solMachine.setTransaction(transaction)
Expand All @@ -336,8 +340,8 @@ func driveHappyPathTransitions(
break
}
}
should.Equal(t, paymentLib.Paid, transaction.Status)
should.Equal(t, idempotencyData, transaction.ExternalIdempotency)
must.Equal(t, paymentLib.Paid, transaction.Status, fmt.Sprintf("%+v", transaction))
must.Equal(t, idempotencyData, transaction.ExternalIdempotency)
t.Log("State is Paid")

return transaction
Expand All @@ -352,16 +356,18 @@ func setupState(
QLDBPaymentTransitionHistoryEntry,
[]byte,
) {
keyBytes, err := base64.StdEncoding.DecodeString(os.Getenv("SOLANA_SIGNING_KEY"))
must.Nil(t, err)
solMachine := SolanaMachine{
signingKey: os.Getenv("SOLANA_SIGNING_KEY"),
signingKey: keyBytes,
solanaRpcClient: *client.NewClient(os.Getenv("SOLANA_RPC_ENDPOINT")),
splMintAddress: mint, // SPL mint address on devnet
splMintDecimals: 8, // SPL mint decimals on devnet
}
idempotencyKey, err := uuid.Parse("1803df27-f29c-537a-9384-bb5b523ea3f7")
must.Nil(t, err)

marshaledData, _ := json.Marshal(state)
marshaledData, err := json.Marshal(state)
must.Nil(t, err)
privkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
must.Nil(t, err)
Expand Down Expand Up @@ -423,6 +429,12 @@ func transition(
mth.Data.UnsafePaymentState = md
sm.setTransaction(&ts)
newTransaction, err := Drive(ctx, &sm)
must.Nil(t, err)
should.True(
t,
err == nil ||
errors.Is(err, SolanaTransactionNotConfirmedError) ||
errors.Is(err, SolanaTransactionNotFoundError) ||
errors.Is(err, SolanaTransactionUnknownError),
)
return newTransaction
}
6 changes: 3 additions & 3 deletions services/payments/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import (

"filippo.io/age"
"filippo.io/age/agessh"
"github.com/brave-intl/bat-go/libs/logging"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kms"
kmsTypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
solTypes "github.com/blocto/solana-go-sdk/types"
appctx "github.com/brave-intl/bat-go/libs/context"
"github.com/brave-intl/bat-go/libs/logging"
"github.com/brave-intl/bat-go/libs/nitro"
nitroawsutils "github.com/brave-intl/bat-go/libs/nitro/aws"
paymentLib "github.com/brave-intl/bat-go/libs/payments"
Expand Down Expand Up @@ -191,7 +191,7 @@ func encryptShares(shares [][]byte, operatorKeys []string) ([]paymentLib.Operato
for i, share := range shares {
recipient, err := agessh.ParseRecipient(operatorKeys[i])
if err != nil {
return nil, fmt.Errorf("failed to parse public key", err)
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
buf := new(bytes.Buffer)
// encrypt each with an operator recipient
Expand All @@ -201,7 +201,7 @@ func encryptShares(shares [][]byte, operatorKeys []string) ([]paymentLib.Operato
}

if _, err = io.WriteString(w, base64.StdEncoding.EncodeToString(share)); err != nil {
return nil, fmt.Errorf("failed to write encoded ciphertext to encrypted buffer", err)
return nil, fmt.Errorf("failed to write encoded ciphertext to encrypted buffer: %w", err)
}

// Cannot defer this close because we are writing and using this writer in a loop. If this
Expand Down
22 changes: 14 additions & 8 deletions services/payments/statemachine.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,12 @@ func (s *Service) DriveTransaction(
state, lastErr := Drive(ctx, stateMachine)
if state != nil {
var errTmp paymentLib.PaymentError
// If the error is already a PaymentError use it
if errors.As(lastErr, &errTmp) {
state.LastError = &errTmp
} else if lastErr != nil {
// Assume any non-categorized error is temporary
// If it's not a PaymentError turn it into one. Assume any error coming to us without
// PaymentError structure is temporary
state.LastError = paymentLib.ProcessingErrorFromError(lastErr, true)
} else {
state.LastError = nil
Expand All @@ -218,20 +220,24 @@ func (s *Service) DriveTransaction(
// current persisted state
history, err := s.datastore.GetPaymentStateHistory(ctx, state.DocumentID)
if err != nil {
return fmt.Errorf("failed to get history from document id", err)
return fmt.Errorf("failed to get history from document id: %w", err)
}
persistedState, err := history.GetAuthenticatedPaymentState(
s.verifierStore,
state.DocumentID,
)
if err != nil {
return fmt.Errorf("failed to validate payment state history", err)
return fmt.Errorf("failed to validate payment state history: %w", err)
}
// If there is idempotency data in qldb and it is different from the idempotency data in the
// current state it means that there was a race between two calls to Authenticate and we are
// operating on the loser. There is no risk to proceeding as long as we retain the winner
// idempotency.
if persistedState != nil && !bytes.Equal(state.ExternalIdempotency, persistedState.ExternalIdempotency) {
// If there is idempotency data in qldb and in our generated state and
// it is different from the idempotency data in the current state it
// means that there was a race between two calls to Authenticate and we
// are operating on the loser. There is no risk to proceeding as long as
// we retain the winner idempotency.
if persistedState != nil &&
persistedState.ExternalIdempotency != nil &&
state.ExternalIdempotency != nil &&
!bytes.Equal(state.ExternalIdempotency, persistedState.ExternalIdempotency) {
Comment on lines +237 to +240
Copy link
Contributor

Choose a reason for hiding this comment

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

At some point (soon) it might be good to extract this into a separate predicate to be able to test it in isolation.

state.ExternalIdempotency = persistedState.ExternalIdempotency
}

Expand Down
Loading