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 }