gitlab.com/SiaPrime/SiaPrime@v1.4.1/node/api/consensus.go (about) 1 package api 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "math/big" 7 "net/http" 8 9 "gitlab.com/SiaPrime/SiaPrime/crypto" 10 "gitlab.com/SiaPrime/SiaPrime/encoding" 11 "gitlab.com/SiaPrime/SiaPrime/modules" 12 "gitlab.com/SiaPrime/SiaPrime/types" 13 14 bolt "github.com/coreos/bbolt" 15 "github.com/julienschmidt/httprouter" 16 ) 17 18 // ConsensusGET contains general information about the consensus set, with tags 19 // to support idiomatic json encodings. 20 type ConsensusGET struct { 21 // Consensus status values. 22 Synced bool `json:"synced"` 23 Height types.BlockHeight `json:"height"` 24 CurrentBlock types.BlockID `json:"currentblock"` 25 Target types.Target `json:"target"` 26 Difficulty types.Currency `json:"difficulty"` 27 28 // Consensus code constants. 29 BlockFrequency types.BlockHeight `json:"blockfrequency"` 30 BlockSizeLimit uint64 `json:"blocksizelimit"` 31 ExtremeFutureThreshold types.Timestamp `json:"extremefuturethreshold"` 32 FutureThreshold types.Timestamp `json:"futurethreshold"` 33 GenesisTimestamp types.Timestamp `json:"genesistimestamp"` 34 MaturityDelay types.BlockHeight `json:"maturitydelay"` 35 MedianTimestampWindow uint64 `json:"mediantimestampwindow"` 36 SiafundCount types.Currency `json:"siafundcount"` 37 SiafundPortion *big.Rat `json:"siafundportion"` 38 39 InitialCoinbase uint64 `json:"initialcoinbase"` 40 MinimumCoinbase uint64 `json:"minimumcoinbase"` 41 42 RootTarget types.Target `json:"roottarget"` 43 RootDepth types.Target `json:"rootdepth"` 44 45 SiacoinPrecision types.Currency `json:"siacoinprecision"` 46 } 47 48 // ConsensusHeadersGET contains information from a blocks header. 49 type ConsensusHeadersGET struct { 50 BlockID types.BlockID `json:"blockid"` 51 } 52 53 // ConsensusFileContract contains information about a file contract 54 type ConsensusFileContract struct { 55 FileSize uint64 `json:"filesize"` 56 FileMerkleRoot crypto.Hash `json:"filemerkleroot"` 57 WindowStart types.BlockHeight `json:"windowstart"` 58 WindowEnd types.BlockHeight `json:"windowend"` 59 Payout types.Currency `json:"payout"` 60 ValidProofOutputs map[string]types.SiacoinOutput `json:"validproofoutputs"` 61 MissedProofOutputs map[string]types.SiacoinOutput `json:"missedproofoutputs"` 62 UnlockHash types.UnlockHash `json:"unlockhash"` 63 RevisionNumber uint64 `json:"revisionnumber"` 64 } 65 66 // ConsensusFileContractRevision contains information about a file contract revision 67 type ConsensusFileContractRevision struct { 68 ParentID types.FileContractID `json:"parentid"` 69 UnlockConditions types.UnlockConditions `json:"unlockconditions"` 70 NewRevisionNumber uint64 `json:"newrevisionnumber"` 71 72 NewFileSize uint64 `json:"newfilesize"` 73 NewFileMerkleRoot crypto.Hash `json:"newfilemerkleroot"` 74 NewWindowStart types.BlockHeight `json:"newwindowstart"` 75 NewWindowEnd types.BlockHeight `json:"newwindowend"` 76 NewValidProofOutputs map[string]types.SiacoinOutput `json:"newvalidproofoutputs"` 77 NewMissedProofOutputs map[string]types.SiacoinOutput `json:"newmissedproofoutputs"` 78 NewUnlockHash types.UnlockHash `json:"newunlockhash"` 79 } 80 81 // ConsensusTransaction contains information about a transaction 82 type ConsensusTransaction struct { 83 SiacoinInputs map[string]types.SiacoinInput `json:"siacoininputs"` 84 SiacoinOutputs map[string]types.SiacoinOutput `json:"siacoinoutputs"` 85 FileContracts map[string]ConsensusFileContract `json:"filecontracts"` 86 FileContractRevisions map[string]ConsensusFileContractRevision `json:"filecontractrevisions"` 87 StorageProofs map[string]types.StorageProof `json:"storageproofs"` 88 SiafundInputs map[string]types.SiafundInput `json:"siafundinputs"` 89 SiafundOutputs map[string]types.SiafundOutput `json:"siafundoutputs"` 90 MinerFees map[string]types.Currency `json:"minerfees"` 91 ArbitraryData [][]byte `json:"arbitrarydata"` 92 TransactionSignatures map[string]types.TransactionSignature `json:"transactionsignatures"` 93 } 94 95 // ConsensusBlock is the object returned by a GET request to 96 // /consensus/block. 97 type ConsensusBlock struct { 98 BlockID types.BlockID `json:"id"` 99 BlockHeight types.BlockHeight `json:"blockheight"` 100 BlockHeader types.BlockHeader `json:"blockheader"` 101 Target types.Target `json:"target"` 102 Difficulty types.Currency `json:"difficulty"` 103 TotalCoins types.Currency `json:"totalcoins"` 104 EstimatedHashrate types.Currency `json:"estimatedhashrate"` 105 106 MinerPayouts map[string]types.SiacoinOutput `json:"minerpayouts"` 107 Transactions map[string]ConsensusTransaction `json:"transactions"` 108 } 109 110 // Scods is a list of Siacoin output diffs 111 type Scods struct { 112 Scods []modules.SiacoinOutputDiff `json:"scods"` 113 } 114 115 // ConsensusBlocksGet contains all fields of a types.Block and additional 116 // fields for ID and Height. 117 type ConsensusBlocksGet struct { 118 ID types.BlockID `json:"id"` 119 Height types.BlockHeight `json:"height"` 120 ParentID types.BlockID `json:"parentid"` 121 Nonce types.BlockNonce `json:"nonce"` 122 Timestamp types.Timestamp `json:"timestamp"` 123 MinerPayouts []types.SiacoinOutput `json:"minerpayouts"` 124 Transactions []ConsensusBlocksGetTxn `json:"transactions"` 125 } 126 127 // ConsensusBlocksGetTxn contains all fields of a types.Transaction and an 128 // additional ID field. 129 type ConsensusBlocksGetTxn struct { 130 ID types.TransactionID `json:"id"` 131 SiacoinInputs []types.SiacoinInput `json:"siacoininputs"` 132 SiacoinOutputs []ConsensusBlocksGetSiacoinOutput `json:"siacoinoutputs"` 133 FileContracts []ConsensusBlocksGetFileContract `json:"filecontracts"` 134 FileContractRevisions []types.FileContractRevision `json:"filecontractrevisions"` 135 StorageProofs []types.StorageProof `json:"storageproofs"` 136 SiafundInputs []types.SiafundInput `json:"siafundinputs"` 137 SiafundOutputs []ConsensusBlocksGetSiafundOutput `json:"siafundoutputs"` 138 MinerFees []types.Currency `json:"minerfees"` 139 ArbitraryData [][]byte `json:"arbitrarydata"` 140 TransactionSignatures []types.TransactionSignature `json:"transactionsignatures"` 141 } 142 143 // ConsensusBlocksGetFileContract contains all fields of a types.FileContract 144 // and an additional ID field. 145 type ConsensusBlocksGetFileContract struct { 146 ID types.FileContractID `json:"id"` 147 FileSize uint64 `json:"filesize"` 148 FileMerkleRoot crypto.Hash `json:"filemerkleroot"` 149 WindowStart types.BlockHeight `json:"windowstart"` 150 WindowEnd types.BlockHeight `json:"windowend"` 151 Payout types.Currency `json:"payout"` 152 ValidProofOutputs []ConsensusBlocksGetSiacoinOutput `json:"validproofoutputs"` 153 MissedProofOutputs []ConsensusBlocksGetSiacoinOutput `json:"missedproofoutputs"` 154 UnlockHash types.UnlockHash `json:"unlockhash"` 155 RevisionNumber uint64 `json:"revisionnumber"` 156 } 157 158 // ConsensusBlocksGetSiacoinOutput contains all fields of a types.SiacoinOutput 159 // and an additional ID field. 160 type ConsensusBlocksGetSiacoinOutput struct { 161 ID types.SiacoinOutputID `json:"id"` 162 Value types.Currency `json:"value"` 163 UnlockHash types.UnlockHash `json:"unlockhash"` 164 } 165 166 // ConsensusBlocksGetSiafundOutput contains all fields of a types.SiafundOutput 167 // and an additional ID field. 168 type ConsensusBlocksGetSiafundOutput struct { 169 ID types.SiafundOutputID `json:"id"` 170 Value types.Currency `json:"value"` 171 UnlockHash types.UnlockHash `json:"unlockhash"` 172 } 173 174 // ConsensusBlocksGetFromBlock is a helper method that uses a types.Block and 175 // types.BlockHeight to create a ConsensusBlocksGet object. 176 func consensusBlocksGetFromBlock(b types.Block, h types.BlockHeight) ConsensusBlocksGet { 177 txns := make([]ConsensusBlocksGetTxn, 0, len(b.Transactions)) 178 for _, t := range b.Transactions { 179 // Get the transaction's SiacoinOutputs. 180 scos := make([]ConsensusBlocksGetSiacoinOutput, 0, len(t.SiacoinOutputs)) 181 for i, sco := range t.SiacoinOutputs { 182 scos = append(scos, ConsensusBlocksGetSiacoinOutput{ 183 ID: t.SiacoinOutputID(uint64(i)), 184 Value: sco.Value, 185 UnlockHash: sco.UnlockHash, 186 }) 187 } 188 // Get the transaction's SiafundOutputs. 189 sfos := make([]ConsensusBlocksGetSiafundOutput, 0, len(t.SiafundOutputs)) 190 for i, sfo := range t.SiafundOutputs { 191 sfos = append(sfos, ConsensusBlocksGetSiafundOutput{ 192 ID: t.SiafundOutputID(uint64(i)), 193 Value: sfo.Value, 194 UnlockHash: sfo.UnlockHash, 195 }) 196 } 197 // Get the transaction's FileContracts. 198 fcos := make([]ConsensusBlocksGetFileContract, 0, len(t.FileContracts)) 199 for i, fc := range t.FileContracts { 200 // Get the FileContract's valid proof outputs. 201 fcid := t.FileContractID(uint64(i)) 202 vpos := make([]ConsensusBlocksGetSiacoinOutput, 0, len(fc.ValidProofOutputs)) 203 for j, vpo := range fc.ValidProofOutputs { 204 vpos = append(vpos, ConsensusBlocksGetSiacoinOutput{ 205 ID: fcid.StorageProofOutputID(types.ProofValid, uint64(j)), 206 Value: vpo.Value, 207 UnlockHash: vpo.UnlockHash, 208 }) 209 } 210 // Get the FileContract's missed proof outputs. 211 mpos := make([]ConsensusBlocksGetSiacoinOutput, 0, len(fc.MissedProofOutputs)) 212 for j, mpo := range fc.MissedProofOutputs { 213 mpos = append(mpos, ConsensusBlocksGetSiacoinOutput{ 214 ID: fcid.StorageProofOutputID(types.ProofMissed, uint64(j)), 215 Value: mpo.Value, 216 UnlockHash: mpo.UnlockHash, 217 }) 218 } 219 fcos = append(fcos, ConsensusBlocksGetFileContract{ 220 ID: fcid, 221 FileSize: fc.FileSize, 222 FileMerkleRoot: fc.FileMerkleRoot, 223 WindowStart: fc.WindowStart, 224 WindowEnd: fc.WindowEnd, 225 Payout: fc.Payout, 226 ValidProofOutputs: vpos, 227 MissedProofOutputs: mpos, 228 UnlockHash: fc.UnlockHash, 229 RevisionNumber: fc.RevisionNumber, 230 }) 231 } 232 txns = append(txns, ConsensusBlocksGetTxn{ 233 ID: t.ID(), 234 SiacoinInputs: t.SiacoinInputs, 235 SiacoinOutputs: scos, 236 FileContracts: fcos, 237 FileContractRevisions: t.FileContractRevisions, 238 StorageProofs: t.StorageProofs, 239 SiafundInputs: t.SiafundInputs, 240 SiafundOutputs: sfos, 241 MinerFees: t.MinerFees, 242 ArbitraryData: t.ArbitraryData, 243 TransactionSignatures: t.TransactionSignatures, 244 }) 245 } 246 return ConsensusBlocksGet{ 247 ID: b.ID(), 248 Height: h, 249 ParentID: b.ParentID, 250 Nonce: b.Nonce, 251 Timestamp: b.Timestamp, 252 MinerPayouts: b.MinerPayouts, 253 Transactions: txns, 254 } 255 } 256 257 // consensusHandler handles the API calls to /consensus. 258 func (api *API) consensusHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 259 cbid := api.cs.CurrentBlock().ID() 260 currentTarget, _ := api.cs.ChildTarget(cbid) 261 WriteJSON(w, ConsensusGET{ 262 Synced: api.cs.Synced(), 263 Height: api.cs.Height(), 264 CurrentBlock: cbid, 265 Target: currentTarget, 266 Difficulty: currentTarget.Difficulty(), 267 268 BlockFrequency: types.BlockFrequency, 269 BlockSizeLimit: types.BlockSizeLimit, 270 ExtremeFutureThreshold: types.ExtremeFutureThreshold, 271 FutureThreshold: types.FutureThreshold, 272 GenesisTimestamp: types.GenesisTimestamp, 273 MaturityDelay: types.MaturityDelay, 274 MedianTimestampWindow: types.MedianTimestampWindow, 275 SiafundCount: types.SiafundCount, 276 SiafundPortion: types.SiafundPortion, 277 278 InitialCoinbase: types.InitialCoinbase, 279 MinimumCoinbase: types.MinimumCoinbase, 280 281 RootTarget: types.RootTarget, 282 RootDepth: types.RootDepth, 283 284 SiacoinPrecision: types.SiacoinPrecision, 285 }) 286 } 287 288 // consensusBlocksIDHandler handles the API calls to /consensus/blocks 289 // endpoint. 290 func (api *API) consensusBlocksHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 291 // Get query params and check them. 292 id, height := req.FormValue("id"), req.FormValue("height") 293 if id != "" && height != "" { 294 WriteError(w, Error{"can't specify both id and height"}, http.StatusBadRequest) 295 } 296 if id == "" && height == "" { 297 WriteError(w, Error{"either id or height has to be provided"}, http.StatusBadRequest) 298 } 299 300 var b types.Block 301 var h types.BlockHeight 302 var exists bool 303 304 // Handle request by id 305 if id != "" { 306 var bid types.BlockID 307 if err := bid.LoadString(id); err != nil { 308 WriteError(w, Error{"failed to unmarshal blockid"}, http.StatusBadRequest) 309 return 310 } 311 b, h, exists = api.cs.BlockByID(bid) 312 } 313 // Handle request by height 314 if height != "" { 315 if _, err := fmt.Sscan(height, &h); err != nil { 316 WriteError(w, Error{"failed to parse block height"}, http.StatusBadRequest) 317 return 318 } 319 b, exists = api.cs.BlockAtHeight(types.BlockHeight(h)) 320 } 321 // Check if block was found 322 if !exists { 323 WriteError(w, Error{"block doesn't exist"}, http.StatusBadRequest) 324 return 325 } 326 // Write response 327 WriteJSON(w, consensusBlocksGetFromBlock(b, h)) 328 } 329 330 // consensusValidateTransactionsetHandler handles the API calls to 331 // /consensus/validate/transactionset. 332 func (api *API) consensusValidateTransactionsetHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 333 var txnset []types.Transaction 334 err := json.NewDecoder(req.Body).Decode(&txnset) 335 if err != nil { 336 WriteError(w, Error{"could not decode transaction set: " + err.Error()}, http.StatusBadRequest) 337 return 338 } 339 _, err = api.cs.TryTransactionSet(txnset) 340 if err != nil { 341 WriteError(w, Error{"transaction set validation failed: " + err.Error()}, http.StatusBadRequest) 342 return 343 } 344 WriteSuccess(w) 345 } 346 347 // consensusBlocksHandler handles API calls to /consensus/blocks/:height. 348 func (api *API) consensusBlocksHandlerSanasol(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 349 // Parse the height that's being requested. 350 var height types.BlockHeight 351 _, err := fmt.Sscan(ps.ByName("height"), &height) 352 if err != nil { 353 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 354 return 355 } 356 357 // Fetch and return the explorer block. 358 block, exists := api.cs.BlockAtHeight(height) 359 if !exists { 360 WriteError(w, Error{"no block found at input height in call to /consensus/blocks"}, http.StatusBadRequest) 361 return 362 } 363 364 // Catalog the new miner payouts. 365 minerpayouts := map[string]types.SiacoinOutput{} 366 for j, payout := range block.MinerPayouts { 367 scoid := block.MinerPayoutID(uint64(j)).String() 368 minerpayouts[scoid] = payout 369 } 370 371 var ct = map[string]ConsensusTransaction{} 372 373 // Update cumulative stats for applied transactions. 374 for _, txn := range block.Transactions { 375 // Add the transaction to the list of active transactions. 376 txid := txn.ID() 377 378 inputs := map[string]types.SiacoinInput{} 379 for _, sci := range txn.SiacoinInputs { 380 inputs[sci.ParentID.String()] = sci 381 } 382 383 outputs := map[string]types.SiacoinOutput{} 384 for j, sco := range txn.SiacoinOutputs { 385 scoid := txn.SiacoinOutputID(uint64(j)).String() 386 outputs[scoid] = sco 387 } 388 389 filecontracts := map[string]ConsensusFileContract{} 390 for k, fc := range txn.FileContracts { 391 fcid := txn.FileContractID(uint64(k)) 392 393 validproofs := map[string]types.SiacoinOutput{} 394 for l, sco := range fc.ValidProofOutputs { 395 scoid := fcid.StorageProofOutputID(types.ProofValid, uint64(l)).String() 396 validproofs[scoid] = sco 397 } 398 399 missedproofs := map[string]types.SiacoinOutput{} 400 for l, sco := range fc.MissedProofOutputs { 401 scoid := fcid.StorageProofOutputID(types.ProofMissed, uint64(l)).String() 402 missedproofs[scoid] = sco 403 } 404 405 filecontracts[fcid.String()] = ConsensusFileContract{ 406 FileSize: fc.FileSize, 407 FileMerkleRoot: fc.FileMerkleRoot, 408 WindowStart: fc.WindowStart, 409 WindowEnd: fc.WindowEnd, 410 Payout: fc.Payout, 411 412 ValidProofOutputs: validproofs, 413 MissedProofOutputs: missedproofs, 414 415 UnlockHash: fc.UnlockHash, 416 RevisionNumber: fc.RevisionNumber, 417 } 418 } 419 420 filecontractrevisions := map[string]ConsensusFileContractRevision{} 421 for _, fcr := range txn.FileContractRevisions { 422 validproofs := map[string]types.SiacoinOutput{} 423 for l, sco := range fcr.NewValidProofOutputs { 424 scoid := fcr.ParentID.StorageProofOutputID(types.ProofValid, uint64(l)).String() 425 validproofs[scoid] = sco 426 } 427 428 missedproofs := map[string]types.SiacoinOutput{} 429 for l, sco := range fcr.NewMissedProofOutputs { 430 scoid := fcr.ParentID.StorageProofOutputID(types.ProofMissed, uint64(l)).String() 431 missedproofs[scoid] = sco 432 } 433 434 filecontractrevisions[fcr.ParentID.String()] = ConsensusFileContractRevision{ 435 ParentID: fcr.ParentID, 436 UnlockConditions: fcr.UnlockConditions, 437 NewRevisionNumber: fcr.NewRevisionNumber, 438 439 NewFileSize: fcr.NewFileSize, 440 NewFileMerkleRoot: fcr.NewFileMerkleRoot, 441 NewWindowStart: fcr.NewWindowStart, 442 NewWindowEnd: fcr.NewWindowEnd, 443 444 NewValidProofOutputs: validproofs, 445 NewMissedProofOutputs: missedproofs, 446 447 NewUnlockHash: fcr.NewUnlockHash, 448 } 449 } 450 451 storageproofs := map[string]types.StorageProof{} 452 for _, sp := range txn.StorageProofs { 453 storageproofs[sp.ParentID.String()] = sp 454 } 455 456 sfinputs := map[string]types.SiafundInput{} 457 for _, sfi := range txn.SiafundInputs { 458 sfinputs[sfi.ParentID.String()] = sfi 459 } 460 461 sfoutputs := map[string]types.SiafundOutput{} 462 for k, sfo := range txn.SiafundOutputs { 463 sfoid := txn.SiafundOutputID(uint64(k)).String() 464 sfoutputs[sfoid] = sfo 465 } 466 467 ct[txid.String()] = ConsensusTransaction{ 468 SiacoinInputs: inputs, 469 SiacoinOutputs: outputs, 470 FileContracts: filecontracts, 471 FileContractRevisions: filecontractrevisions, 472 StorageProofs: storageproofs, 473 SiafundInputs: sfinputs, 474 SiafundOutputs: sfoutputs, 475 ArbitraryData: txn.ArbitraryData, 476 } 477 } 478 479 cbid := block.ID() 480 currentTarget, _ := api.cs.ChildTarget(cbid) 481 482 var estimatedHashrate types.Currency 483 var hashrateEstimationBlocks types.BlockHeight 484 // hashrateEstimationBlocks is the number of blocks that are used to 485 // estimate the current hashrate. 486 hashrateEstimationBlocks = 200 // 33 hours 487 if height > hashrateEstimationBlocks { 488 var totalDifficulty = currentTarget 489 var oldestTimestamp types.Timestamp 490 for i := types.BlockHeight(1); i < hashrateEstimationBlocks; i++ { 491 b, exists := api.cs.BlockAtHeight(height - i) 492 if !exists { 493 panic(fmt.Sprint("ConsensusSet is missing block at height", height-hashrateEstimationBlocks)) 494 } 495 target, exists := api.cs.ChildTarget(b.ParentID) 496 if !exists { 497 panic(fmt.Sprint("ConsensusSet is missing target of known block", b.ParentID)) 498 } 499 totalDifficulty = totalDifficulty.AddDifficulties(target) 500 oldestTimestamp = b.Timestamp 501 } 502 secondsPassed := block.Timestamp - oldestTimestamp 503 estimatedHashrate = totalDifficulty.Difficulty().Div64(uint64(secondsPassed)) 504 } 505 506 WriteJSON(w, ConsensusBlock{ 507 BlockID: block.ID(), 508 BlockHeight: height, 509 BlockHeader: block.Header(), 510 Transactions: ct, 511 MinerPayouts: minerpayouts, 512 Difficulty: currentTarget.Difficulty(), 513 Target: currentTarget, 514 TotalCoins: types.CalculateNumSiacoins(height), 515 EstimatedHashrate: estimatedHashrate, 516 }) 517 } 518 519 // consensusBlocksHandler handles API calls to /consensus/blocks/:height. 520 func (api *API) consensusFutureBlocksHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 521 // Parse the height that's being requested. 522 var height types.BlockHeight 523 _, err := fmt.Sscan(ps.ByName("height"), &height) 524 if err != nil { 525 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 526 return 527 } 528 529 var ( 530 prefixDSCO = []byte("dsco_") 531 ) 532 533 bucketID := append(prefixDSCO, encoding.Marshal(height)...) 534 var scods []modules.SiacoinOutputDiff 535 536 _ = api.cs.Db().View(func(tx *bolt.Tx) error { 537 tx.Bucket(bucketID).ForEach(func(idBytes, scoBytes []byte) error { 538 // Decode the key-value pair into an id and a siacoin output. 539 var id types.SiacoinOutputID 540 var sco types.SiacoinOutput 541 copy(id[:], idBytes) 542 _ = encoding.Unmarshal(scoBytes, &sco) 543 544 // Add the output to the ConsensusSet and record the diff in the 545 // blockNode. 546 scod := modules.SiacoinOutputDiff{ 547 Direction: modules.DiffApply, 548 ID: id, 549 SiacoinOutput: sco, 550 } 551 scods = append(scods, scod) 552 return nil 553 }) 554 return nil 555 }) 556 557 WriteJSON(w, scods) 558 }