github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/notary/actor.go (about)

     1  package notary
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/nspcc-dev/neo-go/pkg/core/state"
    10  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    11  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    12  	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
    13  	"github.com/nspcc-dev/neo-go/pkg/network/payload"
    14  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
    15  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
    16  	"github.com/nspcc-dev/neo-go/pkg/util"
    17  	"github.com/nspcc-dev/neo-go/pkg/vm"
    18  	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
    19  	"github.com/nspcc-dev/neo-go/pkg/wallet"
    20  )
    21  
    22  // Actor encapsulates everything needed to create proper notary requests for
    23  // assisted transactions.
    24  type Actor struct {
    25  	// Actor is the main transaction actor, it has appropriate attributes and
    26  	// transaction modifiers to set ValidUntilBlock. Use it to create main
    27  	// transactions that have incomplete set of signatures. They can be
    28  	// signed (using available wallets), but can not be sent directly to the
    29  	// network. Instead of sending them to the network use Actor methods to
    30  	// wrap them into notary requests.
    31  	actor.Actor
    32  	// FbActor is the fallback transaction actor, it has two required signers
    33  	// and a set of attributes expected from a fallback transaction. It can
    34  	// be used to create _unsigned_ transactions with whatever actions
    35  	// required (but no additional attributes can be added). Signing them
    36  	// while technically possible (with notary contract signature missing),
    37  	// will lead to incorrect transaction because NotValidBefore and
    38  	// Conflicts attributes as well as ValidUntilBlock field can be
    39  	// correctly set only when some main transaction is available.
    40  	FbActor actor.Actor
    41  
    42  	fbScript []byte
    43  	reader   *ContractReader
    44  	sender   *wallet.Account
    45  	rpc      RPCActor
    46  }
    47  
    48  // ActorOptions are used to influence main and fallback actors as well as the
    49  // default Notarize behavior.
    50  type ActorOptions struct {
    51  	// FbAttributes are additional attributes to be added into fallback
    52  	// transaction by an appropriate actor. Irrespective of this setting
    53  	// (which defaults to nil) NotaryAssisted, NotValidBefore and Conflicts
    54  	// attributes are always added.
    55  	FbAttributes []transaction.Attribute
    56  	// FbScript is the script to use in the Notarize convenience method, it
    57  	// defaults to a simple RET instruction (doing nothing).
    58  	FbScript []byte
    59  	// FbSigner is the second signer to be used for the fallback transaction.
    60  	// By default it's derived from the account and has None scope, it has
    61  	// to be a simple signature or deployed contract account, but this setting
    62  	// allows you to give it some other scope to be used in complex fallback
    63  	// scripts.
    64  	FbSigner actor.SignerAccount
    65  	// MainAttribtues are additional attributes to be added into main
    66  	// transaction by an appropriate actor. Irrespective of this setting
    67  	// (which defaults to nil) NotaryAssisted attribute is always added.
    68  	MainAttributes []transaction.Attribute
    69  	// MainCheckerModifier will be used by the main Actor when creating
    70  	// transactions. It defaults to using [actor.DefaultCheckerModifier]
    71  	// for result check and adds MaxNotValidBeforeDelta to the
    72  	// ValidUntilBlock transaction's field. Only override it if you know
    73  	// what you're doing.
    74  	MainCheckerModifier actor.TransactionCheckerModifier
    75  	// MainModifier will be used by the main Actor when creating
    76  	// transactions. By default it adds MaxNotValidBeforeDelta to the
    77  	// ValidUntilBlock transaction's field. Only override it if you know
    78  	// what you're doing.
    79  	MainModifier actor.TransactionModifier
    80  }
    81  
    82  // RPCActor is a set of methods required from RPC client to create Actor.
    83  type RPCActor interface {
    84  	actor.RPCActor
    85  
    86  	SubmitP2PNotaryRequest(req *payload.P2PNotaryRequest) (util.Uint256, error)
    87  }
    88  
    89  // NewDefaultActorOptions returns the default Actor options. Internal functions
    90  // of it need some data from the contract, so it should be added.
    91  func NewDefaultActorOptions(reader *ContractReader, acc *wallet.Account) *ActorOptions {
    92  	opts := &ActorOptions{
    93  		FbScript: []byte{byte(opcode.RET)},
    94  		FbSigner: actor.SignerAccount{
    95  			Signer: transaction.Signer{
    96  				Account: acc.Contract.ScriptHash(),
    97  				Scopes:  transaction.None,
    98  			},
    99  			Account: acc,
   100  		},
   101  		MainModifier: func(t *transaction.Transaction) error {
   102  			nvbDelta, err := reader.GetMaxNotValidBeforeDelta()
   103  			if err != nil {
   104  				return fmt.Errorf("can't get MaxNVBDelta: %w", err)
   105  			}
   106  			t.ValidUntilBlock += nvbDelta
   107  			return nil
   108  		},
   109  	}
   110  	opts.MainCheckerModifier = func(r *result.Invoke, t *transaction.Transaction) error {
   111  		err := actor.DefaultCheckerModifier(r, t)
   112  		if err != nil {
   113  			return err
   114  		}
   115  		return opts.MainModifier(t)
   116  	}
   117  	return opts
   118  }
   119  
   120  // NewActor creates a new notary.Actor using the given RPC client, the set of
   121  // signers for main transactions and the account that will sign notary requests
   122  // (one plain signature or contract-based). The set of signers will be extended
   123  // by the notary contract signer with the None scope (as required by the notary
   124  // protocol) and all transactions created with the resulting Actor will get a
   125  // NotaryAssisted attribute with appropriate number of keys specified
   126  // (depending on signers). A fallback Actor will be created as well with the
   127  // notary contract and simpleAcc signers and a full set of required fallback
   128  // transaction attributes (NotaryAssisted, NotValidBefore and Conflicts).
   129  func NewActor(c RPCActor, signers []actor.SignerAccount, simpleAcc *wallet.Account) (*Actor, error) {
   130  	return newTunedActor(c, signers, simpleAcc, nil)
   131  }
   132  
   133  // NewTunedActor is the same as NewActor, but allows to override the default
   134  // options (see ActorOptions for details). Use with care.
   135  func NewTunedActor(c RPCActor, signers []actor.SignerAccount, opts *ActorOptions) (*Actor, error) {
   136  	return newTunedActor(c, signers, opts.FbSigner.Account, opts)
   137  }
   138  
   139  func newTunedActor(c RPCActor, signers []actor.SignerAccount, simpleAcc *wallet.Account, opts *ActorOptions) (*Actor, error) {
   140  	if len(signers) < 1 {
   141  		return nil, errors.New("at least one signer (sender) is required")
   142  	}
   143  	var nKeys int
   144  	for _, sa := range signers {
   145  		if sa.Account.Contract == nil {
   146  			return nil, fmt.Errorf("empty contract for account %s", sa.Account.Address)
   147  		}
   148  		if sa.Account.Contract.Deployed {
   149  			continue
   150  		}
   151  		if vm.IsSignatureContract(sa.Account.Contract.Script) {
   152  			nKeys++
   153  			continue
   154  		}
   155  		_, pubs, ok := vm.ParseMultiSigContract(sa.Account.Contract.Script)
   156  		if !ok {
   157  			return nil, fmt.Errorf("signer %s is not a contract- or signature-based", sa.Account.Address)
   158  		}
   159  		nKeys += len(pubs)
   160  	}
   161  	if nKeys > 255 {
   162  		return nil, fmt.Errorf("notary subsystem can't handle more than 255 signatures")
   163  	}
   164  	if simpleAcc.Contract == nil {
   165  		return nil, errors.New("bad simple account: no contract")
   166  	}
   167  	if !simpleAcc.CanSign() {
   168  		return nil, errors.New("bad simple account: can't sign")
   169  	}
   170  	if !vm.IsSignatureContract(simpleAcc.Contract.Script) && !simpleAcc.Contract.Deployed {
   171  		return nil, errors.New("bad simple account: neither plain signature, nor contract")
   172  	}
   173  	// Not reusing mainActor/fbActor for ContractReader to make requests a bit lighter.
   174  	reader := NewReader(invoker.New(c, nil))
   175  	if opts == nil {
   176  		defOpts := NewDefaultActorOptions(reader, simpleAcc)
   177  		opts = defOpts
   178  	}
   179  	var notarySA = actor.SignerAccount{
   180  		Signer: transaction.Signer{
   181  			Account: Hash,
   182  			Scopes:  transaction.None,
   183  		},
   184  		Account: FakeContractAccount(Hash),
   185  	}
   186  
   187  	var mainSigners = make([]actor.SignerAccount, len(signers), len(signers)+1)
   188  	copy(mainSigners, signers)
   189  	mainSigners = append(mainSigners, notarySA)
   190  
   191  	mainOpts := actor.Options{
   192  		Attributes: []transaction.Attribute{{
   193  			Type:  transaction.NotaryAssistedT,
   194  			Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)},
   195  		}},
   196  		CheckerModifier: opts.MainCheckerModifier,
   197  		Modifier:        opts.MainModifier,
   198  	}
   199  	mainOpts.Attributes = append(mainOpts.Attributes, opts.MainAttributes...)
   200  
   201  	mainActor, err := actor.NewTuned(c, mainSigners, mainOpts)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	fbSigners := []actor.SignerAccount{notarySA, opts.FbSigner}
   207  	fbOpts := actor.Options{
   208  		Attributes: []transaction.Attribute{{
   209  			Type:  transaction.NotaryAssistedT,
   210  			Value: &transaction.NotaryAssisted{NKeys: 0},
   211  		}, {
   212  			// A stub, it has correct size, but the contents is to be filled per-request.
   213  			Type:  transaction.NotValidBeforeT,
   214  			Value: &transaction.NotValidBefore{},
   215  		}, {
   216  			// A stub, it has correct size, but the contents is to be filled per-request.
   217  			Type:  transaction.ConflictsT,
   218  			Value: &transaction.Conflicts{},
   219  		}},
   220  	}
   221  	fbOpts.Attributes = append(fbOpts.Attributes, opts.FbAttributes...)
   222  	fbActor, err := actor.NewTuned(c, fbSigners, fbOpts)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return &Actor{*mainActor, *fbActor, opts.FbScript, reader, simpleAcc, c}, nil
   227  }
   228  
   229  // Notarize is a simple wrapper for transaction-creating functions that allows to
   230  // send any partially-signed transaction in a notary request with a fallback
   231  // transaction created based on Actor settings and SendRequest adjustment rules.
   232  // The values returned are main and fallback transaction hashes, ValidUntilBlock
   233  // and error if any.
   234  func (a *Actor) Notarize(mainTx *transaction.Transaction, err error) (util.Uint256, util.Uint256, uint32, error) {
   235  	var (
   236  		// Just to simplify return values on error.
   237  		fbHash   util.Uint256
   238  		mainHash util.Uint256
   239  		vub      uint32
   240  	)
   241  	if err != nil {
   242  		return mainHash, fbHash, vub, err
   243  	}
   244  	fbTx, err := a.FbActor.MakeUnsignedRun(a.fbScript, nil)
   245  	if err != nil {
   246  		return mainHash, fbHash, vub, err
   247  	}
   248  	return a.SendRequest(mainTx, fbTx)
   249  }
   250  
   251  // SendRequest creates and sends a notary request using the given main and
   252  // fallback transactions. It accepts signed main transaction and unsigned fallback
   253  // transaction that will be adjusted in its NotValidBefore and Conflicts
   254  // attributes as well as ValidUntilBlock value. Conflicts is set to the main
   255  // transaction hash, while NotValidBefore is set to the middle of current mainTx
   256  // lifetime (between current block and ValidUntilBlock). The values returned are
   257  // main and fallback transaction hashes, ValidUntilBlock and error if any.
   258  func (a *Actor) SendRequest(mainTx *transaction.Transaction, fbTx *transaction.Transaction) (util.Uint256, util.Uint256, uint32, error) {
   259  	var (
   260  		fbHash   util.Uint256
   261  		mainHash = mainTx.Hash()
   262  		vub      = mainTx.ValidUntilBlock
   263  	)
   264  	if len(fbTx.Attributes) < 3 {
   265  		return mainHash, fbHash, vub, errors.New("invalid fallback: missing required attributes")
   266  	}
   267  	if fbTx.Attributes[1].Type != transaction.NotValidBeforeT {
   268  		return mainHash, fbHash, vub, errors.New("invalid fallback: NotValidBefore is missing where expected")
   269  	}
   270  	if fbTx.Attributes[2].Type != transaction.ConflictsT {
   271  		return mainHash, fbHash, vub, errors.New("invalid fallback: Conflicts is missing where expected")
   272  	}
   273  	height, err := a.GetBlockCount()
   274  	if err != nil {
   275  		return mainHash, fbHash, vub, err
   276  	}
   277  	// New values must be created to avoid overwriting originals via a pointer.
   278  	fbTx.Attributes[1].Value = &transaction.NotValidBefore{Height: (height + vub) / 2}
   279  	fbTx.Attributes[2].Value = &transaction.Conflicts{Hash: mainHash}
   280  	fbTx.ValidUntilBlock = vub
   281  	err = a.FbActor.Sign(fbTx)
   282  	if err != nil {
   283  		return mainHash, fbHash, vub, err
   284  	}
   285  	fbTx.Scripts[0].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...) // Must be present.
   286  	return a.SendRequestExactly(mainTx, fbTx)
   287  }
   288  
   289  // SendRequestExactly accepts signed and completely prepared main and fallback
   290  // transactions, creates a P2P notary request containing them, signs and sends
   291  // it to the network. Caller takes full responsibility for transaction
   292  // correctness in this case, use this method only if you know exactly that you
   293  // need to override some of the other method's behavior and you can do it. The
   294  // values returned are main and fallback transaction hashes, ValidUntilBlock
   295  // and error if any.
   296  func (a *Actor) SendRequestExactly(mainTx *transaction.Transaction, fbTx *transaction.Transaction) (util.Uint256, util.Uint256, uint32, error) {
   297  	var (
   298  		fbHash   = fbTx.Hash()
   299  		mainHash = mainTx.Hash()
   300  		vub      = mainTx.ValidUntilBlock
   301  	)
   302  	req := &payload.P2PNotaryRequest{
   303  		MainTransaction:     mainTx,
   304  		FallbackTransaction: fbTx,
   305  	}
   306  	req.Witness = transaction.Witness{
   307  		InvocationScript:   append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, a.sender.SignHashable(a.GetNetwork(), req)...),
   308  		VerificationScript: a.sender.GetVerificationScript(),
   309  	}
   310  	actualHash, err := a.rpc.SubmitP2PNotaryRequest(req)
   311  	if err != nil {
   312  		return mainHash, fbHash, vub, fmt.Errorf("failed to submit notary request: %w", err)
   313  	}
   314  	if !actualHash.Equals(fbHash) {
   315  		return mainHash, fbHash, vub, fmt.Errorf("sent and actual fallback tx hashes mismatch: %v vs %v", fbHash.StringLE(), actualHash.StringLE())
   316  	}
   317  	return mainHash, fbHash, vub, nil
   318  }
   319  
   320  // Wait waits until main or fallback transaction will be accepted to the chain and returns
   321  // the resulting application execution result or actor.ErrTxNotAccepted if both transactions
   322  // failed to persist. Wait can be used if underlying Actor supports transaction awaiting,
   323  // see actor.Actor and actor.Waiter documentation for details. Wait may be used as a wrapper
   324  // for Notarize, SendRequest or SendRequestExactly. Notice that "already exists" or "already
   325  // on chain" answers are not treated as errors by this routine because they mean that some
   326  // of the transactions given might be already accepted or soon going to be accepted. These
   327  // transactions can be waited for in a usual way potentially with positive result.
   328  func (a *Actor) Wait(mainHash, fbHash util.Uint256, vub uint32, err error) (*state.AppExecResult, error) {
   329  	// #2248 will eventually remove this garbage from the code.
   330  	if err != nil && !(strings.Contains(strings.ToLower(err.Error()), "already exists") || strings.Contains(strings.ToLower(err.Error()), "already on chain")) {
   331  		return nil, err
   332  	}
   333  	return a.WaitAny(context.TODO(), vub, mainHash, fbHash)
   334  }