native: add Ledger contract, fix #1696

But don't change the way we process/store transactions and blocks. Effectively
it's just an interface for smart contracts that replaces old syscalls.

Transaction definition is moved temporarily to runtime package and Block
definition is removed (till we solve #1691 properly).
This commit is contained in:
Roman Khimov 2021-02-03 22:01:20 +03:00
parent 641896b2fb
commit ac527650eb
24 changed files with 466 additions and 445 deletions

Binary file not shown.

View file

@ -239,7 +239,7 @@ func scAndVMInteropTypeFromExpr(named *types.Named) (smartcontract.ParamType, st
name := named.Obj().Name()
pkg := named.Obj().Pkg().Name()
switch pkg {
case "blockchain", "contract":
case "runtime", "contract":
return smartcontract.ArrayType, stackitem.ArrayT // Block, Transaction, Contract
case "interop":
if name != "Interface" {

View file

@ -16,7 +16,7 @@ func TestCodeGen_DebugInfo(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/interop"
import "github.com/nspcc-dev/neo-go/pkg/interop/storage"
import "github.com/nspcc-dev/neo-go/pkg/interop/blockchain"
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
func Main(op string) bool {
var s string
_ = s
@ -47,7 +47,7 @@ func unexportedMethod() int { return 1 }
func MethodParams(addr interop.Hash160, h interop.Hash256,
sig interop.Signature, pub interop.PublicKey,
inter interop.Interface,
ctx storage.Context, tx blockchain.Transaction) bool {
ctx storage.Context, tx runtime.Transaction) bool {
return true
}
type MyStruct struct {}

View file

@ -14,13 +14,6 @@ var syscalls = map[string]map[string]string{
"Itoa": interopnames.SystemBinaryItoa,
"Serialize": interopnames.SystemBinarySerialize,
},
"blockchain": {
"GetBlock": interopnames.SystemBlockchainGetBlock,
"GetHeight": interopnames.SystemBlockchainGetHeight,
"GetTransaction": interopnames.SystemBlockchainGetTransaction,
"GetTransactionFromBlock": interopnames.SystemBlockchainGetTransactionFromBlock,
"GetTransactionHeight": interopnames.SystemBlockchainGetTransactionHeight,
},
"contract": {
"Call": interopnames.SystemContractCall,
"CreateStandardAccount": interopnames.SystemContractCreateStandardAccount,

View file

@ -2,10 +2,8 @@ package core
import (
"errors"
"fmt"
"math/big"
"math/rand"
"strings"
"testing"
"time"
@ -1165,12 +1163,16 @@ func TestIsTxStillRelevant(t *testing.T) {
require.NoError(t, bc.AddBlock(bc.newBlock()))
require.True(t, bc.IsTxStillRelevant(tx3, nil, false))
})
/* // neo-project/neo#2289
t.Run("contract witness check fails", func(t *testing.T) {
src := fmt.Sprintf(`package verify
import "github.com/nspcc-dev/neo-go/pkg/interop/blockchain"
import (
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
func Verify() bool {
currentHeight := blockchain.GetHeight()
return currentHeight < %d
currentHeight := contract.Call(util.FromAddress("NV5WuMGkwhQexQ4afTwuRojMeWwfWrEAdv"), "currentIndex", contract.ReadStates)
return currentHeight.(int) < %d
}`, bc.BlockHeight()+2) // deploy + next block
txDeploy, h, err := testchain.NewDeployTx(bc, "TestVerify", neoOwner, strings.NewReader(src))
require.NoError(t, err)
@ -1184,7 +1186,7 @@ func TestIsTxStillRelevant(t *testing.T) {
Account: h,
Scopes: transaction.None,
})
tx.NetworkFee += 1_000_000
tx.NetworkFee += 10_000_000
require.NoError(t, testchain.SignTx(bc, tx))
tx.Scripts = append(tx.Scripts, transaction.Witness{})
@ -1192,6 +1194,7 @@ func TestIsTxStillRelevant(t *testing.T) {
require.NoError(t, bc.AddBlock(bc.newBlock()))
require.False(t, bc.IsTxStillRelevant(tx, mp, false))
})
*/
}
func TestMemPoolRemoval(t *testing.T) {

View file

@ -10,11 +10,6 @@ const (
SystemBinaryDeserialize = "System.Binary.Deserialize"
SystemBinaryItoa = "System.Binary.Itoa"
SystemBinarySerialize = "System.Binary.Serialize"
SystemBlockchainGetBlock = "System.Blockchain.GetBlock"
SystemBlockchainGetHeight = "System.Blockchain.GetHeight"
SystemBlockchainGetTransaction = "System.Blockchain.GetTransaction"
SystemBlockchainGetTransactionFromBlock = "System.Blockchain.GetTransactionFromBlock"
SystemBlockchainGetTransactionHeight = "System.Blockchain.GetTransactionHeight"
SystemCallbackCreate = "System.Callback.Create"
SystemCallbackCreateFromMethod = "System.Callback.CreateFromMethod"
SystemCallbackCreateFromSyscall = "System.Callback.CreateFromSyscall"
@ -69,11 +64,6 @@ var names = []string{
SystemBinaryDeserialize,
SystemBinaryItoa,
SystemBinarySerialize,
SystemBlockchainGetBlock,
SystemBlockchainGetHeight,
SystemBlockchainGetTransaction,
SystemBlockchainGetTransactionFromBlock,
SystemBlockchainGetTransactionHeight,
SystemCallbackCreate,
SystemCallbackCreateFromMethod,
SystemCallbackCreateFromSyscall,

View file

@ -4,13 +4,10 @@ import (
"crypto/elliptic"
"errors"
"fmt"
"math"
"math/big"
"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/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@ -45,144 +42,15 @@ const (
Constant StorageFlag = 0x01
)
// getBlockHashFromElement converts given vm.Element to block hash using given
// Blockchainer if needed. Interop functions accept both block numbers and
// block hashes as parameters, thus this function is needed.
func getBlockHashFromElement(bc blockchainer.Blockchainer, element *vm.Element) (util.Uint256, error) {
var hash util.Uint256
hashbytes := element.Bytes()
if len(hashbytes) <= 5 {
hashint := element.BigInt().Int64()
if hashint < 0 || hashint > math.MaxUint32 {
return hash, errors.New("bad block index")
}
hash = bc.GetHeaderHash(int(hashint))
} else {
return util.Uint256DecodeBytesBE(hashbytes)
}
return hash, nil
}
// blockToStackItem converts block.Block to stackitem.Item
func blockToStackItem(b *block.Block) stackitem.Item {
return stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(b.Hash().BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(b.Version))),
stackitem.NewByteArray(b.PrevHash.BytesBE()),
stackitem.NewByteArray(b.MerkleRoot.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(b.Timestamp))),
stackitem.NewBigInteger(big.NewInt(int64(b.Index))),
stackitem.NewByteArray(b.NextConsensus.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(len(b.Transactions)))),
})
}
// bcGetBlock returns current block.
func bcGetBlock(ic *interop.Context) error {
hash, err := getBlockHashFromElement(ic.Chain, ic.VM.Estack().Pop())
if err != nil {
return err
}
block, err := ic.Chain.GetBlock(hash)
if err != nil || !isTraceableBlock(ic, block.Index) {
ic.VM.Estack().PushVal(stackitem.Null{})
} else {
ic.VM.Estack().PushVal(blockToStackItem(block))
}
return nil
}
// bcGetHeight returns blockchain height.
func bcGetHeight(ic *interop.Context) error {
ic.VM.Estack().PushVal(ic.Chain.BlockHeight())
return nil
}
// getTransactionAndHeight gets parameter from the vm evaluation stack and
// returns transaction and its height if it's present in the blockchain.
func getTransactionAndHeight(cd *dao.Cached, v *vm.VM) (*transaction.Transaction, uint32, error) {
hashbytes := v.Estack().Pop().Bytes()
hash, err := util.Uint256DecodeBytesBE(hashbytes)
if err != nil {
return nil, 0, err
}
return cd.GetTransaction(hash)
}
// isTraceableBlock defines whether we're able to give information about
// the block with index specified.
func isTraceableBlock(ic *interop.Context, index uint32) bool {
height := ic.Chain.BlockHeight()
MaxTraceableBlocks := ic.Chain.GetConfig().MaxTraceableBlocks
return index <= height && index+MaxTraceableBlocks > height
}
// transactionToStackItem converts transaction.Transaction to stackitem.Item
func transactionToStackItem(t *transaction.Transaction) stackitem.Item {
return stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(t.Hash().BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(t.Version))),
stackitem.NewBigInteger(big.NewInt(int64(t.Nonce))),
stackitem.NewByteArray(t.Sender().BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(t.SystemFee))),
stackitem.NewBigInteger(big.NewInt(int64(t.NetworkFee))),
stackitem.NewBigInteger(big.NewInt(int64(t.ValidUntilBlock))),
stackitem.NewByteArray(t.Script),
})
}
// bcGetTransaction returns transaction.
func bcGetTransaction(ic *interop.Context) error {
tx, h, err := getTransactionAndHeight(ic.DAO, ic.VM)
if err != nil || !isTraceableBlock(ic, h) {
ic.VM.Estack().PushVal(stackitem.Null{})
return nil
}
ic.VM.Estack().PushVal(transactionToStackItem(tx))
return nil
}
// bcGetTransactionFromBlock returns transaction with the given index from the
// block with height or hash specified.
func bcGetTransactionFromBlock(ic *interop.Context) error {
hash, err := getBlockHashFromElement(ic.Chain, ic.VM.Estack().Pop())
if err != nil {
return err
}
index := ic.VM.Estack().Pop().BigInt().Int64()
block, err := ic.DAO.GetBlock(hash)
if err != nil || !isTraceableBlock(ic, block.Index) {
ic.VM.Estack().PushVal(stackitem.Null{})
return nil
}
if index < 0 || index >= int64(len(block.Transactions)) {
return errors.New("wrong transaction index")
}
tx := block.Transactions[index]
ic.VM.Estack().PushVal(tx.Hash().BytesBE())
return nil
}
// bcGetTransactionHeight returns transaction height.
func bcGetTransactionHeight(ic *interop.Context) error {
_, h, err := getTransactionAndHeight(ic.DAO, ic.VM)
if err != nil || !isTraceableBlock(ic, h) {
ic.VM.Estack().PushVal(-1)
return nil
}
ic.VM.Estack().PushVal(h)
return nil
}
// engineGetScriptContainer returns transaction or block that contains the script
// being run.
func engineGetScriptContainer(ic *interop.Context) error {
var item stackitem.Item
switch t := ic.Container.(type) {
case *transaction.Transaction:
item = transactionToStackItem(t)
item = native.TransactionToStackItem(t)
case *block.Block:
item = blockToStackItem(t)
item = native.BlockToStackItem(t)
default:
return errors.New("unknown script container")
}

View file

@ -29,166 +29,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestBCGetTransactionHeight(t *testing.T) {
v, tx, ic, chain := createVMAndTX(t)
defer chain.Close()
for i := 0; i < 13; i++ {
require.NoError(t, chain.AddBlock(chain.newBlock()))
}
require.NoError(t, ic.DAO.StoreAsTransaction(tx, 13, nil))
t.Run("good", func(t *testing.T) {
v.Estack().PushVal(tx.Hash().BytesBE())
require.NoError(t, bcGetTransactionHeight(ic))
require.Equal(t, big.NewInt(13), v.Estack().Pop().BigInt())
})
t.Run("bad", func(t *testing.T) {
h := tx.Hash()
h[0] ^= 0xFF
v.Estack().PushVal(h.BytesBE())
require.NoError(t, bcGetTransactionHeight(ic))
require.Equal(t, big.NewInt(-1), v.Estack().Pop().BigInt())
})
}
func TestBCGetTransaction(t *testing.T) {
v, tx, context, chain := createVMAndTX(t)
defer chain.Close()
t.Run("success", func(t *testing.T) {
require.NoError(t, context.DAO.StoreAsTransaction(tx, 0, nil))
v.Estack().PushVal(tx.Hash().BytesBE())
err := bcGetTransaction(context)
require.NoError(t, err)
value := v.Estack().Pop().Value()
actual, ok := value.([]stackitem.Item)
require.True(t, ok)
require.Equal(t, 8, len(actual))
require.Equal(t, tx.Hash().BytesBE(), actual[0].Value().([]byte))
require.Equal(t, int64(tx.Version), actual[1].Value().(*big.Int).Int64())
require.Equal(t, int64(tx.Nonce), actual[2].Value().(*big.Int).Int64())
require.Equal(t, tx.Sender().BytesBE(), actual[3].Value().([]byte))
require.Equal(t, int64(tx.SystemFee), actual[4].Value().(*big.Int).Int64())
require.Equal(t, int64(tx.NetworkFee), actual[5].Value().(*big.Int).Int64())
require.Equal(t, int64(tx.ValidUntilBlock), actual[6].Value().(*big.Int).Int64())
require.Equal(t, tx.Script, actual[7].Value().([]byte))
})
t.Run("isn't traceable", func(t *testing.T) {
require.NoError(t, context.DAO.StoreAsTransaction(tx, 1, nil))
v.Estack().PushVal(tx.Hash().BytesBE())
err := bcGetTransaction(context)
require.NoError(t, err)
_, ok := v.Estack().Pop().Item().(stackitem.Null)
require.True(t, ok)
})
t.Run("bad hash", func(t *testing.T) {
require.NoError(t, context.DAO.StoreAsTransaction(tx, 1, nil))
v.Estack().PushVal(tx.Hash().BytesLE())
err := bcGetTransaction(context)
require.NoError(t, err)
_, ok := v.Estack().Pop().Item().(stackitem.Null)
require.True(t, ok)
})
}
func TestBCGetTransactionFromBlock(t *testing.T) {
v, block, context, chain := createVMAndBlock(t)
defer chain.Close()
require.NoError(t, chain.AddBlock(chain.newBlock()))
require.NoError(t, context.DAO.StoreAsBlock(block, nil))
t.Run("success", func(t *testing.T) {
v.Estack().PushVal(0)
v.Estack().PushVal(block.Hash().BytesBE())
err := bcGetTransactionFromBlock(context)
require.NoError(t, err)
value := v.Estack().Pop().Value()
actual, ok := value.([]byte)
require.True(t, ok)
require.Equal(t, block.Transactions[0].Hash().BytesBE(), actual)
})
t.Run("invalid block hash", func(t *testing.T) {
v.Estack().PushVal(0)
v.Estack().PushVal(block.Hash().BytesBE()[:10])
err := bcGetTransactionFromBlock(context)
require.Error(t, err)
})
t.Run("isn't traceable", func(t *testing.T) {
block.Index = 2
require.NoError(t, context.DAO.StoreAsBlock(block, nil))
v.Estack().PushVal(0)
v.Estack().PushVal(block.Hash().BytesBE())
err := bcGetTransactionFromBlock(context)
require.NoError(t, err)
_, ok := v.Estack().Pop().Item().(stackitem.Null)
require.True(t, ok)
})
t.Run("bad block hash", func(t *testing.T) {
block.Index = 1
require.NoError(t, context.DAO.StoreAsBlock(block, nil))
v.Estack().PushVal(0)
v.Estack().PushVal(block.Hash().BytesLE())
err := bcGetTransactionFromBlock(context)
require.NoError(t, err)
_, ok := v.Estack().Pop().Item().(stackitem.Null)
require.True(t, ok)
})
t.Run("bad transaction index", func(t *testing.T) {
require.NoError(t, context.DAO.StoreAsBlock(block, nil))
v.Estack().PushVal(1)
v.Estack().PushVal(block.Hash().BytesBE())
err := bcGetTransactionFromBlock(context)
require.Error(t, err)
})
}
func TestBCGetBlock(t *testing.T) {
v, context, chain := createVM(t)
defer chain.Close()
block := chain.newBlock()
require.NoError(t, chain.AddBlock(block))
t.Run("success", func(t *testing.T) {
v.Estack().PushVal(block.Hash().BytesBE())
err := bcGetBlock(context)
require.NoError(t, err)
value := v.Estack().Pop().Value()
actual, ok := value.([]stackitem.Item)
require.True(t, ok)
require.Equal(t, 8, len(actual))
require.Equal(t, block.Hash().BytesBE(), actual[0].Value().([]byte))
require.Equal(t, int64(block.Version), actual[1].Value().(*big.Int).Int64())
require.Equal(t, block.PrevHash.BytesBE(), actual[2].Value().([]byte))
require.Equal(t, block.MerkleRoot.BytesBE(), actual[3].Value().([]byte))
require.Equal(t, int64(block.Timestamp), actual[4].Value().(*big.Int).Int64())
require.Equal(t, int64(block.Index), actual[5].Value().(*big.Int).Int64())
require.Equal(t, block.NextConsensus.BytesBE(), actual[6].Value().([]byte))
require.Equal(t, int64(len(block.Transactions)), actual[7].Value().(*big.Int).Int64())
})
t.Run("bad hash", func(t *testing.T) {
v.Estack().PushVal(block.Hash().BytesLE())
err := bcGetTransaction(context)
require.NoError(t, err)
_, ok := v.Estack().Pop().Item().(stackitem.Null)
require.True(t, ok)
})
}
func TestContractIsStandard(t *testing.T) {
v, ic, chain := createVM(t)
defer chain.Close()

View file

@ -39,16 +39,6 @@ var systemInterops = []interop.Function{
{Name: interopnames.SystemBinaryDeserialize, Func: binary.Deserialize, Price: 1 << 14, ParamCount: 1},
{Name: interopnames.SystemBinaryItoa, Func: binary.Itoa, Price: 1 << 12, ParamCount: 2},
{Name: interopnames.SystemBinarySerialize, Func: binary.Serialize, Price: 1 << 12, ParamCount: 1},
{Name: interopnames.SystemBlockchainGetBlock, Func: bcGetBlock, Price: 1 << 16,
RequiredFlags: callflag.ReadStates, ParamCount: 1},
{Name: interopnames.SystemBlockchainGetHeight, Func: bcGetHeight, Price: 1 << 4,
RequiredFlags: callflag.ReadStates},
{Name: interopnames.SystemBlockchainGetTransaction, Func: bcGetTransaction, Price: 1 << 15,
RequiredFlags: callflag.ReadStates, ParamCount: 1},
{Name: interopnames.SystemBlockchainGetTransactionFromBlock, Func: bcGetTransactionFromBlock, Price: 1 << 15,
RequiredFlags: callflag.ReadStates, ParamCount: 2},
{Name: interopnames.SystemBlockchainGetTransactionHeight, Func: bcGetTransactionHeight, Price: 1 << 15,
RequiredFlags: callflag.ReadStates, ParamCount: 1},
{Name: interopnames.SystemContractCall, Func: contract.Call, Price: 1 << 15,
RequiredFlags: callflag.AllowCall, ParamCount: 4},
{Name: interopnames.SystemContractCallNative, Func: native.Call, Price: 0, ParamCount: 1},

View file

@ -9,11 +9,12 @@ import (
// Compatibility test. hashes are taken directly from C# node.
func TestNativeHashes(t *testing.T) {
require.Equal(t, "a501d7d7d10983673b61b7a2d3a813b36f9f0e43", newManagement().Hash.StringLE())
require.Equal(t, "f617baca689d1abddedda7c3b80675c4ac21e932", newNEO().Hash.StringLE())
require.Equal(t, "75844530eb44f4715a42950bb59b4d7ace0b2f3d", newGAS().Hash.StringLE())
require.Equal(t, "e21a28cfc1e662e152f668c86198141cc17b6c37", newPolicy().Hash.StringLE())
require.Equal(t, "69b1909aaa14143b0624ba0d61d5cd3b8b67529d", newDesignate(false).Hash.StringLE())
require.Equal(t, "b82bbf650f963dbf71577d10ea4077e711a13e7b", newOracle().Hash.StringLE())
require.Equal(t, "971d69c6dd10ce88e7dfffec1dc603c6125a8764", newLedger().Hash.StringLE())
require.Equal(t, "f61eebf573ea36593fd43aa150c055ad7906ab83", newNEO().Hash.StringLE())
require.Equal(t, "70e2301955bf1e74cbb31d18c2f96972abadb328", newGAS().Hash.StringLE())
require.Equal(t, "79bcd398505eb779df6e67e4be6c14cded08e2f2", newPolicy().Hash.StringLE())
require.Equal(t, "597b1471bbce497b7809e2c8f10db67050008b02", newDesignate(false).Hash.StringLE())
require.Equal(t, "8dc0e742cbdfdeda51ff8a8b78d46829144c80ee", newOracle().Hash.StringLE())
// Not yet a part of NEO.
//require.Equal(t, "", newNotary().Hash.StringLE()())
}

View file

@ -16,6 +16,7 @@ const reservedContractID = -100
// Contracts is a set of registered native contracts.
type Contracts struct {
Management *Management
Ledger *Ledger
NEO *NEO
GAS *GAS
Policy *Policy
@ -60,6 +61,10 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
cs.Management = mgmt
cs.Contracts = append(cs.Contracts, mgmt)
ledger := newLedger()
cs.Ledger = ledger
cs.Contracts = append(cs.Contracts, ledger)
gas := newGAS()
neo := newNEO()
neo.GAS = gas

View file

@ -48,7 +48,7 @@ type roleData struct {
}
const (
designateContractID = -5
designateContractID = -6
// maxNodeCount is the maximum number of nodes to set the role for.
maxNodeCount = 32

226
pkg/core/native/ledger.go Normal file
View file

@ -0,0 +1,226 @@
package native
import (
"fmt"
"math"
"math/big"
"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/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// Ledger provides an interface to blocks/transactions storage for smart
// contracts. It's not a part of the proper chain's state, so it's just a
// proxy between regular Blockchain/DAO interface and smart contracts.
type Ledger struct {
interop.ContractMD
}
const (
ledgerContractID = -2
prefixBlockHash = 9
prefixCurrentBlock = 12
prefixBlock = 5
prefixTransaction = 11
)
// newLedger creates new Ledger native contract.
func newLedger() *Ledger {
var l = &Ledger{
ContractMD: *interop.NewContractMD(nativenames.Ledger, ledgerContractID),
}
desc := newDescriptor("currentHash", smartcontract.Hash256Type)
md := newMethodAndPrice(l.currentHash, 1000000, callflag.ReadStates)
l.AddMethod(md, desc)
desc = newDescriptor("currentIndex", smartcontract.IntegerType)
md = newMethodAndPrice(l.currentIndex, 1000000, callflag.ReadStates)
l.AddMethod(md, desc)
desc = newDescriptor("getBlock", smartcontract.ArrayType,
manifest.NewParameter("indexOrHash", smartcontract.ByteArrayType))
md = newMethodAndPrice(l.getBlock, 1000000, callflag.ReadStates)
l.AddMethod(md, desc)
desc = newDescriptor("getTransaction", smartcontract.ArrayType,
manifest.NewParameter("hash", smartcontract.ByteArrayType))
md = newMethodAndPrice(l.getTransaction, 1000000, callflag.ReadStates)
l.AddMethod(md, desc)
desc = newDescriptor("getTransactionHeight", smartcontract.IntegerType,
manifest.NewParameter("hash", smartcontract.ByteArrayType))
md = newMethodAndPrice(l.getTransactionHeight, 1000000, callflag.ReadStates)
l.AddMethod(md, desc)
desc = newDescriptor("getTransactionFromBlock", smartcontract.ArrayType,
manifest.NewParameter("blockIndexOrHash", smartcontract.ByteArrayType),
manifest.NewParameter("txIndex", smartcontract.IntegerType))
md = newMethodAndPrice(l.getTransactionFromBlock, 2000000, callflag.ReadStates)
l.AddMethod(md, desc)
return l
}
// Metadata implements Contract interface.
func (l *Ledger) Metadata() *interop.ContractMD {
return &l.ContractMD
}
// Initialize implements Contract interface.
func (l *Ledger) Initialize(ic *interop.Context) error {
return nil
}
// OnPersist implements Contract interface.
func (l *Ledger) OnPersist(ic *interop.Context) error {
// Actual block/tx processing is done in Blockchain.storeBlock().
// Even though C# node add them to storage here, they're not
// accessible to smart contracts (see isTraceableBlock()), thus
// the end effect is the same.
return nil
}
// PostPersist implements Contract interface.
func (l *Ledger) PostPersist(ic *interop.Context) error {
return nil // Actual block/tx processing is done in Blockchain.storeBlock().
}
// currentHash implements currentHash SC method.
func (l *Ledger) currentHash(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.Make(ic.Chain.CurrentBlockHash().BytesBE())
}
// currentIndex implements currentIndex SC method.
func (l *Ledger) currentIndex(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.Make(ic.Chain.BlockHeight())
}
// getBlock implements getBlock SC method.
func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item {
hash := getBlockHashFromItem(ic.Chain, params[0])
block, err := ic.Chain.GetBlock(hash)
if err != nil || !isTraceableBlock(ic.Chain, block.Index) {
return stackitem.Null{}
}
return BlockToStackItem(block)
}
// getTransaction returns transaction to the SC.
func (l *Ledger) getTransaction(ic *interop.Context, params []stackitem.Item) stackitem.Item {
tx, h, err := getTransactionAndHeight(ic.DAO, params[0])
if err != nil || !isTraceableBlock(ic.Chain, h) {
return stackitem.Null{}
}
return TransactionToStackItem(tx)
}
// getTransactionHeight returns transaction height to the SC.
func (l *Ledger) getTransactionHeight(ic *interop.Context, params []stackitem.Item) stackitem.Item {
_, h, err := getTransactionAndHeight(ic.DAO, params[0])
if err != nil || !isTraceableBlock(ic.Chain, h) {
return stackitem.Make(-1)
}
return stackitem.Make(h)
}
// getTransactionFromBlock returns transaction with the given index from the
// block with height or hash specified.
func (l *Ledger) getTransactionFromBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item {
hash := getBlockHashFromItem(ic.Chain, params[0])
index := toUint32(params[1])
block, err := ic.Chain.GetBlock(hash)
if err != nil || !isTraceableBlock(ic.Chain, block.Index) {
return stackitem.Null{}
}
if index >= uint32(len(block.Transactions)) {
panic("wrong transaction index")
}
return TransactionToStackItem(block.Transactions[index])
}
// isTraceableBlock defines whether we're able to give information about
// the block with index specified.
func isTraceableBlock(bc blockchainer.Blockchainer, index uint32) bool {
height := bc.BlockHeight()
MaxTraceableBlocks := bc.GetConfig().MaxTraceableBlocks
return index <= height && index+MaxTraceableBlocks > height
}
// getBlockHashFromItem converts given stackitem.Item to block hash using given
// Blockchainer if needed. Interop functions accept both block numbers and
// block hashes as parameters, thus this function is needed. It's supposed to
// be called within VM context, so it panics if anything goes wrong.
func getBlockHashFromItem(bc blockchainer.Blockchainer, item stackitem.Item) util.Uint256 {
bigindex, err := item.TryInteger()
if err == nil && bigindex.IsInt64() {
index := bigindex.Int64()
if index < 0 || index > math.MaxUint32 {
panic("bad block index")
}
if uint32(index) > bc.BlockHeight() {
panic(fmt.Errorf("no block with index %d", index))
}
return bc.GetHeaderHash(int(index))
}
bytes, err := item.TryBytes()
if err != nil {
panic(err)
}
hash, err := util.Uint256DecodeBytesBE(bytes)
if err != nil {
panic(err)
}
return hash
}
// getTransactionAndHeight returns transaction and its height if it's present
// on the chain. It panics if anything goes wrong.
func getTransactionAndHeight(cd *dao.Cached, item stackitem.Item) (*transaction.Transaction, uint32, error) {
hashbytes, err := item.TryBytes()
if err != nil {
panic(err)
}
hash, err := util.Uint256DecodeBytesBE(hashbytes)
if err != nil {
panic(err)
}
return cd.GetTransaction(hash)
}
// BlockToStackItem converts block.Block to stackitem.Item
func BlockToStackItem(b *block.Block) stackitem.Item {
return stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(b.Hash().BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(b.Version))),
stackitem.NewByteArray(b.PrevHash.BytesBE()),
stackitem.NewByteArray(b.MerkleRoot.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(b.Timestamp))),
stackitem.NewBigInteger(big.NewInt(int64(b.Index))),
stackitem.NewByteArray(b.NextConsensus.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(len(b.Transactions)))),
})
}
// TransactionToStackItem converts transaction.Transaction to stackitem.Item
func TransactionToStackItem(t *transaction.Transaction) stackitem.Item {
return stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(t.Hash().BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(t.Version))),
stackitem.NewBigInteger(big.NewInt(int64(t.Nonce))),
stackitem.NewByteArray(t.Sender().BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(t.SystemFee))),
stackitem.NewBigInteger(big.NewInt(int64(t.NetworkFee))),
stackitem.NewBigInteger(big.NewInt(int64(t.ValidUntilBlock))),
stackitem.NewByteArray(t.Script),
})
}

View file

@ -54,7 +54,7 @@ const (
)
const (
nameServiceID = -7
nameServiceID = -8
prefixRoots = 10
prefixDomainPrice = 22

View file

@ -18,7 +18,7 @@ type GAS struct {
NEO *NEO
}
const gasContractID = -3
const gasContractID = -4
// GASFactor is a divisor for finding GAS integral value.
const GASFactor = NEOTotalSupply

View file

@ -51,7 +51,7 @@ type NEO struct {
}
const (
neoContractID = -2
neoContractID = -3
// NEOTotalSupply is the total amount of NEO in the system.
NEOTotalSupply = 100000000
// prefixCandidate is a prefix used to store validator's data.

View file

@ -3,6 +3,7 @@ package nativenames
// Names of all native contracts.
const (
Management = "ContractManagement"
Ledger = "LedgerContract"
Neo = "NeoToken"
Gas = "GasToken"
Policy = "PolicyContract"

View file

@ -46,7 +46,7 @@ type Oracle struct {
}
const (
oracleContractID = -6
oracleContractID = -7
maxURLLength = 256
maxFilterLength = 128
maxCallbackLength = 32

View file

@ -21,7 +21,7 @@ import (
)
const (
policyContractID = -4
policyContractID = -5
defaultMaxBlockSize = 1024 * 256
defaultMaxTransactionsPerBlock = 512

View file

@ -0,0 +1,177 @@
package core
import (
"math/big"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
func TestLedgerGetTransactionHeight(t *testing.T) {
_, tx, _, chain := createVMAndTX(t)
defer chain.Close()
ledger := chain.contracts.ByName(nativenames.Ledger).Metadata().Hash
for i := 0; i < 13; i++ {
require.NoError(t, chain.AddBlock(chain.newBlock()))
}
require.NoError(t, chain.dao.StoreAsTransaction(tx, 13, nil))
t.Run("good", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionHeight", tx.Hash().BytesBE())
require.NoError(t, err)
checkResult(t, res, stackitem.Make(13))
})
t.Run("bad", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionHeight", tx.Hash().BytesLE())
require.NoError(t, err)
checkResult(t, res, stackitem.Make(-1))
})
t.Run("not a hash", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionHeight", []byte{1})
require.NoError(t, err)
checkFAULTState(t, res)
})
}
func TestLedgerGetTransaction(t *testing.T) {
_, tx, _, chain := createVMAndTX(t)
defer chain.Close()
ledger := chain.contracts.ByName(nativenames.Ledger).Metadata().Hash
t.Run("success", func(t *testing.T) {
require.NoError(t, chain.dao.StoreAsTransaction(tx, 0, nil))
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransaction", tx.Hash().BytesBE())
require.NoError(t, err)
require.Equal(t, vm.HaltState, res.VMState, res.FaultException)
require.Equal(t, 1, len(res.Stack))
value := res.Stack[0].Value()
actual, ok := value.([]stackitem.Item)
require.True(t, ok)
require.Equal(t, 8, len(actual))
require.Equal(t, tx.Hash().BytesBE(), actual[0].Value().([]byte))
require.Equal(t, int64(tx.Version), actual[1].Value().(*big.Int).Int64())
require.Equal(t, int64(tx.Nonce), actual[2].Value().(*big.Int).Int64())
require.Equal(t, tx.Sender().BytesBE(), actual[3].Value().([]byte))
require.Equal(t, int64(tx.SystemFee), actual[4].Value().(*big.Int).Int64())
require.Equal(t, int64(tx.NetworkFee), actual[5].Value().(*big.Int).Int64())
require.Equal(t, int64(tx.ValidUntilBlock), actual[6].Value().(*big.Int).Int64())
require.Equal(t, tx.Script, actual[7].Value().([]byte))
})
t.Run("isn't traceable", func(t *testing.T) {
require.NoError(t, chain.dao.StoreAsTransaction(tx, 2, nil)) // block 1 is added above
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransaction", tx.Hash().BytesBE())
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
})
t.Run("bad hash", func(t *testing.T) {
require.NoError(t, chain.dao.StoreAsTransaction(tx, 0, nil))
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransaction", tx.Hash().BytesLE())
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
})
}
func TestLedgerGetTransactionFromBlock(t *testing.T) {
chain := newTestChain(t)
defer chain.Close()
ledger := chain.contracts.ByName(nativenames.Ledger).Metadata().Hash
res, err := invokeContractMethod(chain, 100000000, ledger, "currentIndex") // adds a block
require.NoError(t, err)
checkResult(t, res, stackitem.Make(0))
bhash := chain.GetHeaderHash(1)
b, err := chain.GetBlock(bhash)
require.NoError(t, err)
t.Run("success", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionFromBlock", bhash.BytesBE(), int64(0))
require.NoError(t, err)
require.Equal(t, vm.HaltState, res.VMState, res.FaultException)
require.Equal(t, 1, len(res.Stack))
value := res.Stack[0].Value()
actual, ok := value.([]stackitem.Item)
require.True(t, ok)
require.Equal(t, b.Transactions[0].Hash().BytesBE(), actual[0].Value().([]byte))
})
t.Run("bad transaction index", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionFromBlock", bhash.BytesBE(), int64(1))
require.NoError(t, err)
checkFAULTState(t, res)
})
t.Run("invalid block hash (>int64)", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionFromBlock", bhash.BytesBE()[:10], int64(0))
require.NoError(t, err)
checkFAULTState(t, res)
})
t.Run("invalid block hash (int64)", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionFromBlock", bhash.BytesBE()[:6], int64(0))
require.NoError(t, err)
checkFAULTState(t, res)
})
t.Run("bad block hash", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionFromBlock", bhash.BytesLE(), int64(0))
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
})
t.Run("isn't traceable", func(t *testing.T) {
b.Index = chain.BlockHeight() + 1
require.NoError(t, chain.dao.StoreAsBlock(b, nil))
res, err := invokeContractMethod(chain, 100000000, ledger, "getTransactionFromBlock", bhash.BytesBE(), int64(0))
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
})
}
func TestLedgerGetBlock(t *testing.T) {
chain := newTestChain(t)
defer chain.Close()
ledger := chain.contracts.ByName(nativenames.Ledger).Metadata().Hash
bhash := chain.GetHeaderHash(0)
res, err := invokeContractMethod(chain, 100000000, ledger, "currentHash") // adds a block
require.NoError(t, err)
checkResult(t, res, stackitem.Make(bhash.BytesBE()))
bhash = chain.GetHeaderHash(1)
b, err := chain.GetBlock(bhash)
require.NoError(t, err)
t.Run("success", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getBlock", bhash.BytesBE())
require.NoError(t, err)
require.Equal(t, vm.HaltState, res.VMState, res.FaultException)
require.Equal(t, 1, len(res.Stack))
value := res.Stack[0].Value()
actual, ok := value.([]stackitem.Item)
require.True(t, ok)
require.Equal(t, 8, len(actual))
require.Equal(t, b.Hash().BytesBE(), actual[0].Value().([]byte))
require.Equal(t, int64(b.Version), actual[1].Value().(*big.Int).Int64())
require.Equal(t, b.PrevHash.BytesBE(), actual[2].Value().([]byte))
require.Equal(t, b.MerkleRoot.BytesBE(), actual[3].Value().([]byte))
require.Equal(t, int64(b.Timestamp), actual[4].Value().(*big.Int).Int64())
require.Equal(t, int64(b.Index), actual[5].Value().(*big.Int).Int64())
require.Equal(t, b.NextConsensus.BytesBE(), actual[6].Value().([]byte))
require.Equal(t, int64(len(b.Transactions)), actual[7].Value().(*big.Int).Int64())
})
t.Run("bad hash", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, ledger, "getBlock", bhash.BytesLE())
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
})
t.Run("isn't traceable", func(t *testing.T) {
b.Index = chain.BlockHeight() + 1
require.NoError(t, chain.dao.StoreAsBlock(b, nil))
res, err := invokeContractMethod(chain, 100000000, ledger, "getBlock", bhash.BytesBE())
require.NoError(t, err)
checkResult(t, res, stackitem.Null{})
})
}

View file

@ -1,96 +0,0 @@
/*
Package blockchain provides functions to access various blockchain data.
*/
package blockchain
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
)
// Transaction represents a NEO transaction. It's similar to Transaction class
// in Neo .net framework.
type Transaction struct {
// Hash represents the hash (256 bit BE value in a 32 byte slice) of the
// given transaction (which also is its ID).
Hash interop.Hash256
// Version represents the transaction version.
Version int
// Nonce is a random number to avoid hash collision.
Nonce int
// Sender represents the sender (160 bit BE value in a 20 byte slice) of the
// given Transaction.
Sender interop.Hash160
// SysFee represents fee to be burned.
SysFee int
// NetFee represents fee to be distributed to consensus nodes.
NetFee int
// ValidUntilBlock is the maximum blockchain height exceeding which
// transaction should fail verification.
ValidUntilBlock int
// Script represents code to run in NeoVM for this transaction.
Script []byte
}
// Block represents a NEO block, it's a data structure that you can get
// block-related data from. It's similar to the Block class in the Neo .net
// framework. To use it you need to get it via GetBlock function call.
type Block struct {
// Hash represents the hash (256 bit BE value in a 32 byte slice) of the
// given block.
Hash interop.Hash256
// Version of the block.
Version int
// PrevHash represents the hash (256 bit BE value in a 32 byte slice) of the
// previous block.
PrevHash interop.Hash256
// MerkleRoot represents the root hash (256 bit BE value in a 32 byte slice)
// of a transaction list.
MerkleRoot interop.Hash256
// Timestamp represents millisecond-precision block timestamp.
Timestamp int
// Index represents the height of the block.
Index int
// NextConsensus represents contract address of the next miner (160 bit BE
// value in a 20 byte slice).
NextConsensus interop.Hash160
// TransactionsLength represents the length of block's transactions array.
TransactionsLength int
}
// GetHeight returns current block height (index of the last accepted block).
// Note that when transaction is being run as a part of new block this block is
// considered as not yet accepted (persisted) and thus you'll get an index of
// the previous (already accepted) block. This function uses
// `System.Blockchain.GetHeight` syscall.
func GetHeight() int {
return 0
}
// GetBlock returns block found by the given hash or index (with the same
// encoding as for GetHeader). This function uses `System.Blockchain.GetBlock`
// syscall.
func GetBlock(heightOrHash interface{}) *Block {
return &Block{}
}
// GetTransaction returns transaction found by the given hash (256 bit in BE
// format represented as a slice of 32 bytes). This function uses
// `System.Blockchain.GetTransaction` syscall.
func GetTransaction(hash interop.Hash256) *Transaction {
return &Transaction{}
}
// GetTransactionFromBlock returns transaction hash (256 bit in BE format
// represented as a slice of 32 bytes) from the block found by the given hash or
// index (with the same encoding as for GetHeader) by its index. This
// function uses `System.Blockchain.GetTransactionFromBlock` syscall.
func GetTransactionFromBlock(heightOrHash interface{}, index int) interop.Hash256 {
return nil
}
// GetTransactionHeight returns transaction's height (index of the block that
// includes it) by the given ID (256 bit in BE format represented as a slice of
// 32 bytes). This function uses `System.Blockchain.GetTransactionHeight` syscall.
func GetTransactionHeight(hash interop.Hash256) int {
return 0
}

View file

@ -2,15 +2,38 @@ package runtime
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/blockchain"
)
// Transaction represents a NEO transaction. It's similar to Transaction class
// in Neo .net framework.
type Transaction struct {
// Hash represents the hash (256 bit BE value in a 32 byte slice) of the
// given transaction (which also is its ID).
Hash interop.Hash256
// Version represents the transaction version.
Version int
// Nonce is a random number to avoid hash collision.
Nonce int
// Sender represents the sender (160 bit BE value in a 20 byte slice) of the
// given Transaction.
Sender interop.Hash160
// SysFee represents fee to be burned.
SysFee int
// NetFee represents fee to be distributed to consensus nodes.
NetFee int
// ValidUntilBlock is the maximum blockchain height exceeding which
// transaction should fail verification.
ValidUntilBlock int
// Script represents code to run in NeoVM for this transaction.
Script []byte
}
// GetScriptContainer returns the transaction that initially triggered current
// execution context. It never changes in a single execution, no matter how deep
// this execution goes. This function uses
// `System.Runtime.GetScriptContainer` syscall.
func GetScriptContainer() *blockchain.Transaction {
return &blockchain.Transaction{}
func GetScriptContainer() *Transaction {
return &Transaction{}
}
// GetExecutingScriptHash returns script hash (160 bit in BE form represented

View file

@ -28,9 +28,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io"
rpc2 "github.com/nspcc-dev/neo-go/pkg/services/oracle/broadcaster"
"github.com/nspcc-dev/neo-go/pkg/rpc/response"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
rpc2 "github.com/nspcc-dev/neo-go/pkg/services/oracle/broadcaster"
"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"
@ -59,7 +59,7 @@ type rpcTestCase struct {
}
const testContractHash = "c6436aab21ebd15279b85af8d7b5808d38455b0a"
const deploymentTxHash = "e6ffce4533231c4efdea9a65c7abc0e7073d96a4ebc66f402db3a84b6f8939ef"
const deploymentTxHash = "9a9d6b0876d1e6cfd68efadd0facaaba7e07efbe7b24282d094a0893645581f3"
const genesisBlockHash = "0542f4350c6e236d0509bcd98188b0034bfbecc1a0c7fcdb8e4295310d468b70"
const verifyContractHash = "03ffc0897543b9b709e0f8cab4a7682dae0ba943"
@ -182,7 +182,7 @@ var rpcTestCases = map[string][]rpcTestCase{
check: func(t *testing.T, e *executor, cs interface{}) {
res, ok := cs.(*state.Contract)
require.True(t, ok)
assert.Equal(t, int32(-4), res.ID)
assert.Equal(t, int32(-5), res.ID)
},
},
{
@ -192,7 +192,7 @@ var rpcTestCases = map[string][]rpcTestCase{
},
{
name: "negative, bad ID",
params: `[-8]`,
params: `[-100]`,
fail: true,
},
{

Binary file not shown.