gitlab.com/jokerrs1/Sia@v1.3.2/node/api/explorer.go (about) 1 package api 2 3 import ( 4 "fmt" 5 "net/http" 6 7 "github.com/NebulousLabs/Sia/build" 8 "github.com/NebulousLabs/Sia/crypto" 9 "github.com/NebulousLabs/Sia/modules" 10 "github.com/NebulousLabs/Sia/types" 11 12 "github.com/julienschmidt/httprouter" 13 ) 14 15 type ( 16 // ExplorerBlock is a block with some extra information such as the id and 17 // height. This information is provided for programs that may not be 18 // complex enough to compute the ID on their own. 19 ExplorerBlock struct { 20 MinerPayoutIDs []types.SiacoinOutputID `json:"minerpayoutids"` 21 Transactions []ExplorerTransaction `json:"transactions"` 22 RawBlock types.Block `json:"rawblock"` 23 24 modules.BlockFacts 25 } 26 27 // ExplorerTransaction is a transcation with some extra information such as 28 // the parent block. This information is provided for programs that may not 29 // be complex enough to compute the extra information on their own. 30 ExplorerTransaction struct { 31 ID types.TransactionID `json:"id"` 32 Height types.BlockHeight `json:"height"` 33 Parent types.BlockID `json:"parent"` 34 RawTransaction types.Transaction `json:"rawtransaction"` 35 36 SiacoinInputOutputs []types.SiacoinOutput `json:"siacoininputoutputs"` // the outputs being spent 37 SiacoinOutputIDs []types.SiacoinOutputID `json:"siacoinoutputids"` 38 FileContractIDs []types.FileContractID `json:"filecontractids"` 39 FileContractValidProofOutputIDs [][]types.SiacoinOutputID `json:"filecontractvalidproofoutputids"` // outer array is per-contract 40 FileContractMissedProofOutputIDs [][]types.SiacoinOutputID `json:"filecontractmissedproofoutputids"` // outer array is per-contract 41 FileContractRevisionValidProofOutputIDs [][]types.SiacoinOutputID `json:"filecontractrevisionvalidproofoutputids"` // outer array is per-revision 42 FileContractRevisionMissedProofOutputIDs [][]types.SiacoinOutputID `json:"filecontractrevisionmissedproofoutputids"` // outer array is per-revision 43 StorageProofOutputIDs [][]types.SiacoinOutputID `json:"storageproofoutputids"` // outer array is per-payout 44 StorageProofOutputs [][]types.SiacoinOutput `json:"storageproofoutputs"` // outer array is per-payout 45 SiafundInputOutputs []types.SiafundOutput `json:"siafundinputoutputs"` // the outputs being spent 46 SiafundOutputIDs []types.SiafundOutputID `json:"siafundoutputids"` 47 SiafundClaimOutputIDs []types.SiacoinOutputID `json:"siafundclaimoutputids"` 48 } 49 50 // ExplorerGET is the object returned as a response to a GET request to 51 // /explorer. 52 ExplorerGET struct { 53 modules.BlockFacts 54 } 55 56 // ExplorerBlockGET is the object returned by a GET request to 57 // /explorer/block. 58 ExplorerBlockGET struct { 59 Block ExplorerBlock `json:"block"` 60 } 61 62 // ExplorerHashGET is the object returned as a response to a GET request to 63 // /explorer/hash. The HashType will indicate whether the hash corresponds 64 // to a block id, a transaction id, a siacoin output id, a file contract 65 // id, or a siafund output id. In the case of a block id, 'Block' will be 66 // filled out and all the rest of the fields will be blank. In the case of 67 // a transaction id, 'Transaction' will be filled out and all the rest of 68 // the fields will be blank. For everything else, 'Transactions' and 69 // 'Blocks' will/may be filled out and everything else will be blank. 70 ExplorerHashGET struct { 71 HashType string `json:"hashtype"` 72 Block ExplorerBlock `json:"block"` 73 Blocks []ExplorerBlock `json:"blocks"` 74 Transaction ExplorerTransaction `json:"transaction"` 75 Transactions []ExplorerTransaction `json:"transactions"` 76 } 77 ) 78 79 // buildExplorerTransaction takes a transaction and the height + id of the 80 // block it appears in an uses that to build an explorer transaction. 81 func (api *API) buildExplorerTransaction(height types.BlockHeight, parent types.BlockID, txn types.Transaction) (et ExplorerTransaction) { 82 // Get the header information for the transaction. 83 et.ID = txn.ID() 84 et.Height = height 85 et.Parent = parent 86 et.RawTransaction = txn 87 88 // Add the siacoin outputs that correspond with each siacoin input. 89 for _, sci := range txn.SiacoinInputs { 90 sco, exists := api.explorer.SiacoinOutput(sci.ParentID) 91 if build.DEBUG && !exists { 92 panic("could not find corresponding siacoin output") 93 } 94 et.SiacoinInputOutputs = append(et.SiacoinInputOutputs, sco) 95 } 96 97 for i := range txn.SiacoinOutputs { 98 et.SiacoinOutputIDs = append(et.SiacoinOutputIDs, txn.SiacoinOutputID(uint64(i))) 99 } 100 101 // Add all of the valid and missed proof ids as extra data to the file 102 // contracts. 103 for i, fc := range txn.FileContracts { 104 fcid := txn.FileContractID(uint64(i)) 105 var fcvpoids []types.SiacoinOutputID 106 var fcmpoids []types.SiacoinOutputID 107 for j := range fc.ValidProofOutputs { 108 fcvpoids = append(fcvpoids, fcid.StorageProofOutputID(types.ProofValid, uint64(j))) 109 } 110 for j := range fc.MissedProofOutputs { 111 fcmpoids = append(fcmpoids, fcid.StorageProofOutputID(types.ProofMissed, uint64(j))) 112 } 113 et.FileContractIDs = append(et.FileContractIDs, fcid) 114 et.FileContractValidProofOutputIDs = append(et.FileContractValidProofOutputIDs, fcvpoids) 115 et.FileContractMissedProofOutputIDs = append(et.FileContractMissedProofOutputIDs, fcmpoids) 116 } 117 118 // Add all of the valid and missed proof ids as extra data to the file 119 // contract revisions. 120 for _, fcr := range txn.FileContractRevisions { 121 var fcrvpoids []types.SiacoinOutputID 122 var fcrmpoids []types.SiacoinOutputID 123 for j := range fcr.NewValidProofOutputs { 124 fcrvpoids = append(fcrvpoids, fcr.ParentID.StorageProofOutputID(types.ProofValid, uint64(j))) 125 } 126 for j := range fcr.NewMissedProofOutputs { 127 fcrmpoids = append(fcrmpoids, fcr.ParentID.StorageProofOutputID(types.ProofMissed, uint64(j))) 128 } 129 et.FileContractValidProofOutputIDs = append(et.FileContractValidProofOutputIDs, fcrvpoids) 130 et.FileContractMissedProofOutputIDs = append(et.FileContractMissedProofOutputIDs, fcrmpoids) 131 } 132 133 // Add all of the output ids and outputs corresponding with each storage 134 // proof. 135 for _, sp := range txn.StorageProofs { 136 fileContract, fileContractRevisions, fileContractExists, _ := api.explorer.FileContractHistory(sp.ParentID) 137 if !fileContractExists && build.DEBUG { 138 panic("could not find a file contract connected with a storage proof") 139 } 140 var storageProofOutputs []types.SiacoinOutput 141 if len(fileContractRevisions) > 0 { 142 storageProofOutputs = fileContractRevisions[len(fileContractRevisions)-1].NewValidProofOutputs 143 } else { 144 storageProofOutputs = fileContract.ValidProofOutputs 145 } 146 var storageProofOutputIDs []types.SiacoinOutputID 147 for i := range storageProofOutputs { 148 storageProofOutputIDs = append(storageProofOutputIDs, sp.ParentID.StorageProofOutputID(types.ProofValid, uint64(i))) 149 } 150 et.StorageProofOutputIDs = append(et.StorageProofOutputIDs, storageProofOutputIDs) 151 et.StorageProofOutputs = append(et.StorageProofOutputs, storageProofOutputs) 152 } 153 154 // Add the siafund outputs that correspond to each siacoin input. 155 for _, sci := range txn.SiafundInputs { 156 sco, exists := api.explorer.SiafundOutput(sci.ParentID) 157 if build.DEBUG && !exists { 158 panic("could not find corresponding siafund output") 159 } 160 et.SiafundInputOutputs = append(et.SiafundInputOutputs, sco) 161 } 162 163 for i := range txn.SiafundOutputs { 164 et.SiafundOutputIDs = append(et.SiafundOutputIDs, txn.SiafundOutputID(uint64(i))) 165 } 166 167 for _, sfi := range txn.SiafundInputs { 168 et.SiafundClaimOutputIDs = append(et.SiafundClaimOutputIDs, sfi.ParentID.SiaClaimOutputID()) 169 } 170 return et 171 } 172 173 // buildExplorerBlock takes a block and its height and uses it to construct an 174 // explorer block. 175 func (api *API) buildExplorerBlock(height types.BlockHeight, block types.Block) ExplorerBlock { 176 var mpoids []types.SiacoinOutputID 177 for i := range block.MinerPayouts { 178 mpoids = append(mpoids, block.MinerPayoutID(uint64(i))) 179 } 180 181 var etxns []ExplorerTransaction 182 for _, txn := range block.Transactions { 183 etxns = append(etxns, api.buildExplorerTransaction(height, block.ID(), txn)) 184 } 185 186 facts, exists := api.explorer.BlockFacts(height) 187 if build.DEBUG && !exists { 188 panic("incorrect request to buildExplorerBlock - block does not exist") 189 } 190 191 return ExplorerBlock{ 192 MinerPayoutIDs: mpoids, 193 Transactions: etxns, 194 RawBlock: block, 195 196 BlockFacts: facts, 197 } 198 } 199 200 // explorerHandler handles API calls to /explorer/blocks/:height. 201 func (api *API) explorerBlocksHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 202 // Parse the height that's being requested. 203 var height types.BlockHeight 204 _, err := fmt.Sscan(ps.ByName("height"), &height) 205 if err != nil { 206 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 207 return 208 } 209 210 // Fetch and return the explorer block. 211 block, exists := api.cs.BlockAtHeight(height) 212 if !exists { 213 WriteError(w, Error{"no block found at input height in call to /explorer/block"}, http.StatusBadRequest) 214 return 215 } 216 WriteJSON(w, ExplorerBlockGET{ 217 Block: api.buildExplorerBlock(height, block), 218 }) 219 } 220 221 // buildTransactionSet returns the blocks and transactions that are associated 222 // with a set of transaction ids. 223 func (api *API) buildTransactionSet(txids []types.TransactionID) (txns []ExplorerTransaction, blocks []ExplorerBlock) { 224 for _, txid := range txids { 225 // Get the block containing the transaction - in the case of miner 226 // payouts, the block might be the transaction. 227 block, height, exists := api.explorer.Transaction(txid) 228 if !exists && build.DEBUG { 229 panic("explorer pointing to nonexistent txn") 230 } 231 232 // Check if the block is the transaction. 233 if types.TransactionID(block.ID()) == txid { 234 blocks = append(blocks, api.buildExplorerBlock(height, block)) 235 } else { 236 // Find the transaction within the block with the correct id. 237 for _, t := range block.Transactions { 238 if t.ID() == txid { 239 txns = append(txns, api.buildExplorerTransaction(height, block.ID(), t)) 240 break 241 } 242 } 243 } 244 } 245 return txns, blocks 246 } 247 248 // explorerHashHandler handles GET requests to /explorer/hash/:hash. 249 func (api *API) explorerHashHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 250 // Scan the hash as a hash. If that fails, try scanning the hash as an 251 // address. 252 hash, err := scanHash(ps.ByName("hash")) 253 if err != nil { 254 addr, err := scanAddress(ps.ByName("hash")) 255 if err != nil { 256 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 257 return 258 } 259 hash = crypto.Hash(addr) 260 } 261 262 // TODO: lookups on the zero hash are too expensive to allow. Need a 263 // better way to handle this case. 264 if hash == (crypto.Hash{}) { 265 WriteError(w, Error{"can't lookup the empty unlock hash"}, http.StatusBadRequest) 266 return 267 } 268 269 // Try the hash as a block id. 270 block, height, exists := api.explorer.Block(types.BlockID(hash)) 271 if exists { 272 WriteJSON(w, ExplorerHashGET{ 273 HashType: "blockid", 274 Block: api.buildExplorerBlock(height, block), 275 }) 276 return 277 } 278 279 // Try the hash as a transaction id. 280 block, height, exists = api.explorer.Transaction(types.TransactionID(hash)) 281 if exists { 282 var txn types.Transaction 283 for _, t := range block.Transactions { 284 if t.ID() == types.TransactionID(hash) { 285 txn = t 286 } 287 } 288 WriteJSON(w, ExplorerHashGET{ 289 HashType: "transactionid", 290 Transaction: api.buildExplorerTransaction(height, block.ID(), txn), 291 }) 292 return 293 } 294 295 // Try the hash as a siacoin output id. 296 txids := api.explorer.SiacoinOutputID(types.SiacoinOutputID(hash)) 297 if len(txids) != 0 { 298 txns, blocks := api.buildTransactionSet(txids) 299 WriteJSON(w, ExplorerHashGET{ 300 HashType: "siacoinoutputid", 301 Blocks: blocks, 302 Transactions: txns, 303 }) 304 return 305 } 306 307 // Try the hash as a file contract id. 308 txids = api.explorer.FileContractID(types.FileContractID(hash)) 309 if len(txids) != 0 { 310 txns, blocks := api.buildTransactionSet(txids) 311 WriteJSON(w, ExplorerHashGET{ 312 HashType: "filecontractid", 313 Blocks: blocks, 314 Transactions: txns, 315 }) 316 return 317 } 318 319 // Try the hash as a siafund output id. 320 txids = api.explorer.SiafundOutputID(types.SiafundOutputID(hash)) 321 if len(txids) != 0 { 322 txns, blocks := api.buildTransactionSet(txids) 323 WriteJSON(w, ExplorerHashGET{ 324 HashType: "siafundoutputid", 325 Blocks: blocks, 326 Transactions: txns, 327 }) 328 return 329 } 330 331 // Try the hash as an unlock hash. Unlock hash is checked last because 332 // unlock hashes do not have collision-free guarantees. Someone can create 333 // an unlock hash that collides with another object id. They will not be 334 // able to use the unlock hash, but they can disrupt the explorer. This is 335 // handled by checking the unlock hash last. Anyone intentionally creating 336 // a colliding unlock hash (such a collision can only happen if done 337 // intentionally) will be unable to find their unlock hash in the 338 // blockchain through the explorer hash lookup. 339 txids = api.explorer.UnlockHash(types.UnlockHash(hash)) 340 if len(txids) != 0 { 341 txns, blocks := api.buildTransactionSet(txids) 342 WriteJSON(w, ExplorerHashGET{ 343 HashType: "unlockhash", 344 Blocks: blocks, 345 Transactions: txns, 346 }) 347 return 348 } 349 350 // Hash not found, return an error. 351 WriteError(w, Error{"unrecognized hash used as input to /explorer/hash"}, http.StatusBadRequest) 352 } 353 354 // explorerHandler handles API calls to /explorer 355 func (api *API) explorerHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 356 facts := api.explorer.LatestBlockFacts() 357 WriteJSON(w, ExplorerGET{ 358 BlockFacts: facts, 359 }) 360 }