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