From 8077f9232d9e82ab1aa58ba631442461dc684309 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Wed, 14 Jul 2021 15:05:28 +0300 Subject: [PATCH] interop: implement `System.Runtime.GetRandom` Our murmur3 implementation is architecture independent and optimized in assembly. Signed-off-by: Evgeniy Stratonikov --- go.mod | 1 + go.sum | 2 + pkg/compiler/syscall_test.go | 1 + pkg/core/blockchain.go | 1 + pkg/core/interop/context.go | 14 +++++ pkg/core/interop/interopnames/names.go | 2 + pkg/core/interop/runtime/util.go | 19 +++++++ pkg/core/interop/runtime/util_test.go | 18 ++++++ pkg/core/interop_system_test.go | 76 ++++++++++++++++++++++++++ pkg/core/interops.go | 1 + pkg/interop/runtime/runtime.go | 7 +++ 11 files changed, 142 insertions(+) diff --git a/go.mod b/go.mod index 9a784e170..9f781f3e6 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/prometheus/client_golang v1.2.1 github.com/stretchr/testify v1.6.1 github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73 + github.com/twmb/murmur3 v1.1.5 github.com/urfave/cli v1.20.0 github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 go.etcd.io/bbolt v1.3.4 diff --git a/go.sum b/go.sum index 771128fe8..09e09df7b 100644 --- a/go.sum +++ b/go.sum @@ -228,6 +228,8 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73 h1:I2drr5K0tykBofr74ZEGliE/Hf6fNkEbcPyFvsy7wZk= github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/twmb/murmur3 v1.1.5 h1:i9OLS9fkuLzBXjt6dptlAEyk58fJsSTXbRg3SgVyqgk= +github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= diff --git a/pkg/compiler/syscall_test.go b/pkg/compiler/syscall_test.go index a41564437..2d864c520 100644 --- a/pkg/compiler/syscall_test.go +++ b/pkg/compiler/syscall_test.go @@ -76,6 +76,7 @@ func TestSyscallExecution(t *testing.T) { "runtime.GetInvocationCounter": {interopnames.SystemRuntimeGetInvocationCounter, nil, false}, "runtime.GetNetwork": {interopnames.SystemRuntimeGetNetwork, nil, false}, "runtime.GetNotifications": {interopnames.SystemRuntimeGetNotifications, []string{u160}, false}, + "runtime.GetRandom": {interopnames.SystemRuntimeGetRandom, nil, false}, "runtime.GetScriptContainer": {interopnames.SystemRuntimeGetScriptContainer, nil, false}, "runtime.GetTime": {interopnames.SystemRuntimeGetTime, nil, false}, "runtime.GetTrigger": {interopnames.SystemRuntimeGetTrigger, nil, false}, diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 46e711615..aad8bf483 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1908,6 +1908,7 @@ func (bc *Blockchain) newInteropContext(trigger trigger.Type, d dao.DAO, block * case block != nil: ic.Container = block } + ic.InitNonceData() return ic } diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index 4379a0fb2..6e7f94618 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -1,6 +1,7 @@ package interop import ( + "encoding/binary" "errors" "fmt" "sort" @@ -39,6 +40,7 @@ type Context struct { Natives []Contract Trigger trigger.Type Block *block.Block + NonceData [16]byte Tx *transaction.Transaction DAO *dao.Cached Notifications []state.NotificationEvent @@ -70,6 +72,18 @@ func NewContext(trigger trigger.Type, bc blockchainer.Blockchainer, d dao.DAO, } } +// InitNonceData initializes nonce to be used in `GetRandom` calculations. +func (ic *Context) InitNonceData() { + if tx, ok := ic.Container.(*transaction.Transaction); ok { + copy(ic.NonceData[:], tx.Hash().BytesBE()) + } + if ic.Block != nil { + nonce := ic.Block.Nonce + nonce ^= binary.LittleEndian.Uint64(ic.NonceData[:]) + binary.LittleEndian.PutUint64(ic.NonceData[:], nonce) + } +} + // Function binds function name, id with the function itself and price, // it's supposed to be inited once for all interopContexts, so it doesn't use // vm.InteropFuncPrice directly. diff --git a/pkg/core/interop/interopnames/names.go b/pkg/core/interop/interopnames/names.go index e075d37d1..eaa5ce8a8 100644 --- a/pkg/core/interop/interopnames/names.go +++ b/pkg/core/interop/interopnames/names.go @@ -26,6 +26,7 @@ const ( SystemRuntimeGetInvocationCounter = "System.Runtime.GetInvocationCounter" SystemRuntimeGetNetwork = "System.Runtime.GetNetwork" SystemRuntimeGetNotifications = "System.Runtime.GetNotifications" + SystemRuntimeGetRandom = "System.Runtime.GetRandom" SystemRuntimeGetScriptContainer = "System.Runtime.GetScriptContainer" SystemRuntimeGetTime = "System.Runtime.GetTime" SystemRuntimeGetTrigger = "System.Runtime.GetTrigger" @@ -64,6 +65,7 @@ var names = []string{ SystemRuntimeGetInvocationCounter, SystemRuntimeGetNetwork, SystemRuntimeGetNotifications, + SystemRuntimeGetRandom, SystemRuntimeGetScriptContainer, SystemRuntimeGetTime, SystemRuntimeGetTrigger, diff --git a/pkg/core/interop/runtime/util.go b/pkg/core/interop/runtime/util.go index b6f372bf2..77f311061 100644 --- a/pkg/core/interop/runtime/util.go +++ b/pkg/core/interop/runtime/util.go @@ -1,13 +1,16 @@ package runtime import ( + "encoding/binary" "errors" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "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" + "github.com/twmb/murmur3" ) // GasLeft returns remaining amount of GAS. @@ -74,3 +77,19 @@ func GetNetwork(ic *interop.Context) error { ic.VM.Estack().PushVal(uint32(m)) return nil } + +// GetRandom returns pseudo-random number which depends on block nonce and transaction hash. +func GetRandom(ic *interop.Context) error { + res := murmur128(ic.NonceData[:], ic.Network) + ic.VM.Estack().PushVal(bigint.FromBytesUnsigned(res)) + copy(ic.NonceData[:], res) + return nil +} + +func murmur128(data []byte, seed uint32) []byte { + h1, h2 := murmur3.SeedSum128(uint64(seed), uint64(seed), data) + result := make([]byte, 16) + binary.LittleEndian.PutUint64(result, h1) + binary.LittleEndian.PutUint64(result[8:], h2) + return result +} diff --git a/pkg/core/interop/runtime/util_test.go b/pkg/core/interop/runtime/util_test.go index b614188a3..4827498c4 100644 --- a/pkg/core/interop/runtime/util_test.go +++ b/pkg/core/interop/runtime/util_test.go @@ -1,6 +1,7 @@ package runtime import ( + "encoding/hex" "testing" "github.com/nspcc-dev/neo-go/internal/random" @@ -114,3 +115,20 @@ func TestRuntimeGetInvocationCounter(t *testing.T) { checkStack(t, ic.VM, 42) }) } + +// Test compatibility with C# implementation. +// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/Cryptography/UT_Murmur128.cs +func TestMurmurCompat(t *testing.T) { + res := murmur128([]byte("hello"), 123) + require.Equal(t, "0bc59d0ad25fde2982ed65af61227a0e", hex.EncodeToString(res)) + + res = murmur128([]byte("world"), 123) + require.Equal(t, "3d3810fed480472bd214a14023bb407f", hex.EncodeToString(res)) + + res = murmur128([]byte("hello world"), 123) + require.Equal(t, "e0a0632d4f51302c55e3b3e48d28795d", hex.EncodeToString(res)) + + bs, _ := hex.DecodeString("718f952132679baa9c5c2aa0d329fd2a") + res = murmur128(bs, 123) + require.Equal(t, "9b4aa747ff0cf4e41b3d96251551c8ae", hex.EncodeToString(res)) +} diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index 62780a6bc..5b763952b 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -8,6 +8,9 @@ import ( "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/testchain" + "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/block" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" @@ -35,6 +38,79 @@ import ( "github.com/stretchr/testify/require" ) +// Tests are taken from +// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/SmartContract/UT_ApplicationEngine.Runtime.cs +func TestRuntimeGetRandomCompatibility(t *testing.T) { + bc := newTestChain(t) + + b := getSharpTestGenesis(t) + tx := getSharpTestTx(util.Uint160{}) + ic := bc.newInteropContext(trigger.Application, dao.NewCached(bc.dao), b, tx) + ic.Network = uint32(netmode.MainNet) + + ic.VM = vm.New() + ic.VM.LoadScript([]byte{0x01}) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "225932872514876835587448704843370203748", ic.VM.Estack().Pop().BigInt().String()) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "190129535548110356450238097068474508661", ic.VM.Estack().Pop().BigInt().String()) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "48930406787011198493485648810190184269", ic.VM.Estack().Pop().BigInt().String()) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "66199389469641263539889463157823839112", ic.VM.Estack().Pop().BigInt().String()) + + require.NoError(t, runtime.GetRandom(ic)) + require.Equal(t, "217172703763162599519098299724476526911", ic.VM.Estack().Pop().BigInt().String()) +} + +func TestRuntimeGetRandomDifferentTransactions(t *testing.T) { + bc := newTestChain(t) + b, _ := bc.GetBlock(bc.GetHeaderHash(0)) + + tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 0) + ic1 := bc.newInteropContext(trigger.Application, dao.NewCached(bc.dao), b, tx1) + ic1.VM = vm.New() + ic1.VM.LoadScript(tx1.Script) + + tx2 := transaction.New([]byte{byte(opcode.PUSH2)}, 0) + ic2 := bc.newInteropContext(trigger.Application, dao.NewCached(bc.dao), b, tx2) + ic2.VM = vm.New() + ic2.VM.LoadScript(tx2.Script) + + require.NoError(t, runtime.GetRandom(ic1)) + require.NoError(t, runtime.GetRandom(ic2)) + require.NotEqual(t, ic1.VM.Estack().Pop().BigInt(), ic2.VM.Estack().Pop().BigInt()) + + require.NoError(t, runtime.GetRandom(ic1)) + require.NoError(t, runtime.GetRandom(ic2)) + require.NotEqual(t, ic1.VM.Estack().Pop().BigInt(), ic2.VM.Estack().Pop().BigInt()) +} + +func getSharpTestTx(sender util.Uint160) *transaction.Transaction { + tx := transaction.New([]byte{byte(opcode.PUSH2)}, 0) + tx.Nonce = 0 + tx.Signers = append(tx.Signers, transaction.Signer{ + Account: sender, + Scopes: transaction.CalledByEntry, + }) + tx.Scripts = append(tx.Scripts, transaction.Witness{}) + return tx +} + +func getSharpTestGenesis(t *testing.T) *block.Block { + const configPath = "../../config" + + cfg, err := config.Load(configPath, netmode.MainNet) + require.NoError(t, err) + b, err := createGenesisBlock(cfg.ProtocolConfiguration) + require.NoError(t, err) + return b +} + func TestContractCreateAccount(t *testing.T) { v, ic, _ := createVM(t) t.Run("Good", func(t *testing.T) { diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 20ad26dca..030b9a8d3 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -52,6 +52,7 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemRuntimeGetInvocationCounter, Func: runtime.GetInvocationCounter, Price: 1 << 4}, {Name: interopnames.SystemRuntimeGetNetwork, Func: runtime.GetNetwork, Price: 1 << 3}, {Name: interopnames.SystemRuntimeGetNotifications, Func: runtime.GetNotifications, Price: 1 << 8, ParamCount: 1}, + {Name: interopnames.SystemRuntimeGetRandom, Func: runtime.GetRandom, Price: 1 << 4}, {Name: interopnames.SystemRuntimeGetScriptContainer, Func: engineGetScriptContainer, Price: 1 << 3}, {Name: interopnames.SystemRuntimeGetTime, Func: runtime.GetTime, Price: 1 << 3, RequiredFlags: callflag.ReadStates}, {Name: interopnames.SystemRuntimeGetTrigger, Func: runtime.GetTrigger, Price: 1 << 3}, diff --git a/pkg/interop/runtime/runtime.go b/pkg/interop/runtime/runtime.go index c5e36a7f1..f82a1ed4d 100644 --- a/pkg/interop/runtime/runtime.go +++ b/pkg/interop/runtime/runtime.go @@ -96,3 +96,10 @@ func GetInvocationCounter() int { func Platform() []byte { return neogointernal.Syscall0("System.Runtime.Platform").([]byte) } + +// GetRandom returns pseudo-random number which depends on block nonce and tx hash. +// Each invocation will return a different number. This function uses +// `System.Runtime.GetRandom` syscall. +func GetRandom() int { + return neogointernal.Syscall0("System.Runtime.GetRandom").(int) +}