diff --git a/common/types.go b/common/types.go index c52855981c79..6dd51e2cda59 100644 --- a/common/types.go +++ b/common/types.go @@ -28,19 +28,20 @@ import ( ) const ( - HashLength = 32 - AddressLength = 20 - BlockSigners = "0x0000000000000000000000000000000000000089" - MasternodeVotingSMC = "0x0000000000000000000000000000000000000088" - RandomizeSMC = "0x0000000000000000000000000000000000000090" - FoudationAddr = "0x0000000000000000000000000000000000000068" - TeamAddr = "0x0000000000000000000000000000000000000099" - TomoXAddr = "0x0000000000000000000000000000000000000091" - VoteMethod = "0x6dd7d8ea" - UnvoteMethod = "0x02aa9be2" - ProposeMethod = "0x01267951" - ResignMethod = "0xae6e43f5" - SignMethod = "0xe341eaa4" + HashLength = 32 + AddressLength = 20 + BlockSigners = "0x0000000000000000000000000000000000000089" + MasternodeVotingSMC = "0x0000000000000000000000000000000000000088" + RandomizeSMC = "0x0000000000000000000000000000000000000090" + FoudationAddr = "0x0000000000000000000000000000000000000068" + TeamAddr = "0x0000000000000000000000000000000000000099" + TomoXAddr = "0x0000000000000000000000000000000000000091" + RelayerRegistrationSMC = "0x00000000000000000000000000000000000000xx" + VoteMethod = "0x6dd7d8ea" + UnvoteMethod = "0x02aa9be2" + ProposeMethod = "0x01267951" + ResignMethod = "0xae6e43f5" + SignMethod = "0xe341eaa4" ) var ( diff --git a/core/block_validator.go b/core/block_validator.go index efe084b87076..d6e2ed857d6c 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -55,81 +55,58 @@ func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engin // ValidateBody validates the given block's uncles and verifies the the block // header's transaction and uncle roots. The headers are assumed to be already // validated at this point. -func (v *BlockValidator) ValidateBody(block *types.Block) error { +func (v *BlockValidator) ValidateBody(block *types.Block) (error, []common.Hash) { // Check whether the block's known, and if not, that it's linkable if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { - return ErrKnownBlock + return ErrKnownBlock, []common.Hash{} } if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { - return consensus.ErrUnknownAncestor + return consensus.ErrUnknownAncestor, []common.Hash{} } - return consensus.ErrPrunedAncestor + return consensus.ErrPrunedAncestor, []common.Hash{} } // Header validity is known at this point, check the uncles and transactions header := block.Header() if err := v.engine.VerifyUncles(v.bc, block); err != nil { - return err + return err, []common.Hash{} } if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash { - return fmt.Errorf("uncle root hash mismatch: have %x, want %x", hash, header.UncleHash) + return fmt.Errorf("uncle root hash mismatch: have %x, want %x", hash, header.UncleHash), []common.Hash{} } if hash := types.DeriveSha(block.Transactions()); hash != header.TxHash { - return fmt.Errorf("transaction root hash mismatch: have %x, want %x", hash, header.TxHash) + return fmt.Errorf("transaction root hash mismatch: have %x, want %x", hash, header.TxHash), []common.Hash{} } engine, _ := v.engine.(*posv.Posv) tomoXService := engine.GetTomoXService() + currentState, err := v.bc.State() + if err != nil { + return err, []common.Hash{} + } + + // validate matchedOrder txs + processedHashes := []common.Hash{} + // clear the previous dry-run cache + if tomoXService != nil { + tomoXService.GetDB().InitDryRunMode() + } for _, tx := range block.Transactions() { if tx.IsMatchingTransaction() { if tomoXService == nil { log.Error("tomox not found") - return tomox.ErrTomoXServiceNotFound + return tomox.ErrTomoXServiceNotFound, []common.Hash{} } log.Debug("process tx match") - txMatch := &tomox.TxDataMatch{} - if err := json.Unmarshal(tx.Data(), txMatch); err != nil { - return fmt.Errorf("transaction match is corrupted. Failed unmarshal. Error: ", err) - } - log.Debug("tx unmarshal", "txMatch", txMatch, "tx.Data()", tx.Data()) - order, err := txMatch.DecodeOrder() + hash, err := v.validateMatchedOrder(tomoXService, currentState, tx) if err != nil { - return fmt.Errorf("transaction match is corrupted. Failed decode order. Error: ", err) - } - trades := txMatch.GetTrades() - log.Debug("Got trades", "number", len(trades), "trades", trades) - for _, trade := range trades { - tradeSDK := &sdktypes.Trade{} - if q, ok := trade["quantity"]; ok { - tradeSDK.Amount = new(big.Int) - tradeSDK.Amount.SetString(q, 10) - } - tradeSDK.PricePoint = order.Price - tradeSDK.PairName = order.PairName - tradeSDK.BaseToken = order.BaseToken - tradeSDK.QuoteToken = order.QuoteToken - tradeSDK.Status = sdktypes.TradeStatusSuccess - tradeSDK.Maker = order.UserAddress - tradeSDK.MakerOrderHash = order.Hash - if u, ok := trade["uAddr"]; ok { - tradeSDK.Taker = common.Address{} - tradeSDK.Taker.SetString(u) - } - tradeSDK.TakerOrderHash = order.Hash //FIXME: will update txMatch to include TakerOrderHash = headOrder.Item.Hash - tradeSDK.TxHash = tx.Hash() - tradeSDK.Hash = tradeSDK.ComputeHash() - log.Debug("TRADE history", "order", order, "trade", tradeSDK) - // put tradeSDK to mongodb on SDK node - if tomoXService.IsSDKNode() { - db := tomoXService.GetDB() - return db.Put(tomox.EmptyKey(), tradeSDK, true) - } + return err, []common.Hash{} } + processedHashes = append(processedHashes, hash) } } - - return nil + return nil, processedHashes } // ValidateState validates the various changes that happen after a state @@ -160,6 +137,53 @@ func (v *BlockValidator) ValidateState(block, parent *types.Block, statedb *stat return nil } +// return values +// hash: for removing from pending list and add to processed list +// orderbook according to the order +func (v *BlockValidator) validateMatchedOrder(tomoXService *tomox.TomoX, currentState *state.StateDB, tx *types.Transaction) (common.Hash, error) { + txMatch := &tomox.TxDataMatch{} + if err := json.Unmarshal(tx.Data(), txMatch); err != nil { + return common.Hash{}, fmt.Errorf("transaction match is corrupted. Failed unmarshal. Error: %s", err.Error()) + } + + // verify orderItem + order, err := txMatch.DecodeOrder() + if err != nil { + return common.Hash{},fmt.Errorf("transaction match is corrupted. Failed decode order. Error: %s ", err.Error()) + } + if err := order.VerifyMatchedOrder(currentState); err != nil { + return common.Hash{},err + } + + ob, err := tomoXService.GetOrderBook(order.PairName, true) + // if orderbook of this pairName has been updated by previous tx in this block, use it + + if err != nil { + return common.Hash{}, err + } + + // verify old state: orderbook hash, bidTree hash, askTree hash + if err := txMatch.VerifyOldTomoXState(ob); err != nil { + return common.Hash{}, err + } + + // process Matching Engine + if _, _, err := ob.ProcessOrder(order, true, true); err != nil { + return common.Hash{}, err + } + + // verify new state + if err := txMatch.VerifyNewTomoXState(ob); err != nil { + return common.Hash{}, err + } + + trades := txMatch.GetTrades() + if err := logTrades(tomoXService, tx.Hash(), order, trades); err != nil { + return common.Hash{}, err + } + return order.Hash, nil +} + // CalcGasLimit computes the gas limit of the next block after parent. // This is miner strategy, not consensus protocol. func CalcGasLimit(parent *types.Block) uint64 { @@ -190,3 +214,37 @@ func CalcGasLimit(parent *types.Block) uint64 { } return limit } + +func logTrades(tomoXService *tomox.TomoX, txHash common.Hash, order *tomox.OrderItem, trades []map[string]string) error { + log.Debug("Got trades", "number", len(trades), "trades", trades) + for _, trade := range trades { + tradeSDK := &sdktypes.Trade{} + if q, ok := trade["quantity"]; ok { + tradeSDK.Amount = new(big.Int) + tradeSDK.Amount.SetString(q, 10) + } + tradeSDK.PricePoint = order.Price + tradeSDK.PairName = order.PairName + tradeSDK.BaseToken = order.BaseToken + tradeSDK.QuoteToken = order.QuoteToken + tradeSDK.Status = sdktypes.TradeStatusSuccess + tradeSDK.Maker = order.UserAddress + tradeSDK.MakerOrderHash = order.Hash + if u, ok := trade["uAddr"]; ok { + tradeSDK.Taker = common.Address{} + tradeSDK.Taker.SetString(u) + } + tradeSDK.TakerOrderHash = order.Hash //FIXME: will update txMatch to include TakerOrderHash = headOrder.Item.Hash + tradeSDK.TxHash = txHash + tradeSDK.Hash = tradeSDK.ComputeHash() + log.Debug("TRADE history", "order", order, "trade", tradeSDK) + // put tradeSDK to mongodb on SDK node + if tomoXService.IsSDKNode() { + db := tomoXService.GetDB() + if err := db.Put(tomox.EmptyKey(), tradeSDK, true); err != nil { + return fmt.Errorf("failed to store tradeSDK %s", err.Error()) + } + } + } + return nil +} diff --git a/core/blockchain.go b/core/blockchain.go index 6b4b6a9faf3e..96b21d83bc8c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -20,7 +20,6 @@ package core import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/tomox" "io" "math/big" "os" @@ -123,16 +122,17 @@ type BlockChain struct { currentBlock atomic.Value // Current head of the block chain currentFastBlock atomic.Value // Current head of the fast-sync chain (may be above the block chain!) - stateCache state.Database // State database to reuse between imports (contains state cache) - bodyCache *lru.Cache // Cache for the most recent block bodies - bodyRLPCache *lru.Cache // Cache for the most recent block bodies in RLP encoded format - blockCache *lru.Cache // Cache for the most recent entire blocks - futureBlocks *lru.Cache // future blocks are blocks added for later processing - resultProcess *lru.Cache // Cache for processed blocks - calculatingBlock *lru.Cache // Cache for processing blocks - downloadingBlock *lru.Cache // Cache for downloading blocks (avoid duplication from fetcher) - quit chan struct{} // blockchain quit channel - running int32 // running must be called atomically + stateCache state.Database // State database to reuse between imports (contains state cache) + bodyCache *lru.Cache // Cache for the most recent block bodies + bodyRLPCache *lru.Cache // Cache for the most recent block bodies in RLP encoded format + blockCache *lru.Cache // Cache for the most recent entire blocks + futureBlocks *lru.Cache // future blocks are blocks added for later processing + resultProcess *lru.Cache // Cache for processed blocks + matchedOrderHashes *lru.Cache // Cache for matchedOrderHashes + calculatingBlock *lru.Cache // Cache for processing blocks + downloadingBlock *lru.Cache // Cache for downloading blocks (avoid duplication from fetcher) + quit chan struct{} // blockchain quit channel + running int32 // running must be called atomically // procInterrupt must be atomically called procInterrupt int32 // interrupt signaler for block processing wg sync.WaitGroup // chain processing wait group for shutting down @@ -163,25 +163,27 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par futureBlocks, _ := lru.New(maxFutureBlocks) badBlocks, _ := lru.New(badBlockLimit) resultProcess, _ := lru.New(blockCacheLimit) + matchedOrderHashes, _ := lru.New(blockCacheLimit) preparingBlock, _ := lru.New(blockCacheLimit) downloadingBlock, _ := lru.New(blockCacheLimit) bc := &BlockChain{ - chainConfig: chainConfig, - cacheConfig: cacheConfig, - db: db, - triegc: prque.New(), - stateCache: state.NewDatabase(db), - quit: make(chan struct{}), - bodyCache: bodyCache, - bodyRLPCache: bodyRLPCache, - blockCache: blockCache, - futureBlocks: futureBlocks, - resultProcess: resultProcess, - calculatingBlock: preparingBlock, - downloadingBlock: downloadingBlock, - engine: engine, - vmConfig: vmConfig, - badBlocks: badBlocks, + chainConfig: chainConfig, + cacheConfig: cacheConfig, + db: db, + triegc: prque.New(), + stateCache: state.NewDatabase(db), + quit: make(chan struct{}), + bodyCache: bodyCache, + bodyRLPCache: bodyRLPCache, + blockCache: blockCache, + futureBlocks: futureBlocks, + resultProcess: resultProcess, + matchedOrderHashes: matchedOrderHashes, + calculatingBlock: preparingBlock, + downloadingBlock: downloadingBlock, + engine: engine, + vmConfig: vmConfig, + badBlocks: badBlocks, } bc.SetValidator(NewBlockValidator(chainConfig, bc, engine)) bc.SetProcessor(NewStateProcessor(chainConfig, bc, engine)) @@ -1045,6 +1047,9 @@ func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) { // only reason this method exists as a separate one is to make locking cleaner // with deferred statements. func (bc *BlockChain) insertChain(chain types.Blocks) (int, []interface{}, []*types.Log, error) { + engine := bc.Engine().(*posv.Posv) + tomoXService := engine.GetTomoXService() + // Do a sanity check that the provided chain is actually ordered and linked for i := 1; i < len(chain); i++ { if chain[i].NumberU64() != chain[i-1].NumberU64()+1 || chain[i].ParentHash() != chain[i-1].Hash() { @@ -1100,8 +1105,9 @@ func (bc *BlockChain) insertChain(chain types.Blocks) (int, []interface{}, []*ty bstart := time.Now() err := <-results + matchedOrderHashes := []common.Hash{} if err == nil { - err = bc.Validator().ValidateBody(block) + err, matchedOrderHashes = bc.Validator().ValidateBody(block) } switch { case err == ErrKnownBlock: @@ -1191,9 +1197,18 @@ func (bc *BlockChain) insertChain(chain types.Blocks) (int, []interface{}, []*ty return i, events, coalescedLogs, err } - if bc.CurrentHeader().Number.Uint64()%common.TomoXSnapshotInterval == 0 { - if err := bc.snapshotTomoX(); err != nil { - log.Error("Failed to snapshot tomox", "err", err) + if tomoXService != nil { + if len(matchedOrderHashes) > 0 { + log.Debug("Applying TxMatches of block", "number", block.NumberU64(), "matchedOrderHashes", matchedOrderHashes) + if err = tomoXService.ApplyTxMatches(matchedOrderHashes); err != nil { + return i, events, coalescedLogs, err + } + } + + if bc.CurrentHeader().Number.Uint64()%common.TomoXSnapshotInterval == 0 { + if err := tomoXService.Snapshot(block.Hash()); err != nil { + log.Error("Failed to snapshot tomox", "err", err) + } } } @@ -1269,9 +1284,10 @@ func (bc *BlockChain) PrepareBlock(block *types.Block) (err error) { if err != nil { return err } - result, err := bc.getResultBlock(block, false) + result, matchedOrderHashes, err := bc.getResultBlock(block, false) if err == nil { bc.resultProcess.Add(block.Hash(), result) + bc.matchedOrderHashes.Add(block.Hash(), matchedOrderHashes) return nil } else if err == ErrKnownBlock { return nil @@ -1282,12 +1298,17 @@ func (bc *BlockChain) PrepareBlock(block *types.Block) (err error) { return err } -func (bc *BlockChain) getResultBlock(block *types.Block, verifiedM2 bool) (*ResultProcessBlock, error) { +func (bc *BlockChain) getResultBlock(block *types.Block, verifiedM2 bool) (*ResultProcessBlock, []common.Hash, error) { var calculatedBlock *CalculatedBlock if verifiedM2 { if result, check := bc.resultProcess.Get(block.HashNoValidator()); check { log.Debug("Get result block from cache ", "number", block.NumberU64(), "hash", block.Hash(), "hash no validator", block.HashNoValidator()) - return result.(*ResultProcessBlock), nil + matchedOrderHashes := []common.Hash{} + val, ok := bc.matchedOrderHashes.Get(block.HashNoValidator()) + if ok && val != nil { + matchedOrderHashes = val.([]common.Hash) + } + return result.(*ResultProcessBlock), matchedOrderHashes, nil } log.Debug("Not found cache prepare block ", "number", block.NumberU64(), "hash", block.Hash(), "validator", block.HashNoValidator()) if calculatedBlock, _ := bc.calculatingBlock.Get(block.HashNoValidator()); calculatedBlock != nil { @@ -1300,22 +1321,22 @@ func (bc *BlockChain) getResultBlock(block *types.Block, verifiedM2 bool) (*Resu // If the chain is terminating, stop processing blocks if atomic.LoadInt32(&bc.procInterrupt) == 1 { log.Debug("Premature abort during blocks processing") - return nil, ErrBlacklistedHash + return nil, []common.Hash{}, ErrBlacklistedHash } // If the header is a banned one, straight out abort if BadHashes[block.Hash()] { bc.reportBlock(block, nil, ErrBlacklistedHash) - return nil, ErrBlacklistedHash + return nil, []common.Hash{}, ErrBlacklistedHash } // Wait for the block's verification to complete bstart := time.Now() - err := bc.Validator().ValidateBody(block) + err, matchedOrderHashes := bc.Validator().ValidateBody(block) switch { case err == ErrKnownBlock: // Block and state both already known. However if the current block is below // this number we did a rollback and we should reimport it nonetheless. if bc.CurrentBlock().NumberU64() >= block.NumberU64() { - return nil, ErrKnownBlock + return nil, []common.Hash{}, ErrKnownBlock } case err == consensus.ErrPrunedAncestor: // Block competing with the canonical chain, store in the db, but don't process @@ -1324,7 +1345,7 @@ func (bc *BlockChain) getResultBlock(block *types.Block, verifiedM2 bool) (*Resu localTd := bc.GetTd(currentBlock.Hash(), currentBlock.NumberU64()) externTd := new(big.Int).Add(bc.GetTd(block.ParentHash(), block.NumberU64()-1), block.Difficulty()) if localTd.Cmp(externTd) > 0 { - return nil, err + return nil, []common.Hash{}, err } // Competitor chain beat canonical, gather all blocks from the common ancestor var winner []*types.Block @@ -1341,18 +1362,18 @@ func (bc *BlockChain) getResultBlock(block *types.Block, verifiedM2 bool) (*Resu // Import all the pruned blocks to make the state available _, _, _, err := bc.insertChain(winner) if err != nil { - return nil, err + return nil, []common.Hash{}, err } case err != nil: bc.reportBlock(block, nil, err) - return nil, err + return nil, []common.Hash{}, err } // Create a new statedb using the parent block and report an // error if it fails. var parent = bc.GetBlock(block.ParentHash(), block.NumberU64()-1) state, err := state.New(parent.Root(), bc.stateCache) if err != nil { - return nil, err + return nil, []common.Hash{}, err } // Process block using the parent state as reference point. receipts, logs, usedGas, err := bc.processor.ProcessBlockNoValidator(calculatedBlock, state, bc.vmConfig) @@ -1361,18 +1382,18 @@ func (bc *BlockChain) getResultBlock(block *types.Block, verifiedM2 bool) (*Resu if err != ErrStopPreparingBlock { bc.reportBlock(block, receipts, err) } - return nil, err + return nil, []common.Hash{}, err } // Validate the state using the default validator err = bc.Validator().ValidateState(block, parent, state, receipts, usedGas) if err != nil { bc.reportBlock(block, receipts, err) - return nil, err + return nil, []common.Hash{}, err } proctime := time.Since(bstart) log.Debug("Calculate new block", "number", block.Number(), "hash", block.Hash(), "uncles", len(block.Uncles()), "txs", len(block.Transactions()), "gas", block.GasUsed(), "elapsed", common.PrettyDuration(time.Since(bstart)), "process", process) - return &ResultProcessBlock{receipts: receipts, logs: logs, state: state, proctime: proctime, usedGas: usedGas}, nil + return &ResultProcessBlock{receipts: receipts, logs: logs, state: state, proctime: proctime, usedGas: usedGas}, matchedOrderHashes, nil } // insertChain will execute the actual chain insertion and event aggregation. The @@ -1388,7 +1409,7 @@ func (bc *BlockChain) insertBlock(block *types.Block) ([]interface{}, []*types.L log.Debug("Stop fetcher a block because downloading", "number", block.NumberU64(), "hash", block.Hash()) return events, coalescedLogs, nil } - result, err := bc.getResultBlock(block, true) + result, matchedOrderHashes, err := bc.getResultBlock(block, true) if err != nil { return events, coalescedLogs, err } @@ -1401,13 +1422,22 @@ func (bc *BlockChain) insertBlock(block *types.Block) ([]interface{}, []*types.L if bc.HasBlockAndState(block.Hash(), block.NumberU64()) { return events, coalescedLogs, nil } + engine := bc.Engine().(*posv.Posv) + tomoXService := engine.GetTomoXService() + if tomoXService != nil { + if len(matchedOrderHashes) > 0 { + log.Debug("Applying TxMatches of block", "number", block.NumberU64(), "matchedOrderHashes", matchedOrderHashes) + if err = tomoXService.ApplyTxMatches(matchedOrderHashes); err != nil { + return events, coalescedLogs, err + } + } - if bc.CurrentHeader().Number.Uint64()%common.TomoXSnapshotInterval == 0 { - if err := bc.snapshotTomoX(); err != nil { - log.Error("Failed to snapshot tomox", "err", err) + if bc.CurrentHeader().Number.Uint64()%common.TomoXSnapshotInterval == 0 { + if err := tomoXService.Snapshot(block.Hash()); err != nil { + log.Error("Failed to snapshot tomox", "err", err) + } } } - status, err := bc.WriteBlockWithState(block, result.receipts, result.state) if err != nil { @@ -1898,21 +1928,3 @@ func (bc *BlockChain) UpdateM1() error { return nil } -func (bc *BlockChain) snapshotTomoX() error { - var ( - tomoX *tomox.TomoX - engine *posv.Posv - ) - - if bc.chainConfig.Posv == nil { - return tomox.ErrUnsupportedEngine - } - engine = bc.Engine().(*posv.Posv) - if tomoX = engine.GetTomoXService(); tomoX == nil { - return tomox.ErrTomoXServiceNotFound - } - if err := tomoX.Snapshot(bc.CurrentHeader().Hash()); err != nil { - return err - } - return nil -} diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 8ec3c3df394e..f503e448badb 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -105,7 +105,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { // Try and process the block err := blockchain.engine.VerifyHeader(blockchain, block.Header(), true) if err == nil { - err = blockchain.validator.ValidateBody(block) + err, _ = blockchain.validator.ValidateBody(block) } if err != nil { if err == ErrKnownBlock { diff --git a/core/types.go b/core/types.go index 3f691cb4203e..b10a650c0b5d 100644 --- a/core/types.go +++ b/core/types.go @@ -17,6 +17,7 @@ package core import ( + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -28,7 +29,7 @@ import ( // type Validator interface { // ValidateBody validates the given block's content. - ValidateBody(block *types.Block) error + ValidateBody(block *types.Block) (error, []common.Hash) // ValidateState validates the given statedb and optionally the receipts and // gas used. diff --git a/core/types/transaction.go b/core/types/transaction.go index 31bc32c0db3c..40ce1c0609da 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -287,6 +287,18 @@ func (tx *Transaction) IsSpecialTransaction() bool { return tx.To().String() == common.RandomizeSMC || tx.To().String() == common.BlockSigners || tx.To().String() == common.TomoXAddr } +func (tx *Transaction) IsMatchingTransaction() bool { + if tx.To() == nil { + return false + } + + if tx.To().String() != common.TomoXAddr { + return false + } + + return true +} + func (tx *Transaction) IsSigningTransaction() bool { if tx.To() == nil { return false @@ -347,20 +359,6 @@ func (tx *Transaction) IsVotingTransaction() (bool, *common.Address) { return b, nil } -func (tx *Transaction) IsMatchingTransaction() bool { - if tx.To() == nil { - return false - } - - if tx.To().String() != common.HexToAddress(common.TomoXAddr).String() { - return false - } - if tx.Data() == nil { - return false - } - return true -} - func (tx *Transaction) String() string { var from, to string if tx.data.V != nil { diff --git a/tomox/common.go b/tomox/common.go index 0b7df958db41..216f69612f97 100644 --- a/tomox/common.go +++ b/tomox/common.go @@ -22,6 +22,7 @@ var ( // errors ErrUnsupportedEngine = errors.New("only POSV supports matching orders") ErrTomoXServiceNotFound = errors.New("can't attach tomoX service") + ErrInvalidDryRunResult = errors.New("failed to apply txMatches, invalid dryRun result") ) // use alloc to prevent reference manipulation diff --git a/tomox/tomox.go b/tomox/tomox.go index 987c6ca9a822..a705f8dfb61c 100644 --- a/tomox/tomox.go +++ b/tomox/tomox.go @@ -1064,6 +1064,25 @@ func (tomox *TomoX) getProcessedOrderHash() []common.Hash { return processedHashes } +func (tomox *TomoX) MarkOrderAsProcessed(hash common.Hash) error { + + if err := tomox.addProcessedOrderHash(hash, 1000); err != nil { + log.Error("Fail to save processed order hash", "err", err) + return err + } + + // Remove order from db pending. + if err := tomox.removePendingHash(hash); err != nil { + log.Error("Fail to remove pending hash", "err", err) + return err + } + if err := tomox.removeOrderPending(hash); err != nil { + log.Error("Fail to remove order pending", "err", err) + return err + } + return nil +} + func (tomox *TomoX) updatePairs(pairs map[string]bool) error { blob, err := json.Marshal(pairs) if err != nil { @@ -1186,3 +1205,19 @@ func (tomox *TomoX) loadSnapshot(hash common.Hash) error { } return nil } + +// save orderbook after matching orders +// update order pending list, processed list +func (tomox *TomoX) ApplyTxMatches(orderHashes []common.Hash) error { + if err := tomox.db.SaveDryRunResult(); err != nil { + log.Error("Failed to save dry-run result") + return err + } + for _, hash := range orderHashes { + if err := tomox.MarkOrderAsProcessed(hash); err != nil { + log.Error("Failed to mark order as processed", "err", err) + } + } + tomox.db.InitDryRunMode() + return nil +} \ No newline at end of file diff --git a/tomox/validator.go b/tomox/validator.go index e2cc5ce8e50b..55977a36806d 100644 --- a/tomox/validator.go +++ b/tomox/validator.go @@ -1,10 +1,262 @@ package tomox -import "github.com/ethereum/go-ethereum/log" +import ( + "errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/sha3" + "github.com/ethereum/go-ethereum/log" + "math/big" + "time" +) + +const ( + // following: https://github.com/tomochain/tomorelayer/blob/master/contracts/Registration.sol#L21 + RelayerListSlot = uint64(3) + TokenBalanceSlot = uint64(0) +) + +var ( + // errors + errFutureOrder = errors.New("verify matched order: future order") + errNoTimestamp = errors.New("verify matched order: no timestamp") + errWrongHash = errors.New("verify matched order: wrong hash") + errInvalidSignature = errors.New("verify matched order: invalid signature") + errNotEnoughBalance = errors.New("verify matched order: not enough balance") + errInvalidPrice = errors.New("verify matched order: invalid price") + errInvalidQuantity = errors.New("verify matched order: invalid quantity") + errInvalidRelayer = errors.New("verify matched order: invalid relayer") + errInvalidOrderType = errors.New("verify matched order: unsupported order type") + errInvalidOrderSide = errors.New("verify matched order: invalid order side") + errOrderBookHashNotMatch = errors.New("verify matched order: orderbook hash not match") + errOrderTreeHashNotMatch = errors.New("verify matched order: ordertree hash not match") + + // supported order types + MatchingOrderType = map[string]bool{ + Market: true, + Limit: true, + } +) + +// verify orderItem +func (o *OrderItem) VerifyMatchedOrder(state *state.StateDB) error { + if err := o.verifyTimestamp(); err != nil { + return err + } + if err := o.verifyOrderSide(); err != nil { + return err + } + if err := o.verifyOrderType(); err != nil { + return err + } + // TODO: for testing without relayer, ignore the rest + // TODO: remove it + return nil + + if err := o.verifyRelayer(state); err != nil { + return err + } + if err := o.verifyBalance(state); err != nil { + return err + } + if err := o.verifySignature(); err != nil { + return err + } + return nil +} + +// verify token balance make sure that user has enough balance +func (o *OrderItem) verifyBalance(state *state.StateDB) error { + orderValueByQuoteToken := Zero() + balance := Zero() + tokenAddr := common.Address{} + + if o.Price == nil || o.Price.Cmp(Zero()) <= 0 { + return errInvalidPrice + } + if o.Quantity == nil || o.Quantity.Cmp(Zero()) <= 0 { + return errInvalidQuantity + } + if o.Side == Bid { + tokenAddr = o.QuoteToken + orderValueByQuoteToken = orderValueByQuoteToken.Mul(o.Quantity, o.Price) + } else { + tokenAddr = o.BaseToken + orderValueByQuoteToken = o.Quantity + } + if tokenAddr == (common.Address{}) { + // native TOMO + balance = state.GetBalance(o.UserAddress) + } else { + // query balance from tokenContract + balance = GetTokenBalance(state, o.UserAddress, tokenAddr) + } + if balance.Cmp(orderValueByQuoteToken) < 0 { + return errNotEnoughBalance + } + return nil +} + +// verify whether the exchange applies to become relayer +func (o *OrderItem) verifyRelayer(state *state.StateDB) error { + if !IsValidRelayer(state, o.ExchangeAddress) { + return errInvalidRelayer + } + return nil +} + +// following: https://github.com/tomochain/tomox-sdk/blob/master/types/order.go#L125 +func (o *OrderItem) computeHash() common.Hash { + sha := sha3.NewKeccak256() + sha.Write(o.ExchangeAddress.Bytes()) + sha.Write(o.UserAddress.Bytes()) + sha.Write(o.BaseToken.Bytes()) + sha.Write(o.QuoteToken.Bytes()) + sha.Write(common.BigToHash(o.Quantity).Bytes()) + sha.Write(common.BigToHash(o.Price).Bytes()) + sha.Write(common.BigToHash(o.encodedSide()).Bytes()) + sha.Write(common.BigToHash(o.Nonce).Bytes()) + sha.Write(common.BigToHash(o.MakeFee).Bytes()) + sha.Write(common.BigToHash(o.TakeFee).Bytes()) + return common.BytesToHash(sha.Sum(nil)) +} + +//verify signatures +func (o *OrderItem) verifySignature() error { + var ( + hash common.Hash + err error + signatureBytes []byte + ) + hash = o.computeHash() + if hash != o.Hash { + return errWrongHash + } + signatureBytes = append(signatureBytes, o.Signature.R.Bytes()...) + signatureBytes = append(signatureBytes, o.Signature.S.Bytes()...) + signatureBytes = append(signatureBytes, o.Signature.V-27) + pubkey, err := crypto.Ecrecover(hash.Bytes(), signatureBytes) + if err != nil { + return err + } + var userAddress common.Address + copy(userAddress[:], crypto.Keccak256(pubkey[1:])[12:]) + if userAddress != o.UserAddress { + return errInvalidSignature + } + return nil +} + +// verify order type +func (o *OrderItem) verifyOrderType() error { + if _, ok := MatchingOrderType[o.Type]; !ok { + return errInvalidOrderType + } + return nil +} + +//verify order side +func (o *OrderItem) verifyOrderSide() error { + + if o.Side != Bid && o.Side != Ask { + return errInvalidOrderSide + } + return nil +} + +//verify timestamp +func (o *OrderItem) verifyTimestamp() error { + // check timestamp of buyOrder + if o.CreatedAt == 0 || o.UpdatedAt == 0 { + return errNoTimestamp + } + if o.CreatedAt > uint64(time.Now().Unix()) || o.UpdatedAt > uint64(time.Now().Unix()) { + return errFutureOrder + } + return nil +} + +func (o *OrderItem) encodedSide() *big.Int { + if o.Side == Bid { + return big.NewInt(0) + } + return big.NewInt(1) +} + +func IsValidRelayer(statedb *state.StateDB, address common.Address) bool { + slotHash := common.BigToHash(new(big.Int).SetUint64(RelayerListSlot)) + retByte := crypto.Keccak256(address.Bytes(), slotHash.Bytes()) + locRelayerState := new(big.Int) + locRelayerState.SetBytes(retByte) + + ret := statedb.GetState(common.StringToAddress(common.RelayerRegistrationSMC), common.BigToHash(locRelayerState)) + if ret.Big().Cmp(new(big.Int).SetUint64(uint64(0))) > 0 { + return true + } + return false +} + +func GetTokenBalance(statedb *state.StateDB, address common.Address, contractAddr common.Address) *big.Int { + slotHash := common.BigToHash(new(big.Int).SetUint64(TokenBalanceSlot)) + retByte := crypto.Keccak256(address.Bytes(), slotHash.Bytes()) + locBalance := new(big.Int) + locBalance.SetBytes(retByte) + + ret := statedb.GetState(contractAddr, common.BigToHash(locBalance)) + return ret.Big() +} + +// verify orderbook, orderTrees before running matching engine +func (tx *TxDataMatch) VerifyOldTomoXState(ob *OrderBook) error { + // verify orderbook + if hash, err := ob.Hash(); err != nil || hash != tx.ObOld { + log.Error("wrong old orderbook", "expected", tx.ObOld, "actual", hash) + return errOrderBookHashNotMatch + } + + // verify order trees + // bidTree tree + bidTree := ob.Bids + if hash, err := bidTree.Hash(); err != nil || hash != tx.BidOld { + log.Error("wrong old bid tree", "expected", tx.BidOld, "actual", hash) + return errOrderTreeHashNotMatch + } + // askTree tree + askTree := ob.Asks + if hash, err := askTree.Hash(); err != nil || hash != tx.AskOld { + log.Error("wrong old ask tree", "expected", tx.AskOld, "actual", hash) + return errOrderTreeHashNotMatch + } + return nil +} + +// verify orderbook, orderTrees after running matching engine +func (tx *TxDataMatch) VerifyNewTomoXState(ob *OrderBook) error { + // verify orderbook + if hash, err := ob.Hash(); err != nil || hash != tx.ObNew { + log.Error("wrong new orderbook", "expected", tx.ObNew, "actual", hash) + return errOrderBookHashNotMatch + } + + // verify order trees + // bidTree tree + bidTree := ob.Bids + if hash, err := bidTree.Hash(); err != nil || hash != tx.BidNew { + log.Error("wrong new bid tree", "expected", tx.BidNew, "actual", hash) + return errOrderTreeHashNotMatch + } + // askTree tree + askTree := ob.Asks + if hash, err := askTree.Hash(); err != nil || hash != tx.AskNew { + log.Error("wrong new ask tree", "expected", tx.AskNew, "actual", hash) + return errOrderTreeHashNotMatch + } + return nil +} func (tx *TxDataMatch) DecodeOrder() (*OrderItem, error) { order := &OrderItem{} - log.Debug("tx.order", "tx.order", tx.Order) if err := DecodeBytesItem(tx.Order, order); err != nil { return order, err } diff --git a/tomox/validator_test.go b/tomox/validator_test.go new file mode 100644 index 000000000000..104601a697fe --- /dev/null +++ b/tomox/validator_test.go @@ -0,0 +1,242 @@ +package tomox + +import ( + "crypto/ecdsa" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "math/big" + "testing" + "time" +) + +var fakeDb, _ = ethdb.NewMemDatabase() + +func TestIsValidRelayer(t *testing.T) { + order := &OrderItem{ + ExchangeAddress: common.StringToAddress("relayer1"), + } + var stateDb, _ = state.New(common.Hash{}, state.NewDatabase(fakeDb)) + slotKec := crypto.Keccak256(order.ExchangeAddress.Bytes(), common.BigToHash(new(big.Int).SetUint64(RelayerListSlot)).Bytes()) + locRelayerState := new(big.Int).SetBytes(slotKec) + stateDb.SetState(common.StringToAddress(common.RelayerRegistrationSMC), common.BigToHash(locRelayerState), common.BigToHash(new(big.Int).SetUint64(0))) + if valid := IsValidRelayer(stateDb, order.ExchangeAddress); valid { + t.Error("TestIsValidRelayer FAILED. It should be invalid relayer", "ExchangeAddress", order.ExchangeAddress) + } + + stateDb.SetState(common.StringToAddress(common.RelayerRegistrationSMC), common.BigToHash(locRelayerState), common.BigToHash(new(big.Int).SetUint64(2500))) + if valid := IsValidRelayer(stateDb, order.ExchangeAddress); !valid { + t.Error("TestIsValidRelayer FAILED. This address should be a valid relayer", "ExchangeAddress", order.ExchangeAddress) + } +} + +func TestOrderItem_VerifyBalance(t *testing.T) { + stateDb, _ := state.New(common.Hash{}, state.NewDatabase(fakeDb)) + addr := common.StringToAddress("userAddress") + + // native tomo + // sell 100 TOMO + // user should have at least 100 TOMO + order1 := &OrderItem{ + UserAddress: addr, + Side: Ask, + PairName: "TOMO/WETH", + BaseToken: common.Address{}, + QuoteToken: common.StringToAddress("weth"), + Quantity: big.NewInt(100), + Price: big.NewInt(1), + } + stateDb.SetBalance(addr, big.NewInt(105)) + + if err := order1.verifyBalance(stateDb); err != nil { + t.Error(err.Error()) + } + + // Test TRC token + // buy 100 TOMO + // user should have at least 100 TOMO + order2 := &OrderItem{ + UserAddress: addr, + Side: Bid, + PairName: "TOMO/WETH", + BaseToken: common.Address{}, + QuoteToken: common.StringToAddress("weth"), + Quantity: big.NewInt(100), + Price: big.NewInt(1), + } + locBalance := new(big.Int) + locBalance.SetBytes(crypto.Keccak256(addr.Bytes(), common.BigToHash(big.NewInt(0)).Bytes())) + stateDb.SetState(common.StringToAddress("weth"), common.BigToHash(locBalance), common.BigToHash(big.NewInt(98))) + + if balance := GetTokenBalance(stateDb, addr, common.StringToAddress("weth")); balance.Cmp(big.NewInt(98)) != 0 { + t.Error("TestGetTokenBalance FAILED. Expected 98", "actual", balance) + } + + if err := order2.verifyBalance(stateDb); err == nil { + t.Error("It should be failed because balance is less than ordervalue") + } +} + +// test full-verify orderItem +// VerifyMatchedOrder consists of some partial tests: +// verifyTimestamp, verifyOrderSide, verifyOrderType, verifyRelayer, verifyBalance, verifySignature +// in this test, we sequentially make each test PASS +func TestOrderItem_VerifyMatchedOrder(t *testing.T) { + stateDb, _ := state.New(common.Hash{}, state.NewDatabase(fakeDb)) + addr := common.StringToAddress("test_user") + + order := &OrderItem{ + PairName: "TOMO/WETH", + UserAddress: addr, + Nonce: big.NewInt(1), + MakeFee: big.NewInt(1), + TakeFee: big.NewInt(1), + } + + // failed due to no timestamp + if err := order.VerifyMatchedOrder(stateDb); err != errNoTimestamp { + t.Error(err) + } + + // failed due to future order + order.CreatedAt = uint64(time.Now().Unix()) + 1000 // future time + order.UpdatedAt = uint64(time.Now().Unix()) + 1000 // future time + if err := order.VerifyMatchedOrder(stateDb); err != errFutureOrder { + t.Error(err) + } + + // set valid timestamp to order + order.CreatedAt = uint64(time.Now().Unix()) - 1000 // passed time + order.UpdatedAt = uint64(time.Now().Unix()) - 1000 // passed time + + // after verifyTimestamp PASS, order should fail the next step: verifyOrderSide + if err := order.VerifyMatchedOrder(stateDb); err != errInvalidOrderSide { + t.Error(err) + } + + // set valid orderSide: Ask + order.Side = Ask + + // after verifyOrderSide PASS, order should fail the next step: verifyOrderType + if err := order.VerifyMatchedOrder(stateDb); err != errInvalidOrderType { + t.Error(err) + } + + // set valid orderType + order.Type = Limit + + // after verifyOrderType PASS, order should fail the next step: verifyRelayer + if err := order.VerifyMatchedOrder(stateDb); err != errInvalidRelayer { + t.Error(err) + } + + // set relayer and mock state to make it valid + order.ExchangeAddress = common.StringToAddress("relayer1") + slotKec := crypto.Keccak256(order.ExchangeAddress.Bytes(), common.BigToHash(new(big.Int).SetUint64(RelayerListSlot)).Bytes()) + locRelayerState := new(big.Int).SetBytes(slotKec) + stateDb.SetState(common.StringToAddress(common.RelayerRegistrationSMC), common.BigToHash(locRelayerState), common.BigToHash(new(big.Int).SetUint64(2500))) + + // then order should fail the next step: verifyBalance + // failed due to missing price + if err := order.VerifyMatchedOrder(stateDb); err != errInvalidPrice { + t.Error(err) + } + + // set price + order.Price = big.NewInt(1) + + // failed due to missing quantity + if err := order.VerifyMatchedOrder(stateDb); err != errInvalidQuantity { + t.Error(err) + } + + // set quantity + order.Quantity = big.NewInt(100) + + // failed due to not enough balance + if err := order.VerifyMatchedOrder(stateDb); err != errNotEnoughBalance { + t.Error(err) + } + + // mock state to make order pass verifyBalance + // balance should greater than or equal 100 TOMO + stateDb.SetBalance(addr, big.NewInt(105)) + + // after pass verifyBalance, order should fail verifySignature + // wrong hash + if err := order.VerifyMatchedOrder(stateDb); err != errWrongHash { + t.Error(err) + } + + // set a wrong signature + privKey, _ := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19") + pubKey := privKey.Public() + pubKeyECDSA, _ := pubKey.(*ecdsa.PublicKey) + pubKeyBytes := crypto.FromECDSAPub(pubKeyECDSA) + + copy(addr[:], crypto.Keccak256(pubKeyBytes[1:])[12:]) + order.UserAddress = addr + // since userAddress has been updated, we should update balance for the user to pass verifyBalance + stateDb.SetBalance(addr, big.NewInt(105)) + + // set valid hash + order.Hash = order.computeHash() + + signatureBytes, _ := crypto.Sign(common.StringToHash("invalid hash").Bytes(), privKey) + sig := &Signature{ + R: common.BytesToHash(signatureBytes[0:32]), + S: common.BytesToHash(signatureBytes[32:64]), + V: signatureBytes[64] + 27, + } + order.Signature = sig + + // wrong signature + if err := order.VerifyMatchedOrder(stateDb); err != errInvalidSignature { + t.Error(err) + } + + // set valid signature + signatureBytes, _ = crypto.Sign(order.Hash.Bytes(), privKey) + sig = &Signature{ + R: common.BytesToHash(signatureBytes[0:32]), + S: common.BytesToHash(signatureBytes[32:64]), + V: signatureBytes[64] + 27, + } + order.Signature = sig + + // Finally, we have a valid order + if err := order.VerifyMatchedOrder(stateDb); err != nil { + t.Error(err) + } + +} + +func TestTxDataMatch_DecodeOrder(t *testing.T) { + txDataMatch := &TxDataMatch{ + Order: []byte("abc"), + } + var err error + if _, err = txDataMatch.DecodeOrder(); err == nil { + t.Error("It should fail") + } + + orderItem := &OrderItem{ + PairName: "TOMO/WETH", + Price: big.NewInt(1), + Quantity: big.NewInt(100), + Type: Limit, + Side: Bid, + UserAddress: common.StringToAddress("aaa"), + ExchangeAddress: common.StringToAddress("bbb"), + Signature: &Signature{}, + } + b, err := EncodeBytesItem(orderItem) + if err != nil { + t.Error(err) + } + txDataMatch.Order = b + if _, err = txDataMatch.DecodeOrder(); err != nil { + t.Error(err) + } +}