Initial commit

This commit is contained in:
DataHoarder 2022-10-08 20:55:01 +02:00
commit 76d70a5f1a
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
33 changed files with 4499 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.idea

19
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}
}

View 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
View 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
View 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()}
}

View 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())
}
}

View 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
View 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, &timestamp, &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
View 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
View 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
View 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
View 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
View 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
View 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")
}
}

View 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
View 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
View file

@ -0,0 +1,5 @@
package monero
const (
BlockTime = 60 * 2
)

320
p2pool/api/api.go Normal file
View 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
View 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
}

View 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
View 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
View file

@ -0,0 +1,8 @@
package p2pool
const (
PPLNSWindow = 2160
BlockTime = 10
UnclePenalty = 20
UncleBlockDepth = 3
)

File diff suppressed because one or more lines are too long

5
types/constants.go Normal file
View file

@ -0,0 +1,5 @@
package types
const (
DonationAddress = "4AeEwC2Uik2Zv4uooAUWjQb2ZvcLDBmLXN4rzSn3wjBoY8EKfNkSUqeg5PxcnWTwB1b2V39PDwU9gaNE5SnxSQPYQyoQtr7"
)

109
types/types.go Normal file
View 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
View 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
View 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
View 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()
}
}