2022-10-08 18:55:01 +00:00
package main
import (
"encoding/hex"
"encoding/json"
2023-03-18 20:19:54 +00:00
"flag"
2022-10-08 18:55:01 +00:00
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/database"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
2022-10-19 08:37:38 +00:00
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
2022-10-08 18:55:01 +00:00
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
2022-11-05 05:29:12 +00:00
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
2023-03-18 20:19:54 +00:00
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
2022-10-08 18:55:01 +00:00
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool"
2023-03-18 20:19:54 +00:00
p2poolapi "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
2022-10-08 18:55:01 +00:00
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/ake-persson/mapslice-json"
"github.com/gorilla/mux"
"log"
"math"
"net/http"
"os"
"strconv"
"strings"
"time"
2022-10-19 09:03:46 +00:00
"unicode"
2022-10-08 18:55:01 +00:00
)
func encodeJson ( r * http . Request , d any ) ( [ ] byte , error ) {
if strings . Index ( strings . ToLower ( r . Header . Get ( "user-agent" ) ) , "mozilla" ) != - 1 {
return json . MarshalIndent ( d , "" , " " )
} else {
return json . Marshal ( d )
}
}
func main ( ) {
2023-03-18 19:40:15 +00:00
torHost := os . Getenv ( "TOR_SERVICE_ADDRESS" )
2022-12-16 19:32:57 +00:00
client . SetDefaultClientSettings ( os . Getenv ( "MONEROD_RPC_URL" ) )
2023-03-18 20:19:54 +00:00
dbString := flag . String ( "db" , "" , "" )
p2poolApiHost := flag . String ( "api-host" , "" , "Host URL for p2pool go observer consensus" )
flag . Parse ( )
db , err := database . NewDatabase ( * dbString )
2022-10-08 18:55:01 +00:00
if err != nil {
log . Panic ( err )
}
defer db . Close ( )
2023-03-20 10:47:09 +00:00
p2api := p2poolapi . NewP2PoolApi ( * p2poolApiHost )
api , err := p2poolapi . New ( db , p2api )
2022-10-08 18:55:01 +00:00
if err != nil {
log . Panic ( err )
}
2023-03-18 20:19:54 +00:00
for status := p2api . Status ( ) ; ! p2api . Status ( ) . Synchronized ; status = p2api . Status ( ) {
log . Printf ( "[API] Not synchronized (height %d, id %s), waiting five seconds" , status . Height , status . Id )
time . Sleep ( time . Second * 5 )
}
log . Printf ( "[CHAIN] Consensus id = %s\n" , p2api . Consensus ( ) . Id ( ) )
getSeedByHeight := func ( height uint64 ) ( hash types . Hash ) {
seedHeight := randomx . SeedHeight ( height )
if h := p2api . MainHeaderByHeight ( seedHeight ) ; h == nil {
return types . ZeroHash
} else {
return h . Id
}
}
getBlockWithUncles := func ( id types . Hash ) ( block * sidechain . PoolBlock , uncles [ ] * sidechain . PoolBlock ) {
block = p2api . ByTemplateId ( id )
if block == nil {
return nil , nil
}
for _ , uncleId := range block . Side . Uncles {
if u := p2api . ByTemplateId ( uncleId ) ; u == nil {
return nil , nil
} else {
uncles = append ( uncles , u )
}
}
return block , uncles
}
2023-03-19 09:01:43 +00:00
_ = getBlockWithUncles
2023-03-18 20:19:54 +00:00
2022-10-08 18:55:01 +00:00
serveMux := mux . NewRouter ( )
serveMux . HandleFunc ( "/api/pool_info" , func ( writer http . ResponseWriter , request * http . Request ) {
tip := api . GetDatabase ( ) . GetChainTip ( )
blockCount := 0
uncleCount := 0
2022-10-28 09:47:14 +00:00
var windowDifficulty types . Difficulty
2022-10-08 18:55:01 +00:00
miners := make ( map [ uint64 ] uint64 )
for b := range api . GetDatabase ( ) . GetBlocksInWindow ( & tip . Height , 0 ) {
blockCount ++
if _ , ok := miners [ b . MinerId ] ; ! ok {
miners [ b . MinerId ] = 0
}
miners [ b . MinerId ] ++
2022-10-28 09:47:14 +00:00
windowDifficulty = windowDifficulty . Add ( b . Difficulty )
2022-10-08 18:55:01 +00:00
for u := range api . GetDatabase ( ) . GetUnclesByParentId ( b . Id ) {
//TODO: check this check is correct :)
2023-03-18 20:19:54 +00:00
if ( tip . Height - u . Block . Height ) > p2api . Consensus ( ) . ChainWindowSize {
2022-10-08 18:55:01 +00:00
continue
}
uncleCount ++
if _ , ok := miners [ u . Block . MinerId ] ; ! ok {
miners [ u . Block . MinerId ] = 0
}
miners [ u . Block . MinerId ] ++
2022-10-28 09:47:14 +00:00
windowDifficulty = windowDifficulty . Add ( u . Block . Difficulty )
2022-10-08 18:55:01 +00:00
}
}
type totalKnownResult struct {
blocksFound uint64
minersKnown uint64
}
totalKnown := cacheResult ( CacheTotalKnownBlocksAndMiners , time . Second * 15 , func ( ) any {
result := & totalKnownResult { }
2022-10-11 10:39:16 +00:00
if err := api . GetDatabase ( ) . Query ( "SELECT (SELECT COUNT(*) FROM blocks WHERE main_found IS TRUE ) + (SELECT COUNT(*) FROM uncles WHERE main_found IS TRUE) as found, COUNT(*) as miners FROM (SELECT miner FROM blocks GROUP BY miner UNION DISTINCT SELECT miner FROM uncles GROUP BY miner) all_known_miners;" , func ( row database . RowScanInterface ) error {
2022-10-08 18:55:01 +00:00
return row . Scan ( & result . blocksFound , & result . minersKnown )
} ) ; err != nil {
return nil
}
return result
} ) . ( * totalKnownResult )
2023-03-19 08:44:01 +00:00
/ *
poolBlocks , _ := api . GetPoolBlocks ( )
2022-10-08 18:55:01 +00:00
2023-03-19 08:44:01 +00:00
poolStats , _ := api . GetPoolStats ( )
2022-10-08 18:55:01 +00:00
2023-03-19 08:44:01 +00:00
globalDiff := tip . Template . Difficulty
2022-10-08 18:55:01 +00:00
2023-03-19 08:44:01 +00:00
currentEffort := float64 ( types . DifficultyFrom64 ( poolStats . PoolStatistics . TotalHashes - poolBlocks [ 0 ] . TotalHashes ) . Mul64 ( 100000 ) . Div ( globalDiff ) . Lo ) / 1000
2022-10-08 18:55:01 +00:00
2023-03-19 08:44:01 +00:00
if currentEffort <= 0 || poolBlocks [ 0 ] . TotalHashes == 0 {
currentEffort = 0
}
2022-10-08 18:55:01 +00:00
2023-03-19 08:44:01 +00:00
var blockEfforts mapslice . MapSlice
for i , b := range poolBlocks {
if i < ( len ( poolBlocks ) - 1 ) && b . TotalHashes > 0 && poolBlocks [ i + 1 ] . TotalHashes > 0 {
blockEfforts = append ( blockEfforts , mapslice . MapItem {
Key : b . Hash . String ( ) ,
Value : float64 ( ( b . TotalHashes - poolBlocks [ i + 1 ] . TotalHashes ) * 100 ) / float64 ( b . Difficulty ) ,
} )
}
2022-10-08 18:55:01 +00:00
}
2023-03-19 08:44:01 +00:00
* /
currentEffort := float64 ( 1 )
var blockEfforts mapslice . MapSlice
blockEfforts = append ( blockEfforts , mapslice . MapItem {
Key : types . ZeroHash . String ( ) ,
Value : float64 ( 1 ) ,
} )
2022-10-08 18:55:01 +00:00
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
if buf , err := encodeJson ( request , poolInfoResult {
SideChain : poolInfoResultSideChain {
Id : tip . Id ,
Height : tip . Height ,
Difficulty : tip . Difficulty ,
Timestamp : tip . Timestamp ,
Effort : poolInfoResultSideChainEffort {
Current : currentEffort ,
Average : func ( ) ( result float64 ) {
for _ , e := range blockEfforts {
result += e . Value . ( float64 )
}
return
} ( ) / float64 ( len ( blockEfforts ) ) ,
Last : blockEfforts ,
} ,
Window : poolInfoResultSideChainWindow {
Miners : len ( miners ) ,
Blocks : blockCount ,
Uncles : uncleCount ,
2022-10-28 09:47:14 +00:00
Weight : windowDifficulty ,
2022-10-08 18:55:01 +00:00
} ,
2023-03-18 20:19:54 +00:00
WindowSize : int ( p2api . Consensus ( ) . ChainWindowSize ) ,
BlockTime : int ( p2api . Consensus ( ) . TargetBlockTime ) ,
UnclePenalty : int ( p2api . Consensus ( ) . UnclePenalty ) ,
2022-10-08 18:55:01 +00:00
Found : totalKnown . blocksFound ,
Miners : totalKnown . minersKnown ,
} ,
MainChain : poolInfoResultMainChain {
Id : tip . Template . Id ,
2022-10-11 10:39:16 +00:00
Height : tip . Main . Height - 1 ,
2022-10-08 18:55:01 +00:00
Difficulty : tip . Template . Difficulty ,
BlockTime : monero . BlockTime ,
} ,
2022-11-30 08:07:33 +00:00
Versions : struct {
P2Pool versionInfo ` json:"p2pool" `
Monero versionInfo ` json:"monero" `
} { P2Pool : getP2PoolVersion ( ) , Monero : getMoneroVersion ( ) } ,
2022-10-08 18:55:01 +00:00
} ) ; err != nil {
log . Panic ( err )
} else {
_ , _ = writer . Write ( buf )
}
} )
2022-10-19 09:03:46 +00:00
serveMux . HandleFunc ( "/api/miner_info/{miner:[^ ]+}" , func ( writer http . ResponseWriter , request * http . Request ) {
2022-10-08 18:55:01 +00:00
minerId := mux . Vars ( request ) [ "miner" ]
var miner * database . Miner
if len ( minerId ) > 10 && minerId [ 0 ] == '4' {
miner = api . GetDatabase ( ) . GetMinerByAddress ( minerId )
}
2022-10-19 09:03:46 +00:00
if miner == nil {
miner = api . GetDatabase ( ) . GetMinerByAlias ( minerId )
}
2022-10-08 18:55:01 +00:00
if miner == nil {
if i , err := strconv . Atoi ( minerId ) ; err == nil {
miner = api . GetDatabase ( ) . GetMiner ( uint64 ( i ) )
}
}
if miner == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
var blockData struct {
Count uint64
LastHeight uint64
}
2022-10-19 09:03:46 +00:00
_ = api . GetDatabase ( ) . Query ( "SELECT COUNT(*) as count, coalesce(MAX(height), 0) as last_height FROM blocks WHERE blocks.miner = $1;" , func ( row database . RowScanInterface ) error {
2022-10-08 18:55:01 +00:00
return row . Scan ( & blockData . Count , & blockData . LastHeight )
} , miner . Id ( ) )
var uncleData struct {
Count uint64
LastHeight uint64
}
2022-10-19 09:03:46 +00:00
_ = api . GetDatabase ( ) . Query ( "SELECT COUNT(*) as count, coalesce(MAX(parent_height), 0) as last_height FROM uncles WHERE uncles.miner = $1;" , func ( row database . RowScanInterface ) error {
2022-10-08 18:55:01 +00:00
return row . Scan ( & uncleData . Count , & uncleData . LastHeight )
} , miner . Id ( ) )
var lastShareHeight uint64
var lastShareTimestamp uint64
if blockData . Count > 0 && blockData . LastHeight > lastShareHeight {
lastShareHeight = blockData . LastHeight
}
if uncleData . Count > 0 && uncleData . LastHeight > lastShareHeight {
lastShareHeight = uncleData . LastHeight
}
if lastShareHeight > 0 {
lastShareTimestamp = api . GetDatabase ( ) . GetBlockByHeight ( lastShareHeight ) . Timestamp
}
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , minerInfoResult {
Id : miner . Id ( ) ,
Address : miner . MoneroAddress ( ) ,
2022-10-19 08:37:38 +00:00
Alias : miner . Alias ( ) ,
2022-10-08 18:55:01 +00:00
Shares : struct {
Blocks uint64 ` json:"blocks" `
Uncles uint64 ` json:"uncles" `
} { Blocks : blockData . Count , Uncles : uncleData . Count } ,
LastShareHeight : lastShareHeight ,
LastShareTimestamp : lastShareTimestamp ,
} )
_ , _ = writer . Write ( buf )
} )
2022-10-19 08:37:38 +00:00
serveMux . HandleFunc ( "/api/miner_alias/{miner:4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+}" , func ( writer http . ResponseWriter , request * http . Request ) {
minerId := mux . Vars ( request ) [ "miner" ]
miner := api . GetDatabase ( ) . GetMinerByAddress ( minerId )
if miner == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
params := request . URL . Query ( )
sig := strings . TrimSpace ( params . Get ( "signature" ) )
message := strings . TrimSpace ( params . Get ( "message" ) )
if len ( message ) > 20 || len ( message ) < 3 || ! func ( ) bool {
for _ , c := range message {
2022-11-06 13:01:06 +00:00
if ! ( c >= '0' && c <= '9' ) && ! ( c >= 'a' && c <= 'z' ) && ! ( c >= 'A' && c <= 'Z' ) && c != '_' && c != '-' && c != '.' {
2022-10-19 08:37:38 +00:00
return false
}
}
return true
2022-10-19 09:03:46 +00:00
} ( ) || ! unicode . IsLetter ( rune ( message [ 0 ] ) ) {
2022-10-19 08:37:38 +00:00
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusBadRequest )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "invalid_message" ,
} )
_ , _ = writer . Write ( buf )
return
}
2022-11-05 08:33:53 +00:00
result := address . VerifyMessage ( miner . MoneroAddress ( ) , [ ] byte ( message ) , sig )
2022-10-19 08:37:38 +00:00
if result == address . ResultSuccessSpend {
2022-10-19 09:03:46 +00:00
if message == "REMOVE_MINER_ALIAS" {
message = ""
}
2022-10-19 08:37:38 +00:00
if api . GetDatabase ( ) . SetMinerAlias ( miner . Id ( ) , message ) != nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusBadRequest )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "duplicate_message" ,
} )
_ , _ = writer . Write ( buf )
return
} else {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
return
}
} else if result == address . ResultSuccessView {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusBadRequest )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "view_signature" ,
} )
_ , _ = writer . Write ( buf )
return
} else {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusBadRequest )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "invalid_signature" ,
} )
_ , _ = writer . Write ( buf )
return
}
} )
2022-10-08 20:14:21 +00:00
serveMux . HandleFunc ( "/api/shares_in_window/{miner:[0-9]+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$}" , func ( writer http . ResponseWriter , request * http . Request ) {
2022-10-08 18:55:01 +00:00
minerId := mux . Vars ( request ) [ "miner" ]
var miner * database . Miner
if len ( minerId ) > 10 && minerId [ 0 ] == '4' {
miner = api . GetDatabase ( ) . GetMinerByAddress ( minerId )
}
if miner == nil {
if i , err := strconv . Atoi ( minerId ) ; err == nil {
miner = api . GetDatabase ( ) . GetMiner ( uint64 ( i ) )
}
}
if miner == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
params := request . URL . Query ( )
2023-03-18 20:19:54 +00:00
window := p2api . Consensus ( ) . ChainWindowSize
2022-10-08 18:55:01 +00:00
if params . Has ( "window" ) {
if i , err := strconv . Atoi ( params . Get ( "window" ) ) ; err == nil {
2023-03-18 20:19:54 +00:00
if i <= int ( p2api . Consensus ( ) . ChainWindowSize * 4 ) {
2022-10-08 18:55:01 +00:00
window = uint64 ( i )
}
}
}
var from * uint64
if params . Has ( "from" ) {
if i , err := strconv . Atoi ( params . Get ( "from" ) ) ; err == nil {
if i >= 0 {
i := uint64 ( i )
from = & i
}
}
}
result := make ( [ ] * sharesInWindowResult , 0 )
for block := range api . GetDatabase ( ) . GetBlocksByMinerIdInWindow ( miner . Id ( ) , from , window ) {
s := & sharesInWindowResult {
Id : block . Id ,
Height : block . Height ,
Timestamp : block . Timestamp ,
2022-10-08 20:39:40 +00:00
Weight : block . Difficulty . Lo ,
2022-10-08 18:55:01 +00:00
}
for u := range api . GetDatabase ( ) . GetUnclesByParentId ( block . Id ) {
uncleWeight := u . Block . Difficulty . Mul64 ( p2pool . UnclePenalty ) . Div64 ( 100 )
s . Uncles = append ( s . Uncles , sharesInWindowResultUncle {
Id : u . Block . Id ,
Height : u . Block . Height ,
2022-10-08 20:39:40 +00:00
Weight : uncleWeight . Lo ,
2022-10-08 18:55:01 +00:00
} )
2022-10-08 20:39:40 +00:00
s . Weight += uncleWeight . Lo
2022-10-08 18:55:01 +00:00
}
result = append ( result , s )
}
for uncle := range api . GetDatabase ( ) . GetUnclesByMinerIdInWindow ( miner . Id ( ) , from , window ) {
s := & sharesInWindowResult {
Parent : & sharesInWindowResultParent {
Id : uncle . ParentId ,
Height : uncle . ParentHeight ,
} ,
Id : uncle . Block . Id ,
Height : uncle . Block . Height ,
Timestamp : uncle . Block . Timestamp ,
2022-10-08 20:39:40 +00:00
Weight : uncle . Block . Difficulty . Mul64 ( 100 - p2pool . UnclePenalty ) . Div64 ( 100 ) . Lo ,
2022-10-08 18:55:01 +00:00
}
result = append ( result , s )
}
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , result )
_ , _ = writer . Write ( buf )
} )
2022-10-08 20:14:21 +00:00
serveMux . HandleFunc ( "/api/payouts/{miner:[0-9]+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$}" , func ( writer http . ResponseWriter , request * http . Request ) {
2022-10-08 18:55:01 +00:00
minerId := mux . Vars ( request ) [ "miner" ]
var miner * database . Miner
if len ( minerId ) > 10 && minerId [ 0 ] == '4' {
miner = api . GetDatabase ( ) . GetMinerByAddress ( minerId )
}
if miner == nil {
if i , err := strconv . Atoi ( minerId ) ; err == nil {
miner = api . GetDatabase ( ) . GetMiner ( uint64 ( i ) )
}
}
if miner == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
params := request . URL . Query ( )
var limit uint64 = 10
if params . Has ( "search_limit" ) {
if i , err := strconv . Atoi ( params . Get ( "search_limit" ) ) ; err == nil {
limit = uint64 ( i )
}
}
payouts := make ( [ ] * database . Payout , 0 )
for payout := range api . GetDatabase ( ) . GetPayoutsByMinerId ( miner . Id ( ) , limit ) {
payouts = append ( payouts , payout )
}
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , payouts )
_ , _ = writer . Write ( buf )
} )
2022-10-08 20:14:21 +00:00
serveMux . HandleFunc ( "/api/redirect/block/{main_height:[0-9]+|.?[0-9A-Za-z]+$}" , func ( writer http . ResponseWriter , request * http . Request ) {
2023-03-18 19:40:15 +00:00
if request . Header . Get ( "host" ) == torHost {
http . Redirect ( writer , request , fmt . Sprintf ( "http://yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion/explorer/block/%d" , utils . DecodeBinaryNumber ( mux . Vars ( request ) [ "main_height" ] ) ) , http . StatusFound )
2023-03-20 10:06:56 +00:00
} else {
http . Redirect ( writer , request , fmt . Sprintf ( "https://p2pool.io/explorer/block/%d" , utils . DecodeBinaryNumber ( mux . Vars ( request ) [ "main_height" ] ) ) , http . StatusFound )
2023-03-18 19:40:15 +00:00
}
2022-10-08 18:55:01 +00:00
} )
2022-10-08 20:14:21 +00:00
serveMux . HandleFunc ( "/api/redirect/transaction/{tx_id:.?[0-9A-Za-z]+}" , func ( writer http . ResponseWriter , request * http . Request ) {
2023-03-18 19:40:15 +00:00
txId := utils . DecodeHexBinaryNumber ( mux . Vars ( request ) [ "tx_id" ] )
if len ( txId ) != types . HashSize * 2 {
writer . WriteHeader ( http . StatusNotFound )
return
}
if request . Header . Get ( "host" ) == torHost {
http . Redirect ( writer , request , fmt . Sprintf ( "http://yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion/explorer/tx/%s" , txId ) , http . StatusFound )
2023-03-20 10:06:56 +00:00
} else {
http . Redirect ( writer , request , fmt . Sprintf ( "https://p2pool.io/explorer/tx/%s" , txId ) , http . StatusFound )
2023-03-18 19:40:15 +00:00
}
2022-10-08 18:55:01 +00:00
} )
serveMux . HandleFunc ( "/api/redirect/block/{coinbase:[0-9]+|.?[0-9A-Za-z]+$}" , func ( writer http . ResponseWriter , request * http . Request ) {
c := api . GetDatabase ( ) . GetBlocksByQuery ( "WHERE height = $1 AND main_found IS TRUE" , utils . DecodeBinaryNumber ( mux . Vars ( request ) [ "coinbase" ] ) )
defer func ( ) {
for range c {
}
} ( )
b := <- c
if b == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
2023-03-18 19:40:15 +00:00
if request . Header . Get ( "host" ) == torHost {
http . Redirect ( writer , request , fmt . Sprintf ( "http://yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion/explorer/tx/%s" , b . Coinbase . Id . String ( ) ) , http . StatusFound )
2023-03-20 10:06:56 +00:00
} else {
http . Redirect ( writer , request , fmt . Sprintf ( "https://p2pool.io/explorer/tx/%s" , b . Coinbase . Id . String ( ) ) , http . StatusFound )
2023-03-18 19:40:15 +00:00
}
2022-10-08 18:55:01 +00:00
} )
2022-10-08 20:14:21 +00:00
serveMux . HandleFunc ( "/api/redirect/share/{height:[0-9]+|.?[0-9A-Za-z]+$}" , func ( writer http . ResponseWriter , request * http . Request ) {
2022-10-08 18:55:01 +00:00
c := utils . DecodeBinaryNumber ( mux . Vars ( request ) [ "height" ] )
blockHeight := c >> 16
blockIdStart := c & 0xFFFF
var b database . BlockInterface
var b1 = <- api . GetDatabase ( ) . GetBlocksByQuery ( "WHERE height = $1 AND id ILIKE $2 LIMIT 1;" , blockHeight , hex . EncodeToString ( [ ] byte { byte ( ( blockIdStart >> 8 ) & 0xFF ) , byte ( blockIdStart & 0xFF ) } ) + "%" )
if b1 == nil {
b2 := <- api . GetDatabase ( ) . GetUncleBlocksByQuery ( "WHERE height = $1 AND id ILIKE $2 LIMIT 1;" , blockHeight , hex . EncodeToString ( [ ] byte { byte ( ( blockIdStart >> 8 ) & 0xFF ) , byte ( blockIdStart & 0xFF ) } ) + "%" )
if b2 != nil {
b = b2
}
} else {
b = b1
}
if b == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
http . Redirect ( writer , request , fmt . Sprintf ( "/share/%s" , b . GetBlock ( ) . Id . String ( ) ) , http . StatusFound )
} )
serveMux . HandleFunc ( "/api/redirect/prove/{height_index:[0-9]+|.[0-9A-Za-z]+}" , func ( writer http . ResponseWriter , request * http . Request ) {
i := utils . DecodeBinaryNumber ( mux . Vars ( request ) [ "height_index" ] )
2023-03-18 20:19:54 +00:00
n := uint64 ( math . Ceil ( math . Log2 ( float64 ( p2api . Consensus ( ) . ChainWindowSize * 4 ) ) ) )
2022-10-08 18:55:01 +00:00
height := i >> n
index := i & ( ( 1 << n ) - 1 )
b := api . GetDatabase ( ) . GetBlockByHeight ( height )
var tx * database . CoinbaseTransactionOutput
if b != nil {
tx = api . GetDatabase ( ) . GetCoinbaseTransactionOutputByIndex ( b . Coinbase . Id , index )
}
if b == nil || tx == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
2022-10-24 12:36:50 +00:00
http . Redirect ( writer , request , fmt . Sprintf ( "/proof/%s/%d" , b . Id . String ( ) , tx . Index ( ) ) , http . StatusFound )
2022-10-08 18:55:01 +00:00
} )
2022-10-24 12:36:50 +00:00
serveMux . HandleFunc ( "/api/redirect/prove/{height:[0-9]+|.[0-9A-Za-z]+}/{miner:[0-9]+|.?[0-9A-Za-z]+}" , func ( writer http . ResponseWriter , request * http . Request ) {
b := api . GetDatabase ( ) . GetBlockByHeight ( utils . DecodeBinaryNumber ( mux . Vars ( request ) [ "height" ] ) )
2022-10-08 18:55:01 +00:00
miner := api . GetDatabase ( ) . GetMiner ( utils . DecodeBinaryNumber ( mux . Vars ( request ) [ "miner" ] ) )
2022-10-24 12:36:50 +00:00
if b == nil || miner == nil {
2022-10-08 18:55:01 +00:00
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
2022-10-24 12:36:50 +00:00
tx := api . GetDatabase ( ) . GetCoinbaseTransactionOutputByMinerId ( b . Coinbase . Id , miner . Id ( ) )
if tx == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
http . Redirect ( writer , request , fmt . Sprintf ( "/proof/%s/%d" , b . Id . String ( ) , tx . Index ( ) ) , http . StatusFound )
2022-10-08 18:55:01 +00:00
} )
2022-10-24 12:36:50 +00:00
serveMux . HandleFunc ( "/api/redirect/miner/{miner:[0-9]+|.?[0-9A-Za-z]+}" , func ( writer http . ResponseWriter , request * http . Request ) {
2022-10-08 18:55:01 +00:00
miner := api . GetDatabase ( ) . GetMiner ( utils . DecodeBinaryNumber ( mux . Vars ( request ) [ "miner" ] ) )
2022-10-24 12:36:50 +00:00
if miner == nil {
2022-10-08 18:55:01 +00:00
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
2022-10-24 12:36:50 +00:00
http . Redirect ( writer , request , fmt . Sprintf ( "/miner/%s" , miner . Address ( ) ) , http . StatusFound )
2022-10-08 18:55:01 +00:00
} )
//other redirects
serveMux . HandleFunc ( "/api/redirect/last_found{kind:|/raw|/info}" , func ( writer http . ResponseWriter , request * http . Request ) {
http . Redirect ( writer , request , fmt . Sprintf ( "/api/block_by_id/%s%s%s" , api . GetDatabase ( ) . GetLastFound ( ) . GetBlock ( ) . Id . String ( ) , mux . Vars ( request ) [ "kind" ] , request . URL . RequestURI ( ) ) , http . StatusFound )
} )
serveMux . HandleFunc ( "/api/redirect/tip{kind:|/raw|/info}" , func ( writer http . ResponseWriter , request * http . Request ) {
http . Redirect ( writer , request , fmt . Sprintf ( "/api/block_by_id/%s%s%s" , api . GetDatabase ( ) . GetChainTip ( ) . Id . String ( ) , mux . Vars ( request ) [ "kind" ] , request . URL . RequestURI ( ) ) , http . StatusFound )
} )
serveMux . HandleFunc ( "/api/found_blocks" , func ( writer http . ResponseWriter , request * http . Request ) {
params := request . URL . Query ( )
var limit uint64 = 50
if params . Has ( "limit" ) {
if i , err := strconv . Atoi ( params . Get ( "limit" ) ) ; err == nil {
limit = uint64 ( i )
}
}
2022-10-11 10:39:16 +00:00
var minerId uint64
if params . Has ( "miner" ) {
id := params . Get ( "miner" )
var miner * database . Miner
if len ( id ) > 10 && id [ 0 ] == '4' {
miner = api . GetDatabase ( ) . GetMinerByAddress ( id )
}
if miner == nil {
if i , err := strconv . Atoi ( id ) ; err == nil {
miner = api . GetDatabase ( ) . GetMiner ( uint64 ( i ) )
}
}
if miner == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
minerId = miner . Id ( )
}
2022-10-12 12:15:47 +00:00
if limit > 200 {
limit = 200
2022-10-08 18:55:01 +00:00
}
if limit == 0 {
limit = 50
}
result := make ( [ ] * database . Block , 0 , limit )
2022-10-11 10:39:16 +00:00
for block := range api . GetDatabase ( ) . GetAllFound ( limit , minerId ) {
2022-10-08 18:55:01 +00:00
MapJSONBlock ( api , block , false , params . Has ( "coinbase" ) )
result = append ( result , block . GetBlock ( ) )
}
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , result )
_ , _ = writer . Write ( buf )
} )
serveMux . HandleFunc ( "/api/shares" , func ( writer http . ResponseWriter , request * http . Request ) {
params := request . URL . Query ( )
var limit uint64 = 50
if params . Has ( "limit" ) {
if i , err := strconv . Atoi ( params . Get ( "limit" ) ) ; err == nil {
limit = uint64 ( i )
}
}
2023-03-18 20:19:54 +00:00
if limit > p2api . Consensus ( ) . ChainWindowSize * 4 * 7 {
limit = p2api . Consensus ( ) . ChainWindowSize * 4 * 7
2022-10-08 18:55:01 +00:00
}
if limit == 0 {
limit = 50
}
onlyBlocks := params . Has ( "onlyBlocks" )
var minerId uint64
if params . Has ( "miner" ) {
id := params . Get ( "miner" )
var miner * database . Miner
if len ( id ) > 10 && id [ 0 ] == '4' {
miner = api . GetDatabase ( ) . GetMinerByAddress ( id )
}
if miner == nil {
if i , err := strconv . Atoi ( id ) ; err == nil {
miner = api . GetDatabase ( ) . GetMiner ( uint64 ( i ) )
}
}
if miner == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
minerId = miner . Id ( )
}
result := make ( [ ] * database . Block , 0 , limit )
for b := range api . GetDatabase ( ) . GetShares ( limit , minerId , onlyBlocks ) {
MapJSONBlock ( api , b , true , params . Has ( "coinbase" ) )
result = append ( result , b . GetBlock ( ) )
}
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , result )
_ , _ = writer . Write ( buf )
} )
2022-10-18 09:03:19 +00:00
serveMux . HandleFunc ( "/api/block_by_{by:id|height}/{block:[0-9a-f]+|[0-9]+}{kind:|/raw|/info|/payouts}" , func ( writer http . ResponseWriter , request * http . Request ) {
2022-10-08 18:55:01 +00:00
params := request . URL . Query ( )
var block database . BlockInterface
if mux . Vars ( request ) [ "by" ] == "id" {
if id , err := types . HashFromString ( mux . Vars ( request ) [ "block" ] ) ; err == nil {
if b := api . GetDatabase ( ) . GetBlockById ( id ) ; b != nil {
block = b
}
}
} else if mux . Vars ( request ) [ "by" ] == "height" {
if height , err := strconv . ParseUint ( mux . Vars ( request ) [ "block" ] , 10 , 0 ) ; err == nil {
if b := api . GetDatabase ( ) . GetBlockByHeight ( height ) ; b != nil {
block = b
}
}
}
isOrphan := false
isInvalid := false
if block == nil && mux . Vars ( request ) [ "by" ] == "id" {
if id , err := types . HashFromString ( mux . Vars ( request ) [ "block" ] ) ; err == nil {
if b := api . GetDatabase ( ) . GetUncleById ( id ) ; b != nil {
block = b
} else {
isOrphan = true
2023-03-19 09:01:43 +00:00
bblock := p2api . ByTemplateId ( id )
2023-03-18 20:19:54 +00:00
2023-03-19 09:01:43 +00:00
if b , _ , _ := database . NewBlockFromBinaryBlock ( getSeedByHeight , p2api . MainDifficultyByHeight , db , bblock , nil , false ) ; b != nil {
2022-10-08 18:55:01 +00:00
block = b
} else {
isInvalid = true
2023-03-18 20:19:54 +00:00
/ * if b , _ = api . GetShareFromFailedRawEntry ( id ) ; b != nil {
2022-10-08 18:55:01 +00:00
block = b
2023-03-18 20:19:54 +00:00
} * /
2022-10-08 18:55:01 +00:00
}
}
}
}
if block == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
switch mux . Vars ( request ) [ "kind" ] {
case "/raw" :
2023-03-18 20:19:54 +00:00
raw := p2api . ByTemplateId ( block . GetBlock ( ) . Id )
2022-10-08 18:55:01 +00:00
if raw == nil {
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusNotFound )
buf , _ := json . Marshal ( struct {
Error string ` json:"error" `
} {
Error : "not_found" ,
} )
_ , _ = writer . Write ( buf )
return
}
writer . Header ( ) . Set ( "Content-Type" , "text/plain" )
writer . WriteHeader ( http . StatusOK )
2023-03-18 20:19:54 +00:00
buf , _ := raw . MarshalBinary ( )
_ , _ = writer . Write ( buf )
2022-10-18 09:03:19 +00:00
case "/payouts" :
result := make ( [ ] * database . Payout , 0 )
if ! isOrphan && ! isInvalid {
blockHeight := block . GetBlock ( ) . Height
if uncle , ok := block . ( * database . UncleBlock ) ; ok {
blockHeight = uncle . ParentHeight
}
if err := db . Query ( "SELECT decode(b.id, 'hex') AS id, decode(b.main_id, 'hex') AS main_id, b.height AS height, b.main_height AS main_height, b.timestamp AS timestamp, decode(b.coinbase_privkey, 'hex') AS coinbase_privkey, b.uncle AS uncle, decode(o.id, 'hex') AS coinbase_id, o.amount AS amount, o.index AS index FROM (SELECT id, amount, index FROM coinbase_outputs WHERE miner = $1 ) o LEFT JOIN LATERAL (SELECT id, coinbase_id, coinbase_privkey, height, main_height, main_id, timestamp, FALSE AS uncle FROM blocks WHERE coinbase_id = o.id UNION SELECT id, coinbase_id, coinbase_privkey, height, main_height, main_id, timestamp, TRUE AS uncle FROM uncles WHERE coinbase_id = o.id) b ON b.coinbase_id = o.id WHERE height >= $2 AND height < $3 ORDER BY main_height ASC;" , func ( row database . RowScanInterface ) error {
var blockId , mainId , privKey , coinbaseId [ ] byte
var height , mainHeight , timestamp , amount , index uint64
var uncle bool
if err := row . Scan ( & blockId , & mainId , & height , & mainHeight , & timestamp , & privKey , & uncle , & coinbaseId , & amount , & index ) ; err != nil {
return err
}
result = append ( result , & database . Payout {
Id : types . HashFromBytes ( blockId ) ,
Height : height ,
Timestamp : timestamp ,
Main : struct {
Id types . Hash ` json:"id" `
Height uint64 ` json:"height" `
} { Id : types . HashFromBytes ( mainId ) , Height : mainHeight } ,
Uncle : uncle ,
Coinbase : struct {
2022-11-06 12:17:26 +00:00
Id types . Hash ` json:"id" `
Reward uint64 ` json:"reward" `
2022-11-05 05:29:12 +00:00
PrivateKey crypto . PrivateKeyBytes ` json:"private_key" `
2022-11-06 12:17:26 +00:00
Index uint64 ` json:"index" `
2022-11-05 05:29:12 +00:00
} { Id : types . HashFromBytes ( coinbaseId ) , Reward : amount , PrivateKey : crypto . PrivateKeyBytes ( types . HashFromBytes ( privKey ) ) , Index : index } ,
2022-10-18 09:03:19 +00:00
} )
return nil
2023-03-18 20:19:54 +00:00
} , block . GetBlock ( ) . MinerId , blockHeight , blockHeight + p2api . Consensus ( ) . ChainWindowSize ) ; err != nil {
2022-10-18 09:03:19 +00:00
return
}
}
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , result )
_ , _ = writer . Write ( buf )
2022-10-08 18:55:01 +00:00
default :
MapJSONBlock ( api , block , true , params . Has ( "coinbase" ) )
func ( ) {
block . GetBlock ( ) . Lock . Lock ( )
defer block . GetBlock ( ) . Lock . Unlock ( )
block . GetBlock ( ) . Orphan = isOrphan
if isInvalid {
block . GetBlock ( ) . Invalid = & isInvalid
}
} ( )
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , block . GetBlock ( ) )
_ , _ = writer . Write ( buf )
}
} )
type difficultyStatResult struct {
Height uint64 ` json:"height" `
Timestamp uint64 ` json:"timestamp" `
Difficulty uint64 ` json:"difficulty" `
}
type minerSeenResult struct {
Height uint64 ` json:"height" `
Timestamp uint64 ` json:"timestamp" `
Miners uint64 ` json:"miners" `
}
serveMux . HandleFunc ( "/api/stats/{kind:difficulty|miner_seen|miner_seen_window$}" , func ( writer http . ResponseWriter , request * http . Request ) {
switch mux . Vars ( request ) [ "kind" ] {
case "difficulty" :
result := make ( [ ] * difficultyStatResult , 0 )
_ = api . GetDatabase ( ) . Query ( "SELECT height,timestamp,difficulty FROM blocks WHERE height % $1 = 0 ORDER BY height DESC;" , func ( row database . RowScanInterface ) error {
r := & difficultyStatResult { }
var difficultyHex string
if err := row . Scan ( & r . Height , & r . Timestamp , & difficultyHex ) ; err != nil {
return err
}
d , _ := types . DifficultyFromString ( difficultyHex )
r . Difficulty = d . Lo
result = append ( result , r )
return nil
2023-03-18 20:19:54 +00:00
} , 3600 / p2api . Consensus ( ) . TargetBlockTime )
2022-10-08 18:55:01 +00:00
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , result )
_ , _ = writer . Write ( buf )
case "miner_seen" :
result := make ( [ ] * minerSeenResult , 0 )
_ = api . GetDatabase ( ) . Query ( "SELECT height,timestamp,(SELECT COUNT(DISTINCT(b.miner)) FROM blocks b WHERE b.height <= blocks.height AND b.height > (blocks.height - $2)) as count FROM blocks WHERE height % $1 = 0 ORDER BY height DESC;" , func ( row database . RowScanInterface ) error {
r := & minerSeenResult { }
if err := row . Scan ( & r . Height , & r . Timestamp , & r . Miners ) ; err != nil {
return err
}
result = append ( result , r )
return nil
2023-03-18 20:19:54 +00:00
} , ( 3600 * 24 ) / p2api . Consensus ( ) . TargetBlockTime , ( 3600 * 24 * 7 ) / p2api . Consensus ( ) . TargetBlockTime )
2022-10-08 18:55:01 +00:00
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , result )
_ , _ = writer . Write ( buf )
case "miner_seen_window" :
result := make ( [ ] * minerSeenResult , 0 )
_ = api . GetDatabase ( ) . Query ( "SELECT height,timestamp,(SELECT COUNT(DISTINCT(b.miner)) FROM blocks b WHERE b.height <= blocks.height AND b.height > (blocks.height - $2)) as count FROM blocks WHERE height % $1 = 0 ORDER BY height DESC;" , func ( row database . RowScanInterface ) error {
r := & minerSeenResult { }
if err := row . Scan ( & r . Height , & r . Timestamp , & r . Miners ) ; err != nil {
return err
}
result = append ( result , r )
return nil
2023-03-18 20:19:54 +00:00
} , p2api . Consensus ( ) . ChainWindowSize , p2api . Consensus ( ) . ChainWindowSize )
2022-10-08 18:55:01 +00:00
writer . Header ( ) . Set ( "Content-Type" , "application/json; charset=utf-8" )
writer . WriteHeader ( http . StatusOK )
buf , _ := encodeJson ( request , result )
_ , _ = writer . Write ( buf )
}
} )
server := & http . Server {
Addr : "0.0.0.0:8080" ,
ReadTimeout : time . Second * 2 ,
Handler : http . HandlerFunc ( func ( writer http . ResponseWriter , request * http . Request ) {
if request . Method != "GET" && request . Method != "HEAD" {
writer . WriteHeader ( http . StatusForbidden )
return
}
serveMux . ServeHTTP ( writer , request )
} ) ,
}
if err := server . ListenAndServe ( ) ; err != nil {
log . Panic ( err )
}
}