// hostDeleteHandler deletes a file contract from the host. func (srv *Server) hostDeleteHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { hash, err := scanAddress(ps.ByName("filecontractid")) if err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } fcid := types.FileContractID(hash) err = srv.host.DeleteContract(fcid) if err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } writeSuccess(w) }
// explorerHashHandler handles GET requests to /explorer/hash/:hash. func (srv *Server) explorerHashHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { // The hash is scanned as an address, because an address can be typecast to // all other necessary types, and will correctly decode hashes whether or // not they have a checksum. hash, err := scanAddress(ps.ByName("hash")) if err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } // Try the hash as a block id. block, height, exists := srv.explorer.Block(types.BlockID(hash)) if exists { writeJSON(w, ExplorerHashGET{ HashType: "blockid", Block: srv.buildExplorerBlock(height, block), }) return } // Try the hash as a transaction id. block, height, exists = srv.explorer.Transaction(types.TransactionID(hash)) if exists { var txn types.Transaction for _, t := range block.Transactions { if t.ID() == types.TransactionID(hash) { txn = t } } writeJSON(w, ExplorerHashGET{ HashType: "transactionid", Transaction: srv.buildExplorerTransaction(height, block.ID(), txn), }) return } // Try the hash as a siacoin output id. txids := srv.explorer.SiacoinOutputID(types.SiacoinOutputID(hash)) if len(txids) != 0 { txns, blocks := srv.buildTransactionSet(txids) writeJSON(w, ExplorerHashGET{ HashType: "siacoinoutputid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as a file contract id. txids = srv.explorer.FileContractID(types.FileContractID(hash)) if len(txids) != 0 { txns, blocks := srv.buildTransactionSet(txids) writeJSON(w, ExplorerHashGET{ HashType: "filecontractid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as a siafund output id. txids = srv.explorer.SiafundOutputID(types.SiafundOutputID(hash)) if len(txids) != 0 { txns, blocks := srv.buildTransactionSet(txids) writeJSON(w, ExplorerHashGET{ HashType: "siafundoutputid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as an unlock hash. Unlock hash is checked last because // unlock hashes do not have collision-free guarantees. Someone can create // an unlock hash that collides with another object id. They will not be // able to use the unlock hash, but they can disrupt the explorer. This is // handled by checking the unlock hash last. Anyone intentionally creating // a colliding unlock hash (such a collision can only happen if done // intentionally) will be unable to find their unlock hash in the // blockchain through the explorer hash lookup. txids = srv.explorer.UnlockHash(types.UnlockHash(hash)) if len(txids) != 0 { txns, blocks := srv.buildTransactionSet(txids) writeJSON(w, ExplorerHashGET{ HashType: "unlockhash", Blocks: blocks, Transactions: txns, }) return } // Hash not found, return an error. writeError(w, "unrecognized hash used as input to /explorer/hash", http.StatusBadRequest) }
// consensusSetHash returns the Merkle root of the current state of consensus. func (cs *ConsensusSet) consensusSetHash() crypto.Hash { // Check is too slow to be done on a full node. if build.Release == "standard" { return crypto.Hash{} } // Items of interest: // 1. genesis block // 3. current height // 4. current target // 5. current depth // 6. current path + diffs // (7) earliest allowed timestamp of next block // 8. unspent siacoin outputs, sorted by id. // 9. open file contracts, sorted by id. // 10. unspent siafund outputs, sorted by id. // 11. delayed siacoin outputs, sorted by height, then sorted by id. // 12. siafund pool // Create a slice of hashes representing all items of interest. tree := crypto.NewTree() tree.PushObject(cs.blockRoot.Block) tree.PushObject(cs.height()) tree.PushObject(cs.currentProcessedBlock().ChildTarget) tree.PushObject(cs.currentProcessedBlock().Depth) // tree.PushObject(cs.earliestChildTimestamp(cs.currentProcessedBlock())) // Add all the blocks in the current path TODO: along with their diffs. for i := 0; i < int(cs.db.pathHeight()); i++ { tree.PushObject(cs.db.getPath(types.BlockHeight(i))) } // Add all of the siacoin outputs, sorted by id. var openSiacoinOutputs crypto.HashSlice cs.db.forEachSiacoinOutputs(func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { openSiacoinOutputs = append(openSiacoinOutputs, crypto.Hash(scoid)) }) sort.Sort(openSiacoinOutputs) for _, id := range openSiacoinOutputs { sco := cs.db.getSiacoinOutputs(types.SiacoinOutputID(id)) tree.PushObject(id) tree.PushObject(sco) } // Add all of the file contracts, sorted by id. var openFileContracts crypto.HashSlice cs.db.forEachFileContracts(func(fcid types.FileContractID, fc types.FileContract) { openFileContracts = append(openFileContracts, crypto.Hash(fcid)) }) sort.Sort(openFileContracts) for _, id := range openFileContracts { // Sanity Check - file contract should exist. fc := cs.db.getFileContracts(types.FileContractID(id)) tree.PushObject(id) tree.PushObject(fc) } // Add all of the siafund outputs, sorted by id. var openSiafundOutputs crypto.HashSlice cs.db.forEachSiafundOutputs(func(sfoid types.SiafundOutputID, sfo types.SiafundOutput) { openSiafundOutputs = append(openSiafundOutputs, crypto.Hash(sfoid)) }) sort.Sort(openSiafundOutputs) for _, id := range openSiafundOutputs { sco := cs.db.getSiafundOutputs(types.SiafundOutputID(id)) tree.PushObject(id) tree.PushObject(sco) } // Get the set of delayed siacoin outputs, sorted by maturity height then // sorted by id and add them. for i := cs.height() + 1; i <= cs.height()+types.MaturityDelay; i++ { var delayedSiacoinOutputs crypto.HashSlice if cs.db.inDelayedSiacoinOutputs(i) { cs.db.forEachDelayedSiacoinOutputsHeight(i, func(id types.SiacoinOutputID, output types.SiacoinOutput) { delayedSiacoinOutputs = append(delayedSiacoinOutputs, crypto.Hash(id)) }) } sort.Sort(delayedSiacoinOutputs) for _, delayedSiacoinOutputID := range delayedSiacoinOutputs { delayedSiacoinOutput := cs.db.getDelayedSiacoinOutputs(i, types.SiacoinOutputID(delayedSiacoinOutputID)) tree.PushObject(delayedSiacoinOutput) tree.PushObject(delayedSiacoinOutputID) } } // Add the siafund pool var siafundPool types.Currency err := cs.db.Update(func(tx *bolt.Tx) error { siafundPool = getSiafundPool(tx) return nil }) if err != nil { panic(err) } tree.PushObject(siafundPool) return tree.Root() }
// TestApplyMissedStorageProof probes the applyMissedStorageProof method of the // consensus set. func TestApplyMissedStorageProof(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplyMissedStorageProof") if err != nil { t.Fatal(err) } defer cst.closeCst() // Create a block node. pb := new(processedBlock) pb.Height = cst.cs.height() // Create a file contract that's expiring and has 1 missed proof output. expiringFC := types.FileContract{ Payout: types.NewCurrency64(300e3), WindowEnd: pb.Height, MissedProofOutputs: []types.SiacoinOutput{{Value: types.NewCurrency64(290e3)}}, } // Assign the contract a 0-id. cst.cs.db.addFileContracts(types.FileContractID{}, expiringFC) cst.cs.db.addFCExpirations(pb.Height) cst.cs.db.addFCExpirationsHeight(pb.Height, types.FileContractID{}) cst.cs.applyMissedStorageProof(pb, types.FileContractID{}) exists := cst.cs.db.inFileContracts(types.FileContractID{}) if exists { t.Error("file contract was not consumed in missed storage proof") } spoid := types.FileContractID{}.StorageProofOutputID(types.ProofMissed, 0) exists = cst.cs.db.inDelayedSiacoinOutputsHeight(pb.Height+types.MaturityDelay, spoid) if !exists { t.Error("missed proof output was never created") } exists = cst.cs.db.inSiacoinOutputs(spoid) if exists { t.Error("storage proof output made it into the siacoin output set") } exists = cst.cs.db.inFileContracts(types.FileContractID{}) if exists { t.Error("file contract remains after expiration") } // Trigger the debug panics. // not exist. defer func() { r := recover() if r != errNilItem { t.Error(r) } }() defer func() { r := recover() if r != errNilItem { t.Error(r) } // Trigger errMissingFileContract cst.cs.applyMissedStorageProof(pb, types.FileContractID(spoid)) }() defer func() { r := recover() if r != errNilItem { t.Error(r) } // Trigger errStorageProofTiming expiringFC.WindowEnd = 0 cst.cs.applyMissedStorageProof(pb, types.FileContractID{}) }() defer func() { r := recover() if r != errNilItem { t.Error(r) } // Trigger errPayoutsAlreadyPaid from siacoin outputs. cst.cs.db.rmDelayedSiacoinOutputsHeight(pb.Height+types.MaturityDelay, spoid) cst.cs.db.addSiacoinOutputs(spoid, types.SiacoinOutput{}) cst.cs.applyMissedStorageProof(pb, types.FileContractID{}) }() // Trigger errPayoutsAlreadyPaid from delayed outputs. cst.cs.db.rmFileContracts(types.FileContractID{}) cst.cs.db.addFileContracts(types.FileContractID{}, expiringFC) cst.cs.db.addDelayedSiacoinOutputsHeight(pb.Height+types.MaturityDelay, spoid, types.SiacoinOutput{}) cst.cs.applyMissedStorageProof(pb, types.FileContractID{}) }
// explorerHashHandler handles GET requests to /explorer/hash/:hash. func (api *API) explorerHashHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { // Scan the hash as a hash. If that fails, try scanning the hash as an // address. hash, err := scanHash(ps.ByName("hash")) if err != nil { addr, err := scanAddress(ps.ByName("hash")) if err != nil { WriteError(w, Error{err.Error()}, http.StatusBadRequest) return } hash = crypto.Hash(addr) } // TODO: lookups on the zero hash are too expensive to allow. Need a // better way to handle this case. if hash == (crypto.Hash{}) { WriteError(w, Error{"can't lookup the empty unlock hash"}, http.StatusBadRequest) return } // Try the hash as a block id. block, height, exists := api.explorer.Block(types.BlockID(hash)) if exists { WriteJSON(w, ExplorerHashGET{ HashType: "blockid", Block: api.buildExplorerBlock(height, block), }) return } // Try the hash as a transaction id. block, height, exists = api.explorer.Transaction(types.TransactionID(hash)) if exists { var txn types.Transaction for _, t := range block.Transactions { if t.ID() == types.TransactionID(hash) { txn = t } } WriteJSON(w, ExplorerHashGET{ HashType: "transactionid", Transaction: api.buildExplorerTransaction(height, block.ID(), txn), }) return } // Try the hash as a siacoin output id. txids := api.explorer.SiacoinOutputID(types.SiacoinOutputID(hash)) if len(txids) != 0 { txns, blocks := api.buildTransactionSet(txids) WriteJSON(w, ExplorerHashGET{ HashType: "siacoinoutputid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as a file contract id. txids = api.explorer.FileContractID(types.FileContractID(hash)) if len(txids) != 0 { txns, blocks := api.buildTransactionSet(txids) WriteJSON(w, ExplorerHashGET{ HashType: "filecontractid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as a siafund output id. txids = api.explorer.SiafundOutputID(types.SiafundOutputID(hash)) if len(txids) != 0 { txns, blocks := api.buildTransactionSet(txids) WriteJSON(w, ExplorerHashGET{ HashType: "siafundoutputid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as an unlock hash. Unlock hash is checked last because // unlock hashes do not have collision-free guarantees. Someone can create // an unlock hash that collides with another object id. They will not be // able to use the unlock hash, but they can disrupt the explorer. This is // handled by checking the unlock hash last. Anyone intentionally creating // a colliding unlock hash (such a collision can only happen if done // intentionally) will be unable to find their unlock hash in the // blockchain through the explorer hash lookup. txids = api.explorer.UnlockHash(types.UnlockHash(hash)) if len(txids) != 0 { txns, blocks := api.buildTransactionSet(txids) WriteJSON(w, ExplorerHashGET{ HashType: "unlockhash", Blocks: blocks, Transactions: txns, }) return } // Hash not found, return an error. WriteError(w, Error{"unrecognized hash used as input to /explorer/hash"}, http.StatusBadRequest) }
// consensusSetHash returns the Merkle root of the current state of consensus. func (cs *State) consensusSetHash() crypto.Hash { // Items of interest: // 1. genesis block // 3. current height // 4. current target // 5. current depth // 6. earliest allowed timestamp of next block // 7. current path, ordered by height. // 8. unspent siacoin outputs, sorted by id. // 9. open file contracts, sorted by id. // 10. unspent siafund outputs, sorted by id. // 11. delayed siacoin outputs, sorted by height, then sorted by id. // TODO: Add the diff set ? // Create a slice of hashes representing all items of interest. tree := crypto.NewTree() tree.PushObject(cs.blockRoot.block) tree.PushObject(cs.height()) tree.PushObject(cs.currentBlockNode().childTarget) tree.PushObject(cs.currentBlockNode().depth) tree.PushObject(cs.currentBlockNode().earliestChildTimestamp()) // Add all the blocks in the current path. for i := 0; i < len(cs.currentPath); i++ { tree.PushObject(cs.currentPath[types.BlockHeight(i)]) } // Add all of the siacoin outputs, sorted by id. var openSiacoinOutputs crypto.HashSlice for siacoinOutputID, _ := range cs.siacoinOutputs { openSiacoinOutputs = append(openSiacoinOutputs, crypto.Hash(siacoinOutputID)) } sort.Sort(openSiacoinOutputs) for _, id := range openSiacoinOutputs { sco, exists := cs.siacoinOutputs[types.SiacoinOutputID(id)] if !exists { panic("trying to push nonexistent siacoin output") } tree.PushObject(id) tree.PushObject(sco) } // Add all of the file contracts, sorted by id. var openFileContracts crypto.HashSlice for fileContractID, _ := range cs.fileContracts { openFileContracts = append(openFileContracts, crypto.Hash(fileContractID)) } sort.Sort(openFileContracts) for _, id := range openFileContracts { // Sanity Check - file contract should exist. fc, exists := cs.fileContracts[types.FileContractID(id)] if !exists { panic("trying to push a nonexistent file contract") } tree.PushObject(id) tree.PushObject(fc) } // Add all of the siafund outputs, sorted by id. var openSiafundOutputs crypto.HashSlice for siafundOutputID, _ := range cs.siafundOutputs { openSiafundOutputs = append(openSiafundOutputs, crypto.Hash(siafundOutputID)) } sort.Sort(openSiafundOutputs) for _, id := range openSiafundOutputs { sco, exists := cs.siafundOutputs[types.SiafundOutputID(id)] if !exists { panic("trying to push nonexistent siafund output") } tree.PushObject(id) tree.PushObject(sco) } // Get the set of delayed siacoin outputs, sorted by maturity height then // sorted by id and add them. for i := cs.height() + 1; i <= cs.height()+types.MaturityDelay; i++ { var delayedSiacoinOutputs crypto.HashSlice for id := range cs.delayedSiacoinOutputs[i] { delayedSiacoinOutputs = append(delayedSiacoinOutputs, crypto.Hash(id)) } sort.Sort(delayedSiacoinOutputs) for _, delayedSiacoinOutputID := range delayedSiacoinOutputs { delayedSiacoinOutput, exists := cs.delayedSiacoinOutputs[i][types.SiacoinOutputID(delayedSiacoinOutputID)] if !exists { panic("trying to push nonexistent delayed siacoin output") } tree.PushObject(delayedSiacoinOutput) tree.PushObject(delayedSiacoinOutputID) } } return tree.Root() }