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