From 2cdfee211a4b58e511acbad78cc4ae4842d74983 Mon Sep 17 00:00:00 2001 From: Anthony De Meulemeester Date: Mon, 16 Apr 2018 22:15:30 +0200 Subject: [PATCH] Persisting more states (#71) * added persistence of assets and spentcoins. * contract params * bumped version --- VERSION | 2 +- pkg/core/account_state.go | 2 +- pkg/core/asset_state.go | 131 +++++++++++++++++++++++++++ pkg/core/asset_state_test.go | 34 +++++++ pkg/core/blockchain.go | 57 +++++++++--- pkg/core/contract_state.go | 23 +++++ pkg/core/spent_coin_state.go | 116 ++++++++++++++++++++++++ pkg/core/spent_coin_state_test.go | 51 +++++++++++ pkg/core/unspent_coin_state.go | 13 ++- pkg/core/unspent_coint_state_test.go | 4 +- pkg/core/util.go | 4 - 11 files changed, 414 insertions(+), 23 deletions(-) create mode 100644 pkg/core/asset_state.go create mode 100644 pkg/core/asset_state_test.go create mode 100644 pkg/core/contract_state.go create mode 100644 pkg/core/spent_coin_state.go create mode 100644 pkg/core/spent_coin_state_test.go diff --git a/VERSION b/VERSION index 72a8a6313..9ed317fb4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.41.0 +0.41.1 diff --git a/pkg/core/account_state.go b/pkg/core/account_state.go index c22fa7380..7bc02817e 100644 --- a/pkg/core/account_state.go +++ b/pkg/core/account_state.go @@ -14,7 +14,7 @@ import ( // Accounts is mapping between a account address and AccountState. type Accounts map[util.Uint160]*AccountState -func (a Accounts) getAndChange(s storage.Store, hash util.Uint160) (*AccountState, error) { +func (a Accounts) getAndUpdate(s storage.Store, hash util.Uint160) (*AccountState, error) { if account, ok := a[hash]; ok { return account, nil } diff --git a/pkg/core/asset_state.go b/pkg/core/asset_state.go new file mode 100644 index 000000000..fff5e6992 --- /dev/null +++ b/pkg/core/asset_state.go @@ -0,0 +1,131 @@ +package core + +import ( + "bytes" + "encoding/binary" + "io" + + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto" + "github.com/CityOfZion/neo-go/pkg/util" +) + +const feeMode = 0x0 + +// Assets is mapping between AssetID and the AssetState. +type Assets map[util.Uint256]*AssetState + +func (a Assets) commit(b storage.Batch) error { + buf := new(bytes.Buffer) + for hash, state := range a { + if err := state.EncodeBinary(buf); err != nil { + return err + } + key := storage.AppendPrefix(storage.STAsset, hash.Bytes()) + b.Put(key, buf.Bytes()) + buf.Reset() + } + return nil +} + +// AssetState represents the state of an NEO registerd Asset. +type AssetState struct { + ID util.Uint256 + AssetType transaction.AssetType + Name string + Amount util.Fixed8 + Available util.Fixed8 + Precision uint8 + FeeMode uint8 + Owner *crypto.PublicKey + Admin util.Uint160 + Issuer util.Uint160 + Expiration uint32 + IsFrozen bool +} + +// DecodeBinary implements the Payload interface. +func (a *AssetState) DecodeBinary(r io.Reader) error { + if err := binary.Read(r, binary.LittleEndian, &a.ID); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &a.AssetType); err != nil { + return err + } + + var err error + a.Name, err = util.ReadVarString(r) + if err != nil { + return err + } + + if err := binary.Read(r, binary.LittleEndian, &a.Amount); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &a.Available); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &a.Precision); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &a.FeeMode); err != nil { + return err + } + + a.Owner = &crypto.PublicKey{} + if err := a.Owner.DecodeBinary(r); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &a.Admin); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &a.Issuer); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &a.Expiration); err != nil { + return err + } + return binary.Read(r, binary.LittleEndian, &a.IsFrozen) +} + +// EncodeBinary implements the Payload interface. +func (a *AssetState) EncodeBinary(w io.Writer) error { + if err := binary.Write(w, binary.LittleEndian, a.ID); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, a.AssetType); err != nil { + return err + } + if err := util.WriteVarUint(w, uint64(len(a.Name))); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, []byte(a.Name)); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, a.Amount); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, a.Available); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, a.Precision); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, a.FeeMode); err != nil { + return err + } + if err := a.Owner.EncodeBinary(w); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, a.Admin); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, a.Issuer); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, a.Expiration); err != nil { + return err + } + return binary.Write(w, binary.LittleEndian, a.IsFrozen) +} diff --git a/pkg/core/asset_state_test.go b/pkg/core/asset_state_test.go new file mode 100644 index 000000000..802152c9a --- /dev/null +++ b/pkg/core/asset_state_test.go @@ -0,0 +1,34 @@ +package core + +import ( + "bytes" + "testing" + + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeAssetState(t *testing.T) { + asset := &AssetState{ + ID: util.RandomUint256(), + AssetType: transaction.Token, + Name: "super cool token", + Amount: util.Fixed8(1000000), + Available: util.Fixed8(100), + Precision: 0, + FeeMode: feeMode, + Owner: &crypto.PublicKey{}, + Admin: util.RandomUint160(), + Issuer: util.RandomUint160(), + Expiration: 10, + IsFrozen: false, + } + + buf := new(bytes.Buffer) + assert.Nil(t, asset.EncodeBinary(buf)) + assetDecode := &AssetState{} + assert.Nil(t, assetDecode.DecodeBinary(buf)) + assert.Equal(t, asset, assetDecode) +} diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f114f482a..fc742b079 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -264,7 +264,9 @@ func (bc *Blockchain) persistBlock(block *Block) error { var ( batch = bc.Batch() unspentCoins = make(UnspentCoins) + spentCoins = make(SpentCoins) accounts = make(Accounts) + assets = make(Assets) ) storeAsBlock(batch, block, 0) @@ -272,21 +274,14 @@ func (bc *Blockchain) persistBlock(block *Block) error { for _, tx := range block.Transactions { storeAsTransaction(batch, tx, block.Index) - - // Add CoinStateConfirmed for each tx output. - unspent := make([]CoinState, len(tx.Outputs)) - for i := 0; i < len(tx.Outputs); i++ { - unspent[i] = CoinStateConfirmed - } - unspentCoins[tx.Hash()] = &UnspentCoinState{unspent} + unspentCoins[tx.Hash()] = NewUnspentCoinState(len(tx.Outputs)) // Process TX outputs. for _, output := range tx.Outputs { - account, err := accounts.getAndChange(bc.Store, output.ScriptHash) + account, err := accounts.getAndUpdate(bc.Store, output.ScriptHash) if err != nil { return err } - if _, ok := account.Balances[output.AssetID]; ok { account.Balances[output.AssetID] += output.Amount } else { @@ -296,37 +291,65 @@ func (bc *Blockchain) persistBlock(block *Block) error { // Process TX inputs that are grouped by previous hash. for prevHash, inputs := range tx.GroupInputsByPrevHash() { - prevTX, _, err := bc.GetTransaction(prevHash) + prevTX, prevTXHeight, err := bc.GetTransaction(prevHash) if err != nil { return fmt.Errorf("could not find previous TX: %s", prevHash) } for _, input := range inputs { - unspent, err := unspentCoins.getAndChange(bc.Store, input.PrevHash) + unspent, err := unspentCoins.getAndUpdate(bc.Store, input.PrevHash) if err != nil { return err } unspent.states[input.PrevIndex] = CoinStateSpent prevTXOutput := prevTX.Outputs[input.PrevIndex] - account, err := accounts.getAndChange(bc.Store, prevTXOutput.ScriptHash) + account, err := accounts.getAndUpdate(bc.Store, prevTXOutput.ScriptHash) if err != nil { return err } + if prevTXOutput.AssetID.Equals(governingTokenTX().Hash()) { + spentCoin := NewSpentCoinState(input.PrevHash, prevTXHeight) + spentCoin.items[input.PrevIndex] = block.Index + spentCoins[input.PrevHash] = spentCoin + } + account.Balances[prevTXOutput.AssetID] -= prevTXOutput.Amount } } // Process the underlying type of the TX. - switch tx.Data.(type) { + switch t := tx.Data.(type) { case *transaction.RegisterTX: + assets[tx.Hash()] = &AssetState{ + ID: tx.Hash(), + AssetType: t.AssetType, + Name: t.Name, + Amount: t.Amount, + Precision: t.Precision, + Owner: t.Owner, + Admin: t.Admin, + } case *transaction.IssueTX: case *transaction.ClaimTX: case *transaction.EnrollmentTX: case *transaction.StateTX: case *transaction.PublishTX: + contract := &ContractState{ + Script: t.Script, + ParamList: t.ParamList, + ReturnType: t.ReturnType, + HasStorage: t.NeedStorage, + Name: t.Name, + CodeVersion: t.CodeVersion, + Author: t.Author, + Email: t.Email, + Description: t.Description, + } + + fmt.Printf("%+v", contract) + case *transaction.InvocationTX: - log.Warn("invocation TX but we have no VM, o noo :(") } } @@ -337,6 +360,12 @@ func (bc *Blockchain) persistBlock(block *Block) error { if err := unspentCoins.commit(batch); err != nil { return err } + if err := spentCoins.commit(batch); err != nil { + return err + } + if err := assets.commit(batch); err != nil { + return err + } if err := bc.PutBatch(batch); err != nil { return err } diff --git a/pkg/core/contract_state.go b/pkg/core/contract_state.go new file mode 100644 index 000000000..f33946d26 --- /dev/null +++ b/pkg/core/contract_state.go @@ -0,0 +1,23 @@ +package core + +import ( + "github.com/CityOfZion/neo-go/pkg/smartcontract" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// ContractState holds information about a smart contract in the NEO blockchain. +type ContractState struct { + Script []byte + ParamList []smartcontract.ParamType + ReturnType smartcontract.ParamType + Properties []int + Name string + CodeVersion string + Author string + Email string + Description string + HasStorage bool + HasDynamicInvoke bool + + scriptHash util.Uint160 +} diff --git a/pkg/core/spent_coin_state.go b/pkg/core/spent_coin_state.go new file mode 100644 index 000000000..9e590dd49 --- /dev/null +++ b/pkg/core/spent_coin_state.go @@ -0,0 +1,116 @@ +package core + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// SpentCoins is mapping between transactions and their spent +// coin state. +type SpentCoins map[util.Uint256]*SpentCoinState + +func (s SpentCoins) getAndUpdate(store storage.Store, hash util.Uint256) (*SpentCoinState, error) { + if spent, ok := s[hash]; ok { + return spent, nil + } + + spent := &SpentCoinState{} + key := storage.AppendPrefix(storage.STSpentCoin, hash.BytesReverse()) + if b, err := store.Get(key); err == nil { + if err := spent.DecodeBinary(bytes.NewReader(b)); err != nil { + return nil, fmt.Errorf("failed to decode (UnspentCoinState): %s", err) + } + } else { + spent = &SpentCoinState{ + items: make(map[uint16]uint32), + } + } + + s[hash] = spent + return spent, nil +} + +func (s SpentCoins) commit(b storage.Batch) error { + buf := new(bytes.Buffer) + for hash, state := range s { + if err := state.EncodeBinary(buf); err != nil { + return err + } + key := storage.AppendPrefix(storage.STSpentCoin, hash.BytesReverse()) + b.Put(key, buf.Bytes()) + buf.Reset() + } + return nil +} + +// SpentCoinState represents the state of a spent coin. +type SpentCoinState struct { + txHash util.Uint256 + txHeight uint32 + + // A mapping between the index of the prevIndex and block height. + items map[uint16]uint32 +} + +// NewSpentCoinState returns a new SpentCoinState object. +func NewSpentCoinState(hash util.Uint256, height uint32) *SpentCoinState { + return &SpentCoinState{ + txHash: hash, + txHeight: height, + items: make(map[uint16]uint32), + } +} + +// DecodeBinary implements the Payload interface. +func (s *SpentCoinState) DecodeBinary(r io.Reader) error { + if err := binary.Read(r, binary.LittleEndian, &s.txHash); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &s.txHeight); err != nil { + return err + } + + s.items = make(map[uint16]uint32) + lenItems := util.ReadVarUint(r) + for i := 0; i < int(lenItems); i++ { + var ( + key uint16 + value uint32 + ) + if err := binary.Read(r, binary.LittleEndian, &key); err != nil { + return err + } + if err := binary.Read(r, binary.LittleEndian, &value); err != nil { + return err + } + s.items[key] = value + } + return nil +} + +// EncodeBinary implements the Payload interface. +func (s *SpentCoinState) EncodeBinary(w io.Writer) error { + if err := binary.Write(w, binary.LittleEndian, s.txHash); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, s.txHeight); err != nil { + return err + } + if err := util.WriteVarUint(w, uint64(len(s.items))); err != nil { + return err + } + for k, v := range s.items { + if err := binary.Write(w, binary.LittleEndian, k); err != nil { + return err + } + if err := binary.Write(w, binary.LittleEndian, v); err != nil { + return err + } + } + return nil +} diff --git a/pkg/core/spent_coin_state_test.go b/pkg/core/spent_coin_state_test.go new file mode 100644 index 000000000..6b61fff6f --- /dev/null +++ b/pkg/core/spent_coin_state_test.go @@ -0,0 +1,51 @@ +package core + +import ( + "bytes" + "testing" + + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeSpentCoinState(t *testing.T) { + spent := &SpentCoinState{ + txHash: util.RandomUint256(), + txHeight: 1001, + items: map[uint16]uint32{ + 1: 3, + 2: 8, + 4: 100, + }, + } + + buf := new(bytes.Buffer) + assert.Nil(t, spent.EncodeBinary(buf)) + spentDecode := new(SpentCoinState) + assert.Nil(t, spentDecode.DecodeBinary(buf)) + assert.Equal(t, spent, spentDecode) +} + +func TestCommitSpentCoins(t *testing.T) { + var ( + store = storage.NewMemoryStore() + batch = store.Batch() + spentCoins = make(SpentCoins) + ) + + txx := []util.Uint256{ + util.RandomUint256(), + util.RandomUint256(), + util.RandomUint256(), + } + + for i := 0; i < len(txx); i++ { + spentCoins[txx[i]] = &SpentCoinState{ + txHash: txx[i], + txHeight: 1, + } + } + assert.Nil(t, spentCoins.commit(batch)) + assert.Nil(t, store.PutBatch(batch)) +} diff --git a/pkg/core/unspent_coin_state.go b/pkg/core/unspent_coin_state.go index 1f0c7af17..21019117a 100644 --- a/pkg/core/unspent_coin_state.go +++ b/pkg/core/unspent_coin_state.go @@ -14,7 +14,7 @@ import ( // coin state. type UnspentCoins map[util.Uint256]*UnspentCoinState -func (u UnspentCoins) getAndChange(s storage.Store, hash util.Uint256) (*UnspentCoinState, error) { +func (u UnspentCoins) getAndUpdate(s storage.Store, hash util.Uint256) (*UnspentCoinState, error) { if unspent, ok := u[hash]; ok { return unspent, nil } @@ -40,6 +40,17 @@ type UnspentCoinState struct { states []CoinState } +// NewUnspentCoinState returns a new unspent coin state with N confirmed states. +func NewUnspentCoinState(n int) *UnspentCoinState { + u := &UnspentCoinState{ + states: make([]CoinState, n), + } + for i := 0; i < n; i++ { + u.states[i] = CoinStateConfirmed + } + return u +} + // commit writes all unspent coin states to the given Batch. func (s UnspentCoins) commit(b storage.Batch) error { buf := new(bytes.Buffer) diff --git a/pkg/core/unspent_coint_state_test.go b/pkg/core/unspent_coint_state_test.go index b02d031a4..223b26ff8 100644 --- a/pkg/core/unspent_coint_state_test.go +++ b/pkg/core/unspent_coint_state_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDecodeEncode(t *testing.T) { +func TestDecodeEncodeUnspentCoinState(t *testing.T) { unspent := &UnspentCoinState{ states: []CoinState{ CoinStateConfirmed, @@ -26,7 +26,7 @@ func TestDecodeEncode(t *testing.T) { assert.Nil(t, unspentDecode.DecodeBinary(buf)) } -func TestCommit(t *testing.T) { +func TestCommitUnspentCoins(t *testing.T) { var ( store = storage.NewMemoryStore() batch = store.Batch() diff --git a/pkg/core/util.go b/pkg/core/util.go index bdba1a5de..120fa2fce 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -96,10 +96,6 @@ func createGenesisBlock(cfg config.ProtocolConfiguration) (*Block, error) { func governingTokenTX() *transaction.Transaction { admin, _ := util.Uint160FromScript([]byte{byte(vm.Opusht)}) - adminB, _ := crypto.Uint160DecodeAddress("Abf2qMs1pzQb8kYk9RuxtUb9jtRKJVuBJt") - if !admin.Equals(adminB) { - panic("kdjdkljfkdjfkdjf") - } registerTX := &transaction.RegisterTX{ AssetType: transaction.GoverningToken, Name: "[{\"lang\":\"zh-CN\",\"name\":\"小蚁股\"},{\"lang\":\"en\",\"name\":\"AntShare\"}]",