Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow complex contract signature schemes in wallet.Account #3256

Merged
merged 2 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pkg/neotest/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 27 additions & 52 deletions pkg/neotest/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package neotest

import (
"bytes"
"errors"
"fmt"
"sort"
"testing"
Expand All @@ -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"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -190,31 +183,28 @@ 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,
}
}

// 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
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
},
},
}
return script.Bytes(), nil
}

// Script implements ContractSigner.
Expand All @@ -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.
Expand All @@ -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)
}
8 changes: 8 additions & 0 deletions pkg/rpcclient/actor/maker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions pkg/wallet/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -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
}
Expand Down