github.com/klaytn/klaytn@v1.12.1/node/sc/kas/anchor.go (about)

     1  // Copyright 2020 The klaytn Authors
     2  // This file is part of the klaytn library.
     3  //
     4  // The klaytn library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The klaytn library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the klaytn library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package kas
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"math/big"
    26  	"net/http"
    27  
    28  	"github.com/klaytn/klaytn/blockchain/types"
    29  	"github.com/klaytn/klaytn/common"
    30  )
    31  
    32  const (
    33  	codeOK              = 0
    34  	codeAlreadyAnchored = 1072100
    35  )
    36  
    37  var (
    38  	errNotFoundBlock      = errors.New("not found block")
    39  	errInvalidBlockNumber = errors.New("invalid block number")
    40  )
    41  
    42  //go:generate mockgen -destination=./mocks/anchordb_mock.go -package=mocks github.com/klaytn/klaytn/kas AnchorDB
    43  type AnchorDB interface {
    44  	WriteAnchoredBlockNumber(blockNum uint64)
    45  	ReadAnchoredBlockNumber() uint64
    46  }
    47  
    48  //go:generate mockgen -destination=./mocks/blockchain_mock.go -package=mocks github.com/klaytn/klaytn/kas BlockChain
    49  type BlockChain interface {
    50  	GetBlockByNumber(number uint64) *types.Block
    51  }
    52  
    53  //go:generate mockgen -destination=./mocks/client_mock.go -package=mocks github.com/klaytn/klaytn/kas HTTPClient
    54  type HTTPClient interface {
    55  	Do(req *http.Request) (*http.Response, error)
    56  }
    57  
    58  type Anchor struct {
    59  	kasConfig *KASConfig
    60  	db        AnchorDB
    61  	bc        BlockChain
    62  	client    HTTPClient
    63  }
    64  
    65  func NewKASAnchor(kasConfig *KASConfig, db AnchorDB, bc BlockChain) *Anchor {
    66  	return &Anchor{
    67  		kasConfig: kasConfig,
    68  		db:        db,
    69  		bc:        bc,
    70  		client:    &http.Client{},
    71  	}
    72  }
    73  
    74  // AnchorPeriodicBlock periodically anchor blocks to KAS.
    75  // if given block is invalid, it does nothing.
    76  func (anchor *Anchor) AnchorPeriodicBlock(block *types.Block) {
    77  	if !anchor.kasConfig.Anchor {
    78  		return
    79  	}
    80  
    81  	if block == nil {
    82  		logger.Error("KAS Anchor : can not anchor nil block")
    83  		return
    84  	}
    85  
    86  	if block.NumberU64()%anchor.kasConfig.AnchorPeriod != 0 {
    87  		return
    88  	}
    89  
    90  	if err := anchor.AnchorBlock(block); err != nil {
    91  		logger.Warn("Failed to anchor a block via KAS", "blkNum", block.NumberU64(), "err", err)
    92  	}
    93  }
    94  
    95  // blockToAnchoringDataInternalType0 makes AnchoringDataInternalType0 from the given block.
    96  // TxCount is the number of transactions of the last N blocks. (N is a anchor period.)
    97  func (anchor *Anchor) blockToAnchoringDataInternalType0(block *types.Block) *types.AnchoringDataInternalType0 {
    98  	start := uint64(0)
    99  	if block.NumberU64() >= anchor.kasConfig.AnchorPeriod {
   100  		start = block.NumberU64() - anchor.kasConfig.AnchorPeriod + 1
   101  	}
   102  	blkCnt := block.NumberU64() - start + 1
   103  
   104  	txCount := len(block.Body().Transactions)
   105  	for i := start; i < block.NumberU64(); i++ {
   106  		block := anchor.bc.GetBlockByNumber(i)
   107  		if block == nil {
   108  			return nil
   109  		}
   110  		txCount += len(block.Body().Transactions)
   111  	}
   112  
   113  	return &types.AnchoringDataInternalType0{
   114  		BlockHash:     block.Hash(),
   115  		TxHash:        block.Header().TxHash,
   116  		ParentHash:    block.Header().ParentHash,
   117  		ReceiptHash:   block.Header().ReceiptHash,
   118  		StateRootHash: block.Header().Root,
   119  		BlockNumber:   block.Header().Number,
   120  		BlockCount:    new(big.Int).SetUint64(blkCnt),
   121  		TxCount:       big.NewInt(int64(txCount)),
   122  	}
   123  }
   124  
   125  // AnchorBlock converts given block to payload and anchor the payload via KAS anchor API.
   126  func (anchor *Anchor) AnchorBlock(block *types.Block) error {
   127  	anchorData := anchor.blockToAnchoringDataInternalType0(block)
   128  
   129  	payload := dataToPayload(anchorData)
   130  
   131  	res, err := anchor.sendRequest(payload)
   132  	if err != nil || res.Code != codeOK {
   133  		if res != nil {
   134  			if res.Code == codeAlreadyAnchored {
   135  				logger.Info("Already anchored a block", "blkNum", block.NumberU64())
   136  				return nil
   137  			}
   138  
   139  			result, _ := json.MarshalIndent(res, "", "	")
   140  			logger.Warn(fmt.Sprintf(`AnchorBlock returns below http raw result with the error(%v) at the block(%v) :
   141  %v`, err, block.NumberU64(), string(result)))
   142  		}
   143  		return err
   144  	}
   145  
   146  	logger.Info("Anchored a block via KAS", "blkNum", block.NumberU64())
   147  	return nil
   148  }
   149  
   150  type respBody struct {
   151  	Code    int         `json:"code"`
   152  	Result  interface{} `json:"result"`
   153  	Message interface{} `json:"message"`
   154  }
   155  
   156  type reqBody struct {
   157  	Operator common.Address `json:"operator"`
   158  	Payload  interface{}    `json:"payload"`
   159  }
   160  
   161  type Payload struct {
   162  	Id string `json:"id"`
   163  	types.AnchoringDataInternalType0
   164  }
   165  
   166  // dataToPayload wraps given AnchoringDataInternalType0 to payload with `id` field.
   167  func dataToPayload(anchorData *types.AnchoringDataInternalType0) *Payload {
   168  	payload := &Payload{
   169  		Id:                         anchorData.BlockNumber.String(),
   170  		AnchoringDataInternalType0: *anchorData,
   171  	}
   172  
   173  	return payload
   174  }
   175  
   176  // sendRequest requests to KAS anchor API with given payload.
   177  func (anchor *Anchor) sendRequest(payload interface{}) (*respBody, error) {
   178  	header := map[string]string{
   179  		"Content-Type": "application/json",
   180  		"X-chain-id":   anchor.kasConfig.XChainId,
   181  	}
   182  
   183  	bodyData := reqBody{
   184  		Operator: anchor.kasConfig.Operator,
   185  		Payload:  payload,
   186  	}
   187  
   188  	bodyDataBytes, err := json.Marshal(bodyData)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	body := bytes.NewReader(bodyDataBytes)
   194  
   195  	// set up timeout for API call
   196  	ctx, cancel := context.WithTimeout(context.Background(), anchor.kasConfig.RequestTimeout)
   197  	defer cancel()
   198  
   199  	req, err := http.NewRequestWithContext(ctx, "POST", anchor.kasConfig.Url, body)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	req.SetBasicAuth(anchor.kasConfig.User, anchor.kasConfig.Pwd)
   204  	for k, v := range header {
   205  		req.Header.Set(k, v)
   206  	}
   207  
   208  	resp, err := anchor.client.Do(req)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	defer resp.Body.Close()
   213  
   214  	v := respBody{}
   215  	json.NewDecoder(resp.Body).Decode(&v)
   216  
   217  	if resp.StatusCode != http.StatusOK {
   218  		return &v, errors.New("http status : " + resp.Status)
   219  	}
   220  
   221  	return &v, nil
   222  }