Skip to content

Commit

Permalink
test: extend solana unit tests (#3158)
Browse files Browse the repository at this point in the history
* improve inbound tests

* fix terminology

* add outbound tests for withdraw spl

* PR comments

* PR comment
  • Loading branch information
skosito authored Nov 15, 2024
1 parent 9c9b808 commit 3c22fbb
Show file tree
Hide file tree
Showing 11 changed files with 478 additions and 64 deletions.
4 changes: 2 additions & 2 deletions e2e/runner/setup_solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ func (r *E2ERunner) SetupSolana(gatewayID, deployerPrivateKey string) {
require.NoError(r, err)

// deploy test spl
tokenAccount := r.DeploySPL(&privkey, true)
r.SPLAddr = tokenAccount.PublicKey()
mintAccount := r.DeploySPL(&privkey, true)
r.SPLAddr = mintAccount.PublicKey()
}

func (r *E2ERunner) ensureSolanaChainParams() error {
Expand Down
36 changes: 18 additions & 18 deletions e2e/runner/solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ func (r *E2ERunner) CreateSignedTransaction(
func (r *E2ERunner) ResolveSolanaATA(
payer solana.PrivateKey,
owner solana.PublicKey,
tokenAccount solana.PublicKey,
mintAccount solana.PublicKey,
) solana.PublicKey {
pdaAta, _, err := solana.FindAssociatedTokenAddress(owner, tokenAccount)
pdaAta, _, err := solana.FindAssociatedTokenAddress(owner, mintAccount)
require.NoError(r, err)

info, _ := r.SolanaClient.GetAccountInfo(r.Ctx, pdaAta)
Expand All @@ -172,7 +172,7 @@ func (r *E2ERunner) ResolveSolanaATA(
return pdaAta
}
// doesn't exist, create it
ataInstruction := associatedtokenaccount.NewCreateInstruction(payer.PublicKey(), owner, tokenAccount).Build()
ataInstruction := associatedtokenaccount.NewCreateInstruction(payer.PublicKey(), owner, mintAccount).Build()
signedTx := r.CreateSignedTransaction(
[]solana.Instruction{ataInstruction},
payer,
Expand All @@ -188,27 +188,27 @@ func (r *E2ERunner) ResolveSolanaATA(
func (r *E2ERunner) SPLDepositAndCall(
privateKey *solana.PrivateKey,
amount uint64,
tokenAccount solana.PublicKey,
mintAccount solana.PublicKey,
receiver ethcommon.Address,
data []byte,
) solana.Signature {
// ata for pda
pda := r.ComputePdaAddress()
pdaAta := r.ResolveSolanaATA(*privateKey, pda, tokenAccount)
pdaAta := r.ResolveSolanaATA(*privateKey, pda, mintAccount)

// deployer ata
ata := r.ResolveSolanaATA(*privateKey, privateKey.PublicKey(), tokenAccount)
ata := r.ResolveSolanaATA(*privateKey, privateKey.PublicKey(), mintAccount)

// deposit spl
seed := [][]byte{[]byte("whitelist"), tokenAccount.Bytes()}
seed := [][]byte{[]byte("whitelist"), mintAccount.Bytes()}
whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram)
require.NoError(r, err)

depositSPLInstruction := r.CreateDepositSPLInstruction(
amount,
privateKey.PublicKey(),
whitelistEntryPDA,
tokenAccount,
mintAccount,
ata,
pdaAta,
receiver,
Expand All @@ -231,36 +231,36 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so
require.NoError(r, err)

// to deploy new spl token, create account instruction and initialize mint instruction have to be in the same transaction
tokenAccount := solana.NewWallet()
mintAccount := solana.NewWallet()
createAccountInstruction := system.NewCreateAccountInstruction(
lamport,
token.MINT_SIZE,
solana.TokenProgramID,
privateKey.PublicKey(),
tokenAccount.PublicKey(),
mintAccount.PublicKey(),
).Build()

initializeMintInstruction := token.NewInitializeMint2Instruction(
6,
privateKey.PublicKey(),
privateKey.PublicKey(),
tokenAccount.PublicKey(),
mintAccount.PublicKey(),
).Build()

signedTx := r.CreateSignedTransaction(
[]solana.Instruction{createAccountInstruction, initializeMintInstruction},
*privateKey,
[]solana.PrivateKey{tokenAccount.PrivateKey},
[]solana.PrivateKey{mintAccount.PrivateKey},
)

// broadcast the transaction and wait for finalization
_, out := r.BroadcastTxSync(signedTx)
r.Logger.Info("create spl logs: %v", out.Meta.LogMessages)

// minting some tokens to deployer for testing
ata := r.ResolveSolanaATA(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey())
ata := r.ResolveSolanaATA(*privateKey, privateKey.PublicKey(), mintAccount.PublicKey())

mintToInstruction := token.NewMintToInstruction(uint64(1_000_000_000), tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).
mintToInstruction := token.NewMintToInstruction(uint64(1_000_000_000), mintAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).
Build()
signedTx = r.CreateSignedTransaction(
[]solana.Instruction{mintToInstruction},
Expand All @@ -274,7 +274,7 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so

// optionally whitelist spl token in gateway
if whitelist {
seed := [][]byte{[]byte("whitelist"), tokenAccount.PublicKey().Bytes()}
seed := [][]byte{[]byte("whitelist"), mintAccount.PublicKey().Bytes()}
whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram)
require.NoError(r, err)

Expand All @@ -283,14 +283,14 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so

// already whitelisted
if whitelistEntryInfo != nil {
return tokenAccount
return mintAccount
}

// create 'whitelist_spl_mint' instruction
instruction := r.CreateWhitelistSPLMintInstruction(
privateKey.PublicKey(),
whitelistEntryPDA,
tokenAccount.PublicKey(),
mintAccount.PublicKey(),
)
// create and sign the transaction
signedTx := r.CreateSignedTransaction([]solana.Instruction{instruction}, *privateKey, []solana.PrivateKey{})
Expand All @@ -304,7 +304,7 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so
require.NotNil(r, whitelistEntryInfo)
}

return tokenAccount
return mintAccount
}

// BroadcastTxSync broadcasts a transaction and waits for it to be finalized
Expand Down
2 changes: 1 addition & 1 deletion pkg/contracts/solana/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func ParseGatewayWithPDA(gatewayAddress string) (solana.PublicKey, solana.Public
return gatewayID, pda, err
}

// ParseRentPayerPda parses the rent payer program derived address from the given string
// ParseRentPayerPDA parses the rent payer program derived address from the given string
func RentPayerPDA(gateway solana.PublicKey) (solana.PublicKey, error) {
var rentPayerPda solana.PublicKey
seed := []byte(RentPayerPDASeed)
Expand Down
14 changes: 7 additions & 7 deletions pkg/contracts/solana/gateway_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ type MsgWithdrawSPL struct {
// amount is the lamports amount for the withdraw_spl
amount uint64

// tokenAccount is the address for the spl token
tokenAccount solana.PublicKey
// mintAccount is the address for the spl token
mintAccount solana.PublicKey

// decimals of spl token
decimals uint8
Expand All @@ -139,15 +139,15 @@ type MsgWithdrawSPL struct {
func NewMsgWithdrawSPL(
chainID, nonce, amount uint64,
decimals uint8,
tokenAccount, to, toAta solana.PublicKey,
mintAccount, to, toAta solana.PublicKey,
) *MsgWithdrawSPL {
return &MsgWithdrawSPL{
chainID: chainID,
nonce: nonce,
amount: amount,
to: to,
recipientAta: toAta,
tokenAccount: tokenAccount,
mintAccount: mintAccount,
decimals: decimals,
}
}
Expand Down Expand Up @@ -176,8 +176,8 @@ func (msg *MsgWithdrawSPL) RecipientAta() solana.PublicKey {
return msg.recipientAta
}

func (msg *MsgWithdrawSPL) TokenAccount() solana.PublicKey {
return msg.tokenAccount
func (msg *MsgWithdrawSPL) MintAccount() solana.PublicKey {
return msg.mintAccount
}

func (msg *MsgWithdrawSPL) Decimals() uint8 {
Expand All @@ -200,7 +200,7 @@ func (msg *MsgWithdrawSPL) Hash() [32]byte {
binary.BigEndian.PutUint64(buff, msg.amount)
message = append(message, buff...)

message = append(message, msg.tokenAccount.Bytes()...)
message = append(message, msg.mintAccount.Bytes()...)

message = append(message, msg.recipientAta.Bytes()...)

Expand Down
49 changes: 39 additions & 10 deletions pkg/contracts/solana/gateway_message_test.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,78 @@
package solana_test

import (
"bytes"
"encoding/hex"
"testing"

"github.com/stretchr/testify/require"

"github.com/gagliardetto/solana-go"
"github.com/zeta-chain/node/pkg/chains"
contracts "github.com/zeta-chain/node/pkg/contracts/solana"
"github.com/zeta-chain/node/testutil"
)

func Test_MsgWithdrawHash(t *testing.T) {
t.Run("should pass for archived inbound, receipt and cctx", func(t *testing.T) {
t.Run("should calculate expected hash", func(t *testing.T) {
// ARRANGE
// #nosec G115 always positive
chainID := uint64(chains.SolanaLocalnet.ChainId)
nonce := uint64(0)
amount := uint64(1336000)
to := solana.MustPublicKeyFromBase58("37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ")

wantHash := "aa609ef9480303e8d743f6e36fe1bea0cc56b8d27dcbd8220846125c1181b681"
wantHashBytes, err := hex.DecodeString(wantHash)
require.NoError(t, err)
wantHashBytes := testutil.HexToBytes(t, wantHash)

// ACT
// create new withdraw message
hash := contracts.NewMsgWithdraw(chainID, nonce, amount, to).Hash()
require.True(t, bytes.Equal(hash[:], wantHashBytes))

// ASSERT
require.EqualValues(t, hash[:], wantHashBytes)
})
}

func Test_MsgWhitelistHash(t *testing.T) {
t.Run("should pass for archived inbound, receipt and cctx", func(t *testing.T) {
t.Run("should calculate expected hash", func(t *testing.T) {
// ARRANGE
// #nosec G115 always positive
chainID := uint64(chains.SolanaLocalnet.ChainId)
nonce := uint64(0)
whitelistCandidate := solana.MustPublicKeyFromBase58("37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ")
whitelistEntry := solana.MustPublicKeyFromBase58("2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s")

wantHash := "cde8fa3ab24b50320db1c47f30492e789177d28e76208176f0a52b8ed54ce2dd"
wantHashBytes, err := hex.DecodeString(wantHash)
require.NoError(t, err)
wantHashBytes := testutil.HexToBytes(t, wantHash)

// ACT
// create new withdraw message
hash := contracts.NewMsgWhitelist(whitelistCandidate, whitelistEntry, chainID, nonce).Hash()
require.True(t, bytes.Equal(hash[:], wantHashBytes))

// ASSERT
require.EqualValues(t, hash[:], wantHashBytes)
})
}

func Test_MsgWithdrawSPLHash(t *testing.T) {
t.Run("should calculate expected hash", func(t *testing.T) {
// ARRANGE
// #nosec G115 always positive
chainID := uint64(chains.SolanaLocalnet.ChainId)
nonce := uint64(0)
amount := uint64(1336000)
mintAccount := solana.MustPublicKeyFromBase58("AS48jKNQsDGkEdDvfwu1QpqjtqbCadrAq9nGXjFmdX3Z")
to := solana.MustPublicKeyFromBase58("37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ")
toAta, _, err := solana.FindAssociatedTokenAddress(to, mintAccount)
require.NoError(t, err)

wantHash := "87fa5c0ed757c6e1ea9d8976537eaf7868bc1f1bbf55ab198a01645d664fe0ae"
wantHashBytes := testutil.HexToBytes(t, wantHash)

// ACT
// create new withdraw message
hash := contracts.NewMsgWithdrawSPL(chainID, nonce, amount, 8, mintAccount, to, toAta).Hash()

// ASSERT
require.EqualValues(t, hash[:], wantHashBytes)
})
}
43 changes: 31 additions & 12 deletions pkg/contracts/solana/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,34 +94,53 @@ func ParseInboundAsDepositSPL(
}, nil
}

// GetSignerDeposit returns the signer address of the deposit instruction
// Note: solana-go is not able to parse the AccountMeta 'is_signer' ATM. This is a workaround.
// getSignerDeposit returns the signer address of the deposit instruction
func getSignerDeposit(tx *solana.Transaction, inst *solana.CompiledInstruction) (string, error) {
instructionAccounts, err := inst.ResolveInstructionAccounts(&tx.Message)
if err != nil {
return "", err
}

// there should be 3 accounts for a deposit instruction
if len(inst.Accounts) != accountsNumDeposit {
return "", fmt.Errorf("want %d accounts, got %d", accountsNumDeposit, len(inst.Accounts))
if len(instructionAccounts) != accountsNumDeposit {
return "", fmt.Errorf("want %d accounts, got %d", accountsNumDeposit, len(instructionAccounts))
}

// sender is the signer account
return tx.Message.AccountKeys[0].String(), nil
// the accounts are [signer, pda, system_program]
// check if first account is signer
if !instructionAccounts[0].IsSigner {
return "", fmt.Errorf("not signer %s", instructionAccounts[0].PublicKey.String())
}

return instructionAccounts[0].PublicKey.String(), nil
}

// getSignerAndSPLFromDepositSPLAccounts returns the signer and spl address of the deposit_spl instruction
func getSignerAndSPLFromDepositSPLAccounts(
tx *solana.Transaction,
inst *solana.CompiledInstruction,
) (string, string, error) {
instructionAccounts, err := inst.ResolveInstructionAccounts(&tx.Message)
if err != nil {
return "", "", err
}

// there should be 7 accounts for a deposit spl instruction
if len(inst.Accounts) != accountsNumberDepositSPL {
if len(instructionAccounts) != accountsNumberDepositSPL {
return "", "", fmt.Errorf(
"want %d accounts, got %d",
accountsNumberDepositSPL,
len(inst.Accounts),
len(instructionAccounts),
)
}

// the accounts are [signer, pda, whitelist_entry, mint_account, token_program, from, to]
signer := tx.Message.AccountKeys[0]
spl := tx.Message.AccountKeys[inst.Accounts[3]]
// check if first account is signer
if !instructionAccounts[0].IsSigner {
return "", "", fmt.Errorf("not signer %s", instructionAccounts[0].PublicKey.String())
}

signer := instructionAccounts[0].PublicKey.String()
spl := instructionAccounts[3].PublicKey.String()

return signer.String(), spl.String(), nil
return signer, spl, nil
}
Loading

0 comments on commit 3c22fbb

Please sign in to comment.