diff --git a/pkg/morph/client/client.go b/pkg/morph/client/client.go index 44f43325..3fa47add 100644 --- a/pkg/morph/client/client.go +++ b/pkg/morph/client/client.go @@ -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 diff --git a/pkg/morph/client/notary.go b/pkg/morph/client/notary.go new file mode 100644 index 00000000..f540ef55 --- /dev/null +++ b/pkg/morph/client/notary.go @@ -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 + } +}