Merge pull request #2229 from nspcc-dev/contract-test

Generic contract testing infrastructure
This commit is contained in:
Roman Khimov 2021-11-11 11:46:55 +03:00 committed by GitHub
commit 0a7f8afcea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1222 additions and 502 deletions

5
.gitignore vendored
View file

@ -28,9 +28,8 @@ bin/
*~
TAGS
# leveldb
chains/
chain/
# storage
/chains
# patch
*.orig

View file

@ -0,0 +1,429 @@
package tests
import (
"strings"
"testing"
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
func newNSClient(t *testing.T) *neotest.ContractInvoker {
bc, acc := chain.NewSingle(t)
e := neotest.NewExecutor(t, bc, acc, acc)
c := neotest.CompileFile(t, e.CommitteeHash, "..", "../nns.yml")
e.DeployContract(t, c, nil)
h, err := e.Chain.GetContractScriptHash(1)
require.NoError(t, err)
require.Equal(t, c.Hash, h)
return e.CommitteeInvoker(h)
}
func TestNameService_Price(t *testing.T) {
const (
minPrice = int64(0)
maxPrice = int64(10000_00000000)
)
c := newNSClient(t)
t.Run("set, not signed by committee", func(t *testing.T) {
acc := c.NewAccount(t)
cAcc := c.WithSigners(acc)
cAcc.InvokeFail(t, "not witnessed by committee", "setPrice", minPrice+1)
})
t.Run("get, default value", func(t *testing.T) {
c.Invoke(t, defaultNameServiceDomainPrice, "getPrice")
})
t.Run("set, too small value", func(t *testing.T) {
c.InvokeFail(t, "The price is out of range.", "setPrice", minPrice-1)
})
t.Run("set, too large value", func(t *testing.T) {
c.InvokeFail(t, "The price is out of range.", "setPrice", maxPrice+1)
})
t.Run("set, success", func(t *testing.T) {
txSet := c.PrepareInvoke(t, "setPrice", int64(defaultNameServiceDomainPrice+1))
txGet := c.PrepareInvoke(t, "getPrice")
c.AddBlockCheckHalt(t, txSet, txGet)
c.CheckHalt(t, txSet.Hash(), stackitem.Null{})
c.CheckHalt(t, txGet.Hash(), stackitem.Make(defaultNameServiceDomainPrice+1))
// Get in the next block.
c.Invoke(t, stackitem.Make(defaultNameServiceDomainPrice+1), "getPrice")
})
}
func TestNonfungible(t *testing.T) {
c := newNSClient(t)
c.Signers = []neotest.Signer{c.NewAccount(t)}
c.Invoke(t, "NNS", "symbol")
c.Invoke(t, 0, "decimals")
c.Invoke(t, 0, "totalSupply")
}
func TestAddRoot(t *testing.T) {
c := newNSClient(t)
t.Run("invalid format", func(t *testing.T) {
c.InvokeFail(t, "invalid root format", "addRoot", "")
})
t.Run("not signed by committee", func(t *testing.T) {
acc := c.NewAccount(t)
c := c.WithSigners(acc)
c.InvokeFail(t, "not witnessed by committee", "addRoot", "some")
})
c.Invoke(t, stackitem.Null{}, "addRoot", "some")
t.Run("already exists", func(t *testing.T) {
c.InvokeFail(t, "already exists", "addRoot", "some")
})
}
func TestExpiration(t *testing.T) {
c := newNSClient(t)
e := c.Executor
bc := e.Chain
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
c.Invoke(t, stackitem.Null{}, "addRoot", "com")
cAcc.Invoke(t, true, "register", "first.com", acc.ScriptHash())
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "first.com", int64(nns.TXT), "sometext")
b1 := e.TopBlock(t)
tx := cAcc.PrepareInvoke(t, "register", "second.com", acc.ScriptHash())
b2 := e.NewUnsignedBlock(t, tx)
b2.Index = b1.Index + 1
b2.PrevHash = b1.Hash()
b2.Timestamp = b1.Timestamp + 10000
require.NoError(t, bc.AddBlock(e.SignBlock(b2)))
e.CheckHalt(t, tx.Hash())
tx = cAcc.PrepareInvoke(t, "isAvailable", "first.com")
b3 := e.NewUnsignedBlock(t, tx)
b3.Index = b2.Index + 1
b3.PrevHash = b2.Hash()
b3.Timestamp = b1.Timestamp + (millisecondsInYear + 1)
require.NoError(t, bc.AddBlock(e.SignBlock(b3)))
e.CheckHalt(t, tx.Hash(), stackitem.NewBool(true))
tx = cAcc.PrepareInvoke(t, "isAvailable", "second.com")
b4 := e.NewUnsignedBlock(t, tx)
b4.Index = b3.Index + 1
b4.PrevHash = b3.Hash()
b4.Timestamp = b3.Timestamp + 1000
require.NoError(t, bc.AddBlock(e.SignBlock(b4)))
e.CheckHalt(t, tx.Hash(), stackitem.NewBool(false))
tx = cAcc.PrepareInvoke(t, "getRecord", "first.com", int64(nns.TXT))
b5 := e.NewUnsignedBlock(t, tx)
b5.Index = b4.Index + 1
b5.PrevHash = b4.Hash()
b5.Timestamp = b4.Timestamp + 1000
require.NoError(t, bc.AddBlock(e.SignBlock(b5)))
e.CheckFault(t, tx.Hash(), "name has expired")
}
const millisecondsInYear = 365 * 24 * 3600 * 1000
func TestRegisterAndRenew(t *testing.T) {
c := newNSClient(t)
e := c.Executor
c.InvokeFail(t, "root not found", "isAvailable", "neo.com")
c.Invoke(t, stackitem.Null{}, "addRoot", "org")
c.InvokeFail(t, "root not found", "isAvailable", "neo.com")
c.Invoke(t, stackitem.Null{}, "addRoot", "com")
c.Invoke(t, true, "isAvailable", "neo.com")
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash)
c.InvokeFail(t, "invalid domain name format", "register", "docs.neo.org", e.CommitteeHash)
c.InvokeFail(t, "invalid domain name format", "register", "\nneo.com'", e.CommitteeHash)
c.InvokeFail(t, "invalid domain name format", "register", "neo.com\n", e.CommitteeHash)
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceSysfee, "register", "neo.org", e.CommitteeHash)
c.InvokeWithFeeFail(t, "GAS limit exceeded", defaultNameServiceDomainPrice, "register", "neo.com", e.CommitteeHash)
c.Invoke(t, true, "isAvailable", "neo.com")
c.Invoke(t, 0, "balanceOf", e.CommitteeHash)
c.Invoke(t, true, "register", "neo.com", e.CommitteeHash)
topBlock := e.TopBlock(t)
expectedExpiration := topBlock.Timestamp + millisecondsInYear
c.Invoke(t, false, "register", "neo.com", e.CommitteeHash)
c.Invoke(t, false, "isAvailable", "neo.com")
props := stackitem.NewMap()
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
c.Invoke(t, props, "properties", "neo.com")
c.Invoke(t, 1, "balanceOf", e.CommitteeHash)
c.Invoke(t, e.CommitteeHash.BytesBE(), "ownerOf", []byte("neo.com"))
t.Run("invalid token ID", func(t *testing.T) {
c.InvokeFail(t, "token not found", "properties", "not.exists")
c.InvokeFail(t, "token not found", "ownerOf", "not.exists")
c.InvokeFail(t, "invalid conversion", "properties", []interface{}{})
c.InvokeFail(t, "invalid conversion", "ownerOf", []interface{}{})
})
// Renew
expectedExpiration += millisecondsInYear
c.Invoke(t, expectedExpiration, "renew", "neo.com")
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
c.Invoke(t, props, "properties", "neo.com")
}
func TestSetGetRecord(t *testing.T) {
c := newNSClient(t)
e := c.Executor
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
c.Invoke(t, stackitem.Null{}, "addRoot", "com")
t.Run("set before register", func(t *testing.T) {
c.InvokeFail(t, "token not found", "setRecord", "neo.com", int64(nns.TXT), "sometext")
})
c.Invoke(t, true, "register", "neo.com", e.CommitteeHash)
t.Run("invalid parameters", func(t *testing.T) {
c.InvokeFail(t, "unsupported record type", "setRecord", "neo.com", int64(0xFF), "1.2.3.4")
c.InvokeFail(t, "invalid record", "setRecord", "neo.com", int64(nns.A), "not.an.ip.address")
})
t.Run("invalid witness", func(t *testing.T) {
cAcc.InvokeFail(t, "not witnessed by admin", "setRecord", "neo.com", int64(nns.A), "1.2.3.4")
})
c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4")
c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4")
c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A))
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df")
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "nspcc.ru")
c.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext")
// Delete record.
t.Run("invalid witness", func(t *testing.T) {
cAcc.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.CNAME))
})
c.Invoke(t, "nspcc.ru", "getRecord", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.CNAME))
c.Invoke(t, stackitem.Null{}, "getRecord", "neo.com", int64(nns.CNAME))
c.Invoke(t, "1.2.3.4", "getRecord", "neo.com", int64(nns.A))
t.Run("SetRecord_compatibility", func(t *testing.T) {
// tests are got from the NNS C# implementation and changed accordingly to non-native implementation behaviour
testCases := []struct {
Type nns.RecordType
Name string
ShouldFail bool
}{
{Type: nns.A, Name: "0.0.0.0", ShouldFail: true},
{Type: nns.A, Name: "1.1.0.1"},
{Type: nns.A, Name: "10.10.10.10", ShouldFail: true},
{Type: nns.A, Name: "255.255.255.255", ShouldFail: true},
{Type: nns.A, Name: "192.168.1.1", ShouldFail: true},
{Type: nns.A, Name: "1a", ShouldFail: true},
{Type: nns.A, Name: "256.0.0.0", ShouldFail: true},
{Type: nns.A, Name: "01.01.01.01", ShouldFail: true},
{Type: nns.A, Name: "00.0.0.0", ShouldFail: true},
{Type: nns.A, Name: "0.0.0.-1", ShouldFail: true},
{Type: nns.A, Name: "0.0.0.0.1", ShouldFail: true},
{Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
{Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
{Type: nns.A, Name: "ff.ff.ff.ff", ShouldFail: true},
{Type: nns.A, Name: "0.0.256", ShouldFail: true},
{Type: nns.A, Name: "0.0.0", ShouldFail: true},
{Type: nns.A, Name: "0.257", ShouldFail: true},
{Type: nns.A, Name: "1.1", ShouldFail: true},
{Type: nns.A, Name: "257", ShouldFail: true},
{Type: nns.A, Name: "1", ShouldFail: true},
// {2000} & {2001} & ]2002, 3ffe[ & {3fff} are valid values for IPv6 fragment0
{Type: nns.AAAA, Name: "2002:db8::8:800:200c:417a", ShouldFail: true},
{Type: nns.AAAA, Name: "3ffd:1b8::8:800:200c:417a"},
{Type: nns.AAAA, Name: "3ffd::101"},
{Type: nns.AAAA, Name: "2003::1"},
{Type: nns.AAAA, Name: "2003::"},
{Type: nns.AAAA, Name: "2002:db8:0:0:8:800:200c:417a", ShouldFail: true},
{Type: nns.AAAA, Name: "3ffd:db8:0:0:8:800:200c:417a"},
{Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:101", ShouldFail: true},
{Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:1"},
{Type: nns.AAAA, Name: "0:0:0:0:0:0:0:1", ShouldFail: true},
{Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:1", ShouldFail: true},
{Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:0"},
{Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:0", ShouldFail: true},
{Type: nns.AAAA, Name: "2002:DB8::8:800:200C:417A", ShouldFail: true},
{Type: nns.AAAA, Name: "3FFD:1B8::8:800:200C:417A"},
{Type: nns.AAAA, Name: "3FFD::101"},
{Type: nns.AAAA, Name: "3fFD::101"},
{Type: nns.AAAA, Name: "2002:DB8:0:0:8:800:200C:417A", ShouldFail: true},
{Type: nns.AAAA, Name: "3FFD:DB8:0:0:8:800:200C:417A"},
{Type: nns.AAAA, Name: "3FFD:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "3FFD::ffff:1.01.1.01", ShouldFail: true},
{Type: nns.AAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true},
{Type: nns.AAAA, Name: "2001::13.1.68.3", ShouldFail: true},
}
for _, testCase := range testCases {
args := []interface{}{"neo.com", int64(testCase.Type), testCase.Name}
t.Run(testCase.Name, func(t *testing.T) {
if testCase.ShouldFail {
c.InvokeFail(t, "", "setRecord", args...)
} else {
c.Invoke(t, stackitem.Null{}, "setRecord", args...)
}
})
}
})
}
func TestSetAdmin(t *testing.T) {
c := newNSClient(t)
e := c.Executor
owner := e.NewAccount(t)
cOwner := c.WithSigners(owner)
admin := e.NewAccount(t)
cAdmin := c.WithSigners(admin)
guest := e.NewAccount(t)
cGuest := c.WithSigners(guest)
c.Invoke(t, stackitem.Null{}, "addRoot", "com")
cOwner.Invoke(t, true, "register", "neo.com", owner.ScriptHash())
cGuest.InvokeFail(t, "not witnessed", "setAdmin", "neo.com", admin.ScriptHash())
// Must be witnessed by both owner and admin.
cOwner.InvokeFail(t, "not witnessed by admin", "setAdmin", "neo.com", admin.ScriptHash())
cAdmin.InvokeFail(t, "not witnessed by owner", "setAdmin", "neo.com", admin.ScriptHash())
cc := c.WithSigners(owner, admin)
cc.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", admin.ScriptHash())
t.Run("set and delete by admin", func(t *testing.T) {
cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext")
cGuest.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT))
cAdmin.Invoke(t, stackitem.Null{}, "deleteRecord", "neo.com", int64(nns.TXT))
})
t.Run("set admin to null", func(t *testing.T) {
cAdmin.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.TXT), "sometext")
cOwner.Invoke(t, stackitem.Null{}, "setAdmin", "neo.com", nil)
cAdmin.InvokeFail(t, "not witnessed by admin", "deleteRecord", "neo.com", int64(nns.TXT))
})
}
func TestTransfer(t *testing.T) {
c := newNSClient(t)
e := c.Executor
from := e.NewAccount(t)
cFrom := c.WithSigners(from)
to := e.NewAccount(t)
cTo := c.WithSigners(to)
c.Invoke(t, stackitem.Null{}, "addRoot", "com")
cFrom.Invoke(t, true, "register", "neo.com", from.ScriptHash())
cFrom.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4")
cFrom.InvokeFail(t, "token not found", "transfer", to.ScriptHash(), "not.exists", nil)
c.Invoke(t, false, "transfer", to.ScriptHash(), "neo.com", nil)
cFrom.Invoke(t, true, "transfer", to.ScriptHash(), "neo.com", nil)
cFrom.Invoke(t, 1, "totalSupply")
cFrom.Invoke(t, to.ScriptHash().BytesBE(), "ownerOf", "neo.com")
// without onNEP11Transfer
ctr := neotest.CompileSource(t, e.CommitteeHash,
strings.NewReader(`package foo
func Main() int { return 0 }`),
&compiler.Options{Name: "foo"})
e.DeployContract(t, ctr, nil)
cTo.InvokeFail(t, "method not found", "transfer", ctr.Hash, []byte("neo.com"), nil)
// with onNEP11Transfer
ctr = neotest.CompileSource(t, e.CommitteeHash,
strings.NewReader(`package foo
import "github.com/nspcc-dev/neo-go/pkg/interop"
func OnNEP11Payment(from interop.Hash160, amount int, token []byte, data interface{}) {}`),
&compiler.Options{Name: "foo"})
e.DeployContract(t, ctr, nil)
cTo.Invoke(t, true, "transfer", ctr.Hash, []byte("neo.com"), nil)
cFrom.Invoke(t, 1, "totalSupply")
cFrom.Invoke(t, ctr.Hash.BytesBE(), "ownerOf", []byte("neo.com"))
}
func TestTokensOf(t *testing.T) {
c := newNSClient(t)
e := c.Executor
acc1 := e.NewAccount(t)
cAcc1 := c.WithSigners(acc1)
acc2 := e.NewAccount(t)
cAcc2 := c.WithSigners(acc2)
c.Invoke(t, stackitem.Null{}, "addRoot", "com")
cAcc1.Invoke(t, true, "register", "neo.com", acc1.ScriptHash())
cAcc2.Invoke(t, true, "register", "nspcc.com", acc2.ScriptHash())
testTokensOf(t, c, [][]byte{[]byte("neo.com")}, acc1.ScriptHash().BytesBE())
testTokensOf(t, c, [][]byte{[]byte("nspcc.com")}, acc2.ScriptHash().BytesBE())
testTokensOf(t, c, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
testTokensOf(t, c, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still
}
func testTokensOf(t *testing.T, c *neotest.ContractInvoker, result [][]byte, args ...interface{}) {
method := "tokensOf"
if len(args) == 0 {
method = "tokens"
}
s, err := c.TestInvoke(t, method, args...)
if result == nil {
require.Error(t, err)
return
}
require.NoError(t, err)
iter := s.Pop().Interop().Value().(*storage.Iterator)
arr := make([]stackitem.Item, 0, len(result))
for i := range result {
require.True(t, iter.Next())
require.Equal(t, result[i], iter.Value().Value())
arr = append(arr, stackitem.Make(result[i]))
}
require.False(t, iter.Next())
}
func TestResolve(t *testing.T) {
c := newNSClient(t)
e := c.Executor
acc := e.NewAccount(t)
cAcc := c.WithSigners(acc)
c.Invoke(t, stackitem.Null{}, "addRoot", "com")
cAcc.Invoke(t, true, "register", "neo.com", acc.ScriptHash())
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.A), "1.2.3.4")
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "neo.com", int64(nns.CNAME), "alias.com")
cAcc.Invoke(t, true, "register", "alias.com", acc.ScriptHash())
cAcc.Invoke(t, stackitem.Null{}, "setRecord", "alias.com", int64(nns.TXT), "sometxt")
c.Invoke(t, "1.2.3.4", "resolve", "neo.com", int64(nns.A))
c.Invoke(t, "alias.com", "resolve", "neo.com", int64(nns.CNAME))
c.Invoke(t, "sometxt", "resolve", "neo.com", int64(nns.TXT))
c.Invoke(t, stackitem.Null{}, "resolve", "neo.com", int64(nns.AAAA))
}
const (
defaultNameServiceDomainPrice = 10_0000_0000
defaultNameServiceSysfee = 6000_0000
)

View file

@ -52,28 +52,6 @@ var neoOwner = testchain.MultisigScriptHash()
// examplesPrefix is a prefix of the example smart-contracts.
const examplesPrefix = "../../examples/"
// newTestChainWithNS should be called before newBlock invocation to properly setup
// global state.
func newTestChainWithNS(t *testing.T) (*Blockchain, util.Uint160) {
bc := newTestChainWithCustomCfg(t, nil)
acc := newAccountWithGAS(t, bc)
// Push NameService contract into the chain.
nsPath := examplesPrefix + "nft-nd-nns/"
nsConfigPath := nsPath + "nns.yml"
txDeploy4, _ := newDeployTx(t, bc, acc.PrivateKey().GetScriptHash(), nsPath, nsPath, &nsConfigPath)
txDeploy4.Nonce = 123
txDeploy4.ValidUntilBlock = bc.BlockHeight() + 1
require.NoError(t, addNetworkFee(bc, txDeploy4, acc))
require.NoError(t, acc.SignTx(testchain.Network(), txDeploy4))
b := bc.newBlock(txDeploy4)
require.NoError(t, bc.AddBlock(b))
checkTxHalt(t, bc, txDeploy4.Hash())
h, err := bc.GetContractScriptHash(1)
require.NoError(t, err)
return bc, h
}
// newTestChain should be called before newBlock invocation to properly setup
// global state.
func newTestChain(t testing.TB) *Blockchain {
@ -488,15 +466,15 @@ func initBasicChain(t *testing.T, bc *Blockchain) {
// register `neo.com` with A record type and priv0 owner via NS
transferFundsToCommittee(t, bc) // block #12
res, err := invokeContractMethodGeneric(bc, defaultNameServiceSysfee,
res, err := invokeContractMethodGeneric(bc, -1,
nsHash, "addRoot", true, "com") // block #13
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
res, err = invokeContractMethodGeneric(bc, defaultNameServiceDomainPrice+defaultNameServiceSysfee+1_0000_000,
res, err = invokeContractMethodGeneric(bc, -1,
nsHash, "register", acc0, "neo.com", priv0ScriptHash) // block #14
require.NoError(t, err)
checkResult(t, res, stackitem.NewBool(true))
res, err = invokeContractMethodGeneric(bc, defaultNameServiceSysfee, nsHash,
res, err = invokeContractMethodGeneric(bc, -1, nsHash,
"setRecord", acc0, "neo.com", int64(nns.A), "1.2.3.4") // block #15
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
@ -596,22 +574,24 @@ func prepareContractMethodInvokeGeneric(chain *Blockchain, sysfee int64,
return nil, w.Err
}
script := w.Bytes()
tx := transaction.New(script, sysfee)
tx := transaction.New(script, 0)
tx.ValidUntilBlock = chain.blockHeight + 1
var err error
switch s := signer.(type) {
case bool:
if s {
addSigners(testchain.CommitteeScriptHash(), tx)
setTxSystemFee(chain, sysfee, tx)
err = testchain.SignTxCommittee(chain, tx)
} else {
addSigners(neoOwner, tx)
setTxSystemFee(chain, sysfee, tx)
err = testchain.SignTx(chain, tx)
}
case *wallet.Account:
signTxWithAccounts(chain, tx, s)
signTxWithAccounts(chain, sysfee, tx, s)
case []*wallet.Account:
signTxWithAccounts(chain, tx, s...)
signTxWithAccounts(chain, sysfee, tx, s...)
default:
panic("invalid signer")
}
@ -621,7 +601,31 @@ func prepareContractMethodInvokeGeneric(chain *Blockchain, sysfee int64,
return tx, nil
}
func signTxWithAccounts(chain *Blockchain, tx *transaction.Transaction, accs ...*wallet.Account) {
func setTxSystemFee(bc *Blockchain, sysFee int64, tx *transaction.Transaction) {
if sysFee >= 0 {
tx.SystemFee = sysFee
return
}
lastBlock := bc.topBlock.Load().(*block.Block)
b := &block.Block{
Header: block.Header{
Index: lastBlock.Index + 1,
Timestamp: lastBlock.Timestamp + 1000,
},
Transactions: []*transaction.Transaction{tx},
}
ttx := *tx // prevent setting 'hash' field
v, f := bc.GetTestVM(trigger.Application, &ttx, b)
defer f()
v.LoadWithFlags(tx.Script, callflag.All)
_ = v.Run()
tx.SystemFee = v.GasConsumed()
}
func signTxWithAccounts(chain *Blockchain, sysFee int64, tx *transaction.Transaction, accs ...*wallet.Account) {
scope := transaction.CalledByEntry
for _, acc := range accs {
accH, _ := address.StringToUint160(acc.Address)
@ -631,6 +635,7 @@ func signTxWithAccounts(chain *Blockchain, tx *transaction.Transaction, accs ...
})
scope = transaction.Global
}
setTxSystemFee(chain, sysFee, tx)
size := io.GetVarSize(tx)
for _, acc := range accs {
if acc.Contract.Deployed {

View file

@ -469,3 +469,10 @@ func TestNEO_TransferZeroWithNonZeroBalance(t *testing.T) {
check(t, false)
})
}
func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account {
acc, err := wallet.NewAccount()
require.NoError(t, err)
transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000)
return acc
}

View file

@ -1,470 +0,0 @@
package core
import (
"testing"
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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/emit"
"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/wallet"
"github.com/stretchr/testify/require"
)
func TestNameService_Price(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
testGetSet(t, bc, nsHash, "Price",
defaultNameServiceDomainPrice, 0, 10000_00000000)
}
func TestNonfungible(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
acc := newAccountWithGAS(t, bc)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "symbol", "NNS")
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "decimals", 0)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "totalSupply", 0)
}
func TestAddRoot(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
t.Run("invalid format", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "addRoot", nil, "")
})
t.Run("not signed by committee", func(t *testing.T) {
aer, err := invokeContractMethod(bc, 1000_0000, nsHash, "addRoot", "some")
require.NoError(t, err)
checkFAULTState(t, aer)
})
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "some")
t.Run("already exists", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "addRoot", nil, "some")
})
}
func TestExpiration(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
true, "first.com", acc.Contract.ScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc,
"setRecord", stackitem.Null{}, "first.com", int64(nns.TXT), "sometext")
b1 := bc.topBlock.Load().(*block.Block)
tx, err := prepareContractMethodInvokeGeneric(bc, defaultRegisterSysfee, nsHash,
"register", acc, "second.com", acc.Contract.ScriptHash())
require.NoError(t, err)
b2 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b1.Index + 1
b.PrevHash = b1.Hash()
b.Timestamp = b1.Timestamp + 10000
}, tx)
require.NoError(t, bc.AddBlock(b2))
checkTxHalt(t, bc, tx.Hash())
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
"isAvailable", acc, "first.com")
require.NoError(t, err)
b3 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b2.Index + 1
b.PrevHash = b2.Hash()
b.Timestamp = b1.Timestamp + (millisecondsInYear + 1)
}, tx)
require.NoError(t, bc.AddBlock(b3))
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.NewBool(true))
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
"isAvailable", acc, "second.com")
require.NoError(t, err)
b4 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b3.Index + 1
b.PrevHash = b3.Hash()
b.Timestamp = b3.Timestamp + 1000
}, tx)
require.NoError(t, bc.AddBlock(b4))
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkResult(t, &aer[0], stackitem.NewBool(false))
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
"getRecord", acc, "first.com", int64(nns.TXT))
require.NoError(t, err)
b5 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
b.Index = b4.Index + 1
b.PrevHash = b4.Hash()
b.Timestamp = b4.Timestamp + 1000
}, tx)
require.NoError(t, bc.AddBlock(b5))
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
checkFAULTState(t, &aer[0]) // name has expired (panic)
}
const millisecondsInYear = 365 * 24 * 3600 * 1000
func TestRegisterAndRenew(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
testNameServiceInvoke(t, bc, nsHash, "isAvailable", nil, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "org")
testNameServiceInvoke(t, bc, nsHash, "isAvailable", nil, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvoke(t, bc, nsHash, "isAvailable", true, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.org", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "register", nil, "docs.neo.org", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "register", nil, "\nneo.com'", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.com\n", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceDomainPrice, true, "register",
nil, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "isAvailable", true, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "balanceOf", 0, testchain.CommitteeScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
true, "neo.com", testchain.CommitteeScriptHash())
topBlock := bc.topBlock.Load().(*block.Block)
expectedExpiration := topBlock.Timestamp + millisecondsInYear
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
false, "neo.com", testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "isAvailable", false, "neo.com")
props := stackitem.NewMap()
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
testNameServiceInvoke(t, bc, nsHash, "properties", props, "neo.com")
testNameServiceInvoke(t, bc, nsHash, "balanceOf", 1, testchain.CommitteeScriptHash())
testNameServiceInvoke(t, bc, nsHash, "ownerOf", testchain.CommitteeScriptHash().BytesBE(), []byte("neo.com"))
t.Run("invalid token ID", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "properties", nil, "not.exists")
testNameServiceInvoke(t, bc, nsHash, "ownerOf", nil, "not.exists")
testNameServiceInvoke(t, bc, nsHash, "properties", nil, []interface{}{})
testNameServiceInvoke(t, bc, nsHash, "ownerOf", nil, []interface{}{})
})
// Renew
expectedExpiration += millisecondsInYear
testNameServiceInvokeAux(t, bc, nsHash, 100_0000_0000, true, "renew", expectedExpiration, "neo.com")
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
testNameServiceInvoke(t, bc, nsHash, "properties", props, "neo.com")
}
func TestSetGetRecord(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
t.Run("set before register", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(nns.TXT), "sometext")
})
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
true, "neo.com", testchain.CommitteeScriptHash())
t.Run("invalid parameters", func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4")
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(nns.A), "not.an.ip.address")
})
t.Run("invalid witness", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil,
"neo.com", int64(nns.A), "1.2.3.4")
})
testNameServiceInvoke(t, bc, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.A))
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4")
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A))
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4")
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A))
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.AAAA), "2001:0201:1f1f:0000:0000:0100:11a0:11df")
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME), "nspcc.ru")
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.TXT), "sometext")
// Delete record.
t.Run("invalid witness", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil,
"neo.com", int64(nns.CNAME))
})
testNameServiceInvoke(t, bc, nsHash, "getRecord", "nspcc.ru", "neo.com", int64(nns.CNAME))
testNameServiceInvoke(t, bc, nsHash, "deleteRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME))
testNameServiceInvoke(t, bc, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME))
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A))
t.Run("SetRecord_compatibility", func(t *testing.T) {
// tests are got from the NNS C# implementation and changed accordingly to non-native implementation behaviour
testCases := []struct {
Type nns.RecordType
Name string
ShouldFail bool
}{
{Type: nns.A, Name: "0.0.0.0", ShouldFail: true},
{Type: nns.A, Name: "1.1.0.1"},
{Type: nns.A, Name: "10.10.10.10", ShouldFail: true},
{Type: nns.A, Name: "255.255.255.255", ShouldFail: true},
{Type: nns.A, Name: "192.168.1.1", ShouldFail: true},
{Type: nns.A, Name: "1a", ShouldFail: true},
{Type: nns.A, Name: "256.0.0.0", ShouldFail: true},
{Type: nns.A, Name: "01.01.01.01", ShouldFail: true},
{Type: nns.A, Name: "00.0.0.0", ShouldFail: true},
{Type: nns.A, Name: "0.0.0.-1", ShouldFail: true},
{Type: nns.A, Name: "0.0.0.0.1", ShouldFail: true},
{Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
{Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
{Type: nns.A, Name: "ff.ff.ff.ff", ShouldFail: true},
{Type: nns.A, Name: "0.0.256", ShouldFail: true},
{Type: nns.A, Name: "0.0.0", ShouldFail: true},
{Type: nns.A, Name: "0.257", ShouldFail: true},
{Type: nns.A, Name: "1.1", ShouldFail: true},
{Type: nns.A, Name: "257", ShouldFail: true},
{Type: nns.A, Name: "1", ShouldFail: true},
// {2000} & {2001} & ]2002, 3ffe[ & {3fff} are valid values for IPv6 fragment0
{Type: nns.AAAA, Name: "2002:db8::8:800:200c:417a", ShouldFail: true},
{Type: nns.AAAA, Name: "3ffd:1b8::8:800:200c:417a"},
{Type: nns.AAAA, Name: "3ffd::101"},
{Type: nns.AAAA, Name: "2003::1"},
{Type: nns.AAAA, Name: "2003::"},
{Type: nns.AAAA, Name: "2002:db8:0:0:8:800:200c:417a", ShouldFail: true},
{Type: nns.AAAA, Name: "3ffd:db8:0:0:8:800:200c:417a"},
{Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:101", ShouldFail: true},
{Type: nns.AAAA, Name: "3ffd:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:1"},
{Type: nns.AAAA, Name: "0:0:0:0:0:0:0:1", ShouldFail: true},
{Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:1", ShouldFail: true},
{Type: nns.AAAA, Name: "2001:200:0:0:0:0:0:0"},
{Type: nns.AAAA, Name: "2002:0:0:0:0:0:0:0", ShouldFail: true},
{Type: nns.AAAA, Name: "2002:DB8::8:800:200C:417A", ShouldFail: true},
{Type: nns.AAAA, Name: "3FFD:1B8::8:800:200C:417A"},
{Type: nns.AAAA, Name: "3FFD::101"},
{Type: nns.AAAA, Name: "3fFD::101"},
{Type: nns.AAAA, Name: "2002:DB8:0:0:8:800:200C:417A", ShouldFail: true},
{Type: nns.AAAA, Name: "3FFD:DB8:0:0:8:800:200C:417A"},
{Type: nns.AAAA, Name: "3FFD:0:0:0:0:0:0:101"},
{Type: nns.AAAA, Name: "3FFD::ffff:1.01.1.01", ShouldFail: true},
{Type: nns.AAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true},
{Type: nns.AAAA, Name: "2001::13.1.68.3", ShouldFail: true},
}
for _, testCase := range testCases {
var expected interface{}
if testCase.ShouldFail {
expected = nil
} else {
expected = stackitem.Null{}
}
t.Run(testCase.Name, func(t *testing.T) {
testNameServiceInvoke(t, bc, nsHash, "setRecord", expected, "neo.com", int64(testCase.Type), testCase.Name)
})
}
})
}
func TestSetAdmin(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
owner := newAccountWithGAS(t, bc)
admin := newAccountWithGAS(t, bc)
guest := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, owner, "register", true,
"neo.com", owner.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, guest, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
// Must be witnessed by both owner and admin.
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, owner, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setAdmin", nil,
"neo.com", admin.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, []*wallet.Account{owner, admin},
"setAdmin", stackitem.Null{},
"neo.com", admin.PrivateKey().GetScriptHash())
t.Run("set and delete by admin", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
"neo.com", int64(nns.TXT), "sometext")
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, guest, "deleteRecord", nil,
"neo.com", int64(nns.TXT))
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{},
"neo.com", int64(nns.TXT))
})
t.Run("set admin to null", func(t *testing.T) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
"neo.com", int64(nns.TXT), "sometext")
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{},
"neo.com", nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", nil,
"neo.com", int64(nns.TXT))
})
}
func TestTransfer(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
from := newAccountWithGAS(t, bc)
to := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "register",
true, "neo.com", from.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{},
"neo.com", int64(nns.A), "1.2.3.4")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "transfer",
nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "transfer",
false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "transfer",
true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "ownerOf",
to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
cs, cs2 := getTestContractState(bc) // cs2 doesn't have OnNEP11Transfer
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs2))
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, to, "transfer",
nil, cs2.Hash.BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, to, "transfer",
true, cs.Hash.BytesBE(), []byte("neo.com"), nil)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1)
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "ownerOf",
cs.Hash.BytesBE(), []byte("neo.com"))
}
func TestTokensOf(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
acc1 := newAccountWithGAS(t, bc)
acc2 := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc1, "register",
true, "neo.com", acc1.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc2, "register",
true, "nspcc.com", acc2.PrivateKey().GetScriptHash())
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE())
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE())
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
testTokensOf(t, bc, nsHash, acc1, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still
}
func testTokensOf(t *testing.T, bc *Blockchain, nsHash util.Uint160, signer *wallet.Account, result [][]byte, args ...interface{}) {
method := "tokensOf"
if len(args) == 0 {
method = "tokens"
}
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, nsHash, method, callflag.All, args...)
for range result {
emit.Opcodes(w.BinWriter, opcode.DUP)
emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
emit.Opcodes(w.BinWriter, opcode.DUP)
emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue)
emit.Opcodes(w.BinWriter, opcode.SWAP)
}
emit.Opcodes(w.BinWriter, opcode.DROP)
emit.Int(w.BinWriter, int64(len(result)))
emit.Opcodes(w.BinWriter, opcode.PACK)
require.NoError(t, w.Err)
script := w.Bytes()
tx := transaction.New(script, defaultNameServiceSysfee)
tx.ValidUntilBlock = bc.BlockHeight() + 1
signTxWithAccounts(bc, tx, signer)
aers, err := persistBlock(bc, tx)
require.NoError(t, err)
if result == nil {
checkFAULTState(t, aers[0])
return
}
arr := make([]stackitem.Item, 0, len(result))
for i := len(result) - 1; i >= 0; i-- {
arr = append(arr, stackitem.Make(result[i]))
}
checkResult(t, aers[0], stackitem.NewArray(arr))
}
func TestResolve(t *testing.T) {
bc, nsHash := newTestChainWithNS(t)
transferFundsToCommittee(t, bc)
acc := newAccountWithGAS(t, bc)
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
true, "neo.com", acc.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"neo.com", int64(nns.A), "1.2.3.4")
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"neo.com", int64(nns.CNAME), "alias.com")
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
true, "alias.com", acc.PrivateKey().GetScriptHash())
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
"alias.com", int64(nns.TXT), "sometxt")
testNameServiceInvoke(t, bc, nsHash, "resolve", "1.2.3.4",
"neo.com", int64(nns.A))
testNameServiceInvoke(t, bc, nsHash, "resolve", "alias.com",
"neo.com", int64(nns.CNAME))
testNameServiceInvoke(t, bc, nsHash, "resolve", "sometxt",
"neo.com", int64(nns.TXT))
testNameServiceInvoke(t, bc, nsHash, "resolve", stackitem.Null{},
"neo.com", int64(nns.AAAA))
}
const (
defaultNameServiceDomainPrice = 10_0000_0000
defaultNameServiceSysfee = 6000_0000
defaultRegisterSysfee = 10_0000_0000 + defaultNameServiceDomainPrice
)
func testNameServiceInvoke(t *testing.T, bc *Blockchain, nsHash util.Uint160, method string, result interface{}, args ...interface{}) {
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, true, method, result, args...)
}
func testNameServiceInvokeAux(t *testing.T, bc *Blockchain, nsHash util.Uint160, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) {
if sysfee < 0 {
sysfee = defaultNameServiceSysfee
}
aer, err := invokeContractMethodGeneric(bc, sysfee, nsHash, method, signer, args...)
require.NoError(t, err)
if result == nil {
checkFAULTState(t, aer)
return
}
checkResult(t, aer, stackitem.Make(result))
}
func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account {
acc, err := wallet.NewAccount()
require.NoError(t, err)
transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000)
return acc
}

8
pkg/neotest/account.go Normal file
View file

@ -0,0 +1,8 @@
package neotest
var _nonce uint32
func nonce() uint32 {
_nonce++
return _nonce
}

260
pkg/neotest/basic.go Normal file
View file

@ -0,0 +1,260 @@
package neotest
import (
"encoding/json"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"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/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
// Executor is a wrapper over chain state.
type Executor struct {
Chain blockchainer.Blockchainer
Validator Signer
Committee Signer
CommitteeHash util.Uint160
Contracts map[string]*Contract
}
// NewExecutor creates new executor instance from provided blockchain and committee.
func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, validator, committee Signer) *Executor {
checkMultiSigner(t, validator)
checkMultiSigner(t, committee)
return &Executor{
Chain: bc,
Validator: validator,
Committee: committee,
CommitteeHash: committee.ScriptHash(),
Contracts: make(map[string]*Contract),
}
}
// TopBlock returns block with the highest index.
func (e *Executor) TopBlock(t *testing.T) *block.Block {
b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(int(e.Chain.BlockHeight())))
require.NoError(t, err)
return b
}
// NativeHash returns native contract hash by name.
func (e *Executor) NativeHash(t *testing.T, name string) util.Uint160 {
h, err := e.Chain.GetNativeContractScriptHash(name)
require.NoError(t, err)
return h
}
// NewUnsignedTx creates new unsigned transaction which invokes method of contract with hash.
func (e *Executor) NewUnsignedTx(t *testing.T, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction {
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, hash, method, callflag.All, args...)
require.NoError(t, w.Err)
script := w.Bytes()
tx := transaction.New(script, 0)
tx.Nonce = nonce()
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
return tx
}
// NewTx creates new transaction which invokes contract method.
// Transaction is signed with signer.
func (e *Executor) NewTx(t *testing.T, signers []Signer,
hash util.Uint160, method string, args ...interface{}) *transaction.Transaction {
tx := e.NewUnsignedTx(t, hash, method, args...)
return e.SignTx(t, tx, -1, signers...)
}
// SignTx signs a transaction using provided signers.
func (e *Executor) SignTx(t *testing.T, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction {
for _, acc := range signers {
tx.Signers = append(tx.Signers, transaction.Signer{
Account: acc.ScriptHash(),
Scopes: transaction.Global,
})
}
addNetworkFee(e.Chain, tx, signers...)
addSystemFee(e.Chain, tx, sysFee)
for _, acc := range signers {
require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx))
}
return tx
}
// NewAccount returns new signer holding 100.0 GAS. This method advances the chain
// by one block with a transfer transaction.
func (e *Executor) NewAccount(t *testing.T) Signer {
acc, err := wallet.NewAccount()
require.NoError(t, err)
tx := e.NewTx(t, []Signer{e.Committee},
e.NativeHash(t, nativenames.Gas), "transfer",
e.Committee.ScriptHash(), acc.Contract.ScriptHash(), int64(100_0000_0000), nil)
e.AddNewBlock(t, tx)
e.CheckHalt(t, tx.Hash())
return NewSingleSigner(acc)
}
// DeployContract compiles and deploys contract to bc.
// data is an optional argument to `_deploy`.
// Returns hash of the deploy transaction.
func (e *Executor) DeployContract(t *testing.T, c *Contract, data interface{}) util.Uint256 {
tx := e.NewDeployTx(t, e.Chain, c, data)
e.AddNewBlock(t, tx)
e.CheckHalt(t, tx.Hash())
return tx.Hash()
}
// CheckHalt checks that transaction persisted with HALT state.
func (e *Executor) CheckHalt(t *testing.T, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult {
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
require.NoError(t, err)
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException)
if len(stack) != 0 {
require.Equal(t, stack, aer[0].Stack)
}
return &aer[0]
}
// CheckFault checks that transaction persisted with FAULT state.
// Raised exception is also checked to contain s as a substring.
func (e *Executor) CheckFault(t *testing.T, h util.Uint256, s string) {
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
require.NoError(t, err)
require.Equal(t, vm.FaultState, aer[0].VMState)
require.True(t, strings.Contains(aer[0].FaultException, s),
"expected: %s, got: %s", s, aer[0].FaultException)
}
// NewDeployTx returns new deployment tx for contract signed by committee.
func (e *Executor) NewDeployTx(t *testing.T, bc blockchainer.Blockchainer, c *Contract, data interface{}) *transaction.Transaction {
rawManifest, err := json.Marshal(c.Manifest)
require.NoError(t, err)
neb, err := c.NEF.Bytes()
require.NoError(t, err)
buf := io.NewBufBinWriter()
emit.AppCall(buf.BinWriter, bc.ManagementContractHash(), "deploy", callflag.All, neb, rawManifest, data)
require.NoError(t, buf.Err)
tx := transaction.New(buf.Bytes(), 100*native.GASFactor)
tx.Nonce = nonce()
tx.ValidUntilBlock = bc.BlockHeight() + 1
tx.Signers = []transaction.Signer{{
Account: e.Committee.ScriptHash(),
Scopes: transaction.Global,
}}
addNetworkFee(bc, tx, e.Committee)
require.NoError(t, e.Committee.SignTx(netmode.UnitTestNet, tx))
return tx
}
func addSystemFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, sysFee int64) {
if sysFee >= 0 {
tx.SystemFee = sysFee
return
}
v, _ := TestInvoke(bc, tx) // ignore error to support failing transactions
tx.SystemFee = v.GasConsumed()
}
func addNetworkFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, signers ...Signer) {
baseFee := bc.GetPolicer().GetBaseExecFee()
size := io.GetVarSize(tx)
for _, sgr := range signers {
netFee, sizeDelta := fee.Calculate(baseFee, sgr.Script())
tx.NetworkFee += netFee
size += sizeDelta
}
tx.NetworkFee += int64(size) * bc.FeePerByte()
}
// NewUnsignedBlock creates new unsigned block from txs.
func (e *Executor) NewUnsignedBlock(t *testing.T, txs ...*transaction.Transaction) *block.Block {
lastBlock := e.TopBlock(t)
b := &block.Block{
Header: block.Header{
NextConsensus: e.Validator.ScriptHash(),
Script: transaction.Witness{
VerificationScript: e.Validator.Script(),
},
Timestamp: lastBlock.Timestamp + 1,
},
Transactions: txs,
}
if e.Chain.GetConfig().StateRootInHeader {
b.StateRootEnabled = true
b.PrevStateRoot = e.Chain.GetStateModule().CurrentLocalStateRoot()
}
b.PrevHash = lastBlock.Hash()
b.Index = e.Chain.BlockHeight() + 1
b.RebuildMerkleRoot()
return b
}
// AddNewBlock creates a new block from provided transactions and adds it on bc.
func (e *Executor) AddNewBlock(t *testing.T, txs ...*transaction.Transaction) *block.Block {
b := e.NewUnsignedBlock(t, txs...)
e.SignBlock(b)
require.NoError(t, e.Chain.AddBlock(b))
return b
}
// SignBlock add validators signature to b.
func (e *Executor) SignBlock(b *block.Block) *block.Block {
invoc := e.Validator.SignHashable(uint32(e.Chain.GetConfig().Magic), b)
b.Script.InvocationScript = invoc
return b
}
// AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt.
func (e *Executor) AddBlockCheckHalt(t *testing.T, txs ...*transaction.Transaction) *block.Block {
b := e.AddNewBlock(t, txs...)
for _, tx := range txs {
e.CheckHalt(t, tx.Hash())
}
return b
}
// TestInvoke creates a test VM with dummy block and executes transaction in it.
func TestInvoke(bc blockchainer.Blockchainer, tx *transaction.Transaction) (*vm.VM, error) {
lastBlock, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight())))
if err != nil {
return nil, err
}
b := &block.Block{
Header: block.Header{
Index: bc.BlockHeight() + 1,
Timestamp: lastBlock.Timestamp + 1,
},
}
// `GetTestVM` as well as `Run` can use transaction hash which will set cached value.
// This is unwanted behaviour so we explicitly copy transaction to perform execution.
ttx := *tx
v, f := bc.GetTestVM(trigger.Application, &ttx, b)
defer f()
v.LoadWithFlags(tx.Script, callflag.All)
err = v.Run()
return v, err
}

152
pkg/neotest/chain/chain.go Normal file
View file

@ -0,0 +1,152 @@
package chain
import (
"encoding/hex"
"sort"
"testing"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
const singleValidatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
// committeeWIFs is a list of unencrypted WIFs sorted by public key.
var committeeWIFs = []string{
"KzfPUYDC9n2yf4fK5ro4C8KMcdeXtFuEnStycbZgX3GomiUsvX6W",
"KzgWE3u3EDp13XPXXuTKZxeJ3Gi8Bsm8f9ijY3ZsCKKRvZUo1Cdn",
singleValidatorWIF,
"L2oEXKRAAMiPEZukwR5ho2S6SMeQLhcK9mF71ZnF7GvT8dU4Kkgz",
// Provide 2 committee extra members so that committee address differs from
// the validators one.
"L1Tr1iq5oz1jaFaMXP21sHDkJYDDkuLtpvQ4wRf1cjKvJYvnvpAb",
"Kz6XTUrExy78q8f4MjDHnwz8fYYyUE8iPXwPRAkHa3qN2JcHYm7e",
}
var (
// committeeAcc is an account used to sign tx as a committee.
committeeAcc *wallet.Account
// multiCommitteeAcc contains committee accounts used in a multi-node setup.
multiCommitteeAcc []*wallet.Account
// multiValidatorAcc contains validator accounts used in a multi-node setup.
multiValidatorAcc []*wallet.Account
// standByCommittee contains list of committee public keys to use in config.
standByCommittee []string
)
func init() {
committeeAcc, _ = wallet.NewAccountFromWIF(singleValidatorWIF)
pubs := keys.PublicKeys{committeeAcc.PrivateKey().PublicKey()}
err := committeeAcc.ConvertMultisig(1, pubs)
if err != nil {
panic(err)
}
mc := smartcontract.GetMajorityHonestNodeCount(len(committeeWIFs))
mv := smartcontract.GetDefaultHonestNodeCount(4)
accs := make([]*wallet.Account, len(committeeWIFs))
pubs = make(keys.PublicKeys, len(accs))
for i := range committeeWIFs {
accs[i], _ = wallet.NewAccountFromWIF(committeeWIFs[i])
pubs[i] = accs[i].PrivateKey().PublicKey()
}
// Config entry must contain validators first in a specific order.
standByCommittee = make([]string, len(pubs))
standByCommittee[0] = hex.EncodeToString(pubs[2].Bytes())
standByCommittee[1] = hex.EncodeToString(pubs[0].Bytes())
standByCommittee[2] = hex.EncodeToString(pubs[3].Bytes())
standByCommittee[3] = hex.EncodeToString(pubs[1].Bytes())
standByCommittee[4] = hex.EncodeToString(pubs[4].Bytes())
standByCommittee[5] = hex.EncodeToString(pubs[5].Bytes())
multiValidatorAcc = make([]*wallet.Account, mv)
sort.Sort(pubs[:4])
vloop:
for i := 0; i < mv; i++ {
for j := range accs {
if accs[j].PrivateKey().PublicKey().Equal(pubs[i]) {
multiValidatorAcc[i] = wallet.NewAccountFromPrivateKey(accs[j].PrivateKey())
err := multiValidatorAcc[i].ConvertMultisig(mv, pubs[:4])
if err != nil {
panic(err)
}
continue vloop
}
}
panic("invalid committee WIFs")
}
multiCommitteeAcc = make([]*wallet.Account, mc)
sort.Sort(pubs)
cloop:
for i := 0; i < mc; i++ {
for j := range accs {
if accs[j].PrivateKey().PublicKey().Equal(pubs[i]) {
multiCommitteeAcc[i] = wallet.NewAccountFromPrivateKey(accs[j].PrivateKey())
err := multiCommitteeAcc[i].ConvertMultisig(mc, pubs)
if err != nil {
panic(err)
}
continue cloop
}
}
panic("invalid committee WIFs")
}
}
// NewSingle creates new blockchain instance with a single validator and
// setups cleanup functions.
func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) {
protoCfg := config.ProtocolConfiguration{
Magic: netmode.UnitTestNet,
SecondsPerBlock: 1,
StandbyCommittee: []string{hex.EncodeToString(committeeAcc.PrivateKey().PublicKey().Bytes())},
ValidatorsCount: 1,
VerifyBlocks: true,
VerifyTransactions: true,
}
st := storage.NewMemoryStore()
log := zaptest.NewLogger(t)
bc, err := core.NewBlockchain(st, protoCfg, log)
require.NoError(t, err)
go bc.Run()
t.Cleanup(bc.Close)
return bc, neotest.NewMultiSigner(committeeAcc)
}
// NewMulti creates new blockchain instance with 4 validators and 6 committee members.
// Second return value is for validator signer, third -- for committee.
func NewMulti(t *testing.T) (*core.Blockchain, neotest.Signer, neotest.Signer) {
protoCfg := config.ProtocolConfiguration{
Magic: netmode.UnitTestNet,
SecondsPerBlock: 1,
StandbyCommittee: standByCommittee,
ValidatorsCount: 4,
VerifyBlocks: true,
VerifyTransactions: true,
}
st := storage.NewMemoryStore()
log := zaptest.NewLogger(t)
bc, err := core.NewBlockchain(st, protoCfg, log)
require.NoError(t, err)
go bc.Run()
t.Cleanup(bc.Close)
return bc, neotest.NewMultiSigner(multiValidatorAcc...), neotest.NewMultiSigner(multiCommitteeAcc...)
}

View file

@ -0,0 +1,21 @@
package chain
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/stretchr/testify/require"
)
// TestNewMulti checks that transaction and block is signed correctly for multi-node setup.
func TestNewMulti(t *testing.T) {
bc, vAcc, cAcc := NewMulti(t)
e := neotest.NewExecutor(t, bc, vAcc, cAcc)
require.NotEqual(t, vAcc.ScriptHash(), cAcc.ScriptHash())
const amount = int64(10_0000_0000)
c := e.CommitteeInvoker(bc.UtilityTokenHash()).WithSigners(vAcc)
c.Invoke(t, true, "transfer", e.Validator.ScriptHash(), e.Committee.ScriptHash(), amount, nil)
}

83
pkg/neotest/client.go Normal file
View file

@ -0,0 +1,83 @@
package neotest
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// ContractInvoker is a client for specific contract.
type ContractInvoker struct {
*Executor
Hash util.Uint160
Signers []Signer
}
// CommitteeInvoker creates new ContractInvoker for contract with hash h.
func (e *Executor) CommitteeInvoker(h util.Uint160) *ContractInvoker {
return &ContractInvoker{
Executor: e,
Hash: h,
Signers: []Signer{e.Committee},
}
}
// TestInvoke creates test VM and invokes method with args.
func (c *ContractInvoker) TestInvoke(t *testing.T, method string, args ...interface{}) (*vm.Stack, error) {
tx := c.PrepareInvokeNoSign(t, method, args...)
b := c.NewUnsignedBlock(t, tx)
v, f := c.Chain.GetTestVM(trigger.Application, tx, b)
t.Cleanup(f)
v.LoadWithFlags(tx.Script, callflag.All)
err := v.Run()
return v.Estack(), err
}
// WithSigners creates new client with the provided signer.
func (c *ContractInvoker) WithSigners(signers ...Signer) *ContractInvoker {
newC := *c
newC.Signers = signers
return &newC
}
// PrepareInvoke creates new invocation transaction.
func (c *ContractInvoker) PrepareInvoke(t *testing.T, method string, args ...interface{}) *transaction.Transaction {
return c.Executor.NewTx(t, c.Signers, c.Hash, method, args...)
}
// PrepareInvokeNoSign creates new unsigned invocation transaction.
func (c *ContractInvoker) PrepareInvokeNoSign(t *testing.T, method string, args ...interface{}) *transaction.Transaction {
return c.Executor.NewUnsignedTx(t, c.Hash, method, args...)
}
// Invoke invokes method with args, persists transaction and checks the result.
// Returns transaction hash.
func (c *ContractInvoker) Invoke(t *testing.T, result interface{}, method string, args ...interface{}) util.Uint256 {
tx := c.PrepareInvoke(t, method, args...)
c.AddNewBlock(t, tx)
c.CheckHalt(t, tx.Hash(), stackitem.Make(result))
return tx.Hash()
}
// InvokeWithFeeFail is like InvokeFail but sets custom system fee for the transaction.
func (c *ContractInvoker) InvokeWithFeeFail(t *testing.T, message string, sysFee int64, method string, args ...interface{}) util.Uint256 {
tx := c.PrepareInvokeNoSign(t, method, args...)
c.Executor.SignTx(t, tx, sysFee, c.Signers...)
c.AddNewBlock(t, tx)
c.CheckFault(t, tx.Hash(), message)
return tx.Hash()
}
// InvokeFail invokes method with args, persists transaction and checks the error message.
// Returns transaction hash.
func (c *ContractInvoker) InvokeFail(t *testing.T, message string, method string, args ...interface{}) {
tx := c.PrepareInvoke(t, method, args...)
c.AddNewBlock(t, tx)
c.CheckFault(t, tx.Hash(), message)
}

85
pkg/neotest/compile.go Normal file
View file

@ -0,0 +1,85 @@
package neotest
import (
"io"
"testing"
"github.com/nspcc-dev/neo-go/cli/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require"
)
// Contract contains contract info for deployment.
type Contract struct {
Hash util.Uint160
NEF *nef.File
Manifest *manifest.Manifest
}
// contracts caches compiled contracts from FS across multiple tests.
var contracts = make(map[string]*Contract)
// CompileSource compiles contract from reader and returns it's NEF, manifest and hash.
func CompileSource(t *testing.T, sender util.Uint160, src io.Reader, opts *compiler.Options) *Contract {
// nef.NewFile() cares about version a lot.
config.Version = "neotest"
avm, di, err := compiler.CompileWithDebugInfo(opts.Name, src)
require.NoError(t, err)
ne, err := nef.NewFile(avm)
require.NoError(t, err)
m, err := compiler.CreateManifest(di, opts)
require.NoError(t, err)
return &Contract{
Hash: state.CreateContractHash(sender, ne.Checksum, m.Name),
NEF: ne,
Manifest: m,
}
}
// CompileFile compiles contract from file and returns it's NEF, manifest and hash.
func CompileFile(t *testing.T, sender util.Uint160, srcPath string, configPath string) *Contract {
if c, ok := contracts[srcPath]; ok {
return c
}
// nef.NewFile() cares about version a lot.
config.Version = "neotest"
avm, di, err := compiler.CompileWithDebugInfo(srcPath, nil)
require.NoError(t, err)
ne, err := nef.NewFile(avm)
require.NoError(t, err)
conf, err := smartcontract.ParseContractConfig(configPath)
require.NoError(t, err)
o := &compiler.Options{}
o.Name = conf.Name
o.ContractEvents = conf.Events
o.ContractSupportedStandards = conf.SupportedStandards
o.Permissions = make([]manifest.Permission, len(conf.Permissions))
for i := range conf.Permissions {
o.Permissions[i] = manifest.Permission(conf.Permissions[i])
}
o.SafeMethods = conf.SafeMethods
m, err := compiler.CreateManifest(di, o)
require.NoError(t, err)
c := &Contract{
Hash: state.CreateContractHash(sender, ne.Checksum, m.Name),
NEF: ne,
Manifest: m,
}
contracts[srcPath] = c
return c
}

141
pkg/neotest/signer.go Normal file
View file

@ -0,0 +1,141 @@
package neotest
import (
"bytes"
"fmt"
"testing"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"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/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
// Signer is a generic interface which can be either simple- or multi-signature signer.
type Signer interface {
// ScriptHash returns signer script hash.
Script() []byte
// Script returns signer verification script.
ScriptHash() util.Uint160
// SignHashable returns invocation script for signing an item.
SignHashable(uint32, hash.Hashable) []byte
// SignTx signs a transaction.
SignTx(netmode.Magic, *transaction.Transaction) error
}
// signer represents simple-signature signer.
type signer wallet.Account
// multiSigner represents single multi-signature signer consisting of provided accounts.
type multiSigner []*wallet.Account
// NewSingleSigner returns multi-signature signer for the provided account.
// It must contain exactly as many accounts as needed to sign the script.
func NewSingleSigner(acc *wallet.Account) Signer {
if !vm.IsSignatureContract(acc.Contract.Script) {
panic("account must have simple-signature verification script")
}
return (*signer)(acc)
}
// Script implements Signer interface.
func (s *signer) Script() []byte {
return (*wallet.Account)(s).Contract.Script
}
// ScriptHash implements Signer interface.
func (s *signer) ScriptHash() util.Uint160 {
return (*wallet.Account)(s).Contract.ScriptHash()
}
// SignHashable implements Signer interface.
func (s *signer) SignHashable(magic uint32, item hash.Hashable) []byte {
return append([]byte{byte(opcode.PUSHDATA1), 64},
(*wallet.Account)(s).PrivateKey().SignHashable(magic, item)...)
}
// SignTx implements Signer interface.
func (s *signer) SignTx(magic netmode.Magic, tx *transaction.Transaction) error {
return (*wallet.Account)(s).SignTx(magic, tx)
}
// NewMultiSigner returns multi-signature signer for the provided account.
// It must contain at least as many accounts as needed to sign the script.
func NewMultiSigner(accs ...*wallet.Account) Signer {
if len(accs) == 0 {
panic("empty account list")
}
script := accs[0].Contract.Script
m, _, ok := vm.ParseMultiSigContract(script)
if !ok {
panic("all accounts must have multi-signature verification script")
}
if len(accs) < m {
panic(fmt.Sprintf("verification script requires %d signatures, "+
"but only %d accounts were provided", m, len(accs)))
}
for _, acc := range accs {
if !bytes.Equal(script, acc.Contract.Script) {
panic("all accounts must have equal verification script")
}
}
return multiSigner(accs[:m])
}
// ScriptHash implements Signer interface.
func (m multiSigner) ScriptHash() util.Uint160 {
return m[0].Contract.ScriptHash()
}
// Script implements Signer interface.
func (m multiSigner) Script() []byte {
return m[0].Contract.Script
}
// SignHashable implements Signer interface.
func (m multiSigner) SignHashable(magic uint32, item hash.Hashable) []byte {
var script []byte
for _, acc := range m {
sign := acc.PrivateKey().SignHashable(magic, item)
script = append(script, byte(opcode.PUSHDATA1), 64)
script = append(script, sign...)
}
return script
}
// SignTx implements Signer interface.
func (m multiSigner) SignTx(magic netmode.Magic, tx *transaction.Transaction) error {
invoc := m.SignHashable(uint32(magic), tx)
verif := m.Script()
for i := range tx.Scripts {
if bytes.Equal(tx.Scripts[i].VerificationScript, verif) {
tx.Scripts[i].InvocationScript = invoc
return nil
}
}
tx.Scripts = append(tx.Scripts, transaction.Witness{
InvocationScript: invoc,
VerificationScript: verif,
})
return nil
}
func checkMultiSigner(t *testing.T, s Signer) {
accs, ok := s.(multiSigner)
require.True(t, ok, "expected to be a multi-signer")
require.True(t, len(accs) > 0, "empty multi-signer")
m := len(accs[0].Contract.Parameters)
require.True(t, m <= len(accs), "honest not count is too big for a multi-signer")
h := accs[0].Contract.ScriptHash()
for i := 1; i < len(accs); i++ {
require.Equal(t, m, len(accs[i].Contract.Parameters), "inconsistent multi-signer accounts")
require.Equal(t, h, accs[i].Contract.ScriptHash(), "inconsistent multi-signer accounts")
}
}