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  }