From 07f3023e84071709f27ae6d15d6f13d29261713b Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 26 Aug 2022 18:10:58 +0300 Subject: [PATCH] rpcclient: add notary.Actor for seamless notary experience --- pkg/rpcclient/notary/accounts.go | 59 +++ pkg/rpcclient/notary/accounts_test.go | 36 ++ pkg/rpcclient/notary/actor.go | 314 ++++++++++++++++ pkg/rpcclient/notary/actor_test.go | 522 ++++++++++++++++++++++++++ pkg/rpcclient/notary/contract.go | 4 +- pkg/rpcclient/rpc.go | 3 + pkg/services/rpcsrv/client_test.go | 41 +- 7 files changed, 972 insertions(+), 7 deletions(-) create mode 100644 pkg/rpcclient/notary/accounts.go create mode 100644 pkg/rpcclient/notary/accounts_test.go create mode 100644 pkg/rpcclient/notary/actor.go create mode 100644 pkg/rpcclient/notary/actor_test.go diff --git a/pkg/rpcclient/notary/accounts.go b/pkg/rpcclient/notary/accounts.go new file mode 100644 index 000000000..e2b6c6905 --- /dev/null +++ b/pkg/rpcclient/notary/accounts.go @@ -0,0 +1,59 @@ +package notary + +import ( + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +// FakeSimpleAccount creates a fake account belonging to the given public key. +// It uses a simple signature contract and this account has SignTx that +// returns no error, but at the same time adds no signature (it obviously can't +// do that, so CanSign() returns false for it). Use this account for Actor when +// simple signatures are needed to be collected. +func FakeSimpleAccount(k *keys.PublicKey) *wallet.Account { + return &wallet.Account{ + Address: k.Address(), + Contract: &wallet.Contract{ + Script: k.GetVerificationScript(), + }, + } +} + +// FakeMultisigAccount creates a fake account belonging to the given "m out of +// len(pkeys)" account for the given set of keys. The account returned has SignTx +// that returns no error, but at the same time adds no signatures (it can't +// do that, so CanSign() returns false for it). Use this account for Actor when +// multisignature account needs to be added into a notary transaction, but you +// have no keys at all for it (if you have at least one (which usually is the +// case) ordinary multisig account works fine already). +func FakeMultisigAccount(m int, pkeys keys.PublicKeys) (*wallet.Account, error) { + script, err := smartcontract.CreateMultiSigRedeemScript(m, pkeys) + if err != nil { + return nil, err + } + return &wallet.Account{ + Address: address.Uint160ToString(hash.Hash160(script)), + Contract: &wallet.Contract{ + Script: script, + }, + }, nil +} + +// FakeContractAccount creates a fake account belonging to some deployed contract. +// SignTx can be called on this account with no error, but at the same time it +// adds no signature or other data into the invocation script (it obviously can't +// do that, so CanSign() returns false for it). Use this account for Actor when +// one of the signers is a contract and it doesn't need a signature or you can +// provide it externally. +func FakeContractAccount(hash util.Uint160) *wallet.Account { + return &wallet.Account{ + Address: address.Uint160ToString(hash), + Contract: &wallet.Contract{ + Deployed: true, + }, + } +} diff --git a/pkg/rpcclient/notary/accounts_test.go b/pkg/rpcclient/notary/accounts_test.go new file mode 100644 index 000000000..a87f3cace --- /dev/null +++ b/pkg/rpcclient/notary/accounts_test.go @@ -0,0 +1,36 @@ +package notary + +import ( + "testing" + + "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" + "github.com/stretchr/testify/require" +) + +func TestFakeAccounts(t *testing.T) { + k, err := keys.NewPrivateKey() + require.NoError(t, err) + + fac := FakeSimpleAccount(k.PublicKey()) + require.False(t, fac.CanSign()) + + sh := k.PublicKey().GetScriptHash() + tx := transaction.New([]byte{1, 2, 3}, 1) + tx.Signers = append(tx.Signers, transaction.Signer{Account: sh}) + require.NoError(t, fac.SignTx(0, tx)) + + fac = FakeContractAccount(sh) + require.False(t, fac.CanSign()) + require.NoError(t, fac.SignTx(0, tx)) + + _, err = FakeMultisigAccount(0, keys.PublicKeys{k.PublicKey()}) + require.Error(t, err) + + fac, err = FakeMultisigAccount(1, keys.PublicKeys{k.PublicKey()}) + require.NoError(t, err) + require.False(t, fac.CanSign()) + tx.Signers[0].Account = hash.Hash160(fac.Contract.Script) + require.NoError(t, fac.SignTx(0, tx)) +} diff --git a/pkg/rpcclient/notary/actor.go b/pkg/rpcclient/notary/actor.go new file mode 100644 index 000000000..b115c9b74 --- /dev/null +++ b/pkg/rpcclient/notary/actor.go @@ -0,0 +1,314 @@ +package notary + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/network/payload" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +// Actor encapsulates everything needed to create proper notary requests for +// assisted transactions. +type Actor struct { + // Actor is the main transaction actor, it has appropriate attributes and + // transaction modifiers to set ValidUntilBlock. Use it to create main + // transactions that have incomplete set of signatures. They can be + // signed (using available wallets), but can not be sent directly to the + // network. Instead of sending them to the network use Actor methods to + // wrap them into notary requests. + actor.Actor + // FbActor is the fallback transaction actor, it has two required signers + // and a set of attributes expected from a fallback transaction. It can + // be used to create _unsigned_ transactions with whatever actions + // required (but no additional attributes can be added). Signing them + // while technically possible (with notary contract signature missing), + // will lead to incorrect transaction because NotValidBefore and + // Conflicts attributes as well as ValidUntilBlock field can be + // correctly set only when some main transaction is available. + FbActor actor.Actor + + fbScript []byte + reader *ContractReader + sender *wallet.Account + rpc RPCActor +} + +// ActorOptions are used to influence main and fallback actors as well as the +// default Notarize behavior. +type ActorOptions struct { + // FbAttributes are additional attributes to be added into fallback + // transaction by an appropriate actor. Irrespective of this setting + // (which defaults to nil) NotaryAssisted, NotValidBefore and Conflicts + // attributes are always added. + FbAttributes []transaction.Attribute + // FbScript is the script to use in the Notarize convenience method, it + // defaults to a simple RET instruction (doing nothing). + FbScript []byte + // FbSigner is the second signer to be used for the fallback transaction. + // By default it's derived from the account and has None scope, it has + // to be a simple signature or deployed contract account, but this setting + // allows you to give it some other scope to be used in complex fallback + // scripts. + FbSigner actor.SignerAccount + // MainAttribtues are additional attributes to be added into main + // transaction by an appropriate actor. Irrespective of this setting + // (which defaults to nil) NotaryAssisted attribute is always added. + MainAttributes []transaction.Attribute + // MainCheckerModifier will be used by the main Actor when creating + // transactions. It defaults to using [actor.DefaultCheckerModifier] + // for result check and adds MaxNotValidBeforeDelta to the + // ValidUntilBlock transaction's field. Only override it if you know + // what you're doing. + MainCheckerModifier actor.TransactionCheckerModifier + // MainModifier will be used by the main Actor when creating + // transactions. By default it adds MaxNotValidBeforeDelta to the + // ValidUntilBlock transaction's field. Only override it if you know + // what you're doing. + MainModifier actor.TransactionModifier +} + +// RPCActor is a set of methods required from RPC client to create Actor. +type RPCActor interface { + actor.RPCActor + + SubmitP2PNotaryRequest(req *payload.P2PNotaryRequest) (util.Uint256, error) +} + +// NewDefaultActorOptions returns the default Actor options. Internal functions +// of it need some data from the contract, so it should be added. +func NewDefaultActorOptions(reader *ContractReader, acc *wallet.Account) ActorOptions { + opts := ActorOptions{ + FbScript: []byte{byte(opcode.RET)}, + FbSigner: actor.SignerAccount{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc, + }, + MainModifier: func(t *transaction.Transaction) error { + nvbDelta, err := reader.GetMaxNotValidBeforeDelta() + if err != nil { + return fmt.Errorf("can't get MaxNVBDelta: %w", err) + } + t.ValidUntilBlock += nvbDelta + return nil + }, + } + opts.MainCheckerModifier = func(r *result.Invoke, t *transaction.Transaction) error { + err := actor.DefaultCheckerModifier(r, t) + if err != nil { + return err + } + return opts.MainModifier(t) + } + return opts +} + +// NewActor creates a new notary.Actor using the given RPC client, the set of +// signers for main transactions and the account that will sign notary requests +// (one plain signature or contract-based). The set of signers will be extended +// by the notary contract signer with the None scope (as required by the notary +// protocol) and all transactions created with the resulting Actor will get a +// NotaryAssisted attribute with appropriate number of keys specified +// (depending on signers). A fallback Actor will be created as well with the +// notary contract and simpleAcc signers and a full set of required fallback +// transaction attributes (NotaryAssisted, NotValidBefore and Conflicts). +func NewActor(c RPCActor, signers []actor.SignerAccount, simpleAcc *wallet.Account) (*Actor, error) { + return newTunedActor(c, signers, simpleAcc, nil) +} + +// NewTunedActor is the same as NewActor, but allows to override the default +// options (see ActorOptions for details). Use with care. +func NewTunedActor(c RPCActor, signers []actor.SignerAccount, opts ActorOptions) (*Actor, error) { + return newTunedActor(c, signers, opts.FbSigner.Account, &opts) +} + +func newTunedActor(c RPCActor, signers []actor.SignerAccount, simpleAcc *wallet.Account, opts *ActorOptions) (*Actor, error) { + if len(signers) < 1 { + return nil, errors.New("at least one signer (sender) is required") + } + var nKeys int + for _, sa := range signers { + if sa.Account.Contract == nil { + return nil, fmt.Errorf("empty contract for account %s", sa.Account.Address) + } + if sa.Account.Contract.Deployed { + continue + } + if vm.IsSignatureContract(sa.Account.Contract.Script) { + nKeys++ + continue + } + _, pubs, ok := vm.ParseMultiSigContract(sa.Account.Contract.Script) + if !ok { + return nil, fmt.Errorf("signer %s is not a contract- or signature-based", sa.Account.Address) + } + nKeys += len(pubs) + } + if nKeys > 255 { + return nil, fmt.Errorf("notary subsystem can't handle more than 255 signatures") + } + if simpleAcc.Contract == nil { + return nil, errors.New("bad simple account: no contract") + } + if !simpleAcc.CanSign() { + return nil, errors.New("bad simple account: can't sign") + } + if !vm.IsSignatureContract(simpleAcc.Contract.Script) && !simpleAcc.Contract.Deployed { + return nil, errors.New("bad simple account: neither plain signature, nor contract") + } + // Not reusing mainActor/fbActor for ContractReader to make requests a bit lighter. + reader := NewReader(invoker.New(c, nil)) + if opts == nil { + defOpts := NewDefaultActorOptions(reader, simpleAcc) + opts = &defOpts + } + var notarySA = actor.SignerAccount{ + Signer: transaction.Signer{ + Account: Hash, + Scopes: transaction.None, + }, + Account: FakeContractAccount(Hash), + } + + var mainSigners = make([]actor.SignerAccount, len(signers), len(signers)+1) + copy(mainSigners, signers) + mainSigners = append(mainSigners, notarySA) + + mainOpts := actor.Options{ + Attributes: []transaction.Attribute{{ + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}, + }}, + CheckerModifier: opts.MainCheckerModifier, + Modifier: opts.MainModifier, + } + mainOpts.Attributes = append(mainOpts.Attributes, opts.MainAttributes...) + + mainActor, err := actor.NewTuned(c, mainSigners, mainOpts) + if err != nil { + return nil, err + } + + fbSigners := []actor.SignerAccount{notarySA, opts.FbSigner} + fbOpts := actor.Options{ + Attributes: []transaction.Attribute{{ + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: 0}, + }, { + // A stub, it has correct size, but the contents is to be filled per-request. + Type: transaction.NotValidBeforeT, + Value: &transaction.NotValidBefore{}, + }, { + // A stub, it has correct size, but the contents is to be filled per-request. + Type: transaction.ConflictsT, + Value: &transaction.Conflicts{}, + }}, + } + fbOpts.Attributes = append(fbOpts.Attributes, opts.FbAttributes...) + fbActor, err := actor.NewTuned(c, fbSigners, fbOpts) + if err != nil { + return nil, err + } + return &Actor{*mainActor, *fbActor, opts.FbScript, reader, simpleAcc, c}, nil +} + +// Notarize is a simple wrapper for transaction-creating functions that allows to +// send any partially-signed transaction in a notary request with a fallback +// transaction created based on Actor settings and SendRequest adjustment rules. +// The values returned are main and fallback transaction hashes, ValidUntilBlock +// and error if any. +func (a *Actor) Notarize(mainTx *transaction.Transaction, err error) (util.Uint256, util.Uint256, uint32, error) { + var ( + // Just to simplify return values on error. + fbHash util.Uint256 + mainHash util.Uint256 + vub uint32 + ) + if err != nil { + return mainHash, fbHash, vub, err + } + fbTx, err := a.FbActor.MakeUnsignedRun(a.fbScript, nil) + if err != nil { + return mainHash, fbHash, vub, err + } + return a.SendRequest(mainTx, fbTx) +} + +// SendRequest creates and sends a notary request using the given main and +// fallback transactions. It accepts signed main transaction and unsigned fallback +// transaction that will be adjusted in its NotValidBefore and Conflicts +// attributes as well as ValidUntilBlock value. Conflicts is set to the main +// transaction hash, while NotValidBefore is set to the middle of current mainTx +// lifetime (between current block and ValidUntilBlock). The values returned are +// main and fallback transaction hashes, ValidUntilBlock and error if any. +func (a *Actor) SendRequest(mainTx *transaction.Transaction, fbTx *transaction.Transaction) (util.Uint256, util.Uint256, uint32, error) { + var ( + fbHash util.Uint256 + mainHash = mainTx.Hash() + vub = mainTx.ValidUntilBlock + ) + if len(fbTx.Attributes) < 3 { + return mainHash, fbHash, vub, errors.New("invalid fallback: missing required attributes") + } + if fbTx.Attributes[1].Type != transaction.NotValidBeforeT { + return mainHash, fbHash, vub, errors.New("invalid fallback: NotValidBefore is missing where expected") + } + if fbTx.Attributes[2].Type != transaction.ConflictsT { + return mainHash, fbHash, vub, errors.New("invalid fallback: Conflicts is missing where expected") + } + height, err := a.GetBlockCount() + if err != nil { + return mainHash, fbHash, vub, err + } + // New values must be created to avoid overwriting originals via a pointer. + fbTx.Attributes[1].Value = &transaction.NotValidBefore{Height: (height + vub) / 2} + fbTx.Attributes[2].Value = &transaction.Conflicts{Hash: mainHash} + fbTx.ValidUntilBlock = vub + err = a.FbActor.Sign(fbTx) + if err != nil { + return mainHash, fbHash, vub, err + } + fbTx.Scripts[0].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...) // Must be present. + return a.SendRequestExactly(mainTx, fbTx) +} + +// SendRequestExactly accepts signed and completely prepared main and fallback +// transactions, creates a P2P notary request containing them, signs and sends +// it to the network. Caller takes full responsibility for transaction +// correctness in this case, use this method only if you know exactly that you +// need to override some of the other method's behavior and you can do it. The +// values returned are main and fallback transaction hashes, ValidUntilBlock +// and error if any. +func (a *Actor) SendRequestExactly(mainTx *transaction.Transaction, fbTx *transaction.Transaction) (util.Uint256, util.Uint256, uint32, error) { + var ( + fbHash = fbTx.Hash() + mainHash = mainTx.Hash() + vub = mainTx.ValidUntilBlock + ) + req := &payload.P2PNotaryRequest{ + MainTransaction: mainTx, + FallbackTransaction: fbTx, + } + req.Witness = transaction.Witness{ + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, a.sender.PrivateKey().SignHashable(uint32(a.GetNetwork()), req)...), + VerificationScript: a.sender.GetVerificationScript(), + } + actualHash, err := a.rpc.SubmitP2PNotaryRequest(req) + if err != nil { + return mainHash, fbHash, vub, fmt.Errorf("failed to submit notary request: %w", err) + } + if !actualHash.Equals(fbHash) { + return mainHash, fbHash, vub, fmt.Errorf("sent and actual fallback tx hashes mismatch: %v vs %v", fbHash.StringLE(), actualHash.StringLE()) + } + return mainHash, fbHash, vub, nil +} diff --git a/pkg/rpcclient/notary/actor_test.go b/pkg/rpcclient/notary/actor_test.go new file mode 100644 index 000000000..c3482c75e --- /dev/null +++ b/pkg/rpcclient/notary/actor_test.go @@ -0,0 +1,522 @@ +package notary + +import ( + "errors" + "testing" + + "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "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/address" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/network/payload" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "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/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +type RPCClient struct { + err error + invRes *result.Invoke + netFee int64 + bCount uint32 + version *result.Version + hash util.Uint256 + nhash util.Uint256 + mirror bool +} + +func (r *RPCClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) { + return r.netFee, r.err +} +func (r *RPCClient) GetBlockCount() (uint32, error) { + return r.bCount, r.err +} +func (r *RPCClient) GetVersion() (*result.Version, error) { + verCopy := *r.version + return &verCopy, r.err +} +func (r *RPCClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) { + return r.hash, r.err +} +func (r *RPCClient) SubmitP2PNotaryRequest(req *payload.P2PNotaryRequest) (util.Uint256, error) { + if r.mirror { + return req.FallbackTransaction.Hash(), nil + } + return r.nhash, r.err +} +func (r *RPCClient) TerminateSession(sessionID uuid.UUID) (bool, error) { + return false, nil // Just a stub, unused by actor. +} +func (r *RPCClient) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) { + return nil, nil // Just a stub, unused by actor. +} + +func TestNewActor(t *testing.T) { + rc := &RPCClient{ + version: &result.Version{ + Protocol: result.Protocol{ + Network: netmode.UnitTestNet, + MillisecondsPerBlock: 1000, + ValidatorsCount: 7, + }, + }, + } + + _, err := NewActor(rc, nil, nil) + require.Error(t, err) + + var ( + keyz [4]*keys.PrivateKey + accs [4]*wallet.Account + faccs [4]*wallet.Account + pkeys [4]*keys.PublicKey + ) + for i := range accs { + keyz[i], err = keys.NewPrivateKey() + require.NoError(t, err) + accs[i] = wallet.NewAccountFromPrivateKey(keyz[i]) + pkeys[i] = keyz[i].PublicKey() + faccs[i] = FakeSimpleAccount(pkeys[i]) + } + var multiAccs [4]*wallet.Account + for i := range accs { + multiAccs[i] = &wallet.Account{} + *multiAccs[i] = *accs[i] + require.NoError(t, multiAccs[i].ConvertMultisig(smartcontract.GetDefaultHonestNodeCount(len(pkeys)), pkeys[:])) + } + + // nil Contract + badMultiAcc0 := &wallet.Account{} + *badMultiAcc0 = *multiAccs[0] + badMultiAcc0.Contract = nil + _, err = NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: multiAccs[0].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: badMultiAcc0, + }}, accs[0]) + require.Error(t, err) + + // Non-standard script. + badMultiAcc0.Contract = &wallet.Contract{} + *badMultiAcc0.Contract = *multiAccs[0].Contract + badMultiAcc0.Contract.Script = append(badMultiAcc0.Contract.Script, byte(opcode.NOP)) + badMultiAcc0.Address = address.Uint160ToString(badMultiAcc0.Contract.ScriptHash()) + _, err = NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: badMultiAcc0.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: badMultiAcc0, + }}, accs[0]) + require.Error(t, err) + + // Too many keys + var ( + manyKeys [256]*keys.PrivateKey + manyPkeys [256]*keys.PublicKey + ) + for i := range manyKeys { + manyKeys[i], err = keys.NewPrivateKey() + require.NoError(t, err) + manyPkeys[i] = manyKeys[i].PublicKey() + } + bigMultiAcc := &wallet.Account{} + *bigMultiAcc = *wallet.NewAccountFromPrivateKey(manyKeys[0]) + require.NoError(t, bigMultiAcc.ConvertMultisig(129, manyPkeys[:])) + + _, err = NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: bigMultiAcc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: bigMultiAcc, + }}, wallet.NewAccountFromPrivateKey(manyKeys[0])) + require.Error(t, err) + + // No contract in the simple account. + badSimple0 := &wallet.Account{} + *badSimple0 = *accs[0] + badSimple0.Contract = nil + _, err = NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: multiAccs[0].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: multiAccs[0], + }}, badSimple0) + require.Error(t, err) + + // Simple account that can't sign. + badSimple0 = FakeSimpleAccount(pkeys[0]) + _, err = NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: multiAccs[0].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: multiAccs[0], + }}, badSimple0) + require.Error(t, err) + + // Multisig account instead of simple one. + _, err = NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: multiAccs[0].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: multiAccs[0], + }}, multiAccs[0]) + require.Error(t, err) + + // Main actor freaking out on hash mismatch. + _, err = NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: accs[0].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: multiAccs[0], + }}, accs[0]) + require.Error(t, err) + + // FB actor freaking out on hash mismatch. + opts := NewDefaultActorOptions(NewReader(invoker.New(rc, nil)), accs[0]) + opts.FbSigner.Signer.Account = multiAccs[0].Contract.ScriptHash() + _, err = NewTunedActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: multiAccs[0].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: multiAccs[0], + }}, opts) + require.Error(t, err) + + // Good, one multisig. + multi0, err := NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: multiAccs[0].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: multiAccs[0], + }}, accs[0]) + require.NoError(t, err) + + script := []byte{byte(opcode.RET)} + rc.invRes = &result.Invoke{ + State: "HALT", + GasConsumed: 3, + Script: script, + Stack: []stackitem.Item{stackitem.Make(42)}, + } + tx, err := multi0.MakeRun(script) + require.NoError(t, err) + require.Equal(t, 1, len(tx.Attributes)) + require.Equal(t, transaction.NotaryAssistedT, tx.Attributes[0].Type) + require.Equal(t, &transaction.NotaryAssisted{NKeys: 4}, tx.Attributes[0].Value) + + // Good, 4 single sigs with one that can sign and one contract. + single4, err := NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: accs[0].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: accs[0], + }, { + Signer: transaction.Signer{ + Account: faccs[1].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: faccs[1], + }, { + Signer: transaction.Signer{ + Account: faccs[2].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: faccs[2], + }, { + Signer: transaction.Signer{ + Account: accs[3].Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: faccs[3], + }, { + Signer: transaction.Signer{ + Account: util.Uint160{1, 2, 3}, + Scopes: transaction.CalledByEntry, + }, + Account: FakeContractAccount(util.Uint160{1, 2, 3}), + }}, accs[0]) + require.NoError(t, err) + + tx, err = single4.MakeRun(script) + require.NoError(t, err) + require.Equal(t, 1, len(tx.Attributes)) + require.Equal(t, transaction.NotaryAssistedT, tx.Attributes[0].Type) + require.Equal(t, &transaction.NotaryAssisted{NKeys: 4}, tx.Attributes[0].Value) // One account can sign, three need to collect additional sigs. +} + +func TestSendRequestExactly(t *testing.T) { + rc := &RPCClient{ + version: &result.Version{ + Protocol: result.Protocol{ + Network: netmode.UnitTestNet, + MillisecondsPerBlock: 1000, + ValidatorsCount: 7, + }, + }, + } + + key0, err := keys.NewPrivateKey() + require.NoError(t, err) + key1, err := keys.NewPrivateKey() + require.NoError(t, err) + + acc0 := wallet.NewAccountFromPrivateKey(key0) + facc1 := FakeSimpleAccount(key1.PublicKey()) + + act, err := NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: acc0.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc0, + }, { + Signer: transaction.Signer{ + Account: facc1.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: facc1, + }}, acc0) + require.NoError(t, err) + + script := []byte{byte(opcode.RET)} + mainTx := transaction.New(script, 1) + fbTx := transaction.New(script, 1) + + // Hashes mismatch + _, _, _, err = act.SendRequestExactly(mainTx, fbTx) + require.Error(t, err) + + // Error returned + rc.err = errors.New("") + _, _, _, err = act.SendRequestExactly(mainTx, fbTx) + require.Error(t, err) + + // OK returned + rc.err = nil + rc.nhash = fbTx.Hash() + mHash, fbHash, vub, err := act.SendRequestExactly(mainTx, fbTx) + require.NoError(t, err) + require.Equal(t, mainTx.Hash(), mHash) + require.Equal(t, fbTx.Hash(), fbHash) + require.Equal(t, mainTx.ValidUntilBlock, vub) +} + +func TestSendRequest(t *testing.T) { + rc := &RPCClient{ + version: &result.Version{ + Protocol: result.Protocol{ + Network: netmode.UnitTestNet, + MillisecondsPerBlock: 1000, + ValidatorsCount: 7, + }, + }, + bCount: 42, + } + + key0, err := keys.NewPrivateKey() + require.NoError(t, err) + key1, err := keys.NewPrivateKey() + require.NoError(t, err) + + acc0 := wallet.NewAccountFromPrivateKey(key0) + facc0 := FakeSimpleAccount(key0.PublicKey()) + facc1 := FakeSimpleAccount(key1.PublicKey()) + + act, err := NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: acc0.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc0, + }, { + Signer: transaction.Signer{ + Account: facc1.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: facc1, + }}, acc0) + require.NoError(t, err) + + script := []byte{byte(opcode.RET)} + rc.invRes = &result.Invoke{ + State: "HALT", + GasConsumed: 3, + Script: script, + Stack: []stackitem.Item{stackitem.Make(42)}, + } + + mainTx, err := act.MakeRun(script) + require.NoError(t, err) + + // No attributes. + fbTx, err := act.FbActor.MakeUnsignedRun(script, nil) + require.NoError(t, err) + fbTx.Attributes = nil + _, _, _, err = act.SendRequest(mainTx, fbTx) + require.Error(t, err) + + // Bad NVB. + fbTx, err = act.FbActor.MakeUnsignedRun(script, nil) + require.NoError(t, err) + fbTx.Attributes[1].Type = transaction.HighPriority + fbTx.Attributes[1].Value = nil + _, _, _, err = act.SendRequest(mainTx, fbTx) + require.Error(t, err) + + // Bad Conflicts. + fbTx, err = act.FbActor.MakeUnsignedRun(script, nil) + require.NoError(t, err) + fbTx.Attributes[2].Type = transaction.HighPriority + fbTx.Attributes[2].Value = nil + _, _, _, err = act.SendRequest(mainTx, fbTx) + require.Error(t, err) + + // GetBlockCount error. + fbTx, err = act.FbActor.MakeUnsignedRun(script, nil) + require.NoError(t, err) + rc.err = errors.New("") + _, _, _, err = act.SendRequest(mainTx, fbTx) + require.Error(t, err) + + // Can't sign suddenly. + rc.err = nil + acc0Backup := &wallet.Account{} + *acc0Backup = *acc0 + *acc0 = *facc0 + fbTx, err = act.FbActor.MakeUnsignedRun(script, nil) + require.NoError(t, err) + _, _, _, err = act.SendRequest(mainTx, fbTx) + require.Error(t, err) + + // Good. + *acc0 = *acc0Backup + fbTx, err = act.FbActor.MakeUnsignedRun(script, nil) + require.NoError(t, err) + _, _, _, err = act.SendRequest(mainTx, fbTx) + require.Error(t, err) +} + +func TestNotarize(t *testing.T) { + rc := &RPCClient{ + version: &result.Version{ + Protocol: result.Protocol{ + Network: netmode.UnitTestNet, + MillisecondsPerBlock: 1000, + ValidatorsCount: 7, + }, + }, + bCount: 42, + } + + key0, err := keys.NewPrivateKey() + require.NoError(t, err) + key1, err := keys.NewPrivateKey() + require.NoError(t, err) + + acc0 := wallet.NewAccountFromPrivateKey(key0) + facc1 := FakeSimpleAccount(key1.PublicKey()) + + act, err := NewActor(rc, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: acc0.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc0, + }, { + Signer: transaction.Signer{ + Account: facc1.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: facc1, + }}, acc0) + require.NoError(t, err) + + script := []byte{byte(opcode.RET)} + + // Immediate error from MakeRun. + rc.invRes = &result.Invoke{ + State: "FAULT", + GasConsumed: 3, + Script: script, + Stack: []stackitem.Item{stackitem.Make(42)}, + } + _, _, _, err = act.Notarize(act.MakeRun(script)) + require.Error(t, err) + + // Explicitly good transaction. but failure to create a fallback. + rc.invRes.State = "HALT" + tx, err := act.MakeRun(script) + require.NoError(t, err) + + rc.invRes.State = "FAULT" + _, _, _, err = act.Notarize(tx, nil) + require.Error(t, err) + + // FB hash mismatch from SendRequestExactly. + rc.invRes.State = "HALT" + _, _, _, err = act.Notarize(act.MakeRun(script)) + require.Error(t, err) + + // Good. + rc.mirror = true + mHash, fbHash, vub, err := act.Notarize(act.MakeRun(script)) + require.NoError(t, err) + require.NotEqual(t, util.Uint256{}, mHash) + require.NotEqual(t, util.Uint256{}, fbHash) + require.Equal(t, uint32(92), vub) +} + +func TestDefaultActorOptions(t *testing.T) { + rc := &RPCClient{ + version: &result.Version{ + Protocol: result.Protocol{ + Network: netmode.UnitTestNet, + MillisecondsPerBlock: 1000, + ValidatorsCount: 7, + }, + }, + } + acc, err := wallet.NewAccount() + require.NoError(t, err) + opts := NewDefaultActorOptions(NewReader(invoker.New(rc, nil)), acc) + rc.invRes = &result.Invoke{ + State: "HALT", + GasConsumed: 3, + Script: opts.FbScript, + Stack: []stackitem.Item{stackitem.Make(42)}, + } + tx := transaction.New(opts.FbScript, 1) + require.Error(t, opts.MainCheckerModifier(&result.Invoke{State: "FAULT"}, tx)) + rc.invRes.State = "FAULT" + require.Error(t, opts.MainCheckerModifier(&result.Invoke{State: "HALT"}, tx)) + rc.invRes.State = "HALT" + require.NoError(t, opts.MainCheckerModifier(&result.Invoke{State: "HALT"}, tx)) + require.Equal(t, uint32(42), tx.ValidUntilBlock) +} diff --git a/pkg/rpcclient/notary/contract.go b/pkg/rpcclient/notary/contract.go index aa216cbd4..eee21bd0a 100644 --- a/pkg/rpcclient/notary/contract.go +++ b/pkg/rpcclient/notary/contract.go @@ -2,8 +2,8 @@ Package notary provides an RPC-based wrapper for the Notary subsystem. It provides both regular ContractReader/Contract interfaces for the notary -contract and notary-specific functions and interfaces to simplify creation of -notary requests. +contract and notary-specific Actor as well as some helper functions to simplify +creation of notary requests. */ package notary diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index 66dc33840..4de2a57c0 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -866,6 +866,9 @@ func getSigners(sender *wallet.Account, cosigners []SignerAccount) ([]transactio // can be multisignature), or it only should have a partial multisignature. // // Note: client should be initialized before SignAndPushP2PNotaryRequest call. +// +// Deprecated: please use Actor from the notary subpackage. This method will be +// deleted in future versions. func (c *Client) SignAndPushP2PNotaryRequest(mainTx *transaction.Transaction, fallbackScript []byte, fallbackSysFee int64, fallbackNetFee int64, fallbackValidFor uint32, acc *wallet.Account) (*payload.P2PNotaryRequest, error) { var err error notaryHash, err := c.GetNativeContractHash(nativenames.Notary) diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index 5315c8f16..43f6613ff 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -985,6 +985,37 @@ func TestSignAndPushInvocationTx(t *testing.T) { }) } +func TestNotaryActor(t *testing.T) { + chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false) + defer chain.Close() + defer rpcSrv.Shutdown() + + c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) + require.NoError(t, err) + + sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain + acc := wallet.NewAccountFromPrivateKey(sender) + + comm, err := c.GetCommittee() + require.NoError(t, err) + + multiAcc := &wallet.Account{} + *multiAcc = *acc + require.NoError(t, multiAcc.ConvertMultisig(smartcontract.GetMajorityHonestNodeCount(len(comm)), comm)) + + nact, err := notary.NewActor(c, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: multiAcc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: multiAcc, + }}, acc) + require.NoError(t, err) + neoW := neo.New(nact) + _, _, _, err = nact.Notarize(neoW.SetRegisterPriceTransaction(1_0000_0000)) + require.NoError(t, err) +} + func TestSignAndPushP2PNotaryRequest(t *testing.T) { chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false) defer chain.Close() @@ -996,23 +1027,23 @@ func TestSignAndPushP2PNotaryRequest(t *testing.T) { require.NoError(t, err) t.Run("client wasn't initialized", func(t *testing.T) { - _, err := c.SignAndPushP2PNotaryRequest(transaction.New([]byte{byte(opcode.RET)}, 123), []byte{byte(opcode.RET)}, -1, 0, 100, acc) + _, err := c.SignAndPushP2PNotaryRequest(transaction.New([]byte{byte(opcode.RET)}, 123), []byte{byte(opcode.RET)}, -1, 0, 100, acc) //nolint:staticcheck // SA1019: c.SignAndPushP2PNotaryRequest is deprecated require.NotNil(t, err) }) require.NoError(t, c.Init()) t.Run("bad account address", func(t *testing.T) { - _, err := c.SignAndPushP2PNotaryRequest(nil, nil, 0, 0, 0, &wallet.Account{Address: "not-an-addr"}) + _, err := c.SignAndPushP2PNotaryRequest(nil, nil, 0, 0, 0, &wallet.Account{Address: "not-an-addr"}) //nolint:staticcheck // SA1019: c.SignAndPushP2PNotaryRequest is deprecated require.NotNil(t, err) }) t.Run("bad fallback script", func(t *testing.T) { - _, err := c.SignAndPushP2PNotaryRequest(nil, []byte{byte(opcode.ASSERT)}, -1, 0, 0, acc) + _, err := c.SignAndPushP2PNotaryRequest(nil, []byte{byte(opcode.ASSERT)}, -1, 0, 0, acc) //nolint:staticcheck // SA1019: c.SignAndPushP2PNotaryRequest is deprecated require.NotNil(t, err) }) t.Run("too large fallbackValidFor", func(t *testing.T) { - _, err := c.SignAndPushP2PNotaryRequest(nil, []byte{byte(opcode.RET)}, -1, 0, 141, acc) + _, err := c.SignAndPushP2PNotaryRequest(nil, []byte{byte(opcode.RET)}, -1, 0, 141, acc) //nolint:staticcheck // SA1019: c.SignAndPushP2PNotaryRequest is deprecated require.NotNil(t, err) }) @@ -1031,7 +1062,7 @@ func TestSignAndPushP2PNotaryRequest(t *testing.T) { } mainTx := expected _ = expected.Hash() - req, err := c.SignAndPushP2PNotaryRequest(&mainTx, []byte{byte(opcode.RET)}, -1, 0, 6, acc) + req, err := c.SignAndPushP2PNotaryRequest(&mainTx, []byte{byte(opcode.RET)}, -1, 0, 6, acc) //nolint:staticcheck // SA1019: c.SignAndPushP2PNotaryRequest is deprecated require.NoError(t, err) // check that request was correctly completed