From bc3186575ff47b75a811a78ddef3b10869f2d4e2 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 6 Dec 2023 19:45:17 +0300 Subject: [PATCH] [#53] proxy: Allow using proxy by trusted accounts Signed-off-by: Evgenii Stratonikov --- proxy/proxy_contract.go | 40 +++++++++++++++---- rpcclient/proxy/client.go | 48 ++++++++++++++++++++++- tests/proxy_test.go | 82 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 155 insertions(+), 15 deletions(-) diff --git a/proxy/proxy_contract.go b/proxy/proxy_contract.go index 7157062..be59473 100644 --- a/proxy/proxy_contract.go +++ b/proxy/proxy_contract.go @@ -5,10 +5,13 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop" "github.com/nspcc-dev/neo-go/pkg/interop/native/gas" "github.com/nspcc-dev/neo-go/pkg/interop/native/management" - "github.com/nspcc-dev/neo-go/pkg/interop/native/neo" "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/interop/storage" + "github.com/nspcc-dev/neo-go/pkg/interop/util" ) +const accountKeyPrefix = 'a' + // OnNEP17Payment is a callback for NEP-17 compatible native GAS contract. func OnNEP17Payment(from interop.Hash160, amount int, data any) { caller := runtime.GetCallingScriptHash() @@ -40,19 +43,40 @@ func Update(script []byte, manifest []byte, data any) { // Verify method returns true if transaction contains valid multisignature of // Alphabet nodes of the Inner Ring. -func Verify() bool { - alphabet := neo.GetCommittee() - sig := common.Multiaddress(alphabet, false) +func Verify(addr interop.Hash160) bool { + common.CheckWitness(addr) - if !runtime.CheckWitness(sig) { - sig = common.Multiaddress(alphabet, true) - return runtime.CheckWitness(sig) + ctx := storage.GetReadOnlyContext() + if storage.Get(ctx, append([]byte{accountKeyPrefix}, addr...)) != nil { + return true } - return true + if util.Equals(addr, common.CommitteeAddress()) { + return true + } + + if util.Equals(addr, common.AlphabetAddress()) { + return true + } + + return false } // Version returns the version of the contract. func Version() int { return common.Version } + +func AddAccount(addr interop.Hash160) { + common.CheckWitness(common.CommitteeAddress()) + + ctx := storage.GetContext() + storage.Put(ctx, append([]byte{accountKeyPrefix}, addr...), []byte{1}) +} + +func RemoveAccount(addr interop.Hash160) { + common.CheckWitness(common.CommitteeAddress()) + + ctx := storage.GetContext() + storage.Delete(ctx, append([]byte{accountKeyPrefix}, addr...)) +} diff --git a/rpcclient/proxy/client.go b/rpcclient/proxy/client.go index 10acfda..7481ff9 100644 --- a/rpcclient/proxy/client.go +++ b/rpcclient/proxy/client.go @@ -52,8 +52,8 @@ func New(actor Actor, hash util.Uint160) *Contract { } // Verify invokes `verify` method of contract. -func (c *ContractReader) Verify() (bool, error) { - return unwrap.Bool(c.invoker.Call(c.hash, "verify")) +func (c *ContractReader) Verify(addr util.Uint160) (bool, error) { + return unwrap.Bool(c.invoker.Call(c.hash, "verify", addr)) } // Version invokes `version` method of contract. @@ -61,6 +61,50 @@ func (c *ContractReader) Version() (*big.Int, error) { return unwrap.BigInt(c.invoker.Call(c.hash, "version")) } +// AddAccount creates a transaction invoking `addAccount` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) AddAccount(addr util.Uint160) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "addAccount", addr) +} + +// AddAccountTransaction creates a transaction invoking `addAccount` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) AddAccountTransaction(addr util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "addAccount", addr) +} + +// AddAccountUnsigned creates a transaction invoking `addAccount` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) AddAccountUnsigned(addr util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "addAccount", nil, addr) +} + +// RemoveAccount creates a transaction invoking `removeAccount` method of the contract. +// This transaction is signed and immediately sent to the network. +// The values returned are its hash, ValidUntilBlock value and error if any. +func (c *Contract) RemoveAccount(addr util.Uint160) (util.Uint256, uint32, error) { + return c.actor.SendCall(c.hash, "removeAccount", addr) +} + +// RemoveAccountTransaction creates a transaction invoking `removeAccount` method of the contract. +// This transaction is signed, but not sent to the network, instead it's +// returned to the caller. +func (c *Contract) RemoveAccountTransaction(addr util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeCall(c.hash, "removeAccount", addr) +} + +// RemoveAccountUnsigned creates a transaction invoking `removeAccount` method of the contract. +// This transaction is not signed, it's simply returned to the caller. +// Any fields of it that do not affect fees can be changed (ValidUntilBlock, +// Nonce), fee values (NetworkFee, SystemFee) can be increased as well. +func (c *Contract) RemoveAccountUnsigned(addr util.Uint160) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(c.hash, "removeAccount", nil, addr) +} + // Update creates a transaction invoking `update` method of the contract. // This transaction is signed and immediately sent to the network. // The values returned are its hash, ValidUntilBlock value and error if any. diff --git a/tests/proxy_test.go b/tests/proxy_test.go index 845fb16..5b89443 100644 --- a/tests/proxy_test.go +++ b/tests/proxy_test.go @@ -1,12 +1,21 @@ package tests import ( + "errors" "path" "testing" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "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/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "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" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" ) const proxyPath = "../proxy" @@ -36,13 +45,76 @@ func newProxyInvoker(t *testing.T) *neotest.ContractInvoker { func TestVerify(t *testing.T) { e := newProxyInvoker(t) + acc := e.NewAccount(t) - const method = "verify" + gas := e.NewInvoker(e.NativeHash(t, nativenames.Gas), e.Validator) + gas.Invoke(t, true, "transfer", e.Validator.ScriptHash(), e.Hash, 100_0000_0000, nil) - e.Invoke(t, stackitem.NewBool(true), method) + t.Run("proxy + committee", func(t *testing.T) { + s := &proxySigner{contract: e.Hash, account: e.CommitteeHash} + tx := e.PrepareInvocation(t, []byte{byte(opcode.RET)}, []neotest.Signer{s, e.Committee}) + require.NoError(t, e.Chain.VerifyTx(tx)) + }) + t.Run("proxy + custom account", func(t *testing.T) { + s := &proxySigner{contract: e.Hash, account: acc.ScriptHash()} + t.Run("bad, only proxy", func(t *testing.T) { + tx := e.PrepareInvocation(t, []byte{byte(opcode.RET)}, []neotest.Signer{s, acc}) + require.Error(t, e.Chain.VerifyTx(tx)) + }) - notAlphabet := e.NewAccount(t) - cNotAlphabet := e.WithSigners(notAlphabet) + e.Invoke(t, stackitem.Null{}, "addAccount", s.account) - cNotAlphabet.Invoke(t, stackitem.NewBool(false), method) + tx := e.PrepareInvocation(t, []byte{byte(opcode.RET)}, []neotest.Signer{s, acc}) + require.NoError(t, e.Chain.VerifyTx(tx)) + }) +} + +type proxySigner struct { + contract util.Uint160 + account util.Uint160 +} + +var _ neotest.ContractSigner = (*proxySigner)(nil) + +func (s *proxySigner) Script() []byte { + return nil +} +func (s *proxySigner) ScriptHash() util.Uint160 { + return s.contract +} +func (s *proxySigner) SignHashable(uint32, hash.Hashable) []byte { + panic("not implemented") +} +func (s *proxySigner) SignTx(_ netmode.Magic, tx *transaction.Transaction) error { + pos := -1 + for i := range tx.Signers { + if tx.Signers[i].Account.Equals(s.contract) { + pos = i + break + } + } + if pos < 0 { + return errors.New("transaction is not signed by this account") + } + 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 + } + + w := transaction.Witness{InvocationScript: invoc} + if len(tx.Scripts) == pos { + tx.Scripts = append(tx.Scripts, w) + } else { + tx.Scripts[pos].InvocationScript = invoc + } + return nil +} +func (s *proxySigner) InvocationScript(tx *transaction.Transaction) ([]byte, error) { + w := io.NewBufBinWriter() + emit.Any(w.BinWriter, s.account) + return w.Bytes(), nil }