diff --git a/changelog.md b/changelog.md index cfa004ea47..fb481eae11 100644 --- a/changelog.md +++ b/changelog.md @@ -32,6 +32,7 @@ * [2372](https://github.com/zeta-chain/node/pull/2372) - add queries for tss fund migration info * [2416](https://github.com/zeta-chain/node/pull/2416) - add Solana chain information * [2465](https://github.com/zeta-chain/node/pull/2465) - add Solana inbound SOL token observation +* [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore ### Refactor diff --git a/pkg/chains/address.go b/pkg/chains/address.go index 5eb0040485..c1a6bb0a7e 100644 --- a/pkg/chains/address.go +++ b/pkg/chains/address.go @@ -1,12 +1,13 @@ package chains import ( - "errors" "fmt" "strings" "github.com/btcsuite/btcutil" eth "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" + "github.com/pkg/errors" ) type Address string @@ -87,6 +88,23 @@ func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Addre return } +// DecodeSolanaWalletAddress decodes a Solana wallet address from a given string. +func DecodeSolanaWalletAddress(inputAddress string) (pk solana.PublicKey, err error) { + // decode the Base58 encoded address + pk, err = solana.PublicKeyFromBase58(inputAddress) + if err != nil { + return solana.PublicKey{}, errors.Wrapf(err, "error decoding solana wallet address %s", inputAddress) + } + + // there are two types of Solana addresses. + // accept address that is generated from keypair. + // reject off-curve address such as program derived address from 'findProgramAddress'. + if !pk.IsOnCurve() { + return solana.PublicKey{}, fmt.Errorf("address %s is not on ed25519 curve", inputAddress) + } + return +} + // IsBtcAddressSupported returns true if the given BTC address is supported func IsBtcAddressSupported(addr btcutil.Address) bool { switch addr.(type) { diff --git a/pkg/chains/address_test.go b/pkg/chains/address_test.go index cfdf53bd08..0a35cf831a 100644 --- a/pkg/chains/address_test.go +++ b/pkg/chains/address_test.go @@ -98,6 +98,54 @@ func TestDecodeBtcAddress(t *testing.T) { }) } +func Test_DecodeSolanaWalletAddress(t *testing.T) { + tests := []struct { + name string + address string + want string + errorMessage string + }{ + { + name: "should decode a valid Solana wallet address", + address: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw", + want: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw", + errorMessage: "", + }, + { + name: "should fail to decode an address with invalid length", + address: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveK", + want: "", + errorMessage: "error decoding solana wallet address", + }, + { + name: "should fail to decode an invalid Base58 address", + address: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFl", // contains invalid character 'l' + want: "", + errorMessage: "error decoding solana wallet address", + }, + { + name: "should fail to decode a program derived address (not on ed25519 curve)", + address: "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + want: "", + errorMessage: "is not on ed25519 curve", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pk, err := DecodeSolanaWalletAddress(tt.address) + if tt.errorMessage != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMessage) + return + } + + require.NoError(t, err) + require.Equal(t, tt.want, pk.String()) + }) + } +} + func Test_IsBtcAddressSupported_P2TR(t *testing.T) { tests := []struct { name string diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 6e2ea1d500..eda77a0c17 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -52,13 +52,14 @@ func (chain Chain) IsExternalChain() bool { // on EVM chain, it is 20Bytes // on Bitcoin chain, it is P2WPKH address, []byte(bech32 encoded string) func (chain Chain) EncodeAddress(b []byte) (string, error) { - if chain.Consensus == Consensus_ethereum { + switch chain.Consensus { + case Consensus_ethereum: addr := ethcommon.BytesToAddress(b) if addr == (ethcommon.Address{}) { return "", fmt.Errorf("invalid EVM address") } return addr.Hex(), nil - } else if chain.Consensus == Consensus_bitcoin { + case Consensus_bitcoin: addrStr := string(b) chainParams, err := GetBTCChainParams(chain.ChainId) if err != nil { @@ -72,8 +73,15 @@ func (chain Chain) EncodeAddress(b []byte) (string, error) { return "", fmt.Errorf("address is not for network %s", chainParams.Name) } return addrStr, nil + case Consensus_solana_consensus: + pk, err := DecodeSolanaWalletAddress(string(b)) + if err != nil { + return "", err + } + return pk.String(), nil + default: + return "", fmt.Errorf("chain id %d not supported", chain.ChainId) } - return "", fmt.Errorf("chain (%d) not supported", chain.ChainId) } func (chain Chain) IsEVMChain() bool { @@ -111,7 +119,7 @@ func IsEVMChain(chainID int64, additionalChains []Chain) bool { // additionalChains is a list of additional chains to search from // in practice, it is used in the protocol to dynamically support new chains without doing an upgrade func IsBitcoinChain(chainID int64, additionalChains []Chain) bool { - return ChainIDInChainList(chainID, ChainListByConsensus(Consensus_bitcoin, additionalChains)) + return ChainIDInChainList(chainID, ChainListByNetwork(Network_btc, additionalChains)) } // IsSolanaChain returns true if the chain is a Solana chain diff --git a/pkg/chains/chain_test.go b/pkg/chains/chain_test.go index e9551d35c7..97c6855b77 100644 --- a/pkg/chains/chain_test.go +++ b/pkg/chains/chain_test.go @@ -1,9 +1,10 @@ package chains_test import ( - "github.com/zeta-chain/zetacore/testutil/sample" "testing" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/btcsuite/btcd/chaincfg" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" @@ -146,43 +147,43 @@ func TestChain_EncodeAddress(t *testing.T) { wantErr bool }{ { - name: "should error if b is not a valid address on the bitcoin network", - chain: chains.Chain{ - ChainName: chains.ChainName_btc_testnet, - ChainId: 18332, - Consensus: chains.Consensus_bitcoin, - }, + name: "should error if b is not a valid address on the bitcoin network", + chain: chains.BitcoinTestnet, b: []byte("bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c"), want: "", wantErr: true, }, { - name: "should pass if b is a valid address on the network", - chain: chains.Chain{ - ChainName: chains.ChainName_btc_mainnet, - ChainId: 8332, - Consensus: chains.Consensus_bitcoin, - }, + name: "should pass if b is a valid address on the network", + chain: chains.BitcoinMainnet, b: []byte("bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c"), want: "bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c", wantErr: false, }, { - name: "should error if b is not a valid address on the evm network", - chain: chains.Chain{ - ChainName: chains.ChainName_goerli_testnet, - ChainId: 5, - }, + name: "should pass if b is a valid wallet address on the solana network", + chain: chains.SolanaMainnet, + b: []byte("DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw"), + want: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw", + wantErr: false, + }, + { + name: "should error if b is not a valid Base58 address", + chain: chains.SolanaMainnet, + b: []byte("9G0P8HkKqegZ7B6cE2hGvkZjHjSH14WZXDNZQmwYLokAc"), // contains invalid digit '0' + want: "", + wantErr: true, + }, + { + name: "should error if b is not a valid address on the evm network", + chain: chains.Ethereum, b: ethcommon.Hex2Bytes("0x321"), want: "", wantErr: true, }, { - name: "should pass if b is a valid address on the evm network", - chain: chains.Chain{ - ChainName: chains.ChainName_goerli_testnet, - ChainId: 5, - }, + name: "should pass if b is a valid address on the evm network", + chain: chains.Ethereum, b: []byte("0x321"), want: "0x0000000000000000000000000000003078333231", wantErr: false, @@ -294,6 +295,12 @@ func TestDecodeAddressFromChainID(t *testing.T) { addr: "bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c", want: []byte("bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c"), }, + { + name: "Solana", + chainID: chains.SolanaMainnet.ChainId, + addr: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw", + want: []byte("DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw"), + }, { name: "Non-supported chain", chainID: 9999, diff --git a/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go b/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go index 0f09c61025..bbd4530484 100644 --- a/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go +++ b/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go @@ -623,6 +623,7 @@ func TestKeeper_CheckMigration(t *testing.T) { authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) chain := chains.Chain{ ChainId: 999, + Network: chains.Network_btc, Consensus: chains.Consensus_bitcoin, CctxGateway: chains.CCTXGateway_observers, }