interop: implement System.Runtime.GetRandom

Our murmur3 implementation is architecture independent and optimized in
assembly.

Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
Evgeniy Stratonikov 2021-07-14 15:05:28 +03:00
parent fdb54f2dc3
commit 8077f9232d
11 changed files with 142 additions and 0 deletions

1
go.mod
View file

@ -17,6 +17,7 @@ require (
github.com/prometheus/client_golang v1.2.1 github.com/prometheus/client_golang v1.2.1
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73 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/urfave/cli v1.20.0
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74
go.etcd.io/bbolt v1.3.4 go.etcd.io/bbolt v1.3.4

2
go.sum
View file

@ -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/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 h1:I2drr5K0tykBofr74ZEGliE/Hf6fNkEbcPyFvsy7wZk=
github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= 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/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 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=

View file

@ -76,6 +76,7 @@ func TestSyscallExecution(t *testing.T) {
"runtime.GetInvocationCounter": {interopnames.SystemRuntimeGetInvocationCounter, nil, false}, "runtime.GetInvocationCounter": {interopnames.SystemRuntimeGetInvocationCounter, nil, false},
"runtime.GetNetwork": {interopnames.SystemRuntimeGetNetwork, nil, false}, "runtime.GetNetwork": {interopnames.SystemRuntimeGetNetwork, nil, false},
"runtime.GetNotifications": {interopnames.SystemRuntimeGetNotifications, []string{u160}, false}, "runtime.GetNotifications": {interopnames.SystemRuntimeGetNotifications, []string{u160}, false},
"runtime.GetRandom": {interopnames.SystemRuntimeGetRandom, nil, false},
"runtime.GetScriptContainer": {interopnames.SystemRuntimeGetScriptContainer, nil, false}, "runtime.GetScriptContainer": {interopnames.SystemRuntimeGetScriptContainer, nil, false},
"runtime.GetTime": {interopnames.SystemRuntimeGetTime, nil, false}, "runtime.GetTime": {interopnames.SystemRuntimeGetTime, nil, false},
"runtime.GetTrigger": {interopnames.SystemRuntimeGetTrigger, nil, false}, "runtime.GetTrigger": {interopnames.SystemRuntimeGetTrigger, nil, false},

View file

@ -1908,6 +1908,7 @@ func (bc *Blockchain) newInteropContext(trigger trigger.Type, d dao.DAO, block *
case block != nil: case block != nil:
ic.Container = block ic.Container = block
} }
ic.InitNonceData()
return ic return ic
} }

View file

@ -1,6 +1,7 @@
package interop package interop
import ( import (
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"sort" "sort"
@ -39,6 +40,7 @@ type Context struct {
Natives []Contract Natives []Contract
Trigger trigger.Type Trigger trigger.Type
Block *block.Block Block *block.Block
NonceData [16]byte
Tx *transaction.Transaction Tx *transaction.Transaction
DAO *dao.Cached DAO *dao.Cached
Notifications []state.NotificationEvent 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, // 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 // it's supposed to be inited once for all interopContexts, so it doesn't use
// vm.InteropFuncPrice directly. // vm.InteropFuncPrice directly.

View file

@ -26,6 +26,7 @@ const (
SystemRuntimeGetInvocationCounter = "System.Runtime.GetInvocationCounter" SystemRuntimeGetInvocationCounter = "System.Runtime.GetInvocationCounter"
SystemRuntimeGetNetwork = "System.Runtime.GetNetwork" SystemRuntimeGetNetwork = "System.Runtime.GetNetwork"
SystemRuntimeGetNotifications = "System.Runtime.GetNotifications" SystemRuntimeGetNotifications = "System.Runtime.GetNotifications"
SystemRuntimeGetRandom = "System.Runtime.GetRandom"
SystemRuntimeGetScriptContainer = "System.Runtime.GetScriptContainer" SystemRuntimeGetScriptContainer = "System.Runtime.GetScriptContainer"
SystemRuntimeGetTime = "System.Runtime.GetTime" SystemRuntimeGetTime = "System.Runtime.GetTime"
SystemRuntimeGetTrigger = "System.Runtime.GetTrigger" SystemRuntimeGetTrigger = "System.Runtime.GetTrigger"
@ -64,6 +65,7 @@ var names = []string{
SystemRuntimeGetInvocationCounter, SystemRuntimeGetInvocationCounter,
SystemRuntimeGetNetwork, SystemRuntimeGetNetwork,
SystemRuntimeGetNotifications, SystemRuntimeGetNotifications,
SystemRuntimeGetRandom,
SystemRuntimeGetScriptContainer, SystemRuntimeGetScriptContainer,
SystemRuntimeGetTime, SystemRuntimeGetTime,
SystemRuntimeGetTrigger, SystemRuntimeGetTrigger,

View file

@ -1,13 +1,16 @@
package runtime package runtime
import ( import (
"encoding/binary"
"errors" "errors"
"github.com/nspcc-dev/neo-go/pkg/core/interop" "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/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/util"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/twmb/murmur3"
) )
// GasLeft returns remaining amount of GAS. // GasLeft returns remaining amount of GAS.
@ -74,3 +77,19 @@ func GetNetwork(ic *interop.Context) error {
ic.VM.Estack().PushVal(uint32(m)) ic.VM.Estack().PushVal(uint32(m))
return nil 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
}

View file

@ -1,6 +1,7 @@
package runtime package runtime
import ( import (
"encoding/hex"
"testing" "testing"
"github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/random"
@ -114,3 +115,20 @@ func TestRuntimeGetInvocationCounter(t *testing.T) {
checkStack(t, ic.VM, 42) 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))
}

View file

@ -8,6 +8,9 @@ import (
"github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/internal/testchain" "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/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
@ -35,6 +38,79 @@ import (
"github.com/stretchr/testify/require" "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) { func TestContractCreateAccount(t *testing.T) {
v, ic, _ := createVM(t) v, ic, _ := createVM(t)
t.Run("Good", func(t *testing.T) { t.Run("Good", func(t *testing.T) {

View file

@ -52,6 +52,7 @@ var systemInterops = []interop.Function{
{Name: interopnames.SystemRuntimeGetInvocationCounter, Func: runtime.GetInvocationCounter, Price: 1 << 4}, {Name: interopnames.SystemRuntimeGetInvocationCounter, Func: runtime.GetInvocationCounter, Price: 1 << 4},
{Name: interopnames.SystemRuntimeGetNetwork, Func: runtime.GetNetwork, Price: 1 << 3}, {Name: interopnames.SystemRuntimeGetNetwork, Func: runtime.GetNetwork, Price: 1 << 3},
{Name: interopnames.SystemRuntimeGetNotifications, Func: runtime.GetNotifications, Price: 1 << 8, ParamCount: 1}, {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.SystemRuntimeGetScriptContainer, Func: engineGetScriptContainer, Price: 1 << 3},
{Name: interopnames.SystemRuntimeGetTime, Func: runtime.GetTime, Price: 1 << 3, RequiredFlags: callflag.ReadStates}, {Name: interopnames.SystemRuntimeGetTime, Func: runtime.GetTime, Price: 1 << 3, RequiredFlags: callflag.ReadStates},
{Name: interopnames.SystemRuntimeGetTrigger, Func: runtime.GetTrigger, Price: 1 << 3}, {Name: interopnames.SystemRuntimeGetTrigger, Func: runtime.GetTrigger, Price: 1 << 3},

View file

@ -96,3 +96,10 @@ func GetInvocationCounter() int {
func Platform() []byte { func Platform() []byte {
return neogointernal.Syscall0("System.Runtime.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)
}