forked from TrueCloudLab/frostfs-node
[#404] morph/client: Define notary client
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
This commit is contained in:
parent
948823c392
commit
779a495625
2 changed files with 414 additions and 0 deletions
|
@ -28,6 +28,8 @@ type Client struct {
|
|||
acc *wallet.Account // neo account
|
||||
|
||||
gas util.Uint160 // native gas script-hash
|
||||
|
||||
notary *notary
|
||||
}
|
||||
|
||||
// ErrNilClient is returned by functions that expect
|
||||
|
|
412
pkg/morph/client/notary.go
Normal file
412
pkg/morph/client/notary.go
Normal file
|
@ -0,0 +1,412 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
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/opcode"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
notary struct {
|
||||
// extra fee to check witness of proxy contract
|
||||
// neo-go does not have an option to calculate it exactly right now
|
||||
extraVerifyFee int64
|
||||
|
||||
txValidTime uint32 // minimum amount of blocks when mainTx will be valid
|
||||
roundTime uint32 // extra amount of blocks to synchronize sidechain height diff of inner ring nodes
|
||||
fallbackTime uint32 // amount of blocks before fallbackTx will be sent
|
||||
|
||||
notary util.Uint160
|
||||
proxy util.Uint160
|
||||
netmap util.Uint160
|
||||
}
|
||||
|
||||
notaryCfg struct {
|
||||
extraVerifyFee int64
|
||||
|
||||
txValidTime, roundTime, fallbackTime uint32
|
||||
}
|
||||
|
||||
NotaryOption func(*notaryCfg)
|
||||
)
|
||||
|
||||
const (
|
||||
defaultNotaryExtraFee = 1000_0000
|
||||
defaultNotaryValidTime = 50
|
||||
defaultNotaryRoundTime = 100
|
||||
defaultNotaryFallbackTime = 40
|
||||
|
||||
innerRingListMethod = "innerRingList"
|
||||
)
|
||||
|
||||
var (
|
||||
errNotaryNotEnabled = errors.New("notary support was not enabled on this client")
|
||||
errInvalidIR = errors.New("invalid inner ring list from netmap contract")
|
||||
)
|
||||
|
||||
func defaultNotaryConfig() *notaryCfg {
|
||||
return ¬aryCfg{
|
||||
extraVerifyFee: defaultNotaryExtraFee,
|
||||
txValidTime: defaultNotaryValidTime,
|
||||
roundTime: defaultNotaryRoundTime,
|
||||
fallbackTime: defaultNotaryFallbackTime,
|
||||
}
|
||||
}
|
||||
|
||||
// EnableNotarySupport creates notary structure in client that provides
|
||||
// ability for client to get inner ring list from netmap contract and
|
||||
// use proxy contract script hash to create tx for notary contract.
|
||||
func (c *Client) EnableNotarySupport(proxy, netmap util.Uint160, opts ...NotaryOption) error {
|
||||
cfg := defaultNotaryConfig()
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
notaryContract, err := c.client.GetNativeContractHash(nativenames.Notary)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't get notary contract script hash")
|
||||
}
|
||||
|
||||
c.notary = ¬ary{
|
||||
notary: notaryContract,
|
||||
proxy: proxy,
|
||||
netmap: netmap,
|
||||
extraVerifyFee: cfg.extraVerifyFee,
|
||||
txValidTime: cfg.txValidTime,
|
||||
roundTime: cfg.roundTime,
|
||||
fallbackTime: cfg.fallbackTime,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invoke invokes contract method by sending tx to notary contract in
|
||||
// blockchain. Fallback tx is a `RET`. Notary support should be enabled
|
||||
// in client to use this function.
|
||||
//
|
||||
// Supported args types: int64, string, util.Uint160, []byte and bool.
|
||||
func (c *Client) NotaryInvoke(contract util.Uint160, method string, args ...interface{}) error {
|
||||
if c.notary == nil {
|
||||
return errNotaryNotEnabled
|
||||
}
|
||||
|
||||
// prepare arguments for test invocation
|
||||
|
||||
irList, err := c.notaryInnerRingList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, n := mn(irList)
|
||||
u8n := uint8(n)
|
||||
|
||||
cosigners, err := c.notaryCosigners(irList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params, err := invocationParams(args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// make test invocation of the method
|
||||
test, err := c.client.InvokeFunction(contract, method, params, cosigners)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if test invocation failed, then return error
|
||||
if len(test.Script) == 0 {
|
||||
return errEmptyInvocationScript
|
||||
}
|
||||
|
||||
// after test invocation we build main multisig transaction
|
||||
|
||||
multiaddrAccount, err := c.notaryMultisigAccount(irList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
until, err := c.notaryTxValidationLimit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prepare main tx
|
||||
mainTx := &transaction.Transaction{
|
||||
Nonce: 1,
|
||||
SystemFee: test.GasConsumed,
|
||||
ValidUntilBlock: until,
|
||||
Script: test.Script,
|
||||
Attributes: []transaction.Attribute{
|
||||
{
|
||||
Type: transaction.NotaryAssistedT,
|
||||
Value: &transaction.NotaryAssisted{NKeys: u8n},
|
||||
},
|
||||
},
|
||||
Signers: cosigners,
|
||||
Network: c.client.GetNetwork(),
|
||||
}
|
||||
|
||||
// calculate notary fee
|
||||
notaryFee, err := c.client.CalculateNotaryFee(u8n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add network fee for cosigners
|
||||
err = c.client.AddNetworkFee(
|
||||
mainTx,
|
||||
notaryFee+c.notary.extraVerifyFee,
|
||||
c.notaryAccounts(multiaddrAccount)...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// define witnesses
|
||||
mainTx.Scripts = c.notaryWitnesses(multiaddrAccount, mainTx)
|
||||
|
||||
resp, err := c.client.SignAndPushP2PNotaryRequest(mainTx,
|
||||
[]byte{byte(opcode.RET)},
|
||||
-1,
|
||||
0,
|
||||
c.notary.fallbackTime,
|
||||
c.acc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.logger.Debug("notary request invoked",
|
||||
zap.String("method", method),
|
||||
zap.Stringer("tx_hash", resp.Hash().Reverse()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) notaryCosigners(ir []*keys.PublicKey) ([]transaction.Signer, error) {
|
||||
s := make([]transaction.Signer, 0, 3)
|
||||
|
||||
// first we have proxy contract signature, as it will pay for the execution
|
||||
s = append(s, transaction.Signer{
|
||||
Account: c.notary.proxy,
|
||||
Scopes: transaction.None,
|
||||
})
|
||||
|
||||
// then we have inner ring multiaddress signature
|
||||
m, _ := mn(ir)
|
||||
|
||||
multisigScript, err := sc.CreateMultiSigRedeemScript(m, ir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't create ir multisig redeem script")
|
||||
}
|
||||
|
||||
s = append(s, transaction.Signer{
|
||||
Account: hash.Hash160(multisigScript),
|
||||
Scopes: transaction.Global,
|
||||
})
|
||||
|
||||
// last one is a placeholder for notary contract signature
|
||||
s = append(s, transaction.Signer{
|
||||
Account: c.notary.notary,
|
||||
Scopes: transaction.None,
|
||||
})
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (c *Client) notaryAccounts(multiaddr *wallet.Account) []*wallet.Account {
|
||||
if multiaddr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
a := make([]*wallet.Account, 0, 3)
|
||||
|
||||
// first we have proxy account, as it will pay for the execution
|
||||
a = append(a, &wallet.Account{
|
||||
Contract: &wallet.Contract{
|
||||
Deployed: true,
|
||||
},
|
||||
})
|
||||
|
||||
// then we have inner ring multiaddress account
|
||||
a = append(a, multiaddr)
|
||||
|
||||
// last one is a placeholder for notary contract account
|
||||
a = append(a, &wallet.Account{
|
||||
Contract: &wallet.Contract{},
|
||||
})
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (c *Client) notaryWitnesses(multiaddr *wallet.Account, tx *transaction.Transaction) []transaction.Witness {
|
||||
if multiaddr == nil || tx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := make([]transaction.Witness, 0, 3)
|
||||
|
||||
// first we have empty proxy witness, because notary will execute `Verify`
|
||||
// method on the proxy contract to check witness
|
||||
w = append(w, transaction.Witness{
|
||||
InvocationScript: []byte{},
|
||||
VerificationScript: []byte{},
|
||||
})
|
||||
|
||||
// then we have inner ring multiaddress witness
|
||||
w = append(w, transaction.Witness{
|
||||
InvocationScript: append(
|
||||
[]byte{byte(opcode.PUSHDATA1), 64},
|
||||
multiaddr.PrivateKey().Sign(tx.GetSignedPart())...,
|
||||
),
|
||||
VerificationScript: multiaddr.GetVerificationScript(),
|
||||
})
|
||||
|
||||
// last one is a placeholder for notary contract witness
|
||||
w = append(w, transaction.Witness{
|
||||
InvocationScript: append(
|
||||
[]byte{byte(opcode.PUSHDATA1), 64},
|
||||
make([]byte, 64)...,
|
||||
),
|
||||
VerificationScript: []byte{},
|
||||
})
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func (c *Client) notaryInnerRingList() ([]*keys.PublicKey, error) {
|
||||
data, err := c.TestInvoke(c.notary.netmap, innerRingListMethod)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "test invoke error")
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil, errors.Wrap(errInvalidIR, "test invoke returned empty stack")
|
||||
}
|
||||
|
||||
prms, err := ArrayFromStackItem(data[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "test invoke returned non array element")
|
||||
}
|
||||
|
||||
res := make([]*keys.PublicKey, 0, len(prms))
|
||||
for i := range prms {
|
||||
nodePrms, err := ArrayFromStackItem(prms[i])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "inner ring node structure is not an array")
|
||||
}
|
||||
|
||||
if len(nodePrms) == 0 {
|
||||
return nil, errors.Wrap(errInvalidIR, "inner ring node structure is empty array")
|
||||
}
|
||||
|
||||
rawKey, err := BytesFromStackItem(nodePrms[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "inner ring public key is not slice of bytes")
|
||||
}
|
||||
|
||||
key, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't parse inner ring public key bytes")
|
||||
}
|
||||
|
||||
res = append(res, key)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) notaryMultisigAccount(ir []*keys.PublicKey) (*wallet.Account, error) {
|
||||
m, _ := mn(ir)
|
||||
|
||||
multisigAccount := wallet.NewAccountFromPrivateKey(c.acc.PrivateKey())
|
||||
|
||||
err := multisigAccount.ConvertMultisig(m, ir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't make inner ring multisig wallet")
|
||||
}
|
||||
|
||||
return multisigAccount, nil
|
||||
}
|
||||
|
||||
func (c *Client) notaryTxValidationLimit() (uint32, error) {
|
||||
bc, err := c.client.GetBlockCount()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "can't get current blockchain height")
|
||||
}
|
||||
|
||||
min := bc + c.notary.txValidTime
|
||||
rounded := (min/c.notary.roundTime + 1) * c.notary.roundTime
|
||||
|
||||
return rounded, nil
|
||||
}
|
||||
|
||||
func invocationParams(args ...interface{}) ([]sc.Parameter, error) {
|
||||
params := make([]sc.Parameter, 0, len(args))
|
||||
|
||||
for i := range args {
|
||||
param, err := toStackParameter(args[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// mn returns M and N multi signature numbers. For NeoFS N is a length of
|
||||
// inner ring list, and M is a 2/3+1 of it (like in dBFT).
|
||||
func mn(ir []*keys.PublicKey) (m int, n int) {
|
||||
n = len(ir)
|
||||
m = n*2/3 + 1
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// WithExtraVerifyFee returns a notary support option for client
|
||||
// that specifies extra fee to check witness of proxy contract.
|
||||
func WithExtraVerifyFee(fee int64) NotaryOption {
|
||||
return func(c *notaryCfg) {
|
||||
c.extraVerifyFee = fee
|
||||
}
|
||||
}
|
||||
|
||||
// WithTxValidTime returns a notary support option for client
|
||||
// that specifies minimum amount of blocks when mainTx will be valid.
|
||||
func WithTxValidTime(t uint32) NotaryOption {
|
||||
return func(c *notaryCfg) {
|
||||
c.txValidTime = t
|
||||
}
|
||||
}
|
||||
|
||||
// WithRoundTime returns a notary support option for client
|
||||
// that specifies extra blocks to synchronize side chain
|
||||
// height diff of inner ring nodes.
|
||||
func WithRoundTime(t uint32) NotaryOption {
|
||||
return func(c *notaryCfg) {
|
||||
c.roundTime = t
|
||||
}
|
||||
}
|
||||
|
||||
// WithFallbackTime returns a notary support option for client
|
||||
// that specifies amount of blocks before fallbackTx will be sent.
|
||||
// Should be less than TxValidTime.
|
||||
func WithFallbackTime(t uint32) NotaryOption {
|
||||
return func(c *notaryCfg) {
|
||||
c.fallbackTime = t
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue