frostfs-node/pkg/morph/client/client.go
Alex Vanin 61dff99774 [#421] morph/client: Add designate role getter wrapper for NeoFSAlphabet role
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2021-03-26 10:16:48 +03:00

329 lines
7.9 KiB
Go

package client
import (
"context"
"crypto/elliptic"
"time"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
"github.com/pkg/errors"
"go.uber.org/zap"
)
// Client is a neo-go wrapper that provides
// smart-contract invocation interface.
//
// Working client must be created via constructor New.
// Using the Client that has been created with new(Client)
// expression (or just declaring a Client variable) is unsafe
// and can lead to panic.
type Client struct {
logger *logger.Logger // logging component
client *client.Client // neo-go client
acc *wallet.Account // neo account
gas util.Uint160 // native gas script-hash
neo util.Uint160 // native neo script-hash
designate util.Uint160 // native designate script-hash
waitInterval time.Duration
notary *notary
}
// ErrNilClient is returned by functions that expect
// a non-nil Client pointer, but received nil.
var ErrNilClient = errors.New("client is nil")
// HaltState returned if TestInvoke function processed without panic.
const HaltState = "HALT"
const (
committeeList = "getCommittee"
designateList = "getDesignatedByRole"
)
var errEmptyInvocationScript = errors.New("got empty invocation script from neo node")
var errScriptDecode = errors.New("could not decode invocation script from neo node")
// Invoke invokes contract method by sending transaction into blockchain.
// Supported args types: int64, string, util.Uint160, []byte and bool.
func (c *Client) Invoke(contract util.Uint160, fee fixedn.Fixed8, method string, args ...interface{}) error {
params := make([]sc.Parameter, 0, len(args))
for i := range args {
param, err := toStackParameter(args[i])
if err != nil {
return err
}
params = append(params, param)
}
cosigner := []transaction.Signer{
{
Account: c.acc.PrivateKey().PublicKey().GetScriptHash(),
Scopes: transaction.Global,
},
}
cosignerAcc := []client.SignerAccount{
{
Signer: cosigner[0],
Account: c.acc,
},
}
resp, err := c.client.InvokeFunction(contract, method, params, cosigner)
if err != nil {
return err
}
if len(resp.Script) == 0 {
return errEmptyInvocationScript
}
script := resp.Script
sysFee := resp.GasConsumed + int64(fee) // consumed gas + extra fee
txHash, err := c.client.SignAndPushInvocationTx(script, c.acc, sysFee, 0, cosignerAcc)
if err != nil {
return err
}
c.logger.Debug("neo client invoke",
zap.String("method", method),
zap.Stringer("tx_hash", txHash.Reverse()))
return nil
}
// TestInvoke invokes contract method locally in neo-go node. This method should
// be used to read data from smart-contract.
func (c *Client) TestInvoke(contract util.Uint160, method string, args ...interface{}) ([]stackitem.Item, error) {
var params = make([]sc.Parameter, 0, len(args))
for i := range args {
p, err := toStackParameter(args[i])
if err != nil {
return nil, err
}
params = append(params, p)
}
cosigner := []transaction.Signer{
{
Account: c.acc.PrivateKey().PublicKey().GetScriptHash(),
Scopes: transaction.Global,
},
}
val, err := c.client.InvokeFunction(contract, method, params, cosigner)
if err != nil {
return nil, err
}
if val.State != HaltState {
return nil, errors.Errorf("chain/client: contract execution finished with state %s", val.State)
}
return val.Stack, nil
}
// TransferGas to the receiver from local wallet
func (c *Client) TransferGas(receiver util.Uint160, amount fixedn.Fixed8) error {
txHash, err := c.client.TransferNEP17(c.acc, receiver, c.gas, int64(amount), 0, nil)
if err != nil {
return err
}
c.logger.Debug("native gas transfer invoke",
zap.String("to", receiver.StringLE()),
zap.Stringer("tx_hash", txHash))
return nil
}
// Wait function blocks routing execution until there
// are `n` new blocks in the chain.
func (c *Client) Wait(ctx context.Context, n uint32) {
var (
err error
height, newHeight uint32
)
height, err = c.client.GetBlockCount()
if err != nil {
c.logger.Error("can't get blockchain height",
zap.String("error", err.Error()))
return
}
for {
select {
case <-ctx.Done():
return
default:
}
newHeight, err = c.client.GetBlockCount()
if err != nil {
c.logger.Error("can't get blockchain height",
zap.String("error", err.Error()))
return
}
if newHeight >= height+n {
return
}
time.Sleep(c.waitInterval)
}
}
// GasBalance returns GAS amount in the client's wallet.
func (c *Client) GasBalance() (int64, error) {
return c.client.NEP17BalanceOf(c.gas, c.acc.PrivateKey().GetScriptHash())
}
// Committee returns keys of chain committee from neo native contract.
func (c *Client) Committee() (keys.PublicKeys, error) {
items, err := c.TestInvoke(c.neo, committeeList)
if err != nil {
return nil, err
}
roleKeys, err := keysFromStack(items)
if err != nil {
return nil, errors.Wrap(err, "can't get committee keys")
}
return roleKeys, nil
}
// NeoFSAlphabetList returns keys that stored in NeoFS Alphabet role. Main chain
// stores alphabet node keys of inner ring there, however side chain stores both
// alphabet and non alphabet node keys of inner ring.
func (c *Client) NeoFSAlphabetList() (keys.PublicKeys, error) {
list, err := c.roleList(native.RoleNeoFSAlphabet)
if err != nil {
return nil, errors.Wrap(err, "can't get alphabet nodes role list")
}
return list, nil
}
func (c *Client) roleList(r native.Role) (keys.PublicKeys, error) {
height, err := c.client.GetBlockCount()
if err != nil {
return nil, errors.Wrap(err, "can't get chain height")
}
items, err := c.TestInvoke(c.designate, designateList, r, int64(height))
if err != nil {
return nil, err
}
roleKeys, err := keysFromStack(items)
if err != nil {
return nil, errors.Wrap(err, "can't get role keys")
}
return roleKeys, nil
}
func toStackParameter(value interface{}) (sc.Parameter, error) {
var result = sc.Parameter{
Value: value,
}
// todo: add more types
switch v := value.(type) {
case []byte:
result.Type = sc.ByteArrayType
case int64: // TODO: add other numerical types
result.Type = sc.IntegerType
case [][]byte:
arr := make([]sc.Parameter, 0, len(v))
for i := range v {
elem, err := toStackParameter(v[i])
if err != nil {
return result, err
}
arr = append(arr, elem)
}
result.Type = sc.ArrayType
result.Value = arr
case string:
result.Type = sc.StringType
case util.Uint160:
result.Type = sc.ByteArrayType
result.Value = v.BytesBE()
case native.Role:
result.Type = sc.IntegerType
result.Value = int64(v)
case keys.PublicKeys:
arr := make([][]byte, 0, len(v))
for i := range v {
arr = append(arr, v[i].Bytes())
}
return toStackParameter(arr)
default:
return result, errors.Errorf("chain/client: unsupported parameter %v", value)
}
return result, nil
}
func keysFromStack(data []stackitem.Item) (keys.PublicKeys, error) {
if len(data) == 0 {
return nil, nil
}
arr, err := ArrayFromStackItem(data[0])
if err != nil {
return nil, errors.Wrap(err, "non array element on stack")
}
res := make([]*keys.PublicKey, 0, len(arr))
for i := range arr {
rawKey, err := BytesFromStackItem(arr[i])
if err != nil {
return nil, errors.Wrap(err, "key is not slice of bytes")
}
key, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256())
if err != nil {
return nil, errors.Wrap(err, "can't parse key")
}
res = append(res, key)
}
return res, nil
}
// MagicNumber returns the magic number of the network
// to which the underlying RPC node client is connected.
func (c *Client) MagicNumber() uint64 {
return uint64(c.client.GetNetwork())
}