From 1bf57815f3f59c4bba22b1619720e2a74c8f772d Mon Sep 17 00:00:00 2001 From: Anton Nikiforov Date: Tue, 17 Oct 2023 10:34:57 +0300 Subject: [PATCH] [#744] morph: Revert commit ddcc156eccc3e82dc50738bc5f3aef9bb50896a2 Required to unblock storage node upgrade from 0.36 to 0.37 Signed-off-by: Anton Nikiforov --- pkg/morph/client/client.go | 61 ++++ pkg/morph/client/notary.go | 417 ++++++++++++++++++--------- pkg/morph/event/notary_preparator.go | 21 +- 3 files changed, 348 insertions(+), 151 deletions(-) diff --git a/pkg/morph/client/client.go b/pkg/morph/client/client.go index 606f3bd66..941a70c01 100644 --- a/pkg/morph/client/client.go +++ b/pkg/morph/client/client.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt" "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + sc "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -157,6 +158,8 @@ func (e *notHaltStateError) Error() string { ) } +var errEmptyInvocationScript = errors.New("got empty invocation script from neo node") + // implementation of error interface for FrostFS-specific errors. type frostfsError struct { err error @@ -470,6 +473,64 @@ func (c *Client) roleList(r noderoles.Role) (keys.PublicKeys, error) { return c.rolemgmt.GetDesignatedByRole(r, height) } +// tries to resolve sc.Parameter from the arg. +// +// Wraps any error to frostfsError. +func toStackParameter(value any) (sc.Parameter, error) { + var res = sc.Parameter{ + Value: value, + } + + switch v := value.(type) { + case []byte: + res.Type = sc.ByteArrayType + case int: + res.Type = sc.IntegerType + res.Value = big.NewInt(int64(v)) + case int64: + res.Type = sc.IntegerType + res.Value = big.NewInt(v) + case uint64: + res.Type = sc.IntegerType + res.Value = new(big.Int).SetUint64(v) + case [][]byte: + arr := make([]sc.Parameter, 0, len(v)) + for i := range v { + elem, err := toStackParameter(v[i]) + if err != nil { + return res, err + } + + arr = append(arr, elem) + } + + res.Type = sc.ArrayType + res.Value = arr + case string: + res.Type = sc.StringType + case util.Uint160: + res.Type = sc.ByteArrayType + res.Value = v.BytesBE() + case noderoles.Role: + res.Type = sc.IntegerType + res.Value = big.NewInt(int64(v)) + case keys.PublicKeys: + arr := make([][]byte, 0, len(v)) + for i := range v { + arr = append(arr, v[i].Bytes()) + } + + return toStackParameter(arr) + case bool: + res.Type = sc.BoolType + res.Value = v + default: + return res, wrapFrostFSError(fmt.Errorf("chain/client: unsupported parameter %v", value)) + } + + 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, error) { diff --git a/pkg/morph/client/notary.go b/pkg/morph/client/notary.go index 17644361a..d42456db2 100644 --- a/pkg/morph/client/notary.go +++ b/pkg/morph/client/notary.go @@ -1,7 +1,6 @@ package client import ( - "crypto/elliptic" "encoding/binary" "errors" "fmt" @@ -20,20 +19,19 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/neorpc" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" 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" - "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/wallet" "go.uber.org/zap" ) type ( notaryInfo struct { - 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 + 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 // mainTx's ValidUntilBlock - fallbackTime + 1 is when fallbackTx is sent alphabetSource AlphabetKeys // source of alphabet node keys to prepare witness @@ -44,7 +42,7 @@ type ( notaryCfg struct { proxy util.Uint160 - txValidTime, roundTime uint32 + txValidTime, roundTime, fallbackTime uint32 alphabetSource AlphabetKeys } @@ -54,8 +52,9 @@ type ( ) const ( - defaultNotaryValidTime = 50 - defaultNotaryRoundTime = 100 + defaultNotaryValidTime = 50 + defaultNotaryRoundTime = 100 + defaultNotaryFallbackTime = 40 notaryBalanceOfMethod = "balanceOf" notaryExpirationOfMethod = "expirationOf" @@ -71,6 +70,7 @@ func defaultNotaryConfig(c *Client) *notaryCfg { return ¬aryCfg{ txValidTime: defaultNotaryValidTime, roundTime: defaultNotaryRoundTime, + fallbackTime: defaultNotaryFallbackTime, alphabetSource: c.Committee, } } @@ -105,6 +105,7 @@ func (c *Client) EnableNotarySupport(opts ...NotaryOption) error { proxy: cfg.proxy, txValidTime: cfg.txValidTime, roundTime: cfg.roundTime, + fallbackTime: cfg.fallbackTime, alphabetSource: cfg.alphabetSource, notary: notary.Hash, } @@ -408,32 +409,33 @@ func (c *Client) NotarySignAndInvokeTX(mainTx *transaction.Transaction) error { return fmt.Errorf("could not fetch current alphabet keys: %w", err) } - cosigners, err := c.notaryCosignersFromTx(mainTx, alphabetList) + multiaddrAccount, err := c.notaryMultisigAccount(alphabetList, false, true) if err != nil { return err } - nAct, err := notary.NewActor(c.client, cosigners, c.acc) - if err != nil { - return err - } + // mainTX is expected to be pre-validated: second witness must exist and be empty + mainTx.Scripts[1].VerificationScript = multiaddrAccount.GetVerificationScript() + mainTx.Scripts[1].InvocationScript = append( + []byte{byte(opcode.PUSHDATA1), 64}, + multiaddrAccount.SignHashable(c.rpcActor.GetNetwork(), mainTx)..., + ) - // Sign exactly the same transaction we've got from the received Notary request. - err = nAct.Sign(mainTx) - if err != nil { - return fmt.Errorf("faield to sign notary request: %w", err) - } - - mainH, fbH, untilActual, err := nAct.Notarize(mainTx, nil) + //lint:ignore SA1019 https://git.frostfs.info/TrueCloudLab/frostfs-node/issues/202 + resp, err := c.client.SignAndPushP2PNotaryRequest(mainTx, + []byte{byte(opcode.RET)}, + -1, + 0, + c.notary.fallbackTime, + c.acc) if err != nil && !alreadyOnChainError(err) { return err } c.logger.Debug(logs.ClientNotaryRequestWithPreparedMainTXInvoked, - zap.String("tx_hash", mainH.StringLE()), - zap.Uint32("valid_until_block", untilActual), - zap.String("fallback_hash", fbH.StringLE())) + zap.Uint32("fallback_valid_for", c.notary.fallbackTime), + zap.Stringer("tx_hash", resp.Hash().Reverse())) return nil } @@ -444,159 +446,75 @@ func (c *Client) notaryInvokeAsCommittee(method string, nonce, vub uint32, args } func (c *Client) notaryInvoke(committee, invokedByAlpha bool, contract util.Uint160, nonce uint32, vub *uint32, method string, args ...any) error { - start := time.Now() - success := false - defer func() { - c.metrics.ObserveInvoke("notaryInvoke", contract.String(), method, success, time.Since(start)) - }() - alphabetList, err := c.notary.alphabetSource() if err != nil { return err } - until, err := c.getUntilValue(vub) - if err != nil { - return err - } - cosigners, err := c.notaryCosigners(invokedByAlpha, alphabetList, committee) if err != nil { return err } - nAct, err := notary.NewActor(c.client, cosigners, c.acc) + params, err := invocationParams(args...) if err != nil { return err } - mainH, fbH, untilActual, err := nAct.Notarize(nAct.MakeTunedCall(contract, method, nil, func(r *result.Invoke, t *transaction.Transaction) error { - if r.State != vmstate.Halt.String() { - return wrapFrostFSError(¬HaltStateError{state: r.State, exception: r.FaultException}) - } + test, err := c.makeTestInvocation(contract, method, params, cosigners) + if err != nil { + return err + } - t.ValidUntilBlock = until - t.Nonce = nonce + multiaddrAccount, err := c.notaryMultisigAccount(alphabetList, committee, invokedByAlpha) + if err != nil { + return err + } - return nil - }, args...)) + until, err := c.getUntilValue(vub) + if err != nil { + return err + } + mainTx, err := c.buildMainTx(invokedByAlpha, nonce, alphabetList, test, cosigners, multiaddrAccount, until) + if err != nil { + return err + } + + //lint:ignore SA1019 https://git.frostfs.info/TrueCloudLab/frostfs-node/issues/202 + resp, err := c.client.SignAndPushP2PNotaryRequest(mainTx, + []byte{byte(opcode.RET)}, + -1, + 0, + c.notary.fallbackTime, + c.acc) if err != nil && !alreadyOnChainError(err) { return err } c.logger.Debug(logs.ClientNotaryRequestInvoked, zap.String("method", method), - zap.Uint32("valid_until_block", untilActual), - zap.String("tx_hash", mainH.StringLE()), - zap.String("fallback_hash", fbH.StringLE())) + zap.Uint32("valid_until_block", until), + zap.Uint32("fallback_valid_for", c.notary.fallbackTime), + zap.Stringer("tx_hash", resp.Hash().Reverse())) - success = true return nil } -func (c *Client) notaryCosignersFromTx(mainTx *transaction.Transaction, alphabetList keys.PublicKeys) ([]actor.SignerAccount, error) { - multiaddrAccount, err := c.notaryMultisigAccount(alphabetList, false, true) +func (c *Client) makeTestInvocation(contract util.Uint160, method string, params []sc.Parameter, cosigners []transaction.Signer) (*result.Invoke, error) { + test, err := c.client.InvokeFunction(contract, method, params, cosigners) if err != nil { return nil, err } - // Here we need to add a committee signature (second witness) to the pre-validated - // main transaction without creating a new one. However, Notary actor demands the - // proper set of signers for constructor, thus, fill it from the main transaction's signers list. - s := make([]actor.SignerAccount, 2, 3) - s[0] = actor.SignerAccount{ - // Proxy contract that will pay for the execution. - Signer: mainTx.Signers[0], - Account: notary.FakeContractAccount(mainTx.Signers[0].Account), - } - s[1] = actor.SignerAccount{ - // Inner ring multisignature. - Signer: mainTx.Signers[1], - Account: multiaddrAccount, - } - if len(mainTx.Signers) > 3 { - // Invoker signature (simple signature account of storage node is expected). - var acc *wallet.Account - script := mainTx.Scripts[2].VerificationScript - if len(script) == 0 { - acc = notary.FakeContractAccount(mainTx.Signers[2].Account) - } else { - pubBytes, ok := vm.ParseSignatureContract(script) - if ok { - pub, err := keys.NewPublicKeyFromBytes(pubBytes, elliptic.P256()) - if err != nil { - return nil, fmt.Errorf("failed to parse verification script of signer #2: invalid public key: %w", err) - } - acc = notary.FakeSimpleAccount(pub) - } else { - m, pubsBytes, ok := vm.ParseMultiSigContract(script) - if !ok { - return nil, errors.New("failed to parse verification script of signer #2: unknown witness type") - } - pubs := make(keys.PublicKeys, len(pubsBytes)) - for i := range pubs { - pubs[i], err = keys.NewPublicKeyFromBytes(pubsBytes[i], elliptic.P256()) - if err != nil { - return nil, fmt.Errorf("failed to parse verification script of signer #2: invalid public key #%d: %w", i, err) - } - } - acc, err = notary.FakeMultisigAccount(m, pubs) - if err != nil { - return nil, fmt.Errorf("failed to create fake account for signer #2: %w", err) - } - } - } - s = append(s, actor.SignerAccount{ - Signer: mainTx.Signers[2], - Account: acc, - }) + if test.State != HaltState { + return nil, wrapFrostFSError(¬HaltStateError{state: test.State, exception: test.FaultException}) } - return s, nil -} - -func (c *Client) notaryCosigners(invokedByAlpha bool, ir []*keys.PublicKey, committee bool) ([]actor.SignerAccount, error) { - multiaddrAccount, err := c.notaryMultisigAccount(ir, committee, invokedByAlpha) - if err != nil { - return nil, err + if len(test.Script) == 0 { + return nil, wrapFrostFSError(errEmptyInvocationScript) } - s := make([]actor.SignerAccount, 2, 3) - // Proxy contract that will pay for the execution. - s[0] = actor.SignerAccount{ - Signer: transaction.Signer{ - Account: c.notary.proxy, - Scopes: transaction.None, - }, - Account: notary.FakeContractAccount(c.notary.proxy), - } - // Inner ring multisignature. - s[1] = actor.SignerAccount{ - Signer: transaction.Signer{ - Account: multiaddrAccount.ScriptHash(), - Scopes: c.cfg.signer.Scopes, - AllowedContracts: c.cfg.signer.AllowedContracts, - AllowedGroups: c.cfg.signer.AllowedGroups, - }, - Account: multiaddrAccount, - } - - if !invokedByAlpha { - // Invoker signature. - s = append(s, actor.SignerAccount{ - Signer: transaction.Signer{ - Account: hash.Hash160(c.acc.GetVerificationScript()), - Scopes: c.cfg.signer.Scopes, - AllowedContracts: c.cfg.signer.AllowedContracts, - AllowedGroups: c.cfg.signer.AllowedGroups, - }, - Account: c.acc, - }) - } - - // The last one is Notary contract that will be added to the signers list - // by Notary actor automatically. - return s, nil + return test, nil } func (c *Client) getUntilValue(vub *uint32) (uint32, error) { @@ -606,6 +524,195 @@ func (c *Client) getUntilValue(vub *uint32) (uint32, error) { return c.notaryTxValidationLimit() } +func (c *Client) buildMainTx(invokedByAlpha bool, nonce uint32, alphabetList keys.PublicKeys, test *result.Invoke, + cosigners []transaction.Signer, multiaddrAccount *wallet.Account, until uint32) (*transaction.Transaction, error) { + // after test invocation we build main multisig transaction + + u8n := uint8(len(alphabetList)) + + if !invokedByAlpha { + u8n++ + } + + // prepare main tx + mainTx := &transaction.Transaction{ + Nonce: nonce, + SystemFee: test.GasConsumed, + ValidUntilBlock: until, + Script: test.Script, + Attributes: []transaction.Attribute{ + { + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: u8n}, + }, + }, + Signers: cosigners, + } + + // calculate notary fee + //lint:ignore SA1019 https://git.frostfs.info/TrueCloudLab/frostfs-node/issues/202 + notaryFee, err := c.client.CalculateNotaryFee(u8n) + if err != nil { + return nil, err + } + + // add network fee for cosigners + //nolint:staticcheck // waits for neo-go v0.99.3 with notary actors + //lint:ignore SA1019 https://git.frostfs.info/TrueCloudLab/frostfs-node/issues/202 + err = c.client.AddNetworkFee( + mainTx, + notaryFee, + c.notaryAccounts(invokedByAlpha, multiaddrAccount)..., + ) + if err != nil { + return nil, err + } + + // define witnesses + mainTx.Scripts = c.notaryWitnesses(invokedByAlpha, multiaddrAccount, mainTx) + + return mainTx, nil +} + +func (c *Client) notaryCosigners(invokedByAlpha bool, ir []*keys.PublicKey, committee bool) ([]transaction.Signer, error) { + s := make([]transaction.Signer, 0, 4) + + // 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 := sigCount(ir, committee) + + multisigScript, err := sc.CreateMultiSigRedeemScript(m, ir) + if err != nil { + // wrap error as FrostFS-specific since the call is not related to any client + return nil, wrapFrostFSError(fmt.Errorf("can't create ir multisig redeem script: %w", err)) + } + + s = append(s, transaction.Signer{ + Account: hash.Hash160(multisigScript), + Scopes: c.cfg.signer.Scopes, + AllowedContracts: c.cfg.signer.AllowedContracts, + AllowedGroups: c.cfg.signer.AllowedGroups, + }) + + if !invokedByAlpha { + // then we have invoker signature + s = append(s, transaction.Signer{ + Account: hash.Hash160(c.acc.GetVerificationScript()), + Scopes: c.cfg.signer.Scopes, + AllowedContracts: c.cfg.signer.AllowedContracts, + AllowedGroups: c.cfg.signer.AllowedGroups, + }) + } + + // 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(invokedByAlpha bool, multiaddr *wallet.Account) []*wallet.Account { + if multiaddr == nil { + return nil + } + + a := make([]*wallet.Account, 0, 4) + + // first we have proxy account, as it will pay for the execution + a = append(a, notary.FakeContractAccount(c.notary.proxy)) + + // then we have inner ring multiaddress account + a = append(a, multiaddr) + + if !invokedByAlpha { + // then we have invoker account + a = append(a, c.acc) + } + + // last one is a placeholder for notary contract account + a = append(a, &wallet.Account{ + Contract: &wallet.Contract{}, + }) + + return a +} + +func (c *Client) notaryWitnesses(invokedByAlpha bool, multiaddr *wallet.Account, tx *transaction.Transaction) []transaction.Witness { + if multiaddr == nil || tx == nil { + return nil + } + + w := make([]transaction.Witness, 0, 4) + + // 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 + + // invocation script should be of the form: + // { PUSHDATA1, 64, signatureBytes... } + // to pass Notary module verification + var invokeScript []byte + + magicNumber := c.rpcActor.GetNetwork() + + if invokedByAlpha { + invokeScript = append( + []byte{byte(opcode.PUSHDATA1), 64}, + multiaddr.SignHashable(magicNumber, tx)..., + ) + } else { + // we can't provide alphabet node signature + // because Storage Node doesn't own alphabet's + // private key. Thus, add dummy witness with + // empty bytes instead of signature + invokeScript = append( + []byte{byte(opcode.PUSHDATA1), 64}, + make([]byte, 64)..., + ) + } + + w = append(w, transaction.Witness{ + InvocationScript: invokeScript, + VerificationScript: multiaddr.GetVerificationScript(), + }) + + if !invokedByAlpha { + // then we have invoker witness + invokeScript = append( + []byte{byte(opcode.PUSHDATA1), 64}, + c.acc.SignHashable(magicNumber, tx)..., + ) + + w = append(w, transaction.Witness{ + InvocationScript: invokeScript, + VerificationScript: c.acc.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) notaryMultisigAccount(ir []*keys.PublicKey, committee, invokedByAlpha bool) (*wallet.Account, error) { m := sigCount(ir, committee) @@ -662,6 +769,21 @@ func (c *Client) depositExpirationOf() (int64, error) { return currentTillBig.Int64(), nil } +func invocationParams(args ...any) ([]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 +} + // sigCount returns the number of required signature. // For FrostFS Alphabet M is a 2/3+1 of it (like in dBFT). // If committee is true, returns M as N/2+1. @@ -699,6 +821,15 @@ func WithAlphabetSource(t AlphabetKeys) NotaryOption { } } +// 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 + } +} + // WithProxyContract sets proxy contract hash. func WithProxyContract(h util.Uint160) NotaryOption { return func(c *notaryCfg) { diff --git a/pkg/morph/event/notary_preparator.go b/pkg/morph/event/notary_preparator.go index 37091f768..05a4624c8 100644 --- a/pkg/morph/event/notary_preparator.go +++ b/pkg/morph/event/notary_preparator.go @@ -192,15 +192,15 @@ func (p Preparator) validateNotaryRequest(nr *payload.P2PNotaryRequest) error { } invokerWitness := ln == 4 - // alphabet node should handle only notary requests that do not yet have inner - // ring multisignature filled => such main TXs either have empty invocation script - // of the inner ring witness (in case if Notary Actor is used to create request) - // or have it filled with dummy bytes (if request was created manually with the old - // neo-go API) + multiInvScript := nr.MainTransaction.Scripts[1].InvocationScript + + // alphabet node should handle only notary requests + // that have been sent unsigned (by storage nodes) => + // such main TXs should have either a dummy or an + // empty script as an invocation script // // this check prevents notary flow recursion - if !(len(nr.MainTransaction.Scripts[1].InvocationScript) == 0 || - bytes.Equal(nr.MainTransaction.Scripts[1].InvocationScript, p.dummyInvocationScript)) { // compatibility with old version + if len(multiInvScript) > 0 && !bytes.Equal(nr.MainTransaction.Scripts[1].InvocationScript, p.dummyInvocationScript) { return ErrTXAlreadyHandled } @@ -227,7 +227,12 @@ func (p Preparator) validateNotaryRequest(nr *payload.P2PNotaryRequest) error { } // validate main TX expiration - return p.validateExpiration(nr.FallbackTransaction) + err = p.validateExpiration(nr.FallbackTransaction) + if err != nil { + return err + } + + return nil } func (p Preparator) validateParameterOpcodes(ops []Op) error {