neo-go/pkg/rpcclient/notary/actor_test.go
Roman Khimov a327a82085 actor: add a new WaitSuccess API
Most of the time people are interested in successful executions. Unfortunately,
unwrap package can't help here because of a different result structure (some
interface abstract can help, but it's still mostly stack-oriented and sessions
can be a problem), so this additional interface is needed.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
2024-06-21 18:39:02 +03:00

608 lines
17 KiB
Go

package notary
import (
"context"
"errors"
"testing"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"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/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"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/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
type RPCClient struct {
err error
invRes *result.Invoke
netFee int64
bCount uint32
version *result.Version
hash util.Uint256
nhash util.Uint256
mirror bool
applog *result.ApplicationLog
}
func (r *RPCClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
return r.invRes, r.err
}
func (r *RPCClient) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
return r.invRes, r.err
}
func (r *RPCClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
return r.invRes, r.err
}
func (r *RPCClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) {
return r.netFee, r.err
}
func (r *RPCClient) GetBlockCount() (uint32, error) {
return r.bCount, r.err
}
func (r *RPCClient) GetVersion() (*result.Version, error) {
verCopy := *r.version
return &verCopy, r.err
}
func (r *RPCClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) {
return r.hash, r.err
}
func (r *RPCClient) SubmitP2PNotaryRequest(req *payload.P2PNotaryRequest) (util.Uint256, error) {
if r.mirror {
return req.FallbackTransaction.Hash(), nil
}
return r.nhash, r.err
}
func (r *RPCClient) TerminateSession(sessionID uuid.UUID) (bool, error) {
return false, nil // Just a stub, unused by actor.
}
func (r *RPCClient) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
return nil, nil // Just a stub, unused by actor.
}
func (r *RPCClient) Context() context.Context {
return context.Background()
}
func (r *RPCClient) GetApplicationLog(hash util.Uint256, trig *trigger.Type) (*result.ApplicationLog, error) {
return r.applog, nil
}
var _ = waiter.RPCPollingBased(&RPCClient{})
func TestNewActor(t *testing.T) {
rc := &RPCClient{
version: &result.Version{
Protocol: result.Protocol{
Network: netmode.UnitTestNet,
MillisecondsPerBlock: 1000,
ValidatorsCount: 7,
},
},
}
_, err := NewActor(rc, nil, nil)
require.Error(t, err)
var (
keyz [4]*keys.PrivateKey
accs [4]*wallet.Account
faccs [4]*wallet.Account
pkeys [4]*keys.PublicKey
)
for i := range accs {
keyz[i], err = keys.NewPrivateKey()
require.NoError(t, err)
accs[i] = wallet.NewAccountFromPrivateKey(keyz[i])
pkeys[i] = keyz[i].PublicKey()
faccs[i] = FakeSimpleAccount(pkeys[i])
}
var multiAccs [4]*wallet.Account
for i := range accs {
multiAccs[i] = &wallet.Account{}
*multiAccs[i] = *accs[i]
require.NoError(t, multiAccs[i].ConvertMultisig(smartcontract.GetDefaultHonestNodeCount(len(pkeys)), pkeys[:]))
}
// nil Contract
badMultiAcc0 := &wallet.Account{}
*badMultiAcc0 = *multiAccs[0]
badMultiAcc0.Contract = nil
_, err = NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: multiAccs[0].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: badMultiAcc0,
}}, accs[0])
require.Error(t, err)
// Non-standard script.
badMultiAcc0.Contract = &wallet.Contract{}
*badMultiAcc0.Contract = *multiAccs[0].Contract
badMultiAcc0.Contract.Script = append(badMultiAcc0.Contract.Script, byte(opcode.NOP))
badMultiAcc0.Address = address.Uint160ToString(badMultiAcc0.Contract.ScriptHash())
_, err = NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: badMultiAcc0.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: badMultiAcc0,
}}, accs[0])
require.Error(t, err)
// Too many keys
var (
manyKeys [256]*keys.PrivateKey
manyPkeys [256]*keys.PublicKey
)
for i := range manyKeys {
manyKeys[i], err = keys.NewPrivateKey()
require.NoError(t, err)
manyPkeys[i] = manyKeys[i].PublicKey()
}
bigMultiAcc := &wallet.Account{}
*bigMultiAcc = *wallet.NewAccountFromPrivateKey(manyKeys[0])
require.NoError(t, bigMultiAcc.ConvertMultisig(129, manyPkeys[:]))
_, err = NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: bigMultiAcc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: bigMultiAcc,
}}, wallet.NewAccountFromPrivateKey(manyKeys[0]))
require.Error(t, err)
// No contract in the simple account.
badSimple0 := &wallet.Account{}
*badSimple0 = *accs[0]
badSimple0.Contract = nil
_, err = NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: multiAccs[0].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: multiAccs[0],
}}, badSimple0)
require.Error(t, err)
// Simple account that can't sign.
badSimple0 = FakeSimpleAccount(pkeys[0])
_, err = NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: multiAccs[0].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: multiAccs[0],
}}, badSimple0)
require.Error(t, err)
// Multisig account instead of simple one.
_, err = NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: multiAccs[0].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: multiAccs[0],
}}, multiAccs[0])
require.Error(t, err)
// Main actor freaking out on hash mismatch.
_, err = NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: accs[0].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: multiAccs[0],
}}, accs[0])
require.Error(t, err)
// FB actor freaking out on hash mismatch.
opts := NewDefaultActorOptions(NewReader(invoker.New(rc, nil)), accs[0])
opts.FbSigner.Signer.Account = multiAccs[0].Contract.ScriptHash()
_, err = NewTunedActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: multiAccs[0].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: multiAccs[0],
}}, opts)
require.Error(t, err)
// Good, one multisig.
multi0, err := NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: multiAccs[0].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: multiAccs[0],
}}, accs[0])
require.NoError(t, err)
script := []byte{byte(opcode.RET)}
rc.invRes = &result.Invoke{
State: "HALT",
GasConsumed: 3,
Script: script,
Stack: []stackitem.Item{stackitem.Make(42)},
}
tx, err := multi0.MakeRun(script)
require.NoError(t, err)
require.Equal(t, 1, len(tx.Attributes))
require.Equal(t, transaction.NotaryAssistedT, tx.Attributes[0].Type)
require.Equal(t, &transaction.NotaryAssisted{NKeys: 4}, tx.Attributes[0].Value)
// Good, 4 single sigs with one that can sign and one contract.
single4, err := NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: accs[0].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: accs[0],
}, {
Signer: transaction.Signer{
Account: faccs[1].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: faccs[1],
}, {
Signer: transaction.Signer{
Account: faccs[2].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: faccs[2],
}, {
Signer: transaction.Signer{
Account: accs[3].Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: faccs[3],
}, {
Signer: transaction.Signer{
Account: util.Uint160{1, 2, 3},
Scopes: transaction.CalledByEntry,
},
Account: FakeContractAccount(util.Uint160{1, 2, 3}),
}}, accs[0])
require.NoError(t, err)
tx, err = single4.MakeRun(script)
require.NoError(t, err)
require.Equal(t, 1, len(tx.Attributes))
require.Equal(t, transaction.NotaryAssistedT, tx.Attributes[0].Type)
require.Equal(t, &transaction.NotaryAssisted{NKeys: 4}, tx.Attributes[0].Value) // One account can sign, three need to collect additional sigs.
}
func TestSendRequestExactly(t *testing.T) {
rc := &RPCClient{
version: &result.Version{
Protocol: result.Protocol{
Network: netmode.UnitTestNet,
MillisecondsPerBlock: 1000,
ValidatorsCount: 7,
},
},
}
key0, err := keys.NewPrivateKey()
require.NoError(t, err)
key1, err := keys.NewPrivateKey()
require.NoError(t, err)
acc0 := wallet.NewAccountFromPrivateKey(key0)
facc1 := FakeSimpleAccount(key1.PublicKey())
act, err := NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: acc0.Contract.ScriptHash(),
Scopes: transaction.None,
},
Account: acc0,
}, {
Signer: transaction.Signer{
Account: facc1.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: facc1,
}}, acc0)
require.NoError(t, err)
script := []byte{byte(opcode.RET)}
mainTx := transaction.New(script, 1)
fbTx := transaction.New(script, 1)
// Hashes mismatch
_, _, _, err = act.SendRequestExactly(mainTx, fbTx)
require.Error(t, err)
// Error returned
rc.err = errors.New("")
_, _, _, err = act.SendRequestExactly(mainTx, fbTx)
require.Error(t, err)
// OK returned
rc.err = nil
rc.nhash = fbTx.Hash()
mHash, fbHash, vub, err := act.SendRequestExactly(mainTx, fbTx)
require.NoError(t, err)
require.Equal(t, mainTx.Hash(), mHash)
require.Equal(t, fbTx.Hash(), fbHash)
require.Equal(t, mainTx.ValidUntilBlock, vub)
}
func TestSendRequest(t *testing.T) {
rc := &RPCClient{
version: &result.Version{
Protocol: result.Protocol{
Network: netmode.UnitTestNet,
MillisecondsPerBlock: 1000,
ValidatorsCount: 7,
},
},
bCount: 42,
}
key0, err := keys.NewPrivateKey()
require.NoError(t, err)
key1, err := keys.NewPrivateKey()
require.NoError(t, err)
acc0 := wallet.NewAccountFromPrivateKey(key0)
facc0 := FakeSimpleAccount(key0.PublicKey())
facc1 := FakeSimpleAccount(key1.PublicKey())
act, err := NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: acc0.Contract.ScriptHash(),
Scopes: transaction.None,
},
Account: acc0,
}, {
Signer: transaction.Signer{
Account: facc1.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: facc1,
}}, acc0)
require.NoError(t, err)
script := []byte{byte(opcode.RET)}
rc.invRes = &result.Invoke{
State: "HALT",
GasConsumed: 3,
Script: script,
Stack: []stackitem.Item{stackitem.Make(42)},
}
mainTx, err := act.MakeRun(script)
require.NoError(t, err)
// No attributes.
fbTx, err := act.FbActor.MakeUnsignedRun(script, nil)
require.NoError(t, err)
fbTx.Attributes = nil
_, _, _, err = act.SendRequest(mainTx, fbTx)
require.Error(t, err)
// Bad NVB.
fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
require.NoError(t, err)
fbTx.Attributes[1].Type = transaction.HighPriority
fbTx.Attributes[1].Value = nil
_, _, _, err = act.SendRequest(mainTx, fbTx)
require.Error(t, err)
// Bad Conflicts.
fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
require.NoError(t, err)
fbTx.Attributes[2].Type = transaction.HighPriority
fbTx.Attributes[2].Value = nil
_, _, _, err = act.SendRequest(mainTx, fbTx)
require.Error(t, err)
// GetBlockCount error.
fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
require.NoError(t, err)
rc.err = errors.New("")
_, _, _, err = act.SendRequest(mainTx, fbTx)
require.Error(t, err)
// Can't sign suddenly.
rc.err = nil
acc0Backup := &wallet.Account{}
*acc0Backup = *acc0
*acc0 = *facc0
fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
require.NoError(t, err)
_, _, _, err = act.SendRequest(mainTx, fbTx)
require.Error(t, err)
// Good.
*acc0 = *acc0Backup
fbTx, err = act.FbActor.MakeUnsignedRun(script, nil)
require.NoError(t, err)
_, _, _, err = act.SendRequest(mainTx, fbTx)
require.Error(t, err)
}
func TestNotarize(t *testing.T) {
rc := &RPCClient{
version: &result.Version{
Protocol: result.Protocol{
Network: netmode.UnitTestNet,
MillisecondsPerBlock: 1000,
ValidatorsCount: 7,
},
},
bCount: 42,
}
key0, err := keys.NewPrivateKey()
require.NoError(t, err)
key1, err := keys.NewPrivateKey()
require.NoError(t, err)
acc0 := wallet.NewAccountFromPrivateKey(key0)
facc1 := FakeSimpleAccount(key1.PublicKey())
act, err := NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: acc0.Contract.ScriptHash(),
Scopes: transaction.None,
},
Account: acc0,
}, {
Signer: transaction.Signer{
Account: facc1.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: facc1,
}}, acc0)
require.NoError(t, err)
script := []byte{byte(opcode.RET)}
// Immediate error from MakeRun.
rc.invRes = &result.Invoke{
State: "FAULT",
GasConsumed: 3,
Script: script,
Stack: []stackitem.Item{stackitem.Make(42)},
}
_, _, _, err = act.Notarize(act.MakeRun(script))
require.Error(t, err)
// Explicitly good transaction. but failure to create a fallback.
rc.invRes.State = "HALT"
tx, err := act.MakeRun(script)
require.NoError(t, err)
rc.invRes.State = "FAULT"
_, _, _, err = act.Notarize(tx, nil)
require.Error(t, err)
// FB hash mismatch from SendRequestExactly.
rc.invRes.State = "HALT"
_, _, _, err = act.Notarize(act.MakeRun(script))
require.Error(t, err)
// Good.
rc.mirror = true
mHash, fbHash, vub, err := act.Notarize(act.MakeRun(script))
require.NoError(t, err)
require.NotEqual(t, util.Uint256{}, mHash)
require.NotEqual(t, util.Uint256{}, fbHash)
require.Equal(t, uint32(92), vub)
}
func TestDefaultActorOptions(t *testing.T) {
rc := &RPCClient{
version: &result.Version{
Protocol: result.Protocol{
Network: netmode.UnitTestNet,
MillisecondsPerBlock: 1000,
ValidatorsCount: 7,
},
},
}
acc, err := wallet.NewAccount()
require.NoError(t, err)
opts := NewDefaultActorOptions(NewReader(invoker.New(rc, nil)), acc)
rc.invRes = &result.Invoke{
State: "HALT",
GasConsumed: 3,
Script: opts.FbScript,
Stack: []stackitem.Item{stackitem.Make(42)},
}
tx := transaction.New(opts.FbScript, 1)
require.Error(t, opts.MainCheckerModifier(&result.Invoke{State: "FAULT"}, tx))
rc.invRes.State = "FAULT"
require.Error(t, opts.MainCheckerModifier(&result.Invoke{State: "HALT"}, tx))
rc.invRes.State = "HALT"
require.NoError(t, opts.MainCheckerModifier(&result.Invoke{State: "HALT"}, tx))
require.Equal(t, uint32(42), tx.ValidUntilBlock)
}
func TestWait(t *testing.T) {
rc := &RPCClient{version: &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}}
key0, err := keys.NewPrivateKey()
require.NoError(t, err)
key1, err := keys.NewPrivateKey()
require.NoError(t, err)
acc0 := wallet.NewAccountFromPrivateKey(key0)
facc1 := FakeSimpleAccount(key1.PublicKey())
act, err := NewActor(rc, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: acc0.Contract.ScriptHash(),
Scopes: transaction.None,
},
Account: acc0,
}, {
Signer: transaction.Signer{
Account: facc1.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: facc1,
}}, acc0)
require.NoError(t, err)
someErr := errors.New("someErr")
_, err = act.Wait(util.Uint256{}, util.Uint256{}, 0, someErr)
require.ErrorIs(t, err, someErr)
_, err = act.WaitSuccess(util.Uint256{}, util.Uint256{}, 0, someErr)
require.ErrorIs(t, err, someErr)
cont := util.Uint256{1, 2, 3}
ex := state.Execution{
Trigger: trigger.Application,
VMState: vmstate.Halt,
GasConsumed: 123,
Stack: []stackitem.Item{stackitem.Null{}},
}
applog := &result.ApplicationLog{
Container: cont,
IsTransaction: true,
Executions: []state.Execution{ex},
}
rc.applog = applog
res, err := act.Wait(util.Uint256{}, util.Uint256{}, 0, nil)
require.NoError(t, err)
require.Equal(t, &state.AppExecResult{
Container: cont,
Execution: ex,
}, res)
// Not successful since result has a different hash.
_, err = act.WaitSuccess(util.Uint256{}, util.Uint256{}, 0, nil)
require.ErrorIs(t, err, ErrFallbackAccepted)
_, err = act.WaitSuccess(util.Uint256{}, util.Uint256{1, 2, 3}, 0, nil)
require.ErrorIs(t, err, ErrFallbackAccepted)
rc.applog.Executions[0].VMState = vmstate.Fault
_, err = act.WaitSuccess(util.Uint256{1, 2, 3}, util.Uint256{}, 0, nil)
require.ErrorIs(t, err, actor.ErrExecFailed)
rc.applog.Executions[0].VMState = vmstate.Halt
res, err = act.WaitSuccess(util.Uint256{1, 2, 3}, util.Uint256{}, 0, nil)
require.NoError(t, err)
require.Equal(t, &state.AppExecResult{
Container: cont,
Execution: ex,
}, res)
}