From 23e85d11c499710073ac87625374069fc5520fab Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 15 Dec 2023 13:50:01 +0300 Subject: [PATCH] [#53] proxy: Allow using proxy by trusted accounts It was reverted because `Verify` with arguments was not well supported by the client software. Thanks to recent `System.Runtime.CurrentSigners` call from neo-go v0.104.0 we can take the best of both worlds. Verify with argument still looks better (less overhead), but this implementation should work too. Sadly, `overloads` are not of much use here because verification routines take the _first_ method from the manifest, albeit with arbitrary number of arguments. This reverts commit a0b73150c6ae2fe4b108d9e2b434904f4a66e5b3 with some changes on-top. Signed-off-by: Evgenii Stratonikov --- proxy/proxy_contract.go | 46 +++++++++++++++++++++++++++++++-------- rpcclient/proxy/client.go | 44 +++++++++++++++++++++++++++++++++++++ tests/proxy_test.go | 26 +++++++++++++++++----- 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/proxy/proxy_contract.go b/proxy/proxy_contract.go index 7157062..d9c1515 100644 --- a/proxy/proxy_contract.go +++ b/proxy/proxy_contract.go @@ -5,10 +5,12 @@ 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" ) +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() @@ -39,20 +41,46 @@ func Update(script []byte, manifest []byte, data any) { } // Verify method returns true if transaction contains valid multisignature of -// Alphabet nodes of the Inner Ring. +// Alphabet nodes of the Inner Ring or any of the trusted accounts added via AddAccount. func Verify() bool { - alphabet := neo.GetCommittee() - sig := common.Multiaddress(alphabet, false) - - if !runtime.CheckWitness(sig) { - sig = common.Multiaddress(alphabet, true) - return runtime.CheckWitness(sig) + if !runtime.GetScriptContainer().Sender.Equals(runtime.GetExecutingScriptHash()) { + return false } - return true + signers := runtime.CurrentSigners() + ctx := storage.GetReadOnlyContext() + for i := 1; /* skip sender */ i < len(signers); i++ { + if storage.Get(ctx, append([]byte{accountKeyPrefix}, signers[i].Account...)) != nil { + return true + } + } + + if runtime.CheckWitness(common.CommitteeAddress()) { + return true + } + + if runtime.CheckWitness(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..011aa9c 100644 --- a/rpcclient/proxy/client.go +++ b/rpcclient/proxy/client.go @@ -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..e1b1e4a 100644 --- a/tests/proxy_test.go +++ b/tests/proxy_test.go @@ -4,9 +4,13 @@ import ( "path" "testing" + "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/neotest" "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/stretchr/testify/require" ) const proxyPath = "../proxy" @@ -36,13 +40,25 @@ 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) + s := neotest.NewContractSigner(e.Hash, func(*transaction.Transaction) []any { return nil }) + t.Run("proxy + committee", func(t *testing.T) { + 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) { + 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", acc.ScriptHash()) - 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)) + }) }