diff --git a/config/config.go b/config/config.go index 2fcfba53a..de855f88d 100644 --- a/config/config.go +++ b/config/config.go @@ -43,6 +43,8 @@ type ( ProtocolConfiguration struct { Magic NetMode `yaml:"Magic"` AddressVersion int64 `yaml:"AddressVersion"` + SecondsPerBlock int `yaml:"SecondsPerBlock"` + LowPriorityThreshold float64 `yaml:"LowPriorityThreshold"` MaxTransactionsPerBlock int64 `yaml:"MaxTransactionsPerBlock"` StandbyValidators []string `yaml:"StandbyValidators"` SeedList []string `yaml:"SeedList"` diff --git a/config/protocol.mainnet.yml b/config/protocol.mainnet.yml index d59b28f40..24866c2d6 100644 --- a/config/protocol.mainnet.yml +++ b/config/protocol.mainnet.yml @@ -1,6 +1,8 @@ ProtocolConfiguration: Magic: 7630401 AddressVersion: 23 + SecondsPerBlock: 15 + LowPriorityThreshold: 0.001 StandbyValidators: - 03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c - 02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093 diff --git a/config/protocol.privnet.yml b/config/protocol.privnet.yml index adbb30d70..929025858 100644 --- a/config/protocol.privnet.yml +++ b/config/protocol.privnet.yml @@ -1,15 +1,17 @@ ProtocolConfiguration: Magic: 56753 AddressVersion: 23 + SecondsPerBlock: 15 + LowPriorityThreshold: 0.000 StandbyValidators: - - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 - - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 + - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 + - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e + - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 + - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 SeedList: - - 127.0.0.1:20334 - - 127.0.0.1:20335 - - 127.0.0.1:20336 + - 127.0.0.1:20334 + - 127.0.0.1:20335 + - 127.0.0.1:20336 SystemFee: EnrollmentTransaction: 1000 IssueTransaction: 500 diff --git a/config/protocol.testnet.yml b/config/protocol.testnet.yml index c0eac24ee..c9aef4fa0 100644 --- a/config/protocol.testnet.yml +++ b/config/protocol.testnet.yml @@ -1,6 +1,8 @@ ProtocolConfiguration: Magic: 1953787457 AddressVersion: 23 + SecondsPerBlock: 15 + LowPriorityThreshold: 0.000 StandbyValidators: - 0327da12b5c40200e9f65569476bbff2218da4f32548ff43b6387ec1416a231ee8 - 026ce35b29147ad09e4afe4ec4a7319095f08198fa8babbe3c56e970b143528d22 diff --git a/config/protocol.unit_testnet.yml b/config/protocol.unit_testnet.yml index baefd1690..b0e8e6ca9 100644 --- a/config/protocol.unit_testnet.yml +++ b/config/protocol.unit_testnet.yml @@ -1,15 +1,17 @@ ProtocolConfiguration: Magic: 56753 AddressVersion: 23 + SecondsPerBlock: 15 + LowPriorityThreshold: 0.000 StandbyValidators: - - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 - - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 + - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 + - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e + - 03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699 + - 02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62 SeedList: - - 127.0.0.1:20334 - - 127.0.0.1:20335 - - 127.0.0.1:20336 + - 127.0.0.1:20334 + - 127.0.0.1:20335 + - 127.0.0.1:20336 SystemFee: EnrollmentTransaction: 1000 IssueTransaction: 500 diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 3e294681e..1cbdda6a7 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -5,6 +5,7 @@ import ( "context" "encoding/binary" "fmt" + "math" "sync/atomic" "time" @@ -12,12 +13,12 @@ import ( "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/util" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) // tuning parameters const ( - secondsPerBlock = 15 headerBatchCount = 2000 version = "0.0.1" ) @@ -55,6 +56,8 @@ type Blockchain struct { // Whether we will verify received blocks. verifyBlocks bool + + memPool MemPool } type headersOpFunc func(headerList *HeaderHashList) @@ -69,10 +72,11 @@ func NewBlockchain(ctx context.Context, s storage.Store, cfg config.ProtocolConf headersOpDone: make(chan struct{}), blockCache: NewCache(), verifyBlocks: false, + memPool: NewMemPool(50000), } - go bc.run(ctx) - if err := bc.init(); err != nil { + go bc.run(ctx) + if err := bc.init(); err != nil { return nil, err } @@ -457,6 +461,10 @@ 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 { + return tx, 0, nil // the height is not actually defined for memPool transaction. Not sure if zero is a good number in this case. + } + key := storage.AppendPrefix(storage.DataTransaction, hash.BytesReverse()) b, err := bc.Get(key) if err != nil { @@ -506,9 +514,17 @@ func (bc *Blockchain) GetHeader(hash util.Uint256) (*Header, error) { return block.Header(), nil } -// HasBlock return true if the blockchain contains he given +// HasTransaction return true if the blockchain contains he given // transaction hash. func (bc *Blockchain) HasTransaction(hash util.Uint256) bool { + if bc.memPool.ContainsKey(hash) { + return true + } + + key := storage.AppendPrefix(storage.DataTransaction, hash.BytesReverse()) + if _, err := bc.Get(key); err == nil { + return true + } return false } @@ -596,7 +612,7 @@ func (bc *Blockchain) GetConfig() config.ProtocolConfiguration { // transaction package because of a import cycle problem. Perhaps we should think to re-design // the code base to avoid this situation. func (bc *Blockchain) References(t *transaction.Transaction) map[util.Uint256]*transaction.Output { - references := make(map[util.Uint256]*transaction.Output) + references := make(map[util.Uint256]*transaction.Output, 0) for prevHash, inputs := range t.GroupInputsByPrevHash() { if tx, _, err := bc.GetTransaction(prevHash); err != nil { @@ -641,8 +657,279 @@ func (bc *Blockchain) SystemFee(t *transaction.Transaction) util.Fixed8 { return bc.GetConfig().SystemFee.TryGetValue(t.Type) } +// IsLowPriority flags a trnsaction as low priority if the network fee is less than +// LowPriorityThreshold +func (bc *Blockchain) IsLowPriority(t *transaction.Transaction) bool { + return bc.NetworkFee(t) < util.NewFixed8FromFloat(bc.GetConfig().LowPriorityThreshold) +} + +// GetMemPool returns the memory pool of the blockchain. +func (bc *Blockchain) GetMemPool() MemPool { + return bc.memPool +} + +// Verify verifies whether a transaction is bonafide or not. +// Golang implementation of Verify method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L270). +func (bc *Blockchain) Verify(t *transaction.Transaction) error { + if t.Size() > transaction.MaxTransactionSize { + return errors.Errorf("invalid transaction size = %d. It shoud be less then MaxTransactionSize = %d", t.Size(), transaction.MaxTransactionSize) + } + if ok := bc.verifyInputs(t); !ok { + return errors.New("invalid transaction's inputs") + } + if ok := bc.memPool.Verify(t); !ok { + return errors.New("invalid transaction due to conflicts with the memory pool") + } + if IsDoubleSpend(bc.Store, t) { + return errors.New("invalid transaction caused by double spending") + } + if ok := bc.verifyOutputs(t); !ok { + return errors.New("invalid transaction's outputs") + } + if ok := bc.verifyResults(t); !ok { + return errors.New("invalid transaction's results") + } + + for _, a := range t.Attributes { + if a.Usage == transaction.ECDH02 || a.Usage == transaction.ECDH03 { + return errors.Errorf("invalid attribute's usage = %s ", a.Usage) + } + } + + return bc.VerifyWitnesses(t) +} + +func (bc *Blockchain) verifyInputs(t *transaction.Transaction) bool { + for i := 1; i < len(t.Inputs); i++ { + for j := 0; j < i; j++ { + if t.Inputs[i].PrevHash == t.Inputs[j].PrevHash && t.Inputs[i].PrevIndex == t.Inputs[j].PrevIndex { + return false + } + } + } + + return true +} + +func (bc *Blockchain) verifyOutputs(t *transaction.Transaction) bool { + for assetID, outputs := range t.GroupOutputByAssetID() { + assetState := bc.GetAssetState(assetID) + if assetState == nil { + return false + } + + if assetState.Expiration < bc.blockHeight+1 && assetState.AssetType != transaction.GoverningToken && assetState.AssetType != transaction.UtilityToken { + return false + } + + for _, out := range outputs { + if int64(out.Amount)%int64(math.Pow10(8-int(assetState.Precision))) != 0 { + return false + } + } + } + + return true +} + +func (bc *Blockchain) verifyResults(t *transaction.Transaction) bool { + results := bc.GetTransationResults(t) + if results == nil { + return false + } + var resultsDestroy []*transaction.Result + var resultsIssue []*transaction.Result + for _, re := range results { + if re.Amount.GreaterThan(util.Fixed8(0)) { + resultsDestroy = append(resultsDestroy, re) + } + + if re.Amount.LessThan(util.Fixed8(0)) { + resultsIssue = append(resultsIssue, re) + } + } + if len(resultsDestroy) > 1 { + return false + } + if len(resultsDestroy) == 1 && resultsDestroy[0].AssetID != utilityTokenTX().Hash() { + return false + } + if bc.SystemFee(t).GreaterThan(util.Fixed8(0)) && (len(resultsDestroy) == 0 || resultsDestroy[0].Amount.LessThan(bc.SystemFee(t))) { + return false + } + + switch t.Type { + case transaction.MinerType, transaction.ClaimType: + for _, r := range resultsIssue { + if r.AssetID != utilityTokenTX().Hash() { + return false + } + } + break + case transaction.IssueType: + for _, r := range resultsIssue { + if r.AssetID == utilityTokenTX().Hash() { + return false + } + } + break + default: + if len(resultsIssue) > 0 { + return false + } + break + } + + return true +} + +// GetTransationResults returns the transaction results aggregate by assetID. +// Golang of GetTransationResults method in C# (https://github.com/neo-project/neo/blob/master/neo/Network/P2P/Payloads/Transaction.cs#L207) +func (bc *Blockchain) GetTransationResults(t *transaction.Transaction) []*transaction.Result { + var tempResults []*transaction.Result + var results []*transaction.Result + tempGroupResult := make(map[util.Uint256]util.Fixed8) + + if references := bc.References(t); references == nil { + return nil + } else { + for _, output := range references { + tempResults = append(tempResults, &transaction.Result{ + AssetID: output.AssetID, + Amount: output.Amount, + }) + } + for _, output := range t.Outputs { + tempResults = append(tempResults, &transaction.Result{ + AssetID: output.AssetID, + Amount: -output.Amount, + }) + } + for _, r := range tempResults { + if amount, ok := tempGroupResult[r.AssetID]; ok { + tempGroupResult[r.AssetID] = amount.Add(r.Amount) + } else { + tempGroupResult[r.AssetID] = r.Amount + } + } + + results = []*transaction.Result{} // this assignment is necessary. (Most of the time amount == 0 and results is the empty slice.) + for assetID, amount := range tempGroupResult { + if amount != util.Fixed8(0) { + results = append(results, &transaction.Result{ + AssetID: assetID, + Amount: amount, + }) + } + } + + return results + + } + +} + +// 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) +func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([]util.Uint160, error) { + references := bc.References(t) + if references == nil { + return nil, errors.New("Invalid operation") + } + hashes := make(map[util.Uint160]bool) + for _, i := range t.Inputs { + h := references[i.PrevHash].ScriptHash + if _, ok := hashes[h]; !ok { + hashes[h] = true + } + } + for _, a := range t.Attributes { + if a.Usage == transaction.Script { + h, err := util.Uint160DecodeBytes(a.Data) + if err != nil { + return nil, err + } + if _, ok := hashes[h]; !ok { + hashes[h] = true + } + } + } + + for a, outputs := range t.GroupOutputByAssetID() { + as := bc.GetAssetState(a) + if as == nil { + return nil, errors.New("Invalid operation") + } + if as.AssetType == transaction.DutyFlag { + for _, o := range outputs { + h := o.ScriptHash + if _, ok := hashes[h]; !ok { + hashes[h] = true + } + } + } + } + // convert hashes to []util.Uint160 + hashesResult := make([]util.Uint160, 0, len(hashes)) + for h := range hashes { + hashesResult = append(hashesResult, h) + } + + return hashesResult, nil + +} + +// VerifyWitnesses verify the scripts (witnesses) that come with a transactions. +// Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87). +// Unfortunately the IVerifiable interface could not be implemented because we can't move the References method in blockchain.go to the transaction.go file +func (bc *Blockchain) VerifyWitnesses(t *transaction.Transaction) error { + hashes, err := bc.GetScriptHashesForVerifying(t) + if err != nil { + return err + } + + witnesses := t.Scripts + if len(hashes) != len(witnesses) { + return errors.Errorf("expected len(hashes) == len(witnesses). got: %d != %d", len(hashes), len(witnesses)) + } + for i := 0; i < len(hashes); i++ { + verification := witnesses[i].VerificationScript + + if len(verification) == 0 { + /*TODO: replicate following C# code: + using (ScriptBuilder sb = new ScriptBuilder()) + { + sb.EmitAppCall(hashes[i].ToArray()); + verification = sb.ToArray(); + } + */ + + } else { + if h, err := witnesses[i].ScriptHash(); err != nil || hashes[i] != h { + return err + } + } + + /*TODO: replicate following C# code: + using (ApplicationEngine engine = new ApplicationEngine(TriggerType.Verification, verifiable, snapshot, Fixed8.Zero)) + { + engine.LoadScript(verification); + engine.LoadScript(verifiable.Witnesses[i].InvocationScript); + if (!engine.Execute()) return false; + if (engine.ResultStack.Count != 1 || !engine.ResultStack.Pop().GetBoolean()) return false; + }*/ + } + + return nil +} + func hashAndIndexToBytes(h util.Uint256, index uint32) []byte { buf := make([]byte, 4) binary.LittleEndian.PutUint32(buf, index) return append(h.BytesReverse(), buf...) } + +func (bc *Blockchain) secondsPerBlock() int { + return bc.config.SecondsPerBlock +} diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 9eec695f0..ba0b6d732 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -25,7 +25,7 @@ type Blockchainer interface { GetAccountState(util.Uint160) *AccountState GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) References(t *transaction.Transaction) map[util.Uint256]*transaction.Output - FeePerByte(t *transaction.Transaction) util.Fixed8 - SystemFee(t *transaction.Transaction) util.Fixed8 - NetworkFee(t *transaction.Transaction) util.Fixed8 + Feer // fee interface + Verify(t *transaction.Transaction) error + GetMemPool() MemPool } diff --git a/pkg/core/feer.go b/pkg/core/feer.go new file mode 100644 index 000000000..61adbe3ca --- /dev/null +++ b/pkg/core/feer.go @@ -0,0 +1,14 @@ +package core + +import ( + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// 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 + FeePerByte(t *transaction.Transaction) util.Fixed8 + SystemFee(t *transaction.Transaction) util.Fixed8 +} diff --git a/pkg/core/mem_pool.go b/pkg/core/mem_pool.go new file mode 100644 index 000000000..0a77110ce --- /dev/null +++ b/pkg/core/mem_pool.go @@ -0,0 +1,267 @@ +package core + +import ( + "sort" + "sync" + "time" + + "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// PoolItem represents a transaction in the the Memory pool. +type PoolItem struct { + txn *transaction.Transaction + timeStamp time.Time + fee Feer +} + +// PoolItems slice of PoolItem +type PoolItems []*PoolItem + +// MemPool stores the unconfirms transactions. +type MemPool struct { + lock *sync.RWMutex + unsortedTxn map[util.Uint256]*PoolItem + unverifiedTxn map[util.Uint256]*PoolItem + sortedHighPrioTxn PoolItems + sortedLowPrioTxn PoolItems + unverifiedSortedHighPrioTxn PoolItems + unverifiedSortedLowPrioTxn PoolItems + + capacity int +} + +func (p PoolItems) Len() int { return len(p) } +func (p PoolItems) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p PoolItems) Less(i, j int) bool { return p[i].CompareTo(p[j]) < 0 } + +// CompareTo returns the difference between two PoolItems. +// difference < 0 implies p < otherP. +// difference = 0 implies p = otherP. +// difference > 0 implies p > otherP. +func (p PoolItem) CompareTo(otherP *PoolItem) int { + if otherP == nil { + return 1 + } + + if p.fee.IsLowPriority(p.txn) && p.fee.IsLowPriority(otherP.txn) { + thisIsClaimTx := p.txn.Type == transaction.ClaimType + otherIsClaimTx := otherP.txn.Type == transaction.ClaimType + + if thisIsClaimTx != otherIsClaimTx { + // This is a claim Tx and other isn't. + if thisIsClaimTx { + return 1 + } + // The other is claim Tx and this isn't. + return -1 + } + } + + // Fees sorted ascending + pFPB := p.fee.FeePerByte(p.txn) + otherFPB := p.fee.FeePerByte(otherP.txn) + if ret := pFPB.CompareTo(otherFPB); ret != 0 { + return ret + } + + pNF := p.fee.NetworkFee(p.txn) + otherNF := p.fee.NetworkFee(otherP.txn) + if ret := pNF.CompareTo(otherNF); ret != 0 { + return ret + } + + // Transaction hash sorted descending + return otherP.txn.Hash().CompareTo(p.txn.Hash()) +} + +// Count returns the total number of uncofirm transactions. +func (mp MemPool) Count() int { + mp.lock.RLock() + defer mp.lock.RUnlock() + + return len(mp.unsortedTxn) + len(mp.unverifiedTxn) +} + +// ContainsKey checks if a transactions hash is in the MemPool. +func (mp MemPool) ContainsKey(hash util.Uint256) bool { + mp.lock.RLock() + defer mp.lock.RUnlock() + + if _, ok := mp.unsortedTxn[hash]; ok { + return true + } + + if _, ok := mp.unverifiedTxn[hash]; ok { + return true + } + + return false +} + +// TryAdd try to add the PoolItem to the MemPool. +func (mp MemPool) TryAdd(hash util.Uint256, pItem *PoolItem) bool { + var pool PoolItems + + mp.lock.RLock() + if _, ok := mp.unsortedTxn[hash]; ok { + return false + } + mp.unsortedTxn[hash] = pItem + mp.lock.RUnlock() + + if pItem.fee.IsLowPriority(pItem.txn) { + pool = mp.sortedLowPrioTxn + } else { + pool = mp.sortedHighPrioTxn + } + + mp.lock.Lock() + pool = append(pool, pItem) + sort.Sort(pool) + mp.lock.Unlock() + + if mp.Count() > mp.capacity { + (&mp).RemoveOverCapacity() + } + mp.lock.RLock() + _, ok := mp.unsortedTxn[hash] + mp.lock.RUnlock() + return ok +} + +// RemoveOverCapacity removes transactions with lowest fees until the total number of transactions +// in the MemPool is within the capacity of the MemPool. +func (mp *MemPool) RemoveOverCapacity() { + for mp.Count()-mp.capacity > 0 { + mp.lock.Lock() + if minItem, argPosition := getLowestFeeTransaction(mp.sortedLowPrioTxn, mp.unverifiedSortedLowPrioTxn); minItem != nil { + if argPosition == 1 { + // minItem belongs to the mp.sortedLowPrioTxn slice. + // The corresponding unsorted pool is is mp.unsortedTxn. + delete(mp.unsortedTxn, minItem.txn.Hash()) + mp.sortedLowPrioTxn = append(mp.sortedLowPrioTxn[:0], mp.sortedLowPrioTxn[1:]...) + } else { + // minItem belongs to the mp.unverifiedSortedLowPrioTxn slice. + // The corresponding unsorted pool is is mp.unverifiedTxn. + delete(mp.unverifiedTxn, minItem.txn.Hash()) + mp.unverifiedSortedLowPrioTxn = append(mp.unverifiedSortedLowPrioTxn[:0], mp.unverifiedSortedLowPrioTxn[1:]...) + + } + } else if minItem, argPosition := getLowestFeeTransaction(mp.sortedHighPrioTxn, mp.unverifiedSortedHighPrioTxn); minItem != nil { + if argPosition == 1 { + // minItem belongs to the mp.sortedHighPrioTxn slice. + // The corresponding unsorted pool is is mp.unsortedTxn. + delete(mp.unsortedTxn, minItem.txn.Hash()) + mp.sortedHighPrioTxn = append(mp.sortedHighPrioTxn[:0], mp.sortedHighPrioTxn[1:]...) + } else { + // minItem belongs to the mp.unverifiedSortedHighPrioTxn slice. + // The corresponding unsorted pool is is mp.unverifiedTxn. + delete(mp.unverifiedTxn, minItem.txn.Hash()) + mp.unverifiedSortedHighPrioTxn = append(mp.unverifiedSortedHighPrioTxn[:0], mp.unverifiedSortedHighPrioTxn[1:]...) + + } + } + mp.lock.Unlock() + } + +} + +// NewPoolItem returns a new PoolItem. +func NewPoolItem(t *transaction.Transaction, fee Feer) *PoolItem { + return &PoolItem{ + txn: t, + timeStamp: time.Now().UTC(), + fee: fee, + } +} + +// NewMemPool returns a new MemPool struct. +func NewMemPool(capacity int) MemPool { + return MemPool{ + lock: new(sync.RWMutex), + unsortedTxn: make(map[util.Uint256]*PoolItem), + unverifiedTxn: make(map[util.Uint256]*PoolItem), + capacity: capacity, + } +} + +// TryGetValue returns a transactions if it esists in the memory pool. +func (mp MemPool) TryGetValue(hash util.Uint256) (*transaction.Transaction, bool) { + mp.lock.Lock() + defer mp.lock.Unlock() + if pItem, ok := mp.unsortedTxn[hash]; ok { + return pItem.txn, ok + } + + if pItem, ok := mp.unverifiedTxn[hash]; ok { + return pItem.txn, ok + } + + return nil, false +} + +// getLowestFeeTransaction returns the PoolItem with the lowest fee amongst the "verifiedTxnSorted" +// and "unverifiedTxnSorted" PoolItems along with a integer. The integer can assume two values, 1 and 2 which indicate +// that the PoolItem with the lowest fee was found in "verifiedTxnSorted" respectively in "unverifiedTxnSorted". +// "verifiedTxnSorted" and "unverifiedTxnSorted" are sorted slice order by transaction fee ascending. This means that +// the transaction with lowest fee start at index 0. +// Reference: GetLowestFeeTransaction method in C# (https://github.com/neo-project/neo/blob/master/neo/Ledger/MemoryPool.cs) +func getLowestFeeTransaction(verifiedTxnSorted PoolItems, unverifiedTxnSorted PoolItems) (*PoolItem, int) { + minItem := min(unverifiedTxnSorted) + verifiedMin := min(verifiedTxnSorted) + if verifiedMin == nil || (minItem != nil && verifiedMin.CompareTo(minItem) >= 0) { + return minItem, 2 + } + + minItem = verifiedMin + return minItem, 1 + +} + +// min return the minimum item in a ascending sorted slice of pool items. +// The function can't be applied to unsorted slice! +func min(sortedPool PoolItems) *PoolItem { + if len(sortedPool) == 0 { + return nil + } + return sortedPool[0] +} + +// GetVerifiedTransactions returns a slice of Input from all the transactions in the memory pool +// whose hash is not included in excludedHashes. +func (mp *MemPool) GetVerifiedTransactions() []*transaction.Transaction { + var t []*transaction.Transaction + + mp.lock.Lock() + defer mp.lock.Unlock() + for _, p := range mp.unsortedTxn { + t = append(t, p.txn) + } + + return t +} + +// Verify verifies if the inputs of a transaction tx are already used in any other transaction in the memory pool. +// If yes, the transaction tx is not a valid transaction and the function return false. +// If no, the transaction tx is a valid transaction and the function return true. +func (mp MemPool) Verify(tx *transaction.Transaction) bool { + count := 0 + inputs := make([]*transaction.Input, 0) + for _, item := range mp.GetVerifiedTransactions() { + if tx.Hash().Equals(item.Hash()) { + inputs = append(inputs, item.Inputs...) + } + } + + for i := 0; i < len(inputs); i++ { + for j := 0; j < len(tx.Inputs); j++ { + if inputs[i].PrevHash.Equals(tx.Inputs[j].PrevHash) { + count++ + } + } + } + + return count == 0 +} diff --git a/pkg/core/transaction/input.go b/pkg/core/transaction/input.go index 15cc617e0..23ef80c03 100644 --- a/pkg/core/transaction/input.go +++ b/pkg/core/transaction/input.go @@ -36,6 +36,6 @@ func (in *Input) EncodeBinary(w io.Writer) error { } // Size returns the size in bytes of the Input -func (in *Input) Size() int { +func (in Input) Size() int { return in.PrevHash.Size() + 2 // 2 = sizeOf uint16 } diff --git a/pkg/core/transaction/result.go b/pkg/core/transaction/result.go new file mode 100644 index 000000000..76acee7a2 --- /dev/null +++ b/pkg/core/transaction/result.go @@ -0,0 +1,9 @@ +package transaction + +import "github.com/CityOfZion/neo-go/pkg/util" + +// Result represents the Result of a transaction. +type Result struct { + AssetID util.Uint256 + Amount util.Fixed8 +} diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index 3dea806cb..7a045d7f7 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -10,6 +10,12 @@ import ( log "github.com/sirupsen/logrus" ) +const ( + // MaxTransactionSize is the upper limit size in bytes that a transaction can reach. It is + // set to be 102400. + MaxTransactionSize = 102400 +) + // Transaction is a process recorded in the NEO blockchain. type Transaction struct { // The type of the transaction. @@ -251,6 +257,15 @@ func (t *Transaction) GroupInputsByPrevHash() map[util.Uint256][]*Input { return m } +// GroupOutputByAssetID groups all TX outputs by their assetID. +func (t Transaction) GroupOutputByAssetID() map[util.Uint256][]*Output { + m := make(map[util.Uint256][]*Output) + for _, out := range t.Outputs { + m[out.AssetID] = append(m[out.AssetID], out) + } + return m +} + // Size returns the size of the transaction in term of bytes func (t *Transaction) Size() int { attrSize := util.GetVarSize(t.Attributes) diff --git a/pkg/core/transaction/witness.go b/pkg/core/transaction/witness.go index 83dfefaac..d3e4d1570 100644 --- a/pkg/core/transaction/witness.go +++ b/pkg/core/transaction/witness.go @@ -55,3 +55,8 @@ func (w *Witness) MarshalJSON() ([]byte, error) { func (w *Witness) Size() int { return util.GetVarSize(w.InvocationScript) + util.GetVarSize(w.VerificationScript) } + +// ScriptHash returns the hash of the VerificationScript. +func (w Witness) ScriptHash() (util.Uint160, error) { + return util.Uint160FromScript(w.VerificationScript) +} diff --git a/pkg/core/unspent_coin_state.go b/pkg/core/unspent_coin_state.go index a47758489..e588e0575 100644 --- a/pkg/core/unspent_coin_state.go +++ b/pkg/core/unspent_coin_state.go @@ -7,6 +7,7 @@ import ( "io" "github.com/CityOfZion/neo-go/pkg/core/storage" + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/util" ) @@ -91,3 +92,32 @@ func (s *UnspentCoinState) DecodeBinary(r io.Reader) error { } return nil } + +// IsDoubleSpend verifies that the input transactions are not double spent. +func IsDoubleSpend(s storage.Store, tx *transaction.Transaction) bool { + if len(tx.Inputs) == 0 { + return false + } + + for prevHash, inputs := range tx.GroupInputsByPrevHash() { + unspent := &UnspentCoinState{} + key := storage.AppendPrefix(storage.STCoin, prevHash.BytesReverse()) + if b, err := s.Get(key); err == nil { + if err := unspent.DecodeBinary(bytes.NewReader(b)); err != nil { + return false + } + if unspent == nil { + return true + } + + for _, input := range inputs { + if int(input.PrevIndex) >= len(unspent.states) || unspent.states[input.PrevIndex] == CoinStateSpent { + return true + } + } + } + + } + + return false +} diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index f28f5fd63..9bb6803b1 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -77,6 +77,18 @@ func (chain testChain) GetTransaction(util.Uint256) (*transaction.Transaction, u panic("TODO") } +func (chain testChain) GetMemPool() core.MemPool { + panic("TODO") +} + +func (chain testChain) IsLowPriority(*transaction.Transaction) bool { + panic("TODO") +} + +func (chain testChain) Verify(*transaction.Transaction) error { + panic("TODO") +} + type testDiscovery struct{} func (d testDiscovery) BackFill(addrs ...string) {} diff --git a/pkg/network/relay_reason.go b/pkg/network/relay_reason.go new file mode 100644 index 000000000..2f624eb2a --- /dev/null +++ b/pkg/network/relay_reason.go @@ -0,0 +1,15 @@ +package network + +// RelayReason is the type which describes the different relay outcome +type RelayReason uint8 + +// List of valid RelayReason. +const ( + RelaySucceed RelayReason = iota + RelayAlreadyExists + RelayOutOfMemory + RelayUnableToVerify + RelayInvalid + RelayPolicyFail + RelayUnknown +) diff --git a/pkg/network/server.go b/pkg/network/server.go index f7160c6da..f6e9f4416 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -7,6 +7,7 @@ import ( "time" "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/network/payload" "github.com/CityOfZion/neo-go/pkg/util" log "github.com/sirupsen/logrus" @@ -310,3 +311,41 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error { } return nil } + +// RelayTxn a new transaction to the local node and the connected peers. +// Reference: the method OnRelay in C#: https://github.com/neo-project/neo/blob/master/neo/Network/P2P/LocalNode.cs#L159 +func (s *Server) RelayTxn(t *transaction.Transaction) RelayReason { + if t.Type == transaction.MinerType { + return RelayInvalid + } + if s.chain.HasTransaction(t.Hash()) { + return RelayAlreadyExists + } + if err := s.chain.Verify(t); err != nil { + return RelayInvalid + } + // TODO: Implement Plugin.CheckPolicy? + //if (!Plugin.CheckPolicy(transaction)) + // return RelayResultReason.PolicyFail; + if ok := s.chain.GetMemPool().TryAdd(t.Hash(), core.NewPoolItem(t, s.chain)); !ok { + return RelayOutOfMemory + } + + for p := range s.Peers() { + payload := payload.NewInventory(payload.TXType, []util.Uint256{t.Hash()}) + s.RelayDirectly(p, payload) + } + + return RelaySucceed +} + +// RelayDirectly relay directly the inventory to the remote peers. +// Reference: the method OnRelayDirectly in C#: https://github.com/neo-project/neo/blob/master/neo/Network/P2P/LocalNode.cs#L166 +func (s *Server) RelayDirectly(p Peer, inv *payload.Inventory) { + if !p.Version().Relay { + return + } + + p.WriteMsg(NewMessage(s.Net, CMDInv, inv)) + +} diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 07576b52a..ce7b62fe3 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -1,6 +1,7 @@ package rpc import ( + "bytes" "context" "encoding/hex" "fmt" @@ -8,6 +9,7 @@ import ( "strconv" "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/crypto" "github.com/CityOfZion/neo-go/pkg/network" "github.com/CityOfZion/neo-go/pkg/rpc/result" @@ -183,7 +185,7 @@ Methods: results = peers - case "getblocksysfee", "getcontractstate", "getrawmempool", "getstorage", "submitblock", "gettxout", "invoke", "invokefunction", "invokescript", "sendrawtransaction": + case "getblocksysfee", "getcontractstate", "getrawmempool", "getstorage", "submitblock", "gettxout", "invoke", "invokefunction", "invokescript": results = "TODO" @@ -227,37 +229,10 @@ Methods: results = "Invalid public account address" } case "getrawtransaction": - param0, err := reqParams.ValueWithType(0, "string") - if err != nil { - resultsErr = err - } else if txHash, err := util.Uint256DecodeString(param0.StringVal); err != nil { - err = errors.Wrapf(err, "param at index 0, (%s), could not be decode to Uint256", param0.StringVal) - resultsErr = NewInvalidParamsError(err.Error(), err) - } else if tx, height, err := s.chain.GetTransaction(txHash); err != nil { - err = errors.Wrapf(err, "Invalid transaction hash: %s", txHash) - resultsErr = NewInvalidParamsError(err.Error(), err) - } else if len(reqParams) >= 2 { - _header := s.chain.GetHeaderHash(int(height)) - header, err := s.chain.GetHeader(_header) - if err != nil { - resultsErr = NewInvalidParamsError(err.Error(), err) - } + results, resultsErr = s.getrawtransaction(reqParams) - param1, _ := reqParams.ValueAt(1) - switch v := param1.RawValue.(type) { - - case int, float64, bool, string: - if v == 0 || v == "0" || v == 0.0 || v == false || v == "false" { - results = hex.EncodeToString(tx.Bytes()) - } else { - results = wrappers.NewTransactionOutputRaw(tx, header, s.chain) - } - default: - results = wrappers.NewTransactionOutputRaw(tx, header, s.chain) - } - } else { - results = hex.EncodeToString(tx.Bytes()) - } + case "sendrawtransaction": + results, resultsErr = s.sendrawtransaction(reqParams) default: resultsErr = NewMethodNotFoundError(fmt.Sprintf("Method '%s' not supported", req.Method), nil) @@ -271,6 +246,86 @@ Methods: req.WriteResponse(w, results) } +func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) { + var resultsErr error + var results interface{} + + param0, err := reqParams.ValueWithType(0, "string") + if err != nil { + resultsErr = err + } else if txHash, err := util.Uint256DecodeString(param0.StringVal); err != nil { + resultsErr = errInvalidParams + } else if tx, height, err := s.chain.GetTransaction(txHash); err != nil { + err = errors.Wrapf(err, "Invalid transaction hash: %s", txHash) + resultsErr = NewInvalidParamsError(err.Error(), err) + } else if len(reqParams) >= 2 { + _header := s.chain.GetHeaderHash(int(height)) + header, err := s.chain.GetHeader(_header) + if err != nil { + resultsErr = NewInvalidParamsError(err.Error(), err) + } + + param1, _ := reqParams.ValueAt(1) + switch v := param1.RawValue.(type) { + + case int, float64, bool, string: + if v == 0 || v == "0" || v == 0.0 || v == false || v == "false" { + results = hex.EncodeToString(tx.Bytes()) + } else { + results = wrappers.NewTransactionOutputRaw(tx, header, s.chain) + } + default: + results = wrappers.NewTransactionOutputRaw(tx, header, s.chain) + } + } else { + results = hex.EncodeToString(tx.Bytes()) + } + + return results, resultsErr +} + +func (s *Server) sendrawtransaction(reqParams Params) (interface{}, error) { + var resultsErr error + var results interface{} + + param, err := reqParams.ValueWithType(0, "string") + if err != nil { + resultsErr = err + } else if byteTx, err := hex.DecodeString(param.StringVal); err != nil { + resultsErr = errInvalidParams + } else { + r := bytes.NewReader(byteTx) + tx := &transaction.Transaction{} + err = tx.DecodeBinary(r) + if err != nil { + err = errors.Wrap(err, "transaction DecodeBinary failed") + } + relayReason := s.coreServer.RelayTxn(tx) + switch relayReason { + case network.RelaySucceed: + results = true + case network.RelayAlreadyExists: + err = errors.New("block or transaction already exists and cannot be sent repeatedly") + case network.RelayOutOfMemory: + err = errors.New("the memory pool is full and no more transactions can be sent") + case network.RelayUnableToVerify: + err = errors.New("the block cannot be validated") + case network.RelayInvalid: + err = errors.New("block or transaction validation failed") + case network.RelayPolicyFail: + err = errors.New("one of the Policy filters failed") + default: + err = errors.New("unknown error") + + } + if err != nil { + resultsErr = NewInternalServerError(err.Error(), err) + } + } + + return results, resultsErr +} + func (s Server) validBlockHeight(param *Param) bool { return param.IntVal >= 0 && param.IntVal <= int(s.chain.BlockHeight()) } diff --git a/pkg/rpc/server_test.go b/pkg/rpc/server_test.go index d22f65d47..d0e70efe6 100644 --- a/pkg/rpc/server_test.go +++ b/pkg/rpc/server_test.go @@ -116,7 +116,7 @@ var testRpcCases = []tc{ { rpcCall: `{ "jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["45a41306c846ea80290416143e8e856559818065be3f4e143c60e43a", 1] }`, method: "getrawtransaction_4", - expectedResult: `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params","data":"param at index 0, (45a41306c846ea80290416143e8e856559818065be3f4e143c60e43a), could not be decode to Uint256: expected string size of 64 got 56"},"id":1}`, + expectedResult: `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params"},"id":1}`, }, // Good case, valid transaction @@ -216,6 +216,27 @@ var testRpcCases = []tc{ method: "validateaddress_4", expectedResult: `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params"},"id":1}`, }, + + // Good case + { + rpcCall: `{ "jsonrpc": "2.0", "id": 1, "method": "sendrawtransaction", "params": ["80000190274d792072617720636f6e7472616374207472616e73616374696f6e206465736372697074696f6e01949354ea0a8b57dfee1e257a1aedd1e0eea2e5837de145e8da9c0f101bfccc8e0100029b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc500a3e11100000000ea610aa6db39bd8c8556c9569d94b5e5a5d0ad199b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5004f2418010000001cc9c05cefffe6cdd7b182816a9152ec218d2ec0014140dbd3cddac5cb2bd9bf6d93701f1a6f1c9dbe2d1b480c54628bbb2a4d536158c747a6af82698edf9f8af1cac3850bcb772bd9c8e4ac38f80704751cc4e0bd0e67232103cbb45da6072c14761c9da545749d9cfd863f860c351066d16df480602a2024c6ac"] }`, + method: "sendrawtransaction_1", + expectedResult: `{"jsonrpc":"2.0","result":true,"id":1}`, + }, + + /* Good case: TODO: uncomment this test case once https://github.com/CityOfZion/neo-go/issues/173 is fixed! + { + rpcCall: `{ "jsonrpc": "2.0", "id": 1, "method": "sendrawtransaction", "params": ["d1001b00046e616d6567d3d8602814a429a91afdbaa3914884a1c90c733101201cc9c05cefffe6cdd7b182816a9152ec218d2ec000000141403387ef7940a5764259621e655b3c621a6aafd869a611ad64adcc364d8dd1edf84e00a7f8b11b630a377eaef02791d1c289d711c08b7ad04ff0d6c9caca22cfe6232103cbb45da6072c14761c9da545749d9cfd863f860c351066d16df480602a2024c6ac"] }`, + method: "sendrawtransaction_2", + expectedResult: `{"jsonrpc":"2.0","result":true,"id":1}`, + },*/ + + // Bad case, incorrect raw transaction + { + rpcCall: `{ "jsonrpc": "2.0", "id": 1, "method": "sendrawtransaction", "params": ["0274d792072617720636f6e7472616374207472616e73616374696f6e206465736372697074696f6e01949354ea0a8b57dfee1e257a1aedd1e0eea2e5837de145e8da9c0f101bfccc8e0100029b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc500a3e11100000000ea610aa6db39bd8c8556c9569d94b5e5a5d0ad199b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5004f2418010000001cc9c05cefffe6cdd7b182816a9152ec218d2ec0014140dbd3cddac5cb2bd9bf6d93701f1a6f1c9dbe2d1b480c54628bbb2a4d536158c747a6af82698edf9f8af1cac3850bcb772bd9c8e4ac38f80704751cc4e0bd0e67232103cbb45da6072c14761c9da545749d9cfd863f860c351066d16df480602a2024c6ac"] }`, + method: "sendrawtransaction_1", + expectedResult: `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params"},"id":1}`, + }, } func TestHandler(t *testing.T) { diff --git a/pkg/util/fixed8.go b/pkg/util/fixed8.go index 6eccc5388..c9e6412e6 100644 --- a/pkg/util/fixed8.go +++ b/pkg/util/fixed8.go @@ -128,3 +128,26 @@ func (f Fixed8) Add(g Fixed8) Fixed8 { func (f Fixed8) Sub(g Fixed8) Fixed8 { return NewFixed8(f.Value() - g.Value()) } + +// LessThan implements Fixd8 < operator. +func (f Fixed8) LessThan(g Fixed8) bool { + return f.Value() < g.Value() +} + +// GreaterThan implements Fixd8 < operator. +func (f Fixed8) GreaterThan(g Fixed8) bool { + return f.Value() > g.Value() +} + +// Equal implements Fixd8 == operator. +func (f Fixed8) Equal(g Fixed8) bool { + return f.Value() == g.Value() +} + +// CompareTo returns the difference between the f and g. +// difference < 0 implies f < g. +// difference = 0 implies f = g. +// difference > 0 implies f > g. +func (f Fixed8) CompareTo(g Fixed8) int { + return int(f.Value() - g.Value()) +} diff --git a/pkg/util/size.go b/pkg/util/size.go index 58a8a04c7..dfc88c614 100644 --- a/pkg/util/size.go +++ b/pkg/util/size.go @@ -65,20 +65,22 @@ func GetVarSize(value interface{}) int { valueLength := v.Len() valueSize := 0 - switch reflect.ValueOf(value).Index(0).Interface().(type) { - case io.Serializable: - for i := 0; i < valueLength; i++ { - elem := v.Index(i).Interface().(io.Serializable) - valueSize += elem.Size() + if valueLength != 0 { + switch reflect.ValueOf(value).Index(0).Interface().(type) { + case io.Serializable: + for i := 0; i < valueLength; i++ { + elem := v.Index(i).Interface().(io.Serializable) + valueSize += elem.Size() + } + case uint8, int8: + valueSize = valueLength + case uint16, int16: + valueSize = valueLength * 2 + case uint32, int32: + valueSize = valueLength * 4 + case uint64, int64: + valueSize = valueLength * 8 } - case uint8, int8: - valueSize = valueLength - case uint16, int16: - valueSize = valueLength * 2 - case uint32, int32: - valueSize = valueLength * 4 - case uint64, int64: - valueSize = valueLength * 8 } return GetVarIntSize(valueLength) + valueSize diff --git a/pkg/util/uint256.go b/pkg/util/uint256.go index b7720a7a4..9565cb956 100644 --- a/pkg/util/uint256.go +++ b/pkg/util/uint256.go @@ -1,6 +1,7 @@ package util import ( + "bytes" "encoding/hex" "encoding/json" "fmt" @@ -74,3 +75,9 @@ func (u Uint256) Size() int { func (u Uint256) MarshalJSON() ([]byte, error) { return []byte(`"0x` + u.String() + `"`), nil } + +// CompareTo compares two Uint256 with each other. Possible output: 1, -1, 0 +// 1 implies u > other. +// -1 implies u < other. +// 0 implies u = other. +func (u Uint256) CompareTo(other Uint256) int { return bytes.Compare(u[:], other[:]) }