Skip to content

Commit

Permalink
stakeholder disapproved blocks page (#733)
Browse files Browse the repository at this point in the history
This adds a Stakeholder Disapproved Blocks page at "/rejects".

This also adds two SVG icons for the Side Chains and Disapproved
Blocks pages. Both are in the official decred identity colors as per
https://www.decred.org/brand/.

Implementation details: 
Add DisapprovedBlocks to explorerDataSource interface.
Add "rejects" template and mount on /rejects.
Implement DisapprovedBlocks handler.
Clean up many comments in explorerroutes.go.
Remove extra span in sidechains.tmpl.
  • Loading branch information
chappjc committed Oct 10, 2018
1 parent 6580ff2 commit 19953f6
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 33 deletions.
6 changes: 6 additions & 0 deletions db/dcrpg/internal/blockstmts.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ const (
JOIN block_chain ON this_hash=hash
WHERE hash = $1;`

SelectDisapprovedBlocks = `SELECT is_mainchain, height, previous_hash, hash, block_chain.next_hash
FROM blocks
JOIN block_chain ON this_hash=hash
WHERE is_valid = FALSE
ORDER BY height DESC;`

IndexBlocksTableOnHeight = `CREATE INDEX uix_block_height ON blocks(height);`

DeindexBlocksTableOnHeight = `DROP INDEX uix_block_height;`
Expand Down
5 changes: 5 additions & 0 deletions db/dcrpg/pgblockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,11 @@ func (pgb *ChainDB) SideChainTips() ([]*dbtypes.BlockStatus, error) {
return RetrieveSideChainTips(pgb.db)
}

// DisapprovedBlocks retrieves all blocks disapproved by stakeholder votes.
func (pgb *ChainDB) DisapprovedBlocks() ([]*dbtypes.BlockStatus, error) {
return RetrieveDisapprovedBlocks(pgb.db)
}

// BlockStatus retrieves the block chain status of the specified block.
func (pgb *ChainDB) BlockStatus(hash string) (dbtypes.BlockStatus, error) {
return RetrieveBlockStatus(pgb.db, hash)
Expand Down
22 changes: 22 additions & 0 deletions db/dcrpg/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -2471,6 +2471,28 @@ func RetrieveSideChainTips(db *sql.DB) (blocks []*dbtypes.BlockStatus, err error
return
}

// RetrieveDisapprovedBlocks retrieves the block chain status for all blocks
// that had their regular transactions invalidated by stakeholder disapproval.
func RetrieveDisapprovedBlocks(db *sql.DB) (blocks []*dbtypes.BlockStatus, err error) {
var rows *sql.Rows
rows, err = db.Query(internal.SelectDisapprovedBlocks)
if err != nil {
return
}
defer closeRows(rows)

for rows.Next() {
var bs dbtypes.BlockStatus
err = rows.Scan(&bs.IsMainchain, &bs.Height, &bs.PrevHash, &bs.Hash, &bs.NextHash)
if err != nil {
return
}

blocks = append(blocks, &bs)
}
return
}

// RetrieveBlockStatus retrieves the block chain status for the block with the
// specified hash.
func RetrieveBlockStatus(db *sql.DB, hash string) (bs dbtypes.BlockStatus, err error) {
Expand Down
6 changes: 3 additions & 3 deletions explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ type explorerDataSource interface {
GetPgChartsData() (map[string]*dbtypes.ChartsData, error)
GetTicketsPriceByHeight() (*dbtypes.ChartsData, error)
SideChainBlocks() ([]*dbtypes.BlockStatus, error)
//SideChainTips() []*dbtypes.BlockStatus
DisapprovedBlocks() ([]*dbtypes.BlockStatus, error)
BlockStatus(hash string) (dbtypes.BlockStatus, error)
GetOldestTxBlockTime(addr string) (int64, error)
TicketPoolVisualization(interval dbtypes.ChartGrouping) ([]*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error)
Expand Down Expand Up @@ -291,8 +291,8 @@ func New(dataSource explorerDataSourceLite, primaryDataSource explorerDataSource
return nil
}
tmpls := []string{"home", "explorer", "mempool", "block", "tx", "address",
"rawtx", "status", "parameters", "agenda", "agendas", "charts", "sidechains",
"ticketpool", "nexthome"}
"rawtx", "status", "parameters", "agenda", "agendas", "charts",
"sidechains", "rejects", "ticketpool", "nexthome"}

tempDefaults := []string{"extras"}

Expand Down
83 changes: 58 additions & 25 deletions explorer/explorerroutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func netName(chainParams *chaincfg.Params) string {
return strings.Title(chainParams.Name)
}

// Home is the page handler for the "/" path
// Home is the page handler for the "/" path.
func (exp *explorerUI) Home(w http.ResponseWriter, r *http.Request) {
height := exp.blockData.GetHeight()

Expand Down Expand Up @@ -79,7 +79,7 @@ func (exp *explorerUI) Home(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// SideChains is the page handler for the "/side" path
// SideChains is the page handler for the "/side" path.
func (exp *explorerUI) SideChains(w http.ResponseWriter, r *http.Request) {
sideBlocks, err := exp.explorerSource.SideChainBlocks()
if err != nil {
Expand Down Expand Up @@ -108,7 +108,37 @@ func (exp *explorerUI) SideChains(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// NextHome is the page handler for the "/nexthome" path
// DisapprovedBlocks is the page handler for the "/rejects" path.
func (exp *explorerUI) DisapprovedBlocks(w http.ResponseWriter, r *http.Request) {
disapprovedBlocks, err := exp.explorerSource.DisapprovedBlocks()
if err != nil {
log.Errorf("Unable to get stakeholder disapproved blocks: %v", err)
exp.StatusPage(w, defaultErrorCode,
"failed to retrieve stakeholder disapproved blocks", ErrorStatusType)
return
}

str, err := exp.templates.execTemplateToString("rejects", struct {
Data []*dbtypes.BlockStatus
Version string
NetName string
}{
disapprovedBlocks,
exp.Version,
exp.NetName,
})

if err != nil {
log.Errorf("Template execute failure: %v", err)
exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType)
return
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
io.WriteString(w, str)
}

// NextHome is the page handler for the "/nexthome" path.
func (exp *explorerUI) NextHome(w http.ResponseWriter, r *http.Request) {
height := exp.blockData.GetHeight()

Expand Down Expand Up @@ -143,7 +173,7 @@ func (exp *explorerUI) NextHome(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// Blocks is the page handler for the "/blocks" path
// Blocks is the page handler for the "/blocks" path.
func (exp *explorerUI) Blocks(w http.ResponseWriter, r *http.Request) {
idx := exp.blockData.GetHeight()

Expand Down Expand Up @@ -205,17 +235,18 @@ func (exp *explorerUI) Blocks(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// Block is the page handler for the "/block" path
// Block is the page handler for the "/block" path.
func (exp *explorerUI) Block(w http.ResponseWriter, r *http.Request) {
// Retrieve the block specified on the path.
hash := getBlockHashCtx(r)

data := exp.blockData.GetExplorerBlock(hash)
if data == nil {
log.Errorf("Unable to get block %s", hash)
exp.StatusPage(w, defaultErrorCode, "could not find that block", NotFoundStatusType)
return
}
// Checking if there exists any regular non-Coinbase transactions in the block.

// Check if there are any regular non-coinbase transactions in the block.
var count int
data.TxAvailable = true
for _, i := range data.Tx {
Expand All @@ -227,6 +258,8 @@ func (exp *explorerUI) Block(w http.ResponseWriter, r *http.Request) {
data.TxAvailable = false
}

// In full mode, retrieve missed votes, main/side chain status, and
// stakeholder approval.
if !exp.liteMode {
var err error
data.Misses, err = exp.explorerSource.BlockMissedVotes(hash)
Expand Down Expand Up @@ -266,7 +299,7 @@ func (exp *explorerUI) Block(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// Mempool is the page handler for the "/mempool" path
// Mempool is the page handler for the "/mempool" path.
func (exp *explorerUI) Mempool(w http.ResponseWriter, r *http.Request) {
exp.MempoolData.RLock()
str, err := exp.templates.execTemplateToString("mempool", struct {
Expand All @@ -290,7 +323,7 @@ func (exp *explorerUI) Mempool(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// Ticketpool is the page handler for the "/ticketpool" path
// Ticketpool is the page handler for the "/ticketpool" path.
func (exp *explorerUI) Ticketpool(w http.ResponseWriter, r *http.Request) {
if exp.liteMode {
exp.StatusPage(w, fullModeRequired,
Expand Down Expand Up @@ -346,7 +379,7 @@ func (exp *explorerUI) Ticketpool(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// TxPage is the page handler for the "/tx" path
// TxPage is the page handler for the "/tx" path.
func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) {
// attempt to get tx hash string from URL path
hash, ok := r.Context().Value(ctxTxHash).(string)
Expand Down Expand Up @@ -716,7 +749,7 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// AddressPage is the page handler for the "/address" path
// AddressPage is the page handler for the "/address" path.
func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) {
// AddressPageData is the data structure passed to the HTML template
type AddressPageData struct {
Expand Down Expand Up @@ -1059,7 +1092,7 @@ func (exp *explorerUI) Charts(w http.ResponseWriter, r *http.Request) {

// Search implements a primitive search algorithm by checking if the value in
// question is a block index, block hash, address hash or transaction hash and
// redirects to the appropriate page or displays an error
// redirects to the appropriate page or displays an error.
func (exp *explorerUI) Search(w http.ResponseWriter, r *http.Request) {
searchStr := r.URL.Query().Get("search")
if searchStr == "" {
Expand All @@ -1068,7 +1101,7 @@ func (exp *explorerUI) Search(w http.ResponseWriter, r *http.Request) {
}

// Attempt to get a block hash by calling GetBlockHash to see if the value
// is a block index and then redirect to the block page if it is
// is a block index and then redirect to the block page if it is.
idx, err := strconv.ParseInt(searchStr, 10, 0)
if err == nil {
_, err = exp.blockData.GetBlockHash(idx)
Expand All @@ -1081,29 +1114,29 @@ func (exp *explorerUI) Search(w http.ResponseWriter, r *http.Request) {
}

// Call GetExplorerAddress to see if the value is an address hash and
// then redirect to the address page if it is
// then redirect to the address page if it is.
address := exp.blockData.GetExplorerAddress(searchStr, 1, 0)
if address != nil {
http.Redirect(w, r, "/address/"+searchStr, http.StatusPermanentRedirect)
return
}

// Check if the value is a valid hash
// Check if the value is a valid hash.
if _, err = chainhash.NewHashFromStr(searchStr); err != nil {
exp.StatusPage(w, "search failed", "Couldn't find any address "+searchStr, NotFoundStatusType)
return
}

// Attempt to get a block index by calling GetBlockHeight to see if the
// value is a block hash and then redirect to the block page if it is
// value is a block hash and then redirect to the block page if it is.
_, err = exp.blockData.GetBlockHeight(searchStr)
if err == nil {
http.Redirect(w, r, "/block/"+searchStr, http.StatusPermanentRedirect)
return
}

// Call GetExplorerTx to see if the value is a transaction hash and then
// redirect to the tx page if it is
// redirect to the tx page if it is.
tx := exp.blockData.GetExplorerTx(searchStr)
if tx != nil {
http.Redirect(w, r, "/tx/"+searchStr, http.StatusPermanentRedirect)
Expand Down Expand Up @@ -1139,9 +1172,9 @@ func (exp *explorerUI) StatusPage(w http.ResponseWriter, code string, message st
w.WriteHeader(http.StatusNotFound)
case ErrorStatusType:
w.WriteHeader(http.StatusInternalServerError)
// When blockchain sync is running status 202 is used to imply that the other
// requests apart from serving the status sync page have been received and
// accepted but cannot be processed now till the sync is complete.
// When blockchain sync is running, status 202 is used to imply that the
// other requests apart from serving the status sync page have been received
// and accepted but cannot be processed now till the sync is complete.
case BlockchainSyncingType:
w.WriteHeader(http.StatusAccepted)
default:
Expand All @@ -1150,12 +1183,12 @@ func (exp *explorerUI) StatusPage(w http.ResponseWriter, code string, message st
io.WriteString(w, str)
}

// NotFound wraps StatusPage to display a 404 page
// NotFound wraps StatusPage to display a 404 page.
func (exp *explorerUI) NotFound(w http.ResponseWriter, r *http.Request) {
exp.StatusPage(w, "Page not found.", "Cannot find page: "+r.URL.Path, NotFoundStatusType)
}

// ParametersPage is the page handler for the "/parameters" path
// ParametersPage is the page handler for the "/parameters" path.
func (exp *explorerUI) ParametersPage(w http.ResponseWriter, r *http.Request) {
cp := exp.ChainParams
addrPrefix := AddressPrefixes(cp)
Expand Down Expand Up @@ -1187,7 +1220,7 @@ func (exp *explorerUI) ParametersPage(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// AgendaPage is the page handler for the "/agenda" path
// AgendaPage is the page handler for the "/agenda" path.
func (exp *explorerUI) AgendaPage(w http.ResponseWriter, r *http.Request) {
if exp.liteMode {
exp.StatusPage(w, fullModeRequired,
Expand All @@ -1200,7 +1233,7 @@ func (exp *explorerUI) AgendaPage(w http.ResponseWriter, r *http.Request) {
"the agenda ID given seems to not exist", NotFoundStatusType)
}

// Attempt to get agendaid string from URL path
// Attempt to get agendaid string from URL path.
agendaid := getAgendaIDCtx(r)
agendaInfo, err := GetAgendaInfo(agendaid)
if err != nil {
Expand Down Expand Up @@ -1244,7 +1277,7 @@ func (exp *explorerUI) AgendaPage(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// AgendasPage is the page handler for the "/agendas" path
// AgendasPage is the page handler for the "/agendas" path.
func (exp *explorerUI) AgendasPage(w http.ResponseWriter, r *http.Request) {
agendas, err := agendadb.GetAllAgendas()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ func mainCore() error {
r.Mount("/explorer", explore.Mux)
r.Get("/blocks", explore.Blocks)
r.Get("/side", explore.SideChains)
r.Get("/rejects", explore.DisapprovedBlocks)
r.Get("/mempool", explore.Mempool)
r.Get("/parameters", explore.ParametersPage)
r.With(explore.BlockHashPathOrIndexCtx).Get("/block/{blockhash}", explore.Block)
Expand Down
3 changes: 3 additions & 0 deletions public/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ a.grayed {
h4 {
margin-top: 10px;
}
h4 img {
height: 50px;
}

/**testnet theme**/
body.theme-testnet .top-nav,
Expand Down
1 change: 1 addition & 0 deletions public/images/dcr-side-chains.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/images/pos-hammer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions views/rejects.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{{define "rejects"}}
<!DOCTYPE html>
<html lang="en">

{{template "html-head" "Stakeholder Disapproved Blocks"}}
<body class="{{ theme }}">
{{template "navbar" . }}
<div class="container main" data-controller="main">
<h4><span title="blocks disapproved by stakeholder voting"><img class="img-size40" src="/images/pos-hammer.svg" alt="pos hammer"> Stakeholder Disapproved Blocks</span></h4>
<h6>There are currently {{len .Data}} blocks that have been <a href="https://docs.decred.org/faq/proof-of-stake/general/#9-what-is-proof-of-stake-voting">disapproved via PoS voting.</a></h6>
<div class="row">
<div class="col-md-12">
<table class="table striped table-responsive" id="disapprovedblockstable">
<thead>
<tr>
<th>Height</th>
<th>Main Chain</th>
<th>Parent</th>
<th>Child</th>
</tr>
</thead>
<tbody>
{{range .Data}}
<tr id="{{ .Height }}">
<td class="mono fs15"><a href="/block/{{.Hash}}" class="fs16 height">{{ .Height }}</a></td>
<td class="mono fs15">{{.IsMainchain}}</td>
<td class="break-word"><a href="/block/{{.PrevHash}}" class="hash lh1rem">{{ .PrevHash }}</a></td>
{{if .NextHash }}
<td class="break-word"><a href="/block/{{.NextHash}}" class="hash lh1rem">{{ .NextHash }}</a></td>
{{else}}
<td class="break-word" title="This block is the tip of its chain.">none</td>
{{end}}
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>

{{ template "footer" . }}

</body>
</html>
{{ end }}
Loading

0 comments on commit 19953f6

Please sign in to comment.