Skip to content

Commit

Permalink
add relayer_key_balance metrics and unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ws4charlie committed Aug 13, 2024
1 parent 6a64051 commit 66b7027
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 18 deletions.
23 changes: 23 additions & 0 deletions pkg/crypto/privkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package crypto

import (
fmt "fmt"

"github.com/gagliardetto/solana-go"
"github.com/pkg/errors"
)

// SolanaPrivateKeyFromString converts a base58 encoded private key to a solana.PrivateKey
func SolanaPrivateKeyFromString(privKeyBase58 string) (*solana.PrivateKey, error) {
privateKey, err := solana.PrivateKeyFromBase58(privKeyBase58)
if err != nil {
return nil, errors.Wrap(err, "invalid base58 private key")
}

// Solana private keys are 64 bytes long
if len(privateKey) != 64 {
return nil, fmt.Errorf("invalid private key length: %d", len(privateKey))
}

return &privateKey, nil
}
67 changes: 67 additions & 0 deletions pkg/crypto/privkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package crypto_test

import (
"testing"

"github.com/gagliardetto/solana-go"
"github.com/stretchr/testify/require"
"github.com/zeta-chain/zetacore/pkg/crypto"
)

func Test_IsValidSolanaPrivateKey(t *testing.T) {
tests := []struct {
name string
input string
output *solana.PrivateKey
errMsg string
}{
{
name: "valid private key",
input: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ",
output: func() *solana.PrivateKey {
privKey, _ := solana.PrivateKeyFromBase58(
"3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ",
)
return &privKey
}(),
},
{
name: "invalid private key - too short",
input: "oR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ",
output: nil,
errMsg: "invalid private key length: 38",
},
{
name: "invalid private key - too long",
input: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQdJ",
output: nil,
errMsg: "invalid private key length: 66",
},
{
name: "invalid private key - bad base58 encoding",
input: "!!!InvalidBase58!!!",
output: nil,
errMsg: "invalid base58 private key",
},
{
name: "invalid private key - empty string",
input: "",
output: nil,
errMsg: "invalid base58 private key",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := crypto.SolanaPrivateKeyFromString(tt.input)
if tt.errMsg != "" {
require.ErrorContains(t, err, tt.errMsg)
require.Nil(t, result)
return
}

require.NoError(t, err)
require.Equal(t, tt.output.String(), result.String())
})
}
}
5 changes: 5 additions & 0 deletions zetaclient/chains/interfaces/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ type SolanaRPCClient interface {
GetHealth(ctx context.Context) (string, error)
GetSlot(ctx context.Context, commitment solrpc.CommitmentType) (uint64, error)
GetAccountInfo(ctx context.Context, account solana.PublicKey) (*solrpc.GetAccountInfoResult, error)
GetBalance(
ctx context.Context,
account solana.PublicKey,
commitment solrpc.CommitmentType,
) (*solrpc.GetBalanceResult, error)
GetRecentBlockhash(ctx context.Context, commitment solrpc.CommitmentType) (*solrpc.GetRecentBlockhashResult, error)
GetRecentPrioritizationFees(
ctx context.Context,
Expand Down
22 changes: 19 additions & 3 deletions zetaclient/chains/solana/signer/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/zeta-chain/zetacore/pkg/chains"
"github.com/zeta-chain/zetacore/pkg/coin"
contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana"
"github.com/zeta-chain/zetacore/pkg/crypto"
"github.com/zeta-chain/zetacore/x/crosschain/types"
observertypes "github.com/zeta-chain/zetacore/x/observer/types"
"github.com/zeta-chain/zetacore/zetaclient/chains/base"
Expand Down Expand Up @@ -69,12 +70,11 @@ func NewSigner(

// construct Solana private key if present
if relayerKey != nil {
privKey, err := solana.PrivateKeyFromBase58(relayerKey.PrivateKey)
signer.relayerKey, err = crypto.SolanaPrivateKeyFromString(relayerKey.PrivateKey)
if err != nil {
return nil, errors.Wrap(err, "unable to construct solana private key")
}
signer.relayerKey = &privKey
logger.Std.Info().Msgf("Solana relayer address: %s", privKey.PublicKey())
logger.Std.Info().Msgf("Solana relayer address: %s", signer.relayerKey.PublicKey())
} else {
logger.Std.Info().Msg("Solana relayer key is not provided")
}
Expand Down Expand Up @@ -138,6 +138,9 @@ func (signer *Signer) TryProcessOutbound(
return
}

// set relayer balance metrics
signer.SetRelayerBalanceMetrics(ctx)

// sign the withdraw transaction by relayer key
tx, err := signer.SignWithdrawTx(ctx, *msg)
if err != nil {
Expand Down Expand Up @@ -191,6 +194,19 @@ func (signer *Signer) GetGatewayAddress() string {
return signer.gatewayID.String()
}

// SetRelayerBalanceMetrics sets the relayer balance metrics
func (signer *Signer) SetRelayerBalanceMetrics(ctx context.Context) {
if signer.HasRelayerKey() {
result, err := signer.client.GetBalance(ctx, signer.relayerKey.PublicKey(), rpc.CommitmentFinalized)
if err != nil {
signer.Logger().Std.Error().Err(err).Msg("GetBalance error")
return
}
solBalance := float64(result.Value) / float64(solana.LAMPORTS_PER_SOL)
metrics.RelayerKeyBalance.WithLabelValues(signer.Chain().Name).Set(solBalance)
}
}

// TODO: get rid of below four functions for Solana and Bitcoin
// https://github.com/zeta-chain/node/issues/2532
func (signer *Signer) SetZetaConnectorAddress(_ ethcommon.Address) {
Expand Down
142 changes: 142 additions & 0 deletions zetaclient/chains/solana/signer/signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package signer_test

import (
"context"
"errors"
"testing"

"github.com/gagliardetto/solana-go/rpc"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/zeta-chain/zetacore/pkg/chains"
"github.com/zeta-chain/zetacore/testutil/sample"
observertypes "github.com/zeta-chain/zetacore/x/observer/types"
"github.com/zeta-chain/zetacore/zetaclient/chains/base"
"github.com/zeta-chain/zetacore/zetaclient/chains/interfaces"
"github.com/zeta-chain/zetacore/zetaclient/chains/solana/signer"
"github.com/zeta-chain/zetacore/zetaclient/keys"
"github.com/zeta-chain/zetacore/zetaclient/metrics"
"github.com/zeta-chain/zetacore/zetaclient/testutils"
"github.com/zeta-chain/zetacore/zetaclient/testutils/mocks"
)

func Test_NewSigner(t *testing.T) {
// test parameters
chain := chains.SolanaDevnet
chainParams := sample.ChainParams(chain.ChainId)
chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId]

tests := []struct {
name string
chain chains.Chain
chainParams observertypes.ChainParams
solClient interfaces.SolanaRPCClient
tss interfaces.TSSSigner
relayerKey *keys.RelayerKey
ts *metrics.TelemetryServer
logger base.Logger
errMessage string
}{
{
name: "should create solana signer successfully with relayer key",
chain: chain,
chainParams: *chainParams,
solClient: nil,
tss: nil,
relayerKey: &keys.RelayerKey{
PrivateKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ",
},
ts: nil,
logger: base.DefaultLogger(),
},
{
name: "should create solana signer successfully without relayer key",
chainParams: *chainParams,
solClient: nil,
tss: nil,
relayerKey: nil,
ts: nil,
logger: base.DefaultLogger(),
},
{
name: "should fail to create solana signer with invalid gateway address",
chainParams: func() observertypes.ChainParams {
cp := *chainParams
cp.GatewayAddress = "invalid"
return cp
}(),
solClient: nil,
tss: nil,
relayerKey: nil,
ts: nil,
logger: base.DefaultLogger(),
errMessage: "cannot parse gateway address",
},
{
name: "should fail to create solana signer with invalid relayer key",
chainParams: *chainParams,
solClient: nil,
tss: nil,
relayerKey: &keys.RelayerKey{
PrivateKey: "3EMjCcCJg53fMEGVj13", // too short
},
ts: nil,
logger: base.DefaultLogger(),
errMessage: "unable to construct solana private key",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := signer.NewSigner(tt.chain, tt.chainParams, tt.solClient, tt.tss, tt.relayerKey, tt.ts, tt.logger)
if tt.errMessage != "" {
require.ErrorContains(t, err, tt.errMessage)
require.Nil(t, s)
return
}

require.NoError(t, err)
require.NotNil(t, s)
})
}
}

func Test_SetRelayerBalanceMetrics(t *testing.T) {
// test parameters
chain := chains.SolanaDevnet
chainParams := sample.ChainParams(chain.ChainId)
chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId]
relayerKey := &keys.RelayerKey{
PrivateKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ",
}
ctx := context.Background()

// mock solana client with RPC error
mckClient := mocks.NewSolanaRPCClient(t)
mckClient.On("GetBalance", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("rpc error"))

// create signer and set relayer balance metrics
s, err := signer.NewSigner(chain, *chainParams, mckClient, nil, relayerKey, nil, base.DefaultLogger())
require.NoError(t, err)
s.SetRelayerBalanceMetrics(ctx)

// assert that relayer key balance metrics is not set (due to RPC error)
balance := testutil.ToFloat64(metrics.RelayerKeyBalance.WithLabelValues(chain.Name))
require.Equal(t, 0.0, balance)

// mock solana client with balance
mckClient = mocks.NewSolanaRPCClient(t)
mckClient.On("GetBalance", mock.Anything, mock.Anything, mock.Anything).Return(&rpc.GetBalanceResult{
Value: 123400000,
}, nil)

// create signer and set relayer balance metrics again
s, err = signer.NewSigner(chain, *chainParams, mckClient, nil, relayerKey, nil, base.DefaultLogger())
require.NoError(t, err)
s.SetRelayerBalanceMetrics(ctx)

// assert that relayer key balance metrics is set correctly
balance = testutil.ToFloat64(metrics.RelayerKeyBalance.WithLabelValues(chain.Name))
require.Equal(t, 0.1234, balance)
}
3 changes: 1 addition & 2 deletions zetaclient/keys/relayer_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"path/filepath"

"github.com/gagliardetto/solana-go"
"github.com/pkg/errors"

"github.com/zeta-chain/zetacore/pkg/chains"
Expand All @@ -29,7 +28,7 @@ func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, err

switch network {
case chains.Network_solana:
privKey, err := solana.PrivateKeyFromBase58(rk.PrivateKey)
privKey, err := crypto.SolanaPrivateKeyFromString(rk.PrivateKey)
if err != nil {
return "", "", errors.Wrap(err, "unable to construct solana private key")
}
Expand Down
7 changes: 7 additions & 0 deletions zetaclient/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ var (
Help: "Tss node blame counter per pubkey",
}, []string{"pubkey"})

// RelayerKeyBalance is a gauge that contains the relayer key balance of the chain
RelayerKeyBalance = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: ZetaClientNamespace,
Name: "relayer_key_balance",
Help: "Relayer key balance of the chain",
}, []string{"chain"})

// HotKeyBurnRate is a gauge that contains the fee burn rate of the hotkey
HotKeyBurnRate = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: ZetaClientNamespace,
Expand Down
11 changes: 10 additions & 1 deletion zetaclient/metrics/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/zeta-chain/zetacore/pkg/chains"
. "gopkg.in/check.v1"
)

Expand Down Expand Up @@ -39,7 +40,7 @@ func (ms *MetricsSuite) TestCurryWith(c *C) {
RPCCount.Reset()
}

func (ms *MetricsSuite) TestMetrics(c *C) {
func (ms *MetricsSuite) Test_RPCCount(c *C) {
GetFilterLogsPerChain.WithLabelValues("chain1").Inc()
GetFilterLogsPerChain.WithLabelValues("chain2").Inc()
GetFilterLogsPerChain.WithLabelValues("chain2").Inc()
Expand Down Expand Up @@ -77,3 +78,11 @@ func (ms *MetricsSuite) TestMetrics(c *C) {
rpcCount = testutil.ToFloat64(RPCCount.With(prometheus.Labels{"host": "127.0.0.1:8886", "code": "502"}))
c.Assert(rpcCount, Equals, 0.0)
}

func (ms *MetricsSuite) Test_RelayerKeyBalance(c *C) {
RelayerKeyBalance.WithLabelValues(chains.SolanaDevnet.Name).Set(2.1564)

// assert that relayer key balance is being set correctly
balance := testutil.ToFloat64(RelayerKeyBalance.WithLabelValues(chains.SolanaDevnet.Name))
c.Assert(balance, Equals, 2.1564)
}
Loading

0 comments on commit 66b7027

Please sign in to comment.