Merge pull request #418 from nspcc-dev/various-verification-fixes2

Transaction verification fixes, interops and block verification. Fixes #12.
This commit is contained in:
Roman Khimov 2019-10-15 19:11:00 +03:00 committed by GitHub
commit 4822c736bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 3022 additions and 538 deletions

View file

@ -49,6 +49,10 @@ type (
StandbyValidators []string `yaml:"StandbyValidators"` StandbyValidators []string `yaml:"StandbyValidators"`
SeedList []string `yaml:"SeedList"` SeedList []string `yaml:"SeedList"`
SystemFee SystemFee `yaml:"SystemFee"` SystemFee SystemFee `yaml:"SystemFee"`
// Whether to verify received blocks.
VerifyBlocks bool `yaml:"VerifyBlocks"`
// Whether to verify transactions in received blocks.
VerifyTransactions bool `yaml:"VerifyTransactions"`
} }
// SystemFee fees related to system. // SystemFee fees related to system.

View file

@ -27,6 +27,8 @@ ProtocolConfiguration:
IssueTransaction: 500 IssueTransaction: 500
PublishTransaction: 500 PublishTransaction: 500
RegisterTransaction: 10000 RegisterTransaction: 10000
VerifyBlocks: true
VerifyTransactions: false
ApplicationConfiguration: ApplicationConfiguration:
DBConfiguration: DBConfiguration:

View file

@ -15,6 +15,8 @@ ProtocolConfiguration:
IssueTransaction: 500 IssueTransaction: 500
PublishTransaction: 500 PublishTransaction: 500
RegisterTransaction: 10000 RegisterTransaction: 10000
VerifyBlocks: true
VerifyTransactions: true
ApplicationConfiguration: ApplicationConfiguration:
DBConfiguration: DBConfiguration:

View file

@ -12,6 +12,8 @@ ProtocolConfiguration:
IssueTransaction: 500 IssueTransaction: 500
PublishTransaction: 500 PublishTransaction: 500
RegisterTransaction: 10000 RegisterTransaction: 10000
VerifyBlocks: true
VerifyTransactions: true
ApplicationConfiguration: ApplicationConfiguration:
DBConfiguration: DBConfiguration:

View file

@ -12,6 +12,8 @@ ProtocolConfiguration:
IssueTransaction: 500 IssueTransaction: 500
PublishTransaction: 500 PublishTransaction: 500
RegisterTransaction: 10000 RegisterTransaction: 10000
VerifyBlocks: true
VerifyTransactions: true
ApplicationConfiguration: ApplicationConfiguration:
DBConfiguration: DBConfiguration:

View file

@ -12,6 +12,8 @@ ProtocolConfiguration:
IssueTransaction: 500 IssueTransaction: 500
PublishTransaction: 500 PublishTransaction: 500
RegisterTransaction: 10000 RegisterTransaction: 10000
VerifyBlocks: true
VerifyTransactions: true
ApplicationConfiguration: ApplicationConfiguration:
DBConfiguration: DBConfiguration:

View file

@ -18,6 +18,8 @@ ProtocolConfiguration:
IssueTransaction: 500 IssueTransaction: 500
PublishTransaction: 500 PublishTransaction: 500
RegisterTransaction: 10000 RegisterTransaction: 10000
VerifyBlocks: true
VerifyTransactions: true
ApplicationConfiguration: ApplicationConfiguration:
DBConfiguration: DBConfiguration:

View file

@ -27,6 +27,8 @@ ProtocolConfiguration:
IssueTransaction: 5 IssueTransaction: 5
PublishTransaction: 5 PublishTransaction: 5
RegisterTransaction: 100 RegisterTransaction: 100
VerifyBlocks: true
VerifyTransactions: false
ApplicationConfiguration: ApplicationConfiguration:
DBConfiguration: DBConfiguration:

View file

@ -17,6 +17,8 @@ ProtocolConfiguration:
IssueTransaction: 500 IssueTransaction: 500
PublishTransaction: 500 PublishTransaction: 500
RegisterTransaction: 10000 RegisterTransaction: 10000
VerifyBlocks: true
VerifyTransactions: true
ApplicationConfiguration: ApplicationConfiguration:
DBConfiguration: DBConfiguration:

View file

@ -27,6 +27,17 @@ func (a Assets) commit(b storage.Batch) error {
return nil return nil
} }
// putAssetStateIntoStore puts given asset state into the given store.
func putAssetStateIntoStore(s storage.Store, as *AssetState) error {
buf := io.NewBufBinWriter()
as.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return buf.Err
}
key := storage.AppendPrefix(storage.STAsset, as.ID.Bytes())
return s.Put(key, buf.Bytes())
}
// AssetState represents the state of an NEO registered Asset. // AssetState represents the state of an NEO registered Asset.
type AssetState struct { type AssetState struct {
ID util.Uint256 ID util.Uint256

View file

@ -3,6 +3,7 @@ package core
import ( import (
"testing" "testing"
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
@ -35,3 +36,25 @@ func TestEncodeDecodeAssetState(t *testing.T) {
assert.Nil(t, r.Err) assert.Nil(t, r.Err)
assert.Equal(t, asset, assetDecode) assert.Equal(t, asset, assetDecode)
} }
func TestPutGetAssetState(t *testing.T) {
s := storage.NewMemoryStore()
asset := &AssetState{
ID: randomUint256(),
AssetType: transaction.Token,
Name: "super cool token",
Amount: util.Fixed8(1000000),
Available: util.Fixed8(100),
Precision: 8,
FeeMode: feeMode,
Owner: &keys.PublicKey{},
Admin: randomUint160(),
Issuer: randomUint160(),
Expiration: 10,
IsFrozen: false,
}
assert.NoError(t, putAssetStateIntoStore(s, asset))
asRead := getAssetStateFromStore(s, asset.ID)
assert.NotNil(t, asRead)
assert.Equal(t, asset, asRead)
}

View file

@ -1,12 +1,14 @@
package core package core
import ( import (
"errors"
"fmt"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto" "github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/Workiva/go-datastructures/queue" "github.com/Workiva/go-datastructures/queue"
log "github.com/sirupsen/logrus"
) )
// Block represents one block in the chain. // Block represents one block in the chain.
@ -28,14 +30,18 @@ func (b *Block) Header() *Header {
} }
} }
// rebuildMerkleRoot rebuild the merkleroot of the block. func merkleTreeFromTransactions(txes []*transaction.Transaction) (*crypto.MerkleTree, error) {
func (b *Block) rebuildMerkleRoot() error { hashes := make([]util.Uint256, len(txes))
hashes := make([]util.Uint256, len(b.Transactions)) for i, tx := range txes {
for i, tx := range b.Transactions {
hashes[i] = tx.Hash() hashes[i] = tx.Hash()
} }
merkle, err := crypto.NewMerkleTree(hashes) return crypto.NewMerkleTree(hashes)
}
// rebuildMerkleRoot rebuild the merkleroot of the block.
func (b *Block) rebuildMerkleRoot() error {
merkle, err := merkleTreeFromTransactions(b.Transactions)
if err != nil { if err != nil {
return err return err
} }
@ -45,26 +51,29 @@ func (b *Block) rebuildMerkleRoot() error {
} }
// Verify the integrity of the block. // Verify the integrity of the block.
func (b *Block) Verify(full bool) bool { func (b *Block) Verify() error {
// There has to be some transaction inside. // There has to be some transaction inside.
if len(b.Transactions) == 0 { if len(b.Transactions) == 0 {
return false return errors.New("no transactions")
} }
// The first TX has to be a miner transaction. // The first TX has to be a miner transaction.
if b.Transactions[0].Type != transaction.MinerType { if b.Transactions[0].Type != transaction.MinerType {
return false return fmt.Errorf("the first transaction is %s", b.Transactions[0].Type)
} }
// If the first TX is a minerTX then all others cant. // If the first TX is a minerTX then all others cant.
for _, tx := range b.Transactions[1:] { for _, tx := range b.Transactions[1:] {
if tx.Type == transaction.MinerType { if tx.Type == transaction.MinerType {
return false return fmt.Errorf("miner transaction %s is not the first one", tx.Hash().ReverseString())
} }
} }
// TODO: When full is true, do a full verification. merkle, err := merkleTreeFromTransactions(b.Transactions)
if full { if err != nil {
log.Warn("full verification of blocks is not yet implemented") return err
} }
return true if !b.MerkleRoot.Equals(merkle.Root()) {
return errors.New("MerkleRoot mismatch")
}
return nil
} }
// NewBlockFromTrimmedBytes returns a new block from trimmed data. // NewBlockFromTrimmedBytes returns a new block from trimmed data.

View file

@ -40,8 +40,11 @@ type BlockBase struct {
// Script used to validate the block // Script used to validate the block
Script *transaction.Witness `json:"script"` Script *transaction.Witness `json:"script"`
// hash of this block, created when binary encoded. // Hash of this block, created when binary encoded (double SHA256).
hash util.Uint256 hash util.Uint256
// Hash of the block used to verify it (single SHA256).
verificationHash util.Uint256
} }
// Verify verifies the integrity of the BlockBase. // Verify verifies the integrity of the BlockBase.
@ -58,6 +61,16 @@ func (b *BlockBase) Hash() util.Uint256 {
return b.hash return b.hash
} }
// VerificationHash returns the hash of the block used to verify it.
func (b *BlockBase) VerificationHash() util.Uint256 {
if b.verificationHash.Equals(util.Uint256{}) {
if b.createHash() != nil {
panic("failed to compute hash!")
}
}
return b.verificationHash
}
// DecodeBinary implements Serializable interface. // DecodeBinary implements Serializable interface.
func (b *BlockBase) DecodeBinary(br *io.BinReader) { func (b *BlockBase) DecodeBinary(br *io.BinReader) {
b.decodeHashableFields(br) b.decodeHashableFields(br)
@ -80,6 +93,16 @@ func (b *BlockBase) EncodeBinary(bw *io.BinWriter) {
b.Script.EncodeBinary(bw) b.Script.EncodeBinary(bw)
} }
// getHashableData returns serialized hashable data of the block.
func (b *BlockBase) getHashableData() ([]byte, error) {
buf := io.NewBufBinWriter()
b.encodeHashableFields(buf.BinWriter)
if buf.Err != nil {
return nil, buf.Err
}
return buf.Bytes(), nil
}
// createHash creates the hash of the block. // createHash creates the hash of the block.
// When calculating the hash value of the block, instead of calculating the entire block, // When calculating the hash value of the block, instead of calculating the entire block,
// only first seven fields in the block head will be calculated, which are // only first seven fields in the block head will be calculated, which are
@ -87,12 +110,12 @@ func (b *BlockBase) EncodeBinary(bw *io.BinWriter) {
// Since MerkleRoot already contains the hash value of all transactions, // Since MerkleRoot already contains the hash value of all transactions,
// the modification of transaction will influence the hash value of the block. // the modification of transaction will influence the hash value of the block.
func (b *BlockBase) createHash() error { func (b *BlockBase) createHash() error {
buf := io.NewBufBinWriter() bb, err := b.getHashableData()
b.encodeHashableFields(buf.BinWriter) if err != nil {
if buf.Err != nil { return err
return buf.Err
} }
b.hash = hash.DoubleSha256(buf.Bytes()) b.hash = hash.DoubleSha256(bb)
b.verificationHash = hash.Sha256(bb)
return nil return nil
} }

View file

@ -6,6 +6,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto" "github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -76,30 +77,59 @@ func TestTrimmedBlock(t *testing.T) {
} }
} }
func newDumbBlock() *Block {
return &Block{
BlockBase: BlockBase{
Version: 0,
PrevHash: hash.Sha256([]byte("a")),
MerkleRoot: hash.Sha256([]byte("b")),
Timestamp: uint32(100500),
Index: 1,
ConsensusData: 1111,
NextConsensus: hash.Hash160([]byte("a")),
Script: &transaction.Witness{
VerificationScript: []byte{0x51}, // PUSH1
InvocationScript: []byte{0x61}, // NOP
},
},
Transactions: []*transaction.Transaction{
{Type: transaction.MinerType},
{Type: transaction.IssueType},
},
}
}
func TestHashBlockEqualsHashHeader(t *testing.T) { func TestHashBlockEqualsHashHeader(t *testing.T) {
block := newBlock(0) block := newDumbBlock()
assert.Equal(t, block.Hash(), block.Header().Hash()) assert.Equal(t, block.Hash(), block.Header().Hash())
} }
func TestBlockVerify(t *testing.T) { func TestBlockVerify(t *testing.T) {
block := newBlock( block := newDumbBlock()
0, assert.NotNil(t, block.Verify())
newTX(transaction.MinerType), assert.Nil(t, block.rebuildMerkleRoot())
newTX(transaction.IssueType), assert.Nil(t, block.Verify())
)
assert.True(t, block.Verify(false))
block.Transactions = []*transaction.Transaction{ block.Transactions = []*transaction.Transaction{
{Type: transaction.IssueType}, {Type: transaction.IssueType},
{Type: transaction.MinerType}, {Type: transaction.MinerType},
} }
assert.False(t, block.Verify(false)) assert.NoError(t, block.rebuildMerkleRoot())
assert.NotNil(t, block.Verify())
block.Transactions = []*transaction.Transaction{ block.Transactions = []*transaction.Transaction{
{Type: transaction.MinerType}, {Type: transaction.MinerType},
{Type: transaction.MinerType}, {Type: transaction.MinerType},
} }
assert.False(t, block.Verify(false)) assert.NoError(t, block.rebuildMerkleRoot())
assert.NotNil(t, block.Verify())
block.Transactions = []*transaction.Transaction{
{Type: transaction.MinerType},
{Type: transaction.IssueType},
{Type: transaction.IssueType},
}
assert.NotNil(t, block.Verify())
} }
func TestBinBlockDecodeEncode(t *testing.T) { func TestBinBlockDecodeEncode(t *testing.T) {

View file

@ -13,6 +13,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm" "github.com/CityOfZion/neo-go/pkg/vm"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -67,9 +68,6 @@ type Blockchain struct {
headersOp chan headersOpFunc headersOp chan headersOpFunc
headersOpDone chan struct{} headersOpDone chan struct{}
// Whether we will verify received blocks.
verifyBlocks bool
memPool MemPool memPool MemPool
} }
@ -84,7 +82,6 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration) (*Blockcha
memStore: storage.NewMemoryStore(), memStore: storage.NewMemoryStore(),
headersOp: make(chan headersOpFunc), headersOp: make(chan headersOpFunc),
headersOpDone: make(chan struct{}), headersOpDone: make(chan struct{}),
verifyBlocks: false,
memPool: NewMemPool(50000), memPool: NewMemPool(50000),
} }
@ -208,14 +205,20 @@ func (bc *Blockchain) AddBlock(block *Block) error {
if expectedHeight != block.Index { if expectedHeight != block.Index {
return fmt.Errorf("expected block %d, but passed block %d", expectedHeight, block.Index) return fmt.Errorf("expected block %d, but passed block %d", expectedHeight, block.Index)
} }
if bc.verifyBlocks { if bc.config.VerifyBlocks {
if !block.Verify(false) { err := block.Verify()
return fmt.Errorf("block %s is invalid", block.Hash()) if err == nil {
err = bc.VerifyBlock(block)
} }
for _, tx := range block.Transactions { if err != nil {
err := bc.Verify(tx) return fmt.Errorf("block %s is invalid: %s", block.Hash().ReverseString(), err)
if err != nil { }
return fmt.Errorf("transaction %s failed to verify: %s", tx.Hash().ReverseString(), err) if bc.config.VerifyTransactions {
for _, tx := range block.Transactions {
err := bc.VerifyTx(tx, block)
if err != nil {
return fmt.Errorf("transaction %s failed to verify: %s", tx.Hash().ReverseString(), err)
}
} }
} }
} }
@ -238,6 +241,7 @@ func (bc *Blockchain) AddHeaders(headers ...*Header) (err error) {
) )
bc.headersOp <- func(headerList *HeaderHashList) { bc.headersOp <- func(headerList *HeaderHashList) {
oldlen := headerList.Len()
for _, h := range headers { for _, h := range headers {
if int(h.Index-1) >= headerList.Len() { if int(h.Index-1) >= headerList.Len() {
err = fmt.Errorf( err = fmt.Errorf(
@ -258,7 +262,7 @@ func (bc *Blockchain) AddHeaders(headers ...*Header) (err error) {
} }
} }
if batch.Len() > 0 { if oldlen != headerList.Len() {
if err = bc.memStore.PutBatch(batch); err != nil { if err = bc.memStore.PutBatch(batch); err != nil {
return return
} }
@ -389,11 +393,15 @@ func (bc *Blockchain) storeBlock(block *Block) error {
case *transaction.EnrollmentTX: case *transaction.EnrollmentTX:
case *transaction.StateTX: case *transaction.StateTX:
case *transaction.PublishTX: case *transaction.PublishTX:
var properties smartcontract.PropertyState
if t.NeedStorage {
properties |= smartcontract.HasStorage
}
contract := &ContractState{ contract := &ContractState{
Script: t.Script, Script: t.Script,
ParamList: t.ParamList, ParamList: t.ParamList,
ReturnType: t.ReturnType, ReturnType: t.ReturnType,
HasStorage: t.NeedStorage, Properties: properties,
Name: t.Name, Name: t.Name,
CodeVersion: t.CodeVersion, CodeVersion: t.CodeVersion,
Author: t.Author, Author: t.Author,
@ -403,6 +411,32 @@ func (bc *Blockchain) storeBlock(block *Block) error {
contracts[contract.ScriptHash()] = contract contracts[contract.ScriptHash()] = contract
case *transaction.InvocationTX: case *transaction.InvocationTX:
vm := vm.New(vm.ModeMute)
vm.SetCheckedHash(tx.VerificationHash().Bytes())
vm.SetScriptGetter(func(hash util.Uint160) []byte {
cs := bc.GetContractState(hash)
if cs == nil {
return nil
}
return cs.Script
})
systemInterop := newInteropContext(0x10, bc, block, tx)
vm.RegisterInteropFuncs(systemInterop.getSystemInteropMap())
vm.RegisterInteropFuncs(systemInterop.getNeoInteropMap())
vm.LoadScript(t.Script)
vm.Run()
if !vm.HasFailed() {
_, err := systemInterop.mem.Persist(bc.memStore)
if err != nil {
return errors.Wrap(err, "failed to persist invocation results")
}
} else {
log.WithFields(log.Fields{
"tx": tx.Hash().ReverseString(),
"block": block.Index,
}).Warn("contract invocation failed")
}
} }
} }
@ -455,7 +489,7 @@ func (bc *Blockchain) persist(ctx context.Context) error {
"persistedKeys": persisted, "persistedKeys": persisted,
"headerHeight": bc.HeaderHeight(), "headerHeight": bc.HeaderHeight(),
"blockHeight": bc.BlockHeight(), "blockHeight": bc.BlockHeight(),
"persistedHeight": bc.persistedHeight, "persistedHeight": atomic.LoadUint32(&bc.persistedHeight),
"took": time.Since(start), "took": time.Since(start),
}).Info("blockchain persist completed") }).Info("blockchain persist completed")
} }
@ -505,6 +539,43 @@ func getTransactionFromStore(s storage.Store, hash util.Uint256) (*transaction.T
return tx, height, nil return tx, height, nil
} }
// GetStorageItem returns an item from storage.
func (bc *Blockchain) GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem {
sItem := getStorageItemFromStore(bc.memStore, scripthash, key)
if sItem == nil {
sItem = getStorageItemFromStore(bc.Store, scripthash, key)
}
return sItem
}
// GetStorageItems returns all storage items for a given scripthash.
func (bc *Blockchain) GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) {
var siMap = make(map[string]*StorageItem)
var err error
saveToMap := func(k, v []byte) {
if err != nil {
return
}
r := io.NewBinReaderFromBuf(v)
si := &StorageItem{}
si.DecodeBinary(r)
if r.Err != nil {
err = r.Err
return
}
// Cut prefix and hash.
siMap[string(k[21:])] = si
}
bc.memStore.Seek(storage.AppendPrefix(storage.STStorage, hash.BytesReverse()), saveToMap)
bc.Store.Seek(storage.AppendPrefix(storage.STStorage, hash.BytesReverse()), saveToMap)
if err != nil {
return nil, err
}
return siMap, nil
}
// GetBlock returns a Block by the given hash. // GetBlock returns a Block by the given hash.
func (bc *Blockchain) GetBlock(hash util.Uint256) (*Block, error) { func (bc *Blockchain) GetBlock(hash util.Uint256) (*Block, error) {
block, err := getBlockFromStore(bc.memStore, hash) block, err := getBlockFromStore(bc.memStore, hash)
@ -517,6 +588,13 @@ func (bc *Blockchain) GetBlock(hash util.Uint256) (*Block, error) {
if len(block.Transactions) == 0 { if len(block.Transactions) == 0 {
return nil, fmt.Errorf("only header is available") return nil, fmt.Errorf("only header is available")
} }
for _, tx := range block.Transactions {
stx, _, err := bc.GetTransaction(tx.Hash())
if err != nil {
return nil, err
}
*tx = *stx
}
return block, nil return block, nil
} }
@ -689,6 +767,15 @@ func (bc *Blockchain) GetAccountState(scriptHash util.Uint160) *AccountState {
return as return as
} }
// GetUnspentCoinState returns unspent coin state for given tx hash.
func (bc *Blockchain) GetUnspentCoinState(hash util.Uint256) *UnspentCoinState {
ucs, err := getUnspentCoinStateFromStore(bc.memStore, hash)
if err != nil {
ucs, _ = getUnspentCoinStateFromStore(bc.Store, hash)
}
return ucs
}
// GetConfig returns the config stored in the blockchain // GetConfig returns the config stored in the blockchain
func (bc *Blockchain) GetConfig() config.ProtocolConfiguration { func (bc *Blockchain) GetConfig() config.ProtocolConfiguration {
return bc.config return bc.config
@ -756,9 +843,26 @@ func (bc *Blockchain) GetMemPool() MemPool {
return bc.memPool return bc.memPool
} }
// Verify verifies whether a transaction is bonafide or not. // VerifyBlock verifies block against its current state.
func (bc *Blockchain) VerifyBlock(block *Block) error {
prevHeader, err := bc.GetHeader(block.PrevHash)
if err != nil {
return errors.Wrap(err, "unable to get previous header")
}
if prevHeader.Index+1 != block.Index {
return errors.New("previous header index doesn't match")
}
if prevHeader.Timestamp >= block.Timestamp {
return errors.New("block is not newer than the previous one")
}
return bc.verifyBlockWitnesses(block, prevHeader)
}
// VerifyTx verifies whether a transaction is bonafide or not. Block parameter
// is used for easy interop access and can be omitted for transactions that are
// not yet added into any block.
// Golang implementation of Verify method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L270). // Golang implementation of Verify method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L270).
func (bc *Blockchain) Verify(t *transaction.Transaction) error { func (bc *Blockchain) VerifyTx(t *transaction.Transaction, block *Block) error {
if io.GetVarSize(t) > transaction.MaxTransactionSize { if io.GetVarSize(t) > transaction.MaxTransactionSize {
return errors.Errorf("invalid transaction size = %d. It shoud be less then MaxTransactionSize = %d", io.GetVarSize(t), transaction.MaxTransactionSize) return errors.Errorf("invalid transaction size = %d. It shoud be less then MaxTransactionSize = %d", io.GetVarSize(t), transaction.MaxTransactionSize)
} }
@ -771,11 +875,11 @@ func (bc *Blockchain) Verify(t *transaction.Transaction) error {
if IsDoubleSpend(bc.Store, t) { if IsDoubleSpend(bc.Store, t) {
return errors.New("invalid transaction caused by double spending") return errors.New("invalid transaction caused by double spending")
} }
if ok := bc.verifyOutputs(t); !ok { if err := bc.verifyOutputs(t); err != nil {
return errors.New("invalid transaction's outputs") return errors.Wrap(err, "wrong outputs")
} }
if ok := bc.verifyResults(t); !ok { if err := bc.verifyResults(t); err != nil {
return errors.New("invalid transaction's results") return err
} }
for _, a := range t.Attributes { for _, a := range t.Attributes {
@ -784,7 +888,7 @@ func (bc *Blockchain) Verify(t *transaction.Transaction) error {
} }
} }
return bc.VerifyWitnesses(t) return bc.verifyTxWitnesses(t, block)
} }
func (bc *Blockchain) verifyInputs(t *transaction.Transaction) bool { func (bc *Blockchain) verifyInputs(t *transaction.Transaction) bool {
@ -799,31 +903,31 @@ func (bc *Blockchain) verifyInputs(t *transaction.Transaction) bool {
return true return true
} }
func (bc *Blockchain) verifyOutputs(t *transaction.Transaction) bool { func (bc *Blockchain) verifyOutputs(t *transaction.Transaction) error {
for assetID, outputs := range t.GroupOutputByAssetID() { for assetID, outputs := range t.GroupOutputByAssetID() {
assetState := bc.GetAssetState(assetID) assetState := bc.GetAssetState(assetID)
if assetState == nil { if assetState == nil {
return false return fmt.Errorf("no asset state for %s", assetID.ReverseString())
} }
if assetState.Expiration < bc.blockHeight+1 && assetState.AssetType != transaction.GoverningToken && assetState.AssetType != transaction.UtilityToken { if assetState.Expiration < bc.blockHeight+1 && assetState.AssetType != transaction.GoverningToken && assetState.AssetType != transaction.UtilityToken {
return false return fmt.Errorf("asset %s expired", assetID.ReverseString())
} }
for _, out := range outputs { for _, out := range outputs {
if int64(out.Amount)%int64(math.Pow10(8-int(assetState.Precision))) != 0 { if int64(out.Amount)%int64(math.Pow10(8-int(assetState.Precision))) != 0 {
return false return fmt.Errorf("output is not compliant with %s asset precision", assetID.ReverseString())
} }
} }
} }
return true return nil
} }
func (bc *Blockchain) verifyResults(t *transaction.Transaction) bool { func (bc *Blockchain) verifyResults(t *transaction.Transaction) error {
results := bc.GetTransationResults(t) results := bc.GetTransactionResults(t)
if results == nil { if results == nil {
return false return errors.New("tx has no results")
} }
var resultsDestroy []*transaction.Result var resultsDestroy []*transaction.Result
var resultsIssue []*transaction.Result var resultsIssue []*transaction.Result
@ -837,43 +941,49 @@ func (bc *Blockchain) verifyResults(t *transaction.Transaction) bool {
} }
} }
if len(resultsDestroy) > 1 { if len(resultsDestroy) > 1 {
return false return errors.New("tx has more than 1 destroy output")
} }
if len(resultsDestroy) == 1 && resultsDestroy[0].AssetID != utilityTokenTX().Hash() { if len(resultsDestroy) == 1 && resultsDestroy[0].AssetID != utilityTokenTX().Hash() {
return false return errors.New("tx destroys non-utility token")
} }
if bc.SystemFee(t).GreaterThan(util.Fixed8(0)) && (len(resultsDestroy) == 0 || resultsDestroy[0].Amount.LessThan(bc.SystemFee(t))) { sysfee := bc.SystemFee(t)
return false if sysfee.GreaterThan(util.Fixed8(0)) {
if len(resultsDestroy) == 0 {
return fmt.Errorf("system requires to pay %s fee, but tx pays nothing", sysfee.String())
}
if resultsDestroy[0].Amount.LessThan(sysfee) {
return fmt.Errorf("system requires to pay %s fee, but tx pays %s only", sysfee.String(), resultsDestroy[0].Amount.String())
}
} }
switch t.Type { switch t.Type {
case transaction.MinerType, transaction.ClaimType: case transaction.MinerType, transaction.ClaimType:
for _, r := range resultsIssue { for _, r := range resultsIssue {
if r.AssetID != utilityTokenTX().Hash() { if r.AssetID != utilityTokenTX().Hash() {
return false return errors.New("miner or claim tx issues non-utility tokens")
} }
} }
break break
case transaction.IssueType: case transaction.IssueType:
for _, r := range resultsIssue { for _, r := range resultsIssue {
if r.AssetID == utilityTokenTX().Hash() { if r.AssetID == utilityTokenTX().Hash() {
return false return errors.New("issue tx issues utility tokens")
} }
} }
break break
default: default:
if len(resultsIssue) > 0 { if len(resultsIssue) > 0 {
return false return errors.New("non issue/miner/claim tx issues tokens")
} }
break break
} }
return true return nil
} }
// GetTransationResults returns the transaction results aggregate by assetID. // GetTransactionResults returns the transaction results aggregate by assetID.
// Golang of GetTransationResults method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L207) // Golang of GetTransationResults method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L207)
func (bc *Blockchain) GetTransationResults(t *transaction.Transaction) []*transaction.Result { func (bc *Blockchain) GetTransactionResults(t *transaction.Transaction) []*transaction.Result {
var tempResults []*transaction.Result var tempResults []*transaction.Result
var results []*transaction.Result var results []*transaction.Result
tempGroupResult := make(map[util.Uint256]util.Fixed8) tempGroupResult := make(map[util.Uint256]util.Fixed8)
@ -918,7 +1028,8 @@ func (bc *Blockchain) GetTransationResults(t *transaction.Transaction) []*transa
// GetScriptHashesForVerifyingClaim returns all ScriptHashes of Claim transaction // GetScriptHashesForVerifyingClaim returns all ScriptHashes of Claim transaction
// which has a different implementation from generic GetScriptHashesForVerifying. // which has a different implementation from generic GetScriptHashesForVerifying.
func (bc *Blockchain) GetScriptHashesForVerifyingClaim(t *transaction.Transaction) ([]util.Uint160, error) { func (bc *Blockchain) GetScriptHashesForVerifyingClaim(t *transaction.Transaction) ([]util.Uint160, error) {
hashes := make([]util.Uint160, 0) // Avoiding duplicates.
hashmap := make(map[util.Uint160]bool)
claim := t.Data.(*transaction.ClaimTX) claim := t.Data.(*transaction.ClaimTX)
clGroups := make(map[util.Uint256][]*transaction.Input) clGroups := make(map[util.Uint256][]*transaction.Input)
@ -934,10 +1045,14 @@ func (bc *Blockchain) GetScriptHashesForVerifyingClaim(t *transaction.Transactio
if len(refTx.Outputs) <= int(input.PrevIndex) { if len(refTx.Outputs) <= int(input.PrevIndex) {
return nil, fmt.Errorf("wrong PrevIndex reference") return nil, fmt.Errorf("wrong PrevIndex reference")
} }
hashes = append(hashes, refTx.Outputs[input.PrevIndex].ScriptHash) hashmap[refTx.Outputs[input.PrevIndex].ScriptHash] = true
} }
} }
if len(hashes) > 0 { if len(hashmap) > 0 {
hashes := make([]util.Uint160, 0, len(hashmap))
for k := range hashmap {
hashes = append(hashes, k)
}
return hashes, nil return hashes, nil
} }
return nil, fmt.Errorf("no hashes found") return nil, fmt.Errorf("no hashes found")
@ -997,12 +1112,63 @@ func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([
} }
// VerifyWitnesses verify the scripts (witnesses) that come with a given // verifyHashAgainstScript verifies given hash against the given witness.
func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, checkedHash util.Uint256, interopCtx *interopContext) error {
verification := witness.VerificationScript
if len(verification) == 0 {
bb := new(bytes.Buffer)
err := vm.EmitAppCall(bb, hash, false)
if err != nil {
return err
}
verification = bb.Bytes()
} else {
if h := witness.ScriptHash(); hash != h {
return errors.New("witness hash mismatch")
}
}
vm := vm.New(vm.ModeMute)
vm.SetCheckedHash(checkedHash.Bytes())
vm.SetScriptGetter(func(hash util.Uint160) []byte {
cs := bc.GetContractState(hash)
if cs == nil {
return nil
}
return cs.Script
})
vm.RegisterInteropFuncs(interopCtx.getSystemInteropMap())
vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap())
vm.LoadScript(verification)
vm.LoadScript(witness.InvocationScript)
vm.Run()
if vm.HasFailed() {
return errors.Errorf("vm failed to execute the script")
}
resEl := vm.Estack().Pop()
if resEl != nil {
res, err := resEl.TryBool()
if err != nil {
return err
}
if !res {
return errors.Errorf("signature check failed")
}
} else {
return errors.Errorf("no result returned from the script")
}
return nil
}
// verifyTxWitnesses verify the scripts (witnesses) that come with a given
// transaction. It can reorder them by ScriptHash, because that's required to // transaction. It can reorder them by ScriptHash, because that's required to
// match a slice of script hashes from the Blockchain. // match a slice of script hashes from the Blockchain. Block parameter
// is used for easy interop access and can be omitted for transactions that are
// not yet added into any block.
// Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87). // Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87).
// Unfortunately the IVerifiable interface could not be implemented because we can't move the References method in blockchain.go to the transaction.go file // Unfortunately the IVerifiable interface could not be implemented because we can't move the References method in blockchain.go to the transaction.go file
func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error { func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *Block) error {
hashes, err := bc.GetScriptHashesForVerifying(t) hashes, err := bc.GetScriptHashesForVerifying(t)
if err != nil { if err != nil {
return err return err
@ -1014,54 +1180,30 @@ func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error {
} }
sort.Slice(hashes, func(i, j int) bool { return hashes[i].Less(hashes[j]) }) sort.Slice(hashes, func(i, j int) bool { return hashes[i].Less(hashes[j]) })
sort.Slice(witnesses, func(i, j int) bool { return witnesses[i].ScriptHash().Less(witnesses[j].ScriptHash()) }) sort.Slice(witnesses, func(i, j int) bool { return witnesses[i].ScriptHash().Less(witnesses[j].ScriptHash()) })
interopCtx := newInteropContext(0, bc, block, t)
for i := 0; i < len(hashes); i++ { for i := 0; i < len(hashes); i++ {
verification := witnesses[i].VerificationScript err := bc.verifyHashAgainstScript(hashes[i], witnesses[i], t.VerificationHash(), interopCtx)
if err != nil {
if len(verification) == 0 { numStr := fmt.Sprintf("witness #%d", i)
bb := new(bytes.Buffer) return errors.Wrap(err, numStr)
err = vm.EmitAppCall(bb, hashes[i], false)
if err != nil {
return err
}
verification = bb.Bytes()
} else {
if h := witnesses[i].ScriptHash(); hashes[i] != h {
return errors.Errorf("hash mismatch for script #%d", i)
}
}
vm := vm.New(vm.ModeMute)
vm.SetCheckedHash(t.VerificationHash().Bytes())
vm.SetScriptGetter(func(hash util.Uint160) []byte {
cs := bc.GetContractState(hash)
if cs == nil {
return nil
}
return cs.Script
})
vm.LoadScript(verification)
vm.LoadScript(witnesses[i].InvocationScript)
vm.Run()
if vm.HasFailed() {
return errors.Errorf("vm failed to execute the script")
}
resEl := vm.Estack().Pop()
if resEl != nil {
res, err := resEl.TryBool()
if err != nil {
return err
}
if !res {
return errors.Errorf("signature check failed")
}
} else {
return errors.Errorf("no result returned from the script")
} }
} }
return nil return nil
} }
// verifyBlockWitnesses is a block-specific implementation of VerifyWitnesses logic.
func (bc *Blockchain) verifyBlockWitnesses(block *Block, prevHeader *Header) error {
var hash util.Uint160
if prevHeader == nil && block.PrevHash.Equals(util.Uint256{}) {
hash = block.Script.ScriptHash()
} else {
hash = prevHeader.NextConsensus
}
interopCtx := newInteropContext(0, bc, nil, nil)
return bc.verifyHashAgainstScript(hash, block.Script, block.VerificationHash(), interopCtx)
}
func hashAndIndexToBytes(h util.Uint256, index uint32) []byte { func hashAndIndexToBytes(h util.Uint256, index uint32) []byte {
buf := io.NewBufBinWriter() buf := io.NewBufBinWriter()
buf.WriteLE(h.BytesReverse()) buf.WriteLE(h.BytesReverse())

View file

@ -4,9 +4,7 @@ import (
"context" "context"
"testing" "testing"
"github.com/CityOfZion/neo-go/config"
"github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -39,9 +37,9 @@ func TestAddHeaders(t *testing.T) {
func TestAddBlock(t *testing.T) { func TestAddBlock(t *testing.T) {
bc := newTestChain(t) bc := newTestChain(t)
blocks := []*Block{ blocks := []*Block{
newBlock(1, newTX(transaction.MinerType)), newBlock(1, newMinerTX()),
newBlock(2, newTX(transaction.MinerType)), newBlock(2, newMinerTX()),
newBlock(3, newTX(transaction.MinerType)), newBlock(3, newMinerTX()),
} }
for i := 0; i < len(blocks); i++ { for i := 0; i < len(blocks); i++ {
@ -70,7 +68,7 @@ func TestAddBlock(t *testing.T) {
func TestGetHeader(t *testing.T) { func TestGetHeader(t *testing.T) {
bc := newTestChain(t) bc := newTestChain(t)
block := newBlock(1, newTX(transaction.MinerType)) block := newBlock(1, newMinerTX())
err := bc.AddBlock(block) err := bc.AddBlock(block)
assert.Nil(t, err) assert.Nil(t, err)
@ -103,7 +101,7 @@ func TestGetBlock(t *testing.T) {
for i := 0; i < len(blocks); i++ { for i := 0; i < len(blocks); i++ {
block, err := bc.GetBlock(blocks[i].Hash()) block, err := bc.GetBlock(blocks[i].Hash())
if err != nil { if err != nil {
t.Fatal(err) t.Fatalf("can't get block %d: %s, attempt %d", i, err, j)
} }
assert.Equal(t, blocks[i].Index, block.Index) assert.Equal(t, blocks[i].Index, block.Index)
assert.Equal(t, blocks[i].Hash(), block.Hash()) assert.Equal(t, blocks[i].Hash(), block.Hash())
@ -138,8 +136,9 @@ func TestGetTransaction(t *testing.T) {
block := getDecodedBlock(t, 2) block := getDecodedBlock(t, 2)
bc := newTestChain(t) bc := newTestChain(t)
assert.Nil(t, bc.AddBlock(b1)) // These are from some kind of different chain, so can't be added via AddBlock().
assert.Nil(t, bc.AddBlock(block)) assert.Nil(t, bc.storeBlock(b1))
assert.Nil(t, bc.storeBlock(block))
// Test unpersisted and persisted access // Test unpersisted and persisted access
for j := 0; j < 2; j++ { for j := 0; j < 2; j++ {
@ -155,16 +154,3 @@ func TestGetTransaction(t *testing.T) {
assert.NoError(t, bc.persist(context.Background())) assert.NoError(t, bc.persist(context.Background()))
} }
} }
func newTestChain(t *testing.T) *Blockchain {
cfg, err := config.Load("../../config", config.ModeUnitTestNet)
if err != nil {
t.Fatal(err)
}
chain, err := NewBlockchain(storage.NewMemoryStore(), cfg.ProtocolConfiguration)
if err != nil {
t.Fatal(err)
}
go chain.Run(context.Background())
return chain
}

View file

@ -15,6 +15,7 @@ type Blockchainer interface {
BlockHeight() uint32 BlockHeight() uint32
HeaderHeight() uint32 HeaderHeight() uint32
GetBlock(hash util.Uint256) (*Block, error) GetBlock(hash util.Uint256) (*Block, error)
GetContractState(hash util.Uint160) *ContractState
GetHeaderHash(int) util.Uint256 GetHeaderHash(int) util.Uint256
GetHeader(hash util.Uint256) (*Header, error) GetHeader(hash util.Uint256) (*Header, error)
CurrentHeaderHash() util.Uint256 CurrentHeaderHash() util.Uint256
@ -23,9 +24,13 @@ type Blockchainer interface {
HasTransaction(util.Uint256) bool HasTransaction(util.Uint256) bool
GetAssetState(util.Uint256) *AssetState GetAssetState(util.Uint256) *AssetState
GetAccountState(util.Uint160) *AccountState GetAccountState(util.Uint160) *AccountState
GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error)
GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem
GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error)
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
GetUnspentCoinState(util.Uint256) *UnspentCoinState
References(t *transaction.Transaction) map[transaction.Input]*transaction.Output References(t *transaction.Transaction) map[transaction.Input]*transaction.Output
Feer // fee interface Feer // fee interface
Verify(t *transaction.Transaction) error VerifyTx(*transaction.Transaction, *Block) error
GetMemPool() MemPool GetMemPool() MemPool
} }

View file

@ -13,17 +13,15 @@ type Contracts map[util.Uint160]*ContractState
// ContractState holds information about a smart contract in the NEO blockchain. // ContractState holds information about a smart contract in the NEO blockchain.
type ContractState struct { type ContractState struct {
Script []byte Script []byte
ParamList []smartcontract.ParamType ParamList []smartcontract.ParamType
ReturnType smartcontract.ParamType ReturnType smartcontract.ParamType
Properties []byte Properties smartcontract.PropertyState
Name string Name string
CodeVersion string CodeVersion string
Author string Author string
Email string Email string
Description string Description string
HasStorage bool
HasDynamicInvoke bool
scriptHash util.Uint160 scriptHash util.Uint160
} }
@ -44,52 +42,80 @@ func (a Contracts) commit(b storage.Batch) error {
} }
// DecodeBinary implements Serializable interface. // DecodeBinary implements Serializable interface.
func (a *ContractState) DecodeBinary(br *io.BinReader) { func (cs *ContractState) DecodeBinary(br *io.BinReader) {
a.Script = br.ReadBytes() cs.Script = br.ReadBytes()
paramBytes := br.ReadBytes() paramBytes := br.ReadBytes()
a.ParamList = make([]smartcontract.ParamType, len(paramBytes)) cs.ParamList = make([]smartcontract.ParamType, len(paramBytes))
for k := range paramBytes { for k := range paramBytes {
a.ParamList[k] = smartcontract.ParamType(paramBytes[k]) cs.ParamList[k] = smartcontract.ParamType(paramBytes[k])
} }
br.ReadLE(&a.ReturnType) br.ReadLE(&cs.ReturnType)
a.Properties = br.ReadBytes() br.ReadLE(&cs.Properties)
a.Name = br.ReadString() cs.Name = br.ReadString()
a.CodeVersion = br.ReadString() cs.CodeVersion = br.ReadString()
a.Author = br.ReadString() cs.Author = br.ReadString()
a.Email = br.ReadString() cs.Email = br.ReadString()
a.Description = br.ReadString() cs.Description = br.ReadString()
br.ReadLE(&a.HasStorage) cs.createHash()
br.ReadLE(&a.HasDynamicInvoke)
a.createHash()
} }
// EncodeBinary implements Serializable interface. // EncodeBinary implements Serializable interface.
func (a *ContractState) EncodeBinary(bw *io.BinWriter) { func (cs *ContractState) EncodeBinary(bw *io.BinWriter) {
bw.WriteBytes(a.Script) bw.WriteBytes(cs.Script)
bw.WriteVarUint(uint64(len(a.ParamList))) bw.WriteVarUint(uint64(len(cs.ParamList)))
for k := range a.ParamList { for k := range cs.ParamList {
bw.WriteLE(a.ParamList[k]) bw.WriteLE(cs.ParamList[k])
} }
bw.WriteLE(a.ReturnType) bw.WriteLE(cs.ReturnType)
bw.WriteBytes(a.Properties) bw.WriteLE(cs.Properties)
bw.WriteString(a.Name) bw.WriteString(cs.Name)
bw.WriteString(a.CodeVersion) bw.WriteString(cs.CodeVersion)
bw.WriteString(a.Author) bw.WriteString(cs.Author)
bw.WriteString(a.Email) bw.WriteString(cs.Email)
bw.WriteString(a.Description) bw.WriteString(cs.Description)
bw.WriteLE(a.HasStorage) }
bw.WriteLE(a.HasDynamicInvoke)
// putContractStateIntoStore puts given contract state into the given store.
func putContractStateIntoStore(s storage.Store, cs *ContractState) error {
buf := io.NewBufBinWriter()
cs.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return buf.Err
}
key := storage.AppendPrefix(storage.STContract, cs.ScriptHash().Bytes())
return s.Put(key, buf.Bytes())
}
// deleteContractStateInStore deletes given contract state in the given store.
func deleteContractStateInStore(s storage.Store, hash util.Uint160) error {
key := storage.AppendPrefix(storage.STContract, hash.Bytes())
return s.Delete(key)
} }
// ScriptHash returns a contract script hash. // ScriptHash returns a contract script hash.
func (a *ContractState) ScriptHash() util.Uint160 { func (cs *ContractState) ScriptHash() util.Uint160 {
if a.scriptHash.Equals(util.Uint160{}) { if cs.scriptHash.Equals(util.Uint160{}) {
a.createHash() cs.createHash()
} }
return a.scriptHash return cs.scriptHash
} }
// createHash creates contract script hash. // createHash creates contract script hash.
func (a *ContractState) createHash() { func (cs *ContractState) createHash() {
a.scriptHash = hash.Hash160(a.Script) cs.scriptHash = hash.Hash160(cs.Script)
}
// HasStorage checks whether the contract has storage property set.
func (cs *ContractState) HasStorage() bool {
return (cs.Properties & smartcontract.HasStorage) != 0
}
// HasDynamicInvoke checks whether the contract has dynamic invoke property set.
func (cs *ContractState) HasDynamicInvoke() bool {
return (cs.Properties & smartcontract.HasDynamicInvoke) != 0
}
// IsPayable checks whether the contract has payable property set.
func (cs *ContractState) IsPayable() bool {
return (cs.Properties & smartcontract.IsPayable) != 0
} }

View file

@ -3,6 +3,7 @@ package core
import ( import (
"testing" "testing"
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/smartcontract"
@ -13,17 +14,15 @@ func TestEncodeDecodeContractState(t *testing.T) {
script := []byte("testscript") script := []byte("testscript")
contract := &ContractState{ contract := &ContractState{
Script: script, Script: script,
ParamList: []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type}, ParamList: []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type},
ReturnType: smartcontract.BoolType, ReturnType: smartcontract.BoolType,
Properties: []byte("smth"), Properties: smartcontract.HasStorage,
Name: "Contracto", Name: "Contracto",
CodeVersion: "1.0.0", CodeVersion: "1.0.0",
Author: "Joe Random", Author: "Joe Random",
Email: "joe@example.com", Email: "joe@example.com",
Description: "Test contract", Description: "Test contract",
HasStorage: true,
HasDynamicInvoke: false,
} }
assert.Equal(t, hash.Hash160(script), contract.ScriptHash()) assert.Equal(t, hash.Hash160(script), contract.ScriptHash())
@ -37,3 +36,42 @@ func TestEncodeDecodeContractState(t *testing.T) {
assert.Equal(t, contract, contractDecoded) assert.Equal(t, contract, contractDecoded)
assert.Equal(t, contract.ScriptHash(), contractDecoded.ScriptHash()) assert.Equal(t, contract.ScriptHash(), contractDecoded.ScriptHash())
} }
func TestContractStateProperties(t *testing.T) {
flaggedContract := ContractState{
Properties: smartcontract.HasStorage | smartcontract.HasDynamicInvoke | smartcontract.IsPayable,
}
nonFlaggedContract := ContractState{
ReturnType: smartcontract.BoolType,
}
assert.Equal(t, true, flaggedContract.HasStorage())
assert.Equal(t, true, flaggedContract.HasDynamicInvoke())
assert.Equal(t, true, flaggedContract.IsPayable())
assert.Equal(t, false, nonFlaggedContract.HasStorage())
assert.Equal(t, false, nonFlaggedContract.HasDynamicInvoke())
assert.Equal(t, false, nonFlaggedContract.IsPayable())
}
func TestPutGetDeleteContractState(t *testing.T) {
s := storage.NewMemoryStore()
script := []byte("testscript")
contract := &ContractState{
Script: script,
ParamList: []smartcontract.ParamType{smartcontract.StringType, smartcontract.IntegerType, smartcontract.Hash160Type},
ReturnType: smartcontract.BoolType,
Properties: smartcontract.HasStorage,
Name: "Contracto",
CodeVersion: "1.0.0",
Author: "Joe Random",
Email: "joe@example.com",
Description: "Test contract",
}
assert.NoError(t, putContractStateIntoStore(s, contract))
csRead := getContractStateFromStore(s, contract.ScriptHash())
assert.NotNil(t, csRead)
assert.Equal(t, contract, csRead)
assert.NoError(t, deleteContractStateInStore(s, contract.ScriptHash()))
csRead2 := getContractStateFromStore(s, contract.ScriptHash())
assert.Nil(t, csRead2)
}

View file

@ -1,6 +1,7 @@
package core package core
import ( import (
"context"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -8,46 +9,105 @@ import (
"testing" "testing"
"time" "time"
"github.com/CityOfZion/neo-go/config"
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/stretchr/testify/require"
) )
var newBlockPrevHash util.Uint256
var unitTestNetCfg config.Config
var privNetKeys = []string{
"KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY",
"KzfPUYDC9n2yf4fK5ro4C8KMcdeXtFuEnStycbZgX3GomiUsvX6W",
"KzgWE3u3EDp13XPXXuTKZxeJ3Gi8Bsm8f9ijY3ZsCKKRvZUo1Cdn",
"L2oEXKRAAMiPEZukwR5ho2S6SMeQLhcK9mF71ZnF7GvT8dU4Kkgz",
}
// newTestChain should be called before newBlock invocation to properly setup
// global state.
func newTestChain(t *testing.T) *Blockchain {
var err error
unitTestNetCfg, err = config.Load("../../config", config.ModeUnitTestNet)
if err != nil {
t.Fatal(err)
}
chain, err := NewBlockchain(storage.NewMemoryStore(), unitTestNetCfg.ProtocolConfiguration)
if err != nil {
t.Fatal(err)
}
go chain.Run(context.Background())
zeroHash, err := chain.GetHeader(chain.GetHeaderHash(0))
require.Nil(t, err)
newBlockPrevHash = zeroHash.Hash()
return chain
}
func newBlock(index uint32, txs ...*transaction.Transaction) *Block { func newBlock(index uint32, txs ...*transaction.Transaction) *Block {
validators, _ := getValidators(unitTestNetCfg.ProtocolConfiguration)
vlen := len(validators)
valScript, _ := smartcontract.CreateMultiSigRedeemScript(
vlen-(vlen-1)/3,
validators,
)
witness := &transaction.Witness{
VerificationScript: valScript,
}
b := &Block{ b := &Block{
BlockBase: BlockBase{ BlockBase: BlockBase{
Version: 0, Version: 0,
PrevHash: hash.Sha256([]byte("a")), PrevHash: newBlockPrevHash,
MerkleRoot: hash.Sha256([]byte("b")), Timestamp: uint32(time.Now().UTC().Unix()) + index,
Timestamp: uint32(time.Now().UTC().Unix()),
Index: index, Index: index,
ConsensusData: 1111, ConsensusData: 1111,
NextConsensus: util.Uint160{}, NextConsensus: witness.ScriptHash(),
Script: &transaction.Witness{ Script: witness,
VerificationScript: []byte{0x0},
InvocationScript: []byte{0x1},
},
}, },
Transactions: txs, Transactions: txs,
} }
_ = b.rebuildMerkleRoot()
b.createHash() b.createHash()
newBlockPrevHash = b.Hash()
invScript := make([]byte, 0)
for _, wif := range privNetKeys {
pKey, err := keys.NewPrivateKeyFromWIF(wif)
if err != nil {
panic(err)
}
b, err := b.getHashableData()
if err != nil {
panic(err)
}
sig, err := pKey.Sign(b)
if err != nil || len(sig) != 64 {
panic(err)
}
// 0x40 is PUSHBYTES64
invScript = append(invScript, 0x40)
invScript = append(invScript, sig...)
}
b.Script.InvocationScript = invScript
return b return b
} }
func makeBlocks(n int) []*Block { func makeBlocks(n int) []*Block {
blocks := make([]*Block, n) blocks := make([]*Block, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
blocks[i] = newBlock(uint32(i+1), newTX(transaction.MinerType)) blocks[i] = newBlock(uint32(i+1), newMinerTX())
} }
return blocks return blocks
} }
func newTX(t transaction.TXType) *transaction.Transaction { func newMinerTX() *transaction.Transaction {
return &transaction.Transaction{ return &transaction.Transaction{
Type: t, Type: transaction.MinerType,
Data: &transaction.MinerTX{},
} }
} }

738
pkg/core/interop_neo.go Normal file
View file

@ -0,0 +1,738 @@
package core
import (
"errors"
"fmt"
"math"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm"
gherr "github.com/pkg/errors"
)
const (
// MaxContractScriptSize is the maximum script size for a contract.
MaxContractScriptSize = 1024 * 1024
// MaxContractParametersNum is the maximum number of parameters for a contract.
MaxContractParametersNum = 252
// MaxContractStringLen is the maximum length for contract metadata strings.
MaxContractStringLen = 252
// MaxAssetNameLen is the maximum length of asset name.
MaxAssetNameLen = 1024
// MaxAssetPrecision is the maximum precision of asset.
MaxAssetPrecision = 8
// BlocksPerYear is a multiplier for asset renewal.
BlocksPerYear = 2000000
// DefaultAssetLifetime is the default lifetime of an asset (which differs
// from assets created by register tx).
DefaultAssetLifetime = 1 + BlocksPerYear
)
// txInOut is used to pushed one key-value pair from References() onto the stack.
type txInOut struct {
in transaction.Input
out transaction.Output
}
// headerGetVersion returns version from the header.
func (ic *interopContext) headerGetVersion(v *vm.VM) error {
header, err := popHeaderFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(header.Version)
return nil
}
// headerGetConsensusData returns consensus data from the header.
func (ic *interopContext) headerGetConsensusData(v *vm.VM) error {
header, err := popHeaderFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(header.ConsensusData)
return nil
}
// headerGetMerkleRoot returns version from the header.
func (ic *interopContext) headerGetMerkleRoot(v *vm.VM) error {
header, err := popHeaderFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(header.MerkleRoot.BytesReverse())
return nil
}
// headerGetNextConsensus returns version from the header.
func (ic *interopContext) headerGetNextConsensus(v *vm.VM) error {
header, err := popHeaderFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(header.NextConsensus.BytesReverse())
return nil
}
// txGetAttributes returns current transaction attributes.
func (ic *interopContext) txGetAttributes(v *vm.VM) error {
txInterface := v.Estack().Pop().Value()
tx, ok := txInterface.(*transaction.Transaction)
if !ok {
return errors.New("value is not a transaction")
}
if len(tx.Attributes) > vm.MaxArraySize {
return errors.New("too many attributes")
}
attrs := make([]vm.StackItem, 0, len(tx.Attributes))
for _, attr := range tx.Attributes {
attrs = append(attrs, vm.NewInteropItem(attr))
}
v.Estack().PushVal(attrs)
return nil
}
// txGetInputs returns current transaction inputs.
func (ic *interopContext) txGetInputs(v *vm.VM) error {
txInterface := v.Estack().Pop().Value()
tx, ok := txInterface.(*transaction.Transaction)
if !ok {
return errors.New("value is not a transaction")
}
if len(tx.Inputs) > vm.MaxArraySize {
return errors.New("too many inputs")
}
inputs := make([]vm.StackItem, 0, len(tx.Inputs))
for _, input := range tx.Inputs {
inputs = append(inputs, vm.NewInteropItem(input))
}
v.Estack().PushVal(inputs)
return nil
}
// txGetOutputs returns current transaction outputs.
func (ic *interopContext) txGetOutputs(v *vm.VM) error {
txInterface := v.Estack().Pop().Value()
tx, ok := txInterface.(*transaction.Transaction)
if !ok {
return errors.New("value is not a transaction")
}
if len(tx.Outputs) > vm.MaxArraySize {
return errors.New("too many outputs")
}
outputs := make([]vm.StackItem, 0, len(tx.Outputs))
for _, output := range tx.Outputs {
outputs = append(outputs, vm.NewInteropItem(output))
}
v.Estack().PushVal(outputs)
return nil
}
// txGetReferences returns current transaction references.
func (ic *interopContext) txGetReferences(v *vm.VM) error {
txInterface := v.Estack().Pop().Value()
tx, ok := txInterface.(*transaction.Transaction)
if !ok {
return fmt.Errorf("type mismatch: %T is not a Transaction", txInterface)
}
refs := ic.bc.References(tx)
if len(refs) > vm.MaxArraySize {
return errors.New("too many references")
}
stackrefs := make([]vm.StackItem, 0, len(refs))
for k, v := range refs {
tio := txInOut{k, *v}
stackrefs = append(stackrefs, vm.NewInteropItem(tio))
}
v.Estack().PushVal(stackrefs)
return nil
}
// txGetType returns current transaction type.
func (ic *interopContext) txGetType(v *vm.VM) error {
txInterface := v.Estack().Pop().Value()
tx, ok := txInterface.(*transaction.Transaction)
if !ok {
return errors.New("value is not a transaction")
}
v.Estack().PushVal(int(tx.Type))
return nil
}
// txGetUnspentCoins returns current transaction unspent coins.
func (ic *interopContext) txGetUnspentCoins(v *vm.VM) error {
txInterface := v.Estack().Pop().Value()
tx, ok := txInterface.(*transaction.Transaction)
if !ok {
return errors.New("value is not a transaction")
}
ucs := ic.bc.GetUnspentCoinState(tx.Hash())
if ucs == nil {
return errors.New("no unspent coin state found")
}
v.Estack().PushVal(vm.NewInteropItem(ucs))
return nil
}
// txGetWitnesses returns current transaction witnesses.
func (ic *interopContext) txGetWitnesses(v *vm.VM) error {
txInterface := v.Estack().Pop().Value()
tx, ok := txInterface.(*transaction.Transaction)
if !ok {
return errors.New("value is not a transaction")
}
if len(tx.Scripts) > vm.MaxArraySize {
return errors.New("too many outputs")
}
scripts := make([]vm.StackItem, 0, len(tx.Scripts))
for _, script := range tx.Scripts {
scripts = append(scripts, vm.NewInteropItem(script))
}
v.Estack().PushVal(scripts)
return nil
}
// popInputFromVM returns transaction.Input from the first estack element.
func popInputFromVM(v *vm.VM) (*transaction.Input, error) {
inInterface := v.Estack().Pop().Value()
input, ok := inInterface.(*transaction.Input)
if !ok {
txio, ok := inInterface.(txInOut)
if !ok {
return nil, fmt.Errorf("type mismatch: %T is not an Input or txInOut", inInterface)
}
input = &txio.in
}
return input, nil
}
// inputGetHash returns hash from the given input.
func (ic *interopContext) inputGetHash(v *vm.VM) error {
input, err := popInputFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(input.PrevHash.Bytes())
return nil
}
// inputGetIndex returns index from the given input.
func (ic *interopContext) inputGetIndex(v *vm.VM) error {
input, err := popInputFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(input.PrevIndex)
return nil
}
// popOutputFromVM returns transaction.Input from the first estack element.
func popOutputFromVM(v *vm.VM) (*transaction.Output, error) {
outInterface := v.Estack().Pop().Value()
output, ok := outInterface.(*transaction.Output)
if !ok {
txio, ok := outInterface.(txInOut)
if !ok {
return nil, fmt.Errorf("type mismatch: %T is not an Output or txInOut", outInterface)
}
output = &txio.out
}
return output, nil
}
// outputGetAssetId returns asset ID from the given output.
func (ic *interopContext) outputGetAssetID(v *vm.VM) error {
output, err := popOutputFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(output.AssetID.Bytes())
return nil
}
// outputGetScriptHash returns scripthash from the given output.
func (ic *interopContext) outputGetScriptHash(v *vm.VM) error {
output, err := popOutputFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(output.ScriptHash.Bytes())
return nil
}
// outputGetValue returns value (amount) from the given output.
func (ic *interopContext) outputGetValue(v *vm.VM) error {
output, err := popOutputFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(int64(output.Amount))
return nil
}
// attrGetData returns tx attribute data.
func (ic *interopContext) attrGetData(v *vm.VM) error {
attrInterface := v.Estack().Pop().Value()
attr, ok := attrInterface.(*transaction.Attribute)
if !ok {
return fmt.Errorf("%T is not an attribute", attr)
}
v.Estack().PushVal(attr.Data)
return nil
}
// attrGetData returns tx attribute usage field.
func (ic *interopContext) attrGetUsage(v *vm.VM) error {
attrInterface := v.Estack().Pop().Value()
attr, ok := attrInterface.(*transaction.Attribute)
if !ok {
return fmt.Errorf("%T is not an attribute", attr)
}
v.Estack().PushVal(int(attr.Usage))
return nil
}
// bcGetAccount returns or creates an account.
func (ic *interopContext) bcGetAccount(v *vm.VM) error {
accbytes := v.Estack().Pop().Bytes()
acchash, err := util.Uint160DecodeBytes(accbytes)
if err != nil {
return err
}
acc := ic.bc.GetAccountState(acchash)
if acc == nil {
acc = NewAccountState(acchash)
}
v.Estack().PushVal(vm.NewInteropItem(acc))
return nil
}
// bcGetAsset returns an asset.
func (ic *interopContext) bcGetAsset(v *vm.VM) error {
asbytes := v.Estack().Pop().Bytes()
ashash, err := util.Uint256DecodeBytes(asbytes)
if err != nil {
return err
}
as := ic.bc.GetAssetState(ashash)
if as == nil {
return errors.New("asset not found")
}
v.Estack().PushVal(vm.NewInteropItem(as))
return nil
}
// accountGetBalance returns balance for a given account.
func (ic *interopContext) accountGetBalance(v *vm.VM) error {
accInterface := v.Estack().Pop().Value()
acc, ok := accInterface.(*AccountState)
if !ok {
return fmt.Errorf("%T is not an account state", acc)
}
asbytes := v.Estack().Pop().Bytes()
ashash, err := util.Uint256DecodeBytes(asbytes)
if err != nil {
return err
}
balance, ok := acc.Balances[ashash]
if !ok {
balance = util.Fixed8(0)
}
v.Estack().PushVal(int64(balance))
return nil
}
// accountGetScriptHash returns script hash of a given account.
func (ic *interopContext) accountGetScriptHash(v *vm.VM) error {
accInterface := v.Estack().Pop().Value()
acc, ok := accInterface.(*AccountState)
if !ok {
return fmt.Errorf("%T is not an account state", acc)
}
v.Estack().PushVal(acc.ScriptHash.Bytes())
return nil
}
// accountGetVotes returns votes of a given account.
func (ic *interopContext) accountGetVotes(v *vm.VM) error {
accInterface := v.Estack().Pop().Value()
acc, ok := accInterface.(*AccountState)
if !ok {
return fmt.Errorf("%T is not an account state", acc)
}
if len(acc.Votes) > vm.MaxArraySize {
return errors.New("too many votes")
}
votes := make([]vm.StackItem, 0, len(acc.Votes))
for _, key := range acc.Votes {
votes = append(votes, vm.NewByteArrayItem(key.Bytes()))
}
v.Estack().PushVal(votes)
return nil
}
// accountIsStandard checks whether given account is standard.
func (ic *interopContext) accountIsStandard(v *vm.VM) error {
accbytes := v.Estack().Pop().Bytes()
acchash, err := util.Uint160DecodeBytes(accbytes)
if err != nil {
return err
}
contract := ic.bc.GetContractState(acchash)
res := contract == nil || vm.IsStandardContract(contract.Script)
v.Estack().PushVal(res)
return nil
}
/*
// storageFind finds stored key-value pair.
func (ic *interopContext) storageFind(v *vm.VM) error {
stcInterface := v.Estack().Pop().Value()
stc, ok := stcInterface.(*StorageContext)
if !ok {
return fmt.Errorf("%T is not a StorageContext", stcInterface)
}
err := ic.checkStorageContext(stc)
if err != nil {
return err
}
prefix := string(v.Estack().Pop().Bytes())
siMap, err := ic.bc.GetStorageItems(stc.ScriptHash)
if err != nil {
return err
}
for k, v := range siMap {
if strings.HasPrefix(k, prefix) {
_ = v
panic("TODO")
}
}
return nil
}
*/
// createContractStateFromVM pops all contract state elements from the VM
// evaluation stack, does a lot of checks and returns ContractState if it
// succeedes.
func (ic *interopContext) createContractStateFromVM(v *vm.VM) (*ContractState, error) {
if ic.trigger != 0x10 {
return nil, errors.New("can't create contract when not triggered by an application")
}
script := v.Estack().Pop().Bytes()
if len(script) > MaxContractScriptSize {
return nil, errors.New("the script is too big")
}
paramBytes := v.Estack().Pop().Bytes()
if len(paramBytes) > MaxContractParametersNum {
return nil, errors.New("too many parameters for a script")
}
paramList := make([]smartcontract.ParamType, len(paramBytes))
for k, v := range paramBytes {
paramList[k] = smartcontract.ParamType(v)
}
retType := smartcontract.ParamType(v.Estack().Pop().BigInt().Int64())
properties := smartcontract.PropertyState(v.Estack().Pop().BigInt().Int64())
name := v.Estack().Pop().Bytes()
if len(name) > MaxContractStringLen {
return nil, errors.New("too big name")
}
version := v.Estack().Pop().Bytes()
if len(version) > MaxContractStringLen {
return nil, errors.New("too big version")
}
author := v.Estack().Pop().Bytes()
if len(author) > MaxContractStringLen {
return nil, errors.New("too big author")
}
email := v.Estack().Pop().Bytes()
if len(email) > MaxContractStringLen {
return nil, errors.New("too big email")
}
desc := v.Estack().Pop().Bytes()
if len(desc) > MaxContractStringLen {
return nil, errors.New("too big description")
}
contract := &ContractState{
Script: script,
ParamList: paramList,
ReturnType: retType,
Properties: properties,
Name: string(name),
CodeVersion: string(version),
Author: string(author),
Email: string(email),
Description: string(desc),
}
return contract, nil
}
// contractCreate creates a contract.
func (ic *interopContext) contractCreate(v *vm.VM) error {
newcontract, err := ic.createContractStateFromVM(v)
if err != nil {
return nil
}
contract := ic.bc.GetContractState(newcontract.ScriptHash())
if contract == nil {
contract = newcontract
err := putContractStateIntoStore(ic.mem, contract)
if err != nil {
return err
}
}
v.Estack().PushVal(vm.NewInteropItem(contract))
return nil
}
// contractGetScript returns a script associated with a contract.
func (ic *interopContext) contractGetScript(v *vm.VM) error {
csInterface := v.Estack().Pop().Value()
cs, ok := csInterface.(*ContractState)
if !ok {
return fmt.Errorf("%T is not a contract state", cs)
}
v.Estack().PushVal(cs.Script)
return nil
}
// contractIsPayable returns whether contract is payable.
func (ic *interopContext) contractIsPayable(v *vm.VM) error {
csInterface := v.Estack().Pop().Value()
cs, ok := csInterface.(*ContractState)
if !ok {
return fmt.Errorf("%T is not a contract state", cs)
}
v.Estack().PushVal(cs.IsPayable())
return nil
}
// contractMigrate migrates a contract.
func (ic *interopContext) contractMigrate(v *vm.VM) error {
newcontract, err := ic.createContractStateFromVM(v)
if err != nil {
return nil
}
contract := ic.bc.GetContractState(newcontract.ScriptHash())
if contract == nil {
contract = newcontract
err := putContractStateIntoStore(ic.mem, contract)
if err != nil {
return err
}
if contract.HasStorage() {
hash := getContextScriptHash(v, 0)
siMap, err := ic.bc.GetStorageItems(hash)
if err != nil {
return err
}
for k, v := range siMap {
v.IsConst = false
_ = putStorageItemIntoStore(ic.mem, hash, []byte(k), v)
}
}
}
v.Estack().PushVal(vm.NewInteropItem(contract))
return ic.contractDestroy(v)
}
// assetCreate creates an asset.
func (ic *interopContext) assetCreate(v *vm.VM) error {
if ic.trigger != 0x10 {
return errors.New("can't create asset when not triggered by an application")
}
atype := transaction.AssetType(v.Estack().Pop().BigInt().Int64())
switch atype {
case transaction.Currency, transaction.Share, transaction.Invoice, transaction.Token:
// ok
default:
return fmt.Errorf("wrong asset type: %x", atype)
}
name := string(v.Estack().Pop().Bytes())
if len(name) > MaxAssetNameLen {
return errors.New("too big name")
}
amount := util.Fixed8(v.Estack().Pop().BigInt().Int64())
if amount == util.Fixed8(0) {
return errors.New("asset amount can't be zero")
}
if amount < -util.Satoshi() {
return errors.New("asset amount can't be negative (except special -Satoshi value")
}
if atype == transaction.Invoice && amount != -util.Satoshi() {
return errors.New("invoice assets can only have -Satoshi amount")
}
precision := byte(v.Estack().Pop().BigInt().Int64())
if precision > MaxAssetPrecision {
return fmt.Errorf("can't have asset precision of more than %d", MaxAssetPrecision)
}
if atype == transaction.Share && precision != 0 {
return errors.New("share assets can only have zero precision")
}
if amount != -util.Satoshi() && (int64(amount)%int64(math.Pow10(int(MaxAssetPrecision-precision))) != 0) {
return errors.New("given asset amount has fractional component")
}
owner := &keys.PublicKey{}
err := owner.DecodeBytes(v.Estack().Pop().Bytes())
if err != nil {
return gherr.Wrap(err, "failed to get owner key")
}
if owner.IsInfinity() {
return errors.New("can't have infinity as an owner key")
}
witnessOk, err := ic.checkKeyedWitness(owner)
if err != nil {
return err
}
if !witnessOk {
return errors.New("witness check didn't succeed")
}
admin, err := util.Uint160DecodeBytes(v.Estack().Pop().Bytes())
if err != nil {
return gherr.Wrap(err, "failed to get admin")
}
issuer, err := util.Uint160DecodeBytes(v.Estack().Pop().Bytes())
if err != nil {
return gherr.Wrap(err, "failed to get issuer")
}
asset := &AssetState{
ID: ic.tx.Hash(),
AssetType: atype,
Name: name,
Amount: amount,
Precision: precision,
Owner: owner,
Admin: admin,
Issuer: issuer,
Expiration: ic.bc.BlockHeight() + DefaultAssetLifetime,
}
err = putAssetStateIntoStore(ic.mem, asset)
if err != nil {
return gherr.Wrap(err, "failed to store asset")
}
v.Estack().PushVal(vm.NewInteropItem(asset))
return nil
}
// assetGetAdmin returns asset admin.
func (ic *interopContext) assetGetAdmin(v *vm.VM) error {
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
v.Estack().PushVal(as.Admin.Bytes())
return nil
}
// assetGetAmount returns the overall amount of asset available.
func (ic *interopContext) assetGetAmount(v *vm.VM) error {
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
v.Estack().PushVal(int64(as.Amount))
return nil
}
// assetGetAssetId returns the id of an asset.
func (ic *interopContext) assetGetAssetID(v *vm.VM) error {
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
v.Estack().PushVal(as.ID.Bytes())
return nil
}
// assetGetAssetType returns type of an asset.
func (ic *interopContext) assetGetAssetType(v *vm.VM) error {
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
v.Estack().PushVal(int(as.AssetType))
return nil
}
// assetGetAvailable returns available (not yet issued) amount of asset.
func (ic *interopContext) assetGetAvailable(v *vm.VM) error {
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
v.Estack().PushVal(int(as.Available))
return nil
}
// assetGetIssuer returns issuer of an asset.
func (ic *interopContext) assetGetIssuer(v *vm.VM) error {
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
v.Estack().PushVal(as.Issuer.Bytes())
return nil
}
// assetGetOwner returns owner of an asset.
func (ic *interopContext) assetGetOwner(v *vm.VM) error {
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
v.Estack().PushVal(as.Owner.Bytes())
return nil
}
// assetGetPrecision returns precision used to measure this asset.
func (ic *interopContext) assetGetPrecision(v *vm.VM) error {
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
v.Estack().PushVal(int(as.Precision))
return nil
}
// assetRenew updates asset expiration date.
func (ic *interopContext) assetRenew(v *vm.VM) error {
if ic.trigger != 0x10 {
return errors.New("can't create asset when not triggered by an application")
}
asInterface := v.Estack().Pop().Value()
as, ok := asInterface.(*AssetState)
if !ok {
return fmt.Errorf("%T is not an asset state", as)
}
years := byte(v.Estack().Pop().BigInt().Int64())
// Not sure why C# code regets an asset from the Store, but we also do it.
asset := ic.bc.GetAssetState(as.ID)
if asset == nil {
return errors.New("can't renew non-existent asset")
}
if asset.Expiration < ic.bc.BlockHeight()+1 {
asset.Expiration = ic.bc.BlockHeight() + 1
}
expiration := uint64(asset.Expiration) + uint64(years)*BlocksPerYear
if expiration > math.MaxUint32 {
expiration = math.MaxUint32
}
asset.Expiration = uint32(expiration)
err := putAssetStateIntoStore(ic.mem, asset)
if err != nil {
return gherr.Wrap(err, "failed to store asset")
}
v.Estack().PushVal(expiration)
return nil
}

583
pkg/core/interop_system.go Normal file
View file

@ -0,0 +1,583 @@
package core
import (
"errors"
"fmt"
"math"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm"
gherr "github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
const (
// MaxStorageKeyLen is the maximum length of a key for storage items.
MaxStorageKeyLen = 1024
)
// StorageContext contains storing script hash and read/write flag, it's used as
// a context for storage manipulation functions.
type StorageContext struct {
ScriptHash util.Uint160
ReadOnly bool
}
// 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, 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.Uint256DecodeReverseBytes(hashbytes)
}
return hash, nil
}
// bcGetBlock returns current block.
func (ic *interopContext) bcGetBlock(v *vm.VM) error {
hash, err := getBlockHashFromElement(ic.bc, v.Estack().Pop())
if err != nil {
return err
}
block, err := ic.bc.GetBlock(hash)
if err != nil {
v.Estack().PushVal([]byte{})
} else {
v.Estack().PushVal(vm.NewInteropItem(block))
}
return nil
}
// bcGetContract returns contract.
func (ic *interopContext) bcGetContract(v *vm.VM) error {
hashbytes := v.Estack().Pop().Bytes()
hash, err := util.Uint160DecodeBytes(hashbytes)
if err != nil {
return err
}
cs := ic.bc.GetContractState(hash)
if cs == nil {
v.Estack().PushVal([]byte{})
} else {
v.Estack().PushVal(vm.NewInteropItem(cs))
}
return nil
}
// bcGetHeader returns block header.
func (ic *interopContext) bcGetHeader(v *vm.VM) error {
hash, err := getBlockHashFromElement(ic.bc, v.Estack().Pop())
if err != nil {
return err
}
header, err := ic.bc.GetHeader(hash)
if err != nil {
v.Estack().PushVal([]byte{})
} else {
v.Estack().PushVal(vm.NewInteropItem(header))
}
return nil
}
// bcGetHeight returns blockchain height.
func (ic *interopContext) bcGetHeight(v *vm.VM) error {
v.Estack().PushVal(ic.bc.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(bc Blockchainer, v *vm.VM) (*transaction.Transaction, uint32, error) {
hashbytes := v.Estack().Pop().Bytes()
hash, err := util.Uint256DecodeReverseBytes(hashbytes)
if err != nil {
return nil, 0, err
}
return bc.GetTransaction(hash)
}
// bcGetTransaction returns transaction.
func (ic *interopContext) bcGetTransaction(v *vm.VM) error {
tx, _, err := getTransactionAndHeight(ic.bc, v)
if err != nil {
return err
}
v.Estack().PushVal(vm.NewInteropItem(tx))
return nil
}
// bcGetTransactionHeight returns transaction height.
func (ic *interopContext) bcGetTransactionHeight(v *vm.VM) error {
_, h, err := getTransactionAndHeight(ic.bc, v)
if err != nil {
return err
}
v.Estack().PushVal(h)
return nil
}
// popHeaderFromVM returns pointer to Header or error. It's main feature is
// proper treatment of Block structure, because C# code implicitly assumes
// that header APIs can also operate on blocks.
func popHeaderFromVM(v *vm.VM) (*Header, error) {
iface := v.Estack().Pop().Value()
header, ok := iface.(*Header)
if !ok {
block, ok := iface.(*Block)
if !ok {
return nil, errors.New("value is not a header or block")
}
return block.Header(), nil
}
return header, nil
}
// headerGetIndex returns block index from the header.
func (ic *interopContext) headerGetIndex(v *vm.VM) error {
header, err := popHeaderFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(header.Index)
return nil
}
// headerGetHash returns header hash of the passed header.
func (ic *interopContext) headerGetHash(v *vm.VM) error {
header, err := popHeaderFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(header.Hash().BytesReverse())
return nil
}
// headerGetPrevHash returns previous header hash of the passed header.
func (ic *interopContext) headerGetPrevHash(v *vm.VM) error {
header, err := popHeaderFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(header.PrevHash.BytesReverse())
return nil
}
// headerGetTimestamp returns timestamp of the passed header.
func (ic *interopContext) headerGetTimestamp(v *vm.VM) error {
header, err := popHeaderFromVM(v)
if err != nil {
return err
}
v.Estack().PushVal(header.Timestamp)
return nil
}
// blockGetTransactionCount returns transactions count in the given block.
func (ic *interopContext) blockGetTransactionCount(v *vm.VM) error {
blockInterface := v.Estack().Pop().Value()
block, ok := blockInterface.(*Block)
if !ok {
return errors.New("value is not a block")
}
v.Estack().PushVal(len(block.Transactions))
return nil
}
// blockGetTransactions returns transactions from the given block.
func (ic *interopContext) blockGetTransactions(v *vm.VM) error {
blockInterface := v.Estack().Pop().Value()
block, ok := blockInterface.(*Block)
if !ok {
return errors.New("value is not a block")
}
if len(block.Transactions) > vm.MaxArraySize {
return errors.New("too many transactions")
}
txes := make([]vm.StackItem, 0, len(block.Transactions))
for _, tx := range block.Transactions {
txes = append(txes, vm.NewInteropItem(tx))
}
v.Estack().PushVal(txes)
return nil
}
// blockGetTransaction returns transaction with the given number from the given
// block.
func (ic *interopContext) blockGetTransaction(v *vm.VM) error {
blockInterface := v.Estack().Pop().Value()
block, ok := blockInterface.(*Block)
if !ok {
return errors.New("value is not a block")
}
index := v.Estack().Pop().BigInt().Int64()
if index < 0 || index >= int64(len(block.Transactions)) {
return errors.New("wrong transaction index")
}
tx := block.Transactions[index]
v.Estack().PushVal(vm.NewInteropItem(tx))
return nil
}
// txGetHash returns transaction's hash.
func (ic *interopContext) txGetHash(v *vm.VM) error {
txInterface := v.Estack().Pop().Value()
tx, ok := txInterface.(*transaction.Transaction)
if !ok {
return errors.New("value is not a transaction")
}
v.Estack().PushVal(tx.Hash().BytesReverse())
return nil
}
// engineGetScriptContainer returns transaction that contains the script being
// run.
func (ic *interopContext) engineGetScriptContainer(v *vm.VM) error {
v.Estack().PushVal(vm.NewInteropItem(ic.tx))
return nil
}
// pushContextScriptHash returns script hash of the invocation stack element
// number n.
func getContextScriptHash(v *vm.VM, n int) util.Uint160 {
ctxIface := v.Istack().Peek(n).Value()
ctx := ctxIface.(*vm.Context)
return hash.Hash160(ctx.Program())
}
// pushContextScriptHash pushes to evaluation stack the script hash of the
// invocation stack element number n.
func pushContextScriptHash(v *vm.VM, n int) error {
h := getContextScriptHash(v, n)
v.Estack().PushVal(h.Bytes())
return nil
}
// engineGetExecutingScriptHash returns executing script hash.
func (ic *interopContext) engineGetExecutingScriptHash(v *vm.VM) error {
return pushContextScriptHash(v, 0)
}
// engineGetCallingScriptHash returns calling script hash.
func (ic *interopContext) engineGetCallingScriptHash(v *vm.VM) error {
return pushContextScriptHash(v, 1)
}
// engineGetEntryScriptHash returns entry script hash.
func (ic *interopContext) engineGetEntryScriptHash(v *vm.VM) error {
return pushContextScriptHash(v, v.Istack().Len()-1)
}
// runtimePlatform returns the name of the platform.
func (ic *interopContext) runtimePlatform(v *vm.VM) error {
v.Estack().PushVal([]byte("NEO"))
return nil
}
// runtimeGetTrigger returns the script trigger.
func (ic *interopContext) runtimeGetTrigger(v *vm.VM) error {
v.Estack().PushVal(ic.trigger)
return nil
}
// checkHashedWitness checks given hash against current list of script hashes
// for verifying in the interop context.
func (ic *interopContext) checkHashedWitness(hash util.Uint160) (bool, error) {
hashes, err := ic.bc.GetScriptHashesForVerifying(ic.tx)
if err != nil {
return false, gherr.Wrap(err, "failed to get script hashes")
}
for _, v := range hashes {
if hash.Equals(v) {
return true, nil
}
}
return false, nil
}
// checkKeyedWitness checks hash of signature check contract with a given public
// key against current list of script hashes for verifying in the interop context.
func (ic *interopContext) checkKeyedWitness(key *keys.PublicKey) (bool, error) {
script, err := smartcontract.CreateSignatureRedeemScript(key)
if err != nil {
return false, gherr.Wrap(err, "failed to create signature script for a key")
}
return ic.checkHashedWitness(hash.Hash160(script))
}
// runtimeCheckWitness should check witnesses.
func (ic *interopContext) runtimeCheckWitness(v *vm.VM) error {
var res bool
var err error
hashOrKey := v.Estack().Pop().Bytes()
hash, err := util.Uint160DecodeBytes(hashOrKey)
if err != nil {
key := &keys.PublicKey{}
err = key.DecodeBytes(hashOrKey)
if err != nil {
return errors.New("parameter given is neither a key nor a hash")
}
res, err = ic.checkKeyedWitness(key)
} else {
res, err = ic.checkHashedWitness(hash)
}
if err != nil {
return gherr.Wrap(err, "failed to check")
}
v.Estack().PushVal(res)
return nil
}
// runtimeNotify should pass stack item to the notify plugin to handle it, but
// in neo-go the only meaningful thing to do here is to log.
func (ic *interopContext) runtimeNotify(v *vm.VM) error {
msg := fmt.Sprintf("%q", v.Estack().Pop().Bytes())
log.Infof("script %s notifies: %s", getContextScriptHash(v, 0), msg)
return nil
}
// runtimeLog log the message passed.
func (ic *interopContext) runtimeLog(v *vm.VM) error {
msg := fmt.Sprintf("%q", v.Estack().Pop().Bytes())
log.Infof("script %s logs: %s", getContextScriptHash(v, 0), msg)
return nil
}
// runtimeGetTime returns timestamp of the block being verified, or the latest
// one in the blockchain if no block is given to interopContext.
func (ic *interopContext) runtimeGetTime(v *vm.VM) error {
var header *Header
if ic.block == nil {
var err error
header, err = ic.bc.GetHeader(ic.bc.CurrentBlockHash())
if err != nil {
return err
}
} else {
header = ic.block.Header()
}
v.Estack().PushVal(header.Timestamp)
return nil
}
/*
// runtimeSerialize should serialize given stack item.
func (ic *interopContext) runtimeSerialize(v *vm.VM) error {
panic("TODO")
}
// runtimeDeserialize should deserialize given stack item.
func (ic *interopContext) runtimeDeserialize(v *vm.VM) error {
panic("TODO")
}
*/
func (ic *interopContext) checkStorageContext(stc *StorageContext) error {
contract := ic.bc.GetContractState(stc.ScriptHash)
if contract == nil {
return errors.New("no contract found")
}
if !contract.HasStorage() {
return errors.New("contract can't have storage")
}
return nil
}
// storageDelete deletes stored key-value pair.
func (ic *interopContext) storageDelete(v *vm.VM) error {
if ic.trigger != 0x10 && ic.trigger != 0x11 {
return errors.New("can't delete when the trigger is not application")
}
stcInterface := v.Estack().Pop().Value()
stc, ok := stcInterface.(*StorageContext)
if !ok {
return fmt.Errorf("%T is not a StorageContext", stcInterface)
}
if stc.ReadOnly {
return errors.New("StorageContext is read only")
}
err := ic.checkStorageContext(stc)
if err != nil {
return err
}
key := v.Estack().Pop().Bytes()
si := getStorageItemFromStore(ic.mem, stc.ScriptHash, key)
if si == nil {
si = ic.bc.GetStorageItem(stc.ScriptHash, key)
}
if si != nil && si.IsConst {
return errors.New("storage item is constant")
}
return deleteStorageItemInStore(ic.mem, stc.ScriptHash, key)
}
// storageGet returns stored key-value pair.
func (ic *interopContext) storageGet(v *vm.VM) error {
stcInterface := v.Estack().Pop().Value()
stc, ok := stcInterface.(*StorageContext)
if !ok {
return fmt.Errorf("%T is not a StorageContext", stcInterface)
}
err := ic.checkStorageContext(stc)
if err != nil {
return err
}
key := v.Estack().Pop().Bytes()
si := getStorageItemFromStore(ic.mem, stc.ScriptHash, key)
if si == nil {
si = ic.bc.GetStorageItem(stc.ScriptHash, key)
}
if si != nil && si.Value != nil {
v.Estack().PushVal(si.Value)
} else {
v.Estack().PushVal([]byte{})
}
return nil
}
// storageGetContext returns storage context (scripthash).
func (ic *interopContext) storageGetContext(v *vm.VM) error {
sc := &StorageContext{
ScriptHash: getContextScriptHash(v, 0),
ReadOnly: false,
}
v.Estack().PushVal(vm.NewInteropItem(sc))
return nil
}
// storageGetReadOnlyContext returns read-only context (scripthash).
func (ic *interopContext) storageGetReadOnlyContext(v *vm.VM) error {
sc := &StorageContext{
ScriptHash: getContextScriptHash(v, 0),
ReadOnly: true,
}
v.Estack().PushVal(vm.NewInteropItem(sc))
return nil
}
func (ic *interopContext) putWithContextAndFlags(stc *StorageContext, key []byte, value []byte, isConst bool) error {
if ic.trigger != 0x10 && ic.trigger != 0x11 {
return errors.New("can't delete when the trigger is not application")
}
if len(key) > MaxStorageKeyLen {
return errors.New("key is too big")
}
if stc.ReadOnly {
return errors.New("StorageContext is read only")
}
err := ic.checkStorageContext(stc)
if err != nil {
return err
}
si := getStorageItemFromStore(ic.mem, stc.ScriptHash, key)
if si == nil {
si = ic.bc.GetStorageItem(stc.ScriptHash, key)
if si == nil {
si = &StorageItem{}
}
}
if si.IsConst {
return errors.New("storage item exists and is read-only")
}
si.Value = value
si.IsConst = isConst
return putStorageItemIntoStore(ic.mem, stc.ScriptHash, key, si)
}
// storagePutInternal is a unified implementation of storagePut and storagePutEx.
func (ic *interopContext) storagePutInternal(v *vm.VM, getFlag bool) error {
stcInterface := v.Estack().Pop().Value()
stc, ok := stcInterface.(*StorageContext)
if !ok {
return fmt.Errorf("%T is not a StorageContext", stcInterface)
}
key := v.Estack().Pop().Bytes()
value := v.Estack().Pop().Bytes()
var flag int
if getFlag {
flag = int(v.Estack().Pop().BigInt().Int64())
}
return ic.putWithContextAndFlags(stc, key, value, flag == 1)
}
// storagePut puts key-value pair into the storage.
func (ic *interopContext) storagePut(v *vm.VM) error {
return ic.storagePutInternal(v, false)
}
// storagePutEx puts key-value pair with given flags into the storage.
func (ic *interopContext) storagePutEx(v *vm.VM) error {
return ic.storagePutInternal(v, true)
}
// storageContextAsReadOnly sets given context to read-only mode.
func (ic *interopContext) storageContextAsReadOnly(v *vm.VM) error {
stcInterface := v.Estack().Pop().Value()
stc, ok := stcInterface.(*StorageContext)
if !ok {
return fmt.Errorf("%T is not a StorageContext", stcInterface)
}
if !stc.ReadOnly {
stx := &StorageContext{
ScriptHash: stc.ScriptHash,
ReadOnly: true,
}
stc = stx
}
v.Estack().PushVal(vm.NewInteropItem(stc))
return nil
}
// contractDestroy destroys a contract.
func (ic *interopContext) contractDestroy(v *vm.VM) error {
if ic.trigger != 0x10 {
return errors.New("can't destroy contract when not triggered by application")
}
hash := getContextScriptHash(v, 0)
cs := ic.bc.GetContractState(hash)
if cs == nil {
return nil
}
err := deleteContractStateInStore(ic.mem, hash)
if err != nil {
return err
}
if cs.HasStorage() {
siMap, err := ic.bc.GetStorageItems(hash)
if err != nil {
return err
}
for k := range siMap {
_ = deleteStorageItemInStore(ic.mem, hash, []byte(k))
}
}
return nil
}
// contractGetStorageContext retrieves StorageContext of a contract.
func (ic *interopContext) contractGetStorageContext(v *vm.VM) error {
csInterface := v.Estack().Pop().Value()
cs, ok := csInterface.(*ContractState)
if !ok {
return fmt.Errorf("%T is not a contract state", cs)
}
if getContractStateFromStore(ic.mem, cs.ScriptHash()) == nil {
return fmt.Errorf("contract was not created in this transaction")
}
stc := &StorageContext{
ScriptHash: cs.ScriptHash(),
}
v.Estack().PushVal(vm.NewInteropItem(stc))
return nil
}

218
pkg/core/interops.go Normal file
View file

@ -0,0 +1,218 @@
package core
/*
Interops are designed to run under VM's execute() panic protection, so it's OK
for them to do things like
smth := v.Estack().Pop().Bytes()
even though technically Pop() can return a nil pointer.
*/
import (
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/vm"
)
type interopContext struct {
bc Blockchainer
trigger byte
block *Block
tx *transaction.Transaction
mem *storage.MemoryStore
}
func newInteropContext(trigger byte, bc Blockchainer, block *Block, tx *transaction.Transaction) *interopContext {
mem := storage.NewMemoryStore()
return &interopContext{bc, trigger, block, tx, mem}
}
// All lists are sorted, keep 'em this way, please.
// getSystemInteropMap returns interop mappings for System namespace.
func (ic *interopContext) getSystemInteropMap() map[string]vm.InteropFuncPrice {
return map[string]vm.InteropFuncPrice{
"System.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1},
"System.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1},
"System.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1},
"System.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200},
"System.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100},
"System.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100},
"System.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1},
"System.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 200},
"System.Blockchain.GetTransactionHeight": {Func: ic.bcGetTransactionHeight, Price: 100},
"System.Contract.Destroy": {Func: ic.contractDestroy, Price: 1},
"System.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1},
"System.ExecutionEngine.GetCallingScriptHash": {Func: ic.engineGetCallingScriptHash, Price: 1},
"System.ExecutionEngine.GetEntryScriptHash": {Func: ic.engineGetEntryScriptHash, Price: 1},
"System.ExecutionEngine.GetExecutingScriptHash": {Func: ic.engineGetExecutingScriptHash, Price: 1},
"System.ExecutionEngine.GetScriptContainer": {Func: ic.engineGetScriptContainer, Price: 1},
"System.Header.GetHash": {Func: ic.headerGetHash, Price: 1},
"System.Header.GetIndex": {Func: ic.headerGetIndex, Price: 1},
"System.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1},
"System.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1},
"System.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200},
"System.Runtime.GetTime": {Func: ic.runtimeGetTime, Price: 1},
"System.Runtime.GetTrigger": {Func: ic.runtimeGetTrigger, Price: 1},
"System.Runtime.Log": {Func: ic.runtimeLog, Price: 1},
"System.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1},
"System.Runtime.Platform": {Func: ic.runtimePlatform, Price: 1},
"System.Storage.Delete": {Func: ic.storageDelete, Price: 100},
"System.Storage.Get": {Func: ic.storageGet, Price: 100},
"System.Storage.GetContext": {Func: ic.storageGetContext, Price: 1},
"System.Storage.GetReadOnlyContext": {Func: ic.storageGetReadOnlyContext, Price: 1},
"System.Storage.Put": {Func: ic.storagePut, Price: 0}, // These don't have static price in C# code.
"System.Storage.PutEx": {Func: ic.storagePutEx, Price: 0},
"System.StorageContext.AsReadOnly": {Func: ic.storageContextAsReadOnly, Price: 1},
"System.Transaction.GetHash": {Func: ic.txGetHash, Price: 1},
// "System.Runtime.Deserialize": {Func: ic.runtimeDeserialize, Price: 1},
// "System.Runtime.Serialize": {Func: ic.runtimeSerialize, Price: 1},
}
}
// getSystemInteropMap returns interop mappings for Neo and (legacy) AntShares namespaces.
func (ic *interopContext) getNeoInteropMap() map[string]vm.InteropFuncPrice {
return map[string]vm.InteropFuncPrice{
"Neo.Account.GetBalance": {Func: ic.accountGetBalance, Price: 1},
"Neo.Account.GetScriptHash": {Func: ic.accountGetScriptHash, Price: 1},
"Neo.Account.GetVotes": {Func: ic.accountGetVotes, Price: 1},
"Neo.Account.IsStandard": {Func: ic.accountIsStandard, Price: 100},
"Neo.Asset.Create": {Func: ic.assetCreate, Price: 0},
"Neo.Asset.GetAdmin": {Func: ic.assetGetAdmin, Price: 1},
"Neo.Asset.GetAmount": {Func: ic.assetGetAmount, Price: 1},
"Neo.Asset.GetAssetId": {Func: ic.assetGetAssetID, Price: 1},
"Neo.Asset.GetAssetType": {Func: ic.assetGetAssetType, Price: 1},
"Neo.Asset.GetAvailable": {Func: ic.assetGetAvailable, Price: 1},
"Neo.Asset.GetIssuer": {Func: ic.assetGetIssuer, Price: 1},
"Neo.Asset.GetOwner": {Func: ic.assetGetOwner, Price: 1},
"Neo.Asset.GetPrecision": {Func: ic.assetGetPrecision, Price: 1},
"Neo.Asset.Renew": {Func: ic.assetRenew, Price: 0},
"Neo.Attribute.GetData": {Func: ic.attrGetData, Price: 1},
"Neo.Attribute.GetUsage": {Func: ic.attrGetUsage, Price: 1},
"Neo.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1},
"Neo.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1},
"Neo.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1},
"Neo.Blockchain.GetAccount": {Func: ic.bcGetAccount, Price: 100},
"Neo.Blockchain.GetAsset": {Func: ic.bcGetAsset, Price: 100},
"Neo.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200},
"Neo.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100},
"Neo.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100},
"Neo.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1},
"Neo.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 100},
"Neo.Blockchain.GetTransactionHeight": {Func: ic.bcGetTransactionHeight, Price: 100},
"Neo.Contract.Create": {Func: ic.contractCreate, Price: 0},
"Neo.Contract.Destroy": {Func: ic.contractDestroy, Price: 1},
"Neo.Contract.GetScript": {Func: ic.contractGetScript, Price: 1},
"Neo.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1},
"Neo.Contract.IsPayable": {Func: ic.contractIsPayable, Price: 1},
"Neo.Contract.Migrate": {Func: ic.contractMigrate, Price: 0},
"Neo.Header.GetConsensusData": {Func: ic.headerGetConsensusData, Price: 1},
"Neo.Header.GetHash": {Func: ic.headerGetHash, Price: 1},
"Neo.Header.GetIndex": {Func: ic.headerGetIndex, Price: 1},
"Neo.Header.GetMerkleRoot": {Func: ic.headerGetMerkleRoot, Price: 1},
"Neo.Header.GetNextConsensus": {Func: ic.headerGetNextConsensus, Price: 1},
"Neo.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1},
"Neo.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1},
"Neo.Header.GetVersion": {Func: ic.headerGetVersion, Price: 1},
"Neo.Input.GetHash": {Func: ic.inputGetHash, Price: 1},
"Neo.Input.GetIndex": {Func: ic.inputGetIndex, Price: 1},
"Neo.Output.GetAssetId": {Func: ic.outputGetAssetID, Price: 1},
"Neo.Output.GetScriptHash": {Func: ic.outputGetScriptHash, Price: 1},
"Neo.Output.GetValue": {Func: ic.outputGetValue, Price: 1},
"Neo.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200},
"Neo.Runtime.GetTime": {Func: ic.runtimeGetTime, Price: 1},
"Neo.Runtime.GetTrigger": {Func: ic.runtimeGetTrigger, Price: 1},
"Neo.Runtime.Log": {Func: ic.runtimeLog, Price: 1},
"Neo.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1},
"Neo.Storage.Delete": {Func: ic.storageDelete, Price: 100},
"Neo.Storage.Get": {Func: ic.storageGet, Price: 100},
"Neo.Storage.GetContext": {Func: ic.storageGetContext, Price: 1},
"Neo.Storage.GetReadOnlyContext": {Func: ic.storageGetReadOnlyContext, Price: 1},
"Neo.Storage.Put": {Func: ic.storagePut, Price: 0},
"Neo.StorageContext.AsReadOnly": {Func: ic.storageContextAsReadOnly, Price: 1},
"Neo.Transaction.GetAttributes": {Func: ic.txGetAttributes, Price: 1},
"Neo.Transaction.GetHash": {Func: ic.txGetHash, Price: 1},
"Neo.Transaction.GetInputs": {Func: ic.txGetInputs, Price: 1},
"Neo.Transaction.GetOutputs": {Func: ic.txGetOutputs, Price: 1},
"Neo.Transaction.GetReferences": {Func: ic.txGetReferences, Price: 200},
"Neo.Transaction.GetType": {Func: ic.txGetType, Price: 1},
"Neo.Transaction.GetUnspentCoins": {Func: ic.txGetUnspentCoins, Price: 200},
"Neo.Transaction.GetWitnesses": {Func: ic.txGetWitnesses, Price: 200},
// "Neo.Blockchain.GetValidators": {Func: ic.bcGetValidators, Price: 200},
// "Neo.Enumerator.Concat": {Func: ic.enumeratorConcat, Price: 1},
// "Neo.Enumerator.Create": {Func: ic.enumeratorCreate, Price: 1},
// "Neo.Enumerator.Next": {Func: ic.enumeratorNext, Price: 1},
// "Neo.Enumerator.Value": {Func: ic.enumeratorValue, Price: 1},
// "Neo.InvocationTransaction.GetScript": {ic.invocationTx_GetScript, 1},
// "Neo.Iterator.Concat": {Func: ic.iteratorConcat, Price: 1},
// "Neo.Iterator.Create": {Func: ic.iteratorCreate, Price: 1},
// "Neo.Iterator.Key": {Func: ic.iteratorKey, Price: 1},
// "Neo.Iterator.Keys": {Func: ic.iteratorKeys, Price: 1},
// "Neo.Iterator.Values": {Func: ic.iteratorValues, Price: 1},
// "Neo.Runtime.Deserialize": {Func: ic.runtimeDeserialize, Price: 1},
// "Neo.Runtime.Serialize": {Func: ic.runtimeSerialize, Price: 1},
// "Neo.Storage.Find": {Func: ic.storageFind, Price: 1},
// "Neo.Witness.GetVerificationScript": {Func: ic.witnessGetVerificationScript, Price: 100},
// Aliases.
// "Neo.Iterator.Next": {Func: ic.enumeratorNext, Price: 1},
// "Neo.Iterator.Value": {Func: ic.enumeratorValue, Price: 1},
// Old compatibility APIs.
"AntShares.Account.GetBalance": {Func: ic.accountGetBalance, Price: 1},
"AntShares.Account.GetScriptHash": {Func: ic.accountGetScriptHash, Price: 1},
"AntShares.Account.GetVotes": {Func: ic.accountGetVotes, Price: 1},
"AntShares.Asset.Create": {Func: ic.assetCreate, Price: 0},
"AntShares.Asset.GetAdmin": {Func: ic.assetGetAdmin, Price: 1},
"AntShares.Asset.GetAmount": {Func: ic.assetGetAmount, Price: 1},
"AntShares.Asset.GetAssetId": {Func: ic.assetGetAssetID, Price: 1},
"AntShares.Asset.GetAssetType": {Func: ic.assetGetAssetType, Price: 1},
"AntShares.Asset.GetAvailable": {Func: ic.assetGetAvailable, Price: 1},
"AntShares.Asset.GetIssuer": {Func: ic.assetGetIssuer, Price: 1},
"AntShares.Asset.GetOwner": {Func: ic.assetGetOwner, Price: 1},
"AntShares.Asset.GetPrecision": {Func: ic.assetGetPrecision, Price: 1},
"AntShares.Asset.Renew": {Func: ic.assetRenew, Price: 0},
"AntShares.Attribute.GetData": {Func: ic.attrGetData, Price: 1},
"AntShares.Attribute.GetUsage": {Func: ic.attrGetUsage, Price: 1},
"AntShares.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1},
"AntShares.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1},
"AntShares.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1},
"AntShares.Blockchain.GetAccount": {Func: ic.bcGetAccount, Price: 100},
"AntShares.Blockchain.GetAsset": {Func: ic.bcGetAsset, Price: 100},
"AntShares.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200},
"AntShares.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100},
"AntShares.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100},
"AntShares.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1},
"AntShares.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 100},
"AntShares.Contract.Create": {Func: ic.contractCreate, Price: 0},
"AntShares.Contract.Destroy": {Func: ic.contractDestroy, Price: 1},
"AntShares.Contract.GetScript": {Func: ic.contractGetScript, Price: 1},
"AntShares.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1},
"AntShares.Contract.Migrate": {Func: ic.contractMigrate, Price: 0},
"AntShares.Header.GetConsensusData": {Func: ic.headerGetConsensusData, Price: 1},
"AntShares.Header.GetHash": {Func: ic.headerGetHash, Price: 1},
"AntShares.Header.GetMerkleRoot": {Func: ic.headerGetMerkleRoot, Price: 1},
"AntShares.Header.GetNextConsensus": {Func: ic.headerGetNextConsensus, Price: 1},
"AntShares.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1},
"AntShares.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1},
"AntShares.Header.GetVersion": {Func: ic.headerGetVersion, Price: 1},
"AntShares.Input.GetHash": {Func: ic.inputGetHash, Price: 1},
"AntShares.Input.GetIndex": {Func: ic.inputGetIndex, Price: 1},
"AntShares.Output.GetAssetId": {Func: ic.outputGetAssetID, Price: 1},
"AntShares.Output.GetScriptHash": {Func: ic.outputGetScriptHash, Price: 1},
"AntShares.Output.GetValue": {Func: ic.outputGetValue, Price: 1},
"AntShares.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200},
"AntShares.Runtime.Log": {Func: ic.runtimeLog, Price: 1},
"AntShares.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1},
"AntShares.Storage.Delete": {Func: ic.storageDelete, Price: 100},
"AntShares.Storage.Get": {Func: ic.storageGet, Price: 100},
"AntShares.Storage.GetContext": {Func: ic.storageGetContext, Price: 1},
"AntShares.Storage.Put": {Func: ic.storagePut, Price: 0},
"AntShares.Transaction.GetAttributes": {Func: ic.txGetAttributes, Price: 1},
"AntShares.Transaction.GetHash": {Func: ic.txGetHash, Price: 1},
"AntShares.Transaction.GetInputs": {Func: ic.txGetInputs, Price: 1},
"AntShares.Transaction.GetOutputs": {Func: ic.txGetOutputs, Price: 1},
"AntShares.Transaction.GetReferences": {Func: ic.txGetReferences, Price: 200},
"AntShares.Transaction.GetType": {Func: ic.txGetType, Price: 1},
// "AntShares.Blockchain.GetValidators": {Func: ic.bcGetValidators, Price: 200},
}
}

View file

@ -71,12 +71,26 @@ func (s *BoltDBStore) Get(key []byte) (val []byte, err error) {
return return
} }
// Delete implements the Store interface.
func (s *BoltDBStore) Delete(key []byte) error {
return s.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket(Bucket)
return b.Delete(key)
})
}
// PutBatch implements the Store interface. // PutBatch implements the Store interface.
func (s *BoltDBStore) PutBatch(batch Batch) error { func (s *BoltDBStore) PutBatch(batch Batch) error {
return s.db.Batch(func(tx *bbolt.Tx) error { return s.db.Batch(func(tx *bbolt.Tx) error {
b := tx.Bucket(Bucket) b := tx.Bucket(Bucket)
for k, v := range batch.(*MemoryBatch).m { for k, v := range batch.(*MemoryBatch).mem {
err := b.Put(*k, v) err := b.Put([]byte(k), v)
if err != nil {
return err
}
}
for k := range batch.(*MemoryBatch).del {
err := b.Delete([]byte(k))
if err != nil { if err != nil {
return err return err
} }

View file

@ -3,83 +3,12 @@ package storage
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestBoltDBBatch(t *testing.T) { func newBoltStoreForTesting(t *testing.T) Store {
boltDB := BoltDBStore{}
want := &MemoryBatch{m: map[*[]byte][]byte{}}
if got := boltDB.Batch(); !reflect.DeepEqual(got, want) {
t.Errorf("BoltDB Batch() = %v, want %v", got, want)
}
}
func TestBoltDBBatch_Len(t *testing.T) {
batch := &MemoryBatch{m: map[*[]byte][]byte{}}
want := len(map[*[]byte][]byte{})
assert.Equal(t, want, batch.Len())
}
func TestBoltDBBatch_PutBatchAndGet(t *testing.T) {
key := []byte("foo")
keycopy := make([]byte, len(key))
copy(keycopy, key)
value := []byte("bar")
valuecopy := make([]byte, len(value))
copy(valuecopy, value)
boltDBStore := openStore(t)
batch := boltDBStore.Batch()
batch.Put(keycopy, valuecopy)
copy(valuecopy, key)
copy(keycopy, value)
errPut := boltDBStore.PutBatch(batch)
assert.Nil(t, errPut, "Error while PutBatch")
result, err := boltDBStore.Get(key)
assert.Nil(t, err)
assert.Equal(t, value, result)
require.NoError(t, boltDBStore.Close())
}
func TestBoltDBBatch_PutAndGet(t *testing.T) {
key := []byte("foo")
value := []byte("bar")
boltDBStore := openStore(t)
errPut := boltDBStore.Put(key, value)
assert.Nil(t, errPut, "Error while Put")
result, err := boltDBStore.Get(key)
assert.Nil(t, err)
assert.Equal(t, value, result)
require.NoError(t, boltDBStore.Close())
}
func TestBoltDBStore_Seek(t *testing.T) {
key := []byte("foo")
value := []byte("bar")
boltDBStore := openStore(t)
errPut := boltDBStore.Put(key, value)
assert.Nil(t, errPut, "Error while Put")
boltDBStore.Seek(key, func(k, v []byte) {
assert.Equal(t, value, v)
})
require.NoError(t, boltDBStore.Close())
}
func openStore(t *testing.T) *BoltDBStore {
testFileName := "test_bolt_db" testFileName := "test_bolt_db"
file, err := ioutil.TempFile("", testFileName) file, err := ioutil.TempFile("", testFileName)
defer func() { defer func() {

View file

@ -48,6 +48,11 @@ func (s *LevelDBStore) Get(key []byte) ([]byte, error) {
return value, err return value, err
} }
// Delete implements the Store interface.
func (s *LevelDBStore) Delete(key []byte) error {
return s.db.Delete(key, nil)
}
// PutBatch implements the Store interface. // PutBatch implements the Store interface.
func (s *LevelDBStore) PutBatch(batch Batch) error { func (s *LevelDBStore) PutBatch(batch Batch) error {
lvldbBatch := batch.(*leveldb.Batch) lvldbBatch := batch.(*leveldb.Batch)

View file

@ -1 +1,41 @@
package storage package storage
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/require"
)
type tempLevelDB struct {
LevelDBStore
dir string
}
func (tldb *tempLevelDB) Close() error {
err := tldb.LevelDBStore.Close()
// Make test fail if failed to cleanup, even though technically it's
// not a LevelDBStore problem.
osErr := os.RemoveAll(tldb.dir)
if osErr != nil {
return osErr
}
return err
}
func newLevelDBForTesting(t *testing.T) Store {
ldbDir, err := ioutil.TempDir(os.TempDir(), "testleveldb")
require.Nil(t, err, "failed to setup temporary directory")
dbConfig := DBConfiguration{
Type: "leveldb",
LevelDBOptions: LevelDBOptions{
DataDirectoryPath: ldbDir,
},
}
newLevelStore, err := NewLevelDBStore(dbConfig.LevelDBOptions)
require.Nil(t, err, "NewLevelDBStore error")
tldb := &tempLevelDB{LevelDBStore: *newLevelStore, dir: ldbDir}
return tldb
}

View file

@ -1,7 +1,6 @@
package storage package storage
import ( import (
"encoding/hex"
"strings" "strings"
"sync" "sync"
) )
@ -11,31 +10,30 @@ import (
type MemoryStore struct { type MemoryStore struct {
mut sync.RWMutex mut sync.RWMutex
mem map[string][]byte mem map[string][]byte
// A map, not a slice, to avoid duplicates.
del map[string]bool
} }
// MemoryBatch a in-memory batch compatible with MemoryStore. // MemoryBatch a in-memory batch compatible with MemoryStore.
type MemoryBatch struct { type MemoryBatch struct {
m map[*[]byte][]byte MemoryStore
} }
// Put implements the Batch interface. // Put implements the Batch interface.
func (b *MemoryBatch) Put(k, v []byte) { func (b *MemoryBatch) Put(k, v []byte) {
vcopy := make([]byte, len(v)) _ = b.MemoryStore.Put(k, v)
copy(vcopy, v)
kcopy := make([]byte, len(k))
copy(kcopy, k)
b.m[&kcopy] = vcopy
} }
// Len implements the Batch interface. // Delete implements Batch interface.
func (b *MemoryBatch) Len() int { func (b *MemoryBatch) Delete(k []byte) {
return len(b.m) _ = b.MemoryStore.Delete(k)
} }
// NewMemoryStore creates a new MemoryStore object. // NewMemoryStore creates a new MemoryStore object.
func NewMemoryStore() *MemoryStore { func NewMemoryStore() *MemoryStore {
return &MemoryStore{ return &MemoryStore{
mem: make(map[string][]byte), mem: make(map[string][]byte),
del: make(map[string]bool),
} }
} }
@ -43,16 +41,42 @@ func NewMemoryStore() *MemoryStore {
func (s *MemoryStore) Get(key []byte) ([]byte, error) { func (s *MemoryStore) Get(key []byte) ([]byte, error) {
s.mut.RLock() s.mut.RLock()
defer s.mut.RUnlock() defer s.mut.RUnlock()
if val, ok := s.mem[makeKey(key)]; ok { if val, ok := s.mem[string(key)]; ok {
return val, nil return val, nil
} }
return nil, ErrKeyNotFound return nil, ErrKeyNotFound
} }
// put puts a key-value pair into the store, it's supposed to be called
// with mutex locked.
func (s *MemoryStore) put(key string, value []byte) {
s.mem[key] = value
delete(s.del, key)
}
// Put implements the Store interface. Never returns an error. // Put implements the Store interface. Never returns an error.
func (s *MemoryStore) Put(key, value []byte) error { func (s *MemoryStore) Put(key, value []byte) error {
newKey := string(key)
vcopy := make([]byte, len(value))
copy(vcopy, value)
s.mut.Lock() s.mut.Lock()
s.mem[makeKey(key)] = value s.put(newKey, vcopy)
s.mut.Unlock()
return nil
}
// drop deletes a key-valu pair from the store, it's supposed to be called
// with mutex locked.
func (s *MemoryStore) drop(key string) {
s.del[key] = true
delete(s.mem, key)
}
// Delete implements Store interface. Never returns an error.
func (s *MemoryStore) Delete(key []byte) error {
newKey := string(key)
s.mut.Lock()
s.drop(newKey)
s.mut.Unlock() s.mut.Unlock()
return nil return nil
} }
@ -60,8 +84,13 @@ func (s *MemoryStore) Put(key, value []byte) error {
// PutBatch implements the Store interface. Never returns an error. // PutBatch implements the Store interface. Never returns an error.
func (s *MemoryStore) PutBatch(batch Batch) error { func (s *MemoryStore) PutBatch(batch Batch) error {
b := batch.(*MemoryBatch) b := batch.(*MemoryBatch)
for k, v := range b.m { s.mut.Lock()
_ = s.Put(*k, v) defer s.mut.Unlock()
for k := range b.del {
s.drop(k)
}
for k, v := range b.mem {
s.put(k, v)
} }
return nil return nil
} }
@ -69,9 +98,8 @@ func (s *MemoryStore) PutBatch(batch Batch) error {
// Seek implements the Store interface. // Seek implements the Store interface.
func (s *MemoryStore) Seek(key []byte, f func(k, v []byte)) { func (s *MemoryStore) Seek(key []byte, f func(k, v []byte)) {
for k, v := range s.mem { for k, v := range s.mem {
if strings.Contains(k, hex.EncodeToString(key)) { if strings.HasPrefix(k, string(key)) {
decodeString, _ := hex.DecodeString(k) f([]byte(k), v)
f(decodeString, v)
} }
} }
} }
@ -83,9 +111,7 @@ func (s *MemoryStore) Batch() Batch {
// newMemoryBatch returns new memory batch. // newMemoryBatch returns new memory batch.
func newMemoryBatch() *MemoryBatch { func newMemoryBatch() *MemoryBatch {
return &MemoryBatch{ return &MemoryBatch{MemoryStore: *NewMemoryStore()}
m: make(map[*[]byte][]byte),
}
} }
// Persist flushes all the MemoryStore contents into the (supposedly) persistent // Persist flushes all the MemoryStore contents into the (supposedly) persistent
@ -94,18 +120,22 @@ func (s *MemoryStore) Persist(ps Store) (int, error) {
s.mut.Lock() s.mut.Lock()
defer s.mut.Unlock() defer s.mut.Unlock()
batch := ps.Batch() batch := ps.Batch()
keys := 0 keys, dkeys := 0, 0
for k, v := range s.mem { for k, v := range s.mem {
kb, _ := hex.DecodeString(k) batch.Put([]byte(k), v)
batch.Put(kb, v)
keys++ keys++
} }
for k := range s.del {
batch.Delete([]byte(k))
dkeys++
}
var err error var err error
if keys != 0 { if keys != 0 || dkeys != 0 {
err = ps.PutBatch(batch) err = ps.PutBatch(batch)
} }
if err == nil { if err == nil {
s.mem = make(map[string][]byte) s.mem = make(map[string][]byte)
s.del = make(map[string]bool)
} }
return keys, err return keys, err
} }
@ -114,11 +144,8 @@ func (s *MemoryStore) Persist(ps Store) (int, error) {
// error. // error.
func (s *MemoryStore) Close() error { func (s *MemoryStore) Close() error {
s.mut.Lock() s.mut.Lock()
s.del = nil
s.mem = nil s.mem = nil
s.mut.Unlock() s.mut.Unlock()
return nil return nil
} }
func makeKey(k []byte) string {
return hex.EncodeToString(k)
}

View file

@ -4,78 +4,8 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGetPut(t *testing.T) {
var (
s = NewMemoryStore()
key = []byte("sparse")
value = []byte("rocks")
)
if err := s.Put(key, value); err != nil {
t.Fatal(err)
}
newVal, err := s.Get(key)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, value, newVal)
require.NoError(t, s.Close())
}
func TestKeyNotExist(t *testing.T) {
var (
s = NewMemoryStore()
key = []byte("sparse")
)
_, err := s.Get(key)
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "key not found")
require.NoError(t, s.Close())
}
func TestPutBatch(t *testing.T) {
var (
s = NewMemoryStore()
key = []byte("sparse")
value = []byte("rocks")
batch = s.Batch()
)
batch.Put(key, value)
if err := s.PutBatch(batch); err != nil {
t.Fatal(err)
}
newVal, err := s.Get(key)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, value, newVal)
require.NoError(t, s.Close())
}
func TestMemoryStore_Seek(t *testing.T) {
var (
s = NewMemoryStore()
key = []byte("sparse")
value = []byte("rocks")
)
if err := s.Put(key, value); err != nil {
t.Fatal(err)
}
s.Seek(key, func(k, v []byte) {
assert.Equal(t, value, v)
})
}
func TestMemoryStorePersist(t *testing.T) { func TestMemoryStorePersist(t *testing.T) {
// temporary Store // temporary Store
ts := NewMemoryStore() ts := NewMemoryStore()
@ -125,4 +55,20 @@ func TestMemoryStorePersist(t *testing.T) {
c, err = ts.Persist(ps) c, err = ts.Persist(ps)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, 0, c) assert.Equal(t, 0, c)
// test persisting deletions
err = ts.Delete([]byte("key"))
assert.Equal(t, nil, err)
c, err = ts.Persist(ps)
assert.Equal(t, nil, err)
assert.Equal(t, 0, c)
v, err = ps.Get([]byte("key"))
assert.Equal(t, ErrKeyNotFound, err)
assert.Equal(t, []byte(nil), v)
v, err = ps.Get([]byte("key2"))
assert.Equal(t, nil, err)
assert.Equal(t, []byte("value2"), v)
}
func newMemoryStoreForTesting(t *testing.T) Store {
return NewMemoryStore()
} }

View file

@ -48,6 +48,12 @@ func (s *RedisStore) Get(k []byte) ([]byte, error) {
return []byte(val), nil return []byte(val), nil
} }
// Delete implements the Store interface.
func (s *RedisStore) Delete(k []byte) error {
s.client.Del(string(k))
return nil
}
// Put implements the Store interface. // Put implements the Store interface.
func (s *RedisStore) Put(k, v []byte) error { func (s *RedisStore) Put(k, v []byte) error {
s.client.Set(string(k), string(v), 0) s.client.Set(string(k), string(v), 0)
@ -57,8 +63,11 @@ func (s *RedisStore) Put(k, v []byte) error {
// PutBatch implements the Store interface. // PutBatch implements the Store interface.
func (s *RedisStore) PutBatch(b Batch) error { func (s *RedisStore) PutBatch(b Batch) error {
pipe := s.client.Pipeline() pipe := s.client.Pipeline()
for k, v := range b.(*MemoryBatch).m { for k, v := range b.(*MemoryBatch).mem {
pipe.Set(string(*k), v, 0) pipe.Set(k, v, 0)
}
for k := range b.(*MemoryBatch).del {
pipe.Del(k)
} }
_, err := pipe.Exec() _, err := pipe.Exec()
return err return err

View file

@ -4,109 +4,18 @@ import (
"testing" "testing"
"github.com/alicebob/miniredis" "github.com/alicebob/miniredis"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestNewRedisStore(t *testing.T) { type mockedRedisStore struct {
redisMock, redisStore := prepareRedisMock(t) RedisStore
key := []byte("testKey") mini *miniredis.Miniredis
value := []byte("testValue")
err := redisStore.Put(key, value)
assert.Nil(t, err, "NewRedisStore Put error")
result, err := redisStore.Get(key)
assert.Nil(t, err, "NewRedisStore Get error")
assert.Equal(t, value, result)
require.NoError(t, redisStore.Close())
redisMock.Close()
}
func TestRedisBatch_Len(t *testing.T) {
want := len(map[string]string{})
b := &MemoryBatch{
m: map[*[]byte][]byte{},
}
assert.Equal(t, len(b.m), want)
}
func TestRedisStore_GetAndPut(t *testing.T) {
prepareRedisMock(t)
type args struct {
k []byte
v []byte
kToLook []byte
}
tests := []struct {
name string
args args
want []byte
wantErr bool
}{
{"TestRedisStore_Get_Strings",
args{
k: []byte("foo"),
v: []byte("bar"),
kToLook: []byte("foo"),
},
[]byte("bar"),
false,
},
{"TestRedisStore_Get_Negative_Strings",
args{
k: []byte("foo"),
v: []byte("bar"),
kToLook: []byte("wrong"),
},
[]byte(nil),
true,
},
}
redisMock, redisStore := prepareRedisMock(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := redisStore.Put(tt.args.k, tt.args.v)
assert.Nil(t, err, "Got error while Put operation processing")
got, err := redisStore.Get(tt.args.kToLook)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
redisMock.FlushDB()
})
}
require.NoError(t, redisStore.Close())
redisMock.Close()
}
func TestRedisStore_PutBatch(t *testing.T) {
batch := &MemoryBatch{m: map[*[]byte][]byte{&[]byte{'f', 'o', 'o', '1'}: []byte("bar1")}}
mock, redisStore := prepareRedisMock(t)
err := redisStore.PutBatch(batch)
assert.Nil(t, err, "Error while PutBatch")
result, err := redisStore.Get([]byte("foo1"))
assert.Nil(t, err)
assert.Equal(t, []byte("bar1"), result)
require.NoError(t, redisStore.Close())
mock.Close()
}
func TestRedisStore_Seek(t *testing.T) {
mock, redisStore := prepareRedisMock(t)
redisStore.Seek([]byte("foo"), func(k, v []byte) {
assert.Equal(t, []byte("bar"), v)
})
require.NoError(t, redisStore.Close())
mock.Close()
} }
func prepareRedisMock(t *testing.T) (*miniredis.Miniredis, *RedisStore) { func prepareRedisMock(t *testing.T) (*miniredis.Miniredis, *RedisStore) {
miniRedis, err := miniredis.Run() miniRedis, err := miniredis.Run()
if err != nil { require.Nil(t, err, "MiniRedis mock creation error")
t.Errorf("MiniRedis mock creation error = %v", err)
}
_ = miniRedis.Set("foo", "bar") _ = miniRedis.Set("foo", "bar")
dbConfig := DBConfiguration{ dbConfig := DBConfiguration{
@ -118,9 +27,18 @@ func prepareRedisMock(t *testing.T) (*miniredis.Miniredis, *RedisStore) {
}, },
} }
newRedisStore, err := NewRedisStore(dbConfig.RedisDBOptions) newRedisStore, err := NewRedisStore(dbConfig.RedisDBOptions)
if err != nil { require.Nil(t, err, "NewRedisStore() error")
t.Errorf("NewRedisStore() error = %v", err)
return nil, nil
}
return miniRedis, newRedisStore return miniRedis, newRedisStore
} }
func (mrs *mockedRedisStore) Close() error {
err := mrs.RedisStore.Close()
mrs.mini.Close()
return err
}
func newRedisStoreForTesting(t *testing.T) Store {
mock, rs := prepareRedisMock(t)
mrs := &mockedRedisStore{RedisStore: *rs, mini: mock}
return mrs
}

View file

@ -32,6 +32,7 @@ type (
// information. // information.
Store interface { Store interface {
Batch() Batch Batch() Batch
Delete(k []byte) error
Get([]byte) ([]byte, error) Get([]byte) ([]byte, error)
Put(k, v []byte) error Put(k, v []byte) error
PutBatch(Batch) error PutBatch(Batch) error
@ -43,8 +44,8 @@ type (
// Each Store implementation is responsible of casting a Batch // Each Store implementation is responsible of casting a Batch
// to its appropriate type. // to its appropriate type.
Batch interface { Batch interface {
Delete(k []byte)
Put(k, v []byte) Put(k, v []byte)
Len() int
} }
// KeyPrefix is a constant byte added as a prefix for each key // KeyPrefix is a constant byte added as a prefix for each key

View file

@ -0,0 +1,239 @@
package storage
import (
"reflect"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type dbSetup struct {
name string
create func(*testing.T) Store
}
type dbTestFunction func(*testing.T, Store)
func testStoreClose(t *testing.T, s Store) {
require.NoError(t, s.Close())
}
func testStorePutAndGet(t *testing.T, s Store) {
key := []byte("foo")
value := []byte("bar")
require.NoError(t, s.Put(key, value))
result, err := s.Get(key)
assert.Nil(t, err)
require.Equal(t, value, result)
require.NoError(t, s.Close())
}
func testStoreGetNonExistent(t *testing.T, s Store) {
key := []byte("sparse")
_, err := s.Get(key)
assert.Equal(t, err, ErrKeyNotFound)
require.NoError(t, s.Close())
}
func testStorePutBatch(t *testing.T, s Store) {
var (
key = []byte("foo")
value = []byte("bar")
batch = s.Batch()
)
// Test that key and value are copied when batching.
keycopy := make([]byte, len(key))
copy(keycopy, key)
valuecopy := make([]byte, len(value))
copy(valuecopy, value)
batch.Put(keycopy, valuecopy)
copy(valuecopy, key)
copy(keycopy, value)
require.NoError(t, s.PutBatch(batch))
newVal, err := s.Get(key)
assert.Nil(t, err)
require.Equal(t, value, newVal)
assert.Equal(t, value, newVal)
require.NoError(t, s.Close())
}
func testStoreSeek(t *testing.T, s Store) {
type kvSeen struct {
key []byte
val []byte
seen bool
}
var (
// Given this prefix...
goodprefix = []byte{'f'}
// these pairs should be found...
goodkvs = []kvSeen{
{[]byte("foo"), []byte("bar"), false},
{[]byte("faa"), []byte("bra"), false},
{[]byte("foox"), []byte("barx"), false},
}
// and these should be not.
badkvs = []kvSeen{
{[]byte("doo"), []byte("pow"), false},
{[]byte("mew"), []byte("qaz"), false},
}
)
for _, v := range goodkvs {
require.NoError(t, s.Put(v.key, v.val))
}
for _, v := range badkvs {
require.NoError(t, s.Put(v.key, v.val))
}
numFound := 0
s.Seek(goodprefix, func(k, v []byte) {
for i := 0; i < len(goodkvs); i++ {
if string(k) == string(goodkvs[i].key) {
assert.Equal(t, string(goodkvs[i].val), string(v))
goodkvs[i].seen = true
}
}
for i := 0; i < len(badkvs); i++ {
if string(k) == string(badkvs[i].key) {
badkvs[i].seen = true
}
}
numFound++
})
assert.Equal(t, len(goodkvs), numFound)
for i := 0; i < len(goodkvs); i++ {
assert.Equal(t, true, goodkvs[i].seen)
}
for i := 0; i < len(badkvs); i++ {
assert.Equal(t, false, badkvs[i].seen)
}
require.NoError(t, s.Close())
}
func testStoreDeleteNonExistent(t *testing.T, s Store) {
key := []byte("sparse")
assert.NoError(t, s.Delete(key))
require.NoError(t, s.Close())
}
func testStorePutAndDelete(t *testing.T, s Store) {
key := []byte("foo")
value := []byte("bar")
require.NoError(t, s.Put(key, value))
err := s.Delete(key)
assert.Nil(t, err)
_, err = s.Get(key)
assert.NotNil(t, err)
assert.Equal(t, err, ErrKeyNotFound)
// Double delete.
err = s.Delete(key)
assert.Nil(t, err)
require.NoError(t, s.Close())
}
func testStorePutBatchWithDelete(t *testing.T, s Store) {
var (
toBeStored = map[string][]byte{
"foo": []byte("bar"),
"bar": []byte("baz"),
}
deletedInBatch = map[string][]byte{
"edc": []byte("rfv"),
"tgb": []byte("yhn"),
}
readdedToBatch = map[string][]byte{
"yhn": []byte("ujm"),
}
toBeDeleted = map[string][]byte{
"qaz": []byte("wsx"),
"qwe": []byte("123"),
}
toStay = map[string][]byte{
"key": []byte("val"),
"faa": []byte("bra"),
}
)
for k, v := range toBeDeleted {
require.NoError(t, s.Put([]byte(k), v))
}
for k, v := range toStay {
require.NoError(t, s.Put([]byte(k), v))
}
batch := s.Batch()
for k, v := range toBeStored {
batch.Put([]byte(k), v)
}
for k := range toBeDeleted {
batch.Delete([]byte(k))
}
for k, v := range readdedToBatch {
batch.Put([]byte(k), v)
}
for k, v := range deletedInBatch {
batch.Put([]byte(k), v)
}
for k := range deletedInBatch {
batch.Delete([]byte(k))
}
for k := range readdedToBatch {
batch.Delete([]byte(k))
}
for k, v := range readdedToBatch {
batch.Put([]byte(k), v)
}
require.NoError(t, s.PutBatch(batch))
toBe := []map[string][]byte{toStay, toBeStored, readdedToBatch}
notToBe := []map[string][]byte{deletedInBatch, toBeDeleted}
for _, kvs := range toBe {
for k, v := range kvs {
value, err := s.Get([]byte(k))
assert.Nil(t, err)
assert.Equal(t, value, v)
}
}
for _, kvs := range notToBe {
for k, v := range kvs {
_, err := s.Get([]byte(k))
assert.Equal(t, ErrKeyNotFound, err, "%s:%s", k, v)
}
}
require.NoError(t, s.Close())
}
func TestAllDBs(t *testing.T) {
var DBs = []dbSetup{
{"BoltDB", newBoltStoreForTesting},
{"LevelDB", newLevelDBForTesting},
{"Memory", newMemoryStoreForTesting},
{"RedisDB", newRedisStoreForTesting},
}
var tests = []dbTestFunction{testStoreClose, testStorePutAndGet,
testStoreGetNonExistent, testStorePutBatch, testStoreSeek,
testStoreDeleteNonExistent, testStorePutAndDelete,
testStorePutBatchWithDelete}
for _, db := range DBs {
for _, test := range tests {
s := db.create(t)
twrapper := func(t *testing.T) {
test(t, s)
}
fname := runtime.FuncForPC(reflect.ValueOf(test).Pointer()).Name()
t.Run(db.name+"/"+fname, twrapper)
}
}
}

64
pkg/core/storage_item.go Normal file
View file

@ -0,0 +1,64 @@
package core
import (
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/util"
)
// StorageItem is the value to be stored with read-only flag.
type StorageItem struct {
Value []byte
IsConst bool
}
// makeStorageItemKey returns a key used to store StorageItem in the DB.
func makeStorageItemKey(scripthash util.Uint160, key []byte) []byte {
return storage.AppendPrefix(storage.STStorage, append(scripthash.BytesReverse(), key...))
}
// getStorageItemFromStore returns StorageItem if it exists in the given Store.
func getStorageItemFromStore(s storage.Store, scripthash util.Uint160, key []byte) *StorageItem {
b, err := s.Get(makeStorageItemKey(scripthash, key))
if err != nil {
return nil
}
r := io.NewBinReaderFromBuf(b)
si := &StorageItem{}
si.DecodeBinary(r)
if r.Err != nil {
return nil
}
return si
}
// putStorageItemIntoStore puts given StorageItem for given script with given
// key into the given Store.
func putStorageItemIntoStore(s storage.Store, scripthash util.Uint160, key []byte, si *StorageItem) error {
buf := io.NewBufBinWriter()
si.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return buf.Err
}
return s.Put(makeStorageItemKey(scripthash, key), buf.Bytes())
}
// deleteStorageItemInStore drops storage item for the given script with the
// given key from the Store.
func deleteStorageItemInStore(s storage.Store, scripthash util.Uint160, key []byte) error {
return s.Delete(makeStorageItemKey(scripthash, key))
}
// EncodeBinary implements Serializable interface.
func (si *StorageItem) EncodeBinary(w *io.BinWriter) {
w.WriteBytes(si.Value)
w.WriteLE(si.IsConst)
}
// DecodeBinary implements Serializable interface.
func (si *StorageItem) DecodeBinary(r *io.BinReader) {
si.Value = r.ReadBytes()
r.ReadLE(&si.IsConst)
}

View file

@ -0,0 +1,26 @@
package core
import (
"testing"
"github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestPutGetDeleteStorageItem(t *testing.T) {
s := storage.NewMemoryStore()
si := &StorageItem{
Value: []byte("smth"),
}
key := []byte("key")
cHash, err := util.Uint160DecodeBytes([]byte("abcdefghijklmnopqrst"))
assert.Nil(t, err)
assert.NoError(t, putStorageItemIntoStore(s, cHash, key, si))
siRead := getStorageItemFromStore(s, cHash, key)
assert.NotNil(t, siRead)
assert.Equal(t, si, siRead)
assert.NoError(t, deleteStorageItemInStore(s, cHash, key))
siRead2 := getStorageItemFromStore(s, cHash, key)
assert.Nil(t, siRead2)
}

View file

@ -47,8 +47,8 @@ func NewPrivateKeyFromBytes(b []byte) (*PrivateKey, error) {
return &PrivateKey{b}, nil return &PrivateKey{b}, nil
} }
// NewPrivateKeyFromRawBytes returns a NEO PrivateKey from the ASN.1 serialized keys. // NewPrivateKeyFromASN1 returns a NEO PrivateKey from the ASN.1 serialized key.
func NewPrivateKeyFromRawBytes(b []byte) (*PrivateKey, error) { func NewPrivateKeyFromASN1(b []byte) (*PrivateKey, error) {
privkey, err := x509.ParseECPrivateKey(b) privkey, err := x509.ParseECPrivateKey(b)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -61,7 +61,7 @@ func NewPublicKeyFromString(s string) (*PublicKey, error) {
// Bytes returns the byte array representation of the public key. // Bytes returns the byte array representation of the public key.
func (p *PublicKey) Bytes() []byte { func (p *PublicKey) Bytes() []byte {
if p.isInfinity() { if p.IsInfinity() {
return []byte{0x00} return []byte{0x00}
} }
@ -78,8 +78,8 @@ func (p *PublicKey) Bytes() []byte {
return append([]byte{prefix}, paddedX...) return append([]byte{prefix}, paddedX...)
} }
// NewPublicKeyFromRawBytes returns a NEO PublicKey from the ASN.1 serialized keys. // NewPublicKeyFromASN1 returns a NEO PublicKey from the ASN.1 serialized key.
func NewPublicKeyFromRawBytes(data []byte) (*PublicKey, error) { func NewPublicKeyFromASN1(data []byte) (*PublicKey, error) {
var ( var (
err error err error
pubkey interface{} pubkey interface{}
@ -225,14 +225,14 @@ func (p *PublicKey) Verify(signature []byte, hash []byte) bool {
return ecdsa.Verify(publicKey, hash, rBytes, sBytes) return ecdsa.Verify(publicKey, hash, rBytes, sBytes)
} }
// isInfinity checks if point P is infinity on EllipticCurve ec. // IsInfinity checks if the key is infinite (null, basically).
func (p *PublicKey) isInfinity() bool { func (p *PublicKey) IsInfinity() bool {
return p.X == nil && p.Y == nil return p.X == nil && p.Y == nil
} }
// String implements the Stringer interface. // String implements the Stringer interface.
func (p *PublicKey) String() string { func (p *PublicKey) String() string {
if p.isInfinity() { if p.IsInfinity() {
return "00" return "00"
} }
bx := hex.EncodeToString(p.X.Bytes()) bx := hex.EncodeToString(p.X.Bytes())

View file

@ -56,6 +56,9 @@ func (chain testChain) HeaderHeight() uint32 {
func (chain testChain) GetBlock(hash util.Uint256) (*core.Block, error) { func (chain testChain) GetBlock(hash util.Uint256) (*core.Block, error) {
panic("TODO") panic("TODO")
} }
func (chain testChain) GetContractState(hash util.Uint160) *core.ContractState {
panic("TODO")
}
func (chain testChain) GetHeaderHash(int) util.Uint256 { func (chain testChain) GetHeaderHash(int) util.Uint256 {
return util.Uint256{} return util.Uint256{}
} }
@ -69,6 +72,15 @@ func (chain testChain) GetAssetState(util.Uint256) *core.AssetState {
func (chain testChain) GetAccountState(util.Uint160) *core.AccountState { func (chain testChain) GetAccountState(util.Uint160) *core.AccountState {
panic("TODO") panic("TODO")
} }
func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) {
panic("TODO")
}
func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *core.StorageItem {
panic("TODO")
}
func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*core.StorageItem, error) {
panic("TODO")
}
func (chain testChain) CurrentHeaderHash() util.Uint256 { func (chain testChain) CurrentHeaderHash() util.Uint256 {
return util.Uint256{} return util.Uint256{}
} }
@ -85,6 +97,10 @@ func (chain testChain) GetTransaction(util.Uint256) (*transaction.Transaction, u
panic("TODO") panic("TODO")
} }
func (chain testChain) GetUnspentCoinState(util.Uint256) *core.UnspentCoinState {
panic("TODO")
}
func (chain testChain) GetMemPool() core.MemPool { func (chain testChain) GetMemPool() core.MemPool {
panic("TODO") panic("TODO")
} }
@ -93,7 +109,7 @@ func (chain testChain) IsLowPriority(*transaction.Transaction) bool {
panic("TODO") panic("TODO")
} }
func (chain testChain) Verify(*transaction.Transaction) error { func (chain testChain) VerifyTx(*transaction.Transaction, *core.Block) error {
panic("TODO") panic("TODO")
} }

View file

@ -399,7 +399,7 @@ func (s *Server) RelayTxn(t *transaction.Transaction) RelayReason {
if s.chain.HasTransaction(t.Hash()) { if s.chain.HasTransaction(t.Hash()) {
return RelayAlreadyExists return RelayAlreadyExists
} }
if err := s.chain.Verify(t); err != nil { if err := s.chain.VerifyTx(t, nil); err != nil {
return RelayInvalid return RelayInvalid
} }
// TODO: Implement Plugin.CheckPolicy? // TODO: Implement Plugin.CheckPolicy?

View file

@ -3,18 +3,16 @@ package rpc
import ( import (
"context" "context"
"net/http" "net/http"
"os"
"testing" "testing"
"time"
"github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/config"
"github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core"
"github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/network" "github.com/CityOfZion/neo-go/pkg/network"
"github.com/CityOfZion/neo-go/pkg/rpc/result" "github.com/CityOfZion/neo-go/pkg/rpc/result"
"github.com/CityOfZion/neo-go/pkg/rpc/wrappers" "github.com/CityOfZion/neo-go/pkg/rpc/wrappers"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -142,6 +140,8 @@ type GetAccountStateResponse struct {
} }
func initServerWithInMemoryChain(ctx context.Context, t *testing.T) (*core.Blockchain, http.HandlerFunc) { func initServerWithInMemoryChain(ctx context.Context, t *testing.T) (*core.Blockchain, http.HandlerFunc) {
var nBlocks uint32
net := config.ModeUnitTestNet net := config.ModeUnitTestNet
configPath := "../../config" configPath := "../../config"
cfg, err := config.Load(configPath, net) cfg, err := config.Load(configPath, net)
@ -152,7 +152,18 @@ func initServerWithInMemoryChain(ctx context.Context, t *testing.T) (*core.Block
require.NoError(t, err, "could not create chain") require.NoError(t, err, "could not create chain")
go chain.Run(ctx) go chain.Run(ctx)
initBlocks(t, chain)
f, err := os.Open("testdata/50testblocks.acc")
require.Nil(t, err)
br := io.NewBinReaderFromIO(f)
br.ReadLE(&nBlocks)
require.Nil(t, br.Err)
for i := 0; i < int(nBlocks); i++ {
block := &core.Block{}
block.DecodeBinary(br)
require.Nil(t, br.Err)
require.NoError(t, chain.AddBlock(block))
}
serverConfig := network.NewServerConfig(cfg) serverConfig := network.NewServerConfig(cfg)
server := network.NewServer(serverConfig, chain) server := network.NewServer(serverConfig, chain)
@ -161,44 +172,3 @@ func initServerWithInMemoryChain(ctx context.Context, t *testing.T) (*core.Block
return chain, handler return chain, handler
} }
func initBlocks(t *testing.T, chain *core.Blockchain) {
blocks := makeBlocks(10)
for i := 0; i < len(blocks); i++ {
require.NoError(t, chain.AddBlock(blocks[i]))
}
}
func makeBlocks(n int) []*core.Block {
blocks := make([]*core.Block, n)
for i := 0; i < n; i++ {
blocks[i] = newBlock(uint32(i+1), newTX(transaction.MinerType))
}
return blocks
}
func newTX(t transaction.TXType) *transaction.Transaction {
return &transaction.Transaction{
Type: t,
}
}
func newBlock(index uint32, txs ...*transaction.Transaction) *core.Block {
b := &core.Block{
BlockBase: core.BlockBase{
Version: 0,
PrevHash: hash.Sha256([]byte("a")),
MerkleRoot: hash.Sha256([]byte("b")),
Timestamp: uint32(time.Now().UTC().Unix()),
Index: index,
ConsensusData: 1111,
NextConsensus: util.Uint160{},
Script: &transaction.Witness{
VerificationScript: []byte{0x0},
InvocationScript: []byte{0x1},
},
},
Transactions: txs,
}
return b
}

BIN
pkg/rpc/testdata/50testblocks.acc vendored Normal file

Binary file not shown.

View file

@ -9,6 +9,21 @@ import (
"github.com/CityOfZion/neo-go/pkg/vm" "github.com/CityOfZion/neo-go/pkg/vm"
) )
// CreateSignatureRedeemScript creates a check signature script runnable by VM.
func CreateSignatureRedeemScript(key *keys.PublicKey) ([]byte, error) {
buf := new(bytes.Buffer)
err := vm.EmitBytes(buf, key.Bytes())
if err != nil {
return nil, err
}
err = vm.EmitOpcode(buf, vm.CHECKSIG)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// CreateMultiSigRedeemScript will create a script runnable by the VM. // CreateMultiSigRedeemScript will create a script runnable by the VM.
func CreateMultiSigRedeemScript(m int, publicKeys keys.PublicKeys) ([]byte, error) { func CreateMultiSigRedeemScript(m int, publicKeys keys.PublicKeys) ([]byte, error) {
if m <= 1 { if m <= 1 {

View file

@ -18,6 +18,17 @@ const (
ArrayType ArrayType
) )
// PropertyState represents contract properties (flags).
type PropertyState byte
// List of supported properties.
const (
NoProperties = 0
HasStorage PropertyState = 1 << iota
HasDynamicInvoke
IsPayable
)
// Parameter represents a smart contract parameter. // Parameter represents a smart contract parameter.
type Parameter struct { type Parameter struct {
// Type of the parameter // Type of the parameter

98
pkg/vm/contract_checks.go Normal file
View file

@ -0,0 +1,98 @@
package vm
import (
"encoding/binary"
)
func getNumOfThingsFromInstr(instr Instruction, param []byte) (int, bool) {
var nthings int
switch instr {
case PUSH1, PUSH2, PUSH3, PUSH4, PUSH5, PUSH6, PUSH7, PUSH8,
PUSH9, PUSH10, PUSH11, PUSH12, PUSH13, PUSH14, PUSH15, PUSH16:
nthings = int(instr-PUSH1) + 1
case PUSHBYTES1:
nthings = int(param[0])
case PUSHBYTES2:
nthings = int(binary.LittleEndian.Uint16(param))
default:
return 0, false
}
if nthings < 1 || nthings > MaxArraySize {
return 0, false
}
return nthings, true
}
// IsMultiSigContract checks whether the passed script is a multi-signature
// contract.
func IsMultiSigContract(script []byte) bool {
var nsigs, nkeys int
ctx := NewContext(script)
instr, param, err := ctx.Next()
if err != nil {
return false
}
nsigs, ok := getNumOfThingsFromInstr(instr, param)
if !ok {
return false
}
for {
instr, param, err = ctx.Next()
if err != nil {
return false
}
if instr != PUSHBYTES33 {
break
}
nkeys++
if nkeys > MaxArraySize {
return false
}
}
if nkeys < nsigs {
return false
}
nkeys2, ok := getNumOfThingsFromInstr(instr, param)
if !ok {
return false
}
if nkeys2 != nkeys {
return false
}
instr, _, err = ctx.Next()
if err != nil || instr != CHECKMULTISIG {
return false
}
instr, _, err = ctx.Next()
if err != nil || instr != RET || ctx.ip != len(script) {
return false
}
return true
}
// IsSignatureContract checks whether the passed script is a signature check
// contract.
func IsSignatureContract(script []byte) bool {
ctx := NewContext(script)
instr, _, err := ctx.Next()
if err != nil || instr != PUSHBYTES33 {
return false
}
instr, _, err = ctx.Next()
if err != nil || instr != CHECKSIG {
return false
}
instr, _, err = ctx.Next()
if err != nil || instr != RET || ctx.ip != len(script) {
return false
}
return true
}
// IsStandardContract checks whether the passed script is a signature or
// multi-signature contract.
func IsStandardContract(script []byte) bool {
return IsSignatureContract(script) || IsMultiSigContract(script)
}

View file

@ -0,0 +1,227 @@
package vm
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsSignatureContractGood(t *testing.T) {
prog := make([]byte, 35)
prog[0] = byte(PUSHBYTES33)
prog[34] = byte(CHECKSIG)
assert.Equal(t, true, IsSignatureContract(prog))
assert.Equal(t, true, IsStandardContract(prog))
}
func TestIsSignatureContractBadNoCheckSig(t *testing.T) {
prog := make([]byte, 34)
prog[0] = byte(PUSHBYTES33)
assert.Equal(t, false, IsSignatureContract(prog))
assert.Equal(t, false, IsStandardContract(prog))
}
func TestIsSignatureContractBadNoCheckSig2(t *testing.T) {
prog := make([]byte, 35)
prog[0] = byte(PUSHBYTES33)
prog[34] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsSignatureContract(prog))
}
func TestIsSignatureContractBadWrongPush(t *testing.T) {
prog := make([]byte, 35)
prog[0] = byte(PUSHBYTES32)
prog[33] = byte(NOP)
prog[34] = byte(CHECKSIG)
assert.Equal(t, false, IsSignatureContract(prog))
}
func TestIsSignatureContractBadWrongInstr(t *testing.T) {
prog := make([]byte, 30)
prog[0] = byte(PUSHBYTES33)
assert.Equal(t, false, IsSignatureContract(prog))
}
func TestIsSignatureContractBadExcessiveInstr(t *testing.T) {
prog := make([]byte, 36)
prog[0] = byte(PUSHBYTES33)
prog[34] = byte(CHECKSIG)
prog[35] = byte(RET)
assert.Equal(t, false, IsSignatureContract(prog))
}
func TestIsMultiSigContractGood(t *testing.T) {
prog := make([]byte, 71)
prog[0] = byte(PUSH2)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(PUSH2)
prog[70] = byte(CHECKMULTISIG)
assert.Equal(t, true, IsMultiSigContract(prog))
assert.Equal(t, true, IsStandardContract(prog))
}
func TestIsMultiSigContractGoodPushBytes1(t *testing.T) {
prog := make([]byte, 73)
prog[0] = byte(PUSHBYTES1)
prog[1] = 2
prog[2] = byte(PUSHBYTES33)
prog[36] = byte(PUSHBYTES33)
prog[70] = byte(PUSHBYTES1)
prog[71] = 2
prog[72] = byte(CHECKMULTISIG)
assert.Equal(t, true, IsMultiSigContract(prog))
}
func TestIsMultiSigContractGoodPushBytes2(t *testing.T) {
prog := make([]byte, 75)
prog[0] = byte(PUSHBYTES2)
prog[1] = 2
prog[3] = byte(PUSHBYTES33)
prog[37] = byte(PUSHBYTES33)
prog[71] = byte(PUSHBYTES2)
prog[72] = 2
prog[74] = byte(CHECKMULTISIG)
assert.Equal(t, true, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadNSigs1(t *testing.T) {
prog := make([]byte, 71)
prog[0] = byte(PUSH0)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(PUSH2)
prog[70] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
assert.Equal(t, false, IsStandardContract(prog))
}
func TestIsMultiSigContractBadNSigs2(t *testing.T) {
prog := make([]byte, 73)
prog[0] = byte(PUSHBYTES2)
prog[1] = 0xff
prog[2] = 0xff
prog[3] = byte(PUSHBYTES33)
prog[37] = byte(PUSHBYTES33)
prog[71] = byte(PUSH2)
prog[72] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadNSigs3(t *testing.T) {
prog := make([]byte, 71)
prog[0] = byte(PUSH5)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(PUSH2)
prog[70] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadExcessiveNOP1(t *testing.T) {
prog := make([]byte, 72)
prog[0] = byte(PUSH2)
prog[1] = byte(NOP)
prog[2] = byte(PUSHBYTES33)
prog[36] = byte(PUSHBYTES33)
prog[70] = byte(PUSH2)
prog[71] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadExcessiveNOP2(t *testing.T) {
prog := make([]byte, 72)
prog[0] = byte(PUSH2)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(NOP)
prog[36] = byte(PUSHBYTES33)
prog[70] = byte(PUSH2)
prog[71] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadExcessiveNOP3(t *testing.T) {
prog := make([]byte, 72)
prog[0] = byte(PUSH2)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(NOP)
prog[70] = byte(PUSH2)
prog[71] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadExcessiveNOP4(t *testing.T) {
prog := make([]byte, 72)
prog[0] = byte(PUSH2)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(PUSH2)
prog[70] = byte(NOP)
prog[71] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadExcessiveNOP5(t *testing.T) {
prog := make([]byte, 72)
prog[0] = byte(PUSH2)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(PUSH2)
prog[70] = byte(CHECKMULTISIG)
prog[71] = byte(NOP)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadNKeys1(t *testing.T) {
prog := make([]byte, 71)
prog[0] = byte(PUSH2)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(PUSH3)
prog[70] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadNKeys2(t *testing.T) {
prog := make([]byte, 1)
prog[0] = byte(PUSH10)
key := make([]byte, 33)
var asize = uint16(MaxArraySize + 1)
for i := 0; i < int(asize); i++ {
prog = append(prog, byte(PUSHBYTES33))
prog = append(prog, key...)
}
prog = append(prog, byte(PUSHBYTES2), byte(asize&0xff), byte((asize<<8)&0xff), byte(CHECKMULTISIG))
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadRead1(t *testing.T) {
prog := make([]byte, 71)
prog[0] = byte(PUSHBYTES75)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(PUSH2)
prog[70] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadRead2(t *testing.T) {
prog := make([]byte, 71)
prog[0] = byte(PUSH2)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES75)
prog[69] = byte(PUSH2)
prog[70] = byte(CHECKMULTISIG)
assert.Equal(t, false, IsMultiSigContract(prog))
}
func TestIsMultiSigContractBadRead3(t *testing.T) {
prog := make([]byte, 71)
prog[0] = byte(PUSH2)
prog[1] = byte(PUSHBYTES33)
prog[35] = byte(PUSHBYTES33)
prog[69] = byte(PUSH2)
prog[70] = byte(PUSHBYTES1)
assert.Equal(t, false, IsMultiSigContract(prog))
}

View file

@ -1,6 +1,7 @@
package vm package vm
import ( import (
"encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/big" "math/big"
@ -23,10 +24,26 @@ func makeStackItem(v interface{}) StackItem {
return &BigIntegerItem{ return &BigIntegerItem{
value: big.NewInt(val), value: big.NewInt(val),
} }
case uint8:
return &BigIntegerItem{
value: big.NewInt(int64(val)),
}
case uint16:
return &BigIntegerItem{
value: big.NewInt(int64(val)),
}
case uint32: case uint32:
return &BigIntegerItem{ return &BigIntegerItem{
value: big.NewInt(int64(val)), value: big.NewInt(int64(val)),
} }
case uint64:
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, val)
bigInt := big.NewInt(0)
bigInt.SetBytes(b)
return &BigIntegerItem{
value: bigInt,
}
case []byte: case []byte:
return &ByteArrayItem{ return &ByteArrayItem{
value: val, value: val,