From 92b74f667ffcf2d7477c21bf1b8115c2e986f731 Mon Sep 17 00:00:00 2001 From: WeebDataHoarder <57538841+WeebDataHoarder@users.noreply.github.com> Date: Sun, 5 Mar 2023 15:06:49 +0100 Subject: [PATCH] Implemented ZMQ fetch of block data, proper main chain handling, refactored client RPC access, updated dependencies --- cmd/daemon/difficulty_cache.go | 4 +- cmd/p2pool/p2pool.go | 65 +++-- database/block.go | 135 +++++---- database/miner.go | 7 +- go.mod | 16 +- go.sum | 32 +-- monero/block/block.go | 19 +- monero/block/func.go | 10 + monero/block/rpc.go | 91 ------ monero/client/client.go | 125 ++++---- monero/client/zmq/types.go | 6 +- p2pool/mainchain/mainchain.go | 444 +++++++++++++++++++++++++++++ p2pool/p2p/client.go | 1 + p2pool/p2p/server.go | 10 +- p2pool/p2pool.go | 400 +++++++++++++++++++++++++- p2pool/sidechain/consensus.go | 128 +++++---- p2pool/sidechain/poolblock.go | 30 +- p2pool/sidechain/poolblock_test.go | 32 ++- p2pool/sidechain/sidechain.go | 67 ++++- utils/units.go | 5 + 20 files changed, 1261 insertions(+), 366 deletions(-) create mode 100644 monero/block/func.go delete mode 100644 monero/block/rpc.go create mode 100644 p2pool/mainchain/mainchain.go diff --git a/cmd/daemon/difficulty_cache.go b/cmd/daemon/difficulty_cache.go index 2ea432d..f0cc342 100644 --- a/cmd/daemon/difficulty_cache.go +++ b/cmd/daemon/difficulty_cache.go @@ -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))) } diff --git a/cmd/p2pool/p2pool.go b/cmd/p2pool/p2pool.go index 4bde7ad..398066c 100644 --- a/cmd/p2pool/p2pool.go +++ b/cmd/p2pool/p2pool.go @@ -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) } } diff --git a/database/block.go b/database/block.go index eb266e6..8d3c843 100644 --- a/database/block.go +++ b/database/block.go @@ -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"` diff --git a/database/miner.go b/database/miner.go index 6966088..b4ff15a 100644 --- a/database/miner.go +++ b/database/miner.go @@ -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 diff --git a/go.mod b/go.mod index 18ac517..1feeca8 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index b23cbe2..cd01a77 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/monero/block/block.go b/monero/block/block.go index dba2024..2262dee 100644 --- a/monero/block/block.go +++ b/monero/block/block.go @@ -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 { diff --git a/monero/block/func.go b/monero/block/func.go new file mode 100644 index 0000000..ef64211 --- /dev/null +++ b/monero/block/func.go @@ -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 diff --git a/monero/block/rpc.go b/monero/block/rpc.go deleted file mode 100644 index 5405aa8..0000000 --- a/monero/block/rpc.go +++ /dev/null @@ -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), - } - } -} diff --git a/monero/client/client.go b/monero/client/client.go index 6d2c903..33d0fc1 100644 --- a/monero/client/client.go +++ b/monero/client/client.go @@ -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 diff --git a/monero/client/zmq/types.go b/monero/client/zmq/types.go index ac8afbd..0dfabc2 100644 --- a/monero/client/zmq/types.go +++ b/monero/client/zmq/types.go @@ -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"` diff --git a/p2pool/mainchain/mainchain.go b/p2pool/mainchain/mainchain.go new file mode 100644 index 0000000..6e9b549 --- /dev/null +++ b/p2pool/mainchain/mainchain.go @@ -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 +} diff --git a/p2pool/p2p/client.go b/p2pool/p2p/client.go index 106906a..1bb5bc4 100644 --- a/p2pool/p2p/client.go +++ b/p2pool/p2p/client.go @@ -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 { diff --git a/p2pool/p2p/server.go b/p2pool/p2p/server.go index a4875ff..e1d2ba2 100644 --- a/p2pool/p2p/server.go +++ b/p2pool/p2p/server.go @@ -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 { diff --git a/p2pool/p2pool.go b/p2pool/p2pool.go index cf104dc..9e94238 100644 --- a/p2pool/p2pool.go +++ b/p2pool/p2pool.go @@ -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) } diff --git a/p2pool/sidechain/consensus.go b/p2pool/sidechain/consensus.go index 2891770..38c5809 100644 --- a/p2pool/sidechain/consensus.go +++ b/p2pool/sidechain/consensus.go @@ -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) diff --git a/p2pool/sidechain/poolblock.go b/p2pool/sidechain/poolblock.go index 35c0159..a8eaff1 100644 --- a/p2pool/sidechain/poolblock.go +++ b/p2pool/sidechain/poolblock.go @@ -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 diff --git a/p2pool/sidechain/poolblock_test.go b/p2pool/sidechain/poolblock_test.go index 1423c14..bc03c31 100644 --- a/p2pool/sidechain/poolblock_test.go +++ b/p2pool/sidechain/poolblock_test.go @@ -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()) } diff --git a/p2pool/sidechain/sidechain.go b/p2pool/sidechain/sidechain.go index 423db71..f858c21 100644 --- a/p2pool/sidechain/sidechain.go +++ b/p2pool/sidechain/sidechain.go @@ -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 } diff --git a/utils/units.go b/utils/units.go index f70f03d..821abc4 100644 --- a/utils/units.go +++ b/utils/units.go @@ -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) +}