diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 22bf454a9..abb4ca456 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -6,12 +6,14 @@ import ( "math" "math/big" "sort" + "strconv" "sync/atomic" "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/crypto/keys" "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" @@ -336,41 +338,26 @@ func (bc *Blockchain) processHeader(h *Header, batch storage.Batch, headerList * // is happening here, quite allot as you can see :). If things are wired together // and all tests are in place, we can make a more optimized and cleaner implementation. func (bc *Blockchain) storeBlock(block *Block) error { - var ( - tmpStore = storage.NewMemCachedStore(bc.store) - unspentCoins = make(UnspentCoins) - spentCoins = make(SpentCoins) - accounts = make(Accounts) - assets = make(Assets) - contracts = make(Contracts) - ) + chainState := NewBlockChainState(bc.store) - if err := storeAsBlock(tmpStore, block, 0); err != nil { + if err := chainState.storeAsBlock(block, 0); err != nil { return err } - if err := storeAsCurrentBlock(tmpStore, block); err != nil { + if err := chainState.storeAsCurrentBlock(block); err != nil { return err } for _, tx := range block.Transactions { - if err := storeAsTransaction(tmpStore, tx, block.Index); err != nil { + if err := chainState.storeAsTransaction(tx, block.Index); err != nil { return err } - unspentCoins[tx.Hash()] = NewUnspentCoinState(len(tx.Outputs)) + chainState.unspentCoins[tx.Hash()] = NewUnspentCoinState(len(tx.Outputs)) // Process TX outputs. - for index, output := range tx.Outputs { - account, err := accounts.getAndUpdate(bc.store, output.ScriptHash) - if err != nil { - return err - } - account.Balances[output.AssetID] = append(account.Balances[output.AssetID], UnspentBalance{ - Tx: tx.Hash(), - Index: uint16(index), - Value: output.Amount, - }) + if err := processOutputs(tx, chainState); err != nil { + return err } // Process TX inputs that are grouped by previous hash. @@ -380,14 +367,14 @@ func (bc *Blockchain) storeBlock(block *Block) error { return fmt.Errorf("could not find previous TX: %s", prevHash) } for _, input := range inputs { - unspent, err := unspentCoins.getAndUpdate(bc.store, input.PrevHash) + unspent, err := chainState.unspentCoins.getAndUpdate(chainState.store, input.PrevHash) if err != nil { return err } unspent.states[input.PrevIndex] = CoinStateSpent prevTXOutput := prevTX.Outputs[input.PrevIndex] - account, err := accounts.getAndUpdate(bc.store, prevTXOutput.ScriptHash) + account, err := chainState.accounts.getAndUpdate(chainState.store, prevTXOutput.ScriptHash) if err != nil { return err } @@ -395,7 +382,20 @@ func (bc *Blockchain) storeBlock(block *Block) error { if prevTXOutput.AssetID.Equals(governingTokenTX().Hash()) { spentCoin := NewSpentCoinState(input.PrevHash, prevTXHeight) spentCoin.items[input.PrevIndex] = block.Index - spentCoins[input.PrevHash] = spentCoin + chainState.spentCoins[input.PrevHash] = spentCoin + + if len(account.Votes) > 0 { + for _, vote := range account.Votes { + validator, err := chainState.validators.getAndUpdate(chainState.store, vote) + if err != nil { + return err + } + validator.Votes -= prevTXOutput.Amount + if !validator.RegisteredAndHasVotes() { + delete(chainState.validators, vote) + } + } + } } balancesLen := len(account.Balances[prevTXOutput.AssetID]) @@ -419,7 +419,7 @@ func (bc *Blockchain) storeBlock(block *Block) error { // Process the underlying type of the TX. switch t := tx.Data.(type) { case *transaction.RegisterTX: - assets[tx.Hash()] = &AssetState{ + chainState.assets[tx.Hash()] = &AssetState{ ID: tx.Hash(), AssetType: t.AssetType, Name: t.Name, @@ -434,7 +434,7 @@ func (bc *Blockchain) storeBlock(block *Block) error { if res.Amount < 0 { var asset *AssetState - asset, ok := assets[res.AssetID] + asset, ok := chainState.assets[res.AssetID] if !ok { asset = bc.GetAssetState(res.AssetID) } @@ -442,14 +442,14 @@ func (bc *Blockchain) storeBlock(block *Block) error { return fmt.Errorf("issue failed: no asset %s", res.AssetID) } asset.Available -= res.Amount - assets[res.AssetID] = asset + chainState.assets[res.AssetID] = asset } } case *transaction.ClaimTX: // Remove claimed NEO from spent coins making it unavalaible for // additional claims. for _, input := range t.Claims { - scs, err := spentCoins.getAndUpdate(bc.store, input.PrevHash) + scs, err := chainState.spentCoins.getAndUpdate(bc.store, input.PrevHash) if err != nil { return err } @@ -458,11 +458,17 @@ func (bc *Blockchain) storeBlock(block *Block) error { delete(scs.items, input.PrevIndex) } else { // Uninitialized, new, forget about it. - delete(spentCoins, input.PrevHash) + delete(chainState.spentCoins, input.PrevHash) } } case *transaction.EnrollmentTX: + if err := processEnrollmentTX(chainState, t); err != nil { + return err + } case *transaction.StateTX: + if err := processStateTX(chainState, t); err != nil { + return err + } case *transaction.PublishTX: var properties smartcontract.PropertyState if t.NeedStorage { @@ -479,16 +485,15 @@ func (bc *Blockchain) storeBlock(block *Block) error { Email: t.Email, Description: t.Description, } - contracts[contract.ScriptHash()] = contract - + chainState.contracts[contract.ScriptHash()] = contract case *transaction.InvocationTX: - systemInterop := newInteropContext(0x10, bc, tmpStore, block, tx) + systemInterop := newInteropContext(0x10, bc, chainState.store, block, tx) v := bc.spawnVMWithInterops(systemInterop) v.SetCheckedHash(tx.VerificationHash().Bytes()) v.LoadScript(t.Script) err := v.Run() if !v.HasFailed() { - _, err = systemInterop.mem.Persist() + _, err := systemInterop.mem.Persist() if err != nil { return errors.Wrap(err, "failed to persist invocation results") } @@ -531,30 +536,14 @@ func (bc *Blockchain) storeBlock(block *Block) error { Stack: v.Stack("estack"), Events: systemInterop.notifications, } - err = putAppExecResultIntoStore(tmpStore, aer) + err = putAppExecResultIntoStore(chainState.store, aer) if err != nil { return errors.Wrap(err, "failed to store notifications") } } } - // Persist all to storage. - if err := accounts.commit(tmpStore); err != nil { - return err - } - if err := unspentCoins.commit(tmpStore); err != nil { - return err - } - if err := spentCoins.commit(tmpStore); err != nil { - return err - } - if err := assets.commit(tmpStore); err != nil { - return err - } - if err := contracts.commit(tmpStore); err != nil { - return err - } - if _, err := tmpStore.Persist(); err != nil { + if err := chainState.commit(); err != nil { return err } @@ -566,6 +555,92 @@ func (bc *Blockchain) storeBlock(block *Block) error { return nil } +// processOutputs processes transaction outputs. +func processOutputs(tx *transaction.Transaction, chainState *BlockChainState) error { + for index, output := range tx.Outputs { + account, err := chainState.accounts.getAndUpdate(chainState.store, output.ScriptHash) + if err != nil { + return err + } + account.Balances[output.AssetID] = append(account.Balances[output.AssetID], UnspentBalance{ + Tx: tx.Hash(), + Index: uint16(index), + Value: output.Amount, + }) + if output.AssetID.Equals(governingTokenTX().Hash()) && len(account.Votes) > 0 { + for _, vote := range account.Votes { + validatorState, err := chainState.validators.getAndUpdate(chainState.store, vote) + if err != nil { + return err + } + validatorState.Votes += output.Amount + } + } + } + return nil +} + +func processValidatorStateDescriptor(descriptor *transaction.StateDescriptor, state *BlockChainState) error { + publicKey := &keys.PublicKey{} + err := publicKey.DecodeBytes(descriptor.Key) + if err != nil { + return err + } + validatorState, err := state.validators.getAndUpdate(state.store, publicKey) + if err != nil { + return err + } + if descriptor.Field == "Registered" { + isRegistered, err := strconv.ParseBool(string(descriptor.Value)) + if err != nil { + return err + } + validatorState.Registered = isRegistered + } + return nil +} + +func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, state *BlockChainState) error { + hash, err := util.Uint160DecodeBytes(descriptor.Key) + if err != nil { + return err + } + account, err := state.accounts.getAndUpdate(state.store, hash) + if err != nil { + return err + } + + if descriptor.Field == "Votes" { + balance := account.GetBalanceValues()[governingTokenTX().Hash()] + for _, vote := range account.Votes { + validator, err := state.validators.getAndUpdate(state.store, vote) + if err != nil { + return err + } + validator.Votes -= balance + if !validator.RegisteredAndHasVotes() { + delete(state.validators, vote) + } + } + + votes := keys.PublicKeys{} + err := votes.DecodeBytes(descriptor.Value) + if err != nil { + return err + } + if votes.Len() != len(account.Votes) { + account.Votes = votes + for _, vote := range votes { + _, err := state.validators.getAndUpdate(state.store, vote) + if err != nil { + return err + } + } + } + } + return nil +} + // persist flushes current in-memory store contents to the persistent storage. func (bc *Blockchain) persist() error { var ( @@ -1134,6 +1209,142 @@ func (bc *Blockchain) GetScriptHashesForVerifyingClaim(t *transaction.Transactio return nil, fmt.Errorf("no hashes found") } +//GetStandByValidators returns validators from the configuration. +func (bc *Blockchain) GetStandByValidators() (keys.PublicKeys, error) { + return getValidators(bc.config) +} + +// GetValidators returns validators. +// Golang implementation of GetValidators method in C# (https://github.com/neo-project/neo/blob/c64748ecbac3baeb8045b16af0d518398a6ced24/neo/Persistence/Snapshot.cs#L182) +func (bc *Blockchain) GetValidators(txes... *transaction.Transaction) ([]*keys.PublicKey, error) { + chainState := NewBlockChainState(bc.store) + if len(txes) > 0 { + for _, tx := range txes { + // iterate through outputs + for index, output := range tx.Outputs { + accountState := bc.GetAccountState(output.ScriptHash) + accountState.Balances[output.AssetID] = append(accountState.Balances[output.AssetID], UnspentBalance{ + Tx: tx.Hash(), + Index: uint16(index), + Value: output.Amount, + }) + if output.AssetID.Equals(governingTokenTX().Hash()) && len(accountState.Votes) > 0 { + for _, vote := range accountState.Votes { + validatorState, err := chainState.validators.getAndUpdate(chainState.store, vote) + if err != nil { + return nil, err + } + validatorState.Votes += output.Amount + } + } + } + + // group inputs by the same previous hash and iterate through inputs + group := make(map[util.Uint256][]*transaction.Input) + for _, input := range tx.Inputs { + group[input.PrevHash] = append(group[input.PrevHash], input) + } + + for hash, inputs := range group { + prevTx, _, err := bc.GetTransaction(hash) + if err != nil { + return nil, err + } + // process inputs + for _, input := range inputs { + prevOutput := prevTx.Outputs[input.PrevIndex] + accountState, err := chainState.accounts.getAndUpdate(chainState.store, prevOutput.ScriptHash) + if err != nil { + return nil, err + } + + // process account state votes: if there are any -> validators will be updated. + if prevOutput.AssetID.Equals(governingTokenTX().Hash()) { + if len(accountState.Votes) > 0 { + for _, vote := range accountState.Votes { + validatorState, err := chainState.validators.getAndUpdate(chainState.store, vote) + if err != nil { + return nil, err + } + validatorState.Votes -= prevOutput.Amount + if !validatorState.Registered && validatorState.Votes.Equal(util.Fixed8(0)) { + delete(chainState.validators, vote) + } + } + } + } + delete(accountState.Balances, prevOutput.AssetID) + } + } + + switch t := tx.Data.(type) { + case *transaction.EnrollmentTX: + if err := processEnrollmentTX(chainState, t); err != nil { + return nil, err + } + case *transaction.StateTX: + if err := processStateTX(chainState, t); err != nil { + return nil, err + } + } + } + } + + validators := getValidatorsFromStore(chainState.store) + + count := GetValidatorsWeightedAverage(validators) + standByValidators, err := bc.GetStandByValidators() + if err != nil { + return nil, err + } + if count < len(standByValidators) { + count = len(standByValidators) + } + + uniqueSBValidators := standByValidators.Unique() + pubKeys := keys.PublicKeys{} + for _, validator := range validators { + if validator.RegisteredAndHasVotes() || uniqueSBValidators.Contains(validator.PublicKey) { + pubKeys = append(pubKeys, validator.PublicKey) + } + } + sort.Sort(sort.Reverse(pubKeys)) + if pubKeys.Len() >= count { + return pubKeys[:count], nil + } + + result := pubKeys.Unique() + for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ { + result = append(result, uniqueSBValidators[i]) + } + return result, nil +} + +func processStateTX(chainState *BlockChainState, tx *transaction.StateTX, ) error { + for _, desc := range tx.Descriptors { + switch desc.Type { + case transaction.Account: + if err := processAccountStateDescriptor(desc, chainState); err != nil { + return err + } + case transaction.Validator: + if err := processValidatorStateDescriptor(desc, chainState); err != nil { + return err + } + } + } + return nil +} + +func processEnrollmentTX(chainState *BlockChainState, tx *transaction.EnrollmentTX) error { + validatorState, err := chainState.validators.getAndUpdate(chainState.store, tx.PublicKey) + if err != nil { + return err + } + validatorState.Registered = true + return nil +} + // GetScriptHashesForVerifying returns all the ScriptHashes of a transaction which will be use // to verify whether the transaction is bonafide or not. // Golang implementation of GetScriptHashesForVerifying method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L190) diff --git a/pkg/core/blockchain_state.go b/pkg/core/blockchain_state.go new file mode 100644 index 000000000..ce472becc --- /dev/null +++ b/pkg/core/blockchain_state.go @@ -0,0 +1,96 @@ +package core + +import ( + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/io" +) + +// BlockChainState represents Blockchain state structure with mempool. +type BlockChainState struct { + store *storage.MemCachedStore + unspentCoins UnspentCoins + spentCoins SpentCoins + accounts Accounts + assets Assets + contracts Contracts + validators Validators +} + +// NewBlockChainState creates blockchain state with it's memchached store. +func NewBlockChainState(store *storage.MemCachedStore) *BlockChainState { + return &BlockChainState{ + store: store, + unspentCoins: make(UnspentCoins), + spentCoins: make(SpentCoins), + accounts: make(Accounts), + assets: make(Assets), + contracts: make(Contracts), + validators: make(Validators), + } +} + +// commit commits all the data in current state into storage. +func (state *BlockChainState) commit() error { + if err := state.accounts.commit(state.store); err != nil { + return err + } + if err := state.unspentCoins.commit(state.store); err != nil { + return err + } + if err := state.spentCoins.commit(state.store); err != nil { + return err + } + if err := state.assets.commit(state.store); err != nil { + return err + } + if err := state.contracts.commit(state.store); err != nil { + return err + } + if err := state.validators.commit(state.store); err != nil { + return err + } + if _, err := state.store.Persist(); err != nil { + return err + } + return nil +} + +// storeAsBlock stores the given block as DataBlock. +func (state *BlockChainState) storeAsBlock(block *Block, sysFee uint32) error { + var ( + key = storage.AppendPrefix(storage.DataBlock, block.Hash().BytesReverse()) + buf = io.NewBufBinWriter() + ) + // sysFee needs to be handled somehow + // buf.WriteLE(sysFee) + b, err := block.Trim() + if err != nil { + return err + } + buf.WriteLE(b) + if buf.Err != nil { + return buf.Err + } + return state.store.Put(key, buf.Bytes()) +} + +// storeAsCurrentBlock stores the given block witch prefix SYSCurrentBlock. +func (state *BlockChainState) storeAsCurrentBlock(block *Block) error { + buf := io.NewBufBinWriter() + buf.WriteLE(block.Hash().BytesReverse()) + buf.WriteLE(block.Index) + return state.store.Put(storage.SYSCurrentBlock.Bytes(), buf.Bytes()) +} + +// storeAsTransaction stores the given TX as DataTransaction. +func (state *BlockChainState) storeAsTransaction(tx *transaction.Transaction, index uint32) error { + key := storage.AppendPrefix(storage.DataTransaction, tx.Hash().BytesReverse()) + buf := io.NewBufBinWriter() + buf.WriteLE(index) + tx.EncodeBinary(buf.BinWriter) + if buf.Err != nil { + return buf.Err + } + return state.store.Put(key, buf.Bytes()) +} diff --git a/pkg/core/blockchain_state_test.go b/pkg/core/blockchain_state_test.go new file mode 100644 index 000000000..3a625c784 --- /dev/null +++ b/pkg/core/blockchain_state_test.go @@ -0,0 +1,46 @@ +package core + +import ( + "testing" + + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/stretchr/testify/require" +) + +func TestNewBlockChainStateAndCommit(t *testing.T) { + memCachedStore := storage.NewMemCachedStore(storage.NewMemoryStore()) + bcState := NewBlockChainState(memCachedStore) + err := bcState.commit() + require.NoError(t, err) +} + +func TestStoreAsBlock(t *testing.T) { + memCachedStore := storage.NewMemCachedStore(storage.NewMemoryStore()) + bcState := NewBlockChainState(memCachedStore) + + block := newBlock(0, newMinerTX()) + err := bcState.storeAsBlock(block, 0) + require.NoError(t, err) +} + +func TestStoreAsCurrentBlock(t *testing.T) { + memCachedStore := storage.NewMemCachedStore(storage.NewMemoryStore()) + bcState := NewBlockChainState(memCachedStore) + + block := newBlock(0, newMinerTX()) + err := bcState.storeAsCurrentBlock(block) + require.NoError(t, err) +} + +func TestStoreAsTransaction(t *testing.T) { + memCachedStore := storage.NewMemCachedStore(storage.NewMemoryStore()) + bcState := NewBlockChainState(memCachedStore) + + tx := &transaction.Transaction{ + Type: transaction.MinerType, + Data: &transaction.MinerTX{}, + } + err := bcState.storeAsTransaction(tx, 0) + require.NoError(t, err) +} diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index c13fd1ae3..0531d683d 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -4,6 +4,7 @@ import ( "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/crypto/keys" "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm" ) @@ -27,6 +28,7 @@ type Blockchainer interface { HasTransaction(util.Uint256) bool GetAssetState(util.Uint256) *AssetState GetAccountState(util.Uint160) *AccountState + GetValidators(txes... *transaction.Transaction) ([]*keys.PublicKey, error) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *StorageItem GetStorageItems(hash util.Uint160) (map[string]*StorageItem, error) diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 1d41100b1..9c177c3e9 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -196,6 +196,16 @@ func (ic *interopContext) txGetWitnesses(v *vm.VM) error { return nil } +// bcGetValidators returns validators. +func (ic *interopContext) bcGetValidators(v *vm.VM) error { + validators, err := ic.bc.GetValidators() + if err != nil { + return err + } + v.Estack().PushVal(validators) + return nil +} + // popInputFromVM returns transaction.Input from the first estack element. func popInputFromVM(v *vm.VM) (*transaction.Input, error) { inInterface := v.Estack().Pop().Value() diff --git a/pkg/core/interops.go b/pkg/core/interops.go index a8e1dfa7d..deafb6e8b 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -101,6 +101,7 @@ func (ic *interopContext) getNeoInteropMap() map[string]vm.InteropFuncPrice { "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.Blockchain.GetValidators": {Func: ic.bcGetValidators, Price: 200}, "Neo.Contract.Create": {Func: ic.contractCreate, Price: 0}, "Neo.Contract.Destroy": {Func: ic.contractDestroy, Price: 1}, "Neo.Contract.GetScript": {Func: ic.contractGetScript, Price: 1}, @@ -139,7 +140,6 @@ func (ic *interopContext) getNeoInteropMap() map[string]vm.InteropFuncPrice { "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}, @@ -185,6 +185,7 @@ func (ic *interopContext) getNeoInteropMap() map[string]vm.InteropFuncPrice { "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.Blockchain.GetValidators": {Func: ic.bcGetValidators, Price: 200}, "AntShares.Contract.Create": {Func: ic.contractCreate, Price: 0}, "AntShares.Contract.Destroy": {Func: ic.contractDestroy, Price: 1}, "AntShares.Contract.GetScript": {Func: ic.contractGetScript, Price: 1}, @@ -215,6 +216,5 @@ func (ic *interopContext) getNeoInteropMap() map[string]vm.InteropFuncPrice { "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}, } } diff --git a/pkg/core/util.go b/pkg/core/util.go index c11032e80..db97c7f86 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -4,11 +4,9 @@ import ( "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/crypto/hash" "github.com/CityOfZion/neo-go/pkg/crypto/keys" - "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/vm" @@ -176,43 +174,3 @@ func headerSliceReverse(dest []*Header) { dest[i], dest[j] = dest[j], dest[i] } } - -// storeAsCurrentBlock stores the given block witch prefix -// SYSCurrentBlock. -func storeAsCurrentBlock(store storage.Store, block *Block) error { - buf := io.NewBufBinWriter() - buf.WriteLE(block.Hash().BytesReverse()) - buf.WriteLE(block.Index) - return store.Put(storage.SYSCurrentBlock.Bytes(), buf.Bytes()) -} - -// storeAsBlock stores the given block as DataBlock. -func storeAsBlock(store storage.Store, block *Block, sysFee uint32) error { - var ( - key = storage.AppendPrefix(storage.DataBlock, block.Hash().BytesReverse()) - buf = io.NewBufBinWriter() - ) - // sysFee needs to be handled somehow - // buf.WriteLE(sysFee) - b, err := block.Trim() - if err != nil { - return err - } - buf.WriteLE(b) - if buf.Err != nil { - return buf.Err - } - return store.Put(key, buf.Bytes()) -} - -// storeAsTransaction stores the given TX as DataTransaction. -func storeAsTransaction(store storage.Store, tx *transaction.Transaction, index uint32) error { - key := storage.AppendPrefix(storage.DataTransaction, tx.Hash().BytesReverse()) - buf := io.NewBufBinWriter() - buf.WriteLE(index) - tx.EncodeBinary(buf.BinWriter) - if buf.Err != nil { - return buf.Err - } - return store.Put(key, buf.Bytes()) -} diff --git a/pkg/core/validator_state.go b/pkg/core/validator_state.go index b93585e99..36fe3a996 100644 --- a/pkg/core/validator_state.go +++ b/pkg/core/validator_state.go @@ -1,16 +1,174 @@ package core import ( + "fmt" + + "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" ) // Validators is a mapping between public keys and ValidatorState. type Validators map[*keys.PublicKey]*ValidatorState +func (v Validators) getAndUpdate(s storage.Store, publicKey *keys.PublicKey) (*ValidatorState, error) { + if validator, ok := v[publicKey]; ok { + return validator, nil + } + + validatorState, err := getValidatorStateFromStore(s, publicKey) + if err != nil { + if err != storage.ErrKeyNotFound { + return nil, err + } + validatorState = &ValidatorState{PublicKey: publicKey} + } + v[publicKey] = validatorState + return validatorState, nil + +} + +// getValidatorsFromStore returns all validators from store. +func getValidatorsFromStore(s storage.Store) []*ValidatorState { + var validators []*ValidatorState + s.Seek(storage.STValidator.Bytes(), func(k, v []byte) { + r := io.NewBinReaderFromBuf(v) + validator := &ValidatorState{} + validator.DecodeBinary(r) + if r.Err != nil { + return + } + validators = append(validators, validator) + }) + return validators +} + +// getValidatorStateFromStore returns validator by publicKey. +func getValidatorStateFromStore(s storage.Store, publicKey *keys.PublicKey) (*ValidatorState, error) { + validatorState := &ValidatorState{} + key := storage.AppendPrefix(storage.STValidator, publicKey.Bytes()) + if b, err := s.Get(key); err == nil { + r := io.NewBinReaderFromBuf(b) + validatorState.DecodeBinary(r) + if r.Err != nil { + return nil, fmt.Errorf("failed to decode (ValidatorState): %s", r.Err) + } + } else { + return nil, err + } + return validatorState, nil +} + +// commit writes all validator states to the given Batch. +func (v Validators) commit(store storage.Store) error { + for _, validator := range v { + if err := putValidatorStateIntoStore(store, validator); err != nil { + return err + } + } + return nil +} + +// putValidatorStateIntoStore puts given ValidatorState into the given store. +func putValidatorStateIntoStore(store storage.Store, vs *ValidatorState) error { + buf := io.NewBufBinWriter() + vs.EncodeBinary(buf.BinWriter) + if buf.Err != nil { + return buf.Err + } + key := storage.AppendPrefix(storage.STValidator, vs.PublicKey.Bytes()) + return store.Put(key, buf.Bytes()) +} + // ValidatorState holds the state of a validator. type ValidatorState struct { PublicKey *keys.PublicKey Registered bool Votes util.Fixed8 } + +// RegisteredAndHasVotes returns true or false whether Validator is registered and has votes. +func (vs *ValidatorState) RegisteredAndHasVotes() bool { + return vs.Registered && vs.Votes > util.Fixed8(0) +} + +// EncodeBinary encodes ValidatorState to the given BinWriter. +func (vs *ValidatorState) EncodeBinary(bw *io.BinWriter) { + vs.PublicKey.EncodeBinary(bw) + bw.WriteLE(vs.Registered) + bw.WriteLE(vs.Votes) +} + +// DecodeBinary decodes ValidatorState from the given BinReader. +func (vs *ValidatorState) DecodeBinary(reader *io.BinReader) { + vs.PublicKey = &keys.PublicKey{} + vs.PublicKey.DecodeBinary(reader) + reader.ReadLE(&vs.Registered) + reader.ReadLE(&vs.Votes) +} + +// GetValidatorsWeightedAverage applies weighted filter based on votes for validator and returns number of validators. +// Get back to it with further investigation in https://github.com/nspcc-dev/neo-go/issues/512. +func GetValidatorsWeightedAverage(validators []*ValidatorState) int { + return int(weightedAverage(applyWeightedFilter(validators))) +} + +// applyWeightedFilter is an implementation of the filter for validators votes. +// C# reference https://github.com/neo-project/neo/blob/41caff115c28d6c7665b2a7ac72967e7ce82e921/neo/Helper.cs#L273 +func applyWeightedFilter(validators []*ValidatorState) map[*ValidatorState]float64 { + var validatorsWithVotes []*ValidatorState + var amount float64 + + weightedVotes := make(map[*ValidatorState]float64) + start := 0.25 + end := 0.75 + sum := float64(0) + current := float64(0) + + for _, validator := range validators { + if validator.Votes > util.Fixed8(0) { + validatorsWithVotes = append(validatorsWithVotes, validator) + amount += validator.Votes.FloatValue() + } + } + + for _, validator := range validatorsWithVotes { + if current >= end { + break + } + weight := validator.Votes.FloatValue() + sum += weight + old := current + current = sum / amount + + if current <= start { + continue + } + + if old < start { + if current > end { + weight = (end - start) * amount + } else { + weight = (current - start) * amount + } + } else if current > end { + weight = (end - old) * amount + } + weightedVotes[validator] = weight + } + return weightedVotes +} + +func weightedAverage(weightedVotes map[*ValidatorState]float64) float64 { + sumWeight := float64(0) + sumValue := float64(0) + for vState, weight := range weightedVotes { + sumWeight += weight + sumValue += vState.Votes.FloatValue() * weight + } + if sumValue == 0 || sumWeight == 0 { + return 0 + } + return sumValue / sumWeight +} diff --git a/pkg/core/validator_state_test.go b/pkg/core/validator_state_test.go new file mode 100644 index 000000000..617e3ff50 --- /dev/null +++ b/pkg/core/validator_state_test.go @@ -0,0 +1,121 @@ +package core + +import ( + "math/big" + "testing" + + "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestGetAndUpdate(t *testing.T) { + store := storage.NewMemoryStore() + state1 := getDefaultValidator() + state2 := getDefaultValidator() + validators := make(Validators) + validators[state1.PublicKey] = state1 + validators[state2.PublicKey] = state2 + err := validators.commit(store) + require.NoError(t, err) + + state, err := validators.getAndUpdate(store, state1.PublicKey) + require.NoError(t, err) + require.Equal(t, state1, state) +} + +func TestCommit(t *testing.T) { + store := storage.NewMemoryStore() + state1 := getDefaultValidator() + state2 := getDefaultValidator() + validators := make(Validators) + validators[state1.PublicKey] = state1 + validators[state2.PublicKey] = state2 + err := validators.commit(store) + require.NoError(t, err) + + validatorsFromStore := getValidatorsFromStore(store) + // 2 equal validators will be stored as 1 unique + require.Len(t, validatorsFromStore, 1) + require.Equal(t, state1, validatorsFromStore[0]) +} + +func TestPutAndGet(t *testing.T) { + store := storage.NewMemoryStore() + state := getDefaultValidator() + err := putValidatorStateIntoStore(store, state) + require.NoError(t, err) + validatorFromStore, err := getValidatorStateFromStore(store, state.PublicKey) + require.NoError(t, err) + require.Equal(t, state.PublicKey, validatorFromStore.PublicKey) +} + +func TestGetFromStore_NoKey(t *testing.T) { + store := storage.NewMemoryStore() + state := getDefaultValidator() + _, err := getValidatorStateFromStore(store, state.PublicKey) + require.Errorf(t, err, "key not found") +} + +func TestValidatorState_DecodeEncodeBinary(t *testing.T) { + state := &ValidatorState{ + PublicKey: &keys.PublicKey{}, + Registered: false, + Votes: util.Fixed8(10), + } + buf := io.NewBufBinWriter() + state.EncodeBinary(buf.BinWriter) + require.NoError(t, buf.Err) + + decodedState := &ValidatorState{} + reader := io.NewBinReaderFromBuf(buf.Bytes()) + decodedState.DecodeBinary(reader) + require.NoError(t, reader.Err) + require.Equal(t, state, decodedState) +} + +func TestRegisteredAndHasVotes_Registered(t *testing.T) { + state := &ValidatorState{ + PublicKey: &keys.PublicKey{ + X: big.NewInt(1), + Y: big.NewInt(1), + }, + Registered: true, + Votes: 0, + } + require.False(t, state.RegisteredAndHasVotes()) +} + +func TestRegisteredAndHasVotes_RegisteredWithVotes(t *testing.T) { + state := &ValidatorState{ + PublicKey: &keys.PublicKey{ + X: big.NewInt(1), + Y: big.NewInt(1), + }, + Registered: true, + Votes: 1, + } + require.True(t, state.RegisteredAndHasVotes()) +} + +func TestRegisteredAndHasVotes_NotRegisteredWithVotes(t *testing.T) { + state := &ValidatorState{ + PublicKey: &keys.PublicKey{ + X: big.NewInt(1), + Y: big.NewInt(1), + }, + Registered: false, + Votes: 1, + } + require.False(t, state.RegisteredAndHasVotes()) +} + +func getDefaultValidator() *ValidatorState { + return &ValidatorState{ + PublicKey: &keys.PublicKey{}, + Registered: false, + Votes: 0, + } +} diff --git a/pkg/crypto/keys/publickey.go b/pkg/crypto/keys/publickey.go index 80af4d5ee..8bae4a652 100644 --- a/pkg/crypto/keys/publickey.go +++ b/pkg/crypto/keys/publickey.go @@ -34,6 +34,34 @@ func (keys PublicKeys) Less(i, j int) bool { return keys[i].Y.Cmp(keys[j].Y) == -1 } +// DecodeBytes decodes a PublicKeys from the given slice of bytes. +func (keys PublicKeys) DecodeBytes(data []byte) error { + b := io.NewBinReaderFromBuf(data) + b.ReadArray(keys) + return b.Err +} + +// Contains checks whether passed param contained in PublicKeys. +func (keys PublicKeys) Contains(pKey *PublicKey) bool { + for _, key := range keys { + if key.Equal(pKey) { + return true + } + } + return false +} + +// Unique returns set of public keys. +func (keys PublicKeys) Unique() PublicKeys { + unique := PublicKeys{} + for _, publicKey := range keys { + if !unique.Contains(publicKey) { + unique = append(unique, publicKey) + } + } + return unique +} + // PublicKey represents a public key and provides a high level // API around the X/Y point. type PublicKey struct { @@ -41,6 +69,11 @@ type PublicKey struct { Y *big.Int } +// Equal returns true in case public keys are equal. +func (p *PublicKey) Equal(key *PublicKey) bool { + return p.X.Cmp(key.X) == 0 && p.Y.Cmp(key.Y) == 0 +} + // NewPublicKeyFromString returns a public key created from the // given hex string. func NewPublicKeyFromString(s string) (*PublicKey, error) { diff --git a/pkg/crypto/keys/publickey_test.go b/pkg/crypto/keys/publickey_test.go index 199577d6e..b35784bbf 100644 --- a/pkg/crypto/keys/publickey_test.go +++ b/pkg/crypto/keys/publickey_test.go @@ -5,49 +5,76 @@ import ( "testing" "github.com/CityOfZion/neo-go/pkg/io" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestEncodeDecodeInfinity(t *testing.T) { key := &PublicKey{} buf := io.NewBufBinWriter() key.EncodeBinary(buf.BinWriter) - assert.Nil(t, buf.Err) + require.NoError(t, buf.Err) b := buf.Bytes() - assert.Equal(t, 1, len(b)) + require.Equal(t, 1, len(b)) keyDecode := &PublicKey{} - assert.Nil(t, keyDecode.DecodeBytes(b)) - assert.Equal(t, []byte{0x00}, keyDecode.Bytes()) + require.NoError(t, keyDecode.DecodeBytes(b)) + require.Equal(t, []byte{0x00}, keyDecode.Bytes()) } func TestEncodeDecodePublicKey(t *testing.T) { for i := 0; i < 4; i++ { k, err := NewPrivateKey() - assert.Nil(t, err) + require.NoError(t, err) p := k.PublicKey() buf := io.NewBufBinWriter() p.EncodeBinary(buf.BinWriter) - assert.Nil(t, buf.Err) + require.NoError(t, buf.Err) b := buf.Bytes() pDecode := &PublicKey{} - assert.Nil(t, pDecode.DecodeBytes(b)) - assert.Equal(t, p.X, pDecode.X) + require.NoError(t, pDecode.DecodeBytes(b)) + require.Equal(t, p.X, pDecode.X) } } func TestDecodeFromString(t *testing.T) { str := "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c" pubKey, err := NewPublicKeyFromString(str) - assert.Nil(t, err) - assert.Equal(t, str, hex.EncodeToString(pubKey.Bytes())) + require.NoError(t, err) + require.Equal(t, str, hex.EncodeToString(pubKey.Bytes())) } func TestPubkeyToAddress(t *testing.T) { pubKey, err := NewPublicKeyFromString("031ee4e73a17d8f76dc02532e2620bcb12425b33c0c9f9694cc2caa8226b68cad4") - assert.Nil(t, err) + require.NoError(t, err) actual := pubKey.Address() expected := "AUpGsNCHzSimeMRVPQfhwrVdiUp8Q2N2Qx" - assert.Equal(t, expected, actual) + require.Equal(t, expected, actual) +} + +func TestDecodeBytes(t *testing.T) { + pubKey := getPubKey(t) + decodedPubKey := &PublicKey{} + err := decodedPubKey.DecodeBytes(pubKey.Bytes()) + require.NoError(t, err) + require.Equal(t, pubKey,decodedPubKey) +} + +func TestContains(t *testing.T) { + pubKey := getPubKey(t) + pubKeys := &PublicKeys{getPubKey(t)} + pubKeys.Contains(pubKey) + require.True(t, pubKeys.Contains(pubKey)) +} + +func TestUnique(t *testing.T) { + pubKeys := &PublicKeys{getPubKey(t), getPubKey(t)} + unique := pubKeys.Unique() + require.Equal(t, 1, unique.Len()) +} + +func getPubKey(t *testing.T) *PublicKey { + pubKey, err := NewPublicKeyFromString("031ee4e73a17d8f76dc02532e2620bcb12425b33c0c9f9694cc2caa8226b68cad4") + require.NoError(t, err) + return pubKey } diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 4d64656e8..14daf897e 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -11,6 +11,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/network/payload" "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm" @@ -77,6 +78,9 @@ func (chain testChain) GetAssetState(util.Uint256) *core.AssetState { func (chain testChain) GetAccountState(util.Uint160) *core.AccountState { panic("TODO") } +func (chain testChain) GetValidators(...*transaction.Transaction) ([]*keys.PublicKey, error) { + panic("TODO") +} func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) { panic("TODO") }