From 962b161652762e26de74e5cfff930873bf1f04e1 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 8 Dec 2023 15:38:46 +0300 Subject: [PATCH 1/2] wallet: allow complex contract verification schemes, close #3015 This was recently added in neotest, but working with the real RPC is still not enjoyable. This commit extends `wallet.Account` with invocation script builder to aid network fee calculations and signing. Signed-off-by: Evgenii Stratonikov --- pkg/rpcclient/actor/maker.go | 8 ++++++++ pkg/wallet/account.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/pkg/rpcclient/actor/maker.go b/pkg/rpcclient/actor/maker.go index 0aa346ab7..8c6c1c7a4 100644 --- a/pkg/rpcclient/actor/maker.go +++ b/pkg/rpcclient/actor/maker.go @@ -191,6 +191,14 @@ func (a *Actor) MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []tr for i := range a.signers { if !a.signers[i].Account.Contract.Deployed { tx.Scripts[i].VerificationScript = a.signers[i].Account.Contract.Script + continue + } + if build := a.signers[i].Account.Contract.InvocationBuilder; build != nil { + invoc, err := build(tx) + if err != nil { + return nil, fmt.Errorf("building witness for contract signer: %w", err) + } + tx.Scripts[i].InvocationScript = invoc } } // CalculateNetworkFee doesn't call Hash or Size, only serializes the diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 4658d63f0..86b14fff5 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -9,8 +9,10 @@ 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/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) @@ -55,6 +57,12 @@ type Contract struct { // Indicates whether the contract has been deployed to the blockchain. Deployed bool `json:"deployed"` + + // InvocationBuilder returns invocation script for deployed contracts. + // In case contract is not deployed or has 0 arguments, this field is ignored. + // It might be executed on a partially formed tx, and is primarily needed to properly + // calculate network fee for complex contract signers. + InvocationBuilder func(tx *transaction.Transaction) ([]byte, error) `json:"-"` } // ContractParam is a descriptor of a contract parameter @@ -78,6 +86,29 @@ func NewAccount() (*Account, error) { return NewAccountFromPrivateKey(priv), nil } +// NewContractAccount creates a contract account belonging to some deployed contract. +// SignTx can be called on this account with no error and will create invocation script, +// which puts provided arguments on stack for use in `verify`. +func NewContractAccount(hash util.Uint160, args ...any) *Account { + return &Account{ + Address: address.Uint160ToString(hash), + Contract: &Contract{ + Parameters: make([]ContractParam, len(args)), + Deployed: true, + InvocationBuilder: func(tx *transaction.Transaction) ([]byte, error) { + w := io.NewBufBinWriter() + for i := range args { + emit.Any(w.BinWriter, args[i]) + } + if w.Err != nil { + return nil, w.Err + } + return w.Bytes(), nil + }, + }, + } +} + // SignTx signs transaction t and updates it's Witnesses. func (a *Account) SignTx(net netmode.Magic, t *transaction.Transaction) error { var ( @@ -108,6 +139,11 @@ func (a *Account) SignTx(net netmode.Magic, t *transaction.Transaction) error { VerificationScript: a.Contract.Script, // Can be nil for deployed contract. }) } + if a.Contract.Deployed && a.Contract.InvocationBuilder != nil { + invoc, err := a.Contract.InvocationBuilder(t) + t.Scripts[pos].InvocationScript = invoc + return err + } if len(a.Contract.Parameters) == 0 { return nil } From c8ff44560e5e4ecd596239d5a07063a4a134cb28 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 8 Dec 2023 15:38:54 +0300 Subject: [PATCH 2/2] neotest: reuse wallet.Account for contract signers Refs #3245 Refs #3233 Signed-off-by: Evgenii Stratonikov --- pkg/neotest/basic.go | 5 +-- pkg/neotest/signer.go | 79 +++++++++++++++---------------------------- 2 files changed, 30 insertions(+), 54 deletions(-) diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index c64a6b4a2..c789d7ae6 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -301,8 +301,9 @@ func AddNetworkFee(t testing.TB, bc *core.Blockchain, tx *transaction.Transactio baseFee := bc.GetBaseExecFee() size := io.GetVarSize(tx) for _, sgr := range signers { - if csgr, ok := sgr.(ContractSigner); ok { - sc, err := csgr.InvocationScript(tx) + csgr, ok := sgr.(SingleSigner) + if ok && csgr.Account().Contract.InvocationBuilder != nil { + sc, err := csgr.Account().Contract.InvocationBuilder(tx) require.NoError(t, err) txCopy := *tx diff --git a/pkg/neotest/signer.go b/pkg/neotest/signer.go index 8446e177f..a8793cc6a 100644 --- a/pkg/neotest/signer.go +++ b/pkg/neotest/signer.go @@ -2,7 +2,6 @@ package neotest import ( "bytes" - "errors" "fmt" "sort" "testing" @@ -11,6 +10,7 @@ import ( "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/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -47,13 +47,6 @@ type MultiSigner interface { Single(n int) SingleSigner } -// ContractSigner is an interface for contract signer. -type ContractSigner interface { - Signer - // InvocationScript returns an invocation script to be used as invocation script for contract-based witness. - InvocationScript(tx *transaction.Transaction) ([]byte, error) -} - // signer represents a simple-signature signer. type signer wallet.Account @@ -190,33 +183,30 @@ func checkMultiSigner(t testing.TB, s Signer) { } } -type contractSigner struct { - params func(tx *transaction.Transaction) []any - scriptHash util.Uint160 -} +type contractSigner wallet.Account // NewContractSigner returns a contract signer for the provided contract hash. // getInvParams must return params to be used as invocation script for contract-based witness. -func NewContractSigner(h util.Uint160, getInvParams func(tx *transaction.Transaction) []any) ContractSigner { +func NewContractSigner(h util.Uint160, getInvParams func(tx *transaction.Transaction) []any) SingleSigner { return &contractSigner{ - scriptHash: h, - params: getInvParams, + Address: address.Uint160ToString(h), + Contract: &wallet.Contract{ + Deployed: true, + InvocationBuilder: func(tx *transaction.Transaction) ([]byte, error) { + params := getInvParams(tx) + script := io.NewBufBinWriter() + for i := range params { + emit.Any(script.BinWriter, params[i]) + } + if script.Err != nil { + return nil, script.Err + } + return script.Bytes(), nil + }, + }, } } -// InvocationScript implements ContractSigner. -func (s *contractSigner) InvocationScript(tx *transaction.Transaction) ([]byte, error) { - params := s.params(tx) - script := io.NewBufBinWriter() - for i := range params { - emit.Any(script.BinWriter, params[i]) - } - if script.Err != nil { - return nil, script.Err - } - return script.Bytes(), nil -} - // Script implements ContractSigner. func (s *contractSigner) Script() []byte { return []byte{} @@ -224,7 +214,12 @@ func (s *contractSigner) Script() []byte { // ScriptHash implements ContractSigner. func (s *contractSigner) ScriptHash() util.Uint160 { - return s.scriptHash + return s.Account().ScriptHash() +} + +// ScriptHash implements ContractSigner. +func (s *contractSigner) Account() *wallet.Account { + return (*wallet.Account)(s) } // SignHashable implements ContractSigner. @@ -234,27 +229,7 @@ func (s *contractSigner) SignHashable(uint32, hash.Hashable) []byte { // SignTx implements ContractSigner. func (s *contractSigner) SignTx(magic netmode.Magic, tx *transaction.Transaction) error { - pos := -1 - for idx := range tx.Signers { - if tx.Signers[idx].Account.Equals(s.ScriptHash()) { - pos = idx - break - } - } - if pos < 0 { - return fmt.Errorf("signer %s not found", s.ScriptHash().String()) - } - if len(tx.Scripts) < pos { - return errors.New("transaction is not yet signed by the previous signer") - } - invoc, err := s.InvocationScript(tx) - if err != nil { - return err - } - if len(tx.Scripts) == pos { - tx.Scripts = append(tx.Scripts, transaction.Witness{}) - } - tx.Scripts[pos].InvocationScript = invoc - tx.Scripts[pos].VerificationScript = s.Script() - return nil + // Here we rely on `len(s.Contract.Parameters) == 0` being after the `s.Contract.InvocationBuilder != nil` check, + // because we cannot determine the list of parameters unless we already have tx. + return s.Account().SignTx(magic, tx) }