Implemented ZMQ fetch of block data, proper main chain handling, refactored client RPC access, updated dependencies

This commit is contained in:
DataHoarder 2023-03-05 15:06:49 +01:00
parent ff946d7431
commit 92b74f667f
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
20 changed files with 1261 additions and 366 deletions

View file

@ -1,6 +1,7 @@
package main
import (
"context"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"golang.org/x/exp/maps"
@ -32,9 +33,10 @@ func setHeightDifficulty(height uint64, difficulty types.Difficulty) {
difficultyCache[height] = difficulty
}
// TODO: remove
func cacheHeightDifficulty(height uint64) {
if _, ok := getHeightDifficulty(height); !ok {
if header, err := client.GetDefaultClient().GetBlockHeaderByHeight(height); err != nil {
if header, err := client.GetDefaultClient().GetBlockHeaderByHeight(height, context.Background()); err != nil {
if template, err := client.GetDefaultClient().GetBlockTemplate(types.DonationAddress); err != nil {
setHeightDifficulty(uint64(template.Height), types.DifficultyFrom64(uint64(template.Difficulty)))
}

View file

@ -2,7 +2,6 @@ package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
@ -23,6 +22,7 @@ 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")
moneroZmqPort := flag.Uint("zmq-port", 18083, "monerod ZMQ pub port number")
p2pListen := flag.String("p2p", fmt.Sprintf("0.0.0.0:%d", currentConsensus.DefaultPort()), "IP:port for p2p server to listen on.")
//TODO: zmq
addPeers := flag.String("addpeers", "", "Comma-separated list of IP:port of other p2pool nodes to connect to")
@ -40,11 +40,20 @@ func main() {
settings := make(map[string]string)
settings["listen"] = *p2pListen
if *useMiniSidechain {
if settings["listen"] == fmt.Sprintf("0.0.0.0:%d", currentConsensus.DefaultPort()) {
settings["listen"] = fmt.Sprintf("0.0.0.0:%d", sidechain.ConsensusMini.DefaultPort())
changeConsensus := func(newConsensus *sidechain.Consensus) {
oldListen := netip.MustParseAddrPort(settings["listen"])
// if default exists, change port to match
if settings["listen"] == fmt.Sprintf("%s:%d", oldListen.Addr().String(), currentConsensus.DefaultPort()) {
settings["listen"] = fmt.Sprintf("%s:%d", oldListen.Addr().String(), newConsensus.DefaultPort())
}
currentConsensus = sidechain.ConsensusMini
currentConsensus = newConsensus
}
settings["rpc-url"] = fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort)
settings["zmq-url"] = fmt.Sprintf("tcp://%s:%d", *moneroHost, *moneroZmqPort)
if *useMiniSidechain {
changeConsensus(sidechain.ConsensusMini)
}
if *consensusConfigFile != "" {
@ -52,24 +61,24 @@ func main() {
if err != nil {
log.Panic(err)
}
var newConsensus sidechain.Consensus
if err = json.Unmarshal(consensusData, &newConsensus); err != nil {
log.Panic(err)
}
if settings["listen"] == fmt.Sprintf("0.0.0.0:%d", currentConsensus.DefaultPort()) {
settings["listen"] = fmt.Sprintf("0.0.0.0:%d", newConsensus.DefaultPort())
if newConsensus, err := sidechain.NewConsensusFromJSON(consensusData); err != nil {
log.Panic(err)
} else {
changeConsensus(newConsensus)
}
currentConsensus = &newConsensus
}
settings["out-peers"] = strconv.FormatUint(*outPeers, 10)
settings["in-peers"] = strconv.FormatUint(*inPeers, 10)
settings["external-port"] = strconv.FormatUint(*p2pExternalPort, 10)
if p2pool := p2pool2.NewP2Pool(currentConsensus, settings); p2pool == nil {
log.Fatal("Could not start p2pool")
if p2pool, err := p2pool2.NewP2Pool(currentConsensus, settings); err != nil {
log.Fatalf("Could not start p2pool: %s", err)
} else {
defer p2pool.Close()
var connectList []netip.AddrPort
for _, peerAddr := range strings.Split(*addPeers, ",") {
if peerAddr == "" {
continue
@ -79,11 +88,7 @@ func main() {
log.Panic(err)
} else {
p2pool.Server().AddToPeerList(addrPort)
go func() {
if err := p2pool.Server().Connect(addrPort); err != nil {
log.Printf("error connecting to peer %s: %s", addrPort.String(), err.Error())
}
}()
connectList = append(connectList, addrPort)
}
}
@ -93,11 +98,7 @@ func main() {
for _, seedNodeIp := range ips {
seedNodeAddr := netip.MustParseAddrPort(fmt.Sprintf("%s:%d", seedNodeIp.String(), currentConsensus.DefaultPort()))
p2pool.Server().AddToPeerList(seedNodeAddr)
go func() {
if err := p2pool.Server().Connect(seedNodeAddr); err != nil {
log.Printf("error connecting to seed node peer %s: %s", seedNodeAddr.String(), err.Error())
}
}()
//connectList = append(connectList, seedNodeAddr)
}
}
@ -147,7 +148,21 @@ func main() {
}
}
if err := p2pool.Server().Listen(); err != nil {
go func() {
for !p2pool.Started() {
time.Sleep(time.Second * 1)
}
for _, addrPort := range connectList {
go func(addrPort netip.AddrPort) {
if err := p2pool.Server().Connect(addrPort); err != nil {
log.Printf("error connecting to peer %s: %s", addrPort.String(), err.Error())
}
}(addrPort)
}
}()
if err := p2pool.Run(); err != nil {
log.Panic(err)
}
}

View file

@ -2,12 +2,15 @@ package database
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"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/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"golang.org/x/exp/slices"
@ -29,8 +32,8 @@ type BlockInterface interface {
}
type BlockCoinbase struct {
Id types.Hash `json:"id"`
Reward uint64 `json:"reward"`
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"`
@ -117,6 +120,24 @@ func NewBlockFromBinaryBlock(db *Database, b *sidechain.PoolBlock, knownUncles [
return nil, nil, errors.New("could not get or create miner")
}
getSeedByHeight := func(height uint64) (hash types.Hash) {
seedHeight := randomx.SeedHeight(height)
if h, err := client.GetDefaultClient().GetBlockHeaderByHeight(seedHeight, context.Background()); err != nil {
return types.ZeroHash
} else {
hash, _ := types.HashFromString(h.BlockHeader.Hash)
return hash
}
}
getDifficultyByHeight := func(height uint64) types.Difficulty {
if h, err := client.GetDefaultClient().GetBlockHeaderByHeight(height, context.Background()); err != nil {
return types.ZeroDifficulty
} else {
return types.DifficultyFrom64(h.BlockHeader.Difficulty)
}
}
block = &Block{
Id: types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId)),
Height: b.Side.Height,
@ -134,18 +155,18 @@ func NewBlockFromBinaryBlock(db *Database, b *sidechain.PoolBlock, knownUncles [
Difficulty: b.Side.Difficulty,
Timestamp: b.Main.Timestamp,
MinerId: miner.Id(),
PowHash: b.PowHash(),
PowHash: b.PowHash(getSeedByHeight),
Main: BlockMainData{
Id: b.MainId(),
Height: b.Main.Coinbase.GenHeight,
Found: b.IsProofHigherThanMainDifficulty(),
Found: b.IsProofHigherThanMainDifficulty(getDifficultyByHeight, getSeedByHeight),
},
Template: struct {
Id types.Hash `json:"id"`
Difficulty types.Difficulty `json:"difficulty"`
}{
Id: b.Main.PreviousId,
Difficulty: b.MainDifficulty(),
Difficulty: b.MainDifficulty(getDifficultyByHeight),
},
}
@ -176,18 +197,18 @@ func NewBlockFromBinaryBlock(db *Database, b *sidechain.PoolBlock, knownUncles [
Difficulty: uncle.Side.Difficulty,
Timestamp: uncle.Main.Timestamp,
MinerId: uncleMiner.Id(),
PowHash: uncle.PowHash(),
PowHash: uncle.PowHash(getSeedByHeight),
Main: BlockMainData{
Id: uncle.MainId(),
Height: uncle.Main.Coinbase.GenHeight,
Found: uncle.IsProofHigherThanMainDifficulty(),
Found: uncle.IsProofHigherThanMainDifficulty(getDifficultyByHeight, getSeedByHeight),
},
Template: struct {
Id types.Hash `json:"id"`
Difficulty types.Difficulty `json:"difficulty"`
}{
Id: uncle.Main.PreviousId,
Difficulty: uncle.MainDifficulty(),
Difficulty: uncle.MainDifficulty(getDifficultyByHeight),
},
},
ParentId: block.Id,
@ -202,61 +223,61 @@ func NewBlockFromBinaryBlock(db *Database, b *sidechain.PoolBlock, knownUncles [
}
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"`
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"`
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"`
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"`

View file

@ -43,8 +43,6 @@ func (m *Miner) MoneroAddress() *address.Address {
}
}
type outputResult struct {
Miner *Miner
Output *transaction.Output
@ -53,7 +51,10 @@ type outputResult struct {
func MatchOutputs(c *transaction.CoinbaseTransaction, miners []*Miner, privateKey crypto.PrivateKey) (result []outputResult) {
addresses := make(map[address.PackedAddress]*Miner, len(miners))
outputs := c.Outputs
outputs := make([]*transaction.Output, len(c.Outputs))
for i, o := range c.Outputs {
outputs[i] = &o
}
for _, m := range miners {
addresses[*m.MoneroAddress().ToPackedAddress()] = m

16
go.mod
View file

@ -4,7 +4,7 @@ go 1.19
require (
filippo.io/edwards25519 v1.0.1-0.20220803165937-8c58ed0e3550
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20221005074023-b6ca970f3050
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230305004001-d8d87587c8a8
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221025112134-5190471ef823
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20221007140323-a2daa2d5fc48
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20221027134633-11f5607e6752
@ -18,9 +18,9 @@ require (
github.com/lib/pq v1.10.7
github.com/stretchr/testify v1.7.0
github.com/tyler-sommer/stick v1.0.4
golang.org/x/crypto v0.5.0
golang.org/x/exp v0.0.0-20230113213754-f9f960f08ad4
golang.org/x/net v0.5.0
golang.org/x/crypto v0.7.0
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017
golang.org/x/net v0.8.0
lukechampine.com/uint128 v1.2.0
)
@ -30,8 +30,8 @@ require (
github.com/go-zeromq/goczmq/v4 v4.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

32
go.sum
View file

@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.0.1-0.20220803165937-8c58ed0e3550 h1:Mqu6Q2e//30TWeP5bM9Th5KEzWdFAFd80Y2ZXN9fmeE=
filippo.io/edwards25519 v1.0.1-0.20220803165937-8c58ed0e3550/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/go-monero v0.0.0-20230305004001-d8d87587c8a8 h1:FFhYIN01+QhQ53VH1RA08nH318kFYEkBmEdz0iMm0kA=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230305004001-d8d87587c8a8/go.mod h1:E6nns07xjNbnF+3LKZAjQ9zwpJGLivti8WCKfm+/OII=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221025112134-5190471ef823 h1:HxIuImZsB15Ix59K5VznoHzZ1ut5hW0AAqiDJpOd2+g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221025112134-5190471ef823/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20221007140323-a2daa2d5fc48 h1:ExrYG0RSrx/I4McPWgUF4B8R2OkblMrMki2ia8vG6Bw=
@ -54,10 +54,10 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
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.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20230113213754-f9f960f08ad4 h1:CNkDRtCj8otM5CFz5jYvbr8ioXX8flVsLfDWEj0M5kk=
golang.org/x/exp v0.0.0-20230113213754-f9f960f08ad4/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017 h1:3Ea9SZLCB0aRIhSEjM+iaGIlzzeDJdpi579El/YIhEE=
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
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=
@ -68,14 +68,14 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
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/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
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/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/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=
@ -84,13 +84,13 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
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.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/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/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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=
@ -104,7 +104,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -3,7 +3,7 @@ package block
import (
"bytes"
"encoding/binary"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"errors"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
@ -294,21 +294,24 @@ func (b *Block) TxTreeHash() (rootHash types.Hash) {
return
}
func (b *Block) Difficulty() types.Difficulty {
func (b *Block) Difficulty(f GetDifficultyByHeightFunc) types.Difficulty {
//cached by sidechain.Share
d, _ := client.GetDefaultClient().GetDifficultyByHeight(b.Coinbase.GenHeight)
return d
return f(b.Coinbase.GenHeight)
}
func (b *Block) PowHash() types.Hash {
func (b *Block) PowHash(f GetSeedByHeightFunc) types.Hash {
//cached by sidechain.Share
h, _ := HashBlob(b.Coinbase.GenHeight, b.HashingBlob())
h, _ := b.PowHashWithError(f)
return h
}
func (b *Block) PowHashWithError() (types.Hash, error) {
func (b *Block) PowHashWithError(f GetSeedByHeightFunc) (types.Hash, error) {
//not cached
return HashBlob(b.Coinbase.GenHeight, b.HashingBlob())
if seed := f(b.Coinbase.GenHeight); seed == types.ZeroHash {
return types.ZeroHash, errors.New("could not get seed")
} else {
return hasher.Hash(seed[:], b.HashingBlob())
}
}
func (b *Block) Id() types.Hash {

10
monero/block/func.go Normal file
View file

@ -0,0 +1,10 @@
package block
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
)
type GetDifficultyByHeightFunc func(height uint64) types.Difficulty
type GetSeedByHeightFunc func(height uint64) (hash types.Hash)
type GetBlockHeaderByHashFunc func(hash types.Hash) *Header
type GetBlockHeaderByHeightFunc func(height uint64) *Header

View file

@ -1,91 +0,0 @@
package block
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"github.com/floatdrop/lru"
"sync"
)
func HashBlob(height uint64, blob []byte) (hash types.Hash, err error) {
if seed, err := client.GetDefaultClient().GetSeedByHeight(height); err != nil {
return types.ZeroHash, err
} else {
return hasher.Hash(seed[:], blob)
}
}
var blockHeaderByHash = lru.New[types.Hash, *Header](128)
var blockHeaderByHashLock sync.Mutex
func GetBlockHeaderByHeight(height uint64) *Header {
//TODO: cache
if header, err := client.GetDefaultClient().GetBlockHeaderByHeight(height); err != nil {
return nil
} else {
prevHash, _ := types.HashFromString(header.BlockHeader.PrevHash)
h, _ := types.HashFromString(header.BlockHeader.Hash)
return &Header{
MajorVersion: uint8(header.BlockHeader.MajorVersion),
MinorVersion: uint8(header.BlockHeader.MinorVersion),
Timestamp: uint64(header.BlockHeader.Timestamp),
PreviousId: prevHash,
Height: header.BlockHeader.Difficulty,
Nonce: uint32(header.BlockHeader.Nonce),
Reward: header.BlockHeader.Reward,
Id: h,
Difficulty: types.DifficultyFrom64(header.BlockHeader.Difficulty),
}
}
}
func GetBlockHeaderByHash(hash types.Hash) *Header {
blockHeaderByHashLock.Lock()
defer blockHeaderByHashLock.Unlock()
if h := blockHeaderByHash.Get(hash); h == nil {
if header, err := client.GetDefaultClient().GetBlockHeaderByHash(hash); err != nil || len(header.BlockHeaders) != 1 {
return nil
} else {
prevHash, _ := types.HashFromString(header.BlockHeaders[0].PrevHash)
blockHash, _ := types.HashFromString(header.BlockHeaders[0].Hash)
blockHeader := &Header{
MajorVersion: uint8(header.BlockHeaders[0].MajorVersion),
MinorVersion: uint8(header.BlockHeaders[0].MinorVersion),
Timestamp: uint64(header.BlockHeaders[0].Timestamp),
PreviousId: prevHash,
Height: header.BlockHeaders[0].Height,
Nonce: uint32(header.BlockHeaders[0].Nonce),
Reward: header.BlockHeaders[0].Reward,
Id: blockHash,
Difficulty: types.DifficultyFrom64(header.BlockHeaders[0].Difficulty),
}
blockHeaderByHash.Set(hash, blockHeader)
return blockHeader
}
} else {
return *h
}
}
func GetLastBlockHeader() *Header {
if header, err := client.GetDefaultClient().GetLastBlockHeader(); err != nil {
return nil
} else {
prevHash, _ := types.HashFromString(header.BlockHeader.PrevHash)
h, _ := types.HashFromString(header.BlockHeader.Hash)
return &Header{
MajorVersion: uint8(header.BlockHeader.MajorVersion),
MinorVersion: uint8(header.BlockHeader.MinorVersion),
Timestamp: uint64(header.BlockHeader.Timestamp),
PreviousId: prevHash,
Height: header.BlockHeader.Difficulty,
Nonce: uint32(header.BlockHeader.Nonce),
Reward: header.BlockHeader.Reward,
Id: h,
Difficulty: types.DifficultyFrom64(header.BlockHeader.Difficulty),
}
}
}

View file

@ -9,7 +9,6 @@ import (
"fmt"
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc"
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"github.com/floatdrop/lru"
@ -54,15 +53,11 @@ func GetDefaultClient() *Client {
}
}
// Client TODO: ratelimit
// Client
type Client struct {
c *rpc.Client
d *daemon.Client
difficultyCache *lru.LRU[uint64, types.Difficulty]
seedCache *lru.LRU[uint64, types.Hash]
coinbaseTransactionCache *lru.LRU[types.Hash, *transaction.CoinbaseTransaction]
throttler <-chan time.Time
@ -76,10 +71,8 @@ func NewClient(address string) (*Client, error) {
return &Client{
c: c,
d: daemon.NewClient(c),
difficultyCache: lru.New[uint64, types.Difficulty](1024),
seedCache: lru.New[uint64, types.Hash](1024),
coinbaseTransactionCache: lru.New[types.Hash, *transaction.CoinbaseTransaction](1024),
throttler: time.Tick(time.Second / 2),
throttler: time.Tick(time.Second / 8),
}, nil
}
@ -252,63 +245,27 @@ func (c *Client) GetOuts(inputs ...uint64) ([]Output, error) {
}
}
func (c *Client) GetBlockIdByHeight(height uint64) (types.Hash, error) {
if r, err := c.GetBlockHeaderByHeight(height); err != nil {
return types.ZeroHash, err
} else {
if h, err := types.HashFromString(r.BlockHeader.Hash); err != nil {
return types.ZeroHash, err
} else {
return h, nil
}
}
}
func (c *Client) AddSeedByHeightToCache(seedHeight uint64, seed types.Hash) {
c.seedCache.Set(seedHeight, seed)
}
func (c *Client) GetSeedByHeight(height uint64) (types.Hash, error) {
seedHeight := randomx.SeedHeight(height)
if seed := c.seedCache.Get(seedHeight); seed == nil {
if seed, err := c.GetBlockIdByHeight(seedHeight); err != nil {
return types.ZeroHash, err
} else {
c.AddSeedByHeightToCache(seedHeight, seed)
return seed, nil
}
} else {
return *seed, nil
}
}
func (c *Client) GetDifficultyByHeight(height uint64) (types.Difficulty, error) {
if difficulty := c.difficultyCache.Get(height); difficulty == nil {
if header, err := c.GetBlockHeaderByHeight(height); err != nil {
if template, err := c.GetBlockTemplate(types.DonationAddress); err != nil {
return types.ZeroDifficulty, err
} else if uint64(template.Height) == height {
difficulty := types.DifficultyFrom64(uint64(template.Difficulty))
c.difficultyCache.Set(height, difficulty)
return difficulty, nil
} else {
return types.ZeroDifficulty, errors.New("height not found and is not next template")
}
} else {
difficulty := types.DifficultyFrom64(uint64(header.BlockHeader.Difficulty))
c.difficultyCache.Set(height, difficulty)
return difficulty, nil
}
} else {
return *difficulty, nil
}
}
func (c *Client) GetBlockHeaderByHash(hash types.Hash) (*daemon.GetBlockHeaderByHashResult, error) {
func (c *Client) GetVersion() (*daemon.GetVersionResult, error) {
<-c.throttler
if result, err := c.d.GetBlockHeaderByHash(context.Background(), []string{hash.String()}); err != nil {
if result, err := c.d.GetVersion(context.Background()); err != nil {
return nil, err
} else {
return result, nil
}
}
func (c *Client) GetInfo() (*daemon.GetInfoResult, error) {
<-c.throttler
if result, err := c.d.GetInfo(context.Background()); err != nil {
return nil, err
} else {
return result, nil
}
}
func (c *Client) GetBlockHeaderByHash(hash types.Hash, ctx context.Context) (*daemon.GetBlockHeaderByHashResult, error) {
<-c.throttler
if result, err := c.d.GetBlockHeaderByHash(ctx, []string{hash.String()}); err != nil {
return nil, err
} else {
return result, nil
@ -324,9 +281,43 @@ func (c *Client) GetLastBlockHeader() (*daemon.GetLastBlockHeaderResult, error)
}
}
func (c *Client) GetBlockHeaderByHeight(height uint64) (*daemon.GetBlockHeaderByHeightResult, error) {
func (c *Client) GetBlockHeaderByHeight(height uint64, ctx context.Context) (*daemon.GetBlockHeaderByHeightResult, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-c.throttler:
if result, err := c.d.GetBlockHeaderByHeight(ctx, height); err != nil {
return nil, err
} else {
return result, nil
}
}
}
func (c *Client) GetBlockHeadersRangeResult(start, end uint64, ctx context.Context) (*daemon.GetBlockHeadersRangeResult, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-c.throttler:
if result, err := c.d.GetBlockHeadersRange(ctx, start, end); err != nil {
return nil, err
} else {
return result, nil
}
}
}
func (c *Client) SubmitBlock(blob []byte) (*daemon.SubmitBlockResult, error) {
if result, err := c.d.SubmitBlock(context.Background(), blob); err != nil {
return nil, err
} else {
return result, nil
}
}
func (c *Client) GetMinerData() (*daemon.GetMinerDataResult, error) {
<-c.throttler
if result, err := c.d.GetBlockHeaderByHeight(context.Background(), height); err != nil {
if result, err := c.d.GetMinerData(context.Background()); err != nil {
return nil, err
} else {
return result, nil

View file

@ -41,9 +41,13 @@ type FullChainMain struct {
} `json:"inputs"`
Outputs []struct {
Amount uint64 `json:"amount"`
ToKey struct {
ToKey *struct {
Key crypto.PublicKeyBytes `json:"key"`
} `json:"to_key"`
ToTaggedKey *struct {
Key crypto.PublicKeyBytes `json:"key"`
ViewTag string `json:"view_tag"`
} `json:"to_tagged_key"`
} `json:"outputs"`
Extra string `json:"extra"`
Signatures []interface{} `json:"signatures"`

View file

@ -0,0 +1,444 @@
package mainchain
import (
"context"
"fmt"
mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client/zmq"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"golang.org/x/exp/slices"
"log"
"sync"
"sync/atomic"
"time"
)
const TimestampWindow = 60
const BlockHeadersRequired = 720
type MainChain struct {
p2pool P2PoolInterface
lock sync.RWMutex
sidechain *sidechain.SideChain
highest uint64
mainchainByHeight map[uint64]*sidechain.ChainMain
mainchainByHash map[types.Hash]*sidechain.ChainMain
tip atomic.Pointer[sidechain.ChainMain]
tipMinerData atomic.Pointer[MinerData]
medianTimestamp atomic.Uint64
}
type P2PoolInterface interface {
ClientRPC() *client.Client
ClientZMQ() *zmq.Client
Context() context.Context
Started() bool
UpdateBlockFound(data *sidechain.ChainMain, block *sidechain.PoolBlock)
}
func NewMainChain(s *sidechain.SideChain, p2pool P2PoolInterface) *MainChain {
m := &MainChain{
sidechain: s,
p2pool: p2pool,
mainchainByHeight: make(map[uint64]*sidechain.ChainMain),
mainchainByHash: make(map[types.Hash]*sidechain.ChainMain),
}
return m
}
func (c *MainChain) Listen() error {
ctx := c.p2pool.Context()
s, err := c.p2pool.ClientZMQ().Listen(ctx)
if err != nil {
return err
}
for {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-s.ErrC:
//TODO: retry connection
return err
case fullChainMain := <-s.FullChainMainC:
log.Print(fullChainMain)
case fullMinerData := <-s.FullMinerDataC:
log.Print(fullMinerData)
}
}
}
func (c *MainChain) getTimestamps(timestamps []uint64) bool {
_ = timestamps[TimestampWindow-1]
if len(c.mainchainByHeight) <= TimestampWindow {
return false
}
for i := 0; i < TimestampWindow; i++ {
h, ok := c.mainchainByHeight[c.highest-uint64(i)]
if !ok {
break
}
timestamps[i] = h.Timestamp
}
return true
}
func (c *MainChain) updateMedianTimestamp() {
var timestamps [TimestampWindow]uint64
if !c.getTimestamps(timestamps[:]) {
c.medianTimestamp.Store(0)
return
}
slices.Sort(timestamps[:])
// Shift it +1 block compared to Monero's code because we don't have the latest block yet when we receive new miner data
ts := (timestamps[TimestampWindow/2] + timestamps[TimestampWindow/2+1]) / 2
log.Printf("[MainChain] Median timestamp updated to %d", ts)
c.medianTimestamp.Store(ts)
}
func (c *MainChain) HandleMainHeader(mainHeader *mainblock.Header) {
c.lock.Lock()
defer c.lock.Unlock()
mainData := &sidechain.ChainMain{
Difficulty: mainHeader.Difficulty,
Height: mainHeader.Height,
Timestamp: mainHeader.Timestamp,
Reward: mainHeader.Reward,
Id: mainHeader.Id,
}
c.mainchainByHeight[mainHeader.Height] = mainData
c.mainchainByHash[mainHeader.Id] = mainData
if mainData.Height > c.highest {
c.highest = mainData.Height
}
log.Printf("[MainChain] new main chain block: height = %d, id = %s, timestamp = %d, reward = %s", mainData.Height, mainData.Id.String(), mainData.Timestamp, utils.XMRUnits(mainData.Reward))
c.updateMedianTimestamp()
}
func (c *MainChain) HandleMainBlock(b *mainblock.Block) {
mainData := &sidechain.ChainMain{
Difficulty: types.ZeroDifficulty,
Height: b.Coinbase.GenHeight,
Timestamp: b.Timestamp,
Reward: b.Coinbase.TotalReward,
Id: b.Id(),
}
func() {
c.lock.Lock()
defer c.lock.Unlock()
if h, ok := c.mainchainByHeight[mainData.Height]; ok {
mainData.Difficulty = h.Difficulty
} else {
return
}
c.mainchainByHash[mainData.Id] = mainData
c.mainchainByHeight[mainData.Height] = mainData
if mainData.Height > c.highest {
c.highest = mainData.Height
}
log.Printf("[MainChain] new main chain block: height = %d, id = %s, timestamp = %d, reward = %s", mainData.Height, mainData.Id.String(), mainData.Timestamp, utils.XMRUnits(mainData.Reward))
c.updateMedianTimestamp()
}()
extraMergeMiningTag := b.Coinbase.Extra.GetTag(transaction.TxExtraTagMergeMining)
if extraMergeMiningTag == nil {
return
}
sidechainHashData := extraMergeMiningTag.Data
if len(sidechainHashData) != types.HashSize {
return
}
sidechainId := types.HashFromBytes(sidechainHashData)
if block := c.sidechain.GetPoolBlockByTemplateId(sidechainId); block != nil {
c.p2pool.UpdateBlockFound(mainData, block)
} else {
c.sidechain.WatchMainChainBlock(mainData, sidechainId)
}
c.updateTip()
}
// HandleChainMain
// deprecated
func (c *MainChain) HandleChainMain(mainData *sidechain.ChainMain, extra []byte) {
func() {
c.lock.Lock()
defer c.lock.Unlock()
if h, ok := c.mainchainByHeight[mainData.Height]; ok {
h.Height = mainData.Height
h.Timestamp = mainData.Timestamp
h.Reward = mainData.Reward
mainData.Id = h.Id
mainData.Difficulty = h.Difficulty
c.mainchainByHash[h.Id] = h
} else {
return
}
if mainData.Height > c.highest {
c.highest = mainData.Height
}
log.Printf("[MainChain] new main chain block: height = %d, id = %s, timestamp = %d, reward = %s", mainData.Height, mainData.Id.String(), mainData.Timestamp, utils.XMRUnits(mainData.Reward))
c.updateMedianTimestamp()
}()
var tags transaction.ExtraTags
if err := tags.UnmarshalBinary(extra); err != nil {
return
}
extraMergeMiningTag := tags.GetTag(transaction.TxExtraTagMergeMining)
if extraMergeMiningTag == nil {
return
}
sidechainHashData := extraMergeMiningTag.Data
if len(sidechainHashData) != types.HashSize {
return
}
sidechainId := types.HashFromBytes(sidechainHashData)
if block := c.sidechain.GetPoolBlockByTemplateId(sidechainId); block != nil {
c.p2pool.UpdateBlockFound(mainData, block)
} else {
c.sidechain.WatchMainChainBlock(mainData, sidechainId)
}
c.updateTip()
}
func (c *MainChain) GetChainMainByHeight(height uint64) *sidechain.ChainMain {
c.lock.RLock()
defer c.lock.RUnlock()
return c.mainchainByHeight[height]
}
func (c *MainChain) GetChainMainByHash(hash types.Hash) *sidechain.ChainMain {
c.lock.RLock()
defer c.lock.RUnlock()
return c.mainchainByHash[hash]
}
func (c *MainChain) GetChainMainTip() *sidechain.ChainMain {
return c.tip.Load()
}
func (c *MainChain) updateTip() {
if minerData := c.tipMinerData.Load(); minerData != nil {
if d := c.GetChainMainByHash(minerData.PrevId); d != nil {
c.tip.Store(d)
}
}
}
func (c *MainChain) Cleanup() {
if tip := c.GetChainMainTip(); tip != nil {
c.lock.Lock()
defer c.lock.Unlock()
c.cleanup(tip.Height)
}
}
func (c *MainChain) cleanup(height uint64) {
// Expects m_mainchainLock to be already locked here
// Deletes everything older than 720 blocks, except for the 3 latest RandomX seed heights
const PruneDistance = BlockHeadersRequired
seedHeight := randomx.SeedHeight(height)
seedHeights := []uint64{seedHeight, seedHeight - randomx.SeedHashEpochBlocks, seedHeight - randomx.SeedHashEpochBlocks*2}
for h, m := range c.mainchainByHeight {
if (h + PruneDistance) >= height {
continue
}
if !slices.Contains(seedHeights, h) {
delete(c.mainchainByHash, m.Id)
delete(c.mainchainByHeight, h)
}
}
}
func (c *MainChain) DownloadBlockHeaders(currentHeight uint64) error {
seedHeight := randomx.SeedHeight(currentHeight)
var prevSeedHeight uint64
if seedHeight > randomx.SeedHashEpochBlocks {
prevSeedHeight = seedHeight - randomx.SeedHashEpochBlocks
}
// First download 2 RandomX seeds
for _, h := range []uint64{prevSeedHeight, seedHeight} {
if err := c.getBlockHeader(h); err != nil {
return err
}
}
var startHeight uint64
if currentHeight > BlockHeadersRequired {
startHeight = currentHeight - BlockHeadersRequired
}
if rangeResult, err := c.p2pool.ClientRPC().GetBlockHeadersRangeResult(startHeight, currentHeight-1, c.p2pool.Context()); err != nil {
return fmt.Errorf("couldn't download block headers range for height %d to %d: %s", startHeight, currentHeight-1, err)
} else {
for _, header := range rangeResult.Headers {
prevHash, _ := types.HashFromString(header.PrevHash)
h, _ := types.HashFromString(header.Hash)
c.HandleMainHeader(&mainblock.Header{
MajorVersion: uint8(header.MajorVersion),
MinorVersion: uint8(header.MinorVersion),
Timestamp: uint64(header.Timestamp),
PreviousId: prevHash,
Height: header.Height,
Nonce: uint32(header.Nonce),
Reward: header.Reward,
Id: h,
Difficulty: types.DifficultyFrom64(header.Difficulty),
})
}
log.Printf("[MainChain] Downloaded headers for range %d to %d", startHeight, currentHeight-1)
}
c.updateMedianTimestamp()
return nil
}
func (c *MainChain) HandleMinerData(minerData *MinerData) {
var missingHeights []uint64
func() {
c.lock.Lock()
defer c.lock.Unlock()
mainData := &sidechain.ChainMain{
Difficulty: minerData.Difficulty,
Height: minerData.Height,
}
if existingMainData, ok := c.mainchainByHeight[mainData.Height]; !ok {
c.mainchainByHeight[mainData.Height] = mainData
} else {
existingMainData.Difficulty = mainData.Difficulty
mainData = existingMainData
}
prevMainData := &sidechain.ChainMain{
Height: minerData.Height - 1,
Id: minerData.PrevId,
}
if existingPrevMainData, ok := c.mainchainByHeight[prevMainData.Height]; !ok {
c.mainchainByHeight[prevMainData.Height] = prevMainData
} else {
existingPrevMainData.Id = prevMainData.Id
// timestamp and reward is unknown here
existingPrevMainData.Timestamp = 0
existingPrevMainData.Reward = 0
prevMainData = existingPrevMainData
}
c.mainchainByHash[prevMainData.Id] = prevMainData
c.cleanup(minerData.Height)
minerData.TimeReceived = time.Now()
c.tipMinerData.Store(minerData)
c.updateMedianTimestamp()
log.Printf("[MainChain] new miner data: major_version = %d, height = %d, prev_id = %s, seed_hash = %s, difficulty = %s", minerData.MajorVersion, minerData.Height, minerData.PrevId.String(), minerData.SeedHash.String(), minerData.Difficulty.StringNumeric())
if c.p2pool.Started() {
for h := minerData.Height; h > 0 && (h+BlockHeadersRequired) > minerData.Height; h-- {
if d, ok := c.mainchainByHeight[h]; !ok || d.Difficulty.Equals(types.ZeroDifficulty) {
log.Printf("[MainChain] Main chain data for height = %d is missing, requesting from monerod again", h)
missingHeights = append(missingHeights, h)
}
}
}
}()
var wg sync.WaitGroup
for _, h := range missingHeights {
wg.Add(1)
go func(height uint64) {
wg.Done()
if err := c.getBlockHeader(height); err != nil {
log.Printf("[MainChain] %s", err)
}
}(h)
}
wg.Wait()
c.updateTip()
}
func (c *MainChain) getBlockHeader(height uint64) error {
if header, err := c.p2pool.ClientRPC().GetBlockHeaderByHeight(height, c.p2pool.Context()); err != nil {
return fmt.Errorf("couldn't download block header for height %d: %s", height, err)
} else {
prevHash, _ := types.HashFromString(header.BlockHeader.PrevHash)
h, _ := types.HashFromString(header.BlockHeader.Hash)
c.HandleMainHeader(&mainblock.Header{
MajorVersion: uint8(header.BlockHeader.MajorVersion),
MinorVersion: uint8(header.BlockHeader.MinorVersion),
Timestamp: uint64(header.BlockHeader.Timestamp),
PreviousId: prevHash,
Height: header.BlockHeader.Height,
Nonce: uint32(header.BlockHeader.Nonce),
Reward: header.BlockHeader.Reward,
Id: h,
Difficulty: types.DifficultyFrom64(header.BlockHeader.Difficulty),
})
}
return nil
}
type MinerData struct {
MajorVersion uint8
Height uint64
PrevId types.Hash
SeedHash types.Hash
Difficulty types.Difficulty
MedianWeight uint64
AlreadyGeneratedCoins uint64
MedianTimestamp uint64
//TxBacklog any
TimeReceived time.Time
}

View file

@ -204,6 +204,7 @@ func (c *Client) SendPeerListResponse(list []netip.AddrPort) {
buf := make([]byte, 0, 1+len(list)*(1+16+2))
buf = append(buf, byte(len(list)))
for i := range list {
//TODO: check ipv4 gets sent properly
if list[i].Addr().Is6() && !IsPeerVersionInformation(list[i]) {
buf = append(buf, 1)
} else {

View file

@ -1,6 +1,7 @@
package p2p
import (
"context"
"crypto/rand"
"encoding/binary"
"errors"
@ -42,9 +43,11 @@ type Server struct {
clientsLock sync.RWMutex
clients []*Client
ctx context.Context
}
func NewServer(sidechain *sidechain.SideChain, listenAddress string, externalListenPort uint16, maxOutgoingPeers, maxIncomingPeers uint32) (*Server, error) {
func NewServer(sidechain *sidechain.SideChain, listenAddress string, externalListenPort uint16, maxOutgoingPeers, maxIncomingPeers uint32, ctx context.Context) (*Server, error) {
peerId := make([]byte, int(unsafe.Sizeof(uint64(0))))
_, err := rand.Read(peerId)
if err != nil {
@ -64,6 +67,7 @@ func NewServer(sidechain *sidechain.SideChain, listenAddress string, externalLis
MaxOutgoingPeers: utils.Min(utils.Max(maxOutgoingPeers, 10), 450),
MaxIncomingPeers: utils.Min(utils.Max(maxIncomingPeers, 10), 450),
versionInformation: PeerVersionInformation{SoftwareId: SoftwareIdGoObserver, SoftwareVersion: CurrentSoftwareVersion, Protocol: SupportedProtocolVersion},
ctx: ctx,
}
s.PendingOutgoingConnections = utils.NewCircularBuffer[string](int(s.MaxOutgoingPeers))
@ -161,7 +165,7 @@ func (s *Server) DownloadMissingBlocks() {
func (s *Server) Listen() (err error) {
var listener net.Listener
var ok bool
if listener, err = net.Listen("tcp", s.listenAddress.String()); err != nil {
if listener, err = (&net.ListenConfig{}).Listen(s.ctx, "tcp", s.listenAddress.String()); err != nil {
return err
} else if s.listener, ok = listener.(*net.TCPListener); !ok {
return errors.New("not a tcp listener")
@ -258,7 +262,7 @@ func (s *Server) Connect(addrPort netip.AddrPort) error {
s.NumOutgoingConnections.Add(1)
if conn, err := net.DialTimeout("tcp", addrPort.String(), time.Second*5); err != nil {
if conn, err := (&net.Dialer{Timeout: time.Second * 5}).DialContext(s.ctx, "tcp", addrPort.String()); err != nil {
s.NumOutgoingConnections.Add(-1)
return err
} else if tcpConn, ok := conn.(*net.TCPConn); !ok {

View file

@ -1,17 +1,38 @@
package p2pool
import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client/zmq"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/mainchain"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/p2p"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"log"
"strconv"
"sync/atomic"
"time"
)
type P2Pool struct {
consensus *sidechain.Consensus
sidechain *sidechain.SideChain
mainchain *mainchain.MainChain
server *p2p.Server
ctx context.Context
ctxCancel context.CancelFunc
rpcClient *client.Client
zmqClient *zmq.Client
started atomic.Bool
}
func (p *P2Pool) GetBlob(key []byte) (blob []byte, err error) {
@ -26,16 +47,35 @@ func (p *P2Pool) RemoveBlob(key []byte) (err error) {
return nil
}
func NewP2Pool(consensus *sidechain.Consensus, settings map[string]string) *P2Pool {
func (p *P2Pool) Close() {
p.ctxCancel()
_ = p.zmqClient.Close()
}
func NewP2Pool(consensus *sidechain.Consensus, settings map[string]string) (*P2Pool, error) {
pool := &P2Pool{
consensus: consensus,
}
var err error
pool.ctx, pool.ctxCancel = context.WithCancel(context.Background())
listenAddress := fmt.Sprintf("0.0.0.0:%d", pool.Consensus().DefaultPort())
if pool.rpcClient, err = client.NewClient(settings["rpc-url"]); err != nil {
return nil, err
}
pool.zmqClient = zmq.NewClient(settings["zmq-url"], zmq.TopicFullChainMain, zmq.TopicFullMinerData)
pool.sidechain = sidechain.NewSideChain(pool)
pool.mainchain = mainchain.NewMainChain(pool.sidechain, pool)
if pool.mainchain == nil {
return nil, errors.New("could not create MainChain")
}
if addr, ok := settings["listen"]; ok {
listenAddress = addr
}
@ -55,21 +95,373 @@ func NewP2Pool(consensus *sidechain.Consensus, settings map[string]string) *P2Po
externalListenPort, _ = strconv.ParseUint(externalPort, 10, 0)
}
if pool.server, err = p2p.NewServer(pool.sidechain, listenAddress, uint16(externalListenPort), uint32(maxOutgoingPeers), uint32(maxIncomingPeers)); err != nil {
return nil
if pool.server, err = p2p.NewServer(pool.sidechain, listenAddress, uint16(externalListenPort), uint32(maxOutgoingPeers), uint32(maxIncomingPeers), pool.ctx); err != nil {
return nil, err
}
return pool
return pool, nil
}
func (p *P2Pool) GetChainMainByHash(hash types.Hash) *sidechain.ChainMain {
return p.mainchain.GetChainMainByHash(hash)
}
func (p *P2Pool) GetChainMainByHeight(height uint64) *sidechain.ChainMain {
return p.mainchain.GetChainMainByHeight(height)
}
func (p *P2Pool) GetChainMainTip() *sidechain.ChainMain {
return p.mainchain.GetChainMainTip()
}
// GetMinimalBlockHeaderByHeight Only Id / Height / Timestamp are assured
func (p *P2Pool) GetMinimalBlockHeaderByHeight(height uint64) *block.Header {
if chainMain := p.mainchain.GetChainMainByHeight(height); chainMain != nil && chainMain.Id != types.ZeroHash {
return &block.Header{
Timestamp: chainMain.Timestamp,
Height: chainMain.Height,
Reward: chainMain.Reward,
Difficulty: chainMain.Difficulty,
Id: chainMain.Id,
}
} else {
if header, err := p.ClientRPC().GetBlockHeaderByHeight(height, p.ctx); err != nil {
return nil
} else {
prevHash, _ := types.HashFromString(header.BlockHeader.PrevHash)
h, _ := types.HashFromString(header.BlockHeader.Hash)
blockHeader := &block.Header{
MajorVersion: uint8(header.BlockHeader.MajorVersion),
MinorVersion: uint8(header.BlockHeader.MinorVersion),
Timestamp: uint64(header.BlockHeader.Timestamp),
PreviousId: prevHash,
Height: header.BlockHeader.Height,
Nonce: uint32(header.BlockHeader.Nonce),
Reward: header.BlockHeader.Reward,
Id: h,
Difficulty: types.DifficultyFrom64(header.BlockHeader.Difficulty),
}
//cache it. next block found will clean it up
p.mainchain.HandleMainHeader(blockHeader)
return blockHeader
}
}
}
func (p *P2Pool) GetDifficultyByHeight(height uint64) types.Difficulty {
if chainMain := p.mainchain.GetChainMainByHeight(height); chainMain != nil && chainMain.Difficulty != types.ZeroDifficulty {
return chainMain.Difficulty
} else {
//TODO cache
if header, err := p.ClientRPC().GetBlockHeaderByHeight(height, p.ctx); err != nil {
return types.ZeroDifficulty
} else {
prevHash, _ := types.HashFromString(header.BlockHeader.PrevHash)
h, _ := types.HashFromString(header.BlockHeader.Hash)
blockHeader := &block.Header{
MajorVersion: uint8(header.BlockHeader.MajorVersion),
MinorVersion: uint8(header.BlockHeader.MinorVersion),
Timestamp: uint64(header.BlockHeader.Timestamp),
PreviousId: prevHash,
Height: header.BlockHeader.Height,
Nonce: uint32(header.BlockHeader.Nonce),
Reward: header.BlockHeader.Reward,
Id: h,
Difficulty: types.DifficultyFrom64(header.BlockHeader.Difficulty),
}
//cache it. next block found will clean it up
p.mainchain.HandleMainHeader(blockHeader)
return blockHeader.Difficulty
}
}
}
// GetMinimalBlockHeaderByHash Only Id / Height / Timestamp are assured
func (p *P2Pool) GetMinimalBlockHeaderByHash(hash types.Hash) *block.Header {
if chainMain := p.mainchain.GetChainMainByHash(hash); chainMain != nil && chainMain.Id != types.ZeroHash {
return &block.Header{
Timestamp: chainMain.Timestamp,
Height: chainMain.Height,
Reward: chainMain.Reward,
Difficulty: chainMain.Difficulty,
Id: chainMain.Id,
}
} else {
if header, err := p.ClientRPC().GetBlockHeaderByHash(hash, p.ctx); err != nil {
return nil
} else {
prevHash, _ := types.HashFromString(header.BlockHeader.PrevHash)
h, _ := types.HashFromString(header.BlockHeader.Hash)
blockHeader := &block.Header{
MajorVersion: uint8(header.BlockHeader.MajorVersion),
MinorVersion: uint8(header.BlockHeader.MinorVersion),
Timestamp: uint64(header.BlockHeader.Timestamp),
PreviousId: prevHash,
Height: header.BlockHeader.Height,
Nonce: uint32(header.BlockHeader.Nonce),
Reward: header.BlockHeader.Reward,
Id: h,
Difficulty: types.DifficultyFrom64(header.BlockHeader.Difficulty),
}
//cache it. next block found will clean it up
p.mainchain.HandleMainHeader(blockHeader)
return blockHeader
}
}
}
func (p *P2Pool) ClientRPC() *client.Client {
return p.rpcClient
}
func (p *P2Pool) ClientZMQ() *zmq.Client {
return p.zmqClient
}
func (p *P2Pool) Context() context.Context {
return p.ctx
}
func (p *P2Pool) Server() *p2p.Server {
return p.server
}
func (p *P2Pool) Run() (err error) {
if err = p.getInfo(); err != nil {
return err
}
if err = p.getVersion(); err != nil {
return err
}
if err = p.getInfo(); err != nil {
return err
}
if err = p.getMinerData(); err != nil {
return err
}
if zmqStream, err := p.ClientZMQ().Listen(p.ctx); err != nil {
return err
} else {
go func() {
for {
select {
case <-zmqStream.ErrC:
p.Close()
return
case fullChainMain := <-zmqStream.FullChainMainC:
if len(fullChainMain.MinerTx.Inputs) < 1 {
continue
}
d := &sidechain.ChainMain{
Difficulty: types.ZeroDifficulty,
Height: fullChainMain.MinerTx.Inputs[0].Gen.Height,
Timestamp: uint64(fullChainMain.Timestamp),
Reward: 0,
Id: types.ZeroHash,
}
for _, o := range fullChainMain.MinerTx.Outputs {
d.Reward += o.Amount
}
outputs := make(transaction.Outputs, 0, len(fullChainMain.MinerTx.Outputs))
var totalReward uint64
for i, o := range fullChainMain.MinerTx.Outputs {
if o.ToKey != nil {
outputs = append(outputs, transaction.Output{
Index: uint64(i),
Reward: o.Amount,
Type: transaction.TxOutToKey,
EphemeralPublicKey: o.ToKey.Key,
ViewTag: 0,
})
} else if o.ToTaggedKey != nil {
tk, _ := hex.DecodeString(o.ToTaggedKey.ViewTag)
outputs = append(outputs, transaction.Output{
Index: uint64(i),
Reward: o.Amount,
Type: transaction.TxOutToTaggedKey,
EphemeralPublicKey: o.ToTaggedKey.Key,
ViewTag: tk[0],
})
} else {
//error
break
}
totalReward += o.Amount
}
if len(outputs) != len(fullChainMain.MinerTx.Outputs) {
continue
}
extraDataRaw, _ := hex.DecodeString(fullChainMain.MinerTx.Extra)
extraTags := transaction.ExtraTags{}
if err = extraTags.UnmarshalBinary(extraDataRaw); err != nil {
continue
}
blockData := &block.Block{
MajorVersion: uint8(fullChainMain.MajorVersion),
MinorVersion: uint8(fullChainMain.MinorVersion),
Timestamp: uint64(fullChainMain.Timestamp),
PreviousId: fullChainMain.PrevID,
Nonce: uint32(fullChainMain.Nonce),
Coinbase: &transaction.CoinbaseTransaction{
Version: uint8(fullChainMain.MinerTx.Version),
UnlockTime: uint64(fullChainMain.MinerTx.UnlockTime),
InputCount: uint8(len(fullChainMain.MinerTx.Inputs)),
InputType: transaction.TxInGen,
GenHeight: fullChainMain.MinerTx.Inputs[0].Gen.Height,
Outputs: outputs,
OutputsBlobSize: 0,
TotalReward: totalReward,
Extra: extraTags,
ExtraBaseRCT: 0,
},
Transactions: fullChainMain.TxHashes,
TransactionParentIndices: nil,
}
p.mainchain.HandleMainBlock(blockData)
case fullMinerData := <-zmqStream.FullMinerDataC:
p.mainchain.HandleMinerData(&mainchain.MinerData{
MajorVersion: fullMinerData.MajorVersion,
Height: fullMinerData.Height,
PrevId: fullMinerData.PrevId,
SeedHash: fullMinerData.SeedHash,
Difficulty: fullMinerData.Difficulty,
MedianWeight: fullMinerData.MedianWeight,
AlreadyGeneratedCoins: fullMinerData.AlreadyGeneratedCoins,
MedianTimestamp: fullMinerData.MedianTimestamp,
TimeReceived: time.Now(),
})
}
}
}()
}
p.started.Store(true)
if err = p.Server().Listen(); err != nil {
return err
}
return nil
}
func (p *P2Pool) getInfo() error {
if info, err := p.ClientRPC().GetInfo(); err != nil {
return err
} else {
if info.BusySyncing {
log.Printf("[P2Pool] monerod is busy syncing, trying again in 5 seconds")
time.Sleep(time.Second * 5)
return p.getInfo()
} else if !info.Synchronized {
log.Printf("[P2Pool] monerod is not synchronized, trying again in 5 seconds")
time.Sleep(time.Second * 5)
return p.getInfo()
}
networkType := sidechain.NetworkInvalid
if info.Mainnet {
networkType = sidechain.NetworkMainnet
} else if info.Testnet {
networkType = sidechain.NetworkTestnet
} else if info.Stagenet {
networkType = sidechain.NetworkStagenet
}
if p.sidechain.Consensus().NetworkType != networkType {
return fmt.Errorf("monerod is on %d, but you're mining to a %d sidechain", networkType, p.sidechain.Consensus().NetworkType)
}
}
return nil
}
const RequiredMajor = 3
const RequiredMinor = 10
const RequiredMoneroVersion = (RequiredMajor << 16) | RequiredMinor
func (p *P2Pool) getVersion() error {
if version, err := p.ClientRPC().GetVersion(); err != nil {
return err
} else {
if version.Version < RequiredMoneroVersion {
return fmt.Errorf("monerod RPC v%d.%d is incompatible, update to RPC >= v%d.%d (Monero v0.18.0.0 or newer)", version.Version>>16, version.Version&0xffff, RequiredMajor, RequiredMinor)
}
}
return nil
}
func (p *P2Pool) getMinerData() error {
if minerData, err := p.ClientRPC().GetMinerData(); err != nil {
return err
} else {
prevId, _ := types.HashFromString(minerData.PrevId)
seedHash, _ := types.HashFromString(minerData.SeedHash)
diff, _ := types.DifficultyFromString(minerData.Difficulty)
p.mainchain.HandleMinerData(&mainchain.MinerData{
MajorVersion: minerData.MajorVersion,
Height: minerData.Height,
PrevId: prevId,
SeedHash: seedHash,
Difficulty: diff,
MedianWeight: minerData.MedianWeight,
AlreadyGeneratedCoins: minerData.AlreadyGeneratedCoins,
MedianTimestamp: minerData.MedianTimestamp,
TimeReceived: time.Now(),
})
return p.mainchain.DownloadBlockHeaders(minerData.Height)
}
}
func (p *P2Pool) Consensus() *sidechain.Consensus {
return p.consensus
}
func (p *P2Pool) UpdateBlockFound(data *sidechain.ChainMain, block *sidechain.PoolBlock) {
log.Printf("[P2Pool] BLOCK FOUND: main chain block at height %d, id %s was mined by this p2pool", data.Height, data.Id)
//TODO
}
func (p *P2Pool) SubmitBlock(b *block.Block) {
//TODO: do not submit multiple times
go func() {
if blob, err := b.MarshalBinary(); err == nil {
var templateId types.Hash
var extraNonce uint32
if t := b.Coinbase.Extra.GetTag(transaction.TxExtraTagMergeMining); t != nil {
templateId = types.HashFromBytes(t.Data)
}
if t := b.Coinbase.Extra.GetTag(transaction.TxExtraTagNonce); t != nil {
extraNonce = binary.LittleEndian.Uint32(t.Data)
}
log.Printf("[P2Pool] submit_block: height = %d, template id = %s, nonce = %d, extra_nonce = %d, blob = %d bytes", b.Coinbase.GenHeight, templateId.String(), b.Nonce, extraNonce, len(blob))
if result, err := p.ClientRPC().SubmitBlock(blob); err != nil {
log.Printf("[P2Pool] submit_block: daemon returned error: %s, height = %d, template id = %s, nonce = %d, extra_nonce = %d", err, b.Coinbase.GenHeight, templateId.String(), b.Nonce, extraNonce)
} else {
if result.Status == "OK" {
log.Printf("[P2Pool] submit_block: BLOCK ACCEPTED at height = %d, template id = %s", b.Coinbase.GenHeight, templateId.String())
} else {
log.Printf("[P2Pool] submit_block: daemon sent unrecognizable reply: %s, height = %d, template id = %s, nonce = %d, extra_nonce = %d", result.Status, b.Coinbase.GenHeight, templateId.String(), b.Nonce, extraNonce)
}
}
}
}()
}
func (p *P2Pool) Started() bool {
return p.started.Load()
}
func (p *P2Pool) Broadcast(block *sidechain.PoolBlock) {
p.server.Broadcast(block)
}

View file

@ -2,6 +2,7 @@ package sidechain
import (
"encoding/json"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
@ -98,99 +99,120 @@ func NewConsensus(networkType NetworkType, poolName, poolPassword string, target
UnclePenalty: unclePenalty,
}
if len(c.PoolName) > 128 {
return nil
}
if len(c.PoolPassword) > 128 {
return nil
}
if c.TargetBlockTime < 1 || c.TargetBlockTime > monero.BlockTime {
return nil
}
if c.MinimumDifficulty < SmallestMinimumDifficulty || c.MinimumDifficulty > LargestMinimumDifficulty {
return nil
}
if c.ChainWindowSize < 60 || c.ChainWindowSize > 2160 {
return nil
}
if c.UnclePenalty < 1 || c.UnclePenalty > 99 {
return nil
}
var emptyHash types.Hash
c.id = c.CalculateId()
if c.id == emptyHash {
if !c.verify() {
return nil
}
return c
}
func (i *Consensus) CalculateSideTemplateId(main *mainblock.Block, side *SideData) types.Hash {
func NewConsensusFromJSON(data []byte) (*Consensus, error) {
var c Consensus
if err := json.Unmarshal(data, &c); err != nil {
return nil, err
}
if !c.verify() {
return nil, errors.New("could not verify")
}
return &c, nil
}
func (c *Consensus) verify() bool {
if len(c.PoolName) > 128 {
return false
}
if len(c.PoolPassword) > 128 {
return false
}
if c.TargetBlockTime < 1 || c.TargetBlockTime > monero.BlockTime {
return false
}
if c.MinimumDifficulty < SmallestMinimumDifficulty || c.MinimumDifficulty > LargestMinimumDifficulty {
return false
}
if c.ChainWindowSize < 60 || c.ChainWindowSize > 2160 {
return false
}
if c.UnclePenalty < 1 || c.UnclePenalty > 99 {
return false
}
var emptyHash types.Hash
c.id = c.CalculateId()
if c.id == emptyHash {
return false
}
return true
}
func (c *Consensus) CalculateSideTemplateId(main *mainblock.Block, side *SideData) types.Hash {
mainData, _ := main.SideChainHashingBlob()
sideData, _ := side.MarshalBinary()
return i.CalculateSideChainIdFromBlobs(mainData, sideData)
return c.CalculateSideChainIdFromBlobs(mainData, sideData)
}
func (i *Consensus) CalculateSideChainIdFromBlobs(mainBlob, sideBlob []byte) types.Hash {
return crypto.PooledKeccak256(mainBlob, sideBlob, i.id[:])
func (c *Consensus) CalculateSideChainIdFromBlobs(mainBlob, sideBlob []byte) types.Hash {
return crypto.PooledKeccak256(mainBlob, sideBlob, c.id[:])
}
func (i *Consensus) Id() types.Hash {
func (c *Consensus) Id() types.Hash {
var h types.Hash
if i.id == h {
if c.id == h {
//this data race is fine
i.id = i.CalculateId()
return i.id
c.id = c.CalculateId()
return c.id
}
return i.id
return c.id
}
func (i *Consensus) IsDefault() bool {
return i.id == ConsensusDefault.id
func (c *Consensus) IsDefault() bool {
return c.id == ConsensusDefault.id
}
func (i *Consensus) IsMini() bool {
return i.id == ConsensusMini.id
func (c *Consensus) IsMini() bool {
return c.id == ConsensusMini.id
}
func (i *Consensus) DefaultPort() uint16 {
if i.IsMini() {
func (c *Consensus) DefaultPort() uint16 {
if c.IsMini() {
return 37888
}
return 37889
}
func (i *Consensus) SeedNode() string {
if i.IsMini() {
func (c *Consensus) SeedNode() string {
if c.IsMini() {
return "seeds-mini.p2pool.io"
} else if i.IsDefault() {
} else if c.IsDefault() {
return "seeds.p2pool.io"
}
return ""
}
func (i *Consensus) CalculateId() types.Hash {
func (c *Consensus) CalculateId() types.Hash {
var buf []byte
buf = append(buf, i.NetworkType.String()...)
buf = append(buf, c.NetworkType.String()...)
buf = append(buf, 0)
buf = append(buf, i.PoolName...)
buf = append(buf, c.PoolName...)
buf = append(buf, 0)
buf = append(buf, i.PoolPassword...)
buf = append(buf, c.PoolPassword...)
buf = append(buf, 0)
buf = append(buf, strconv.FormatUint(i.TargetBlockTime, 10)...)
buf = append(buf, strconv.FormatUint(c.TargetBlockTime, 10)...)
buf = append(buf, 0)
buf = append(buf, strconv.FormatUint(i.MinimumDifficulty, 10)...)
buf = append(buf, strconv.FormatUint(c.MinimumDifficulty, 10)...)
buf = append(buf, 0)
buf = append(buf, strconv.FormatUint(i.ChainWindowSize, 10)...)
buf = append(buf, strconv.FormatUint(c.ChainWindowSize, 10)...)
buf = append(buf, 0)
buf = append(buf, strconv.FormatUint(i.UnclePenalty, 10)...)
buf = append(buf, strconv.FormatUint(c.UnclePenalty, 10)...)
buf = append(buf, 0)
return randomx.ConsensusHash(buf)

View file

@ -235,7 +235,7 @@ func (b *PoolBlock) CalculateFullId(consensus *Consensus) FullId {
return buf
}
func (b *PoolBlock) MainDifficulty() types.Difficulty {
func (b *PoolBlock) MainDifficulty(f mainblock.GetDifficultyByHeightFunc) types.Difficulty {
if difficulty, ok := func() (types.Difficulty, bool) {
b.cache.lock.RLock()
defer b.cache.lock.RUnlock()
@ -250,7 +250,7 @@ func (b *PoolBlock) MainDifficulty() types.Difficulty {
b.cache.lock.Lock()
defer b.cache.lock.Unlock()
if b.cache.mainDifficulty == types.ZeroDifficulty { //check again for race
b.cache.mainDifficulty = b.Main.Difficulty()
b.cache.mainDifficulty = b.Main.Difficulty(f)
}
return b.cache.mainDifficulty
}
@ -277,12 +277,12 @@ func (b *PoolBlock) SideTemplateId(consensus *Consensus) types.Hash {
}
}
func (b *PoolBlock) PowHash() types.Hash {
h, _ := b.PowHashWithError()
func (b *PoolBlock) PowHash(f mainblock.GetSeedByHeightFunc) types.Hash {
h, _ := b.PowHashWithError(f)
return h
}
func (b *PoolBlock) PowHashWithError() (powHash types.Hash, err error) {
func (b *PoolBlock) PowHashWithError(f mainblock.GetSeedByHeightFunc) (powHash types.Hash, err error) {
if hash, ok := func() (types.Hash, bool) {
b.cache.lock.RLock()
defer b.cache.lock.RUnlock()
@ -297,7 +297,7 @@ func (b *PoolBlock) PowHashWithError() (powHash types.Hash, err error) {
b.cache.lock.Lock()
defer b.cache.lock.Unlock()
if b.cache.powHash == types.ZeroHash { //check again for race
b.cache.powHash, err = b.Main.PowHashWithError()
b.cache.powHash, err = b.Main.PowHashWithError(f)
}
return b.cache.powHash, err
}
@ -359,28 +359,28 @@ func (b *PoolBlock) FromCompactReader(reader readerAndByteReader) (err error) {
return nil
}
func (b *PoolBlock) IsProofHigherThanMainDifficulty() bool {
r, _ := b.IsProofHigherThanMainDifficultyWithError()
func (b *PoolBlock) IsProofHigherThanMainDifficulty(difficultyFunc mainblock.GetDifficultyByHeightFunc, seedFunc mainblock.GetSeedByHeightFunc) bool {
r, _ := b.IsProofHigherThanMainDifficultyWithError(difficultyFunc, seedFunc)
return r
}
func (b *PoolBlock) IsProofHigherThanMainDifficultyWithError() (bool, error) {
if mainDifficulty := b.MainDifficulty(); mainDifficulty == types.ZeroDifficulty {
func (b *PoolBlock) IsProofHigherThanMainDifficultyWithError(difficultyFunc mainblock.GetDifficultyByHeightFunc, seedFunc mainblock.GetSeedByHeightFunc) (bool, error) {
if mainDifficulty := b.MainDifficulty(difficultyFunc); mainDifficulty == types.ZeroDifficulty {
return false, errors.New("could not get main difficulty")
} else if powHash, err := b.PowHashWithError(); err != nil {
} else if powHash, err := b.PowHashWithError(seedFunc); err != nil {
return false, err
} else {
return mainDifficulty.CheckPoW(powHash), nil
}
}
func (b *PoolBlock) IsProofHigherThanDifficulty() bool {
r, _ := b.IsProofHigherThanDifficultyWithError()
func (b *PoolBlock) IsProofHigherThanDifficulty(f mainblock.GetSeedByHeightFunc) bool {
r, _ := b.IsProofHigherThanDifficultyWithError(f)
return r
}
func (b *PoolBlock) IsProofHigherThanDifficultyWithError() (bool, error) {
if powHash, err := b.PowHashWithError(); err != nil {
func (b *PoolBlock) IsProofHigherThanDifficultyWithError(f mainblock.GetSeedByHeightFunc) (bool, error) {
if powHash, err := b.PowHashWithError(f); err != nil {
return false, err
} else {
return b.Side.Difficulty.CheckPoW(powHash), nil

View file

@ -1,10 +1,12 @@
package sidechain
import (
"context"
"encoding/hex"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
block2 "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"io"
"log"
@ -72,24 +74,42 @@ func TestPoolBlockDecode(t *testing.T) {
proofResult, _ := types.DifficultyFromString("00000000000000000000006ef6334490")
if types.DifficultyFromPoW(block.PowHash()).Cmp(proofResult) != 0 {
t.Fatalf("expected PoW difficulty %s, got %s", proofResult.String(), types.DifficultyFromPoW(block.PowHash()).String())
getSeedByHeight := func(height uint64) (hash types.Hash) {
seedHeight := randomx.SeedHeight(height)
if h, err := client.GetDefaultClient().GetBlockHeaderByHeight(seedHeight, context.Background()); err != nil {
return types.ZeroHash
} else {
hash, _ := types.HashFromString(h.BlockHeader.Hash)
return hash
}
}
getDifficultyByHeight := func(height uint64) types.Difficulty {
if h, err := client.GetDefaultClient().GetBlockHeaderByHeight(height, context.Background()); err != nil {
return types.ZeroDifficulty
} else {
return types.DifficultyFrom64(h.BlockHeader.Difficulty)
}
}
if types.DifficultyFromPoW(block.PowHash(getSeedByHeight)).Cmp(proofResult) != 0 {
t.Fatalf("expected PoW difficulty %s, got %s", proofResult.String(), types.DifficultyFromPoW(block.PowHash(getSeedByHeight)).String())
}
t.Log(block.Main.Id().String())
//t.Log(block.Main.PowHash().String())
//t.Log(block.Main.PowHash().String())
if !block.IsProofHigherThanMainDifficulty() {
if !block.IsProofHigherThanMainDifficulty(getDifficultyByHeight, getSeedByHeight) {
t.Fatal("expected proof higher than difficulty")
}
block.cache.powHash[31] = 1
if block.IsProofHigherThanMainDifficulty() {
if block.IsProofHigherThanMainDifficulty(getDifficultyByHeight, getSeedByHeight) {
t.Fatal("expected proof lower than difficulty")
}
log.Print(types.DifficultyFromPoW(block.PowHash()).String())
log.Print(block.MainDifficulty().String())
log.Print(types.DifficultyFromPoW(block.PowHash(getSeedByHeight)).String())
log.Print(block.MainDifficulty(getDifficultyByHeight).String())
}

View file

@ -7,6 +7,8 @@ import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
@ -28,6 +30,23 @@ type P2PoolInterface interface {
ConsensusProvider
Cache
Broadcast(block *PoolBlock)
ClientRPC() *client.Client
GetChainMainByHeight(height uint64) *ChainMain
GetChainMainByHash(hash types.Hash) *ChainMain
GetMinimalBlockHeaderByHeight(height uint64) *mainblock.Header
GetMinimalBlockHeaderByHash(hash types.Hash) *mainblock.Header
GetDifficultyByHeight(height uint64) types.Difficulty
UpdateBlockFound(data *ChainMain, block *PoolBlock)
SubmitBlock(block *mainblock.Block)
GetChainMainTip() *ChainMain
}
type ChainMain struct {
Difficulty types.Difficulty
Height uint64
Timestamp uint64
Reward uint64
Id types.Hash
}
type SideChain struct {
@ -36,6 +55,9 @@ type SideChain struct {
sidechainLock sync.RWMutex
watchBlock *ChainMain
watchBlockSidechainId types.Hash
sharesCache Shares
blocksByTemplateId map[types.Hash]*PoolBlock
@ -133,6 +155,17 @@ func (c *SideChain) isPoolBlockTransactionKeyIsDeterministic(block *PoolBlock) b
return bytes.Compare(block.CoinbaseExtra(SideCoinbasePublicKey), kP.PublicKey.AsSlice()) == 0 && bytes.Compare(block.Side.CoinbasePrivateKey[:], kP.PrivateKey.AsSlice()) == 0
}
func (c *SideChain) getSeedByHeightFunc() mainblock.GetSeedByHeightFunc {
return func(height uint64) (hash types.Hash) {
seedHeight := randomx.SeedHeight(height)
if h := c.server.GetMinimalBlockHeaderByHeight(seedHeight); h != nil {
return h.Id
} else {
return types.ZeroHash
}
}
}
func (c *SideChain) AddPoolBlockExternal(block *PoolBlock) (missingBlocks []types.Hash, err error) {
// Technically some p2pool node could keep stuffing block with transactions until reward is less than 0.6 XMR
// But default transaction picking algorithm never does that. It's better to just ban such nodes
@ -201,7 +234,7 @@ func (c *SideChain) AddPoolBlockExternal(block *PoolBlock) (missingBlocks []type
// This check is not always possible to perform because of mainchain reorgs
//TODO: cache current miner data?
if data := mainblock.GetBlockHeaderByHash(block.Main.PreviousId); data != nil {
if data := c.server.GetChainMainByHash(block.Main.PreviousId); data != nil {
if data.Height+1 != block.Main.Coinbase.GenHeight {
return nil, fmt.Errorf("wrong mainchain height %d, expected %d", block.Main.Coinbase.GenHeight, data.Height+1)
}
@ -209,11 +242,16 @@ func (c *SideChain) AddPoolBlockExternal(block *PoolBlock) (missingBlocks []type
//TODO warn unknown block, reorg
}
if _, err := block.PowHashWithError(); err != nil {
if _, err := block.PowHashWithError(c.getSeedByHeightFunc()); err != nil {
return nil, err
} else {
//TODO: fast monero submission
if isHigher, err := block.IsProofHigherThanDifficultyWithError(); err != nil {
if isHigherMainChain, err := block.IsProofHigherThanMainDifficultyWithError(c.server.GetDifficultyByHeight, c.getSeedByHeightFunc()); err != nil {
log.Printf("[SideChain] add_external_block: couldn't get mainchain difficulty for height = %d: %s", block.Main.Coinbase.GenHeight, err)
} else if isHigherMainChain {
log.Printf("[SideChain]: add_external_block: block %s has enough PoW for Monero height %d, submitting it", templateId.String(), block.Main.Coinbase.GenHeight)
c.server.SubmitBlock(&block.Main)
}
if isHigher, err := block.IsProofHigherThanDifficultyWithError(c.getSeedByHeightFunc()); err != nil {
return nil, err
} else if !isHigher {
return nil, fmt.Errorf("not enough PoW for height = %d, mainchain height %d", block.Side.Height, block.Main.Coinbase.GenHeight)
@ -252,6 +290,11 @@ func (c *SideChain) AddPoolBlock(block *PoolBlock) (err error) {
log.Printf("[SideChain] add_block: height = %d, id = %s, mainchain height = %d, verified = %t, total = %d", block.Side.Height, block.SideTemplateId(c.Consensus()), block.Main.Coinbase.GenHeight, block.Verified.Load(), len(c.blocksByTemplateId))
if block.SideTemplateId(c.Consensus()) == c.watchBlockSidechainId {
c.server.UpdateBlockFound(c.watchBlock, block)
c.watchBlockSidechainId = types.ZeroHash
}
if l, ok := c.blocksByHeight[block.Side.Height]; ok {
c.blocksByHeight[block.Side.Height] = append(l, block)
} else {
@ -1016,6 +1059,14 @@ func (c *SideChain) GetPoolBlockCount() int {
return len(c.blocksByTemplateId)
}
func (c *SideChain) WatchMainChainBlock(mainData *ChainMain, possibleId types.Hash) {
c.sidechainLock.Lock()
defer c.sidechainLock.Unlock()
c.watchBlock = mainData
c.watchBlockSidechainId = possibleId
}
func (c *SideChain) GetChainTip() *PoolBlock {
return c.chainTip.Load()
}
@ -1085,7 +1136,7 @@ func (c *SideChain) isLongerChain(block, candidate *PoolBlock) (isLonger, isAlte
candidateTotalDiff = candidateTotalDiff.Add(newChain.Side.Difficulty)
if !newChain.Main.PreviousId.Equals(mainchainPrevId) {
if data := mainblock.GetBlockHeaderByHash(newChain.Main.PreviousId); data != nil {
if data := c.server.GetMinimalBlockHeaderByHash(newChain.Main.PreviousId); data != nil {
mainchainPrevId = data.Id
candidateMainchainHeight = utils.Max(candidateMainchainHeight, data.Height)
}
@ -1100,14 +1151,14 @@ func (c *SideChain) isLongerChain(block, candidate *PoolBlock) (isLonger, isAlte
}
// Final check: candidate chain must be built on top of recent mainchain blocks
if data := mainblock.GetLastBlockHeader(); data != nil {
if candidateMainchainHeight+10 < data.Height {
if headerTip := c.server.GetChainMainTip(); headerTip != nil {
if candidateMainchainHeight+10 < headerTip.Height {
//TODO: warn received a longer alternative chain but it's stale: height
return false, true
}
limit := c.Consensus().ChainWindowSize * 4 * c.Consensus().TargetBlockTime / monero.BlockTime
if candidateMainchainMinHeight+limit < data.Height {
if candidateMainchainMinHeight+limit < headerTip.Height {
//TODO: warn received a longer alternative chain but it's stale: min height
return false, true
}

View file

@ -13,3 +13,8 @@ func SiUnits(number float64, decimals int) string {
return fmt.Sprintf("%.*f ", decimals, number)
}
func XMRUnits(v uint64) string {
const denomination = 1000000000000
return fmt.Sprintf("%d.%012d", v/denomination, v%denomination)
}