Added all entries to single docker-compose.yml

Working docker-compose version, removed old unused database code
This commit is contained in:
DataHoarder 2023-04-11 03:20:38 +02:00
parent e56d7ee46e
commit 4d78339795
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
27 changed files with 987 additions and 1738 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
.git/
.env
docker-compose.override.yml
p2pool.cache
p2pool_peers.txt

26
.env.example Normal file
View file

@ -0,0 +1,26 @@
# Monerod p2pool compatible node with open RPC and ZMQ ports
MONEROD_HOST=p2pmd.xmrvsbeast.com
MONEROD_RPC_PORT=18081
MONEROD_ZMQ_PORT=18083
# Tor private key and public address. You can generate vanity ones via https://github.com/cathugger/mkp224o
TOR_SERVICE_KEY=
TOR_SERVICE_ADDRESS=
NET_SERVICE_ADDRESS=
SITE_TITLE=
# This port will be presented externally
SITE_PORT=8189
# Change for mini if you want public reachable address
P2POOL_PORT=37889
# This port is what should be reachable externally, and will be reachable via Tor as well
P2POOL_EXTERNAL_PORT=37889
P2POOL_OUT_PEERS=24
P2POOL_IN_PEERS=64
# Extra arg examples
#P2POOL_EXTRA_ARGS=-light-mode
#P2POOL_EXTRA_ARGS=-mini
#P2POOL_EXTRA_ARGS=-consensus-config /data/consensus.json

4
.gitignore vendored
View file

@ -1,3 +1,5 @@
.idea
p2pool.cache
p2pool_peers.txt
p2pool_peers.txt
docker-compose.override.yml
.env

View file

@ -35,11 +35,13 @@ func encodeJson(r *http.Request, d any) ([]byte, error) {
func main() {
torHost := os.Getenv("TOR_SERVICE_ADDRESS")
moneroHost := flag.String("host", "127.0.0.1", "IP address of your Monero node")
moneroRpcPort := flag.Uint("rpc-port", 18081, "monerod RPC API port number")
dbString := flag.String("db", "", "")
p2poolApiHost := flag.String("api-host", "", "Host URL for p2pool go observer consensus")
flag.Parse()
client.SetDefaultClientSettings(os.Getenv("MONEROD_RPC_URL"))
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
p2api := p2poolapi.NewP2PoolApi(*p2poolApiHost)

View file

@ -3,6 +3,7 @@ package main
import (
"context"
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon"
utils2 "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"git.gammaspectra.live/P2Pool/p2pool-observer/index"
@ -13,7 +14,6 @@ import (
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"log"
"os"
"time"
)
@ -22,13 +22,16 @@ func blockId(b *sidechain.PoolBlock) types.Hash {
}
func main() {
moneroHost := flag.String("host", "127.0.0.1", "IP address of your Monero node")
moneroRpcPort := flag.Uint("rpc-port", 18081, "monerod RPC API port number")
startFromHeight := flag.Uint64("from", 0, "Start sync from this height")
dbString := flag.String("db", "", "")
p2poolApiHost := flag.String("api-host", "", "Host URL for p2pool go observer consensus")
fullMode := flag.Bool("full-mode", false, "Allocate RandomX dataset, uses 2GB of RAM")
flag.Parse()
randomx.UseFullMemory.Store(*fullMode)
client.SetDefaultClientSettings(os.Getenv("MONEROD_RPC_URL"))
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
p2api := p2poolapi.NewP2PoolApi(*p2poolApiHost)
@ -128,11 +131,6 @@ func main() {
log.Panic(err)
}
heightCount := maxHeight - 1 - currentHeight + 1
const strideSize = 1000
strides := heightCount / strideSize
ctx := context.Background()
scanHeader := func(h daemon.BlockHeader) error {
@ -147,6 +145,11 @@ func main() {
return nil
}
heightCount := maxHeight - 1 - currentHeight + 1
const strideSize = 1000
strides := heightCount / strideSize
//backfill headers
for stride := uint64(0); stride <= strides; stride++ {
start := currentHeight + stride*strideSize
@ -173,8 +176,8 @@ func main() {
if header == nil {
break
}
cur, err := client.GetDefaultClient().GetBlockHeaderByHash(header.Id, ctx)
if err != nil {
cur, _ := client.GetDefaultClient().GetBlockHeaderByHash(header.Id, ctx)
if cur == nil {
break
}
if err := scanHeader(cur.BlockHeader); err != nil {
@ -206,7 +209,7 @@ func main() {
log.Panicf("main tip height less than ours, abort: %d < %d", mainTip.Height, currentMainTip.Height)
} else {
var prevHash types.Hash
for cur, err := client.GetDefaultClient().GetBlockHeaderByHash(mainTip.Id, ctx); err != nil; cur, err = client.GetDefaultClient().GetBlockHeaderByHash(prevHash, ctx) {
for cur, _ := client.GetDefaultClient().GetBlockHeaderByHash(mainTip.Id, ctx); cur != nil; cur, _ = client.GetDefaultClient().GetBlockHeaderByHash(prevHash, ctx) {
curHash, _ := types.HashFromString(cur.BlockHeader.Hash)
if indexDb.GetMainBlockByHeight(cur.BlockHeader.Height).Id == curHash {
break

View file

@ -42,7 +42,7 @@ func main() {
addPeers := flag.String("addpeers", "", "Comma-separated list of IP:port of other p2pool nodes to connect to")
lightMode := flag.Bool("light-mode", false, "Don't allocate RandomX dataset, saves 2GB of RAM")
peerList := flag.String("peer-list", "p2pool_peers.txt", "Either a path or an URL to obtain peer lists from. If it is a path, new peers will be saved to this path")
consensusConfigFile := flag.String("config", "", "Name of the p2pool config file")
consensusConfigFile := flag.String("consensus-config", "", "Name of the p2pool consensus config file")
useMiniSidechain := flag.Bool("mini", false, "Connect to p2pool-mini sidechain. Note that it will also change default p2p port.")
outPeers := flag.Uint64("out-peers", 10, "Maximum number of outgoing connections for p2p server (any value between 10 and 450)")

View file

@ -0,0 +1,517 @@
package main
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"errors"
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/archive"
crypto2 "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/mempool"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/floatdrop/lru"
"golang.org/x/exp/slices"
"hash"
"log"
"math"
"os"
"runtime"
"sync"
"sync/atomic"
)
func main() {
inputConsensus := flag.String("consensus", "config.json", "Input config.json consensus file")
inputArchive := flag.String("input", "", "Input path for archive database")
moneroHost := flag.String("host", "127.0.0.1", "IP address of your Monero node")
moneroRpcPort := flag.Uint("rpc-port", 18081, "monerod RPC API port number")
inputTemplate := flag.String("template", "", "Template data for share recovery")
flag.Parse()
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
cf, err := os.ReadFile(*inputConsensus)
if err != nil {
log.Panic(err)
}
inputTemplateData, err := os.ReadFile(*inputTemplate)
if err != nil {
log.Panic(err)
}
jsonTemplate, err := JSONFromTemplate(inputTemplateData)
if err != nil {
log.Panic(err)
}
consensus, err := sidechain.NewConsensusFromJSON(cf)
if err != nil {
log.Panic(err)
}
var headerCacheLock sync.RWMutex
headerByHeightCache := make(map[uint64]*daemon.BlockHeader)
getHeaderByHeight := func(height uint64) *daemon.BlockHeader {
if v := func() *daemon.BlockHeader {
headerCacheLock.RLock()
defer headerCacheLock.RUnlock()
return headerByHeightCache[height]
}(); v == nil {
if r, err := client.GetDefaultClient().GetBlockHeaderByHeight(height, context.Background()); err == nil {
headerCacheLock.Lock()
defer headerCacheLock.Unlock()
headerByHeightCache[r.BlockHeader.Height] = &r.BlockHeader
return &r.BlockHeader
}
return nil
} else {
return v
}
}
getDifficultyByHeight := func(height uint64) types.Difficulty {
if v := getHeaderByHeight(height); v != nil {
return types.DifficultyFrom64(v.Difficulty)
}
return types.ZeroDifficulty
}
getSeedByHeight := func(height uint64) (hash types.Hash) {
seedHeight := randomx.SeedHeight(height)
if v := getHeaderByHeight(seedHeight); v != nil {
h, _ := types.HashFromString(v.Hash)
return h
}
return types.ZeroHash
}
_ = getSeedByHeight
archiveCache, err := archive.NewCache(*inputArchive, consensus, getDifficultyByHeight)
if err != nil {
log.Panic(err)
}
defer archiveCache.Close()
blockCache := lru.New[types.Hash, *sidechain.PoolBlock](int(consensus.ChainWindowSize * 4))
derivationCache := sidechain.NewDerivationCache()
getByTemplateIdDirect := func(h types.Hash) *sidechain.PoolBlock {
if v := blockCache.Get(h); v == nil {
if bs := archiveCache.LoadByTemplateId(h); len(bs) != 0 {
blockCache.Set(h, bs[0])
return bs[0]
} else {
return nil
}
} else {
return *v
}
}
processBlock := func(b *sidechain.PoolBlock) error {
var preAllocatedShares sidechain.Shares
if len(b.Main.Coinbase.Outputs) == 0 {
//cannot use SideTemplateId() as it might not be proper to calculate yet. fetch from coinbase only here
if b2 := getByTemplateIdDirect(types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId))); b2 != nil && len(b2.Main.Coinbase.Outputs) != 0 {
b.Main.Coinbase.Outputs = b2.Main.Coinbase.Outputs
} else {
preAllocatedShares = sidechain.PreAllocateShares(consensus.ChainWindowSize * 2)
}
}
_, err := b.PreProcessBlock(consensus, derivationCache, preAllocatedShares, getDifficultyByHeight, getByTemplateIdDirect)
return err
}
getByTemplateId := func(h types.Hash) *sidechain.PoolBlock {
if v := getByTemplateIdDirect(h); v != nil {
if processBlock(v) != nil {
return nil
}
return v
} else {
return nil
}
}
var poolBlock *sidechain.PoolBlock
var expectedMainId types.Hash
var expectedTemplateId types.Hash
var expectedCoinbaseId types.Hash
var baseMainReward, deltaReward uint64
getTransactionsEntries := func(h ...types.Hash) (r mempool.Mempool) {
if data, jsonTx, err := client.GetDefaultClient().GetTransactions(h...); err != nil {
log.Printf("could not get txids: %s", err)
return nil
} else {
r = make(mempool.Mempool, len(jsonTx))
for i, tx := range jsonTx {
r[i] = mempool.NewEntryFromRPCData(h[i], data[i], tx)
}
return r
}
}
if v2, ok := jsonTemplate.(JsonBlock2); ok {
parentBlock := getByTemplateId(v2.PrevId)
keyPair := crypto.NewKeyPairFromPrivate(&v2.CoinbasePriv)
expectedMainId = v2.MainId
expectedCoinbaseId = v2.CoinbaseId
expectedTemplateId = v2.Id
header := getHeaderByHeight(v2.MainHeight)
var nextBlock *sidechain.PoolBlock
for _, t2 := range archiveCache.LoadBySideChainHeight(v2.Height + 1) {
if t2.Side.Parent == expectedTemplateId {
if err := processBlock(t2); err == nil {
nextBlock = t2
break
}
}
}
if parentBlock == nil {
parentBlock = getByTemplateIdDirect(v2.PrevId)
}
if parentBlock == nil {
//use forward method
} else {
if parentBlock.Main.Coinbase.GenHeight == v2.MainHeight {
baseMainReward = parentBlock.Main.Coinbase.TotalReward
for _, tx := range getTransactionsEntries(parentBlock.Main.Transactions...) {
baseMainReward -= tx.Fee
}
deltaReward = v2.CoinbaseReward - baseMainReward
} else if nextBlock != nil && nextBlock.Main.Coinbase.GenHeight == v2.MainHeight {
for _, tx := range getTransactionsEntries(nextBlock.Main.Transactions...) {
baseMainReward -= tx.Fee
}
deltaReward = v2.CoinbaseReward - baseMainReward
} else {
//fallback method can fail on very full blocks
headerId, _ := types.HashFromString(header.Hash)
if b, err := client.GetDefaultClient().GetBlock(headerId, context.Background()); err != nil {
log.Panic(err)
} else {
//generalization, actual reward could be different in some cases
ij, _ := b.InnerJSON()
baseMainReward = b.BlockHeader.Reward
var txs []types.Hash
for _, txid := range ij.TxHashes {
h, _ := types.HashFromString(txid)
txs = append(txs, h)
}
for _, tx := range getTransactionsEntries(txs...) {
baseMainReward -= tx.Fee
}
}
deltaReward = v2.CoinbaseReward - baseMainReward
}
log.Printf("pool delta reward: %d (%s), base %d (%s), expected %d (%s)", deltaReward, utils.XMRUnits(deltaReward), baseMainReward, utils.XMRUnits(baseMainReward), v2.CoinbaseReward, utils.XMRUnits(v2.CoinbaseReward))
poolBlock = &sidechain.PoolBlock{
Main: block.Block{
MajorVersion: uint8(header.MajorVersion),
MinorVersion: uint8(header.MinorVersion),
Timestamp: v2.Ts,
PreviousId: v2.MinerMainId,
Coinbase: &transaction.CoinbaseTransaction{
Version: 2,
UnlockTime: v2.MainHeight + monero.MinerRewardUnlockTime,
InputCount: 1,
InputType: transaction.TxInGen,
GenHeight: v2.MainHeight,
TotalReward: v2.CoinbaseReward,
Extra: transaction.ExtraTags{
transaction.ExtraTag{
Tag: transaction.TxExtraTagPubKey,
VarIntLength: 0,
Data: types.Bytes(keyPair.PublicKey.AsSlice()),
},
transaction.ExtraTag{
Tag: transaction.TxExtraTagNonce,
VarIntLength: 4,
Data: make(types.Bytes, 4), //TODO: expand nonce size as needed
},
transaction.ExtraTag{
Tag: transaction.TxExtraTagMergeMining,
VarIntLength: types.HashSize,
Data: v2.Id[:],
},
},
ExtraBaseRCT: 0,
},
Transactions: nil,
},
Side: sidechain.SideData{
PublicSpendKey: v2.Wallet.SpendPub.AsBytes(),
PublicViewKey: v2.Wallet.ViewPub.AsBytes(),
CoinbasePrivateKey: keyPair.PrivateKey.AsBytes(),
Parent: v2.PrevId,
Uncles: func() (result []types.Hash) {
for _, u := range v2.Uncles {
result = append(result, u.Id)
}
return result
}(),
Height: v2.Height,
Difficulty: v2.Diff,
CumulativeDifficulty: parentBlock.Side.CumulativeDifficulty.Add(v2.Diff),
// no extrabuffer
},
NetworkType: consensus.NetworkType,
}
}
poolBlock.Depth.Store(math.MaxUint64)
if poolBlock.ShareVersion() > sidechain.ShareVersion_V1 {
poolBlock.Side.CoinbasePrivateKeySeed = parentBlock.Side.CoinbasePrivateKeySeed
if parentBlock.Main.PreviousId != poolBlock.Main.PreviousId {
poolBlock.Side.CoinbasePrivateKeySeed = parentBlock.CalculateTransactionPrivateKeySeed()
}
} else {
expectedSeed := poolBlock.CalculateTransactionPrivateKeySeed()
kP := crypto.NewKeyPairFromPrivate(crypto2.GetDeterministicTransactionPrivateKey(expectedSeed, poolBlock.Main.PreviousId))
if bytes.Compare(poolBlock.CoinbaseExtra(sidechain.SideCoinbasePublicKey), kP.PublicKey.AsSlice()) == 0 && bytes.Compare(kP.PrivateKey.AsSlice(), poolBlock.Side.CoinbasePrivateKey[:]) == 0 {
poolBlock.Side.CoinbasePrivateKeySeed = expectedSeed
} else {
log.Printf("not deterministic private key")
}
}
currentOutputs, _ := sidechain.CalculateOutputs(poolBlock, consensus, getDifficultyByHeight, getByTemplateIdDirect, derivationCache, sidechain.PreAllocateShares(consensus.ChainWindowSize*2))
if currentOutputs == nil {
log.Panic("could not calculate outputs blob")
}
poolBlock.Main.Coinbase.Outputs = currentOutputs
if blob, err := currentOutputs.MarshalBinary(); err != nil {
log.Panic(err)
} else {
poolBlock.Main.Coinbase.OutputsBlobSize = uint64(len(blob))
}
log.Printf("expected main id %s, template id %s, coinbase id %s", expectedMainId, expectedTemplateId, expectedCoinbaseId)
rctHash := crypto.Keccak256([]byte{0})
type partialBlobWork struct {
Hashers [2]hash.Hash
Tx *transaction.CoinbaseTransaction
EncodedBuffer []byte
EncodedOffset int
TempHash types.Hash
}
var stop atomic.Bool
var foundExtraNonce atomic.Uint32
var foundExtraNonceSize atomic.Uint32
minerTxBlob, _ := poolBlock.Main.Coinbase.MarshalBinary()
searchForNonces := func(nonceSize int, max uint64) {
coinbases := make([]*partialBlobWork, runtime.NumCPU())
utils.SplitWork(0, max, func(workIndex uint64, routineIndex int) error {
if stop.Load() {
return errors.New("found nonce")
}
w := coinbases[routineIndex]
if workIndex%(1024*256) == 0 {
log.Printf("try %d/%d @ %d ~%.2f%%", workIndex, max, nonceSize, (float64(workIndex)/math.MaxUint32)*100)
}
binary.LittleEndian.PutUint32(w.EncodedBuffer[w.EncodedOffset:], uint32(workIndex))
idHasher := w.Hashers[0]
txHasher := w.Hashers[1]
txHasher.Write(w.EncodedBuffer)
crypto.HashFastSum(txHasher, w.TempHash[:])
idHasher.Write(w.TempHash[:])
// Base RCT, single 0 byte in miner tx
idHasher.Write(rctHash[:])
// Prunable RCT, empty in miner tx
idHasher.Write(types.ZeroHash[:])
crypto.HashFastSum(idHasher, w.TempHash[:])
if w.TempHash == expectedCoinbaseId {
foundExtraNonce.Store(uint32(workIndex))
foundExtraNonceSize.Store(uint32(nonceSize))
//FOUND!
stop.Store(true)
return errors.New("found nonce")
}
idHasher.Reset()
txHasher.Reset()
return nil
}, func(routines, routineIndex int) error {
if len(coinbases) < routines {
coinbases = slices.Grow(coinbases, routines)
}
tx := &transaction.CoinbaseTransaction{}
if err := tx.UnmarshalBinary(minerTxBlob); err != nil {
return err
}
tx.Extra[1].VarIntLength = uint64(nonceSize)
tx.Extra[1].Data = make([]byte, nonceSize)
buf, _ := tx.MarshalBinary()
coinbases[routineIndex] = &partialBlobWork{
Hashers: [2]hash.Hash{crypto.GetKeccak256Hasher(), crypto.GetKeccak256Hasher()},
Tx: tx,
EncodedBuffer: buf[:len(buf)-1], /* remove RCT */
EncodedOffset: len(buf) - 1 - (types.HashSize + 1 + 1 /*Merge mining tag*/) - nonceSize,
}
return nil
})
}
nonceSize := sidechain.SideExtraNonceSize
//do quick search first
for ; nonceSize <= sidechain.SideExtraNonceMaxSize; nonceSize++ {
if stop.Load() {
break
}
searchForNonces(nonceSize, math.MaxUint16)
}
//do deeper search next
for nonceSize = sidechain.SideExtraNonceSize; nonceSize <= sidechain.SideExtraNonceMaxSize; nonceSize++ {
if stop.Load() {
break
}
searchForNonces(nonceSize, math.MaxUint32)
}
log.Printf("found extra nonce %d, size %d", foundExtraNonce.Load(), foundExtraNonceSize.Load())
poolBlock.Main.Coinbase.Extra[1].VarIntLength = uint64(foundExtraNonceSize.Load())
poolBlock.Main.Coinbase.Extra[1].Data = make([]byte, foundExtraNonceSize.Load())
binary.LittleEndian.PutUint32(poolBlock.Main.Coinbase.Extra[1].Data, foundExtraNonce.Load())
if poolBlock.Main.Coinbase.CalculateId() != expectedCoinbaseId {
log.Panic()
}
log.Printf("got coinbase id %s", poolBlock.Main.Coinbase.CalculateId())
minerTxBlob, _ = poolBlock.Main.Coinbase.MarshalBinary()
log.Printf("raw coinbase: %s", hex.EncodeToString(minerTxBlob))
var collectedTransactions mempool.Mempool
collectTxs := func(hashes ...types.Hash) {
var txs []types.Hash
for _, h := range hashes {
if slices.ContainsFunc(collectedTransactions, func(entry *mempool.MempoolEntry) bool {
return entry.Id == h
}) {
continue
}
txs = append(txs, h)
}
collectedTransactions = append(collectedTransactions, getTransactionsEntries(txs...)...)
}
collectTxsHex := func(hashes ...string) {
var txs []types.Hash
for _, txid := range hashes {
h, _ := types.HashFromString(txid)
if slices.ContainsFunc(collectedTransactions, func(entry *mempool.MempoolEntry) bool {
return entry.Id == h
}) {
continue
}
txs = append(txs, h)
}
collectedTransactions = append(collectedTransactions, getTransactionsEntries(txs...)...)
}
collectTxs(parentBlock.Main.Transactions...)
if nextBlock != nil {
collectTxs(nextBlock.Main.Transactions...)
}
if bh, err := client.GetDefaultClient().GetBlockByHeight(poolBlock.Main.Coinbase.GenHeight, context.Background()); err == nil {
ij, _ := bh.InnerJSON()
collectTxsHex(ij.TxHashes...)
}
/*if bh, err := client.GetDefaultClient().GetBlockByHeight(poolBlock.Main.Coinbase.GenHeight+1, context.Background()); err == nil {
ij, _ := bh.InnerJSON()
collectTxsHex(ij.TxHashes...)
}*/
for _, uncleId := range poolBlock.Side.Uncles {
if u := getByTemplateId(uncleId); u != nil {
if u.Main.Coinbase.GenHeight == poolBlock.Main.Coinbase.GenHeight {
collectTxs(u.Main.Transactions...)
}
}
}
if bh, err := client.GetDefaultClient().GetBlockByHeight(poolBlock.Main.Coinbase.GenHeight-1, context.Background()); err == nil {
ij, _ := bh.InnerJSON()
for _, txH := range ij.TxHashes {
//remove mined tx
txid, _ := types.HashFromString(txH)
if i := slices.IndexFunc(collectedTransactions, func(entry *mempool.MempoolEntry) bool {
return entry.Id == txid
}); i != -1 {
collectedTransactions = slices.Delete(collectedTransactions, i, i+1)
}
}
}
minerTxWeight := uint64(len(minerTxBlob))
totalTxWeight := collectedTransactions.Weight()
medianWeight := getHeaderByHeight(poolBlock.Main.Coinbase.GenHeight).LongTermWeight
log.Printf("collected transaction candidates: %d", len(collectedTransactions))
if totalTxWeight+minerTxWeight <= medianWeight {
//special case
for solution := range collectedTransactions.PerfectSum(deltaReward) {
log.Printf("got %d, %d (%s)", solution.Weight(), solution.Fees(), utils.XMRUnits(solution.Fees()))
}
} else {
//sort in preference order
pickedTxs := collectedTransactions.Pick(baseMainReward, minerTxWeight, medianWeight)
log.Printf("got %d, %d (%s)", pickedTxs.Weight(), pickedTxs.Fees(), utils.XMRUnits(pickedTxs.Fees()))
}
//TODO: nonce
}
}

View file

@ -0,0 +1,107 @@
package main
import (
"encoding/json"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
)
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 crypto.PrivateKeyBytes `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 crypto.PrivateKeyBytes `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 crypto.PrivateKeyBytes `json:"tx_priv"`
TxPub crypto.PublicKeyBytes `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 JSONFromTemplate(data []byte) (any, error) {
var version versionBlock
if err := json.Unmarshal(data, &version); err != nil {
return nil, err
} else {
if version.Version == 2 {
var b JsonBlock2
if err = json.Unmarshal(data, &b); err != nil {
return nil, err
}
return b, nil
} else if version.Version == 0 || version.Version == 1 {
var b JsonBlock1
if err = json.Unmarshal(data, &b); err != nil {
return nil, err
}
return b, nil
} else {
return nil, fmt.Errorf("unknown version %d", version.Version)
}
}
}

View file

@ -15,7 +15,7 @@
<div style="text-align: center">
<h2>Weekly Miners</h2>
<p>This is a list of the miners that have shares in the last 28 windows, or about seven days.</p>
<p>This is a list of the miners that have shares in the last 28 full windows, or about seven days.</p>
<p class="small">Entries are sorted by current window "weight". There are more total miners currently active, but without a share to show at the moment.</p>
<p class="small">Pool share % is relative to whole pool hashrate. Miners can join or leave anytime and there is no ceiling limit. </p>
<table class="center datatable" style="border-collapse: collapse; max-width: calc(4em + 12em + 8em + 8em + 12em + 32em)">

View file

@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
address2 "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
@ -113,6 +114,7 @@ func toFloat64(t any) float64 {
}
func main() {
client.SetDefaultClientSettings(os.Getenv("MONEROD_RPC_URL"))
env := twig.New(&loader{})
render := func(writer http.ResponseWriter, template string, ctx map[string]stick.Value) {
@ -629,12 +631,13 @@ func main() {
poolInfo := getFromAPI("pool_info", 5).(map[string]any)
windowSize := toUint64(poolInfo["sidechain"].(map[string]any)["consensus"].(map[string]any)["pplns_window"])
currentWindowSize := toUint64(poolInfo["sidechain"].(map[string]any)["window_size"])
shareCount := uint64(currentWindowSize)
size := uint64(30)
cacheTime := 30
if params.Has("weekly") {
shareCount = sidechain.PPLNSWindow * 4 * 7
shareCount = windowSize * 4 * 7
size *= 2
if params.Has("refresh") {
writer.Header().Set("refresh", "3600")

View file

@ -1,461 +0,0 @@
package database
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"sync"
)
var NilHash = types.ZeroHash
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 = types.DifficultyFromBytes(bytes.Repeat([]byte{0xff}, types.DifficultySize))
}
type BlockInterface interface {
GetBlock() *Block
}
type BlockCoinbase struct {
Id types.Hash `json:"id"`
Reward uint64 `json:"reward"`
PrivateKey crypto.PrivateKeyBytes `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"`
Alias string `json:"alias,omitempty"`
}
type JSONUncleBlockExtra struct {
Id types.Hash `json:"id"`
Height uint64 `json:"height"`
Difficulty uint64 `json:"difficulty"`
Timestamp uint64 `json:"timestamp"`
Miner string `json:"miner"`
MinerAlias string `json:"miner_alias,omitempty"`
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"`
MinerAlias string `json:"miner_alias,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:"-"`
//Parent extra JSON field, do not use
Parent *JSONBlockParent `json:"parent,omitempty"`
//Uncles extra JSON field, do not use
Uncles []any `json:"uncles"`
//Weight extra JSON field, do not use
Weight uint64 `json:"weight"`
//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(getSeedByHeight mainblock.GetSeedByHeightFunc, getDifficultyByHeight mainblock.GetDifficultyByHeightFunc, db *Database, b *sidechain.PoolBlock, knownUncles sidechain.UniquePoolBlockSlice, errOnUncles bool) (block *Block, uncles []*UncleBlock, err error) {
if b == nil {
return nil, nil, errors.New("nil block")
}
miner := db.GetOrCreateMinerByAddress(b.GetAddress().ToBase58())
if miner == nil {
return nil, nil, errors.New("could not get or create miner")
}
block = &Block{
Id: types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId)),
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.PowHash(getSeedByHeight),
Main: BlockMainData{
Id: b.MainId(),
Height: b.Main.Coinbase.GenHeight,
Found: b.IsProofHigherThanMainDifficulty(getDifficultyByHeight, getSeedByHeight),
},
Template: struct {
Id types.Hash `json:"id"`
Difficulty types.Difficulty `json:"difficulty"`
}{
Id: b.Main.PreviousId,
Difficulty: b.MainDifficulty(getDifficultyByHeight),
},
}
for _, u := range b.Side.Uncles {
if uncle := knownUncles.Get(u); uncle != nil {
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: types.HashFromBytes(uncle.CoinbaseExtra(sidechain.SideTemplateId)),
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.AsBytes(),
},
Difficulty: uncle.Side.Difficulty,
Timestamp: uncle.Main.Timestamp,
MinerId: uncleMiner.Id(),
PowHash: uncle.PowHash(getSeedByHeight),
Main: BlockMainData{
Id: uncle.MainId(),
Height: uncle.Main.Coinbase.GenHeight,
Found: uncle.IsProofHigherThanMainDifficulty(getDifficultyByHeight, getSeedByHeight),
},
Template: struct {
Id types.Hash `json:"id"`
Difficulty types.Difficulty `json:"difficulty"`
}{
Id: uncle.Main.PreviousId,
Difficulty: uncle.MainDifficulty(getDifficultyByHeight),
},
},
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 crypto.PrivateKeyBytes `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 crypto.PrivateKeyBytes `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 crypto.PrivateKeyBytes `json:"tx_priv"`
TxPub crypto.PublicKeyBytes `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.DifficultyFrom64(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: crypto.PrivateKeyBytes(NilHash),
},
Difficulty: types.DifficultyFrom64(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.Template.Difficulty.CheckPoW(b.PowHash)
}

View file

@ -1,66 +0,0 @@
package database
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"golang.org/x/exp/slices"
)
type CoinbaseTransaction struct {
id types.Hash
privateKey crypto.PrivateKeyBytes
outputs []*CoinbaseTransactionOutput
}
func NewCoinbaseTransaction(id types.Hash, privateKey crypto.PrivateKeyBytes, 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() crypto.PrivateKeyBytes {
return t.privateKey
}
func (t *CoinbaseTransaction) Id() types.Hash {
return t.id
}
func (t *CoinbaseTransaction) GetEphemeralPublicKey(miner *Miner, index int64) crypto.PublicKey {
if index != -1 {
return address.GetEphemeralPublicKey(miner.MoneroAddress(), &t.privateKey, uint64(index))
} else {
return address.GetEphemeralPublicKey(miner.MoneroAddress(), &t.privateKey, t.OutputByMiner(miner.Id()).Index())
}
}

View file

@ -1,35 +0,0 @@
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
}

View file

@ -1,1027 +0,0 @@
package database
import (
"context"
"database/sql"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"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
GetMinerByAlias *sql.Stmt
GetMinerByAddressBounds *sql.Stmt
InsertMiner *sql.Stmt
GetUnclesByParentId *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, 4096),
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, alias, address FROM miners WHERE id = $1;"); err != nil {
return nil, err
}
if db.statements.GetMinerByAddress, err = db.handle.Prepare("SELECT id, alias, address FROM miners WHERE address = $1;"); err != nil {
return nil, err
}
if db.statements.GetMinerByAlias, err = db.handle.Prepare("SELECT id, alias, address FROM miners WHERE alias = $1;"); err != nil {
return nil, err
}
if db.statements.GetMinerByAddressBounds, err = db.handle.Prepare("SELECT id, alias, 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, alias, address;"); err != nil {
return nil, err
}
if db.statements.GetUnclesByParentId, err = db.PrepareUncleBlocksByQueryStatement("WHERE parent_id = encode($1, 'hex');"); 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*4*30 {
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.alias, &m.addr); err != nil {
return nil
}
return m
}
return nil
}
}
func (db *Database) GetMinerByAlias(alias string) *Miner {
if rows, err := db.statements.GetMinerByAlias.Query(alias); err != nil {
return nil
} else {
defer rows.Close()
if rows.Next() {
m := &Miner{}
if err = rows.Scan(&m.id, &m.alias, &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.alias, &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.alias, &m.addr); err != nil {
return nil
}
return m
}
return nil
}
}
}
func (db *Database) GetMinerByAddressBounds(addrStart, addrEnd string) *Miner {
if rows, err := db.statements.GetMinerByAddressBounds.Query(addrStart+"%", "%"+addrEnd); err != nil {
return nil
} else {
defer rows.Close()
if rows.Next() {
m := &Miner{}
if err = rows.Scan(&m.id, &m.alias, &m.addr); err != nil {
return nil
}
return m
}
return nil
}
}
func (db *Database) SetMinerAlias(minerId uint64, alias string) error {
miner := db.GetMiner(minerId)
if miner == nil {
return nil
}
if alias == "" {
if err := db.Query("UPDATE miners SET alias = NULL WHERE id = $1;", nil, miner.Id()); err != nil {
return err
}
miner.alias.String = ""
miner.alias.Valid = false
} else {
if err := db.Query("UPDATE miners SET alias = $2 WHERE id = $1;", nil, miner.Id(), alias); err != nil {
return err
}
miner.alias.String = alias
miner.alias.Valid = true
}
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) PrepareBlocksByQueryStatement(where string) (stmt *sql.Stmt, err error) {
return db.handle.Prepare(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 (db *Database) PrepareUncleBlocksByQueryStatement(where string) (stmt *sql.Stmt, err error) {
return db.handle.Prepare(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 (db *Database) GetBlocksByQuery(where string, params ...any) chan *Block {
if stmt, err := db.PrepareBlocksByQueryStatement(where); err != nil {
returnChannel := make(chan *Block)
close(returnChannel)
return returnChannel
} else {
return db.GetBlocksByQueryStatement(stmt, params...)
}
}
func (db *Database) GetBlocksByQueryStatement(stmt *sql.Stmt, params ...any) chan *Block {
returnChannel := make(chan *Block)
go func() {
defer close(returnChannel)
err := db.QueryStatement(stmt, func(row RowScanInterface) (err error) {
block := &Block{}
var difficultyHex, minerMainDifficultyHex string
var IdPtr, PreviousIdPtr, CoinbaseIdPtr, CoinbasePrivateKeyPtr, PowHashPtr, MainIdPtr, MinerMainId sql.RawBytes
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 {
if stmt, err := db.PrepareUncleBlocksByQueryStatement(where); err != nil {
returnChannel := make(chan *UncleBlock)
close(returnChannel)
return returnChannel
} else {
return db.GetUncleBlocksByQueryStatement(stmt, params...)
}
}
func (db *Database) GetUncleBlocksByQueryStatement(stmt *sql.Stmt, params ...any) chan *UncleBlock {
returnChannel := make(chan *UncleBlock)
go func() {
defer close(returnChannel)
err := db.QueryStatement(stmt, func(row RowScanInterface) (err error) {
uncle := &UncleBlock{}
block := &uncle.Block
var difficultyHex, minerMainDifficultyHex string
var ParentId, IdPtr, PreviousIdPtr, CoinbaseIdPtr, CoinbasePrivateKeyPtr, PowHashPtr, MainIdPtr, MinerMainId sql.RawBytes
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 = encode($1, 'hex');", id[:])
defer func() {
for range r {
}
}()
return <-r
}
func (db *Database) GetBlockByPreviousId(id types.Hash) *Block {
r := db.GetBlocksByQuery("WHERE previous_id = encode($1, 'hex');", id[:])
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, 0)
defer func() {
for range r {
}
}()
return <-r
}
func (db *Database) GetUncleById(id types.Hash) *UncleBlock {
r := db.GetUncleBlocksByQuery("WHERE id = encode($1, 'hex');", id[:])
defer func() {
for range r {
}
}()
return <-r
}
func (db *Database) getUnclesByParentId(id types.Hash) chan *UncleBlock {
return db.GetUncleBlocksByQueryStatement(db.statements.GetUnclesByParentId, id[:])
}
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 > ($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, minerId uint64) chan BlockInterface {
blocks := db.GetFound(limit, minerId)
uncles := db.GetFoundUncles(limit, minerId)
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
result := make(chan BlockInterface)
if limit == 0 {
if minerId != 0 {
blocks = db.GetBlocksByQuery("WHERE miner = $1 ORDER BY height DESC;", minerId)
if !onlyBlocks {
uncles = db.GetUncleBlocksByQuery("WHERE miner = $1 ORDER 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)
}
}
}
if onlyBlocks {
go func() {
defer close(result)
for b := range blocks {
result <- b
}
}()
return result
}
go func() {
defer func() {
if blocks == nil {
return
}
for range blocks {
}
}()
defer func() {
if uncles == nil {
return
}
for range uncles {
}
}()
defer close(result)
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, minerId uint64) chan *Block {
if minerId == 0 {
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)
}
} else {
if limit == 0 {
return db.GetBlocksByQuery("WHERE main_found IS TRUE AND miner = $1 ORDER BY main_height DESC;", minerId)
} else {
return db.GetBlocksByQuery("WHERE main_found IS TRUE AND miner = $2 ORDER BY main_height DESC LIMIT $1;", limit, minerId)
}
}
}
func (db *Database) GetFoundUncles(limit, minerId uint64) chan *UncleBlock {
if minerId == 0 {
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)
}
} else {
if limit == 0 {
return db.GetUncleBlocksByQuery("WHERE main_found IS TRUE AND miner = $1 ORDER BY main_height DESC;", minerId)
} else {
return db.GetUncleBlocksByQuery("WHERE main_found IS TRUE AND miner = $2 ORDER BY main_height DESC LIMIT $1;", limit, minerId)
}
}
}
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, block.Id.String()); err != nil {
return n, err
} else if err = db.Query("DELETE FROM uncles WHERE parent_id = $1;", nil, block.Id.String()); err != nil {
return n, err
} else if err = db.Query("DELETE FROM blocks WHERE id = $1;", nil, block.Id.String()); err != nil {
return n, err
}
n++
block = db.GetBlockByPreviousId(block.Id)
if block == nil {
return n, nil
}
}
}
}
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 = encode($1, 'hex');", func(row RowScanInterface) error {
return row.Scan(&count)
}, block.Coinbase.Id[:]); 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 = encode($1, 'hex') 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[:]); 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 = encode($1, 'hex') 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[:], 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 = encode($1, 'hex') 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[:], 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,omitempty"`
Coinbase struct {
Id types.Hash `json:"id"`
Reward uint64 `json:"reward"`
PrivateKey crypto.PrivateKeyBytes `json:"private_key"`
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)
miner := db.getMiner(minerId)
if miner == nil {
return
}
resultFunc := func(row RowScanInterface) error {
var blockId, mainId, privKey, coinbaseId []byte
var height, mainHeight, timestamp, amount, index uint64
var uncle bool
if err := row.Scan(&blockId, &mainId, &height, &mainHeight, &timestamp, &privKey, &uncle, &coinbaseId, &amount, &index); err != nil {
return err
}
out <- &Payout{
Id: types.HashFromBytes(blockId),
Height: height,
Timestamp: timestamp,
Main: struct {
Id types.Hash `json:"id"`
Height uint64 `json:"height"`
}{Id: types.HashFromBytes(mainId), Height: mainHeight},
Uncle: uncle,
Coinbase: struct {
Id types.Hash `json:"id"`
Reward uint64 `json:"reward"`
PrivateKey crypto.PrivateKeyBytes `json:"private_key"`
Index uint64 `json:"index"`
}{Id: types.HashFromBytes(coinbaseId), Reward: amount, PrivateKey: crypto.PrivateKeyBytes(types.HashFromBytes(privKey)), Index: index},
}
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 (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 (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 b.Id != block.Id {
return errors.New("block exists but has different id")
}
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 u.Block.Id != uncle.Block.Id {
return errors.New("block exists but has different id")
}
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()
}

View file

@ -1,103 +0,0 @@
package database
import (
"bytes"
"database/sql"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"sync/atomic"
)
type Miner struct {
id uint64
addr string
alias sql.NullString
moneroAddress atomic.Pointer[address.Address]
}
func (m *Miner) Id() uint64 {
return m.id
}
func (m *Miner) Alias() string {
if m.alias.Valid {
return m.alias.String
}
return ""
}
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 outputResult struct {
Miner *Miner
Output *transaction.Output
}
func MatchOutputs(c *transaction.CoinbaseTransaction, miners []*Miner, privateKey crypto.PrivateKey) (result []outputResult) {
addresses := make(map[address.PackedAddress]*Miner, len(miners))
outputs := make([]*transaction.Output, len(c.Outputs))
for i := range c.Outputs {
outputs[i] = &c.Outputs[i]
}
//TODO: this sorting will be inefficient come the hard fork
for _, m := range miners {
addresses[*m.MoneroAddress().ToPackedAddress()] = m
}
sortedAddresses := maps.Keys(addresses)
slices.SortFunc(sortedAddresses, func(a address.PackedAddress, b address.PackedAddress) bool {
return a.Compare(&b) < 0
})
result = make([]outputResult, 0, len(miners))
for _, k := range sortedAddresses {
derivation := privateKey.GetDerivationCofactor(k.ViewPublicKey())
for i, o := range outputs {
if o == nil {
continue
}
if o.Type == transaction.TxOutToTaggedKey && o.ViewTag != crypto.GetDerivationViewTagForOutputIndex(derivation, o.Index) { //fast check
continue
}
sharedData := crypto.GetDerivationSharedDataForOutputIndex(derivation, o.Index)
if bytes.Compare(o.EphemeralPublicKey[:], address.GetPublicKeyForSharedData(&k, sharedData).AsSlice()) == 0 {
//TODO: maybe clone?
result = append(result, outputResult{
Miner: addresses[k],
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
}

View file

@ -1,14 +0,0 @@
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
}

View file

@ -1,13 +0,0 @@
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
}

View file

@ -5,10 +5,46 @@ networks:
external: false
volumes:
p2pool:
external: false
db:
external: false
services:
tor:
image: goldy/tor-hidden-service:v0.4.7.12-54c0e54
tmpfs:
- /tmp
restart: always
environment:
TOR_SOCKS_PORT: 9050
SERVICE1_TOR_SERVICE_HOSTS: 80:site:80,${P2POOL_EXTERNAL_PORT}:p2pool:${P2POOL_PORT}
SERVICE1_TOR_SERVICE_VERSION: '3'
SERVICE1_TOR_SERVICE_KEY: ${TOR_SERVICE_KEY}
depends_on:
- site
- p2pool
networks:
- p2pool-observer
site:
build:
context: ./docker/nginx
dockerfile: Dockerfile
args:
- TOR_SERVICE_ADDRESS=${TOR_SERVICE_ADDRESS}
- NET_SERVICE_ADDRESS=${NET_SERVICE_ADDRESS}
restart: always
depends_on:
- api
- web
tmpfs:
- /run
- /var/cache/nginx
- /tmp
networks:
- p2pool-observer
ports:
- ${SITE_PORT}:80
db:
image: postgres:15.2
restart: always
@ -46,4 +82,92 @@ services:
# needed configuration. The read-only flag does not make volumes and tmpfs read-only.
- /tmp
- /run
- /run/postgresql
- /run/postgresql
p2pool:
build:
context: ./
dockerfile: ./docker/golang/Dockerfile
restart: always
depends_on:
- db
security_opt:
- no-new-privileges:true
volumes:
- p2pool:/data:rw
networks:
- p2pool-observer
working_dir: /data
ports:
- ${P2POOL_PORT}:${P2POOL_PORT}
command: >-
/usr/bin/p2pool
-out-peers ${P2POOL_OUT_PEERS}
-in-peers ${P2POOL_IN_PEERS}
-host ${MONEROD_HOST}
-rpc-port ${MONEROD_RPC_PORT}
-zmq-port ${MONEROD_ZMQ_PORT}
-p2p 0.0.0.0:${P2POOL_PORT}
-p2p-external-port ${P2POOL_EXTERNAL_PORT}
-api-bind '0.0.0.0:3131'
-archive /data/archive.db ${P2POOL_EXTRA_ARGS}
api:
build:
context: ./
dockerfile: ./docker/golang/Dockerfile
restart: always
environment:
- TOR_SERVICE_ADDRESS=${TOR_SERVICE_ADDRESS}
depends_on:
- db
- p2pool
security_opt:
- no-new-privileges:true
networks:
- p2pool-observer
command: >-
/usr/bin/api
-host ${MONEROD_HOST}
-rpc-port ${MONEROD_RPC_PORT}
-api-host "http://p2pool:3131"
-db="host=db port=5432 dbname=p2pool user=p2pool password=p2pool sslmode=disable"
web:
build:
context: ./
dockerfile: ./docker/golang/Dockerfile
restart: always
environment:
- MONEROD_RPC_URL=http://${MONEROD_HOST}:${MONEROD_RPC_PORT}
- TOR_SERVICE_ADDRESS=${TOR_SERVICE_ADDRESS}
- NET_SERVICE_ADDRESS=${NET_SERVICE_ADDRESS}
- API_URL=http://api:8080/api/
- SITE_TITLE=${SITE_TITLE}
depends_on:
- api
security_opt:
- no-new-privileges:true
networks:
- p2pool-observer
command: >-
/usr/bin/web
daemon:
build:
context: ./
dockerfile: ./docker/golang/Dockerfile
restart: always
environment:
- TOR_SERVICE_ADDRESS=${TOR_SERVICE_ADDRESS}
depends_on:
- db
- p2pool
security_opt:
- no-new-privileges:true
networks:
- p2pool-observer
command: >-
/usr/bin/daemon
-host ${MONEROD_HOST}
-rpc-port ${MONEROD_RPC_PORT}
-api-host "http://p2pool:3131"
-db="host=db port=5432 dbname=p2pool user=p2pool password=p2pool sslmode=disable"

40
docker/golang/Dockerfile Normal file
View file

@ -0,0 +1,40 @@
FROM golang:1.20-alpine
ENV CFLAGS="-march=native -Ofast -flto"
ENV CXXFLAGS="-march=native -Ofast -flto"
ENV LDFLAGS="-flto"
ENV CGO_CFLAGS="-march=native -Ofast"
ENV GOPROXY="direct"
RUN apk update && apk add --no-cache \
git gcc g++ musl-dev bash autoconf automake cmake make libtool gettext
RUN git clone --depth 1 --branch master https://github.com/tevador/RandomX.git /tmp/RandomX && cd /tmp/RandomX && \
mkdir build && cd build && \
cmake .. -DCMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX:PATH=/usr && \
make -j$(nproc) && \
make install && \
cd ../ && \
rm -rf /tmp/RandomX
WORKDIR /usr/src/p2pool
COPY ./ .
# p2pool
RUN go build -buildvcs=false -trimpath -v -o /usr/bin/p2pool git.gammaspectra.live/P2Pool/p2pool-observer/cmd/p2pool
# observer stuff
RUN go build -buildvcs=false -trimpath -v -o /usr/bin/api git.gammaspectra.live/P2Pool/p2pool-observer/cmd/api
RUN go build -buildvcs=false -trimpath -v -o /usr/bin/daemon git.gammaspectra.live/P2Pool/p2pool-observer/cmd/daemon
RUN go build -buildvcs=false -trimpath -v -o /usr/bin/web git.gammaspectra.live/P2Pool/p2pool-observer/cmd/web
# utilities
RUN go build -buildvcs=false -trimpath -v -o /usr/bin/cachetoarchive git.gammaspectra.live/P2Pool/p2pool-observer/cmd/cachetoarchive
RUN go build -buildvcs=false -trimpath -v -o /usr/bin/archivetoindex git.gammaspectra.live/P2Pool/p2pool-observer/cmd/archivetoindex
RUN go build -buildvcs=false -trimpath -v -o /usr/bin/archivetoarchive git.gammaspectra.live/P2Pool/p2pool-observer/cmd/archivetoarchive
WORKDIR /data

9
docker/nginx/Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM nginx:mainline
COPY static /web
COPY snippets/*.conf /etc/nginx/snippets/
COPY site.conf /etc/nginx/conf.d/site.conf
ARG TOR_SERVICE_ADDRESS
RUN echo "add_header Onion-Location \"http://${TOR_SERVICE_ADDRESS}\$request_uri\" always;" > /etc/nginx/snippets/onion-headers.conf

104
docker/nginx/site.conf Normal file
View file

@ -0,0 +1,104 @@
server {
listen 80 fastopen=200 default_server;
access_log /dev/null;
error_log /dev/null;
server_name _;
root /web;
proxy_connect_timeout 60;
proxy_read_timeout 120;
max_ranges 1;
tcp_nodelay on;
gzip on;
gzip_types text/html text/css text/xml text/plain text/javascript text/xml application/xml application/x-javascript application/javascript application/json image/svg+xml application/font-woff application/font-woff2 application/font-ttf application/octet-stream application/wasm;
gzip_min_length 1000;
gzip_proxied any;
gzip_comp_level 5;
gzip_disable "MSIE [1-6]\.";
server_tokens off;
real_ip_header X-Forwarded-For;
set_real_ip_from 0.0.0.0/0;
absolute_redirect off;
location ~/c/(.*)$ {
return 302 /api/redirect/coinbase/$1;
}
location ~/t/(.*)$ {
return 302 /api/redirect/transaction/$1;
}
location ~/b/(.*)$ {
return 302 /api/redirect/block/$1;
}
location ~/s/(.*)$ {
return 302 /api/redirect/share/$1;
}
location ~/p/(.*)$ {
return 302 /api/redirect/prove/$1;
}
location ~/m/(.*)$ {
return 302 /api/redirect/miner/$1;
}
try_files $uri $uri/ @web;
include snippets/onion-headers.conf;
location = / {
proxy_pass http://web:8444;
proxy_http_version 1.1;
proxy_set_header Connection "";
add_header Allow "GET, POST";
include snippets/security-headers.conf;
include snippets/csp.conf;
include snippets/onion-headers.conf;
}
location = /api {
proxy_pass http://web:8444;
proxy_http_version 1.1;
proxy_set_header Connection "";
add_header Allow "GET, POST";
include snippets/security-headers.conf;
include snippets/csp.conf;
include snippets/onion-headers.conf;
}
location @web {
proxy_pass http://web:8444;
proxy_http_version 1.1;
proxy_set_header Connection "";
add_header Allow "GET, POST";
include snippets/security-headers.conf;
include snippets/csp.conf;
include snippets/onion-headers.conf;
}
location ^~ /api/ {
proxy_pass http://api:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
add_header Allow "HEAD, OPTIONS, GET, POST";
include snippets/security-headers.conf;
include snippets/cors.conf;
include snippets/onion-headers.conf;
}
location ~* \.(jpg|jpeg|png|webp|gif|svg|ico|css|js|mjs|xml|woff|ttf|ttc|wasm|data|mem)$ {
add_header Cache-Control "public, max-age=2592000"; # 30 days
gzip on;
sendfile on;
}
}

View file

@ -0,0 +1,5 @@
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Max-Age 1728000 always;
add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS' always;
add_header Access-Control-Allow-Headers 'DNT,Authorization,Origin,Accept,User-Agent,X-Requested-With,If-Modified-Since,If-None-Match,Keep-Alive,Cache-Control,Content-Type,Range' always;
add_header Access-Control-Expose-Headers 'Content-Type, Accept-Ranges, Content-Encoding, Content-Length, Content-Range, Cache-Control' always;

View file

@ -0,0 +1 @@
add_header Content-Security-Policy "default-src 'none'; img-src blob: data: ; object-src 'none'; style-src 'unsafe-inline'; style-src-elem 'unsafe-inline'; style-src-attr 'unsafe-inline'; prefetch-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; navigate-to * 'self'" always;

View file

@ -0,0 +1,18 @@
add_header X-Frame-Options deny always;
add_header X-DNS-Prefetch-Control off always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header X-Robots-Tag "nofollow, notranslate" always;
add_header Tk "N" always;
add_header Permissions-Policy "interest-cohort=(), autoplay=(), oversized-images=(), payment=(), fullscreen=(), camera=(), microphone=(), geolocation=(), usb=(), midi=()" always;
add_header Vary 'Origin, Accept-Encoding' always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Expect-CT "max-age=31536000, enforce" always;
add_header Expect-Staple 'max-age=31536000; includeSubDomains; preload' always;

View file

@ -0,0 +1,3 @@
User-agent: *
Disallow: /miner
Allow: /

View file

@ -1,6 +1,7 @@
package client
import (
"context"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"os"
"testing"
@ -22,7 +23,7 @@ func TestOutputIndexes(t *testing.T) {
}
func TestInputs(t *testing.T) {
if result, err := GetDefaultClient().GetTransactionInputs(txHash); err != nil {
if result, err := GetDefaultClient().GetTransactionInputs(context.Background(), txHash); err != nil {
t.Fatal(err)
} else {
t.Log(result)

View file

@ -22,8 +22,6 @@ const (
const (
PPLNSWindow = 2160
BlockTime = 10
UnclePenalty = 20
UncleBlockDepth = 3
)