diff --git a/consensus/bor/api.go b/consensus/bor/api.go index 98f7c46b6e615f3adc7625b0c7699cfdc189fc85..672d378215aea51b5d6a3d9f0b2b7bbdb55cadec 100644 --- a/consensus/bor/api.go +++ b/consensus/bor/api.go @@ -17,17 +17,32 @@ package bor import ( + "math" + "math/big" + "strconv" + "sync" + + lru "github.com/hashicorp/golang-lru" "github.com/maticnetwork/bor/common" "github.com/maticnetwork/bor/consensus" "github.com/maticnetwork/bor/core/types" + "github.com/maticnetwork/bor/crypto" "github.com/maticnetwork/bor/rpc" + "github.com/xsleonard/go-merkle" + "golang.org/x/crypto/sha3" +) + +var ( + // MaxCheckpointLength is the maximum number of blocks that can be requested for constructing a checkpoint root hash + MaxCheckpointLength = uint64(math.Pow(2, 15)) ) // API is a user facing RPC API to allow controlling the signer and voting // mechanisms of the proof-of-authority scheme. type API struct { - chain consensus.ChainReader - bor *Bor + chain consensus.ChainReader + bor *Bor + rootHashCache *lru.ARCCache } // GetSnapshot retrieves the state snapshot at a given block. @@ -105,3 +120,71 @@ func (api *API) GetCurrentValidators() ([]*Validator, error) { } return snap.ValidatorSet.Validators, nil } + +// GetRootHash returns the merkle root of the start to end block headers +func (api *API) GetRootHash(start int64, end int64) ([]byte, error) { + if err := api.initializeRootHashCache(); err != nil { + return nil, err + } + key := getRootHashKey(start, end) + if root, known := api.rootHashCache.Get(key); known { + return root.([]byte), nil + } + length := uint64(end - start + 1) + if length > MaxCheckpointLength { + return nil, &MaxCheckpointLengthExceededError{start, end} + } + currentHeaderNumber := api.chain.CurrentHeader().Number.Int64() + if start > end || end > currentHeaderNumber { + return nil, &InvalidStartEndBlockError{start, end, currentHeaderNumber} + } + blockHeaders := make([]*types.Header, end-start+1) + wg := new(sync.WaitGroup) + concurrent := make(chan bool, 20) + for i := start; i <= end; i++ { + wg.Add(1) + concurrent <- true + go func(number int64) { + blockHeaders[number-start] = api.chain.GetHeaderByNumber(uint64(number)) + <-concurrent + wg.Done() + }(i) + } + wg.Wait() + close(concurrent) + + headers := make([][32]byte, nextPowerOfTwo(length)) + for i := 0; i < len(blockHeaders); i++ { + blockHeader := blockHeaders[i] + header := crypto.Keccak256(appendBytes32( + blockHeader.Number.Bytes(), + new(big.Int).SetUint64(blockHeader.Time).Bytes(), + blockHeader.TxHash.Bytes(), + blockHeader.ReceiptHash.Bytes(), + )) + + var arr [32]byte + copy(arr[:], header) + headers[i] = arr + } + + tree := merkle.NewTreeWithOpts(merkle.TreeOptions{EnableHashSorting: false, DisableHashLeaves: true}) + if err := tree.Generate(convert(headers), sha3.NewLegacyKeccak256()); err != nil { + return nil, err + } + root := tree.Root().Hash + api.rootHashCache.Add(key, root) + return root, nil +} + +func (api *API) initializeRootHashCache() error { + var err error + if api.rootHashCache == nil { + api.rootHashCache, err = lru.NewARC(10) + } + return err +} + +func getRootHashKey(start int64, end int64) string { + return strconv.FormatInt(start, 10) + "-" + strconv.FormatInt(end, 10) +} diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 84fabf3336685fc8a33671fea37bfe9e94f6a3a0..945395dd6be9462f3195642c5156ecc38a986361 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -441,7 +441,7 @@ func (c *Bor) verifyCascadingFields(chain consensus.ChainReader, header *types.H copy(validatorsBytes[i*validatorHeaderBytesLength:], validator.HeaderBytes()) } // len(header.Extra) >= extraVanity+extraSeal has already been validated in validateHeaderExtraField, so this won't result in a panic - if !bytes.Equal(header.Extra[extraVanity : len(header.Extra)-extraSeal], validatorsBytes) { + if !bytes.Equal(header.Extra[extraVanity:len(header.Extra)-extraSeal], validatorsBytes) { return errMismatchingSprintValidators } } diff --git a/consensus/bor/bor_test/helper.go b/consensus/bor/bor_test/helper.go index 1ead54ed88a9d02c3bdc2a5c8176700dfd6a2ab2..48735a7c5f71f7c784aab6a3283b982bf833fda5 100644 --- a/consensus/bor/bor_test/helper.go +++ b/consensus/bor/bor_test/helper.go @@ -107,13 +107,13 @@ func buildMinimalNextHeader(t *testing.T, block *types.Block, borConfig *params. header.Time += bor.CalcProducerDelay(header.Number.Uint64(), borConfig.Period, borConfig.Sprint, borConfig.ProducerDelay) isSprintEnd := (header.Number.Uint64()+1)%borConfig.Sprint == 0 if isSprintEnd { - header.Extra = make([]byte, 32 + 40 + 65) // vanity + validatorBytes + extraSeal + header.Extra = make([]byte, 32+40+65) // vanity + validatorBytes + extraSeal // the genesis file was initialized with a validator 0x71562b71999873db5b286df957af199ec94617f7 with power 10 // So, if you change ./genesis.json, do change the following as well validatorBytes, _ := hex.DecodeString("71562b71999873db5b286df957af199ec94617f7000000000000000000000000000000000000000a") copy(header.Extra[32:72], validatorBytes) } else { - header.Extra = make([]byte, 32 + 65) // vanity + extraSeal + header.Extra = make([]byte, 32+65) // vanity + extraSeal } _key, _ := hex.DecodeString(privKey) sig, err := secp256k1.Sign(crypto.Keccak256(bor.BorRLP(header)), _key) diff --git a/consensus/bor/errors.go b/consensus/bor/errors.go index a3b0dc656d08352a5a8ad3f5f1dddfca3aee52b3..ae7da982a7651cc19e6826186e46c6e78e14ea78 100644 --- a/consensus/bor/errors.go +++ b/consensus/bor/errors.go @@ -40,3 +40,31 @@ func (e *TotalVotingPowerExceededError) Error() string { e.Validators, ) } + +type InvalidStartEndBlockError struct { + Start int64 + End int64 + CurrentHeader int64 +} + +func (e *InvalidStartEndBlockError) Error() string { + return fmt.Sprintf( + "Invalid parameters start: %d and end block: %d params", + e.Start, + e.End, + ) +} + +type MaxCheckpointLengthExceededError struct { + Start int64 + End int64 +} + +func (e *MaxCheckpointLengthExceededError) Error() string { + return fmt.Sprintf( + "Start: %d and end block: %d exceed max allowed checkpoint length: %d", + e.Start, + e.End, + MaxCheckpointLength, + ) +} diff --git a/consensus/bor/merkle.go b/consensus/bor/merkle.go new file mode 100644 index 0000000000000000000000000000000000000000..bdfbaba9834eb57f23243a049b5f9e40592c31a7 --- /dev/null +++ b/consensus/bor/merkle.go @@ -0,0 +1,48 @@ +package bor + +func appendBytes32(data ...[]byte) []byte { + var result []byte + for _, v := range data { + paddedV, err := convertTo32(v) + if err == nil { + result = append(result, paddedV[:]...) + } + } + return result +} + +func nextPowerOfTwo(n uint64) uint64 { + if n == 0 { + return 1 + } + // http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + n-- + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + n |= n >> 32 + n++ + return n +} + +func convertTo32(input []byte) (output [32]byte, err error) { + l := len(input) + if l > 32 || l == 0 { + return + } + copy(output[32-l:], input[:]) + return +} + +func convert(input []([32]byte)) [][]byte { + var output [][]byte + for _, in := range input { + newInput := make([]byte, len(in[:])) + copy(newInput, in[:]) + output = append(output, newInput) + + } + return output +} diff --git a/eth/api_backend.go b/eth/api_backend.go index ff03d1a91e0537368b4c8ad9f10c07e916ef819a..7958f86b37c261d9a6d47eb85762d0473bf28d21 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -248,3 +248,17 @@ func (b *EthAPIBackend) ServiceFilter(ctx context.Context, session *bloombits.Ma go session.Multiplex(bloomRetrievalBatch, bloomRetrievalWait, b.eth.bloomRequests) } } + +func (b *EthAPIBackend) GetRootHash(ctx context.Context, starBlockNr rpc.BlockNumber, endBlockNr rpc.BlockNumber) ([]byte, error) { + var api *bor.API + for _, _api := range b.eth.Engine().APIs(b.eth.BlockChain()) { + if _api.Namespace == "bor" { + api = _api.Service.(*bor.API) + } + } + root, err := api.GetRootHash(starBlockNr.Int64(), endBlockNr.Int64()) + if err != nil { + return nil, err + } + return root, nil +} diff --git a/go.mod b/go.mod index ca433a71c6e86be991aa6fc41768bd611f3d735e..efce11a0bf9f85afb93992b53e0a8f53161a7f88 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208 + github.com/xsleonard/go-merkle v1.1.0 golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 golang.org/x/net v0.0.0-20200301022130-244492dfa37a golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e diff --git a/go.sum b/go.sum index c51ed2d8e957a19c286f02059467ee6d93ff1938..c15b30bb97f700cbf66266ae02be6fb655867871 100644 --- a/go.sum +++ b/go.sum @@ -176,6 +176,8 @@ github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZ github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208 h1:1cngl9mPEoITZG8s8cVcUy5CeIBYhEESkOB7m6Gmkrk= github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees= +github.com/xsleonard/go-merkle v1.1.0 h1:fHe1fuhJjGH22ZzVTAH0jqHLhTGhOq3wQjJN+8P0jQg= +github.com/xsleonard/go-merkle v1.1.0/go.mod h1:cW4z+UZ/4f2n9IJgIiyDCdYguchoDyDAPmpuOWGxdGg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA= diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 2a939ed2c463f6d81a3d249a6d45afffd50d95a9..0ad624e7ed3dbf88787e096451291689b7a4ddac 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -716,6 +716,14 @@ func (s *PublicBlockChainAPI) GetStorageAt(ctx context.Context, address common.A return res[:], state.Error() } +func (s *PublicBlockChainAPI) GetRootHash(ctx context.Context, starBlockNr rpc.BlockNumber, endBlockNr rpc.BlockNumber) ([]byte, error) { + root, err := s.b.GetRootHash(ctx, starBlockNr, endBlockNr) + if err != nil { + return nil, err + } + return root, nil +} + // CallArgs represents the arguments for a call. type CallArgs struct { From *common.Address `json:"from"` diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index e0deeab81fdb42e2c907b223fe1469d085b52866..21cef13f441506bdc7c7d38e356d9da287b03620 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -62,6 +62,7 @@ type Backend interface { SubscribeStateEvent(ch chan<- core.NewStateChangeEvent) event.Subscription SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription + GetRootHash(ctx context.Context, starBlockNr rpc.BlockNumber, endBlockNr rpc.BlockNumber) ([]byte, error) // Transaction pool API SendTx(ctx context.Context, signedTx *types.Transaction) error diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index d3fea7b50cc52366df901f91d244f686f11e2841..9ee4b4f3a7a46ceaf19b8c8afc51eca8b0b2f5bb 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -149,6 +149,11 @@ web3._extend({ call: 'bor_getCurrentValidators', params: 0 }), + new web3._extend.Method({ + name: 'getRootHash', + call: 'bor_getRootHash', + params: 2, + }), ] }); ` diff --git a/les/api_backend.go b/les/api_backend.go index 5c8ec82d8812f6c828364e2d4a44334ebb163371..4739abcdebf951e96caefd8e46766dd391bbcbe2 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -223,3 +223,7 @@ func (b *LesApiBackend) ServiceFilter(ctx context.Context, session *bloombits.Ma go session.Multiplex(bloomRetrievalBatch, bloomRetrievalWait, b.eth.bloomRequests) } } + +func (b *LesApiBackend) GetRootHash(ctx context.Context, starBlockNr rpc.BlockNumber, endBlockNr rpc.BlockNumber) ([]byte, error) { + return nil, errors.New("Not implemented") +}