diff --git a/consensus/pandora/api.go b/consensus/pandora/api.go index 976b25884dfa..a8ac964fdafa 100644 --- a/consensus/pandora/api.go +++ b/consensus/pandora/api.go @@ -12,9 +12,7 @@ import ( ) var ( - errPandoraStopped = errors.New("pandora stopped") - errInvalidParentHash = errors.New("invalid parent hash") - errInvalidBlockNumber = errors.New("invalid block number") + errPandoraStopped = errors.New("pandora stopped") ) // API is a user facing RPC API to allow controlling the signer and voting @@ -32,8 +30,8 @@ func (api *API) GetShardingWork(parentHash common.Hash, blockNumber uint64, slot } var ( - shardingInfoCh = make(chan [4]string) - errorCh = make(chan error) + shardingInfoCh = make(chan [4]string, 1) + errorCh = make(chan error, 1) ) select { @@ -45,26 +43,6 @@ func (api *API) GetShardingWork(parentHash common.Hash, blockNumber uint64, slot select { case shardingInfo := <-shardingInfoCh: log.Debug("Sending current sharding info to validator", "shardingInfo", fmt.Sprintf("%+v", shardingInfo)) - curBlockHeader := api.pandora.currentBlock.Header() - if curBlockHeader != nil { - log.Debug("Current Block Header Data", "time", curBlockHeader.Time, "block number", curBlockHeader.Number) - // When producing block #1, validator does not know about hash of block #0 - // so do not check the parent hash and block number 1 - if blockNumber == 1 { - return shardingInfo, nil - } - if curBlockHeader.ParentHash != parentHash { - log.Error("Mis-match in parentHash", - "blockNumber", curBlockHeader.Number.Uint64(), - "remoteParentHash", curBlockHeader.ParentHash, "receivedParentHash", parentHash) - return emptyRes, errInvalidParentHash - } - if curBlockHeader.Number.Uint64() != blockNumber { - log.Error("Mis-match in block number", - "remoteBlockNumber", curBlockHeader.Number.Uint64(), "receivedBlockNumber", blockNumber) - return emptyRes, errInvalidBlockNumber - } - } return shardingInfo, nil case err := <-errorCh: return emptyRes, err diff --git a/consensus/pandora/consensus.go b/consensus/pandora/consensus.go index caf44fb8936a..a1db0ade7cc2 100644 --- a/consensus/pandora/consensus.go +++ b/consensus/pandora/consensus.go @@ -1,6 +1,9 @@ package pandora import ( + "math/big" + "runtime" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/state" @@ -9,8 +12,6 @@ import ( "github.com/ethereum/go-ethereum/trie" "github.com/pkg/errors" "golang.org/x/crypto/sha3" - "math/big" - "runtime" ) var ( @@ -22,6 +23,42 @@ func (pan *Pandora) Author(header *types.Header) (common.Address, error) { return header.Coinbase, nil } +// SealHash returns the hash of a block prior to it being sealed. +func (p *Pandora) SealHash(header *types.Header) (hash common.Hash) { + hasher := sha3.NewLegacyKeccak256() + + extraData := header.Extra + extraDataLen := len(extraData) + + // Bls signature is 96 bytes long and will be inserted at the bottom of the extraData field + if extraDataLen > signatureSize { + //extraData = extraData[:extraDataLen-signatureSize] + pandoraExtraData := new(ExtraDataSealed) + pandoraExtraData.FromHeader(header) + headerExtra := new(ExtraData) + headerExtra.Epoch = pandoraExtraData.Epoch + headerExtra.Turn = pandoraExtraData.Turn + headerExtra.Slot = pandoraExtraData.Slot + extraData, _ = rlp.EncodeToBytes(headerExtra) + } + rlp.Encode(hasher, []interface{}{ + header.ParentHash, + header.UncleHash, + header.Coinbase, + header.Root, + header.TxHash, + header.ReceiptHash, + header.Bloom, + header.Difficulty, + header.Number, + header.GasLimit, + header.GasUsed, + header.Time, + extraData, + }) + hasher.Sum(hash[:0]) + return hash +} func (p *Pandora) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header, seal bool) error { number := header.Number.Uint64() if chain.GetHeader(header.Hash(), number) != nil { @@ -125,29 +162,6 @@ func (p *Pandora) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header return types.NewBlock(header, txs, uncles, receipts, trie.NewStackTrie(nil)), nil } -// SealHash returns the hash of a block prior to it being sealed. -func (p *Pandora) SealHash(header *types.Header) (hash common.Hash) { - hasher := sha3.NewLegacyKeccak256() - extraData := header.Extra - rlp.Encode(hasher, []interface{}{ - header.ParentHash, - header.UncleHash, - header.Coinbase, - header.Root, - header.TxHash, - header.ReceiptHash, - header.Bloom, - header.Difficulty, - header.Number, - header.GasLimit, - header.GasUsed, - header.Time, - extraData, - }) - hasher.Sum(hash[:0]) - return hash -} - func (p *Pandora) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int { return calcDifficulty() } diff --git a/consensus/pandora/helpers.go b/consensus/pandora/helpers.go index 486405dcdae0..8d0e7aafdb70 100644 --- a/consensus/pandora/helpers.go +++ b/consensus/pandora/helpers.go @@ -1,7 +1,11 @@ package pandora import ( + "math/big" + "math/bits" + "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" @@ -9,8 +13,9 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/pkg/errors" + common2 "github.com/silesiacoin/bls/common" "github.com/silesiacoin/bls/herumi" - "math/big" ) // copyEpochInfo @@ -61,6 +66,45 @@ func getDummyHeader() *types.Header { } } +func Mul64(a, b uint64) (uint64, error) { + overflows, val := bits.Mul64(a, b) + if overflows > 0 { + return 0, errors.New("multiplication overflows") + } + return val, nil +} + +func (p *Pandora) StartSlot(epoch uint64) (uint64, error) { + slot, err := Mul64(p.config.SlotsPerEpoch, epoch) + if err != nil { + return slot, errors.Errorf("start slot calculation overflows: %v", err) + } + return slot, nil +} + +func (pandoraExtraDataSealed *ExtraDataSealed) FromHeader(header *types.Header) { + err := rlp.DecodeBytes(header.Extra, pandoraExtraDataSealed) + + if nil != err { + panic(err.Error()) + } +} + +func (pandoraExtraDataSealed *ExtraDataSealed) FromExtraDataAndSignature( + pandoraExtraData ExtraData, + signature common2.Signature, +) { + var blsSignatureBytes BlsSignatureBytes + signatureBytes := signature.Marshal() + + if len(signatureBytes) != signatureSize { + panic("Incompatible bls mode detected") + } + + copy(blsSignatureBytes[:], signatureBytes[:]) + pandoraExtraDataSealed.ExtraData = pandoraExtraData + pandoraExtraDataSealed.BlsSignatureBytes = &blsSignatureBytes +} func (p *Pandora) verifyHeaderWorker(chain consensus.ChainHeaderReader, headers []*types.Header, index int) error { var parent *types.Header if index == 0 { diff --git a/consensus/pandora/pandora.go b/consensus/pandora/pandora.go index 5e039344cca7..75f002842932 100644 --- a/consensus/pandora/pandora.go +++ b/consensus/pandora/pandora.go @@ -2,14 +2,19 @@ package pandora import ( "context" + "sync" + "time" + + "github.com/ethereum/go-ethereum/rlp" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/pkg/errors" - "sync" - "time" ) var ( @@ -22,6 +27,8 @@ var ( errInvalidEpochInfo = errors.New("invalid epoch info") errEmptyOrchestratorUrl = errors.New("orchestrator url is empty") errNoShardingBlock = errors.New("no pandora sharding block available yet") + errInvalidParentHash = errors.New("invalid parent hash") + errInvalidBlockNumber = errors.New("invalid block number") errOlderBlockTime = errors.New("timestamp older than parent") errSigFailedToVerify = errors.New("signature did not verify") ) @@ -42,6 +49,7 @@ type Pandora struct { epochInfoCache *EpochInfoCache currentEpoch uint64 currentEpochInfo *EpochInfo + currentBlock *types.Block dialRPC DialRPCFn endpoint string connected bool @@ -52,10 +60,10 @@ type Pandora struct { apiResponse [4]string results chan<- *types.Block + works map[common.Hash]*types.Block fetchShardingInfoCh chan *shardingInfoReq // Channel used for remote sealer to fetch mining work submitShardingInfoCh chan *shardingResult - currentBlock *types.Block newSealRequestCh chan *sealTask @@ -100,6 +108,7 @@ func New( submitShardingInfoCh: make(chan *shardingResult), newSealRequestCh: make(chan *sealTask), subscriptionErrCh: make(chan error), + works: make(map[common.Hash]*types.Block), } pandora.start() @@ -122,30 +131,126 @@ func (p *Pandora) start() { }() } +func (p *Pandora) updateBlockHeader(currentBlock *types.Block, slotNumber uint64, epoch uint64) [4]string { + currentHeader := currentBlock.Header() + + // modify the header with slot, epoch and turn + extraData := new(ExtraData) + extraData.Slot = slotNumber + extraData.Epoch = epoch + + // calculate turn + startSlot, err := p.StartSlot(epoch) + if err != nil { + log.Error("error while calculating start slot from epoch", "error", err, "epoch", epoch) + } + extraData.Turn = slotNumber - startSlot + + extraDataInBytes, err := rlp.EncodeToBytes(extraData) + if err != nil { + log.Error("error while encoding extra data to bytes", "error", err) + } + + currentHeader.Extra = extraDataInBytes + + // get the updated block + updatedBlock := currentBlock.WithSeal(currentHeader) + // update the current block with this newly created block + //p.currentBlock = updatedBlock + + rlpHeader, _ := rlp.EncodeToBytes(updatedBlock.Header()) + + hash := p.SealHash(updatedBlock.Header()) + + var retVal [4]string + retVal[0] = hash.Hex() + retVal[1] = updatedBlock.Header().ReceiptHash.Hex() + retVal[2] = hexutil.Encode(rlpHeader) + retVal[3] = hexutil.Encode(updatedBlock.Header().Number.Bytes()) + + p.works[hash] = updatedBlock + + return retVal +} + // run subscribes to all the services for the ETH1.0 chain. func (p *Pandora) run(done <-chan struct{}) { log.Debug("Pandora chain service is starting") p.runError = nil + // ticker is needed to clean up the map + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + // the loop waits for any error which comes from consensus info subscription // if any subscription error happens, it will try to reconnect and re-subscribe with pandora chain again. for { select { - case sealReqeust := <-p.newSealRequestCh: - log.Debug("new seal request in pandora engine", "block number", sealReqeust.block.Number()) + case sealRequest := <-p.newSealRequestCh: + log.Debug("new seal request in pandora engine", "block number", sealRequest.block.Number()) // first save it to result channel. so that we can send worker about the info - p.results = sealReqeust.results - // then prepare hash and set the block to current state + p.results = sealRequest.results + // then simply save the block into current block. We will use it again + p.currentBlock = sealRequest.block case shardingInfoReq := <-p.fetchShardingInfoCh: - curHeader := getDummyHeader() - hash := p.SealHash(curHeader) - shardingInfo := prepareShardingInfo(curHeader, hash) - shardingInfoReq.res <- shardingInfo + // Get sharding work API is called and we got slot number from vanguard + if p.currentBlock == nil { + // no block is available. worker has not submit any block to seal. So something went wrong. send error + shardingInfoReq.errc <- errNoShardingBlock + } else { + // current block available. now put that info into header extra data and generate seal hash + // before that check if current block is valid and compatible with the request + + currentBlock := p.currentBlock + currentBlockHeader := currentBlock.Header() + + if shardingInfoReq.blockNumber > 1 { + // When producing block #1, validator does not know about hash of block #0 + // so do not check the parent hash and block number 1 + + if currentBlockHeader.ParentHash != shardingInfoReq.parentHash { + log.Error("Mis-match in parentHash", + "blockNumber", currentBlockHeader.Number.Uint64(), + "remoteParentHash", currentBlockHeader.ParentHash, "receivedParentHash", shardingInfoReq.parentHash) + shardingInfoReq.errc <- errInvalidParentHash + // error found. so don't do anything + continue + } + + if currentBlockHeader.Number.Uint64() != shardingInfoReq.blockNumber { + log.Error("Mis-match in block number", + "remoteBlockNumber", currentBlockHeader.Number.Uint64(), "receivedBlockNumber", shardingInfoReq.blockNumber) + shardingInfoReq.errc <- errInvalidBlockNumber + // error found. so don't do anything + continue + } + } + // now modify the current block header and generate seal hash + log.Debug("for GetShardingWork updating block header extra data", "slot", shardingInfoReq.slot, "epoch", shardingInfoReq.epoch) + shardingInfoReq.res <- p.updateBlockHeader(currentBlock, shardingInfoReq.slot, shardingInfoReq.epoch) + } case submitSignatureData := <-p.submitShardingInfoCh: - log.Debug("get submit signature api called", "submitSignatureData", submitSignatureData) + if p.submitWork(submitSignatureData.nonce, submitSignatureData.hash, submitSignatureData.blsSeal) { + log.Debug("submitWork is successful", "nonce", submitSignatureData.nonce, "hash", submitSignatureData.hash) + submitSignatureData.errc <- nil + } else { + log.Debug("submitWork is failed", "nonce", submitSignatureData.nonce, "hash", submitSignatureData.hash, "signature", submitSignatureData.blsSeal, + "current block number", p.currentBlock.NumberU64()) + submitSignatureData.errc <- errors.New("invalid submit work request") + } + + case <-ticker.C: + // Clear stale pending blocks + if p.currentBlock != nil { + for hash, block := range p.works { + if block.NumberU64()+staleThreshold <= p.currentBlock.NumberU64() { + delete(p.works, hash) + } + } + } case err := <-p.subscriptionErrCh: log.Debug("Got subscription error", "err", err) diff --git a/consensus/pandora/sealer.go b/consensus/pandora/sealer.go index 38d821701251..b2c9c4082a48 100644 --- a/consensus/pandora/sealer.go +++ b/consensus/pandora/sealer.go @@ -1,8 +1,19 @@ package pandora import ( + "time" + + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/silesiacoin/bls/herumi" +) + +const ( + // staleThreshold is the maximum depth of the acceptable stale but valid ethash solution. + staleThreshold = 7 ) func (pan *Pandora) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { @@ -10,3 +21,74 @@ func (pan *Pandora) Seal(chain consensus.ChainHeaderReader, block *types.Block, pan.newSealRequestCh <- &sealTask{block: block, results: results} return nil } + +func (pan *Pandora) submitWork(nonce types.BlockNonce, sealHash common.Hash, blsSignatureBytes *BlsSignatureBytes) bool { + if pan.currentBlock == nil { + log.Error("No block found while submitting work", "sealhash", sealHash) + return false + } + + // Make sure the work submitted is present + block := pan.works[sealHash] + if block == nil { + log.Warn("Work submitted but none pending", "sealhash", sealHash, "curnumber", pan.currentBlock.NumberU64()) + return false + } + // Verify the correctness of submitted result. + header := block.Header() + extraDataWithSignature := new(ExtraDataSealed) + blsSignature, err := herumi.SignatureFromBytes(blsSignatureBytes[:]) + + if nil != err { + log.Error("error while forming signature from bytes", "error", err, "method name", "Seal") + return false + } + + pandoraExtraData := new(ExtraData) + err = rlp.DecodeBytes(header.Extra, pandoraExtraData) + + if nil != err { + log.Error("rlp decode failed while converting pandora Extra data", "error", err) + return false + } + + extraDataWithSignature.FromExtraDataAndSignature(*pandoraExtraData, blsSignature) + header.Extra, err = rlp.EncodeToBytes(extraDataWithSignature) + + if nil != err { + log.Error("Invalid extraData in header", "sealhash", sealHash, "err", err) + return false + } + + start := time.Now() + + if err := pan.verifyBLSSignature(header); err != nil { + log.Warn("Invalid proof-of-work submitted", "sealhash", sealHash, "elapsed", common.PrettyDuration(time.Since(start)), "err", err) + return false + } + + // Make sure the result channel is assigned. + if pan.results == nil { + log.Error("Ethash result channel is empty, submitted mining result is rejected") + return false + } + log.Debug("Verified correct proof-of-work", "sealhash", sealHash, "elapsed", common.PrettyDuration(time.Since(start))) + + // Solutions seems to be valid, return to the miner and notify acceptance. + solution := block.WithSeal(header) + + // The submitted solution is within the scope of acceptance. + if solution.NumberU64()+staleThreshold > pan.currentBlock.NumberU64() { + select { + case pan.results <- solution: + log.Debug("Work submitted is acceptable", "number", solution.NumberU64(), "sealhash", sealHash, "hash", solution.Hash()) + return true + default: + log.Warn("Sealing result is not read by miner", "mode", "remote", "sealhash", sealHash) + return false + } + } + // The submitted block is too old to accept, drop it. + log.Warn("Work submitted is too old", "number", solution.NumberU64(), "sealhash", sealHash, "hash", solution.Hash()) + return false +} diff --git a/consensus/pandora/types.go b/consensus/pandora/types.go index 49d442738cd8..90ee9b9f56e4 100644 --- a/consensus/pandora/types.go +++ b/consensus/pandora/types.go @@ -1,10 +1,11 @@ package pandora import ( + "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" bls_common "github.com/silesiacoin/bls/common" - "time" ) const signatureSize = 96 diff --git a/go.mod b/go.mod index f617f2a1d678..5db9a5114e27 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,6 @@ require ( golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 golang.org/x/text v0.3.3 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - golang.org/x/tools v0.1.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6 gopkg.in/urfave/cli.v1 v1.20.0 diff --git a/go.sum b/go.sum index 8c5c980c8182..24c54fb6ef24 100644 --- a/go.sum +++ b/go.sum @@ -530,7 +530,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -601,7 +600,6 @@ golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -646,8 +644,6 @@ golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=