Initial commit
This commit is contained in:
commit
76d70a5f1a
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.idea
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2022 WeebDataHoarder, p2pool-observer Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
785
cmd/api/api.go
Normal file
785
cmd/api/api.go
Normal file
|
@ -0,0 +1,785 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/database"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
|
||||||
|
"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"
|
||||||
|
"lukechampine.com/uint128"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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() {
|
||||||
|
client.SetClientSettings(os.Getenv("MONEROD_RPC_URL"))
|
||||||
|
db, err := database.NewDatabase(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
api, err := api.New(db, os.Getenv("API_FOLDER"))
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serveMux := mux.NewRouter()
|
||||||
|
serveMux.HandleFunc("/api/pool_info", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
tip := api.GetDatabase().GetChainTip()
|
||||||
|
|
||||||
|
blockCount := 0
|
||||||
|
uncleCount := 0
|
||||||
|
|
||||||
|
var windowDifficulty uint128.Uint128
|
||||||
|
|
||||||
|
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]++
|
||||||
|
|
||||||
|
windowDifficulty = windowDifficulty.Add(b.Difficulty.Uint128)
|
||||||
|
|
||||||
|
for u := range api.GetDatabase().GetUnclesByParentId(b.Id) {
|
||||||
|
//TODO: check this check is correct :)
|
||||||
|
if (tip.Height - u.Block.Height) > p2pool.PPLNSWindow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
uncleCount++
|
||||||
|
if _, ok := miners[u.Block.MinerId]; !ok {
|
||||||
|
miners[u.Block.MinerId] = 0
|
||||||
|
}
|
||||||
|
miners[u.Block.MinerId]++
|
||||||
|
|
||||||
|
windowDifficulty = windowDifficulty.Add(u.Block.Difficulty.Uint128)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type totalKnownResult struct {
|
||||||
|
blocksFound uint64
|
||||||
|
minersKnown uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
totalKnown := cacheResult(CacheTotalKnownBlocksAndMiners, time.Second*15, func() any {
|
||||||
|
result := &totalKnownResult{}
|
||||||
|
if err := api.GetDatabase().Query("SELECT (SELECT COUNT(*) FROM blocks WHERE main_found = 'y') + (SELECT COUNT(*) FROM uncles WHERE main_found = 'y') 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 {
|
||||||
|
return row.Scan(&result.blocksFound, &result.minersKnown)
|
||||||
|
}); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}).(*totalKnownResult)
|
||||||
|
|
||||||
|
poolBlocks, _ := api.GetPoolBlocks()
|
||||||
|
|
||||||
|
poolStats, _ := api.GetPoolStats()
|
||||||
|
|
||||||
|
globalDiff := tip.Template.Difficulty
|
||||||
|
|
||||||
|
currentEffort := float64(uint128.From64(poolStats.PoolStatistics.TotalHashes-poolBlocks[0].TotalHashes).Mul64(100000).Div(globalDiff.Uint128).Lo) / 1000
|
||||||
|
|
||||||
|
if currentEffort <= 0 {
|
||||||
|
currentEffort = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockEfforts mapslice.MapSlice
|
||||||
|
for i, b := range poolBlocks {
|
||||||
|
if i < (len(poolBlocks)-1) && b.TotalHashes > 0 {
|
||||||
|
blockEfforts = append(blockEfforts, mapslice.MapItem{
|
||||||
|
Key: b.Hash.String(),
|
||||||
|
Value: float64((b.TotalHashes-poolBlocks[i+1].TotalHashes)*100) / float64(b.Difficulty),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
Weight: types.Difficulty{Uint128: windowDifficulty},
|
||||||
|
},
|
||||||
|
WindowSize: p2pool.PPLNSWindow,
|
||||||
|
BlockTime: p2pool.BlockTime,
|
||||||
|
UnclePenalty: p2pool.UnclePenalty,
|
||||||
|
Found: totalKnown.blocksFound,
|
||||||
|
Miners: totalKnown.minersKnown,
|
||||||
|
},
|
||||||
|
MainChain: poolInfoResultMainChain{
|
||||||
|
Id: tip.Template.Id,
|
||||||
|
Height: tip.Main.Height,
|
||||||
|
Difficulty: tip.Template.Difficulty,
|
||||||
|
BlockTime: monero.BlockTime,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
} else {
|
||||||
|
_, _ = writer.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
serveMux.HandleFunc("/api/miner_info/{miner:^[0-9]+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockData struct {
|
||||||
|
Count uint64
|
||||||
|
LastHeight uint64
|
||||||
|
}
|
||||||
|
_ = api.GetDatabase().Query("SELECT COUNT(*) as count, MAX(height) as last_height FROM blocks WHERE blocks.miner = $1;", func(row database.RowScanInterface) error {
|
||||||
|
return row.Scan(&blockData.Count, &blockData.LastHeight)
|
||||||
|
}, miner.Id())
|
||||||
|
var uncleData struct {
|
||||||
|
Count uint64
|
||||||
|
LastHeight uint64
|
||||||
|
}
|
||||||
|
_ = api.GetDatabase().Query("SELECT COUNT(*) as count, MAX(parent_height) as last_height FROM uncles WHERE uncles.miner = $1;", func(row database.RowScanInterface) error {
|
||||||
|
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(),
|
||||||
|
Shares: struct {
|
||||||
|
Blocks uint64 `json:"blocks"`
|
||||||
|
Uncles uint64 `json:"uncles"`
|
||||||
|
}{Blocks: blockData.Count, Uncles: uncleData.Count},
|
||||||
|
LastShareHeight: lastShareHeight,
|
||||||
|
LastShareTimestamp: lastShareTimestamp,
|
||||||
|
})
|
||||||
|
_, _ = writer.Write(buf)
|
||||||
|
})
|
||||||
|
|
||||||
|
serveMux.HandleFunc("/api/shares_in_window/{miner:^[0-9]+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
window := uint64(p2pool.PPLNSWindow)
|
||||||
|
if params.Has("window") {
|
||||||
|
if i, err := strconv.Atoi(params.Get("window")); err == nil {
|
||||||
|
if i <= p2pool.PPLNSWindow*4 {
|
||||||
|
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,
|
||||||
|
Weight: block.Difficulty,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
Weight: types.Difficulty{Uint128: uncleWeight},
|
||||||
|
})
|
||||||
|
s.Weight.Uint128 = s.Weight.Add(uncleWeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
Weight: types.Difficulty{Uint128: uncle.Block.Difficulty.Mul64(100 - p2pool.UnclePenalty).Div64(100)},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
serveMux.HandleFunc("/api/payouts/{miner:^[0-9]+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
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)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
serveMux.HandleFunc("/api/redirect/block/{main_height:^[0-9]+|.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
http.Redirect(writer, request, fmt.Sprintf("https://xmrchain.net/block/%d", utils.DecodeBinaryNumber(mux.Vars(request)["main_height"])), http.StatusFound)
|
||||||
|
})
|
||||||
|
serveMux.HandleFunc("/api/redirect/transaction/{tx_id:^.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
http.Redirect(writer, request, fmt.Sprintf("https://xmrchain.net/tx/%s", utils.DecodeHexBinaryNumber(mux.Vars(request)["tx_id"])), http.StatusFound)
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
http.Redirect(writer, request, fmt.Sprintf("https://xmrchain.net/tx/%s", b.Coinbase.Id.String()), http.StatusFound)
|
||||||
|
})
|
||||||
|
serveMux.HandleFunc("/api/redirect/share/{height:^[0-9]+|.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
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"])
|
||||||
|
n := uint64(math.Ceil(math.Log2(p2pool.PPLNSWindow * 4)))
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
miner := api.GetDatabase().GetMiner(tx.Miner())
|
||||||
|
|
||||||
|
http.Redirect(writer, request, fmt.Sprintf("https://www.exploremonero.com/receipt/%s/%s/%s", b.Coinbase.Id.String(), miner.Address(), b.Coinbase.PrivateKey), http.StatusFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
serveMux.HandleFunc("/api/redirect/miner/{miner:[0-9]+|.?[0-9A-Za-z]+}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
miner := api.GetDatabase().GetMiner(utils.DecodeBinaryNumber(mux.Vars(request)["miner"]))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(writer, request, fmt.Sprintf("/miner/%s", miner.Address()), http.StatusFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
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"]))
|
||||||
|
miner := api.GetDatabase().GetMiner(utils.DecodeBinaryNumber(mux.Vars(request)["miner"]))
|
||||||
|
if b == nil || 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
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(writer, request, fmt.Sprintf("https://www.exploremonero.com/receipt/%s/%s/%s", b.Coinbase.Id.String(), miner.Address(), b.Coinbase.PrivateKey), http.StatusFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
//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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]*database.Block, 0, limit)
|
||||||
|
|
||||||
|
for block := range api.GetDatabase().GetAllFound(limit) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > p2pool.PPLNSWindow {
|
||||||
|
limit = p2pool.PPLNSWindow
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
serveMux.HandleFunc("/api/block_by_{by:id|height}/{block:[0-9a-f]+|[0-9]+}{kind:|/raw|/info}", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if b, _, _ := api.GetShareFromRawEntry(id, false); b != nil {
|
||||||
|
block = b
|
||||||
|
} else {
|
||||||
|
isInvalid = true
|
||||||
|
if b, _ = api.GetShareFromFailedRawEntry(id); b != nil {
|
||||||
|
block = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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":
|
||||||
|
raw, _ := api.GetRawBlockBytes(block.GetBlock().Id)
|
||||||
|
|
||||||
|
if raw == nil {
|
||||||
|
raw, _ = api.GetFailedRawBlockBytes(block.GetBlock().Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
_, _ = writer.Write(raw)
|
||||||
|
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
|
||||||
|
}, 3600/p2pool.BlockTime)
|
||||||
|
|
||||||
|
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
|
||||||
|
}, (3600*24)/p2pool.BlockTime, (3600*24*7)/p2pool.BlockTime)
|
||||||
|
|
||||||
|
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
|
||||||
|
}, p2pool.PPLNSWindow, p2pool.PPLNSWindow)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
109
cmd/api/block.go
Normal file
109
cmd/api/block.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/database"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MapJSONBlock fills special values for any block
|
||||||
|
func MapJSONBlock(api *api.Api, block database.BlockInterface, extraUncleData, extraCoinbaseData bool) {
|
||||||
|
|
||||||
|
b := block.GetBlock()
|
||||||
|
b.Lock.Lock()
|
||||||
|
defer b.Lock.Unlock()
|
||||||
|
|
||||||
|
if extraCoinbaseData {
|
||||||
|
|
||||||
|
var tx *database.CoinbaseTransaction
|
||||||
|
if b.Main.Found {
|
||||||
|
tx = api.GetDatabase().GetCoinbaseTransaction(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Main.Found && tx != nil {
|
||||||
|
b.Coinbase.Payouts = make([]*database.JSONCoinbaseOutput, 0, len(tx.Outputs()))
|
||||||
|
for _, output := range tx.Outputs() {
|
||||||
|
b.Coinbase.Payouts = append(b.Coinbase.Payouts, &database.JSONCoinbaseOutput{
|
||||||
|
Amount: output.Amount(),
|
||||||
|
Index: output.Index(),
|
||||||
|
Address: api.GetDatabase().GetMiner(output.Miner()).Address(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payoutHint := api.GetBlockWindowPayouts(b)
|
||||||
|
|
||||||
|
addresses := make(map[[32]byte]*database.JSONCoinbaseOutput, len(payoutHint))
|
||||||
|
|
||||||
|
var k [32]byte
|
||||||
|
|
||||||
|
for minerId, amount := range payoutHint {
|
||||||
|
miner := api.GetDatabase().GetMiner(minerId)
|
||||||
|
copy(k[:], miner.MoneroAddress().SpendPub.Bytes())
|
||||||
|
copy(k[types.HashSize:], miner.MoneroAddress().ViewPub.Bytes())
|
||||||
|
addresses[k] = &database.JSONCoinbaseOutput{
|
||||||
|
Address: miner.Address(),
|
||||||
|
Amount: amount.Lo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedAddresses := maps.Keys(addresses)
|
||||||
|
|
||||||
|
slices.SortFunc(sortedAddresses, func(a [32]byte, b [32]byte) bool {
|
||||||
|
return bytes.Compare(a[:], b[:]) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Coinbase.Payouts = make([]*database.JSONCoinbaseOutput, len(sortedAddresses))
|
||||||
|
|
||||||
|
for i, key := range sortedAddresses {
|
||||||
|
addresses[key].Index = uint64(i)
|
||||||
|
b.Coinbase.Payouts[i] = addresses[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
weight := b.Difficulty
|
||||||
|
|
||||||
|
if uncle, ok := block.(*database.UncleBlock); ok {
|
||||||
|
b.Parent = &database.JSONBlockParent{
|
||||||
|
Id: uncle.ParentId,
|
||||||
|
Height: uncle.ParentHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
weight.Uint128 = weight.Mul64(100 - p2pool.UnclePenalty).Div64(100)
|
||||||
|
} else {
|
||||||
|
for u := range api.GetDatabase().GetUnclesByParentId(b.Id) {
|
||||||
|
uncleWeight := u.Block.Difficulty.Mul64(p2pool.UnclePenalty).Div64(100)
|
||||||
|
weight.Uint128 = weight.Add(uncleWeight)
|
||||||
|
|
||||||
|
if !extraUncleData {
|
||||||
|
b.Uncles = append(b.Uncles, &database.JSONUncleBlockSimple{
|
||||||
|
Id: u.Block.Id,
|
||||||
|
Height: u.Block.Height,
|
||||||
|
Weight: uncleWeight.Lo,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
b.Uncles = append(b.Uncles, &database.JSONUncleBlockExtra{
|
||||||
|
Id: u.Block.Id,
|
||||||
|
Height: u.Block.Height,
|
||||||
|
Difficulty: u.Block.Difficulty,
|
||||||
|
Timestamp: u.Block.Timestamp,
|
||||||
|
Miner: api.GetDatabase().GetMiner(u.Block.MinerId).Address(),
|
||||||
|
PowHash: u.Block.PowHash,
|
||||||
|
Weight: uncleWeight.Lo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Weight = weight.Lo
|
||||||
|
|
||||||
|
if b.IsProofHigherThanDifficulty() && !b.Main.Found {
|
||||||
|
b.Main.Orphan = true
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Address = api.GetDatabase().GetMiner(b.MinerId).Address()
|
||||||
|
}
|
52
cmd/api/cache.go
Normal file
52
cmd/api/cache.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CacheTotalKnownBlocksAndMiners = iota
|
||||||
|
totalSize
|
||||||
|
)
|
||||||
|
|
||||||
|
var cache = make([]*cachedResult, totalSize)
|
||||||
|
var cacheLock sync.RWMutex
|
||||||
|
|
||||||
|
type cachedResult struct {
|
||||||
|
t time.Time
|
||||||
|
result any
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheResult(k uint, cacheTime time.Duration, result func() any) any {
|
||||||
|
if k >= totalSize {
|
||||||
|
return result()
|
||||||
|
}
|
||||||
|
if r := func() any {
|
||||||
|
if cacheTime > 0 {
|
||||||
|
cacheLock.RLock()
|
||||||
|
defer cacheLock.RUnlock()
|
||||||
|
|
||||||
|
if r := cache[k]; r != nil && r.t.Add(cacheTime).After(time.Now()) {
|
||||||
|
return r.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}(); r != nil {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
r := result()
|
||||||
|
|
||||||
|
if cacheTime > 0 && r != nil {
|
||||||
|
cacheLock.Lock()
|
||||||
|
defer cacheLock.Unlock()
|
||||||
|
cache[k] = &cachedResult{
|
||||||
|
t: time.Now(),
|
||||||
|
result: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
76
cmd/api/types.go
Normal file
76
cmd/api/types.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"github.com/ake-persson/mapslice-json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type poolInfoResult struct {
|
||||||
|
SideChain poolInfoResultSideChain `json:"sidechain"`
|
||||||
|
MainChain poolInfoResultMainChain `json:"mainchain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type poolInfoResultSideChain struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
Timestamp uint64 `json:"timestamp"`
|
||||||
|
Effort poolInfoResultSideChainEffort `json:"effort"`
|
||||||
|
Window poolInfoResultSideChainWindow `json:"window"`
|
||||||
|
WindowSize int `json:"window_size"`
|
||||||
|
BlockTime int `json:"block_time"`
|
||||||
|
UnclePenalty int `json:"uncle_penalty"`
|
||||||
|
Found uint64 `json:"found"`
|
||||||
|
Miners uint64 `json:"miners"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type poolInfoResultSideChainEffort struct {
|
||||||
|
Current float64 `json:"current"`
|
||||||
|
Average float64 `json:"average"`
|
||||||
|
Last mapslice.MapSlice `json:"last"`
|
||||||
|
}
|
||||||
|
type poolInfoResultSideChainWindow struct {
|
||||||
|
Miners int `json:"miners"`
|
||||||
|
Blocks int `json:"blocks"`
|
||||||
|
Uncles int `json:"uncles"`
|
||||||
|
Weight types.Difficulty `json:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type poolInfoResultMainChain struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
BlockTime int `json:"block_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type minerInfoResult struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Address *address.Address `json:"address"`
|
||||||
|
Shares struct {
|
||||||
|
Blocks uint64 `json:"blocks"`
|
||||||
|
Uncles uint64 `json:"uncles"`
|
||||||
|
} `json:"shares"`
|
||||||
|
LastShareHeight uint64 `json:"last_share_height"`
|
||||||
|
LastShareTimestamp uint64 `json:"last_share_timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type sharesInWindowResult struct {
|
||||||
|
Parent *sharesInWindowResultParent `json:"parent,omitempty"`
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Timestamp uint64 `json:"timestamp"`
|
||||||
|
Weight types.Difficulty `json:"weight"`
|
||||||
|
Uncles []sharesInWindowResultUncle `json:"uncles,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type sharesInWindowResultParent struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type sharesInWindowResultUncle struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Weight types.Difficulty `json:"weight"`
|
||||||
|
}
|
242
cmd/daemon/daemon.go
Normal file
242
cmd/daemon/daemon.go
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/database"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client.SetClientSettings(os.Getenv("MONEROD_RPC_URL"))
|
||||||
|
db, err := database.NewDatabase(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
api, err := api.New(db, os.Getenv("API_FOLDER"))
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: force-insert section
|
||||||
|
|
||||||
|
tip := db.GetChainTip()
|
||||||
|
|
||||||
|
isFresh := tip == nil
|
||||||
|
|
||||||
|
tipHeight := uint64(1)
|
||||||
|
|
||||||
|
if tip != nil {
|
||||||
|
tipHeight = tip.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[CHAIN] Last known database tip is %d\n", tipHeight)
|
||||||
|
|
||||||
|
poolStats, err := api.GetPoolStats()
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
diskTip := poolStats.PoolStatistics.Height
|
||||||
|
|
||||||
|
log.Printf("[CHAIN] Last known disk tip is %d\n", diskTip)
|
||||||
|
|
||||||
|
startFrom := tipHeight
|
||||||
|
|
||||||
|
if diskTip > tipHeight && !api.BlockExists(tipHeight+1) {
|
||||||
|
for i := diskTip; api.BlockExists(i); i-- {
|
||||||
|
startFrom = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFresh || startFrom != tipHeight {
|
||||||
|
block, _, err := api.GetShareEntry(startFrom)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
id := block.Id
|
||||||
|
if block, uncles, err := api.GetShareFromRawEntry(id, true); err != nil {
|
||||||
|
log.Panicf("[CHAIN] Could not find block %s to insert at height %d. Check disk or uncles\n", id.String(), startFrom)
|
||||||
|
} else {
|
||||||
|
if err = db.InsertBlock(block, nil); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
for _, uncle := range uncles {
|
||||||
|
if err = db.InsertUncleBlock(uncle, nil); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: handle jumps in blocks (missing data)
|
||||||
|
|
||||||
|
knownTip := startFrom
|
||||||
|
|
||||||
|
log.Printf("[CHAIN] Starting tip from height %d\n", knownTip)
|
||||||
|
|
||||||
|
runs := 0
|
||||||
|
|
||||||
|
//Fix blocks without height
|
||||||
|
for b := range db.GetBlocksByQuery("WHERE miner_main_difficulty = 'ffffffffffffffffffffffffffffffff' ORDER BY main_height ASC;") {
|
||||||
|
cacheHeightDifficulty(b.Main.Height)
|
||||||
|
|
||||||
|
if diff, ok := getHeightDifficulty(b.Main.Height); ok {
|
||||||
|
log.Printf("[CHAIN] Filling main difficulty for share %d, main height %d\n", b.Height, b.Main.Height)
|
||||||
|
_ = db.SetBlockMainDifficulty(b.Id, diff)
|
||||||
|
b = db.GetBlockById(b.Id)
|
||||||
|
|
||||||
|
if !b.Main.Found && b.IsProofHigherThanDifficulty() {
|
||||||
|
log.Printf("[CHAIN] BLOCK FOUND! Main height %d, main id %s\n", b.Main.Height, b.Main.Id.String())
|
||||||
|
|
||||||
|
if tx, _ := client.GetClient().GetCoinbaseTransaction(b.Coinbase.Id); tx != nil {
|
||||||
|
_ = db.SetBlockFound(b.Id, true)
|
||||||
|
processFoundBlockWithTransaction(api, b, tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Fix uncle without height
|
||||||
|
for u := range db.GetUncleBlocksByQuery("WHERE miner_main_difficulty = 'ffffffffffffffffffffffffffffffff' ORDER BY main_height ASC;") {
|
||||||
|
cacheHeightDifficulty(u.Block.Main.Height)
|
||||||
|
|
||||||
|
if diff, ok := getHeightDifficulty(u.Block.Main.Height); ok {
|
||||||
|
log.Printf("[CHAIN] Filling main difficulty for uncle share %d, main height %d\n", u.Block.Height, u.Block.Main.Height)
|
||||||
|
_ = db.SetBlockMainDifficulty(u.Block.Id, diff)
|
||||||
|
u = db.GetUncleById(u.Block.Id)
|
||||||
|
|
||||||
|
if !u.Block.Main.Found && u.Block.IsProofHigherThanDifficulty() {
|
||||||
|
log.Printf("[CHAIN] BLOCK FOUND! Main height %d, main id %s\n", u.Block.Main.Height, u.Block.Main.Id.String())
|
||||||
|
|
||||||
|
if tx, _ := client.GetClient().GetCoinbaseTransaction(u.Block.Coinbase.Id); tx != nil {
|
||||||
|
_ = db.SetBlockFound(u.Block.Id, true)
|
||||||
|
processFoundBlockWithTransaction(api, u, tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
runs++
|
||||||
|
|
||||||
|
diskTip, _, _ := api.GetShareEntry(knownTip)
|
||||||
|
|
||||||
|
if rawTip, _, _ := api.GetShareFromRawEntry(diskTip.Id, false); rawTip != nil {
|
||||||
|
diskTip = rawTip
|
||||||
|
}
|
||||||
|
|
||||||
|
dbTip := db.GetBlockByHeight(knownTip)
|
||||||
|
|
||||||
|
if dbTip.Id != diskTip.Id { //Reorg has happened, delete old values
|
||||||
|
for h := knownTip; h > 0; h-- {
|
||||||
|
dbBlock := db.GetBlockByHeight(h)
|
||||||
|
diskBlock, _, _ := api.GetShareEntry(h)
|
||||||
|
if dbBlock.PreviousId == diskBlock.PreviousId {
|
||||||
|
log.Printf("[REORG] Found matching head %s at height %d\n", dbBlock.PreviousId.String(), dbBlock.Height-1)
|
||||||
|
deleted, _ := db.DeleteBlockById(dbBlock.Id)
|
||||||
|
log.Printf("[REORG] Deleted %d block(s).\n", deleted)
|
||||||
|
log.Printf("[REORG] Next tip %s : %d.\n", diskBlock.PreviousId, diskBlock.Height)
|
||||||
|
knownTip = dbBlock.Height - 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for h := knownTip + 1; api.BlockExists(h); h++ {
|
||||||
|
diskBlock, _, _ := api.GetShareEntry(h)
|
||||||
|
if diskBlock == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
id := diskBlock.Id
|
||||||
|
|
||||||
|
var uncles []*database.UncleBlock
|
||||||
|
diskBlock, uncles, err = api.GetShareFromRawEntry(id, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[CHAIN] Could not find block %s to insert at height %d. Check disk or uncles\n", id.String(), h)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
prevBlock := db.GetBlockByHeight(h - 1)
|
||||||
|
if diskBlock.PreviousId != prevBlock.Id {
|
||||||
|
log.Printf("[CHAIN] Possible reorg occurred, aborting insertion at height %d: prev id %s != id %s\n", h, diskBlock.PreviousId.String(), prevBlock.Id.String())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[CHAIN] Inserting block %s at height %d\n", diskBlock.Id.String(), diskBlock.Height)
|
||||||
|
|
||||||
|
cacheHeightDifficulty(diskBlock.Main.Height)
|
||||||
|
|
||||||
|
diff, ok := getHeightDifficulty(diskBlock.Main.Height)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
err = db.InsertBlock(diskBlock, &diff)
|
||||||
|
} else {
|
||||||
|
err = db.InsertBlock(diskBlock, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
for _, uncle := range uncles {
|
||||||
|
log.Printf("[CHAIN] Inserting uncle %s @ %s at %d", uncle.Block.Main.Id.String(), diskBlock.Id.String(), diskBlock.Height)
|
||||||
|
|
||||||
|
diff, ok := getHeightDifficulty(uncle.Block.Main.Height)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
err = db.InsertUncleBlock(uncle, &diff)
|
||||||
|
} else {
|
||||||
|
err = db.InsertUncleBlock(uncle, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uncle.Block.Main.Found {
|
||||||
|
log.Printf("[CHAIN] BLOCK FOUND! (uncle) Main height %d, main id %s", uncle.Block.Main.Height, uncle.Block.Main.Id.String())
|
||||||
|
|
||||||
|
if b, _ := api.GetRawBlock(uncle.Block.Id); b != nil {
|
||||||
|
processFoundBlockWithTransaction(api, uncle, b.Main.Coinbase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
knownTip = diskBlock.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
if diskBlock.Main.Found {
|
||||||
|
log.Printf("[CHAIN] BLOCK FOUND! Main height %d, main id %s", diskBlock.Main.Height, diskBlock.Main.Id.String())
|
||||||
|
|
||||||
|
if b, _ := api.GetRawBlock(diskBlock.Id); b != nil {
|
||||||
|
processFoundBlockWithTransaction(api, diskBlock, b.Main.Coinbase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if runs%10 == 0 { //Every 10 seconds or so
|
||||||
|
for foundBlock := range db.GetAllFound(10) {
|
||||||
|
//Scan last 10 found blocks and set status accordingly if found/not found
|
||||||
|
|
||||||
|
// Look between +1 block and +4 blocks
|
||||||
|
if (diskTip.Main.Height-1) > foundBlock.GetBlock().Main.Height && (diskTip.Main.Height-5) < foundBlock.GetBlock().Main.Height || db.GetCoinbaseTransaction(foundBlock.GetBlock()) == nil {
|
||||||
|
if tx, _ := client.GetClient().GetCoinbaseTransaction(foundBlock.GetBlock().Coinbase.Id); tx == nil {
|
||||||
|
// If more than two minutes have passed before we get utxo, remove from found
|
||||||
|
log.Printf("[CHAIN] Block that was found at main height %d, cannot find output, marking not found\n", foundBlock.GetBlock().Main.Height)
|
||||||
|
_ = db.SetBlockFound(foundBlock.GetBlock().Id, false)
|
||||||
|
} else {
|
||||||
|
processFoundBlockWithTransaction(api, foundBlock, tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFresh {
|
||||||
|
//TODO: Do migration tasks
|
||||||
|
isFresh = false
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
}
|
||||||
|
}
|
46
cmd/daemon/difficulty_cache.go
Normal file
46
cmd/daemon/difficulty_cache.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/rand"
|
||||||
|
"lukechampine.com/uint128"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var difficultyCache = make(map[uint64]types.Difficulty)
|
||||||
|
var difficultyCacheLock sync.RWMutex
|
||||||
|
|
||||||
|
func getHeightDifficulty(height uint64) (difficulty types.Difficulty, ok bool) {
|
||||||
|
difficultyCacheLock.RLock()
|
||||||
|
defer difficultyCacheLock.RUnlock()
|
||||||
|
difficulty, ok = difficultyCache[height]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHeightDifficulty(height uint64, difficulty types.Difficulty) {
|
||||||
|
difficultyCacheLock.Lock()
|
||||||
|
defer difficultyCacheLock.Unlock()
|
||||||
|
|
||||||
|
if len(difficultyCache) >= 1024 {
|
||||||
|
//Delete key at random
|
||||||
|
//TODO: FIFO
|
||||||
|
keys := maps.Keys(difficultyCache)
|
||||||
|
delete(difficultyCache, keys[rand.Intn(len(keys))])
|
||||||
|
}
|
||||||
|
|
||||||
|
difficultyCache[height] = difficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheHeightDifficulty(height uint64) {
|
||||||
|
if _, ok := getHeightDifficulty(height); !ok {
|
||||||
|
if header, err := client.GetClient().GetBlockHeaderByHeight(height); err != nil {
|
||||||
|
if template, err := client.GetClient().GetBlockTemplate(types.DonationAddress); err != nil {
|
||||||
|
setHeightDifficulty(uint64(template.Height), types.Difficulty{Uint128: uint128.From64(uint64(template.Difficulty))})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHeightDifficulty(header.BlockHeader.Height, types.Difficulty{Uint128: uint128.From64(header.BlockHeader.Difficulty)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
cmd/daemon/utils.go
Normal file
40
cmd/daemon/utils.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/database"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/block"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func processFoundBlockWithTransaction(api *api.Api, b database.BlockInterface, tx *block.CoinbaseTransaction) bool {
|
||||||
|
if api.GetDatabase().CoinbaseTransactionExists(b.GetBlock()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.Printf("[OUTPUT] Trying to insert transaction %s\n", b.GetBlock().Coinbase.Id.String())
|
||||||
|
|
||||||
|
payoutHint := api.GetBlockWindowPayouts(b.GetBlock())
|
||||||
|
|
||||||
|
miners := make([]*database.Miner, 0, len(payoutHint))
|
||||||
|
for minerId, _ := range payoutHint {
|
||||||
|
miners = append(miners, api.GetDatabase().GetMiner(minerId))
|
||||||
|
if miners[len(miners)-1] == nil {
|
||||||
|
log.Panicf("minerId %d is nil", minerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := database.MatchOutputs(tx, miners, b.GetBlock().Coinbase.PrivateKey)
|
||||||
|
|
||||||
|
if len(outputs) == len(miners) && len(outputs) == len(tx.Outputs) {
|
||||||
|
newOutputs := make([]*database.CoinbaseTransactionOutput, 0, len(outputs))
|
||||||
|
for _, o := range outputs {
|
||||||
|
newOutputs = append(newOutputs, database.NewCoinbaseTransactionOutput(b.GetBlock().Coinbase.Id, o.Output.Index, o.Output.Reward, o.Miner.Id()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.GetDatabase().InsertCoinbaseTransaction(database.NewCoinbaseTransaction(b.GetBlock().Coinbase.Id, b.GetBlock().Coinbase.PrivateKey, newOutputs)) == nil
|
||||||
|
} else {
|
||||||
|
log.Printf("[OUTPUT] Could not find all outputs! Coinbase transaction %s, got %d, expected %d, real %d\n", b.GetBlock().Coinbase.Id.String(), len(outputs), len(miners), len(tx.Outputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
473
database/block.go
Normal file
473
database/block.go
Normal file
|
@ -0,0 +1,473 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||||
|
p2poolBlock "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/block"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"lukechampine.com/uint128"
|
||||||
|
"math/bits"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var NilHash types.Hash
|
||||||
|
var UndefinedHash types.Hash
|
||||||
|
var UndefinedDifficulty types.Difficulty
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
copy(NilHash[:], bytes.Repeat([]byte{0}, types.HashSize))
|
||||||
|
copy(UndefinedHash[:], bytes.Repeat([]byte{0xff}, types.HashSize))
|
||||||
|
UndefinedDifficulty.Uint128 = uint128.FromBytes(bytes.Repeat([]byte{0xff}, types.DifficultySize))
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockInterface interface {
|
||||||
|
GetBlock() *Block
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockCoinbase struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Reward uint64 `json:"reward"`
|
||||||
|
PrivateKey types.Hash `json:"private_key"`
|
||||||
|
//Payouts extra JSON field, do not use
|
||||||
|
Payouts []*JSONCoinbaseOutput `json:"payouts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockMainData struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Found bool `json:"found"`
|
||||||
|
|
||||||
|
//Orphan extra JSON field, do not use
|
||||||
|
Orphan bool `json:"orphan,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONBlockParent struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONUncleBlockSimple struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Weight uint64 `json:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONCoinbaseOutput struct {
|
||||||
|
Amount uint64 `json:"amount"`
|
||||||
|
Index uint64 `json:"index"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONUncleBlockExtra struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
Timestamp uint64 `json:"timestamp"`
|
||||||
|
Miner string `json:"miner"`
|
||||||
|
PowHash types.Hash `json:"pow"`
|
||||||
|
Weight uint64 `json:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Block struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
PreviousId types.Hash `json:"previous_id"`
|
||||||
|
Coinbase BlockCoinbase `json:"coinbase"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
Timestamp uint64 `json:"timestamp"`
|
||||||
|
MinerId uint64 `json:"-"`
|
||||||
|
//Address extra JSON field, do not use
|
||||||
|
Address string `json:"miner,omitempty"`
|
||||||
|
PowHash types.Hash `json:"pow"`
|
||||||
|
|
||||||
|
Main BlockMainData `json:"main"`
|
||||||
|
|
||||||
|
Template struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
} `json:"template"`
|
||||||
|
|
||||||
|
//Lock extra JSON field, do not use
|
||||||
|
Lock sync.Mutex `json:"-"`
|
||||||
|
|
||||||
|
//Weight extra JSON field, do not use
|
||||||
|
Weight uint64 `json:"weight"`
|
||||||
|
//Parent extra JSON field, do not use
|
||||||
|
Parent *JSONBlockParent `json:"parent,omitempty"`
|
||||||
|
//Uncles extra JSON field, do not use
|
||||||
|
Uncles []any `json:"uncles,omitempty"`
|
||||||
|
|
||||||
|
//Orphan extra JSON field, do not use
|
||||||
|
Orphan bool `json:"orphan,omitempty"`
|
||||||
|
|
||||||
|
//Invalid extra JSON field, do not use
|
||||||
|
Invalid *bool `json:"invalid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBlockFromBinaryBlock(db *Database, b *p2poolBlock.Block, knownUncles []*p2poolBlock.Block, errOnUncles bool) (block *Block, uncles []*UncleBlock, err error) {
|
||||||
|
miner := db.GetOrCreateMinerByAddress(b.GetAddress().ToBase58())
|
||||||
|
if miner == nil {
|
||||||
|
return nil, nil, errors.New("could not get or create miner")
|
||||||
|
}
|
||||||
|
|
||||||
|
block = &Block{
|
||||||
|
Id: b.Main.CoinbaseExtra.SideId,
|
||||||
|
Height: b.Side.Height,
|
||||||
|
PreviousId: b.Side.Parent,
|
||||||
|
Coinbase: BlockCoinbase{
|
||||||
|
Id: b.Main.Coinbase.Id(),
|
||||||
|
Reward: func() (v uint64) {
|
||||||
|
for _, o := range b.Main.Coinbase.Outputs {
|
||||||
|
v += o.Reward
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
PrivateKey: b.Side.CoinbasePrivateKey,
|
||||||
|
},
|
||||||
|
Difficulty: b.Side.Difficulty,
|
||||||
|
Timestamp: b.Main.Timestamp,
|
||||||
|
MinerId: miner.Id(),
|
||||||
|
PowHash: b.Extra.PowHash,
|
||||||
|
Main: BlockMainData{
|
||||||
|
Id: b.Extra.MainId,
|
||||||
|
Height: b.Main.Coinbase.GenHeight,
|
||||||
|
Found: b.IsProofHigherThanDifficulty(),
|
||||||
|
},
|
||||||
|
Template: struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
}{
|
||||||
|
Id: b.Main.Parent,
|
||||||
|
Difficulty: b.Extra.MainDifficulty,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range b.Side.Uncles {
|
||||||
|
if i := slices.IndexFunc(knownUncles, func(uncle *p2poolBlock.Block) bool {
|
||||||
|
return uncle.Main.CoinbaseExtra.SideId == u
|
||||||
|
}); i != -1 {
|
||||||
|
uncle := knownUncles[i]
|
||||||
|
uncleMiner := db.GetOrCreateMinerByAddress(uncle.GetAddress().ToBase58())
|
||||||
|
if uncleMiner == nil {
|
||||||
|
return nil, nil, errors.New("could not get or create miner")
|
||||||
|
}
|
||||||
|
uncles = append(uncles, &UncleBlock{
|
||||||
|
Block: Block{
|
||||||
|
Id: uncle.Main.CoinbaseExtra.SideId,
|
||||||
|
Height: uncle.Side.Height,
|
||||||
|
PreviousId: uncle.Side.Parent,
|
||||||
|
Coinbase: BlockCoinbase{
|
||||||
|
Id: uncle.Main.Coinbase.Id(),
|
||||||
|
Reward: func() (v uint64) {
|
||||||
|
for _, o := range uncle.Main.Coinbase.Outputs {
|
||||||
|
v += o.Reward
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
PrivateKey: uncle.Side.CoinbasePrivateKey,
|
||||||
|
},
|
||||||
|
Difficulty: uncle.Side.Difficulty,
|
||||||
|
Timestamp: uncle.Main.Timestamp,
|
||||||
|
MinerId: uncleMiner.Id(),
|
||||||
|
PowHash: uncle.Extra.PowHash,
|
||||||
|
Main: BlockMainData{
|
||||||
|
Id: uncle.Extra.MainId,
|
||||||
|
Height: uncle.Main.Coinbase.GenHeight,
|
||||||
|
Found: uncle.IsProofHigherThanDifficulty(),
|
||||||
|
},
|
||||||
|
Template: struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
}{
|
||||||
|
Id: uncle.Main.Parent,
|
||||||
|
Difficulty: uncle.Extra.MainDifficulty,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ParentId: block.Id,
|
||||||
|
ParentHeight: block.Height,
|
||||||
|
})
|
||||||
|
} else if errOnUncles {
|
||||||
|
return nil, nil, fmt.Errorf("could not find uncle %s", hex.EncodeToString(u[:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return block, uncles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonBlock2 struct {
|
||||||
|
MinerMainId types.Hash `json:"miner_main_id"`
|
||||||
|
CoinbaseReward uint64 `json:"coinbase_reward,string"`
|
||||||
|
CoinbaseId types.Hash `json:"coinbase_id"`
|
||||||
|
Version uint64 `json:"version,string"`
|
||||||
|
Diff types.Difficulty `json:"diff"`
|
||||||
|
Wallet *address.Address `json:"wallet"`
|
||||||
|
MinerMainDiff types.Difficulty `json:"miner_main_diff"`
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height,string"`
|
||||||
|
PowHash types.Hash `json:"pow_hash"`
|
||||||
|
MainId types.Hash `json:"main_id"`
|
||||||
|
MainHeight uint64 `json:"main_height,string"`
|
||||||
|
Ts uint64 `json:"ts,string"`
|
||||||
|
PrevId types.Hash `json:"prev_id"`
|
||||||
|
CoinbasePriv types.Hash `json:"coinbase_priv"`
|
||||||
|
Lts uint64 `json:"lts,string"`
|
||||||
|
MainFound string `json:"main_found,omitempty"`
|
||||||
|
|
||||||
|
Uncles []struct {
|
||||||
|
MinerMainId types.Hash `json:"miner_main_id"`
|
||||||
|
CoinbaseReward uint64 `json:"coinbase_reward,string"`
|
||||||
|
CoinbaseId types.Hash `json:"coinbase_id"`
|
||||||
|
Version uint64 `json:"version,string"`
|
||||||
|
Diff types.Difficulty `json:"diff"`
|
||||||
|
Wallet *address.Address `json:"wallet"`
|
||||||
|
MinerMainDiff types.Difficulty `json:"miner_main_diff"`
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height,string"`
|
||||||
|
PowHash types.Hash `json:"pow_hash"`
|
||||||
|
MainId types.Hash `json:"main_id"`
|
||||||
|
MainHeight uint64 `json:"main_height,string"`
|
||||||
|
Ts uint64 `json:"ts,string"`
|
||||||
|
PrevId types.Hash `json:"prev_id"`
|
||||||
|
CoinbasePriv types.Hash `json:"coinbase_priv"`
|
||||||
|
Lts uint64 `json:"lts,string"`
|
||||||
|
MainFound string `json:"main_found,omitempty"`
|
||||||
|
} `json:"uncles,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonBlock1 struct {
|
||||||
|
Wallet *address.Address `json:"wallet"`
|
||||||
|
Height uint64 `json:"height,string"`
|
||||||
|
MHeight uint64 `json:"mheight,string"`
|
||||||
|
PrevId types.Hash `json:"prev_id"`
|
||||||
|
Ts uint64 `json:"ts,string"`
|
||||||
|
PowHash types.Hash `json:"pow_hash"`
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
PrevHash types.Hash `json:"prev_hash"`
|
||||||
|
Diff uint64 `json:"diff,string"`
|
||||||
|
TxCoinbase types.Hash `json:"tx_coinbase"`
|
||||||
|
Lts uint64 `json:"lts,string"`
|
||||||
|
MHash types.Hash `json:"mhash"`
|
||||||
|
TxPriv types.Hash `json:"tx_priv"`
|
||||||
|
TxPub types.Hash `json:"tx_pub"`
|
||||||
|
BlockFound string `json:"main_found,omitempty"`
|
||||||
|
|
||||||
|
Uncles []struct {
|
||||||
|
Diff uint64 `json:"diff,string"`
|
||||||
|
PrevId types.Hash `json:"prev_id"`
|
||||||
|
Ts uint64 `json:"ts,string"`
|
||||||
|
MHeight uint64 `json:"mheight,string"`
|
||||||
|
PrevHash types.Hash `json:"prev_hash"`
|
||||||
|
Height uint64 `json:"height,string"`
|
||||||
|
Wallet *address.Address `json:"wallet"`
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
} `json:"uncles,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type versionBlock struct {
|
||||||
|
Version uint64 `json:"version,string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBlockFromJSONBlock(db *Database, data []byte) (block *Block, uncles []*UncleBlock, err error) {
|
||||||
|
var version versionBlock
|
||||||
|
|
||||||
|
if err = json.Unmarshal(data, &version); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else {
|
||||||
|
if version.Version == 2 {
|
||||||
|
|
||||||
|
var b JsonBlock2
|
||||||
|
if err = json.Unmarshal(data, &b); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
miner := db.GetOrCreateMinerByAddress(b.Wallet.ToBase58())
|
||||||
|
if miner == nil {
|
||||||
|
return nil, nil, errors.New("could not get or create miner")
|
||||||
|
}
|
||||||
|
|
||||||
|
block = &Block{
|
||||||
|
Id: b.Id,
|
||||||
|
Height: b.Height,
|
||||||
|
PreviousId: b.PrevId,
|
||||||
|
Coinbase: BlockCoinbase{
|
||||||
|
Id: b.CoinbaseId,
|
||||||
|
Reward: b.CoinbaseReward,
|
||||||
|
PrivateKey: b.CoinbasePriv,
|
||||||
|
},
|
||||||
|
Difficulty: b.Diff,
|
||||||
|
Timestamp: b.Ts,
|
||||||
|
MinerId: miner.Id(),
|
||||||
|
PowHash: b.PowHash,
|
||||||
|
Main: BlockMainData{
|
||||||
|
Id: b.MainId,
|
||||||
|
Height: b.MainHeight,
|
||||||
|
Found: b.MainFound == "true",
|
||||||
|
},
|
||||||
|
Template: struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
}{
|
||||||
|
Id: b.MinerMainId,
|
||||||
|
Difficulty: b.MinerMainDiff,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if block.IsProofHigherThanDifficulty() {
|
||||||
|
block.Main.Found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range b.Uncles {
|
||||||
|
uncleMiner := db.GetOrCreateMinerByAddress(u.Wallet.ToBase58())
|
||||||
|
if uncleMiner == nil {
|
||||||
|
return nil, nil, errors.New("could not get or create miner")
|
||||||
|
}
|
||||||
|
|
||||||
|
uncle := &UncleBlock{
|
||||||
|
Block: Block{
|
||||||
|
Id: u.Id,
|
||||||
|
Height: u.Height,
|
||||||
|
PreviousId: u.PrevId,
|
||||||
|
Coinbase: BlockCoinbase{
|
||||||
|
Id: u.CoinbaseId,
|
||||||
|
Reward: u.CoinbaseReward,
|
||||||
|
PrivateKey: u.CoinbasePriv,
|
||||||
|
},
|
||||||
|
Difficulty: u.Diff,
|
||||||
|
Timestamp: u.Ts,
|
||||||
|
MinerId: uncleMiner.Id(),
|
||||||
|
PowHash: u.PowHash,
|
||||||
|
Main: BlockMainData{
|
||||||
|
Id: u.MainId,
|
||||||
|
Height: u.MainHeight,
|
||||||
|
Found: u.MainFound == "true",
|
||||||
|
},
|
||||||
|
Template: struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
}{
|
||||||
|
Id: u.MinerMainId,
|
||||||
|
Difficulty: u.MinerMainDiff,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ParentId: block.Id,
|
||||||
|
ParentHeight: block.Height,
|
||||||
|
}
|
||||||
|
|
||||||
|
if uncle.Block.IsProofHigherThanDifficulty() {
|
||||||
|
uncle.Block.Main.Found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
uncles = append(uncles, uncle)
|
||||||
|
}
|
||||||
|
return block, uncles, nil
|
||||||
|
} else if version.Version == 0 || version.Version == 1 {
|
||||||
|
|
||||||
|
var b JsonBlock1
|
||||||
|
if err = json.Unmarshal(data, &b); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
miner := db.GetOrCreateMinerByAddress(b.Wallet.ToBase58())
|
||||||
|
if miner == nil {
|
||||||
|
return nil, nil, errors.New("could not get or create miner")
|
||||||
|
}
|
||||||
|
|
||||||
|
block = &Block{
|
||||||
|
Id: b.Id,
|
||||||
|
Height: b.Height,
|
||||||
|
PreviousId: b.PrevId,
|
||||||
|
Coinbase: BlockCoinbase{
|
||||||
|
Id: b.TxCoinbase,
|
||||||
|
Reward: 0,
|
||||||
|
PrivateKey: b.TxPriv,
|
||||||
|
},
|
||||||
|
Difficulty: types.Difficulty{Uint128: uint128.From64(b.Diff)},
|
||||||
|
Timestamp: b.Ts,
|
||||||
|
MinerId: miner.Id(),
|
||||||
|
PowHash: b.PowHash,
|
||||||
|
Main: BlockMainData{
|
||||||
|
Id: b.MHash,
|
||||||
|
Height: b.MHeight,
|
||||||
|
Found: b.BlockFound == "true",
|
||||||
|
},
|
||||||
|
Template: struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
}{
|
||||||
|
Id: UndefinedHash,
|
||||||
|
Difficulty: UndefinedDifficulty,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range b.Uncles {
|
||||||
|
uncleMiner := db.GetOrCreateMinerByAddress(u.Wallet.ToBase58())
|
||||||
|
if uncleMiner == nil {
|
||||||
|
return nil, nil, errors.New("could not get or create miner")
|
||||||
|
}
|
||||||
|
|
||||||
|
uncle := &UncleBlock{
|
||||||
|
Block: Block{
|
||||||
|
Id: u.Id,
|
||||||
|
Height: u.Height,
|
||||||
|
PreviousId: u.PrevId,
|
||||||
|
Coinbase: BlockCoinbase{
|
||||||
|
Id: NilHash,
|
||||||
|
Reward: 0,
|
||||||
|
PrivateKey: NilHash,
|
||||||
|
},
|
||||||
|
Difficulty: types.Difficulty{Uint128: uint128.From64(b.Diff)},
|
||||||
|
Timestamp: u.Ts,
|
||||||
|
MinerId: uncleMiner.Id(),
|
||||||
|
PowHash: NilHash,
|
||||||
|
Main: BlockMainData{
|
||||||
|
Id: NilHash,
|
||||||
|
Height: 0,
|
||||||
|
Found: false,
|
||||||
|
},
|
||||||
|
Template: struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Difficulty types.Difficulty `json:"difficulty"`
|
||||||
|
}{
|
||||||
|
Id: UndefinedHash,
|
||||||
|
Difficulty: UndefinedDifficulty,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ParentId: block.Id,
|
||||||
|
ParentHeight: block.Height,
|
||||||
|
}
|
||||||
|
|
||||||
|
uncles = append(uncles, uncle)
|
||||||
|
}
|
||||||
|
return block, uncles, nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, fmt.Errorf("unknown version %d", version.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) GetBlock() *Block {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) IsProofHigherThanDifficulty() bool {
|
||||||
|
return b.GetProofDifficulty().Cmp(b.Template.Difficulty.Uint128) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) GetProofDifficulty() types.Difficulty {
|
||||||
|
base := uint256.NewInt(0).SetBytes32(bytes.Repeat([]byte{0xff}, 32))
|
||||||
|
pow := uint256.NewInt(0).SetBytes32(b.PowHash[:])
|
||||||
|
pow = &uint256.Int{bits.ReverseBytes64(pow[3]), bits.ReverseBytes64(pow[2]), bits.ReverseBytes64(pow[1]), bits.ReverseBytes64(pow[0])}
|
||||||
|
|
||||||
|
if pow.Eq(uint256.NewInt(0)) {
|
||||||
|
return types.Difficulty{}
|
||||||
|
}
|
||||||
|
|
||||||
|
powResult := uint256.NewInt(0).Div(base, pow).Bytes32()
|
||||||
|
return types.Difficulty{Uint128: uint128.FromBytes(powResult[16:]).ReverseBytes()}
|
||||||
|
}
|
64
database/coinbase_transaction.go
Normal file
64
database/coinbase_transaction.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CoinbaseTransaction struct {
|
||||||
|
id types.Hash
|
||||||
|
privateKey types.Hash
|
||||||
|
outputs []*CoinbaseTransactionOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCoinbaseTransaction(id types.Hash, privateKey types.Hash, outputs []*CoinbaseTransactionOutput) *CoinbaseTransaction {
|
||||||
|
return &CoinbaseTransaction{
|
||||||
|
id: id,
|
||||||
|
privateKey: privateKey,
|
||||||
|
outputs: outputs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CoinbaseTransaction) Outputs() []*CoinbaseTransactionOutput {
|
||||||
|
return t.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CoinbaseTransaction) Reward() (result uint64) {
|
||||||
|
for _, o := range t.outputs {
|
||||||
|
result += o.amount
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CoinbaseTransaction) OutputByIndex(index uint64) *CoinbaseTransactionOutput {
|
||||||
|
if uint64(len(t.outputs)) > index {
|
||||||
|
return t.outputs[index]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CoinbaseTransaction) OutputByMiner(miner uint64) *CoinbaseTransactionOutput {
|
||||||
|
if i := slices.IndexFunc(t.outputs, func(e *CoinbaseTransactionOutput) bool {
|
||||||
|
return e.Miner() == miner
|
||||||
|
}); i != -1 {
|
||||||
|
return t.outputs[i]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CoinbaseTransaction) PrivateKey() types.Hash {
|
||||||
|
return t.privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CoinbaseTransaction) Id() types.Hash {
|
||||||
|
return t.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CoinbaseTransaction) GetEphemeralPublicKey(miner *Miner, index int64) types.Hash {
|
||||||
|
if index != -1 {
|
||||||
|
return miner.MoneroAddress().GetEphemeralPublicKey(t.privateKey, uint64(index))
|
||||||
|
} else {
|
||||||
|
return miner.MoneroAddress().GetEphemeralPublicKey(t.privateKey, t.OutputByMiner(miner.Id()).Index())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
database/coinbase_transaction_output.go
Normal file
35
database/coinbase_transaction_output.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import "git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
|
||||||
|
type CoinbaseTransactionOutput struct {
|
||||||
|
id types.Hash
|
||||||
|
index uint64
|
||||||
|
amount uint64
|
||||||
|
miner uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCoinbaseTransactionOutput(id types.Hash, index, amount, miner uint64) *CoinbaseTransactionOutput {
|
||||||
|
return &CoinbaseTransactionOutput{
|
||||||
|
id: id,
|
||||||
|
index: index,
|
||||||
|
amount: amount,
|
||||||
|
miner: miner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CoinbaseTransactionOutput) Miner() uint64 {
|
||||||
|
return o.miner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CoinbaseTransactionOutput) Amount() uint64 {
|
||||||
|
return o.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CoinbaseTransactionOutput) Index() uint64 {
|
||||||
|
return o.index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CoinbaseTransactionOutput) Id() types.Hash {
|
||||||
|
return o.id
|
||||||
|
}
|
924
database/database.go
Normal file
924
database/database.go
Normal file
|
@ -0,0 +1,924 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
handle *sql.DB
|
||||||
|
statements struct {
|
||||||
|
GetMinerById *sql.Stmt
|
||||||
|
GetMinerByAddress *sql.Stmt
|
||||||
|
GetMinerByAddressBounds *sql.Stmt
|
||||||
|
InsertMiner *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheLock sync.RWMutex
|
||||||
|
minerCache map[uint64]*Miner
|
||||||
|
unclesByParentCache map[types.Hash][]*UncleBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatabase(connStr string) (db *Database, err error) {
|
||||||
|
db = &Database{
|
||||||
|
minerCache: make(map[uint64]*Miner, 1024),
|
||||||
|
unclesByParentCache: make(map[types.Hash][]*UncleBlock, p2pool.PPLNSWindow*16),
|
||||||
|
}
|
||||||
|
if db.handle, err = sql.Open("postgres", connStr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if db.statements.GetMinerById, err = db.handle.Prepare("SELECT id, address FROM miners WHERE id = $1;"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if db.statements.GetMinerByAddress, err = db.handle.Prepare("SELECT id, address FROM miners WHERE address = $1;"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if db.statements.GetMinerByAddressBounds, err = db.handle.Prepare("SELECT id, address FROM miners WHERE address LIKE $1 AND address LIKE $2;"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if db.statements.InsertMiner, err = db.handle.Prepare("INSERT INTO miners (address) VALUES ($1) RETURNING id, address;"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//cache methods
|
||||||
|
|
||||||
|
func (db *Database) GetMiner(miner uint64) *Miner {
|
||||||
|
if m := func() *Miner {
|
||||||
|
db.cacheLock.RLock()
|
||||||
|
defer db.cacheLock.RUnlock()
|
||||||
|
return db.minerCache[miner]
|
||||||
|
}(); m != nil {
|
||||||
|
return m
|
||||||
|
} else if m = db.getMiner(miner); m != nil {
|
||||||
|
db.cacheLock.Lock()
|
||||||
|
defer db.cacheLock.Unlock()
|
||||||
|
db.minerCache[miner] = m
|
||||||
|
return m
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetUnclesByParentId(id types.Hash) chan *UncleBlock {
|
||||||
|
if c := func() chan *UncleBlock {
|
||||||
|
db.cacheLock.RLock()
|
||||||
|
defer db.cacheLock.RUnlock()
|
||||||
|
if s, ok := db.unclesByParentCache[id]; ok {
|
||||||
|
c := make(chan *UncleBlock, len(s))
|
||||||
|
defer close(c)
|
||||||
|
for _, u := range s {
|
||||||
|
c <- u
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); c != nil {
|
||||||
|
return c
|
||||||
|
} else if c = db.getUnclesByParentId(id); c != nil {
|
||||||
|
c2 := make(chan *UncleBlock)
|
||||||
|
go func() {
|
||||||
|
val := make([]*UncleBlock, 0, 6)
|
||||||
|
defer func() {
|
||||||
|
db.cacheLock.Lock()
|
||||||
|
defer db.cacheLock.Unlock()
|
||||||
|
if len(db.unclesByParentCache) >= p2pool.PPLNSWindow*16 {
|
||||||
|
for k := range db.unclesByParentCache {
|
||||||
|
delete(db.unclesByParentCache, k)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.unclesByParentCache[id] = val
|
||||||
|
}()
|
||||||
|
defer close(c2)
|
||||||
|
for u := range c {
|
||||||
|
val = append(val, u)
|
||||||
|
c2 <- u
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return c2
|
||||||
|
} else {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) getMiner(miner uint64) *Miner {
|
||||||
|
if rows, err := db.statements.GetMinerById.Query(miner); err != nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
if rows.Next() {
|
||||||
|
m := &Miner{}
|
||||||
|
if err = rows.Scan(&m.id, &m.addr); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetMinerByAddress(addr string) *Miner {
|
||||||
|
if rows, err := db.statements.GetMinerByAddress.Query(addr); err != nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
if rows.Next() {
|
||||||
|
m := &Miner{}
|
||||||
|
if err = rows.Scan(&m.id, &m.addr); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetOrCreateMinerByAddress(addr string) *Miner {
|
||||||
|
if m := db.GetMinerByAddress(addr); m != nil {
|
||||||
|
return m
|
||||||
|
} else {
|
||||||
|
if rows, err := db.statements.InsertMiner.Query(addr); err != nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
if rows.Next() {
|
||||||
|
m = &Miner{}
|
||||||
|
if err = rows.Scan(&m.id, &m.addr); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetMinerByAddressBounds(addrStart, addrEnd string) *Miner {
|
||||||
|
if rows, err := db.statements.GetMinerByAddress.Query(addrStart+"%", "%"+addrEnd); err != nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
if rows.Next() {
|
||||||
|
m := &Miner{}
|
||||||
|
if err = rows.Scan(&m.id, &m.addr); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowScanInterface interface {
|
||||||
|
Scan(dest ...any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) Query(query string, callback func(row RowScanInterface) error, params ...any) error {
|
||||||
|
if stmt, err := db.handle.Prepare(query); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
return db.QueryStatement(stmt, callback, params...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) QueryStatement(stmt *sql.Stmt, callback func(row RowScanInterface) error, params ...any) error {
|
||||||
|
if rows, err := stmt.Query(params...); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
for callback != nil && rows.Next() {
|
||||||
|
//callback will call sql.Rows.Scan
|
||||||
|
if err = callback(rows); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetBlocksByQuery(where string, params ...any) chan *Block {
|
||||||
|
returnChannel := make(chan *Block)
|
||||||
|
go func() {
|
||||||
|
defer close(returnChannel)
|
||||||
|
err := db.Query(fmt.Sprintf("SELECT decode(id, 'hex'), height, decode(previous_id, 'hex'), decode(coinbase_id, 'hex'), coinbase_reward, decode(coinbase_privkey, 'hex'), difficulty, timestamp, miner, decode(pow_hash, 'hex'), main_height, decode(main_id, 'hex'), main_found, decode(miner_main_id, 'hex'), miner_main_difficulty FROM blocks %s;", where), func(row RowScanInterface) (err error) {
|
||||||
|
block := &Block{}
|
||||||
|
|
||||||
|
var difficultyHex, minerMainDifficultyHex string
|
||||||
|
|
||||||
|
var (
|
||||||
|
IdPtr = block.Id[:]
|
||||||
|
PreviousIdPtr = block.PreviousId[:]
|
||||||
|
CoinbaseIdPtr = block.Coinbase.Id[:]
|
||||||
|
CoinbasePrivateKeyPtr = block.Coinbase.PrivateKey[:]
|
||||||
|
PowHashPtr = block.PowHash[:]
|
||||||
|
MainIdPtr = block.Main.Id[:]
|
||||||
|
MinerMainId = block.Template.Id[:]
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = row.Scan(&IdPtr, &block.Height, &PreviousIdPtr, &CoinbaseIdPtr, &block.Coinbase.Reward, &CoinbasePrivateKeyPtr, &difficultyHex, &block.Timestamp, &block.MinerId, &PowHashPtr, &block.Main.Height, &MainIdPtr, &block.Main.Found, &MinerMainId, &minerMainDifficultyHex); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(block.Id[:], IdPtr)
|
||||||
|
copy(block.PreviousId[:], PreviousIdPtr)
|
||||||
|
copy(block.Coinbase.Id[:], CoinbaseIdPtr)
|
||||||
|
copy(block.Coinbase.PrivateKey[:], CoinbasePrivateKeyPtr)
|
||||||
|
copy(block.PowHash[:], PowHashPtr)
|
||||||
|
copy(block.Main.Id[:], MainIdPtr)
|
||||||
|
copy(block.Template.Id[:], MinerMainId)
|
||||||
|
|
||||||
|
block.Difficulty, _ = types.DifficultyFromString(difficultyHex)
|
||||||
|
block.Template.Difficulty, _ = types.DifficultyFromString(minerMainDifficultyHex)
|
||||||
|
|
||||||
|
returnChannel <- block
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, params...)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return returnChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetUncleBlocksByQuery(where string, params ...any) chan *UncleBlock {
|
||||||
|
returnChannel := make(chan *UncleBlock)
|
||||||
|
go func() {
|
||||||
|
defer close(returnChannel)
|
||||||
|
err := db.Query(fmt.Sprintf("SELECT decode(parent_id, 'hex'), parent_height, decode(id, 'hex'), height, decode(previous_id, 'hex'), decode(coinbase_id, 'hex'), coinbase_reward, decode(coinbase_privkey, 'hex'), difficulty, timestamp, miner, decode(pow_hash, 'hex'), main_height, decode(main_id, 'hex'), main_found, decode(miner_main_id, 'hex'), miner_main_difficulty FROM uncles %s;", where), func(row RowScanInterface) (err error) {
|
||||||
|
uncle := &UncleBlock{}
|
||||||
|
block := &uncle.Block
|
||||||
|
|
||||||
|
var difficultyHex, minerMainDifficultyHex string
|
||||||
|
|
||||||
|
var (
|
||||||
|
ParentId = uncle.ParentId[:]
|
||||||
|
IdPtr = block.Id[:]
|
||||||
|
PreviousIdPtr = block.PreviousId[:]
|
||||||
|
CoinbaseIdPtr = block.Coinbase.Id[:]
|
||||||
|
CoinbasePrivateKeyPtr = block.Coinbase.PrivateKey[:]
|
||||||
|
PowHashPtr = block.PowHash[:]
|
||||||
|
MainIdPtr = block.Main.Id[:]
|
||||||
|
MinerMainId = block.Template.Id[:]
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = row.Scan(&ParentId, &uncle.ParentHeight, &IdPtr, &block.Height, &PreviousIdPtr, &CoinbaseIdPtr, &block.Coinbase.Reward, &CoinbasePrivateKeyPtr, &difficultyHex, &block.Timestamp, &block.MinerId, &PowHashPtr, &block.Main.Height, &MainIdPtr, &block.Main.Found, &MinerMainId, &minerMainDifficultyHex); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(uncle.ParentId[:], ParentId)
|
||||||
|
copy(block.Id[:], IdPtr)
|
||||||
|
copy(block.PreviousId[:], PreviousIdPtr)
|
||||||
|
copy(block.Coinbase.Id[:], CoinbaseIdPtr)
|
||||||
|
copy(block.Coinbase.PrivateKey[:], CoinbasePrivateKeyPtr)
|
||||||
|
copy(block.PowHash[:], PowHashPtr)
|
||||||
|
copy(block.Main.Id[:], MainIdPtr)
|
||||||
|
copy(block.Template.Id[:], MinerMainId)
|
||||||
|
|
||||||
|
block.Difficulty, _ = types.DifficultyFromString(difficultyHex)
|
||||||
|
block.Template.Difficulty, _ = types.DifficultyFromString(minerMainDifficultyHex)
|
||||||
|
|
||||||
|
returnChannel <- uncle
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, params...)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return returnChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetBlockById(id types.Hash) *Block {
|
||||||
|
r := db.GetBlocksByQuery("WHERE id = $1;", id.String())
|
||||||
|
defer func() {
|
||||||
|
for range r {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return <-r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetBlockByPreviousId(id types.Hash) *Block {
|
||||||
|
r := db.GetBlocksByQuery("WHERE previous_id = $1;", id.String())
|
||||||
|
defer func() {
|
||||||
|
for range r {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return <-r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetBlockByHeight(height uint64) *Block {
|
||||||
|
r := db.GetBlocksByQuery("WHERE height = $1;", height)
|
||||||
|
defer func() {
|
||||||
|
for range r {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return <-r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetBlocksInWindow(startHeight *uint64, windowSize uint64) chan *Block {
|
||||||
|
if windowSize == 0 {
|
||||||
|
windowSize = p2pool.PPLNSWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
if startHeight == nil {
|
||||||
|
return db.GetBlocksByQuery("WHERE height > ((SELECT MAX(height) FROM blocks) - $1) AND height <= ((SELECT MAX(height) FROM blocks)) ORDER BY height DESC", windowSize)
|
||||||
|
} else {
|
||||||
|
return db.GetBlocksByQuery("WHERE height > ($1) AND height <= ($2) ORDER BY height DESC", *startHeight-windowSize, *startHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetBlocksByMinerIdInWindow(minerId uint64, startHeight *uint64, windowSize uint64) chan *Block {
|
||||||
|
if windowSize == 0 {
|
||||||
|
windowSize = p2pool.PPLNSWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
if startHeight == nil {
|
||||||
|
return db.GetBlocksByQuery("WHERE height > ((SELECT MAX(height) FROM blocks) - $2) AND height <= ((SELECT MAX(height) FROM blocks)) AND miner = $1 ORDER BY height DESC", minerId, windowSize)
|
||||||
|
} else {
|
||||||
|
return db.GetBlocksByQuery("WHERE height > ($2) AND height <= ($3) AND miner = $1 ORDER BY height DESC", minerId, *startHeight-windowSize, *startHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetChainTip() *Block {
|
||||||
|
r := db.GetBlocksByQuery("ORDER BY height DESC LIMIT 1;")
|
||||||
|
defer func() {
|
||||||
|
for range r {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return <-r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetLastFound() BlockInterface {
|
||||||
|
r := db.GetAllFound(1)
|
||||||
|
defer func() {
|
||||||
|
for range r {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return <-r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetUncleById(id types.Hash) *UncleBlock {
|
||||||
|
r := db.GetUncleBlocksByQuery("WHERE id = $1;", id.String())
|
||||||
|
defer func() {
|
||||||
|
for range r {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return <-r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) getUnclesByParentId(id types.Hash) chan *UncleBlock {
|
||||||
|
return db.GetUncleBlocksByQuery("WHERE parent_id = $1;", id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetUnclesByParentHeight(height uint64) chan *UncleBlock {
|
||||||
|
return db.GetUncleBlocksByQuery("WHERE parent_height = $1;", height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetUnclesInWindow(startHeight *uint64, windowSize uint64) chan *UncleBlock {
|
||||||
|
if windowSize == 0 {
|
||||||
|
windowSize = p2pool.PPLNSWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO add p2pool.UncleBlockDepth ?
|
||||||
|
if startHeight == nil {
|
||||||
|
return db.GetUncleBlocksByQuery("WHERE parent_height > ((SELECT MAX(height) FROM blocks) - $1) AND parent_height <= ((SELECT MAX(height) FROM blocks)) AND height > ((SELECT MAX(height) FROM blocks) - $1) AND height <= ((SELECT MAX(height) FROM blocks)) ORDER BY height DESC", windowSize)
|
||||||
|
} else {
|
||||||
|
return db.GetUncleBlocksByQuery("WHERE parent_height > ($1) AND parent_height <= ($2) AND height > ($1) AND height <= ($2) ORDER BY height DESC", *startHeight-windowSize, *startHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetUnclesByMinerIdInWindow(minerId uint64, startHeight *uint64, windowSize uint64) chan *UncleBlock {
|
||||||
|
if windowSize == 0 {
|
||||||
|
windowSize = p2pool.PPLNSWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO add p2pool.UncleBlockDepth ?
|
||||||
|
if startHeight == nil {
|
||||||
|
return db.GetUncleBlocksByQuery("WHERE parent_height > ((SELECT MAX(height) FROM blocks) - $2) AND parent_height <= ((SELECT MAX(height) FROM blocks)) AND height > ((SELECT MAX(height) FROM blocks) - $2) AND height <= ((SELECT MAX(height) FROM blocks)) AND miner = $1 ORDER BY height DESC", minerId, windowSize)
|
||||||
|
} else {
|
||||||
|
return db.GetUncleBlocksByQuery("WHERE parent_height > ($3 - $2) AND parent_height <= ($3) AND height > ($2) AND height <= ($3) AND miner = $1 ORDER BY height DESC", minerId, *startHeight-windowSize, *startHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetAllFound(limit uint64) chan BlockInterface {
|
||||||
|
blocks := db.GetFound(limit)
|
||||||
|
uncles := db.GetFoundUncles(limit)
|
||||||
|
|
||||||
|
result := make(chan BlockInterface)
|
||||||
|
go func() {
|
||||||
|
defer close(result)
|
||||||
|
defer func() {
|
||||||
|
for range blocks {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
for range uncles {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var i uint64
|
||||||
|
|
||||||
|
var currentBlock *Block
|
||||||
|
var currentUncle *UncleBlock
|
||||||
|
|
||||||
|
for {
|
||||||
|
var current BlockInterface
|
||||||
|
|
||||||
|
if limit != 0 && i >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentBlock == nil {
|
||||||
|
currentBlock = <-blocks
|
||||||
|
}
|
||||||
|
if currentUncle == nil {
|
||||||
|
currentUncle = <-uncles
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentBlock != nil {
|
||||||
|
if current == nil || currentBlock.Main.Height > current.GetBlock().Main.Height {
|
||||||
|
current = currentBlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentUncle != nil {
|
||||||
|
if current == nil || currentUncle.Block.Main.Height > current.GetBlock().Main.Height {
|
||||||
|
current = currentUncle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentBlock == current {
|
||||||
|
currentBlock = nil
|
||||||
|
} else if currentUncle == current {
|
||||||
|
currentUncle = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result <- current
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetShares(limit uint64, minerId uint64, onlyBlocks bool) chan BlockInterface {
|
||||||
|
var blocks chan *Block
|
||||||
|
var uncles chan *UncleBlock
|
||||||
|
|
||||||
|
if limit == 0 {
|
||||||
|
if minerId != 0 {
|
||||||
|
blocks = db.GetBlocksByQuery("WHERE miner = $1 ORDER BY height DESC;", limit, minerId)
|
||||||
|
if !onlyBlocks {
|
||||||
|
uncles = db.GetUncleBlocksByQuery("WHERE miner = $1ORDER BY height DESC, timestamp DESC;", minerId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blocks = db.GetBlocksByQuery("ORDER BY height DESC;")
|
||||||
|
if !onlyBlocks {
|
||||||
|
uncles = db.GetUncleBlocksByQuery("ORDER BY height DESC, timestamp DESC;")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if minerId != 0 {
|
||||||
|
blocks = db.GetBlocksByQuery("WHERE miner = $2 ORDER BY height DESC LIMIT $1;", limit, minerId)
|
||||||
|
if !onlyBlocks {
|
||||||
|
uncles = db.GetUncleBlocksByQuery("WHERE miner = $2 ORDER BY height DESC, timestamp DESC LIMIT $1;", limit, minerId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blocks = db.GetBlocksByQuery("ORDER BY height DESC LIMIT $1;", limit)
|
||||||
|
if !onlyBlocks {
|
||||||
|
uncles = db.GetUncleBlocksByQuery("ORDER BY height DESC, timestamp DESC LIMIT $1;", limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(chan BlockInterface)
|
||||||
|
go func() {
|
||||||
|
defer close(result)
|
||||||
|
defer func() {
|
||||||
|
for range blocks {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
for range uncles {
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var i uint64
|
||||||
|
|
||||||
|
var currentBlock *Block
|
||||||
|
var currentUncle *UncleBlock
|
||||||
|
|
||||||
|
for {
|
||||||
|
var current BlockInterface
|
||||||
|
|
||||||
|
if limit != 0 && i >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentBlock == nil {
|
||||||
|
currentBlock = <-blocks
|
||||||
|
}
|
||||||
|
if !onlyBlocks && currentUncle == nil {
|
||||||
|
currentUncle = <-uncles
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentBlock != nil {
|
||||||
|
if current == nil || currentBlock.Height > current.GetBlock().Height {
|
||||||
|
current = currentBlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !onlyBlocks && currentUncle != nil {
|
||||||
|
if current == nil || currentUncle.Block.Height > current.GetBlock().Height {
|
||||||
|
current = currentUncle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentBlock == current {
|
||||||
|
currentBlock = nil
|
||||||
|
} else if !onlyBlocks && currentUncle == current {
|
||||||
|
currentUncle = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result <- current
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetFound(limit uint64) chan *Block {
|
||||||
|
if limit == 0 {
|
||||||
|
return db.GetBlocksByQuery("WHERE main_found IS TRUE ORDER BY main_height DESC;")
|
||||||
|
} else {
|
||||||
|
return db.GetBlocksByQuery("WHERE main_found IS TRUE ORDER BY main_height DESC LIMIT $1;", limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetFoundUncles(limit uint64) chan *UncleBlock {
|
||||||
|
if limit == 0 {
|
||||||
|
return db.GetUncleBlocksByQuery("WHERE main_found IS TRUE ORDER BY main_height DESC;")
|
||||||
|
} else {
|
||||||
|
return db.GetUncleBlocksByQuery("WHERE main_found IS TRUE ORDER BY main_height DESC LIMIT $1;", limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) DeleteBlockById(id types.Hash) (n int, err error) {
|
||||||
|
if block := db.GetBlockById(id); block == nil {
|
||||||
|
return 0, nil
|
||||||
|
} else {
|
||||||
|
for {
|
||||||
|
if err = db.Query("DELETE FROM coinbase_outputs WHERE id = (SELECT coinbase_id FROM blocks WHERE id = $1) OR id = (SELECT coinbase_id FROM uncles WHERE id = $1);", nil, id.String()); err != nil {
|
||||||
|
return n, err
|
||||||
|
} else if err = db.Query("DELETE FROM uncles WHERE parent_id = $1;", nil, id.String()); err != nil {
|
||||||
|
return n, err
|
||||||
|
} else if err = db.Query("DELETE FROM blocks WHERE id = $1;", nil, id.String()); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
block = db.GetBlockByPreviousId(block.Id)
|
||||||
|
if block == nil {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) SetBlockMainDifficulty(id types.Hash, difficulty types.Difficulty) error {
|
||||||
|
if err := db.Query("UPDATE blocks SET miner_main_difficulty = $2 WHERE id = $1;", nil, id.String(), difficulty.String()); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = db.Query("UPDATE uncles SET miner_main_difficulty = $2 WHERE id = $1;", nil, id.String(), difficulty.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) SetBlockFound(id types.Hash, found bool) error {
|
||||||
|
if err := db.Query("UPDATE blocks SET main_found = $2 WHERE id = $1;", nil, id.String(), found); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = db.Query("UPDATE uncles SET main_found = $2 WHERE id = $1;", nil, id.String(), found); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
if err := db.Query("DELETE FROM coinbase_outputs WHERE id = (SELECT coinbase_id FROM blocks WHERE id = $1) OR id = (SELECT coinbase_id FROM uncles WHERE id = $1);", nil, id.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) CoinbaseTransactionExists(block *Block) bool {
|
||||||
|
var count uint64
|
||||||
|
if err := db.Query("SELECT COUNT(*) as count FROM coinbase_outputs WHERE id = $1;", func(row RowScanInterface) error {
|
||||||
|
return row.Scan(&count)
|
||||||
|
}, block.Coinbase.Id.String()); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetCoinbaseTransaction(block *Block) *CoinbaseTransaction {
|
||||||
|
var outputs []*CoinbaseTransactionOutput
|
||||||
|
if err := db.Query("SELECT index, amount, miner FROM coinbase_outputs WHERE id = $1 ORDER BY index DESC;", func(row RowScanInterface) error {
|
||||||
|
output := &CoinbaseTransactionOutput{
|
||||||
|
id: block.Coinbase.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.Scan(&output.index, &output.amount, &output.miner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outputs = append(outputs, output)
|
||||||
|
return nil
|
||||||
|
}, block.Coinbase.Id.String()); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CoinbaseTransaction{
|
||||||
|
id: block.Coinbase.Id,
|
||||||
|
privateKey: block.Coinbase.PrivateKey,
|
||||||
|
outputs: outputs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetCoinbaseTransactionOutputByIndex(coinbaseId types.Hash, index uint64) *CoinbaseTransactionOutput {
|
||||||
|
output := &CoinbaseTransactionOutput{
|
||||||
|
id: coinbaseId,
|
||||||
|
index: index,
|
||||||
|
}
|
||||||
|
if err := db.Query("SELECT amount, miner FROM coinbase_outputs WHERE id = $1 AND index = $2 ORDER BY index DESC;", func(row RowScanInterface) error {
|
||||||
|
if err := row.Scan(&output.amount, &output.miner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, coinbaseId.String(), index); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetCoinbaseTransactionOutputByMinerId(coinbaseId types.Hash, minerId uint64) *CoinbaseTransactionOutput {
|
||||||
|
output := &CoinbaseTransactionOutput{
|
||||||
|
id: coinbaseId,
|
||||||
|
miner: minerId,
|
||||||
|
}
|
||||||
|
if err := db.Query("SELECT amount, index FROM coinbase_outputs WHERE id = $1 AND miner = $2 ORDER BY index DESC;", func(row RowScanInterface) error {
|
||||||
|
if err := row.Scan(&output.amount, &output.index); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, coinbaseId.String(), minerId); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payout struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Main struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
} `json:"main"`
|
||||||
|
Timestamp uint64 `json:"timestamp"`
|
||||||
|
Uncle bool `json:"uncle"`
|
||||||
|
Coinbase struct {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Reward uint64 `json:"reward"`
|
||||||
|
PrivateKey types.Hash `json:"private_key"`
|
||||||
|
Miner uint64 `json:"miner"`
|
||||||
|
Index uint64 `json:"index"`
|
||||||
|
} `json:"coinbase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) GetPayoutsByMinerId(minerId uint64, limit uint64) chan *Payout {
|
||||||
|
out := make(chan *Payout)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(out)
|
||||||
|
resultFunc := func(row 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, ×tamp, &privKey, &uncle, &coinbaseId, &amount, &index); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out <- &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 {
|
||||||
|
Id types.Hash `json:"id"`
|
||||||
|
Reward uint64 `json:"reward"`
|
||||||
|
PrivateKey types.Hash `json:"private_key"`
|
||||||
|
Miner uint64 `json:"miner"`
|
||||||
|
Index uint64 `json:"index"`
|
||||||
|
}{Id: types.HashFromBytes(coinbaseId), Reward: amount, PrivateKey: types.HashFromBytes(privKey), Index: index, Miner: minerId},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit == 0 {
|
||||||
|
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\n(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 ORDER BY main_height DESC;", resultFunc, minerId); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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\n(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 ORDER BY main_height DESC LIMIT $2;", resultFunc, minerId, limit); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) InsertCoinbaseTransaction(coinbase *CoinbaseTransaction) error {
|
||||||
|
if tx, err := db.handle.BeginTx(context.Background(), nil); err != nil {
|
||||||
|
return err
|
||||||
|
} else if stmt, err := tx.Prepare("INSERT INTO coinbase_outputs (id, index, miner, amount) VALUES ($1, $2, $3, $4);"); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer stmt.Close()
|
||||||
|
for _, o := range coinbase.Outputs() {
|
||||||
|
if rows, err := stmt.Query(o.Id().String(), o.Index(), o.Miner(), o.Amount()); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
if err = rows.Close(); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) InsertBlock(b *Block, fallbackDifficulty *types.Difficulty) error {
|
||||||
|
|
||||||
|
block := db.GetBlockById(b.Id)
|
||||||
|
mainDiff := b.Template.Difficulty
|
||||||
|
if mainDiff == UndefinedDifficulty && fallbackDifficulty != nil {
|
||||||
|
mainDiff = *fallbackDifficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
if block != nil { //Update found status if existent
|
||||||
|
if block.Template.Difficulty != mainDiff && mainDiff != UndefinedDifficulty {
|
||||||
|
if err := db.SetBlockMainDifficulty(block.Id, mainDiff); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Main.Found && !block.Main.Found {
|
||||||
|
if err := db.SetBlockFound(block.Id, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Query(
|
||||||
|
"INSERT INTO blocks (id, height, previous_id, coinbase_id, coinbase_reward, coinbase_privkey, difficulty, timestamp, miner, pow_hash, main_height, main_id, main_found, miner_main_id, miner_main_difficulty) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15);",
|
||||||
|
nil,
|
||||||
|
b.Id.String(),
|
||||||
|
b.Height,
|
||||||
|
b.PreviousId.String(),
|
||||||
|
b.Coinbase.Id.String(),
|
||||||
|
b.Coinbase.Reward,
|
||||||
|
b.Coinbase.PrivateKey.String(),
|
||||||
|
b.Difficulty.String(),
|
||||||
|
b.Timestamp,
|
||||||
|
b.MinerId,
|
||||||
|
b.PowHash.String(),
|
||||||
|
b.Main.Height,
|
||||||
|
b.Main.Id.String(),
|
||||||
|
b.Main.Found,
|
||||||
|
b.Template.Id.String(),
|
||||||
|
b.Template.Difficulty.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) InsertUncleBlock(u *UncleBlock, fallbackDifficulty *types.Difficulty) error {
|
||||||
|
|
||||||
|
if b := db.GetBlockById(u.ParentId); b == nil {
|
||||||
|
return errors.New("parent does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
uncle := db.GetUncleById(u.Block.Id)
|
||||||
|
|
||||||
|
mainDiff := u.Block.Template.Difficulty
|
||||||
|
if mainDiff == UndefinedDifficulty && fallbackDifficulty != nil {
|
||||||
|
mainDiff = *fallbackDifficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
if uncle != nil { //Update found status if existent
|
||||||
|
if uncle.Block.Template.Difficulty != mainDiff && mainDiff != UndefinedDifficulty {
|
||||||
|
if err := db.SetBlockMainDifficulty(u.Block.Id, mainDiff); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Block.Main.Found && !uncle.Block.Main.Found {
|
||||||
|
if err := db.SetBlockFound(u.Block.Id, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Query(
|
||||||
|
"INSERT INTO uncles (parent_id, parent_height, id, height, previous_id, coinbase_id, coinbase_reward, coinbase_privkey, difficulty, timestamp, miner, pow_hash, main_height, main_id, main_found, miner_main_id, miner_main_difficulty) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17);",
|
||||||
|
nil,
|
||||||
|
u.ParentId.String(),
|
||||||
|
u.ParentHeight,
|
||||||
|
u.Block.Id.String(),
|
||||||
|
u.Block.Height,
|
||||||
|
u.Block.PreviousId.String(),
|
||||||
|
u.Block.Coinbase.Id.String(),
|
||||||
|
u.Block.Coinbase.Reward,
|
||||||
|
u.Block.Coinbase.PrivateKey.String(),
|
||||||
|
u.Block.Difficulty.String(),
|
||||||
|
u.Block.Timestamp,
|
||||||
|
u.Block.MinerId,
|
||||||
|
u.Block.PowHash.String(),
|
||||||
|
u.Block.Main.Height,
|
||||||
|
u.Block.Main.Id.String(),
|
||||||
|
u.Block.Main.Found,
|
||||||
|
u.Block.Template.Id.String(),
|
||||||
|
u.Block.Template.Difficulty.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) Close() error {
|
||||||
|
|
||||||
|
//cleanup statements
|
||||||
|
v := reflect.ValueOf(db.statements)
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
if stmt, ok := v.Field(i).Interface().(*sql.Stmt); ok && stmt != nil {
|
||||||
|
//v.Field(i).Elem().Set(reflect.ValueOf((*sql.Stmt)(nil)))
|
||||||
|
stmt.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.handle.Close()
|
||||||
|
}
|
93
database/miner.go
Normal file
93
database/miner.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"filippo.io/edwards25519"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/block"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Miner struct {
|
||||||
|
id uint64
|
||||||
|
addr string
|
||||||
|
moneroAddress atomic.Pointer[address.Address]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Miner) Id() uint64 {
|
||||||
|
return m.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Miner) Address() string {
|
||||||
|
return m.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Miner) MoneroAddress() *address.Address {
|
||||||
|
if a := m.moneroAddress.Load(); a != nil {
|
||||||
|
return a
|
||||||
|
} else {
|
||||||
|
a = address.FromBase58(m.addr)
|
||||||
|
m.moneroAddress.Store(a)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type addressSortType [types.HashSize * 2]byte
|
||||||
|
|
||||||
|
type outputResult struct {
|
||||||
|
Miner *Miner
|
||||||
|
Output *block.CoinbaseTransactionOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchOutputs(c *block.CoinbaseTransaction, miners []*Miner, privateKey types.Hash) (result []outputResult) {
|
||||||
|
addresses := make(map[addressSortType]*Miner, len(miners))
|
||||||
|
|
||||||
|
outputs := c.Outputs
|
||||||
|
|
||||||
|
var k addressSortType
|
||||||
|
for _, m := range miners {
|
||||||
|
copy(k[:], m.MoneroAddress().SpendPub.Bytes())
|
||||||
|
copy(k[types.HashSize:], m.MoneroAddress().ViewPub.Bytes())
|
||||||
|
addresses[k] = m
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedAddresses := maps.Keys(addresses)
|
||||||
|
|
||||||
|
slices.SortFunc(sortedAddresses, func(a addressSortType, b addressSortType) bool {
|
||||||
|
return bytes.Compare(a[:], b[:]) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
result = make([]outputResult, 0, len(miners))
|
||||||
|
|
||||||
|
for _, k = range sortedAddresses {
|
||||||
|
miner := addresses[k]
|
||||||
|
|
||||||
|
pK, _ := edwards25519.NewScalar().SetCanonicalBytes(privateKey[:])
|
||||||
|
derivation := miner.MoneroAddress().GetDerivationForPrivateKey(pK)
|
||||||
|
for i, o := range outputs {
|
||||||
|
if o == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sharedData := address.GetDerivationSharedDataForOutputIndex(derivation, uint64(o.Index))
|
||||||
|
if bytes.Compare(o.EphemeralPublicKey[:], miner.MoneroAddress().GetPublicKeyForSharedData(sharedData).Bytes()) == 0 {
|
||||||
|
//TODO: maybe clone?
|
||||||
|
result = append(result, outputResult{
|
||||||
|
Miner: miner,
|
||||||
|
Output: o,
|
||||||
|
})
|
||||||
|
outputs[i] = nil
|
||||||
|
outputs = slices.Compact(outputs)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(result, func(a outputResult, b outputResult) bool {
|
||||||
|
return a.Output.Index < b.Output.Index
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
14
database/subscription.go
Normal file
14
database/subscription.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
type Subscription struct {
|
||||||
|
miner uint64
|
||||||
|
nick string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) Miner() uint64 {
|
||||||
|
return s.miner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscription) Nick() string {
|
||||||
|
return s.nick
|
||||||
|
}
|
13
database/uncle_block.go
Normal file
13
database/uncle_block.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import "git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
|
||||||
|
type UncleBlock struct {
|
||||||
|
Block Block
|
||||||
|
ParentId types.Hash
|
||||||
|
ParentHeight uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UncleBlock) GetBlock() *Block {
|
||||||
|
return &u.Block
|
||||||
|
}
|
19
go.mod
Normal file
19
go.mod
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
module git.gammaspectra.live/P2Pool/p2pool-observer
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.0.0
|
||||||
|
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20221005074023-b6ca970f3050
|
||||||
|
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20221007140323-a2daa2d5fc48
|
||||||
|
github.com/ake-persson/mapslice-json v0.0.0-20210720081907-22c8edf57807
|
||||||
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/holiman/uint256 v1.2.1
|
||||||
|
github.com/jxskiss/base62 v1.1.0
|
||||||
|
github.com/lib/pq v1.10.7
|
||||||
|
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
|
||||||
|
golang.org/x/exp v0.0.0-20221006183845-316c7553db56
|
||||||
|
lukechampine.com/uint128 v1.2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
|
74
go.sum
Normal file
74
go.sum
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
|
||||||
|
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||||
|
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20221005074023-b6ca970f3050 h1:OoSrxnnLqy/HIsZy/KcR52WHaJiB4RiTMaHlMfY/740=
|
||||||
|
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20221005074023-b6ca970f3050/go.mod h1:FsZfI1sMcXZr55H0aKJ1LhMdPpW5YuyCubu5ilvQ0fM=
|
||||||
|
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20221007140323-a2daa2d5fc48 h1:ExrYG0RSrx/I4McPWgUF4B8R2OkblMrMki2ia8vG6Bw=
|
||||||
|
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20221007140323-a2daa2d5fc48/go.mod h1:XeSC8jK8RXnnzVAmp9e9AQZCDIbML3UoCRkxxGA+lpU=
|
||||||
|
github.com/ake-persson/mapslice-json v0.0.0-20210720081907-22c8edf57807 h1:w3nrGk00TWs/4iZ3Q0k9c0vL0e/wRziArKU4e++d/nA=
|
||||||
|
github.com/ake-persson/mapslice-json v0.0.0-20210720081907-22c8edf57807/go.mod h1:fGnnfniJiO/ajHAVHqMSUSL8sE9LmU9rzclCtoeB+y8=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
|
||||||
|
github.com/gordonklaus/ineffassign v0.0.0-20210522101830-0589229737b2/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/holiman/uint256 v1.2.1 h1:XRtyuda/zw2l+Bq/38n5XUoEF72aSOu/77Thd9pPp2o=
|
||||||
|
github.com/holiman/uint256 v1.2.1/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
|
||||||
|
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
|
||||||
|
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
|
||||||
|
github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||||
|
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
|
||||||
|
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/exp v0.0.0-20221006183845-316c7553db56 h1:BrYbdKcCNjLyrN6aKqXy4hPw9qGI8IATkj4EWv9Q+kQ=
|
||||||
|
golang.org/x/exp v0.0.0-20221006183845-316c7553db56/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
|
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
|
||||||
|
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||||
|
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
127
monero/address/address.go
Normal file
127
monero/address/address.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package address
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"filippo.io/edwards25519"
|
||||||
|
"git.gammaspectra.live/P2Pool/moneroutil"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Address struct {
|
||||||
|
Network uint8
|
||||||
|
SpendPub edwards25519.Point
|
||||||
|
ViewPub edwards25519.Point
|
||||||
|
Checksum []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var scalar8, _ = edwards25519.NewScalar().SetCanonicalBytes([]byte{8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||||
|
|
||||||
|
func FromBase58(address string) *Address {
|
||||||
|
raw := moneroutil.DecodeMoneroBase58(address)
|
||||||
|
|
||||||
|
if len(raw) != 69 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
checksum := moneroutil.GetChecksum(raw[:65])
|
||||||
|
if bytes.Compare(checksum[:], raw[65:]) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
a := &Address{
|
||||||
|
Network: raw[0],
|
||||||
|
Checksum: checksum[:],
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := a.SpendPub.SetBytes(raw[1:33]); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := a.ViewPub.SetBytes(raw[33:65]); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromRawAddress(network uint8, spend, view types.Hash) *Address {
|
||||||
|
nice := make([]byte, 69)
|
||||||
|
nice[0] = network
|
||||||
|
copy(nice[1:], spend[:])
|
||||||
|
copy(nice[33:], view[:])
|
||||||
|
|
||||||
|
checksum := moneroutil.GetChecksum(nice[:65])
|
||||||
|
a := &Address{
|
||||||
|
Network: nice[0],
|
||||||
|
Checksum: checksum[:],
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := a.SpendPub.SetBytes(nice[1:33]); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := a.ViewPub.SetBytes(nice[33:65]); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Address) ToBase58() string {
|
||||||
|
return moneroutil.EncodeMoneroBase58([]byte{a.Network}, a.SpendPub.Bytes(), a.ViewPub.Bytes(), a.Checksum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Address) GetDerivationForPrivateKey(privateKey *edwards25519.Scalar) *edwards25519.Point {
|
||||||
|
point := (&edwards25519.Point{}).ScalarMult(privateKey, &a.ViewPub)
|
||||||
|
return (&edwards25519.Point{}).ScalarMult(scalar8, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDerivationSharedDataForOutputIndex(derivation *edwards25519.Point, outputIndex uint64) *edwards25519.Scalar {
|
||||||
|
varIntBuf := make([]byte, binary.MaxVarintLen64)
|
||||||
|
data := append(derivation.Bytes(), varIntBuf[:binary.PutUvarint(varIntBuf, outputIndex)]...)
|
||||||
|
hasher := sha3.NewLegacyKeccak256()
|
||||||
|
if _, err := hasher.Write(data); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var wideBytes [64]byte
|
||||||
|
copy(wideBytes[:], hasher.Sum([]byte{}))
|
||||||
|
scalar, _ := edwards25519.NewScalar().SetUniformBytes(wideBytes[:])
|
||||||
|
|
||||||
|
return scalar
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Address) GetPublicKeyForSharedData(sharedData *edwards25519.Scalar) *edwards25519.Point {
|
||||||
|
sG := (&edwards25519.Point{}).ScalarBaseMult(sharedData)
|
||||||
|
|
||||||
|
return (&edwards25519.Point{}).Add(sG, &a.SpendPub)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Address) GetEphemeralPublicKey(privateKey types.Hash, outputIndex uint64) (result types.Hash) {
|
||||||
|
pK, _ := edwards25519.NewScalar().SetCanonicalBytes(privateKey[:])
|
||||||
|
copy(result[:], a.GetPublicKeyForSharedData(GetDerivationSharedDataForOutputIndex(a.GetDerivationForPrivateKey(pK), outputIndex)).Bytes())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Address) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(a.ToBase58())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Address) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr := FromBase58(s); addr != nil {
|
||||||
|
a.Network = addr.Network
|
||||||
|
a.SpendPub = addr.SpendPub
|
||||||
|
a.ViewPub = addr.ViewPub
|
||||||
|
a.Checksum = addr.Checksum
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("invalid address")
|
||||||
|
}
|
||||||
|
}
|
33
monero/address/address_test.go
Normal file
33
monero/address/address_test.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package address
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"filippo.io/edwards25519"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var privateKey = edwards25519.NewScalar()
|
||||||
|
|
||||||
|
var testAddress = FromBase58("42HEEF3NM9cHkJoPpDhNyJHuZ6DFhdtymCohF9CwP5KPM1Mp3eH2RVXCPRrxe4iWRogT7299R8PP7drGvThE8bHmRDq1qWp")
|
||||||
|
|
||||||
|
var ephemeralPubKey, _ = hex.DecodeString("20efc1310db960b0e8d22c8b85b3414fcaa1ed9aab40cf757321dd6099a62d5e")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
h, _ := hex.DecodeString("74b98b1e7ce5fc50d1634f8634622395ec2a19a4698a016fedd8139df374ac00")
|
||||||
|
if _, err := privateKey.SetCanonicalBytes(h); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddress(t *testing.T) {
|
||||||
|
derivation := testAddress.GetDerivationForPrivateKey(privateKey)
|
||||||
|
|
||||||
|
sharedData := GetDerivationSharedDataForOutputIndex(derivation, 37)
|
||||||
|
ephemeralPublicKey := testAddress.GetPublicKeyForSharedData(sharedData)
|
||||||
|
|
||||||
|
if bytes.Compare(ephemeralPublicKey.Bytes(), ephemeralPubKey) != 0 {
|
||||||
|
t.Fatalf("ephemeral key mismatch, expected %s, got %s", hex.EncodeToString(ephemeralPubKey), hex.EncodeToString(ephemeralPublicKey.Bytes()))
|
||||||
|
}
|
||||||
|
}
|
112
monero/client/client.go
Normal file
112
monero/client/client.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc"
|
||||||
|
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/block"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var client atomic.Pointer[Client]
|
||||||
|
|
||||||
|
var lock sync.Mutex
|
||||||
|
|
||||||
|
var address = "http://localhost:18081"
|
||||||
|
|
||||||
|
func SetClientSettings(addr string) {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
address = addr
|
||||||
|
client.Store(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClient() *Client {
|
||||||
|
if c := client.Load(); c == nil {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if c = client.Load(); c == nil {
|
||||||
|
//fallback for lock racing
|
||||||
|
if c, err := newClient(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
} else {
|
||||||
|
client.Store(c)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client TODO: ratelimit
|
||||||
|
type Client struct {
|
||||||
|
c *rpc.Client
|
||||||
|
d *daemon.Client
|
||||||
|
throttler <-chan time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient() (*Client, error) {
|
||||||
|
c, err := rpc.NewClient(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
c: c,
|
||||||
|
d: daemon.NewClient(c),
|
||||||
|
throttler: time.Tick(time.Second / 2),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetCoinbaseTransaction(txId types.Hash) (*block.CoinbaseTransaction, error) {
|
||||||
|
<-c.throttler
|
||||||
|
if result, err := c.d.GetTransactions(context.Background(), []string{txId.String()}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
if len(result.Txs) != 1 {
|
||||||
|
return nil, errors.New("invalid transaction count")
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf, err := hex.DecodeString(result.Txs[0].PrunedAsHex); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
tx := &block.CoinbaseTransaction{}
|
||||||
|
if err = tx.FromReader(bytes.NewReader(buf)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tx.Id() != txId {
|
||||||
|
return nil, fmt.Errorf("expected %s, got %s", txId.String(), tx.Id().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetBlockHeaderByHeight(height uint64) (*daemon.GetBlockHeaderByHeightResult, error) {
|
||||||
|
<-c.throttler
|
||||||
|
if result, err := c.d.GetBlockHeaderByHeight(context.Background(), height); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetBlockTemplate(address string) (*daemon.GetBlockTemplateResult, error) {
|
||||||
|
<-c.throttler
|
||||||
|
if result, err := c.d.GetBlockTemplate(context.Background(), address, 60); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
5
monero/constants.go
Normal file
5
monero/constants.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package monero
|
||||||
|
|
||||||
|
const (
|
||||||
|
BlockTime = 60 * 2
|
||||||
|
)
|
320
p2pool/api/api.go
Normal file
320
p2pool/api/api.go
Normal file
|
@ -0,0 +1,320 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/database"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool"
|
||||||
|
p2poolblock "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/block"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"io"
|
||||||
|
"lukechampine.com/uint128"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Api struct {
|
||||||
|
db *database.Database
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *database.Database, p string) (*Api, error) {
|
||||||
|
api := &Api{
|
||||||
|
db: db,
|
||||||
|
path: path.Clean(p),
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, err := os.Stat(api.path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !info.IsDir() {
|
||||||
|
return nil, fmt.Errorf("directory path does not exist %s %s", p, api.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) getBlockPath(height uint64) string {
|
||||||
|
index := strconv.FormatInt(int64(height), 10)
|
||||||
|
return fmt.Sprintf("%s/share/%s/%s", a.path, index[len(index)-1:], index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) getRawBlockPath(id types.Hash) string {
|
||||||
|
index := id.String()
|
||||||
|
return fmt.Sprintf("%s/blocks/%s/%s", a.path, index[:1], index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) getFailedRawBlockPath(id types.Hash) string {
|
||||||
|
index := id.String()
|
||||||
|
return fmt.Sprintf("%s/failed_blocks/%s/%s", a.path, index[:1], index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) BlockExists(height uint64) bool {
|
||||||
|
_, err := os.Stat(a.getBlockPath(height))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetShareEntry(height uint64) (*database.Block, []*database.UncleBlock, error) {
|
||||||
|
if f, err := os.Open(a.getBlockPath(height)); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else {
|
||||||
|
defer f.Close()
|
||||||
|
if buf, err := io.ReadAll(f); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else {
|
||||||
|
return database.NewBlockFromJSONBlock(a.db, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetShareFromFailedRawEntry(id types.Hash) (*database.Block, error) {
|
||||||
|
|
||||||
|
if b, err := a.GetFailedRawBlock(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
b, _, err := database.NewBlockFromBinaryBlock(a.db, b, nil, false)
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetFailedRawBlockBytes(id types.Hash) (buf []byte, err error) {
|
||||||
|
if f, err := os.Open(a.getFailedRawBlockPath(id)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer f.Close()
|
||||||
|
return io.ReadAll(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetFailedRawBlock(id types.Hash) (b *p2poolblock.Block, err error) {
|
||||||
|
if buf, err := a.GetFailedRawBlockBytes(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
data := make([]byte, len(buf)/2)
|
||||||
|
_, _ = hex.Decode(data, buf)
|
||||||
|
return p2poolblock.NewBlockFromBytes(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetRawBlockBytes(id types.Hash) (buf []byte, err error) {
|
||||||
|
if f, err := os.Open(a.getRawBlockPath(id)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer f.Close()
|
||||||
|
return io.ReadAll(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetRawBlock(id types.Hash) (b *p2poolblock.Block, err error) {
|
||||||
|
if buf, err := a.GetRawBlockBytes(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
data := make([]byte, len(buf)/2)
|
||||||
|
_, _ = hex.Decode(data, buf)
|
||||||
|
return p2poolblock.NewBlockFromBytes(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetDatabase() *database.Database {
|
||||||
|
return a.db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetShareFromRawEntry(id types.Hash, errOnUncles bool) (b *database.Block, uncles []*database.UncleBlock, err error) {
|
||||||
|
var raw *p2poolblock.Block
|
||||||
|
if raw, err = a.GetRawBlock(id); err != nil {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
u := make([]*p2poolblock.Block, 0, len(raw.Side.Uncles))
|
||||||
|
for _, uncleId := range raw.Side.Uncles {
|
||||||
|
if uncle, err := a.GetRawBlock(uncleId); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else {
|
||||||
|
u = append(u, uncle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.NewBlockFromBinaryBlock(a.db, raw, u, errOnUncles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetBlockWindowPayouts(tip *database.Block) (shares map[uint64]uint128.Uint128) {
|
||||||
|
shares = make(map[uint64]uint128.Uint128)
|
||||||
|
|
||||||
|
blockCount := 0
|
||||||
|
|
||||||
|
block := tip
|
||||||
|
|
||||||
|
blockCache := make(map[uint64]*database.Block, p2pool.PPLNSWindow)
|
||||||
|
for b := range a.db.GetBlocksInWindow(&tip.Height, p2pool.PPLNSWindow) {
|
||||||
|
blockCache[b.Height] = b
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, ok := shares[block.MinerId]; !ok {
|
||||||
|
shares[block.MinerId] = uint128.From64(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
shares[block.MinerId] = shares[block.MinerId].Add(block.Difficulty.Uint128)
|
||||||
|
|
||||||
|
for uncle := range a.db.GetUnclesByParentId(block.Id) {
|
||||||
|
if (tip.Height - uncle.Block.Height) >= p2pool.PPLNSWindow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := shares[uncle.Block.MinerId]; !ok {
|
||||||
|
shares[uncle.Block.MinerId] = uint128.From64(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
product := uncle.Block.Difficulty.Uint128.Mul64(p2pool.UnclePenalty)
|
||||||
|
unclePenalty := product.Div64(100)
|
||||||
|
|
||||||
|
shares[block.MinerId] = shares[block.MinerId].Add(unclePenalty)
|
||||||
|
shares[uncle.Block.MinerId] = shares[uncle.Block.MinerId].Add(uncle.Block.Difficulty.Uint128.Sub(unclePenalty))
|
||||||
|
}
|
||||||
|
|
||||||
|
blockCount++
|
||||||
|
if b, ok := blockCache[block.Height-1]; ok && b.Id == block.PreviousId {
|
||||||
|
block = b
|
||||||
|
} else {
|
||||||
|
block = a.db.GetBlockById(block.PreviousId)
|
||||||
|
}
|
||||||
|
if block == nil || blockCount >= p2pool.PPLNSWindow {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalReward := tip.Coinbase.Reward
|
||||||
|
|
||||||
|
if totalReward > 0 {
|
||||||
|
totalWeight := uint128.From64(0)
|
||||||
|
for _, w := range shares {
|
||||||
|
totalWeight = totalWeight.Add(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := uint128.From64(0)
|
||||||
|
rewardGiven := uint128.From64(0)
|
||||||
|
|
||||||
|
for miner, weight := range shares {
|
||||||
|
w = w.Add(weight)
|
||||||
|
nextValue := w.Mul64(totalReward).Div(totalWeight)
|
||||||
|
shares[miner] = nextValue.Sub(rewardGiven)
|
||||||
|
rewardGiven = nextValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if blockCount != p2pool.PPLNSWindow {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return shares
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetWindowPayouts(height, totalReward *uint64) (shares map[uint64]uint128.Uint128) {
|
||||||
|
shares = make(map[uint64]uint128.Uint128)
|
||||||
|
|
||||||
|
var tip uint64
|
||||||
|
if height != nil {
|
||||||
|
tip = *height
|
||||||
|
} else {
|
||||||
|
tip = a.db.GetChainTip().Height
|
||||||
|
}
|
||||||
|
|
||||||
|
blockCount := 0
|
||||||
|
|
||||||
|
for block := range a.db.GetBlocksInWindow(&tip, p2pool.PPLNSWindow) {
|
||||||
|
if _, ok := shares[block.MinerId]; !ok {
|
||||||
|
shares[block.MinerId] = uint128.From64(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
shares[block.MinerId] = shares[block.MinerId].Add(block.Difficulty.Uint128)
|
||||||
|
|
||||||
|
for uncle := range a.db.GetUnclesByParentId(block.Id) {
|
||||||
|
if (tip - uncle.Block.Height) >= p2pool.PPLNSWindow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := shares[uncle.Block.MinerId]; !ok {
|
||||||
|
shares[uncle.Block.MinerId] = uint128.From64(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
product := uncle.Block.Difficulty.Uint128.Mul64(p2pool.UnclePenalty)
|
||||||
|
unclePenalty := product.Div64(100)
|
||||||
|
|
||||||
|
shares[block.MinerId] = shares[block.MinerId].Add(unclePenalty)
|
||||||
|
shares[uncle.Block.MinerId] = shares[uncle.Block.MinerId].Add(uncle.Block.Difficulty.Uint128.Sub(unclePenalty))
|
||||||
|
}
|
||||||
|
|
||||||
|
blockCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalReward != nil && *totalReward > 0 {
|
||||||
|
totalWeight := uint128.From64(0)
|
||||||
|
for _, w := range shares {
|
||||||
|
totalWeight = totalWeight.Add(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := uint128.From64(0)
|
||||||
|
rewardGiven := uint128.From64(0)
|
||||||
|
|
||||||
|
for miner, weight := range shares {
|
||||||
|
w = w.Add(weight)
|
||||||
|
nextValue := w.Mul64(*totalReward).Div(totalWeight)
|
||||||
|
shares[miner] = nextValue.Sub(rewardGiven)
|
||||||
|
rewardGiven = nextValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if blockCount != p2pool.PPLNSWindow {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return shares
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetPoolBlocks() (result []struct {
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Hash types.Hash `json:"hash"`
|
||||||
|
Difficulty uint64 `json:"difficulty"`
|
||||||
|
TotalHashes uint64 `json:"totalHashes"`
|
||||||
|
Ts uint64 `json:"ts"`
|
||||||
|
}, err error) {
|
||||||
|
f, err := os.Open(fmt.Sprintf("%s/pool/blocks", a.path))
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if buf, err := io.ReadAll(f); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else {
|
||||||
|
err = json.Unmarshal(buf, &result)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Api) GetPoolStats() (result struct {
|
||||||
|
PoolList []string `json:"pool_list"`
|
||||||
|
PoolStatistics struct {
|
||||||
|
HashRate uint64 `json:"hashRate"`
|
||||||
|
Difficulty uint64 `json:"difficulty"`
|
||||||
|
Hash types.Hash `json:"hash"`
|
||||||
|
Height uint64 `json:"height"`
|
||||||
|
Miners uint64 `json:"miners"`
|
||||||
|
TotalHashes uint64 `json:"totalHashes"`
|
||||||
|
LastBlockFoundTime uint64 `json:"lastBlockFoundTime"`
|
||||||
|
LastBlockFound uint64 `json:"lastBlockFound"`
|
||||||
|
TotalBlocksFound uint64 `json:"totalBlocksFound"`
|
||||||
|
} `json:"pool_statistics"`
|
||||||
|
}, err error) {
|
||||||
|
f, err := os.Open(fmt.Sprintf("%s/pool/stats", a.path))
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if buf, err := io.ReadAll(f); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else {
|
||||||
|
err = json.Unmarshal(buf, &result)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
}
|
347
p2pool/block/block.go
Normal file
347
p2pool/block/block.go
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
package block
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.gammaspectra.live/P2Pool/moneroutil"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
"io"
|
||||||
|
"lukechampine.com/uint128"
|
||||||
|
"math/bits"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Block struct {
|
||||||
|
Main struct {
|
||||||
|
MajorVersion uint8
|
||||||
|
MinorVersion uint8
|
||||||
|
Timestamp uint64
|
||||||
|
Parent types.Hash
|
||||||
|
Nonce types.Nonce
|
||||||
|
|
||||||
|
Coinbase *CoinbaseTransaction
|
||||||
|
|
||||||
|
CoinbaseExtra struct {
|
||||||
|
PublicKey types.Hash
|
||||||
|
ExtraNonce []byte
|
||||||
|
SideId types.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
Transactions []types.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
Side struct {
|
||||||
|
PublicSpendKey types.Hash
|
||||||
|
PublicViewKey types.Hash
|
||||||
|
CoinbasePrivateKey types.Hash
|
||||||
|
Parent types.Hash
|
||||||
|
Uncles []types.Hash
|
||||||
|
Height uint64
|
||||||
|
Difficulty types.Difficulty
|
||||||
|
CumulativeDifficulty types.Difficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
Extra struct {
|
||||||
|
MainId types.Hash
|
||||||
|
PowHash types.Hash
|
||||||
|
MainDifficulty types.Difficulty
|
||||||
|
Peer []byte
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBlockFromBytes(buf []byte) (*Block, error) {
|
||||||
|
b := &Block{}
|
||||||
|
return b, b.UnmarshalBinary(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) UnmarshalBinary(data []byte) error {
|
||||||
|
|
||||||
|
if len(data) < 32 {
|
||||||
|
return errors.New("invalid block data")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
version uint64
|
||||||
|
|
||||||
|
mainDataSize uint64
|
||||||
|
mainData []byte
|
||||||
|
|
||||||
|
sideDataSize uint64
|
||||||
|
sideData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch version {
|
||||||
|
case 1:
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(reader, b.Extra.MainId[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(reader, b.Extra.PowHash[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &b.Extra.MainDifficulty.Hi); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &b.Extra.MainDifficulty.Lo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Extra.MainDifficulty.ReverseBytes()
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &mainDataSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mainData = make([]byte, mainDataSize)
|
||||||
|
if _, err = io.ReadFull(reader, mainData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &sideDataSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sideData = make([]byte, sideDataSize)
|
||||||
|
if _, err = io.ReadFull(reader, sideData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//Ignore error when unable to read peer
|
||||||
|
_ = func() error {
|
||||||
|
var peerSize uint64
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &peerSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.Extra.Peer = make([]byte, peerSize)
|
||||||
|
if _, err = io.ReadFull(reader, b.Extra.Peer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &mainDataSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mainData = make([]byte, mainDataSize)
|
||||||
|
if _, err = io.ReadFull(reader, mainData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sideData, err = io.ReadAll(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown block version %d", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = b.unmarshalMainData(mainData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = b.unmarshalTxExtra(b.Main.Coinbase.Extra); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = b.unmarshalSideData(sideData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) unmarshalMainData(data []byte) error {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
txCount uint64
|
||||||
|
transactionHash types.Hash
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &b.Main.MajorVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &b.Main.MinorVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Main.Timestamp, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(reader, b.Main.Parent[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(reader, b.Main.Nonce[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coinbase Tx Decoding
|
||||||
|
{
|
||||||
|
b.Main.Coinbase = &CoinbaseTransaction{}
|
||||||
|
if err = b.Main.Coinbase.FromReader(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if txCount, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if txCount < 8192 {
|
||||||
|
b.Main.Transactions = make([]types.Hash, 0, txCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < int(txCount); i++ {
|
||||||
|
if _, err = io.ReadFull(reader, transactionHash[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//TODO: check if copy is needed
|
||||||
|
b.Main.Transactions = append(b.Main.Transactions, transactionHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) unmarshalTxExtra(data []byte) error {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
extraTag uint8
|
||||||
|
nonceSize uint64
|
||||||
|
mergeSize uint8
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &extraTag); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch extraTag {
|
||||||
|
default:
|
||||||
|
fallthrough
|
||||||
|
case TxExtraTagPadding, TxExtraTagAdditionalPubKeys:
|
||||||
|
return fmt.Errorf("unknown extra tag %d", extraTag)
|
||||||
|
case TxExtraTagPubKey:
|
||||||
|
if _, err = io.ReadFull(reader, b.Main.CoinbaseExtra.PublicKey[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case TxExtraNonce:
|
||||||
|
if nonceSize, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Main.CoinbaseExtra.ExtraNonce = make([]byte, nonceSize)
|
||||||
|
if _, err = io.ReadFull(reader, b.Main.CoinbaseExtra.ExtraNonce); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case TxExtraMergeMiningTag:
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &mergeSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mergeSize != types.HashSize {
|
||||||
|
return fmt.Errorf("hash size %d is not %d", mergeSize, types.HashSize)
|
||||||
|
}
|
||||||
|
if _, err = io.ReadFull(reader, b.Main.CoinbaseExtra.SideId[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) IsProofHigherThanDifficulty() bool {
|
||||||
|
return b.GetProofDifficulty().Cmp(b.Extra.MainDifficulty.Uint128) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) GetProofDifficulty() types.Difficulty {
|
||||||
|
base := uint256.NewInt(0).SetBytes32(bytes.Repeat([]byte{0xff}, 32))
|
||||||
|
pow := uint256.NewInt(0).SetBytes32(b.Extra.PowHash[:])
|
||||||
|
pow = &uint256.Int{bits.ReverseBytes64(pow[3]), bits.ReverseBytes64(pow[2]), bits.ReverseBytes64(pow[1]), bits.ReverseBytes64(pow[0])}
|
||||||
|
|
||||||
|
if pow.Eq(uint256.NewInt(0)) {
|
||||||
|
return types.Difficulty{}
|
||||||
|
}
|
||||||
|
|
||||||
|
powResult := uint256.NewInt(0).Div(base, pow).Bytes32()
|
||||||
|
return types.Difficulty{Uint128: uint128.FromBytes(powResult[16:]).ReverseBytes()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) GetAddress() *address.Address {
|
||||||
|
return address.FromRawAddress(moneroutil.MainNetwork, b.Side.PublicSpendKey, b.Side.PublicViewKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) unmarshalSideData(data []byte) error {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
uncleCount uint64
|
||||||
|
uncleHash types.Hash
|
||||||
|
)
|
||||||
|
if _, err = io.ReadFull(reader, b.Side.PublicSpendKey[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = io.ReadFull(reader, b.Side.PublicViewKey[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = io.ReadFull(reader, b.Side.CoinbasePrivateKey[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = io.ReadFull(reader, b.Side.Parent[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if uncleCount, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < int(uncleCount); i++ {
|
||||||
|
if _, err = io.ReadFull(reader, uncleHash[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//TODO: check if copy is needed
|
||||||
|
b.Side.Uncles = append(b.Side.Uncles, uncleHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Side.Height, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if b.Side.Difficulty.Lo, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Side.Difficulty.Hi, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if b.Side.CumulativeDifficulty.Lo, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Side.CumulativeDifficulty.Hi, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
64
p2pool/block/block_test.go
Normal file
64
p2pool/block/block_test.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package block
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBlockDecode(t *testing.T) {
|
||||||
|
f, err := os.Open("testdata/1783223a701d16192ce9ff83c603b48b3e1785e3779b42079ede6e52ea7f0d2d.hex")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
buf, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
contents, err := hex.DecodeString(string(buf))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := NewBlockFromBytes(contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hex.EncodeToString(block.Extra.MainId[:]) != "05892769e709b6cfebd5d71e5cadf38ba0abde8048a0eea3792d981861ad9a69" {
|
||||||
|
t.Fatalf("expected main id 05892769e709b6cfebd5d71e5cadf38ba0abde8048a0eea3792d981861ad9a69, got %s", hex.EncodeToString(block.Extra.MainId[:]))
|
||||||
|
}
|
||||||
|
if hex.EncodeToString(block.Main.CoinbaseExtra.SideId[:]) != "1783223a701d16192ce9ff83c603b48b3e1785e3779b42079ede6e52ea7f0d2d" {
|
||||||
|
t.Fatalf("expected side id 1783223a701d16192ce9ff83c603b48b3e1785e3779b42079ede6e52ea7f0d2d, got %s", hex.EncodeToString(block.Main.CoinbaseExtra.SideId[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
txId := block.Main.Coinbase.Id()
|
||||||
|
|
||||||
|
if hex.EncodeToString(txId[:]) != "41e8976fafbf9263996733b8f857a11ca385a78798c33617af8c77cfd989da60" {
|
||||||
|
t.Fatalf("expected coinbase id 41e8976fafbf9263996733b8f857a11ca385a78798c33617af8c77cfd989da60, got %s", hex.EncodeToString(txId[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
proofResult, _ := types.DifficultyFromString("00000000000000000000006ef6334490")
|
||||||
|
|
||||||
|
if block.GetProofDifficulty().Cmp(proofResult.Uint128) != 0 {
|
||||||
|
t.Fatalf("expected PoW difficulty %s, got %s", proofResult.String(), block.GetProofDifficulty().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !block.IsProofHigherThanDifficulty() {
|
||||||
|
t.Fatal("expected proof higher than difficulty")
|
||||||
|
}
|
||||||
|
|
||||||
|
block.Extra.PowHash[31] = 1
|
||||||
|
|
||||||
|
if block.IsProofHigherThanDifficulty() {
|
||||||
|
t.Fatal("expected proof lower than difficulty")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Print(block.GetProofDifficulty().String())
|
||||||
|
log.Print(block.Extra.MainDifficulty.String())
|
||||||
|
}
|
198
p2pool/block/transaction.go
Normal file
198
p2pool/block/transaction.go
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
package block
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.gammaspectra.live/P2Pool/moneroutil"
|
||||||
|
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TxInGen = 0xff
|
||||||
|
|
||||||
|
const TxOutToKey = 2
|
||||||
|
const TxOutToTaggedKey = 3
|
||||||
|
|
||||||
|
const TxExtraTagPadding = 0x00
|
||||||
|
const TxExtraTagPubKey = 0x01
|
||||||
|
const TxExtraNonce = 0x02
|
||||||
|
const TxExtraMergeMiningTag = 0x03
|
||||||
|
const TxExtraTagAdditionalPubKeys = 0x04
|
||||||
|
|
||||||
|
type CoinbaseTransaction struct {
|
||||||
|
id types.Hash
|
||||||
|
idLock sync.Mutex
|
||||||
|
Version uint8
|
||||||
|
UnlockTime uint64
|
||||||
|
InputCount uint8
|
||||||
|
InputType uint8
|
||||||
|
GenHeight uint64
|
||||||
|
Outputs []*CoinbaseTransactionOutput
|
||||||
|
|
||||||
|
Extra []byte
|
||||||
|
|
||||||
|
ExtraBaseRCT uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CoinbaseTransaction) FromReader(reader *bytes.Reader) (err error) {
|
||||||
|
var (
|
||||||
|
txExtraSize uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &c.Version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UnlockTime, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &c.InputCount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &c.InputType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InputType != TxInGen {
|
||||||
|
return errors.New("invalid coinbase input type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.GenHeight, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputCount uint64
|
||||||
|
|
||||||
|
if outputCount, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputCount < 8192 {
|
||||||
|
c.Outputs = make([]*CoinbaseTransactionOutput, 0, outputCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := 0; index < int(outputCount); index++ {
|
||||||
|
o := &CoinbaseTransactionOutput{
|
||||||
|
Index: uint64(index),
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Reward, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &o.Type); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.Type {
|
||||||
|
case TxOutToTaggedKey, TxOutToKey:
|
||||||
|
if _, err = io.ReadFull(reader, o.EphemeralPublicKey[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Type == TxOutToTaggedKey {
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &o.ViewTag); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown %d TXOUT key", o.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Outputs = append(c.Outputs, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
if txExtraSize, err = binary.ReadUvarint(reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Extra = make([]byte, txExtraSize)
|
||||||
|
if _, err = io.ReadFull(reader, c.Extra); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Read(reader, binary.BigEndian, &c.ExtraBaseRCT); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CoinbaseTransaction) Bytes() []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
varIntBuf := make([]byte, binary.MaxVarintLen64)
|
||||||
|
|
||||||
|
_ = binary.Write(buf, binary.BigEndian, c.Version)
|
||||||
|
_, _ = buf.Write(varIntBuf[:binary.PutUvarint(varIntBuf, c.UnlockTime)])
|
||||||
|
_ = binary.Write(buf, binary.BigEndian, c.InputCount)
|
||||||
|
_ = binary.Write(buf, binary.BigEndian, c.InputType)
|
||||||
|
_, _ = buf.Write(varIntBuf[:binary.PutUvarint(varIntBuf, c.GenHeight)])
|
||||||
|
|
||||||
|
_, _ = buf.Write(varIntBuf[:binary.PutUvarint(varIntBuf, uint64(len(c.Outputs)))])
|
||||||
|
|
||||||
|
for _, o := range c.Outputs {
|
||||||
|
_, _ = buf.Write(varIntBuf[:binary.PutUvarint(varIntBuf, o.Reward)])
|
||||||
|
_ = binary.Write(buf, binary.BigEndian, o.Type)
|
||||||
|
|
||||||
|
switch o.Type {
|
||||||
|
case TxOutToTaggedKey, TxOutToKey:
|
||||||
|
_, _ = buf.Write(o.EphemeralPublicKey[:])
|
||||||
|
|
||||||
|
if o.Type == TxOutToTaggedKey {
|
||||||
|
_ = binary.Write(buf, binary.BigEndian, o.ViewTag)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = buf.Write(varIntBuf[:binary.PutUvarint(varIntBuf, uint64(len(c.Extra)))])
|
||||||
|
_, _ = buf.Write(c.Extra)
|
||||||
|
_ = binary.Write(buf, binary.BigEndian, c.ExtraBaseRCT)
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CoinbaseTransaction) Id() types.Hash {
|
||||||
|
if c.id != (types.Hash{}) {
|
||||||
|
return c.id
|
||||||
|
}
|
||||||
|
|
||||||
|
c.idLock.Lock()
|
||||||
|
defer c.idLock.Unlock()
|
||||||
|
if c.id != (types.Hash{}) {
|
||||||
|
return c.id
|
||||||
|
}
|
||||||
|
|
||||||
|
idHasher := sha3.NewLegacyKeccak256()
|
||||||
|
|
||||||
|
txBytes := c.Bytes()
|
||||||
|
// remove base RCT
|
||||||
|
idHasher.Write(hashKeccak(txBytes[:len(txBytes)-1]))
|
||||||
|
// Base RCT, single 0 byte in miner tx
|
||||||
|
idHasher.Write(hashKeccak([]byte{c.ExtraBaseRCT}))
|
||||||
|
// Prunable RCT, empty in miner tx
|
||||||
|
idHasher.Write(make([]byte, types.HashSize))
|
||||||
|
|
||||||
|
copy(c.id[:], idHasher.Sum([]byte{}))
|
||||||
|
|
||||||
|
return c.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashKeccak(data ...[]byte) []byte {
|
||||||
|
d := moneroutil.Keccak256(data...)
|
||||||
|
return d[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
type CoinbaseTransactionOutput struct {
|
||||||
|
Index uint64
|
||||||
|
Reward uint64
|
||||||
|
Type uint8
|
||||||
|
EphemeralPublicKey types.Hash
|
||||||
|
ViewTag uint8
|
||||||
|
}
|
8
p2pool/constants.go
Normal file
8
p2pool/constants.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package p2pool
|
||||||
|
|
||||||
|
const (
|
||||||
|
PPLNSWindow = 2160
|
||||||
|
BlockTime = 10
|
||||||
|
UnclePenalty = 20
|
||||||
|
UncleBlockDepth = 3
|
||||||
|
)
|
1
testdata/1783223a701d16192ce9ff83c603b48b3e1785e3779b42079ede6e52ea7f0d2d.hex
vendored
Normal file
1
testdata/1783223a701d16192ce9ff83c603b48b3e1785e3779b42079ede6e52ea7f0d2d.hex
vendored
Normal file
File diff suppressed because one or more lines are too long
5
types/constants.go
Normal file
5
types/constants.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
const (
|
||||||
|
DonationAddress = "4AeEwC2Uik2Zv4uooAUWjQb2ZvcLDBmLXN4rzSn3wjBoY8EKfNkSUqeg5PxcnWTwB1b2V39PDwU9gaNE5SnxSQPYQyoQtr7"
|
||||||
|
)
|
109
types/types.go
Normal file
109
types/types.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"lukechampine.com/uint128"
|
||||||
|
)
|
||||||
|
|
||||||
|
const HashSize = 32
|
||||||
|
const DifficultySize = 16
|
||||||
|
const NonceSize = 4
|
||||||
|
|
||||||
|
type Hash [HashSize]byte
|
||||||
|
|
||||||
|
func (h Hash) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(h.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashFromString(s string) (Hash, error) {
|
||||||
|
var h Hash
|
||||||
|
if buf, err := hex.DecodeString(s); err != nil {
|
||||||
|
return h, err
|
||||||
|
} else {
|
||||||
|
if len(buf) != HashSize {
|
||||||
|
return h, errors.New("wrong hash size")
|
||||||
|
}
|
||||||
|
copy(h[:], buf)
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashFromBytes(buf []byte) (h Hash) {
|
||||||
|
if len(buf) != HashSize {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy(h[:], buf)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hash) Equals(o Hash) bool {
|
||||||
|
return bytes.Compare(h[:], o[:]) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Hash) String() string {
|
||||||
|
return hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hash) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf, err := hex.DecodeString(s); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
if len(buf) != HashSize {
|
||||||
|
return errors.New("wrong hash size")
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(h[:], buf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Difficulty struct {
|
||||||
|
uint128.Uint128
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Difficulty) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(d.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func DifficultyFromString(s string) (Difficulty, error) {
|
||||||
|
if buf, err := hex.DecodeString(s); err != nil {
|
||||||
|
return Difficulty{}, err
|
||||||
|
} else {
|
||||||
|
if len(buf) != DifficultySize {
|
||||||
|
return Difficulty{}, errors.New("wrong hash size")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Difficulty{Uint128: uint128.FromBytes(buf).ReverseBytes()}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Difficulty) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff, err := DifficultyFromString(s); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
d.Uint128 = diff.Uint128
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Difficulty) String() string {
|
||||||
|
var buf [DifficultySize]byte
|
||||||
|
d.ReverseBytes().PutBytes(buf[:])
|
||||||
|
return hex.EncodeToString(buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
type Nonce [NonceSize]byte
|
15
types/types_test.go
Normal file
15
types/types_test.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDifficulty(t *testing.T) {
|
||||||
|
hexDiff := "000000000000000000000000683a8b1c"
|
||||||
|
diff, err := DifficultyFromString(hexDiff)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff.String() != hexDiff {
|
||||||
|
t.Fatalf("expected %s, got %s", hexDiff, diff)
|
||||||
|
}
|
||||||
|
}
|
64
utils/number.go
Normal file
64
utils/number.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"github.com/jxskiss/base62"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encoding = base62.NewEncoding("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||||
|
|
||||||
|
func DecodeBinaryNumber(i string) uint64 {
|
||||||
|
if n, err := strconv.ParseUint(i, 10, 0); strings.Index(i, ".") == -1 && err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := encoding.ParseUint([]byte(strings.ReplaceAll(i, ".", ""))); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeBinaryNumber(n uint64) string {
|
||||||
|
v1 := string(encoding.FormatUint(n))
|
||||||
|
v2 := strconv.FormatUint(n, 10)
|
||||||
|
|
||||||
|
if !strings.ContainsAny(v1, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") {
|
||||||
|
v1 = "." + v1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v1) >= len(v2) {
|
||||||
|
return v2
|
||||||
|
}
|
||||||
|
|
||||||
|
return v1
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeHexBinaryNumber(i string) string {
|
||||||
|
if _, err := hex.DecodeString(i); strings.Index(i, ".") == -1 && err != nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := encoding.Decode([]byte(strings.ReplaceAll(i, ".", ""))); err == nil {
|
||||||
|
return hex.EncodeToString(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeHexBinaryNumber(v2 string) string {
|
||||||
|
b, _ := hex.DecodeString(v2)
|
||||||
|
v1 := string(encoding.Encode(b))
|
||||||
|
|
||||||
|
if !strings.ContainsAny(v1, "GHIJKLMNOPQRSTUVWXYZghijklmnopqrstuvwxyz") {
|
||||||
|
v1 = "." + v1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v1) >= len(v2) {
|
||||||
|
return v2
|
||||||
|
}
|
||||||
|
|
||||||
|
return v1
|
||||||
|
}
|
12
utils/number_test.go
Normal file
12
utils/number_test.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNumber(t *testing.T) {
|
||||||
|
s := "S"
|
||||||
|
n := uint64(28)
|
||||||
|
|
||||||
|
if DecodeBinaryNumber(s) != n {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue