diff --git a/config/config.go b/config/config.go index d84ee6e36..8f6658e11 100644 --- a/config/config.go +++ b/config/config.go @@ -46,7 +46,7 @@ type ( AddressVersion byte `yaml:"AddressVersion"` SecondsPerBlock int `yaml:"SecondsPerBlock"` LowPriorityThreshold float64 `yaml:"LowPriorityThreshold"` - MaxTransactionsPerBlock int64 `yaml:"MaxTransactionsPerBlock"` + MaxTransactionsPerBlock int `yaml:"MaxTransactionsPerBlock"` MemPoolSize int `yaml:"MemPoolSize"` StandbyValidators []string `yaml:"StandbyValidators"` SeedList []string `yaml:"SeedList"` @@ -59,6 +59,13 @@ type ( FreeGasLimit util.Fixed8 `yaml:"FreeGasLimit"` // SaveStorageBatch enables storage batch saving before every persist. SaveStorageBatch bool `yaml:"SaveStorageBatch"` + // Maximum number of low priority transactions accepted into block. + MaxFreeTransactionsPerBlock int `yaml:"MaxFreeTransactionsPerBlock"` + // Maximum size of low priority transaction in bytes. + MaxFreeTransactionSize int `yaml:"MaxFreeTransactionSize"` + // FeePerExtraByte sets the expected per-byte fee for + // transactions exceeding the MaxFreeTransactionSize. + FeePerExtraByte float64 `yaml:"FeePerExtraByte"` } // SystemFee fees related to system. diff --git a/config/protocol.mainnet.yml b/config/protocol.mainnet.yml index 601008add..b1f354fab 100644 --- a/config/protocol.mainnet.yml +++ b/config/protocol.mainnet.yml @@ -31,6 +31,10 @@ ProtocolConfiguration: VerifyBlocks: true VerifyTransactions: false FreeGasLimit: 10.0 + MaxTransactionsPerBlock: 500 + MaxFreeTransactionsPerBlock: 20 + MaxFreeTransactionSize: 1024 + FeePerExtraByte: 0.00001 ApplicationConfiguration: # LogPath could be set up in case you need stdout logs to some proper file. diff --git a/config/protocol.testnet.yml b/config/protocol.testnet.yml index ecfe6e2af..09ea83a23 100644 --- a/config/protocol.testnet.yml +++ b/config/protocol.testnet.yml @@ -31,6 +31,10 @@ ProtocolConfiguration: VerifyBlocks: true VerifyTransactions: false FreeGasLimit: 10.0 + MaxTransactionsPerBlock: 500 + MaxFreeTransactionsPerBlock: 20 + MaxFreeTransactionSize: 1024 + FeePerExtraByte: 0.00001 ApplicationConfiguration: # LogPath could be set up in case you need stdout logs to some proper file. diff --git a/go.mod b/go.mod index 6f2ea2e94..0cf506aed 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/go-redis/redis v6.10.2+incompatible github.com/go-yaml/yaml v2.1.0+incompatible github.com/mr-tron/base58 v1.1.2 - github.com/nspcc-dev/dbft v0.0.0-20200211143830-4deeb124d7f9 + github.com/nspcc-dev/dbft v0.0.0-20200218131838-be55fd41ea78 github.com/nspcc-dev/rfc6979 v0.2.0 github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v1.2.1 diff --git a/go.sum b/go.sum index dfa063289..90995b721 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,8 @@ github.com/nspcc-dev/dbft v0.0.0-20191209120240-0d6b7568d9ae h1:T5V1QANlNMKun0EP github.com/nspcc-dev/dbft v0.0.0-20191209120240-0d6b7568d9ae/go.mod h1:3FjXOoHmA51EGfb5GS/HOv7VdmngNRTssSeQ729dvGY= github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a h1:ajvxgEe9qY4vvoSmrADqdDx7hReodKTnT2IXN++qZG8= github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a/go.mod h1:/YFK+XOxxg0Bfm6P92lY5eDSLYfp06XOdL8KAVgXjVk= -github.com/nspcc-dev/dbft v0.0.0-20200211143830-4deeb124d7f9 h1:P3uOj+M3DkUlwGEFHbmwRHUZdpCuFzfM30j6iBjrPbY= -github.com/nspcc-dev/dbft v0.0.0-20200211143830-4deeb124d7f9/go.mod h1:O0qtn62prQSqizzoagHmuuKoz8QMkU3SzBoKdEvm3aQ= +github.com/nspcc-dev/dbft v0.0.0-20200218131838-be55fd41ea78 h1:cb8hWea3yyqbxzmnqqSSgqVCRoKNrFaS99Cqt0Qm7nQ= +github.com/nspcc-dev/dbft v0.0.0-20200218131838-be55fd41ea78/go.mod h1:O0qtn62prQSqizzoagHmuuKoz8QMkU3SzBoKdEvm3aQ= github.com/nspcc-dev/neofs-crypto v0.2.0 h1:ftN+59WqxSWz/RCgXYOfhmltOOqU+udsNQSvN6wkFck= github.com/nspcc-dev/neofs-crypto v0.2.0/go.mod h1:F/96fUzPM3wR+UGsPi3faVNmFlA9KAEAUQR7dMxZmNA= github.com/nspcc-dev/neofs-crypto v0.2.3 h1:aca3X2aly92ENRbFK+kH6Hd+J9EQ4Eu6XMVoITSIKtc= diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index d4daca7da..fdb57dfc6 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -9,8 +9,8 @@ import ( "github.com/CityOfZion/neo-go/config" "github.com/CityOfZion/neo-go/pkg/core" coreb "github.com/CityOfZion/neo-go/pkg/core/block" + "github.com/CityOfZion/neo-go/pkg/core/mempool" "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" @@ -42,6 +42,9 @@ type Service interface { OnTransaction(tx *transaction.Transaction) // GetPayload returns Payload with specified hash if it is present in the local cache. GetPayload(h util.Uint256) *Payload + // OnNewBlock notifies consensus service that there is a new block in + // the chain (without explicitly passing it to the service). + OnNewBlock() } type service struct { @@ -57,6 +60,9 @@ type service struct { // everything in single thread. messages chan Payload transactions chan *transaction.Transaction + // blockEvents is used to pass a new block event to the consensus + // process. + blockEvents chan struct{} lastProposal []util.Uint256 wallet *wallet.Wallet } @@ -101,6 +107,7 @@ func NewService(cfg Config) (Service, error) { messages: make(chan Payload, 100), transactions: make(chan *transaction.Transaction, 100), + blockEvents: make(chan struct{}, 1), } if cfg.Wallet == nil { @@ -168,14 +175,7 @@ func (s *service) eventLoop() { s.log.Debug("timer fired", zap.Uint32("height", hv.Height), zap.Uint("view", uint(hv.View))) - if s.Chain.BlockHeight() >= s.dbft.BlockIndex { - s.log.Debug("chain already advanced", - zap.Uint32("dbft index", s.dbft.BlockIndex), - zap.Uint32("chain index", s.Chain.BlockHeight())) - s.dbft.InitializeConsensus(0) - } else { - s.dbft.OnTimeout(hv) - } + s.dbft.OnTimeout(hv) case msg := <-s.messages: fields := []zap.Field{ zap.Uint16("from", msg.validatorIndex), @@ -204,6 +204,11 @@ func (s *service) eventLoop() { s.dbft.OnReceive(&msg) case tx := <-s.transactions: s.dbft.OnTransaction(tx) + case <-s.blockEvents: + s.log.Debug("new block in the chain", + zap.Uint32("dbft index", s.dbft.BlockIndex), + zap.Uint32("chain index", s.Chain.BlockHeight())) + s.dbft.InitializeConsensus(0) } } } @@ -215,16 +220,15 @@ func (s *service) validatePayload(p *Payload) bool { } pub := validators[p.validatorIndex] - vs := pub.(*publicKey).GetVerificationScript() - h := hash.Hash160(vs) + h := pub.(*publicKey).GetScriptHash() return p.Verify(h) } func (s *service) getKeyPair(pubs []crypto.PublicKey) (int, crypto.PrivateKey, crypto.PublicKey) { for i := range pubs { - script := pubs[i].(*publicKey).GetVerificationScript() - acc := s.wallet.GetAccount(hash.Hash160(script)) + sh := pubs[i].(*publicKey).GetScriptHash() + acc := s.wallet.GetAccount(sh) if acc == nil { continue } @@ -276,6 +280,20 @@ func (s *service) OnTransaction(tx *transaction.Transaction) { } } +// OnNewBlock notifies consensus process that there is a new block in the chain +// and dbft should probably be reinitialized. +func (s *service) OnNewBlock() { + if s.dbft != nil { + // If there is something in the queue already, the second + // consecutive event doesn't make much sense (reinitializing + // dbft twice doesn't improve it in any way). + select { + case s.blockEvents <- struct{}{}: + default: + } + } +} + // GetPayload returns payload stored in cache. func (s *service) GetPayload(h util.Uint256) *Payload { p := s.cache.Get(h) @@ -393,13 +411,13 @@ func (s *service) getBlock(h util.Uint256) block.Block { func (s *service) getVerifiedTx(count int) []block.Transaction { pool := s.Config.Chain.GetMemPool() - var txx []*transaction.Transaction + var txx []mempool.TxWithFee if s.dbft.ViewNumber > 0 { - txx = make([]*transaction.Transaction, 0, len(s.lastProposal)) + txx = make([]mempool.TxWithFee, 0, len(s.lastProposal)) for i := range s.lastProposal { - if tx, ok := pool.TryGetValue(s.lastProposal[i]); ok { - txx = append(txx, tx) + if tx, fee, ok := pool.TryGetValue(s.lastProposal[i]); ok { + txx = append(txx, mempool.TxWithFee{Tx: tx, Fee: fee}) } } @@ -410,11 +428,30 @@ func (s *service) getVerifiedTx(count int) []block.Transaction { txx = pool.GetVerifiedTransactions() } - res := make([]block.Transaction, len(txx)+1) - for i := range txx { - res[i+1] = txx[i] + if len(txx) > 0 { + txx = s.Config.Chain.ApplyPolicyToTxSet(txx) } + res := make([]block.Transaction, len(txx)+1) + var netFee util.Fixed8 + for i := range txx { + res[i+1] = txx[i].Tx + netFee += txx[i].Fee + } + + var txOuts []transaction.Output + if netFee != 0 { + sh := s.wallet.GetChangeAddress() + if sh.Equals(util.Uint160{}) { + pk := s.dbft.Pub.(*publicKey) + sh = pk.GetScriptHash() + } + txOuts = []transaction.Output{transaction.Output{ + AssetID: core.UtilityTokenID(), + Amount: netFee, + ScriptHash: sh, + }} + } for { nonce := rand.Uint32() res[0] = &transaction.Transaction{ @@ -423,7 +460,7 @@ func (s *service) getVerifiedTx(count int) []block.Transaction { Data: &transaction.MinerTX{Nonce: nonce}, Attributes: nil, Inputs: nil, - Outputs: nil, + Outputs: txOuts, Scripts: nil, Trimmed: false, } diff --git a/pkg/consensus/consensus_test.go b/pkg/consensus/consensus_test.go index db31c204d..7243bce01 100644 --- a/pkg/consensus/consensus_test.go +++ b/pkg/consensus/consensus_test.go @@ -239,6 +239,6 @@ func newTestChain(t *testing.T) *core.Blockchain { type feer struct{} func (fs *feer) NetworkFee(*transaction.Transaction) util.Fixed8 { return util.Fixed8(0) } -func (fs *feer) IsLowPriority(*transaction.Transaction) bool { return false } +func (fs *feer) IsLowPriority(util.Fixed8) bool { return false } func (fs *feer) FeePerByte(*transaction.Transaction) util.Fixed8 { return util.Fixed8(0) } func (fs *feer) SystemFee(*transaction.Transaction) util.Fixed8 { return util.Fixed8(0) } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index dce705a94..a75021378 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -49,6 +49,9 @@ var ( // ErrOOM is returned when adding transaction to the memory pool because // it reached its full capacity. ErrOOM = errors.New("no space left in the memory pool") + // ErrPolicy is returned on attempt to add transaction that doesn't + // comply with node's configured policy into the mempool. + ErrPolicy = errors.New("not allowed by policy") ) var ( genAmount = []int{8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} @@ -125,6 +128,22 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L cfg.MemPoolSize = defaultMemPoolSize log.Info("mempool size is not set or wrong, setting default value", zap.Int("MemPoolSize", cfg.MemPoolSize)) } + if cfg.MaxTransactionsPerBlock <= 0 { + cfg.MaxTransactionsPerBlock = 0 + log.Info("MaxTransactionsPerBlock is not set or wrong, setting default value (unlimited)", zap.Int("MaxTransactionsPerBlock", cfg.MaxTransactionsPerBlock)) + } + if cfg.MaxFreeTransactionsPerBlock <= 0 { + cfg.MaxFreeTransactionsPerBlock = 0 + log.Info("MaxFreeTransactionsPerBlock is not set or wrong, setting default value (unlimited)", zap.Int("MaxFreeTransactionsPerBlock", cfg.MaxFreeTransactionsPerBlock)) + } + if cfg.MaxFreeTransactionSize <= 0 { + cfg.MaxFreeTransactionSize = 0 + log.Info("MaxFreeTransactionSize is not set or wrong, setting default value (unlimited)", zap.Int("MaxFreeTransactionSize", cfg.MaxFreeTransactionSize)) + } + if cfg.FeePerExtraByte <= 0 { + cfg.FeePerExtraByte = 0 + log.Info("FeePerExtraByte is not set or wrong, setting default value", zap.Float64("FeePerExtraByte", cfg.FeePerExtraByte)) + } bc := &Blockchain{ config: cfg, dao: newDao(s), @@ -432,7 +451,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { return err } - if prevTXOutput.AssetID.Equals(governingTokenTX().Hash()) { + if prevTXOutput.AssetID.Equals(GoverningTokenID()) { spentCoin := NewSpentCoinState(input.PrevHash, prevTXHeight) spentCoin.items[input.PrevIndex] = block.Index if err = cache.PutSpentCoinState(input.PrevHash, spentCoin); err != nil { @@ -648,14 +667,14 @@ func processOutputs(tx *transaction.Transaction, dao *cachedDao) error { } func processTXWithValidatorsAdd(output *transaction.Output, account *state.Account, dao *cachedDao) error { - if output.AssetID.Equals(governingTokenTX().Hash()) && len(account.Votes) > 0 { + if output.AssetID.Equals(GoverningTokenID()) && len(account.Votes) > 0 { return modAccountVotes(account, dao, output.Amount) } return nil } func processTXWithValidatorsSubtract(output *transaction.Output, account *state.Account, dao *cachedDao) error { - if output.AssetID.Equals(governingTokenTX().Hash()) && len(account.Votes) > 0 { + if output.AssetID.Equals(GoverningTokenID()) && len(account.Votes) > 0 { return modAccountVotes(account, dao, -output.Amount) } return nil @@ -724,7 +743,7 @@ func processAccountStateDescriptor(descriptor *transaction.StateDescriptor, dao } if descriptor.Field == "Votes" { - balance := account.GetBalanceValues()[governingTokenTX().Hash()] + balance := account.GetBalanceValues()[GoverningTokenID()] if err = modAccountVotes(account, dao, -balance); err != nil { return err } @@ -814,7 +833,7 @@ func (bc *Blockchain) headerListLen() (n int) { // GetTransaction returns a TX and its height by the given hash. func (bc *Blockchain) GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error) { - if tx, ok := bc.memPool.TryGetValue(hash); ok { + if tx, _, ok := bc.memPool.TryGetValue(hash); ok { return tx, 0, nil // the height is not actually defined for memPool transaction. Not sure if zero is a good number in this case. } return bc.dao.GetTransaction(hash) @@ -999,14 +1018,14 @@ func (bc *Blockchain) FeePerByte(t *transaction.Transaction) util.Fixed8 { func (bc *Blockchain) NetworkFee(t *transaction.Transaction) util.Fixed8 { inputAmount := util.Fixed8FromInt64(0) for _, txOutput := range bc.References(t) { - if txOutput.AssetID == utilityTokenTX().Hash() { + if txOutput.AssetID == UtilityTokenID() { inputAmount.Add(txOutput.Amount) } } outputAmount := util.Fixed8FromInt64(0) for _, txOutput := range t.Outputs { - if txOutput.AssetID == utilityTokenTX().Hash() { + if txOutput.AssetID == UtilityTokenID() { outputAmount.Add(txOutput.Amount) } } @@ -1019,10 +1038,10 @@ func (bc *Blockchain) SystemFee(t *transaction.Transaction) util.Fixed8 { return bc.GetConfig().SystemFee.TryGetValue(t.Type) } -// IsLowPriority flags a transaction as low priority if the network fee is less than +// IsLowPriority checks given fee for being less than configured // LowPriorityThreshold. -func (bc *Blockchain) IsLowPriority(t *transaction.Transaction) bool { - return bc.NetworkFee(t) < util.Fixed8FromFloat(bc.GetConfig().LowPriorityThreshold) +func (bc *Blockchain) IsLowPriority(fee util.Fixed8) bool { + return fee < util.Fixed8FromFloat(bc.GetConfig().LowPriorityThreshold) } // GetMemPool returns the memory pool of the blockchain. @@ -1030,6 +1049,24 @@ func (bc *Blockchain) GetMemPool() *mempool.Pool { return &bc.memPool } +// ApplyPolicyToTxSet applies configured policies to given transaction set. It +// expects slice to be ordered by fee and returns a subslice of it. +func (bc *Blockchain) ApplyPolicyToTxSet(txes []mempool.TxWithFee) []mempool.TxWithFee { + if bc.config.MaxTransactionsPerBlock != 0 && len(txes) > bc.config.MaxTransactionsPerBlock { + txes = txes[:bc.config.MaxTransactionsPerBlock] + } + maxFree := bc.config.MaxFreeTransactionsPerBlock + if maxFree != 0 { + lowStart := sort.Search(len(txes), func(i int) bool { + return bc.IsLowPriority(txes[i].Fee) + }) + if lowStart+maxFree < len(txes) { + txes = txes[:lowStart+maxFree] + } + } + return txes +} + // VerifyBlock verifies block against its current state. func (bc *Blockchain) VerifyBlock(block *block.Block) error { prevHeader, err := bc.GetHeader(block.PrevHash) @@ -1127,6 +1164,18 @@ func (bc *Blockchain) PoolTx(t *transaction.Transaction) error { if err := bc.verifyTx(t, nil); err != nil { return err } + // Policying. + if t.Type != transaction.ClaimType { + txSize := io.GetVarSize(t) + maxFree := bc.config.MaxFreeTransactionSize + if maxFree != 0 && txSize > maxFree { + netFee := bc.NetworkFee(t) + if bc.IsLowPriority(netFee) || + netFee < util.Fixed8FromFloat(bc.config.FeePerExtraByte)*util.Fixed8(txSize-maxFree) { + return ErrPolicy + } + } + } if err := bc.memPool.Add(t, bc); err != nil { switch err { case mempool.ErrOOM: @@ -1192,7 +1241,7 @@ func (bc *Blockchain) verifyResults(t *transaction.Transaction) error { if len(resultsDestroy) > 1 { 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 != UtilityTokenID() { return errors.New("tx destroys non-utility token") } sysfee := bc.SystemFee(t) @@ -1208,14 +1257,14 @@ func (bc *Blockchain) verifyResults(t *transaction.Transaction) error { switch t.Type { case transaction.MinerType, transaction.ClaimType: for _, r := range resultsIssue { - if r.AssetID != utilityTokenTX().Hash() { + if r.AssetID != UtilityTokenID() { return errors.New("miner or claim tx issues non-utility tokens") } } break case transaction.IssueType: for _, r := range resultsIssue { - if r.AssetID == utilityTokenTX().Hash() { + if r.AssetID == UtilityTokenID() { return errors.New("issue tx issues utility tokens") } } @@ -1409,20 +1458,20 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P } uniqueSBValidators := standByValidators.Unique() - pubKeys := keys.PublicKeys{} + result := keys.PublicKeys{} for _, validator := range validators { if validator.RegisteredAndHasVotes() || uniqueSBValidators.Contains(validator.PublicKey) { - pubKeys = append(pubKeys, validator.PublicKey) + result = append(result, validator.PublicKey) } } - if pubKeys.Len() >= count { - return pubKeys[:count], nil - } - result := pubKeys.Unique() - for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ { - if !result.Contains(uniqueSBValidators[i]) { - result = append(result, uniqueSBValidators[i]) + if result.Len() >= count { + result = result[:count] + } else { + for i := 0; i < uniqueSBValidators.Len() && result.Len() < count; i++ { + if !result.Contains(uniqueSBValidators[i]) { + result = append(result, uniqueSBValidators[i]) + } } } sort.Sort(result) diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 79e6bc08f..dae78c48b 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -15,6 +15,7 @@ import ( // Blockchainer is an interface that abstract the implementation // of the blockchain. type Blockchainer interface { + ApplyPolicyToTxSet([]mempool.TxWithFee) []mempool.TxWithFee GetConfig() config.ProtocolConfiguration AddHeaders(...*block.Header) error AddBlock(*block.Block) error diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 811f036ae..7f00f51fe 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -8,7 +8,6 @@ import ( "github.com/CityOfZion/neo-go/pkg/core/block" "github.com/CityOfZion/neo-go/pkg/core/state" "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/trigger" "github.com/CityOfZion/neo-go/pkg/util" @@ -311,7 +310,7 @@ func (ic *interopContext) checkHashedWitness(hash util.Uint160) (bool, error) { // 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) { - return ic.checkHashedWitness(hash.Hash160(key.GetVerificationScript())) + return ic.checkHashedWitness(key.GetScriptHash()) } // runtimeCheckWitness checks witnesses. diff --git a/pkg/core/mempool/feer.go b/pkg/core/mempool/feer.go index f92c0e1d1..74b3fb036 100644 --- a/pkg/core/mempool/feer.go +++ b/pkg/core/mempool/feer.go @@ -8,7 +8,7 @@ import ( // Feer is an interface that abstract the implementation of the fee calculation. type Feer interface { NetworkFee(t *transaction.Transaction) util.Fixed8 - IsLowPriority(t *transaction.Transaction) bool + IsLowPriority(util.Fixed8) bool FeePerByte(t *transaction.Transaction) util.Fixed8 SystemFee(t *transaction.Transaction) util.Fixed8 } diff --git a/pkg/core/mempool/mem_pool.go b/pkg/core/mempool/mem_pool.go index a2f3ef9af..9a99a1774 100644 --- a/pkg/core/mempool/mem_pool.go +++ b/pkg/core/mempool/mem_pool.go @@ -35,6 +35,12 @@ type item struct { // items is a slice of item. type items []*item +// TxWithFee combines transaction and its precalculated network fee. +type TxWithFee struct { + Tx *transaction.Transaction + Fee util.Fixed8 +} + // Pool stores the unconfirms transactions. type Pool struct { lock sync.RWMutex @@ -128,8 +134,8 @@ func (mp *Pool) Add(t *transaction.Transaction, fee Feer) error { timeStamp: time.Now().UTC(), perByteFee: fee.FeePerByte(t), netFee: fee.NetworkFee(t), - isLowPrio: fee.IsLowPriority(t), } + pItem.isLowPrio = fee.IsLowPriority(pItem.netFee) mp.lock.Lock() if !mp.verifyInputs(t) { mp.lock.Unlock() @@ -224,29 +230,28 @@ func NewMemPool(capacity int) Pool { } } -// TryGetValue returns a transaction if it exists in the memory pool. -func (mp *Pool) TryGetValue(hash util.Uint256) (*transaction.Transaction, bool) { +// TryGetValue returns a transaction and its fee if it exists in the memory pool. +func (mp *Pool) TryGetValue(hash util.Uint256) (*transaction.Transaction, util.Fixed8, bool) { mp.lock.RLock() defer mp.lock.RUnlock() if pItem, ok := mp.verifiedMap[hash]; ok { - return pItem.txn, ok + return pItem.txn, pItem.netFee, ok } - return nil, false + return nil, 0, false } // GetVerifiedTransactions returns a slice of Input from all the transactions in the memory pool // whose hash is not included in excludedHashes. -func (mp *Pool) GetVerifiedTransactions() []*transaction.Transaction { +func (mp *Pool) GetVerifiedTransactions() []TxWithFee { mp.lock.RLock() defer mp.lock.RUnlock() - var t = make([]*transaction.Transaction, len(mp.verifiedTxes)) - var i int + var t = make([]TxWithFee, len(mp.verifiedTxes)) - for _, p := range mp.verifiedTxes { - t[i] = p.txn - i++ + for i := range mp.verifiedTxes { + t[i].Tx = mp.verifiedTxes[i].txn + t[i].Fee = mp.verifiedTxes[i].netFee } return t diff --git a/pkg/core/mempool/mem_pool_test.go b/pkg/core/mempool/mem_pool_test.go index 4cd529a06..b6a9401be 100644 --- a/pkg/core/mempool/mem_pool_test.go +++ b/pkg/core/mempool/mem_pool_test.go @@ -22,7 +22,7 @@ func (fs *FeerStub) NetworkFee(*transaction.Transaction) util.Fixed8 { return fs.netFee } -func (fs *FeerStub) IsLowPriority(*transaction.Transaction) bool { +func (fs *FeerStub) IsLowPriority(util.Fixed8) bool { return fs.lowPriority } @@ -37,16 +37,16 @@ func (fs *FeerStub) SystemFee(*transaction.Transaction) util.Fixed8 { func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) { mp := NewMemPool(10) tx := newMinerTX(0) - _, ok := mp.TryGetValue(tx.Hash()) + _, _, ok := mp.TryGetValue(tx.Hash()) require.Equal(t, false, ok) require.NoError(t, mp.Add(tx, fs)) // Re-adding should fail. require.Error(t, mp.Add(tx, fs)) - tx2, ok := mp.TryGetValue(tx.Hash()) + tx2, _, ok := mp.TryGetValue(tx.Hash()) require.Equal(t, true, ok) require.Equal(t, tx, tx2) mp.Remove(tx.Hash()) - _, ok = mp.TryGetValue(tx.Hash()) + _, _, ok = mp.TryGetValue(tx.Hash()) require.Equal(t, false, ok) // Make sure nothing left in the mempool after removal. assert.Equal(t, 0, len(mp.verifiedMap)) @@ -173,8 +173,8 @@ func TestGetVerified(t *testing.T) { require.Equal(t, mempoolSize, mp.Count()) verTxes := mp.GetVerifiedTransactions() require.Equal(t, mempoolSize, len(verTxes)) - for _, tx := range verTxes { - require.Contains(t, txes, tx) + for _, txf := range verTxes { + require.Contains(t, txes, txf.Tx) } for _, tx := range txes { mp.Remove(tx.Hash()) @@ -210,8 +210,8 @@ func TestRemoveStale(t *testing.T) { }) require.Equal(t, mempoolSize/2, mp.Count()) verTxes := mp.GetVerifiedTransactions() - for _, tx := range verTxes { - require.NotContains(t, txes1, tx) - require.Contains(t, txes2, tx) + for _, txf := range verTxes { + require.NotContains(t, txes1, txf.Tx) + require.Contains(t, txes2, txf.Tx) } } diff --git a/pkg/core/util.go b/pkg/core/util.go index 4465a0621..77fcddc22 100644 --- a/pkg/core/util.go +++ b/pkg/core/util.go @@ -13,6 +13,17 @@ import ( "github.com/CityOfZion/neo-go/pkg/vm/opcode" ) +var ( + // governingTokenTX represents transaction that is used to create + // governing (NEO) token. It's a part of the genesis block. + governingTokenTX transaction.Transaction + + // utilityTokenTX represents transaction that is used to create + // utility (GAS) token. It's a part of the genesis block. It's mostly + // useful for its hash that represents GAS asset ID. + utilityTokenTX transaction.Transaction +) + // createGenesisBlock creates a genesis block based on the given configuration. func createGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) { validators, err := getValidators(cfg) @@ -38,8 +49,6 @@ func createGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) }, } - governingTX := governingTokenTX() - utilityTX := utilityTokenTX() rawScript, err := smartcontract.CreateMultiSigRedeemScript( len(cfg.StandbyValidators)/2+1, validators, @@ -62,16 +71,16 @@ func createGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) Outputs: []transaction.Output{}, Scripts: []transaction.Witness{}, }, - governingTX, - utilityTX, + &governingTokenTX, + &utilityTokenTX, { Type: transaction.IssueType, Data: &transaction.IssueTX{}, // no fields. Inputs: []transaction.Input{}, Outputs: []transaction.Output{ { - AssetID: governingTX.Hash(), - Amount: governingTX.Data.(*transaction.RegisterTX).Amount, + AssetID: governingTokenTX.Hash(), + Amount: governingTokenTX.Data.(*transaction.RegisterTX).Amount, ScriptHash: scriptOut, }, }, @@ -92,7 +101,7 @@ func createGenesisBlock(cfg config.ProtocolConfiguration) (*block.Block, error) return b, nil } -func governingTokenTX() *transaction.Transaction { +func init() { admin := hash.Hash160([]byte{byte(opcode.PUSHT)}) registerTX := &transaction.RegisterTX{ AssetType: transaction.GoverningToken, @@ -102,7 +111,7 @@ func governingTokenTX() *transaction.Transaction { Admin: admin, } - tx := &transaction.Transaction{ + governingTokenTX = transaction.Transaction{ Type: transaction.RegisterType, Data: registerTX, Attributes: []transaction.Attribute{}, @@ -111,19 +120,15 @@ func governingTokenTX() *transaction.Transaction { Scripts: []transaction.Witness{}, } - return tx -} - -func utilityTokenTX() *transaction.Transaction { - admin := hash.Hash160([]byte{byte(opcode.PUSHF)}) - registerTX := &transaction.RegisterTX{ + admin = hash.Hash160([]byte{byte(opcode.PUSHF)}) + registerTX = &transaction.RegisterTX{ AssetType: transaction.UtilityToken, Name: "[{\"lang\":\"zh-CN\",\"name\":\"小蚁币\"},{\"lang\":\"en\",\"name\":\"AntCoin\"}]", Amount: calculateUtilityAmount(), Precision: 8, Admin: admin, } - tx := &transaction.Transaction{ + utilityTokenTX = transaction.Transaction{ Type: transaction.RegisterType, Data: registerTX, Attributes: []transaction.Attribute{}, @@ -131,8 +136,16 @@ func utilityTokenTX() *transaction.Transaction { Outputs: []transaction.Output{}, Scripts: []transaction.Witness{}, } +} - return tx +// GoverningTokenID returns the governing token (NEO) hash. +func GoverningTokenID() util.Uint256 { + return governingTokenTX.Hash() +} + +// UtilityTokenID returns the utility token (GAS) hash. +func UtilityTokenID() util.Uint256 { + return utilityTokenTX.Hash() } func getValidators(cfg config.ProtocolConfiguration) ([]*keys.PublicKey, error) { diff --git a/pkg/core/util_test.go b/pkg/core/util_test.go index b2222a56a..aaa39ae63 100644 --- a/pkg/core/util_test.go +++ b/pkg/core/util_test.go @@ -50,12 +50,10 @@ func TestGetConsensusAddressMainNet(t *testing.T) { func TestUtilityTokenTX(t *testing.T) { expect := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7" - tx := utilityTokenTX() - assert.Equal(t, expect, tx.Hash().StringLE()) + assert.Equal(t, expect, UtilityTokenID().StringLE()) } func TestGoverningTokenTX(t *testing.T) { expect := "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b" - tx := governingTokenTX() - assert.Equal(t, expect, tx.Hash().StringLE()) + assert.Equal(t, expect, GoverningTokenID().StringLE()) } diff --git a/pkg/crypto/keys/private_key.go b/pkg/crypto/keys/private_key.go index c3aef6663..bf99021d4 100644 --- a/pkg/crypto/keys/private_key.go +++ b/pkg/crypto/keys/private_key.go @@ -10,6 +10,7 @@ import ( "fmt" "math/big" + "github.com/CityOfZion/neo-go/pkg/util" "github.com/nspcc-dev/rfc6979" ) @@ -98,10 +99,11 @@ func (p *PrivateKey) Address() string { return pk.Address() } -// Signature creates the signature using the private key. -func (p *PrivateKey) Signature() []byte { +// GetScriptHash returns verification script hash for public key associated with +// the private key. +func (p *PrivateKey) GetScriptHash() util.Uint160 { pk := p.PublicKey() - return pk.Signature() + return pk.GetScriptHash() } // Sign signs arbitrary length data using the private key. diff --git a/pkg/crypto/keys/publickey.go b/pkg/crypto/keys/publickey.go index b22e2f9f3..60951851e 100644 --- a/pkg/crypto/keys/publickey.go +++ b/pkg/crypto/keys/publickey.go @@ -12,6 +12,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/encoding/address" "github.com/CityOfZion/neo-go/pkg/io" + "github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/vm/opcode" "github.com/pkg/errors" ) @@ -233,18 +234,14 @@ func (p *PublicKey) GetVerificationScript() []byte { return b } -// Signature returns a NEO-specific hash of the key. -func (p *PublicKey) Signature() []byte { - sig := hash.Hash160(p.GetVerificationScript()) - - return sig.BytesBE() +// GetScriptHash returns a Hash160 of verification script for the key. +func (p *PublicKey) GetScriptHash() util.Uint160 { + return hash.Hash160(p.GetVerificationScript()) } // Address returns a base58-encoded NEO-specific address based on the key hash. func (p *PublicKey) Address() string { - sig := hash.Hash160(p.GetVerificationScript()) - - return address.Uint160ToString(sig) + return address.Uint160ToString(p.GetScriptHash()) } // Verify returns true if the signature is valid and corresponds diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 1ab191d87..2759c37df 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -26,6 +26,9 @@ type testChain struct { blockheight uint32 } +func (chain testChain) ApplyPolicyToTxSet([]mempool.TxWithFee) []mempool.TxWithFee { + panic("TODO") +} func (chain testChain) GetConfig() config.ProtocolConfiguration { panic("TODO") } @@ -122,7 +125,7 @@ func (chain testChain) GetMemPool() *mempool.Pool { panic("TODO") } -func (chain testChain) IsLowPriority(*transaction.Transaction) bool { +func (chain testChain) IsLowPriority(util.Fixed8) bool { panic("TODO") } diff --git a/pkg/network/server.go b/pkg/network/server.go index 5a45ed393..dacac6461 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -64,7 +64,7 @@ type ( unregister chan peerDrop quit chan struct{} - connected *atomic.Bool + consensusStarted *atomic.Bool log *zap.Logger } @@ -88,17 +88,24 @@ func NewServer(config ServerConfig, chain core.Blockchainer, log *zap.Logger) (* } s := &Server{ - ServerConfig: config, - chain: chain, - id: randomID(), - quit: make(chan struct{}), - register: make(chan Peer), - unregister: make(chan peerDrop), - peers: make(map[Peer]bool), - connected: atomic.NewBool(false), - log: log, + ServerConfig: config, + chain: chain, + id: randomID(), + quit: make(chan struct{}), + register: make(chan Peer), + unregister: make(chan peerDrop), + peers: make(map[Peer]bool), + consensusStarted: atomic.NewBool(false), + log: log, } - s.bQueue = newBlockQueue(maxBlockBatch, chain, log, s.relayBlock) + s.bQueue = newBlockQueue(maxBlockBatch, chain, log, func(b *block.Block) { + if s.consensusStarted.Load() { + s.consensus.OnNewBlock() + } else { + s.tryStartConsensus() + } + s.relayBlock(b) + }) srv, err := consensus.NewService(consensus.Config{ Logger: log, @@ -274,13 +281,13 @@ func (s *Server) runProto() { } func (s *Server) tryStartConsensus() { - if s.Wallet == nil || s.connected.Load() { + if s.Wallet == nil || s.consensusStarted.Load() { return } - if s.HandshakedPeersCount() >= s.MinPeers { - s.log.Info("minimum amount of peers were connected to") - if s.connected.CAS(false, true) { + if s.IsInSync() { + s.log.Info("node reached synchronized state, starting consensus") + if s.consensusStarted.CAS(false, true) { s.consensus.Start() } } @@ -336,6 +343,39 @@ func (s *Server) getVersionMsg() *Message { return s.MkMsg(CMDVersion, payload) } +// IsInSync answers the question of whether the server is in sync with the +// network or not (at least how the server itself sees it). The server operates +// with the data that it has, the number of peers (that has to be more than +// minimum number) and height of these peers (our chain has to be not lower +// than 2/3 of our peers have). Ideally we would check for the highest of the +// peers, but the problem is that they can lie to us and send whatever height +// they want to. +func (s *Server) IsInSync() bool { + var peersNumber int + var notHigher int + + if s.MinPeers == 0 { + return true + } + + ourLastBlock := s.chain.BlockHeight() + + s.lock.RLock() + for p := range s.peers { + if p.Handshaked() { + peersNumber++ + if ourLastBlock >= p.LastBlockIndex() { + notHigher++ + } + } + } + s.lock.RUnlock() + + // Checking bQueue would also be nice, but it can be filled with garbage + // easily at the moment. + return peersNumber >= s.MinPeers && (3*notHigher > 2*peersNumber) // && s.bQueue.length() == 0 +} + // When a peer sends out his version we reply with verack after validating // the version. func (s *Server) handleVersionCmd(p Peer, version *payload.Version) error { @@ -746,15 +786,14 @@ func (s *Server) verifyAndPoolTX(t *transaction.Transaction) RelayReason { if t.Type == transaction.MinerType { return RelayInvalid } - // TODO: Implement Plugin.CheckPolicy? - //if (!Plugin.CheckPolicy(transaction)) - // return RelayResultReason.PolicyFail; if err := s.chain.PoolTx(t); err != nil { switch err { case core.ErrAlreadyExists: return RelayAlreadyExists case core.ErrOOM: return RelayOutOfMemory + case core.ErrPolicy: + return RelayPolicyFail default: return RelayInvalid } diff --git a/pkg/rpc/rpc.go b/pkg/rpc/rpc.go index 9beaad3d3..8ba1af6c5 100644 --- a/pkg/rpc/rpc.go +++ b/pkg/rpc/rpc.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "fmt" + "github.com/CityOfZion/neo-go/pkg/core" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/smartcontract" @@ -160,15 +161,12 @@ func (c *Client) SignAndPushInvocationTx(script []byte, wif *keys.WIF, gas util. var txHash util.Uint256 var err error - gasIDB, _ := hex.DecodeString("602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7") - gasID, _ := util.Uint256DecodeBytesLE(gasIDB) - tx := transaction.NewInvocationTX(script, gas) fromAddress := wif.PrivateKey.Address() if gas > 0 { - if err = AddInputsAndUnspentsToTx(tx, fromAddress, gasID, gas, c); err != nil { + if err = AddInputsAndUnspentsToTx(tx, fromAddress, core.UtilityTokenID(), gas, c); err != nil { return txHash, errors.Wrap(err, "failed to add inputs and unspents to transaction") } } diff --git a/pkg/wallet/testdata/wallet1.json b/pkg/wallet/testdata/wallet1.json new file mode 100644 index 000000000..d03726fd9 --- /dev/null +++ b/pkg/wallet/testdata/wallet1.json @@ -0,0 +1 @@ +{"name":"wallet1","version":"1.0","scrypt":{"n":16384,"r":8,"p":8},"accounts":[{"address":"AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs","label":null,"isDefault":false,"lock":false,"key":"6PYLmjBYJ4wQTCEfqvnznGJwZeW9pfUcV5m5oreHxqryUgqKpTRAFt9L8Y","contract":{"script":"2102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2ac","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"extra":null},{"address":"AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU","label":null,"isDefault":false,"lock":false,"key":"6PYLmjBYJ4wQTCEfqvnznGJwZeW9pfUcV5m5oreHxqryUgqKpTRAFt9L8Y","contract":{"script":"532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae","parameters":[{"name":"parameter0","type":"Signature"},{"name":"parameter1","type":"Signature"},{"name":"parameter2","type":"Signature"}],"deployed":false},"extra":null}],"extra":null} \ No newline at end of file diff --git a/pkg/wallet/testdata/wallet2.json b/pkg/wallet/testdata/wallet2.json new file mode 100644 index 000000000..926d0d688 --- /dev/null +++ b/pkg/wallet/testdata/wallet2.json @@ -0,0 +1 @@ +{"name":"wallet2","version":"1.0","scrypt":{"n":16384,"r":8,"p":8},"accounts":[{"address":"AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs","label":null,"isDefault":false,"lock":false,"key":"6PYLmjBYJ4wQTCEfqvnznGJwZeW9pfUcV5m5oreHxqryUgqKpTRAFt9L8Y","contract":{"script":"2102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2ac","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"extra":null},{"address":"AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU","label":null,"isDefault":false,"lock":false,"key":"6PYXHjPaNvW8YknSXaKsTWjf9FRxo1s4naV2jdmSQEgzaqKGX368rndN3L","contract":{"script":"532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae","parameters":[{"name":"parameter0","type":"Signature"},{"name":"parameter1","type":"Signature"},{"name":"parameter2","type":"Signature"}],"deployed":false},"extra":null},{"address":"AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17","label":null,"isDefault":true,"lock":false,"key":"6PYXHjPaNvW8YknSXaKsTWjf9FRxo1s4naV2jdmSQEgzaqKGX368rndN3L","contract":{"script":"2102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406eac","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"extra":null}],"extra":null} diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index c54aa20cc..fd9c9ab61 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -7,6 +7,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/CityOfZion/neo-go/pkg/vm" ) const ( @@ -129,3 +130,24 @@ func (w *Wallet) GetAccount(h util.Uint160) *Account { return nil } + +// GetChangeAddress returns the default address to send transaction's change to. +func (w *Wallet) GetChangeAddress() util.Uint160 { + var res util.Uint160 + var acc *Account + + for i := range w.Accounts { + if acc == nil || w.Accounts[i].Default { + if w.Accounts[i].Contract != nil && vm.IsSignatureContract(w.Accounts[i].Contract.Script) { + acc = w.Accounts[i] + if w.Accounts[i].Default { + break + } + } + } + } + if acc != nil { + res = acc.Contract.ScriptHash() + } + return res +} diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go index dca5590da..c22972713 100644 --- a/pkg/wallet/wallet_test.go +++ b/pkg/wallet/wallet_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/CityOfZion/neo-go/pkg/encoding/address" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -145,3 +146,20 @@ func TestWallet_GetAccount(t *testing.T) { assert.Equal(t, acc, wallet.GetAccount(h), "can't get %d account", i) } } + +func TestWalletGetChangeAddress(t *testing.T) { + w1, err := NewWalletFromFile("testdata/wallet1.json") + require.NoError(t, err) + sh := w1.GetChangeAddress() + // No default address, the first one is used. + expected, err := address.StringToUint160("AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs") + require.NoError(t, err) + require.Equal(t, expected, sh) + w2, err := NewWalletFromFile("testdata/wallet2.json") + require.NoError(t, err) + sh = w2.GetChangeAddress() + // Default address. + expected, err = address.StringToUint160("AWLYWXB8C9Lt1nHdDZJnC5cpYJjgRDLk17") + require.NoError(t, err) + require.Equal(t, expected, sh) +}