Implement rpc server method: sendrawtransaction (#174)

* Added new config attributes: 'SecondsPerBlock','LowPriorityThreshold'

* Added new files:

* Added new method: CompareTo

* Fixed empty Slice case

* Added new methods: LessThan, GreaterThan, Equal, CompareTo

* Added new method: InputIntersection

* Added MaxTransactionSize, GroupOutputByAssetID

* Added ned method: ScriptHash

* Added new method: IsDoubleSpend

* Refactor blockchainer, Added Feer interface, Verify and GetMemPool method

* 1) Added MemPool
2) Added new methods to satisfy the blockchainer interface: IsLowPriority, Verify, GetMemPool

* Added new methods: RelayTxn, RelayDirectly

* Fixed tests

* Implemented RPC server method sendrawtransaction

* Refactor getrawtransaction, sendrawtransaction in separate methods

* Moved 'secondsPerBlock' to config file

* Implemented Kim suggestions:
1) Fixed data race issues
2) refactor Verify method
3) Get rid of unused InputIntersection  method due to refactoring Verify method
4) Fixed bug in https://github.com/CityOfZion/neo-go/pull/174#discussion_r264108135
5) minor simplications of the code

* Fixed minor issues related to

1) space
2) getter methods do not need pointer on the receiver
3) error message
4) refactoring  CompareTo method in uint256.go

* Fixed small issues

* Use sync.RWMutex instead of sync.Mutex

* Refined (R)Lock/(R)Unlock

* return error instead of bool in Verify methods
This commit is contained in:
dauTT 2019-03-20 13:30:05 +01:00 committed by decentralisedkev
parent a5e85d69de
commit 095653af23
22 changed files with 879 additions and 68 deletions

View file

@ -43,6 +43,8 @@ type (
ProtocolConfiguration struct { ProtocolConfiguration struct {
Magic NetMode `yaml:"Magic"` Magic NetMode `yaml:"Magic"`
AddressVersion int64 `yaml:"AddressVersion"` AddressVersion int64 `yaml:"AddressVersion"`
SecondsPerBlock int `yaml:"SecondsPerBlock"`
LowPriorityThreshold float64 `yaml:"LowPriorityThreshold"`
MaxTransactionsPerBlock int64 `yaml:"MaxTransactionsPerBlock"` MaxTransactionsPerBlock int64 `yaml:"MaxTransactionsPerBlock"`
StandbyValidators []string `yaml:"StandbyValidators"` StandbyValidators []string `yaml:"StandbyValidators"`
SeedList []string `yaml:"SeedList"` SeedList []string `yaml:"SeedList"`

View file

@ -1,6 +1,8 @@
ProtocolConfiguration: ProtocolConfiguration:
Magic: 7630401 Magic: 7630401
AddressVersion: 23 AddressVersion: 23
SecondsPerBlock: 15
LowPriorityThreshold: 0.001
StandbyValidators: StandbyValidators:
- 03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c - 03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c
- 02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093 - 02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093

View file

@ -1,6 +1,8 @@
ProtocolConfiguration: ProtocolConfiguration:
Magic: 56753 Magic: 56753
AddressVersion: 23 AddressVersion: 23
SecondsPerBlock: 15
LowPriorityThreshold: 0.000
StandbyValidators: StandbyValidators:
- 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2
- 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e

View file

@ -1,6 +1,8 @@
ProtocolConfiguration: ProtocolConfiguration:
Magic: 1953787457 Magic: 1953787457
AddressVersion: 23 AddressVersion: 23
SecondsPerBlock: 15
LowPriorityThreshold: 0.000
StandbyValidators: StandbyValidators:
- 0327da12b5c40200e9f65569476bbff2218da4f32548ff43b6387ec1416a231ee8 - 0327da12b5c40200e9f65569476bbff2218da4f32548ff43b6387ec1416a231ee8
- 026ce35b29147ad09e4afe4ec4a7319095f08198fa8babbe3c56e970b143528d22 - 026ce35b29147ad09e4afe4ec4a7319095f08198fa8babbe3c56e970b143528d22

View file

@ -1,6 +1,8 @@
ProtocolConfiguration: ProtocolConfiguration:
Magic: 56753 Magic: 56753
AddressVersion: 23 AddressVersion: 23
SecondsPerBlock: 15
LowPriorityThreshold: 0.000
StandbyValidators: StandbyValidators:
- 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2
- 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e - 02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e

View file

@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"math"
"sync/atomic" "sync/atomic"
"time" "time"
@ -12,12 +13,12 @@ import (
"github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// tuning parameters // tuning parameters
const ( const (
secondsPerBlock = 15
headerBatchCount = 2000 headerBatchCount = 2000
version = "0.0.1" version = "0.0.1"
) )
@ -55,6 +56,8 @@ type Blockchain struct {
// Whether we will verify received blocks. // Whether we will verify received blocks.
verifyBlocks bool verifyBlocks bool
memPool MemPool
} }
type headersOpFunc func(headerList *HeaderHashList) type headersOpFunc func(headerList *HeaderHashList)
@ -69,6 +72,7 @@ func NewBlockchain(ctx context.Context, s storage.Store, cfg config.ProtocolConf
headersOpDone: make(chan struct{}), headersOpDone: make(chan struct{}),
blockCache: NewCache(), blockCache: NewCache(),
verifyBlocks: false, verifyBlocks: false,
memPool: NewMemPool(50000),
} }
go bc.run(ctx) go bc.run(ctx)
@ -457,6 +461,10 @@ func (bc *Blockchain) headerListLen() (n int) {
// GetTransaction returns a TX and its height by the given hash. // GetTransaction returns a TX and its height by the given hash.
func (bc *Blockchain) GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error) { 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()) key := storage.AppendPrefix(storage.DataTransaction, hash.BytesReverse())
b, err := bc.Get(key) b, err := bc.Get(key)
if err != nil { if err != nil {
@ -506,9 +514,17 @@ func (bc *Blockchain) GetHeader(hash util.Uint256) (*Header, error) {
return block.Header(), nil return block.Header(), nil
} }
// HasBlock return true if the blockchain contains he given // HasTransaction return true if the blockchain contains he given
// transaction hash. // transaction hash.
func (bc *Blockchain) HasTransaction(hash util.Uint256) bool { 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 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 // transaction package because of a import cycle problem. Perhaps we should think to re-design
// the code base to avoid this situation. // the code base to avoid this situation.
func (bc *Blockchain) References(t *transaction.Transaction) map[util.Uint256]*transaction.Output { 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() { for prevHash, inputs := range t.GroupInputsByPrevHash() {
if tx, _, err := bc.GetTransaction(prevHash); err != nil { 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) 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 { func hashAndIndexToBytes(h util.Uint256, index uint32) []byte {
buf := make([]byte, 4) buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, index) binary.LittleEndian.PutUint32(buf, index)
return append(h.BytesReverse(), buf...) return append(h.BytesReverse(), buf...)
} }
func (bc *Blockchain) secondsPerBlock() int {
return bc.config.SecondsPerBlock
}

View file

@ -25,7 +25,7 @@ type Blockchainer interface {
GetAccountState(util.Uint160) *AccountState GetAccountState(util.Uint160) *AccountState
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
References(t *transaction.Transaction) map[util.Uint256]*transaction.Output References(t *transaction.Transaction) map[util.Uint256]*transaction.Output
FeePerByte(t *transaction.Transaction) util.Fixed8 Feer // fee interface
SystemFee(t *transaction.Transaction) util.Fixed8 Verify(t *transaction.Transaction) error
NetworkFee(t *transaction.Transaction) util.Fixed8 GetMemPool() MemPool
} }

14
pkg/core/feer.go Normal file
View file

@ -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
}

267
pkg/core/mem_pool.go Normal file
View file

@ -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
}

View file

@ -36,6 +36,6 @@ func (in *Input) EncodeBinary(w io.Writer) error {
} }
// Size returns the size in bytes of the Input // 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 return in.PrevHash.Size() + 2 // 2 = sizeOf uint16
} }

View file

@ -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
}

View file

@ -10,6 +10,12 @@ import (
log "github.com/sirupsen/logrus" 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. // Transaction is a process recorded in the NEO blockchain.
type Transaction struct { type Transaction struct {
// The type of the transaction. // The type of the transaction.
@ -251,6 +257,15 @@ func (t *Transaction) GroupInputsByPrevHash() map[util.Uint256][]*Input {
return m 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 // Size returns the size of the transaction in term of bytes
func (t *Transaction) Size() int { func (t *Transaction) Size() int {
attrSize := util.GetVarSize(t.Attributes) attrSize := util.GetVarSize(t.Attributes)

View file

@ -55,3 +55,8 @@ func (w *Witness) MarshalJSON() ([]byte, error) {
func (w *Witness) Size() int { func (w *Witness) Size() int {
return util.GetVarSize(w.InvocationScript) + util.GetVarSize(w.VerificationScript) 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)
}

View file

@ -7,6 +7,7 @@ import (
"io" "io"
"github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/storage"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
) )
@ -91,3 +92,32 @@ func (s *UnspentCoinState) DecodeBinary(r io.Reader) error {
} }
return nil 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
}

View file

@ -77,6 +77,18 @@ func (chain testChain) GetTransaction(util.Uint256) (*transaction.Transaction, u
panic("TODO") 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{} type testDiscovery struct{}
func (d testDiscovery) BackFill(addrs ...string) {} func (d testDiscovery) BackFill(addrs ...string) {}

View file

@ -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
)

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/CityOfZion/neo-go/pkg/core" "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/network/payload"
"github.com/CityOfZion/neo-go/pkg/util" "github.com/CityOfZion/neo-go/pkg/util"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -310,3 +311,41 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error {
} }
return nil 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))
}

View file

@ -1,6 +1,7 @@
package rpc package rpc
import ( import (
"bytes"
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
@ -8,6 +9,7 @@ import (
"strconv" "strconv"
"github.com/CityOfZion/neo-go/pkg/core" "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/crypto"
"github.com/CityOfZion/neo-go/pkg/network" "github.com/CityOfZion/neo-go/pkg/network"
"github.com/CityOfZion/neo-go/pkg/rpc/result" "github.com/CityOfZion/neo-go/pkg/rpc/result"
@ -183,7 +185,7 @@ Methods:
results = peers 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" results = "TODO"
@ -227,12 +229,32 @@ Methods:
results = "Invalid public account address" results = "Invalid public account address"
} }
case "getrawtransaction": case "getrawtransaction":
results, resultsErr = s.getrawtransaction(reqParams)
case "sendrawtransaction":
results, resultsErr = s.sendrawtransaction(reqParams)
default:
resultsErr = NewMethodNotFoundError(fmt.Sprintf("Method '%s' not supported", req.Method), nil)
}
if resultsErr != nil {
req.WriteErrorResponse(w, resultsErr)
return
}
req.WriteResponse(w, results)
}
func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) {
var resultsErr error
var results interface{}
param0, err := reqParams.ValueWithType(0, "string") param0, err := reqParams.ValueWithType(0, "string")
if err != nil { if err != nil {
resultsErr = err resultsErr = err
} else if txHash, err := util.Uint256DecodeString(param0.StringVal); err != nil { } 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 = errInvalidParams
resultsErr = NewInvalidParamsError(err.Error(), err)
} else if tx, height, err := s.chain.GetTransaction(txHash); err != nil { } else if tx, height, err := s.chain.GetTransaction(txHash); err != nil {
err = errors.Wrapf(err, "Invalid transaction hash: %s", txHash) err = errors.Wrapf(err, "Invalid transaction hash: %s", txHash)
resultsErr = NewInvalidParamsError(err.Error(), err) resultsErr = NewInvalidParamsError(err.Error(), err)
@ -259,16 +281,49 @@ Methods:
results = hex.EncodeToString(tx.Bytes()) 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: default:
resultsErr = NewMethodNotFoundError(fmt.Sprintf("Method '%s' not supported", req.Method), nil) err = errors.New("unknown error")
}
if err != nil {
resultsErr = NewInternalServerError(err.Error(), err)
}
} }
if resultsErr != nil { return results, resultsErr
req.WriteErrorResponse(w, resultsErr)
return
}
req.WriteResponse(w, results)
} }
func (s Server) validBlockHeight(param *Param) bool { func (s Server) validBlockHeight(param *Param) bool {

View file

@ -116,7 +116,7 @@ var testRpcCases = []tc{
{ {
rpcCall: `{ "jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["45a41306c846ea80290416143e8e856559818065be3f4e143c60e43a", 1] }`, rpcCall: `{ "jsonrpc": "2.0", "id": 1, "method": "getrawtransaction", "params": ["45a41306c846ea80290416143e8e856559818065be3f4e143c60e43a", 1] }`,
method: "getrawtransaction_4", 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 // Good case, valid transaction
@ -216,6 +216,27 @@ var testRpcCases = []tc{
method: "validateaddress_4", method: "validateaddress_4",
expectedResult: `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid Params"},"id":1}`, 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) { func TestHandler(t *testing.T) {

View file

@ -128,3 +128,26 @@ func (f Fixed8) Add(g Fixed8) Fixed8 {
func (f Fixed8) Sub(g Fixed8) Fixed8 { func (f Fixed8) Sub(g Fixed8) Fixed8 {
return NewFixed8(f.Value() - g.Value()) 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())
}

View file

@ -65,6 +65,7 @@ func GetVarSize(value interface{}) int {
valueLength := v.Len() valueLength := v.Len()
valueSize := 0 valueSize := 0
if valueLength != 0 {
switch reflect.ValueOf(value).Index(0).Interface().(type) { switch reflect.ValueOf(value).Index(0).Interface().(type) {
case io.Serializable: case io.Serializable:
for i := 0; i < valueLength; i++ { for i := 0; i < valueLength; i++ {
@ -80,6 +81,7 @@ func GetVarSize(value interface{}) int {
case uint64, int64: case uint64, int64:
valueSize = valueLength * 8 valueSize = valueLength * 8
} }
}
return GetVarIntSize(valueLength) + valueSize return GetVarIntSize(valueLength) + valueSize
default: default:

View file

@ -1,6 +1,7 @@
package util package util
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -74,3 +75,9 @@ func (u Uint256) Size() int {
func (u Uint256) MarshalJSON() ([]byte, error) { func (u Uint256) MarshalJSON() ([]byte, error) {
return []byte(`"0x` + u.String() + `"`), nil 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[:]) }