Merge pull request #2229 from nspcc-dev/contract-test
Generic contract testing infrastructure
This commit is contained in:
commit
0a7f8afcea
12 changed files with 1222 additions and 502 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -28,9 +28,8 @@ bin/
|
||||||
*~
|
*~
|
||||||
TAGS
|
TAGS
|
||||||
|
|
||||||
# leveldb
|
# storage
|
||||||
chains/
|
/chains
|
||||||
chain/
|
|
||||||
|
|
||||||
# patch
|
# patch
|
||||||
*.orig
|
*.orig
|
||||||
|
|
429
examples/nft-nd-nns/tests/nonnative_name_service_test.go
Normal file
429
examples/nft-nd-nns/tests/nonnative_name_service_test.go
Normal 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
|
||||||
|
)
|
|
@ -52,28 +52,6 @@ var neoOwner = testchain.MultisigScriptHash()
|
||||||
// examplesPrefix is a prefix of the example smart-contracts.
|
// examplesPrefix is a prefix of the example smart-contracts.
|
||||||
const examplesPrefix = "../../examples/"
|
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
|
// newTestChain should be called before newBlock invocation to properly setup
|
||||||
// global state.
|
// global state.
|
||||||
func newTestChain(t testing.TB) *Blockchain {
|
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
|
// register `neo.com` with A record type and priv0 owner via NS
|
||||||
transferFundsToCommittee(t, bc) // block #12
|
transferFundsToCommittee(t, bc) // block #12
|
||||||
res, err := invokeContractMethodGeneric(bc, defaultNameServiceSysfee,
|
res, err := invokeContractMethodGeneric(bc, -1,
|
||||||
nsHash, "addRoot", true, "com") // block #13
|
nsHash, "addRoot", true, "com") // block #13
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.Null{})
|
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
|
nsHash, "register", acc0, "neo.com", priv0ScriptHash) // block #14
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
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
|
"setRecord", acc0, "neo.com", int64(nns.A), "1.2.3.4") // block #15
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.Null{})
|
checkResult(t, res, stackitem.Null{})
|
||||||
|
@ -596,22 +574,24 @@ func prepareContractMethodInvokeGeneric(chain *Blockchain, sysfee int64,
|
||||||
return nil, w.Err
|
return nil, w.Err
|
||||||
}
|
}
|
||||||
script := w.Bytes()
|
script := w.Bytes()
|
||||||
tx := transaction.New(script, sysfee)
|
tx := transaction.New(script, 0)
|
||||||
tx.ValidUntilBlock = chain.blockHeight + 1
|
tx.ValidUntilBlock = chain.blockHeight + 1
|
||||||
var err error
|
var err error
|
||||||
switch s := signer.(type) {
|
switch s := signer.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
if s {
|
if s {
|
||||||
addSigners(testchain.CommitteeScriptHash(), tx)
|
addSigners(testchain.CommitteeScriptHash(), tx)
|
||||||
|
setTxSystemFee(chain, sysfee, tx)
|
||||||
err = testchain.SignTxCommittee(chain, tx)
|
err = testchain.SignTxCommittee(chain, tx)
|
||||||
} else {
|
} else {
|
||||||
addSigners(neoOwner, tx)
|
addSigners(neoOwner, tx)
|
||||||
|
setTxSystemFee(chain, sysfee, tx)
|
||||||
err = testchain.SignTx(chain, tx)
|
err = testchain.SignTx(chain, tx)
|
||||||
}
|
}
|
||||||
case *wallet.Account:
|
case *wallet.Account:
|
||||||
signTxWithAccounts(chain, tx, s)
|
signTxWithAccounts(chain, sysfee, tx, s)
|
||||||
case []*wallet.Account:
|
case []*wallet.Account:
|
||||||
signTxWithAccounts(chain, tx, s...)
|
signTxWithAccounts(chain, sysfee, tx, s...)
|
||||||
default:
|
default:
|
||||||
panic("invalid signer")
|
panic("invalid signer")
|
||||||
}
|
}
|
||||||
|
@ -621,7 +601,31 @@ func prepareContractMethodInvokeGeneric(chain *Blockchain, sysfee int64,
|
||||||
return tx, nil
|
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
|
scope := transaction.CalledByEntry
|
||||||
for _, acc := range accs {
|
for _, acc := range accs {
|
||||||
accH, _ := address.StringToUint160(acc.Address)
|
accH, _ := address.StringToUint160(acc.Address)
|
||||||
|
@ -631,6 +635,7 @@ func signTxWithAccounts(chain *Blockchain, tx *transaction.Transaction, accs ...
|
||||||
})
|
})
|
||||||
scope = transaction.Global
|
scope = transaction.Global
|
||||||
}
|
}
|
||||||
|
setTxSystemFee(chain, sysFee, tx)
|
||||||
size := io.GetVarSize(tx)
|
size := io.GetVarSize(tx)
|
||||||
for _, acc := range accs {
|
for _, acc := range accs {
|
||||||
if acc.Contract.Deployed {
|
if acc.Contract.Deployed {
|
||||||
|
|
|
@ -469,3 +469,10 @@ func TestNEO_TransferZeroWithNonZeroBalance(t *testing.T) {
|
||||||
check(t, false)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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
8
pkg/neotest/account.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package neotest
|
||||||
|
|
||||||
|
var _nonce uint32
|
||||||
|
|
||||||
|
func nonce() uint32 {
|
||||||
|
_nonce++
|
||||||
|
return _nonce
|
||||||
|
}
|
260
pkg/neotest/basic.go
Normal file
260
pkg/neotest/basic.go
Normal 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
152
pkg/neotest/chain/chain.go
Normal 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...)
|
||||||
|
}
|
21
pkg/neotest/chain/chain_test.go
Normal file
21
pkg/neotest/chain/chain_test.go
Normal 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
83
pkg/neotest/client.go
Normal 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
85
pkg/neotest/compile.go
Normal 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
141
pkg/neotest/signer.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue