P2Pool consensus v3.0.0
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing

Migrate cmd utils, cmds to own repository
This commit is contained in:
DataHoarder 2024-04-03 19:10:32 +02:00
parent bd934bb5a3
commit fe75cf259c
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
182 changed files with 162 additions and 18685 deletions

View file

@ -1,12 +0,0 @@
.git/
.env
.env.example
.gitignore
testdata/
default.pgo.tmp
docker/nginx
docker/postgres
docker-compose.override.yml
p2pool.cache
p2pool_peers.txt
README.md

View file

@ -1,33 +0,0 @@
#GOPROXY=direct
# Monerod p2pool compatible node with open RPC and ZMQ ports
MONEROD_HOST=p2pmd.xmrvsbeast.com
MONEROD_RPC_PORT=18081
MONEROD_ZMQ_PORT=18083
# Tor private key and public address. You can generate vanity ones via https://github.com/cathugger/mkp224o
TOR_SERVICE_KEY=
TOR_SERVICE_ADDRESS=
NET_SERVICE_ADDRESS=
# Other APIs to lookup for transaction sweep besides local, for example, https://p2pool.observer. Separate by commas.
TRANSACTION_LOOKUP_OTHER=
SITE_TITLE=
# This port will be presented externally
SITE_PORT=8189
# If specified, these IRC details will be shown on site. If the network is supported a Matrix link will be provided.
#SITE_IRC_URL=ircs://irc.libera.chat:6697/#p2pool-observer/monero.social
# Change for mini if you want public reachable address
P2POOL_PORT=37889
# This port is what should be reachable externally, and will be reachable via Tor as well
P2POOL_EXTERNAL_PORT=37889
P2POOL_OUT_PEERS=24
P2POOL_IN_PEERS=64
# Extra arg examples
#P2POOL_EXTRA_ARGS=-light-mode
#P2POOL_EXTRA_ARGS=-mini
#P2POOL_EXTRA_ARGS=-consensus-config /data/consensus.json

2
.gitignore vendored
View file

@ -1,6 +1,4 @@
.idea
p2pool.cache
p2pool_peers.txt
docker-compose.override.yml
.env
default.pgo.tmp

View file

@ -7,7 +7,7 @@ Other general tools to work with Monero cryptography are also included.
## Reporting issues
You can give feedback or report / discuss issues on:
* [The issue tracker on git.gammaspectra.live/P2Pool/p2pool-observer](https://git.gammaspectra.live/P2Pool/p2pool-observer/issues?state=open)
* [The issue tracker on git.gammaspectra.live/P2Pool/consensus](https://git.gammaspectra.live/P2Pool/consensus/issues?state=open)
* Via IRC on [#p2pool-observer@libera.chat](ircs://irc.libera.chat/#p2pool-observer), or via [Matrix](https://matrix.to/#/#p2pool-observer:monero.social)
* Any of the relevant rooms for the specific observer instances listed below.
@ -42,7 +42,7 @@ If desired each tool can be run individually, but that is left to the user to co
### Initial setup
```bash
$ git clone https://git.gammaspectra.live/P2Pool/p2pool-observer.git test-instance
$ git clone https://git.gammaspectra.live/P2Pool/consensus.git test-instance
$ cd test-instance
$ cp .env.example .env
```
@ -67,7 +67,7 @@ It is recommended to run `docker system prune` regularly or after update to clea
When a new instance starts with previously imported archives you might want to backfill sweep transactions. For new instances this is not necessary, and you can also skip this step and just rely on future data.
```bash
$ docker-compose exec --workdir /usr/src/p2pool daemon \
go run -v git.gammaspectra.live/P2Pool/p2pool-observer/cmd/scansweeps \
go run -v git.gammaspectra.live/P2Pool/consensus/cmd/scansweeps \
-host MONEROD_HOST -rpc-port MONEROD_RPC_PORT \
-api-host "http://p2pool:3131" \
-db="host=db port=5432 dbname=p2pool user=p2pool password=p2pool sslmode=disable"

File diff suppressed because it is too large Load diff

View file

@ -1,52 +0,0 @@
package main
import (
"sync"
"time"
)
const (
CacheTotalKnownBlocksAndMiners = iota
totalSize
)
var cache = make([]*cachedResult, totalSize)
var cacheLock sync.RWMutex
type cachedResult struct {
t time.Time
result any
}
func cacheResult(k uint, cacheTime time.Duration, result func() any) any {
if k >= totalSize {
return result()
}
if r := func() any {
if cacheTime > 0 {
cacheLock.RLock()
defer cacheLock.RUnlock()
if r := cache[k]; r != nil && r.t.Add(cacheTime).After(time.Now()) {
return r.result
}
}
return nil
}(); r != nil {
return r
}
r := result()
if cacheTime > 0 && r != nil {
cacheLock.Lock()
defer cacheLock.Unlock()
cache[k] = &cachedResult{
t: time.Now(),
result: r,
}
}
return r
}

Binary file not shown.

View file

@ -1,42 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/api
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0 => ../index
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0 => ../utils
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/httputils v0.0.0 => ../httputils
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/httputils v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0
github.com/gorilla/mux v1.8.1
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/floatdrop/lru v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,48 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,124 +0,0 @@
package main
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"slices"
)
func PayoutAmountHint(shares map[uint64]types.Difficulty, reward uint64) map[uint64]uint64 {
amountShares := make(map[uint64]uint64)
totalWeight := types.DifficultyFrom64(0)
for _, w := range shares {
totalWeight = totalWeight.Add(w)
}
w := types.DifficultyFrom64(0)
rewardGiven := types.DifficultyFrom64(0)
for miner, weight := range shares {
w = w.Add(weight)
nextValue := w.Mul64(reward).Div(totalWeight)
amountShares[miner] = nextValue.Sub(rewardGiven).Lo
rewardGiven = nextValue
}
return amountShares
}
func Outputs(p2api *api.P2PoolApi, indexDb *index.Index, tip *index.SideBlock, derivationCache sidechain.DerivationCacheInterface, preAllocatedShares sidechain.Shares, preAllocatedRewards []uint64) (outputs transaction.Outputs, bottomHeight uint64) {
if tip == nil {
return nil, 0
}
poolBlock := p2api.LightByMainId(tip.MainId)
if poolBlock != nil {
window := index.QueryIterateToSlice(indexDb.GetSideBlocksInPPLNSWindow(tip))
if len(window) == 0 {
return nil, 0
}
var hintIndex int
getByTemplateIdFull := func(h types.Hash) *index.SideBlock {
if i := slices.IndexFunc(window, func(e *index.SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex = i
return window[i]
}
return nil
}
getByTemplateId := func(h types.Hash) *index.SideBlock {
//fast lookup first
if i := slices.IndexFunc(window[hintIndex:], func(e *index.SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex += i
return window[hintIndex]
}
return getByTemplateIdFull(h)
}
getUnclesOf := func(h types.Hash) index.QueryIterator[index.SideBlock] {
parentEffectiveHeight := window[hintIndex].EffectiveHeight
if window[hintIndex].TemplateId != h {
parentEffectiveHeight = 0
}
startIndex := 0
return &index.FakeQueryResult[index.SideBlock]{
NextFunction: func() (int, *index.SideBlock) {
for _, b := range window[startIndex+hintIndex:] {
if b.UncleOf == h {
startIndex++
return startIndex, b
}
startIndex++
if parentEffectiveHeight != 0 && b.EffectiveHeight < parentEffectiveHeight {
//early exit
break
}
}
return 0, nil
},
}
}
return index.CalculateOutputs(indexDb,
tip,
poolBlock.GetTransactionOutputType(),
poolBlock.Main.Coinbase.TotalReward,
&poolBlock.Side.CoinbasePrivateKey,
poolBlock.Side.CoinbasePrivateKeySeed,
p2api.MainDifficultyByHeight,
getByTemplateId,
getUnclesOf,
derivationCache,
preAllocatedShares,
preAllocatedRewards,
)
} else {
return nil, 0
}
}
func PayoutHint(p2api *api.P2PoolApi, indexDb *index.Index, tip *index.SideBlock) (shares map[uint64]types.Difficulty, blockDepth uint64) {
shares = make(map[uint64]types.Difficulty)
blockDepth, err := index.BlocksInPPLNSWindowFast(indexDb, tip, p2api.MainDifficultyByHeight, func(b *index.SideBlock, weight types.Difficulty) {
if _, ok := shares[b.Miner]; !ok {
shares[b.Miner] = types.DifficultyFrom64(0)
}
shares[b.Miner] = shares[b.Miner].Add(weight)
})
if err != nil {
return nil, blockDepth
}
return shares, blockDepth
}

View file

@ -1,71 +0,0 @@
package main
import (
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"io"
"net/http"
"sync"
"time"
)
var moneroVersion, p2poolVersion cmdutils.VersionInfo
var moneroVersionLock, p2poolVersionLock sync.Mutex
const versionCheckInterval = 3600
func getMoneroVersion() cmdutils.VersionInfo {
moneroVersionLock.Lock()
defer moneroVersionLock.Unlock()
now := time.Now().Unix()
if (moneroVersion.CheckedTimestamp + versionCheckInterval) >= now {
return moneroVersion
}
response, err := http.DefaultClient.Get("https://api.github.com/repos/monero-project/monero/releases/latest")
if err != nil {
return moneroVersion
}
defer response.Body.Close()
var releaseData cmdutils.ReleaseDataJson
if data, err := io.ReadAll(response.Body); err != nil {
return moneroVersion
} else if err = utils.UnmarshalJSON(data, &releaseData); err != nil {
return moneroVersion
} else {
moneroVersion.Version = releaseData.TagName
moneroVersion.Timestamp = releaseData.PublishedAt.Unix()
moneroVersion.Link = "https://github.com/monero-project/monero/releases/tag/" + releaseData.TagName
moneroVersion.CheckedTimestamp = now
}
return moneroVersion
}
func getP2PoolVersion() cmdutils.VersionInfo {
p2poolVersionLock.Lock()
defer p2poolVersionLock.Unlock()
now := time.Now().Unix()
if (p2poolVersion.CheckedTimestamp + versionCheckInterval) >= now {
return p2poolVersion
}
response, err := http.DefaultClient.Get("https://api.github.com/repos/SChernykh/p2pool/releases/latest")
if err != nil {
return p2poolVersion
}
defer response.Body.Close()
var releaseData cmdutils.ReleaseDataJson
if data, err := io.ReadAll(response.Body); err != nil {
return p2poolVersion
} else if err = utils.UnmarshalJSON(data, &releaseData); err != nil {
return p2poolVersion
} else {
p2poolVersion.Version = releaseData.TagName
p2poolVersion.Timestamp = releaseData.PublishedAt.Unix()
p2poolVersion.Link = "https://github.com/SChernykh/p2pool/releases/tag/" + releaseData.TagName
p2poolVersion.CheckedTimestamp = now
}
return p2poolVersion
}

View file

@ -1,129 +0,0 @@
package main
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"io"
"net/http"
"net/url"
)
var apiUrl string
func getTypeFromAPI[T any](method string) *T {
uri, _ := url.Parse(apiUrl + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return nil
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
var result T
decoder := utils.NewJSONDecoder(response.Body)
if decoder.Decode(&result) != nil {
return nil
} else {
return &result
}
} else {
return nil
}
}
}
func getSliceFromAPI[T any](method string) []T {
uri, _ := url.Parse(apiUrl + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return nil
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
var result []T
decoder := utils.NewJSONDecoder(response.Body)
if decoder.Decode(&result) != nil {
return nil
} else {
return result
}
} else {
return nil
}
}
}
func getStreamFromAPI[T any](method string) <-chan T {
result := make(chan T, 1)
go func() {
defer close(result)
uri, _ := url.Parse(apiUrl + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
var err error
// Read opening
var b [1]byte
for {
if _, err = response.Body.Read(b[:]); err != nil {
return
}
if b[0] == '[' {
break
} else if b[0] != ' ' && b[0] != 0xa {
return
}
}
decoder := utils.NewJSONDecoder(response.Body)
for decoder.More() {
var item T
if err := decoder.Decode(&item); err != nil {
return
} else {
result <- item
}
}
}
}
}()
return result
}
func getFromAPIRaw(method string) []byte {
uri, _ := url.Parse(apiUrl + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return nil
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
if data, err := io.ReadAll(response.Body); err != nil {
return nil
} else {
return data
}
} else {
return nil
}
}
}

View file

@ -1,92 +0,0 @@
package main
import (
"flag"
"fmt"
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/legacy"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"slices"
"strconv"
)
func main() {
selectedApiUrl := flag.String("api", "", "Input API url, for example, https://p2pool.observer/api/")
outputFile := flag.String("output", "p2pool.cache", "Output p2pool.cache path")
fromBlock := flag.String("from", "tip", "Block to start from. Can be an ID or a height")
flag.Parse()
apiUrl = *selectedApiUrl
poolInfo := getTypeFromAPI[cmdutils.PoolInfoResult]("pool_info")
if poolInfo == nil {
panic("could not fetch consensus")
}
consensusData, _ := utils.MarshalJSON(poolInfo.SideChain.Consensus)
consensus, err := sidechain.NewConsensusFromJSON(consensusData)
if err != nil {
utils.Panic(err)
}
utils.Logf("Consensus", "Consensus id = %s", consensus.Id)
cache, err := legacy.NewCache(consensus, *outputFile)
if err != nil {
utils.Panic(err)
}
defer cache.Close()
var toFetchUrls []string
if *fromBlock == "tip" {
toFetchUrls = append(toFetchUrls, fmt.Sprintf("redirect/tip/raw"))
} else if n, err := strconv.ParseUint(*fromBlock, 10, 0); err == nil {
toFetchUrls = append(toFetchUrls, fmt.Sprintf("block_by_height/%d/raw", n))
} else {
toFetchUrls = append(toFetchUrls, fmt.Sprintf("block_by_id/%s/raw", *fromBlock))
}
var fetches int
addBlockId := func(h types.Hash) {
k := fmt.Sprintf("block_by_id/%s/raw", h)
if slices.Contains(toFetchUrls, k) {
return
}
toFetchUrls = append(toFetchUrls, k)
}
for len(toFetchUrls) > 0 {
nextUrl := toFetchUrls[0]
fmt.Printf("[%d] fetching %s\n", fetches, nextUrl)
toFetchUrls = slices.Delete(toFetchUrls, 0, 1)
rawBlock := getFromAPIRaw(nextUrl)
b := &sidechain.PoolBlock{}
err := b.UnmarshalBinary(consensus, &sidechain.NilDerivationCache{}, rawBlock)
if err != nil {
panic(fmt.Errorf("could not fetch block from url %s: %w", nextUrl, err))
}
cache.Store(b)
for _, u := range b.Side.Uncles {
addBlockId(u)
}
addBlockId(b.Side.Parent)
fetches++
if fetches >= legacy.NumBlocks {
print("reached max limit of block cache, exiting\n")
break
}
}
cache.Flush()
}

View file

@ -1,41 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/apitocache
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0 => ../../p2pool/cache
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0 => ../index
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0 => ../utils
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0 // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/floatdrop/lru v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,46 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,114 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/archive"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/floatdrop/lru"
"math"
"os"
)
func main() {
inputConsensus := flag.String("consensus", "config.json", "Input config.json consensus file")
inputArchive := flag.String("input", "", "Input path for archive database")
outputArchive := flag.String("output", "", "Output path for archive database")
moneroHost := flag.String("host", "127.0.0.1", "IP address of your Monero node")
moneroRpcPort := flag.Uint("rpc-port", 18081, "monerod RPC API port number")
flag.Parse()
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
cf, err := os.ReadFile(*inputConsensus)
consensus, err := sidechain.NewConsensusFromJSON(cf)
if err != nil {
utils.Panic(err)
}
difficultyCache := lru.New[uint64, types.Difficulty](1024)
getDifficultyByHeight := func(height uint64) types.Difficulty {
if v := difficultyCache.Get(height); v == nil {
if r, err := client.GetDefaultClient().GetBlockHeaderByHeight(height, context.Background()); err == nil {
d := types.DifficultyFrom64(r.BlockHeader.Difficulty)
difficultyCache.Set(height, d)
return d
}
return types.ZeroDifficulty
} else {
return *v
}
}
inputCache, err := archive.NewCache(*inputArchive, consensus, getDifficultyByHeight)
if err != nil {
utils.Panic(err)
}
defer inputCache.Close()
outputCache, err := archive.NewCache(*outputArchive, consensus, getDifficultyByHeight)
if err != nil {
utils.Panic(err)
}
defer outputCache.Close()
derivationCache := sidechain.NewDerivationLRUCache()
blockCache := lru.New[types.Hash, *sidechain.PoolBlock](int(consensus.ChainWindowSize * 4))
getByTemplateId := func(h types.Hash) *sidechain.PoolBlock {
if v := blockCache.Get(h); v == nil {
if bs := inputCache.LoadByTemplateId(h); len(bs) != 0 {
bs[0].Depth.Store(math.MaxUint64)
blockCache.Set(h, bs[0])
return bs[0]
} else if bs = outputCache.LoadByTemplateId(h); len(bs) != 0 {
bs[0].Depth.Store(math.MaxUint64)
blockCache.Set(h, bs[0])
return bs[0]
} else {
return nil
}
} else {
return *v
}
}
preAllocatedShares := sidechain.PreAllocateShares(consensus.ChainWindowSize * 4)
blocksProcessed := 0
for blocksAtHeight := range inputCache.ScanHeights(0, math.MaxUint64) {
for i, b := range blocksAtHeight {
blocksProcessed++
if _, err := b.PreProcessBlock(consensus, derivationCache, preAllocatedShares, getDifficultyByHeight, getByTemplateId); err != nil {
utils.Errorf("", "error processing block %s at %d, %s", types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId)), b.Side.Height, err)
} else {
if parent := getByTemplateId(b.Side.Parent); parent != nil {
topDepth := parent.Depth.Load()
if topDepth == math.MaxUint64 {
b.Depth.Store(consensus.ChainWindowSize * 2)
} else if topDepth == 0 {
b.Depth.Store(0)
} else {
b.Depth.Store(topDepth - 1)
}
} else {
utils.Errorf("", "block %s at %d, could not get parent, fallback", types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId)), b.Side.Height)
b.Depth.Store(math.MaxUint64)
}
outputCache.Store(b)
if i == 0 {
blockCache.Set(types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId)), b)
}
}
}
}
utils.Logf("Archive", "blocks processed %d", blocksProcessed)
}

View file

@ -1,35 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/archivetoarchive
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0 => ../../p2pool/cache
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0
github.com/floatdrop/lru v1.3.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
go.etcd.io/bbolt v1.3.9 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,47 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,347 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"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/p2pool/cache/archive"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/floatdrop/lru"
"math"
"os"
"sync"
)
func main() {
inputConsensus := flag.String("consensus", "config.json", "Input config.json consensus file")
inputArchive := flag.String("input", "", "Input path for archive database")
moneroHost := flag.String("host", "127.0.0.1", "IP address of your Monero node")
moneroRpcPort := flag.Uint("rpc-port", 18081, "monerod RPC API port number")
connString := flag.String("conn", "", "Connection string for postgres database")
flag.Parse()
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
client.GetDefaultClient().SetThrottle(1000)
cf, err := os.ReadFile(*inputConsensus)
consensus, err := sidechain.NewConsensusFromJSON(cf)
if err != nil {
utils.Panic(err)
}
if err = consensus.InitHasher(2, randomx.FlagSecure, randomx.FlagFullMemory); err != nil {
utils.Panic(err)
}
var headerCacheLock sync.RWMutex
headerByHeightCache := make(map[uint64]*block.Header)
headerByIdCache := make(map[types.Hash]*block.Header)
getHeaderByHeight := func(height uint64) *block.Header {
if v := func() *block.Header {
headerCacheLock.RLock()
defer headerCacheLock.RUnlock()
return headerByHeightCache[height]
}(); v == nil {
if r, err := client.GetDefaultClient().GetBlockHeaderByHeight(height, context.Background()); err == nil {
headerCacheLock.Lock()
defer headerCacheLock.Unlock()
prevHash, _ := types.HashFromString(r.BlockHeader.PrevHash)
h, _ := types.HashFromString(r.BlockHeader.Hash)
header := &block.Header{
MajorVersion: uint8(r.BlockHeader.MajorVersion),
MinorVersion: uint8(r.BlockHeader.MinorVersion),
Timestamp: uint64(r.BlockHeader.Timestamp),
PreviousId: prevHash,
Height: r.BlockHeader.Height,
Nonce: uint32(r.BlockHeader.Nonce),
Reward: r.BlockHeader.Reward,
Id: h,
Difficulty: types.DifficultyFrom64(r.BlockHeader.Difficulty),
}
headerByIdCache[header.Id] = header
headerByHeightCache[header.Height] = header
return header
}
return nil
} else {
return v
}
}
getDifficultyByHeight := func(height uint64) types.Difficulty {
if v := getHeaderByHeight(height); v != nil {
return v.Difficulty
}
return types.ZeroDifficulty
}
getSeedByHeight := func(height uint64) (hash types.Hash) {
seedHeight := randomx.SeedHeight(height)
if v := getHeaderByHeight(seedHeight); v != nil {
return v.Id
}
return types.ZeroHash
}
archiveCache, err := archive.NewCache(*inputArchive, consensus, getDifficultyByHeight)
if err != nil {
utils.Panic(err)
}
defer archiveCache.Close()
blockCache := lru.New[types.Hash, *sidechain.PoolBlock](int(consensus.ChainWindowSize * 4))
derivationCache := sidechain.NewDerivationLRUCache()
getByTemplateIdDirect := func(h types.Hash) *sidechain.PoolBlock {
if v := blockCache.Get(h); v == nil {
if bs := archiveCache.LoadByTemplateId(h); len(bs) != 0 {
blockCache.Set(h, bs[0])
return bs[0]
} else {
return nil
}
} else {
return *v
}
}
processBlock := func(b *sidechain.PoolBlock) error {
var preAllocatedShares sidechain.Shares
if len(b.Main.Coinbase.Outputs) == 0 {
//cannot use SideTemplateId() as it might not be proper to calculate yet. fetch from coinbase only here
if b2 := getByTemplateIdDirect(types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId))); b2 != nil && len(b2.Main.Coinbase.Outputs) != 0 {
b.Main.Coinbase.Outputs = b2.Main.Coinbase.Outputs
} else {
preAllocatedShares = sidechain.PreAllocateShares(consensus.ChainWindowSize * 2)
}
}
_, err := b.PreProcessBlock(consensus, derivationCache, preAllocatedShares, getDifficultyByHeight, getByTemplateIdDirect)
return err
}
getByTemplateId := func(h types.Hash) *sidechain.PoolBlock {
if v := getByTemplateIdDirect(h); v != nil {
if processBlock(v) != nil {
return nil
}
return v
} else {
return nil
}
}
indexDb, err := index.OpenIndex(*connString, consensus, getDifficultyByHeight, getSeedByHeight, getByTemplateIdDirect)
if err != nil {
utils.Panic(err)
}
defer indexDb.Close()
totalStored := 0
var lastRangeHeight, rangeStart uint64 = math.MaxUint64, math.MaxUint64
var lastTipEntries []*sidechain.PoolBlock
type rangeEntry struct {
//todo: check time of range
startHeight uint64
tipHeight uint64
tipEntries []*sidechain.PoolBlock
}
var heightRanges []rangeEntry
/*id, _ := types.HashFromString("b83a96d30c7db3b15a65fa43ddcf6914bb4e176dea49a94a5263c9adca93d4cc")
heightRanges = append(heightRanges, rangeEntry{
startHeight: 3087255,
tipHeight: 4728558,
tipEntries: []*sidechain.PoolBlock{getByTemplateId(id)},
})*/
if len(heightRanges) == 0 {
for blocksAtHeight := range archiveCache.ScanHeights(0, math.MaxUint64) {
if len(blocksAtHeight) == 0 {
utils.Panicf("no blocks at %d + 1?", lastRangeHeight)
}
if lastRangeHeight == math.MaxUint64 {
rangeStart = blocksAtHeight[0].Side.Height
} else if blocksAtHeight[0].Side.Height != (lastRangeHeight + 1) { // new range
heightRanges = append(heightRanges, rangeEntry{
startHeight: rangeStart,
tipHeight: lastRangeHeight,
tipEntries: lastTipEntries,
})
utils.Logf("", "range %d -> %d, total of %d height(s)", rangeStart, lastRangeHeight, lastRangeHeight-rangeStart+1)
utils.Logf("", "missing %d -> %d, total of %d height(s)", lastRangeHeight+1, blocksAtHeight[0].Side.Height-1, (blocksAtHeight[0].Side.Height-1)-(lastRangeHeight+1)+1)
rangeStart = blocksAtHeight[0].Side.Height
}
lastRangeHeight = blocksAtHeight[0].Side.Height
totalStored += len(blocksAtHeight)
lastTipEntries = blocksAtHeight
}
utils.Logf("", "range %d -> %d, total of %d height(s)", rangeStart, lastRangeHeight, lastRangeHeight-rangeStart+1)
heightRanges = append(heightRanges, rangeEntry{
startHeight: rangeStart,
tipHeight: lastRangeHeight,
tipEntries: lastTipEntries,
})
utils.Logf("", "total stored %d", totalStored)
}
var lastTime, lastHeight uint64 = math.MaxUint64, math.MaxUint64
for i := len(heightRanges) - 1; i >= 0; i-- {
r := heightRanges[i]
var bestTip *sidechain.PoolBlock
if len(r.tipEntries) == 0 {
bestTip = r.tipEntries[0]
} else {
for _, b := range r.tipEntries {
if (b.Main.Timestamp - 60*5) > lastTime { //2m offset of max time drift
continue
}
if bestTip == nil {
bestTip = b
} else if bestTip.Main.Coinbase.GenHeight < b.Main.Coinbase.GenHeight {
bestTip = b
} else if bestTip.Main.Timestamp < b.Main.Timestamp {
bestTip = b
}
}
}
if bestTip == nil {
utils.Logf("", "skipped range %d to %d due to: nil tip", r.startHeight, r.tipHeight)
continue
} else if bestTip.Main.Coinbase.GenHeight > lastHeight {
utils.Logf("", "skipped range %d to %d due to: main height %d > %d", r.startHeight, r.tipHeight, bestTip.Main.Coinbase.GenHeight, lastHeight)
continue
} else if (bestTip.Main.Timestamp - 60*5) > lastTime {
utils.Logf("", "skipped range %d to %d due to: timestamp %d > %d", r.startHeight, r.tipHeight, bestTip.Main.Timestamp, lastTime)
continue
}
if err := processBlock(bestTip); err != nil {
utils.Logf("", "skipped range %d to %d due to: could not process tip: %s", r.startHeight, r.tipHeight, err)
continue
}
lastTime = bestTip.Main.Timestamp
lastHeight = bestTip.Main.Coinbase.GenHeight
if r.startHeight-r.tipHeight > consensus.ChainWindowSize*2 &&
indexDb.GetTipSideBlockByTemplateId(bestTip.SideTemplateId(consensus)) != nil &&
indexDb.GetTipSideBlockByHeight(r.startHeight+consensus.ChainWindowSize+1) != nil &&
index.QueryHasResults(indexDb.GetSideBlocksByHeight(r.startHeight)) {
continue
}
// skip inserted heights
for bestTip != nil && indexDb.GetTipSideBlockByTemplateId(bestTip.SideTemplateId(consensus)) != nil {
utils.Logf("", "skip id = %s, template id = %s, height = %d", bestTip.MainId(), bestTip.SideTemplateId(consensus), bestTip.Side.Height)
bestTip = getByTemplateId(bestTip.Side.Parent)
}
for cur := bestTip; cur != nil; cur = getByTemplateId(cur.Side.Parent) {
utils.Logf("", "id = %s, template id = %s, height = %d", cur.MainId(), cur.SideTemplateId(consensus), cur.Side.Height)
if err = indexDb.InsertOrUpdatePoolBlock(cur, index.InclusionInVerifiedChain); err != nil {
utils.Errorf("", "error inserting %s, %s at %d: %s", cur.SideTemplateId(consensus), cur.MainId(), cur.Side.Height, err)
break
}
lastTime = cur.Main.Timestamp
lastHeight = cur.Main.Coinbase.GenHeight
curId := cur.SideTemplateId(consensus)
for _, e := range archiveCache.LoadBySideChainHeight(cur.Side.Height) {
if cur.FullId() == e.FullId() {
continue
}
timeDiff := int64(e.Main.Timestamp) - int64(cur.Main.Timestamp)
if timeDiff < 0 {
timeDiff = -timeDiff
}
if timeDiff > 3600*12 { //More than 12 hours difference, do not include
continue
}
if processBlock(e) != nil {
utils.Errorf("", "error processing orphan/alternate %s, %s at %d: %s", e.SideTemplateId(consensus), e.MainId(), e.Side.Height, err)
continue
}
if indexDb.GetSideBlockByMainId(e.MainId()) != nil {
continue
}
if curId == e.SideTemplateId(consensus) {
if err = indexDb.InsertOrUpdatePoolBlock(e, index.InclusionAlternateInVerifiedChain); err != nil {
utils.Panicf("error inserting alternate %s, %s at %d: %s", e.SideTemplateId(consensus), e.MainId(), e.Side.Height, err)
break
}
} else {
if err = indexDb.InsertOrUpdatePoolBlock(e, index.InclusionOrphan); err != nil {
utils.Panicf("error inserting orphan %s, %s at %d: %s", e.SideTemplateId(consensus), e.MainId(), e.Side.Height, err)
break
}
}
}
}
blockCache = lru.New[types.Hash, *sidechain.PoolBlock](int(consensus.ChainWindowSize * 4))
headerByIdCache = make(map[types.Hash]*block.Header)
headerByHeightCache = make(map[uint64]*block.Header)
derivationCache.Clear()
}
var maxHeight, minHeight uint64
if err := indexDb.Query("SELECT MAX(main_height), MIN(main_height) FROM side_blocks WHERE inclusion = $1;", func(row index.RowScanInterface) error {
return row.Scan(&maxHeight, &minHeight)
}, index.InclusionInVerifiedChain); err != nil {
utils.Panic(err)
}
heightCount := maxHeight - minHeight + 1
const strideSize = 1000
strides := heightCount / strideSize
ctx := context.Background()
for stride := uint64(0); stride <= strides; stride++ {
start := minHeight + stride*strideSize
end := min(maxHeight, minHeight+stride*strideSize+strideSize)
utils.Logf("", "checking %d to %d", start, end)
if headers, err := client.GetDefaultClient().GetBlockHeadersRangeResult(start, end, ctx); err != nil {
utils.Panic(err)
} else {
for _, h := range headers.Headers {
if err := cmdutils.FindAndInsertMainHeader(h, indexDb, func(b *sidechain.PoolBlock) {
archiveCache.Store(b)
}, client.GetDefaultClient(), getDifficultyByHeight, getByTemplateIdDirect, archiveCache.LoadByMainId, archiveCache.LoadByMainChainHeight, processBlock); err != nil {
utils.Panic(err)
continue
}
}
}
}
mainBlocks, _ := indexDb.GetMainBlocksByQuery("WHERE side_template_id IS NOT NULL ORDER BY height DESC;")
index.QueryIterate(mainBlocks, func(_ int, mb *index.MainBlock) (stop bool) {
if err := cmdutils.FindAndInsertMainHeaderOutputs(mb, indexDb, client.GetDefaultClient(), getDifficultyByHeight, getByTemplateIdDirect, archiveCache.LoadByMainId, archiveCache.LoadByMainChainHeight, processBlock); err != nil {
utils.Error(err)
}
return false
})
}

View file

@ -1,42 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/archivetoindex
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0 => ../index
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0 => ../utils
replace git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0 => ../../p2pool/cache
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0
github.com/floatdrop/lru v1.3.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
go.etcd.io/bbolt v1.3.9 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,49 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,104 +0,0 @@
package main
import (
"context"
"flag"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/archive"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/legacy"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/floatdrop/lru"
"math"
"os"
)
type loadee struct {
c *sidechain.Consensus
cb func(block *sidechain.PoolBlock)
}
func (l *loadee) Consensus() *sidechain.Consensus {
return l.c
}
func (l *loadee) AddCachedBlock(block *sidechain.PoolBlock) {
l.cb(block)
}
func main() {
inputConsensus := flag.String("consensus", "config.json", "Input config.json consensus file")
inputFile := flag.String("input", "p2pool.cache", "Input p2pool.cache path")
outputArchive := flag.String("output", "", "Output path for archive database")
flag.Parse()
cf, err := os.ReadFile(*inputConsensus)
consensus, err := sidechain.NewConsensusFromJSON(cf)
if err != nil {
utils.Panic(err)
}
cache, err := legacy.NewCache(consensus, *inputFile)
if err != nil {
utils.Panic(err)
}
defer cache.Close()
difficultyCache := lru.New[uint64, types.Difficulty](1024)
getDifficultyByHeight := func(height uint64) types.Difficulty {
if v := difficultyCache.Get(height); v == nil {
if r, err := client.GetDefaultClient().GetBlockHeaderByHeight(height, context.Background()); err == nil {
d := types.DifficultyFrom64(r.BlockHeader.Difficulty)
difficultyCache.Set(height, d)
return d
}
return types.ZeroDifficulty
} else {
return *v
}
}
archiveCache, err := archive.NewCache(*outputArchive, consensus, getDifficultyByHeight)
if err != nil {
utils.Panic(err)
}
defer archiveCache.Close()
cachedBlocks := make(map[types.Hash]*sidechain.PoolBlock)
l := &loadee{
c: consensus,
cb: func(block *sidechain.PoolBlock) {
expectedBlockId := types.HashFromBytes(block.CoinbaseExtra(sidechain.SideTemplateId))
calculatedBlockId := block.SideTemplateId(consensus)
if expectedBlockId != calculatedBlockId {
utils.Errorf("", "block height %d, template id %s, expected %s", block.Side.Height, calculatedBlockId, expectedBlockId)
} else {
cachedBlocks[expectedBlockId] = block
}
},
}
cache.LoadAll(l)
var storeBlock func(b *sidechain.PoolBlock)
storeBlock = func(b *sidechain.PoolBlock) {
if parent := cachedBlocks[b.Side.Parent]; parent != nil {
b.FillTransactionParentIndices(parent)
storeBlock(parent)
}
b.Depth.Store(math.MaxUint64)
archiveCache.Store(b)
}
for _, b := range cachedBlocks {
if b.Depth.Load() == math.MaxUint64 {
continue
}
storeBlock(b)
}
}

View file

@ -1,35 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cachetoarchive
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0 => ../../p2pool/cache
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0
github.com/floatdrop/lru v1.3.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
go.etcd.io/bbolt v1.3.9 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,47 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,353 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
p2poolapi "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"net/http"
_ "net/http/pprof"
"sync"
"sync/atomic"
"time"
)
func blockId(b *sidechain.PoolBlock) types.Hash {
return types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId))
}
var sideBlocksLock sync.RWMutex
func main() {
moneroHost := flag.String("host", "127.0.0.1", "IP address of your Monero node")
moneroRpcPort := flag.Uint("rpc-port", 18081, "monerod RPC API port number")
startFromHeight := flag.Uint64("from", 0, "Start sync from this height")
dbString := flag.String("db", "", "")
p2poolApiHost := flag.String("api-host", "", "Host URL for p2pool go observer consensus")
fullMode := flag.Bool("full-mode", false, "Allocate RandomX dataset, uses 2GB of RAM")
debugListen := flag.String("debug-listen", "", "Provide a bind address and port to expose a pprof HTTP API on it.")
hookProxy := flag.String("hook-proxy", "", "socks5 proxy host:port for webhook requests")
flag.Parse()
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
if *hookProxy != "" {
cmdutils.SetWebHookProxy(*hookProxy)
}
p2api := p2poolapi.NewP2PoolApi(*p2poolApiHost)
if err := p2api.WaitSync(); err != nil {
utils.Panic(err)
}
if *fullMode {
if err := p2api.Consensus().InitHasher(1, randomx.FlagSecure, randomx.FlagFullMemory); err != nil {
utils.Panic(err)
}
} else {
if err := p2api.Consensus().InitHasher(1, randomx.FlagSecure); err != nil {
utils.Panic(err)
}
}
indexDb, err := index.OpenIndex(*dbString, p2api.Consensus(), p2api.DifficultyByHeight, p2api.SeedByHeight, p2api.ByTemplateId)
if err != nil {
utils.Panic(err)
}
defer indexDb.Close()
dbTip := indexDb.GetSideBlockTip()
var tipHeight uint64
if dbTip != nil {
tipHeight = dbTip.SideHeight
}
utils.Logf("CHAIN", "Last known database tip is %d\n", tipHeight)
window, uncles := p2api.StateFromTip()
if *startFromHeight != 0 {
tip := p2api.BySideHeight(*startFromHeight)
if len(tip) != 0 {
window, uncles = p2api.WindowFromTemplateId(blockId(tip[0]))
} else {
tip = p2api.BySideHeight(*startFromHeight + p2api.Consensus().ChainWindowSize)
if len(tip) != 0 {
window, uncles = p2api.WindowFromTemplateId(blockId(tip[0]))
}
}
}
insertFromTip := func(tip *sidechain.PoolBlock) {
if indexDb.GetTipSideBlockByTemplateId(blockId(tip)) != nil {
//reached old tip
return
}
for cur := tip; cur != nil; cur = p2api.ByTemplateId(cur.Side.Parent) {
utils.Logf("CHAIN", "Inserting share %s at height %d\n", blockId(cur).String(), cur.Side.Height)
for _, u := range cur.Side.Uncles {
utils.Logf("CHAIN", "Inserting uncle %s at parent height %d\n", u.String(), cur.Side.Height)
}
if err := indexDb.InsertOrUpdatePoolBlock(cur, index.InclusionInVerifiedChain); err != nil {
utils.Panic(err)
}
if indexDb.GetTipSideBlockByTemplateId(cur.Side.Parent) != nil {
//reached old tip
break
}
}
}
var backfillScan []types.Hash
for len(window) > 0 {
utils.Logf("CHAIN", "Found range %d -> %d (%s to %s), %d shares, %d uncles", window[0].Side.Height, window[len(window)-1].Side.Height, window[0].SideTemplateId(p2api.Consensus()), window[len(window)-1].SideTemplateId(p2api.Consensus()), len(window), len(uncles))
for _, b := range window {
indexDb.CachePoolBlock(b)
}
for _, u := range uncles {
indexDb.CachePoolBlock(u)
}
for _, b := range window {
if indexDb.GetTipSideBlockByTemplateId(b.SideTemplateId(p2api.Consensus())) != nil {
//reached old tip
window = nil
break
}
utils.Logf("CHAIN", "Inserting share %s at height %d\n", blockId(b).String(), b.Side.Height)
for _, u := range b.Side.Uncles {
utils.Logf("CHAIN", "Inserting uncle %s at parent height %d\n", u.String(), b.Side.Height)
}
if err := indexDb.InsertOrUpdatePoolBlock(b, index.InclusionInVerifiedChain); err != nil {
utils.Panic(err)
}
if b.IsProofHigherThanMainDifficulty(p2api.Consensus().GetHasher(), indexDb.GetDifficultyByHeight, indexDb.GetSeedByHeight) {
backfillScan = append(backfillScan, b.MainId())
}
for _, uncleId := range b.Side.Uncles {
if u := uncles.Get(uncleId); u != nil && u.IsProofHigherThanMainDifficulty(p2api.Consensus().GetHasher(), indexDb.GetDifficultyByHeight, indexDb.GetSeedByHeight) {
backfillScan = append(backfillScan, u.MainId())
}
}
}
if len(window) == 0 {
break
}
parent := p2api.ByTemplateId(window[len(window)-1].Side.Parent)
if parent == nil {
break
}
window, uncles = p2api.WindowFromTemplateId(blockId(parent))
if len(window) == 0 {
insertFromTip(parent)
break
}
}
var maxHeight, currentHeight uint64
if err = indexDb.Query("SELECT (SELECT MAX(main_height) FROM side_blocks) AS max_height, (SELECT MAX(height) FROM main_blocks) AS current_height;", func(row index.RowScanInterface) error {
return row.Scan(&maxHeight, &currentHeight)
}); err != nil {
utils.Panic(err)
}
ctx := context.Background()
scanHeader := func(h daemon.BlockHeader) error {
if err := cmdutils.FindAndInsertMainHeader(h, indexDb, func(b *sidechain.PoolBlock) {
p2api.InsertAlternate(b)
}, client.GetDefaultClient(), indexDb.GetDifficultyByHeight, indexDb.GetByTemplateId, p2api.ByMainId, p2api.LightByMainHeight, func(b *sidechain.PoolBlock) error {
_, err := b.PreProcessBlock(p2api.Consensus(), &sidechain.NilDerivationCache{}, sidechain.PreAllocateShares(p2api.Consensus().ChainWindowSize*2), indexDb.GetDifficultyByHeight, indexDb.GetByTemplateId)
return err
}); err != nil {
return err
}
return nil
}
heightCount := maxHeight - 1 - currentHeight + 1
const strideSize = 1000
strides := heightCount / strideSize
//backfill headers
for stride := uint64(0); stride <= strides; stride++ {
start := currentHeight + stride*strideSize
end := min(maxHeight-1, currentHeight+stride*strideSize+strideSize)
utils.Logf("", "checking %d to %d", start, end)
if headers, err := client.GetDefaultClient().GetBlockHeadersRangeResult(start, end, ctx); err != nil {
utils.Panic(err)
} else {
for _, h := range headers.Headers {
if err := scanHeader(h); err != nil {
utils.Panic(err)
continue
}
}
}
}
// backfill any missing headers when p2pool was down
for _, mainId := range backfillScan {
utils.Logf("", "checking backfill %s", mainId)
if header, err := client.GetDefaultClient().GetBlockHeaderByHash(mainId, ctx); err != nil {
utils.Errorf("", "not found %s", mainId)
} else {
if err := scanHeader(*header); err != nil {
utils.Panic(err)
continue
}
}
}
setupEventHandler(p2api, indexDb)
var doCheckOfOldBlocks atomic.Bool
doCheckOfOldBlocks.Store(true)
go func() {
//do deep scan for any missed main headers or deep reorgs every once in a while
for range time.Tick(time.Second * monero.BlockTime) {
if !doCheckOfOldBlocks.Load() {
continue
}
mainTip := indexDb.GetMainBlockTip()
for h := mainTip.Height; h >= 0 && h >= (mainTip.Height-monero.TransactionUnlockTime); h-- {
header := indexDb.GetMainBlockByHeight(h)
if header == nil {
break
}
cur, _ := client.GetDefaultClient().GetBlockHeaderByHash(header.Id, ctx)
if cur == nil {
break
}
go func() {
sideBlocksLock.Lock()
defer sideBlocksLock.Unlock()
if err := scanHeader(*cur); err != nil {
utils.Panic(err)
}
}()
}
}
}()
go func() {
//process older full blocks and sweeps
for range time.Tick(time.Second * monero.BlockTime) {
actualTip := indexDb.GetMainBlockTip()
mainTip := actualTip
maxDepth := mainTip.Height - randomx.SeedHashEpochBlocks*4
//find top start height
for h := mainTip.Height - monero.TransactionUnlockTime; h >= maxDepth; h-- {
mainTip = indexDb.GetMainBlockByHeight(h)
if mainTip == nil {
continue
}
if isProcessed, ok := mainTip.GetMetadata("processed").(bool); ok && isProcessed {
break
}
}
if mainTip.Height == maxDepth {
utils.Logf("", "Reached maxdepth %d: Use scansweeps to backfill data", maxDepth)
}
for h := mainTip.Height - monero.MinerRewardUnlockTime; h <= actualTip.Height-monero.TransactionUnlockTime; h++ {
b := indexDb.GetMainBlockByHeight(h)
if b == nil {
continue
}
if isProcessed, ok := b.GetMetadata("processed").(bool); ok && isProcessed {
continue
}
if err := cmdutils.ProcessFullBlock(b, indexDb); err != nil {
utils.Logf("", "error processing block %s at %d: %s", b.Id, b.Height, err)
}
}
}
}()
if *debugListen != "" {
go func() {
if err := http.ListenAndServe(*debugListen, nil); err != nil {
utils.Panic(err)
}
}()
}
for range time.Tick(time.Second * 1) {
currentTip := indexDb.GetSideBlockTip()
currentMainTip := indexDb.GetMainBlockTip()
tip := p2api.Tip()
mainTip := p2api.MainTip()
if tip == nil || mainTip == nil {
utils.Errorf("", "could not fetch tip or main tip")
continue
}
if blockId(tip) != currentTip.TemplateId {
if tip.Side.Height < currentTip.SideHeight {
//wtf
utils.Panicf("tip height less than ours, abort: %d < %d", tip.Side.Height, currentTip.SideHeight)
} else {
func() {
sideBlocksLock.Lock()
defer sideBlocksLock.Unlock()
insertFromTip(tip)
}()
}
}
if mainTip.Id != currentMainTip.Id {
if mainTip.Height < currentMainTip.Height {
//wtf
utils.Panicf("main tip height less than ours, abort: %d < %d", mainTip.Height, currentMainTip.Height)
} else {
var prevHash types.Hash
for cur, _ := client.GetDefaultClient().GetBlockHeaderByHash(mainTip.Id, ctx); cur != nil; cur, _ = client.GetDefaultClient().GetBlockHeaderByHash(prevHash, ctx) {
curHash, _ := types.HashFromString(cur.Hash)
curDb := indexDb.GetMainBlockByHeight(cur.Height)
if curDb != nil {
if curDb.Id == curHash {
break
} else { //there has been a swap
doCheckOfOldBlocks.Store(true)
}
}
utils.Logf("MAIN", "Insert main block %d, id %s", cur.Height, curHash)
func() {
sideBlocksLock.Lock()
defer sideBlocksLock.Unlock()
doCheckOfOldBlocks.Store(true)
if err := scanHeader(*cur); err != nil {
utils.Panic(err)
}
prevHash, _ = types.HashFromString(cur.PrevHash)
}()
}
}
}
}
}

Binary file not shown.

View file

@ -1,39 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/daemon
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0 => ../index
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0 => ../utils
require (
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0
nhooyr.io/websocket v1.8.10
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/floatdrop/lru v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,48 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

View file

@ -1,519 +0,0 @@
package main
import (
"context"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
types2 "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"net/http"
"nhooyr.io/websocket"
"slices"
"sync"
"sync/atomic"
"time"
)
type listener struct {
ListenerId uint64
Write func(buf []byte)
Context context.Context
Cancel func()
}
type SortedBuf[T any] struct {
buf []T
compare func(a, b T) int
}
func NewSortedBuf[T any](size int, compare func(a, b T) int) *SortedBuf[T] {
return &SortedBuf[T]{
buf: make([]T, size, size+1),
compare: compare,
}
}
func (b *SortedBuf[T]) Insert(value T) bool {
if b.Has(value) {
return false
}
b.buf = append(b.buf, value)
slices.SortFunc(b.buf, func(i, j T) int {
//keep highest value at 0
return b.compare(i, j) * -1
})
b.buf = b.buf[:len(b.buf)-1]
return b.Has(value)
}
func (b *SortedBuf[T]) Remove(value T) {
var zeroValue T
for i, v := range b.buf {
if b.compare(value, v) == 0 {
b.buf[i] = zeroValue
}
}
slices.SortFunc(b.buf, func(i, j T) int {
//keep highest value at index 0
return b.compare(i, j) * -1
})
}
func (b *SortedBuf[T]) Has(value T) bool {
for _, v := range b.buf {
if b.compare(value, v) == 0 {
//value inserted
return true
}
}
return false
}
var listenerLock sync.RWMutex
var listenerIdCounter atomic.Uint64
var listeners []*listener
func setupEventHandler(p2api *api.P2PoolApi, indexDb *index.Index) {
server := &http.Server{
Addr: "0.0.0.0:8787",
ReadTimeout: time.Second * 2,
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
requestTime := time.Now()
c, err := websocket.Accept(writer, request, nil)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
return
}
listenerId := listenerIdCounter.Add(1)
defer func() {
listenerLock.Lock()
defer listenerLock.Unlock()
if i := slices.IndexFunc(listeners, func(listener *listener) bool {
return listener.ListenerId == listenerId
}); i != -1 {
listeners = slices.Delete(listeners, i, i+1)
}
utils.Logf("WS", "Client %d detached after %.02f seconds", listenerId, time.Now().Sub(requestTime).Seconds())
}()
ctx, cancel := context.WithCancel(request.Context())
defer cancel()
func() {
listenerLock.Lock()
defer listenerLock.Unlock()
listeners = append(listeners, &listener{
ListenerId: listenerId,
Write: func(buf []byte) {
ctx2, cancel2 := context.WithTimeout(ctx, time.Second*5)
defer cancel2()
if c.Write(ctx2, websocket.MessageText, buf) != nil {
cancel()
}
},
Context: ctx,
Cancel: cancel,
})
utils.Logf("WS", "Client %d attached", listenerId)
}()
defer c.Close(websocket.StatusInternalError, "closing")
c.CloseRead(context.Background())
select {
case <-ctx.Done():
//wait
}
}),
}
go func() {
if err := server.ListenAndServe(); err != nil {
utils.Panic(err)
}
}()
//remember last few template for events
sideBlockBuffer := NewSortedBuf[*index.SideBlock](int(p2api.Consensus().ChainWindowSize*2), index.SortSideBlock)
//remember last few found main for events
foundBlockBuffer := NewSortedBuf[*index.FoundBlock](10, func(a, b *index.FoundBlock) int {
if a == b {
return 0
}
if a == nil {
return -1
} else if b == nil {
return 1
}
if a.MainBlock.Height < b.MainBlock.Height {
return -1
} else if a.MainBlock.Height > b.MainBlock.Height {
return 1
}
if a.SideHeight < b.SideHeight {
return -1
} else if a.SideHeight > b.SideHeight {
return 1
}
//same height, sort by main id
return a.MainBlock.Id.Compare(b.MainBlock.Id)
})
//initialize
tip := indexDb.GetSideBlockTip()
for cur := tip; cur != nil; cur = indexDb.GetTipSideBlockByTemplateId(cur.ParentTemplateId) {
if (cur.EffectiveHeight + p2api.Consensus().ChainWindowSize) < tip.EffectiveHeight {
break
}
sideBlockBuffer.Insert(cur)
index.QueryIterate(indexDb.GetSideBlocksByUncleOfId(cur.TemplateId), func(_ int, u *index.SideBlock) (stop bool) {
sideBlockBuffer.Insert(u)
return false
})
}
func() {
foundBlocks, _ := indexDb.GetFoundBlocks("", 5)
index.QueryIterate(foundBlocks, func(_ int, b *index.FoundBlock) (stop bool) {
foundBlockBuffer.Insert(b)
return false
})
}()
fillMainCoinbaseOutputs := func(outputs index.MainCoinbaseOutputs) (result index.MainCoinbaseOutputs) {
result = make(index.MainCoinbaseOutputs, 0, len(outputs))
for _, output := range outputs {
miner := indexDb.GetMiner(output.Miner)
output.MinerAddress = miner.Address()
output.MinerAlias = miner.Alias()
result = append(result, output)
}
return result
}
fillFoundBlockResult := func(foundBlock *index.FoundBlock) *index.FoundBlock {
miner := indexDb.GetMiner(foundBlock.Miner)
foundBlock.MinerAddress = miner.Address()
foundBlock.MinerAlias = miner.Alias()
return foundBlock
}
var minerData *types2.MinerData
var minerDataLock sync.Mutex
getMinerData := func(expectedHeight uint64) *types2.MinerData {
minerDataLock.Lock()
defer minerDataLock.Unlock()
if minerData == nil || minerData.Height < expectedHeight {
minerData = p2api.MinerData()
}
return minerData
}
fillSideBlockResult := func(mainTip *index.MainBlock, sideBlock *index.SideBlock) *index.SideBlock {
miner := indexDb.GetMiner(sideBlock.Miner)
sideBlock.MinerAddress = miner.Address()
sideBlock.MinerAlias = miner.Alias()
mainTipAtHeight := indexDb.GetMainBlockByHeight(sideBlock.MainHeight)
if mainTipAtHeight != nil {
sideBlock.MinedMainAtHeight = mainTipAtHeight.Id == sideBlock.MainId
sideBlock.MainDifficulty = mainTipAtHeight.Difficulty
} else {
minerData := getMinerData(sideBlock.MainHeight)
if minerData.Height == sideBlock.MainHeight {
sideBlock.MainDifficulty = minerData.Difficulty.Lo
} else if mainTip.Height == sideBlock.MainHeight {
sideBlock.MainDifficulty = mainTip.Difficulty
}
}
index.QueryIterate(indexDb.GetSideBlocksByUncleOfId(sideBlock.TemplateId), func(_ int, u *index.SideBlock) (stop bool) {
sideBlock.Uncles = append(sideBlock.Uncles, index.SideBlockUncleEntry{
TemplateId: u.TemplateId,
Miner: u.Miner,
SideHeight: u.SideHeight,
Difficulty: u.Difficulty,
})
return false
})
return sideBlock
}
go func() {
var blocksToReport []*index.SideBlock
for range time.Tick(time.Second * 1) {
//reuse
blocksToReport = blocksToReport[:0]
func() {
sideBlocksLock.RLock()
defer sideBlocksLock.RUnlock()
mainTip := indexDb.GetMainBlockTip()
tip := indexDb.GetSideBlockTip()
for cur := tip; cur != nil; cur = indexDb.GetTipSideBlockByTemplateId(cur.ParentTemplateId) {
if (cur.EffectiveHeight + p2api.Consensus().ChainWindowSize) < tip.EffectiveHeight {
break
}
var pushedNew bool
index.QueryIterate(indexDb.GetSideBlocksByUncleOfId(cur.TemplateId), func(_ int, u *index.SideBlock) (stop bool) {
if sideBlockBuffer.Insert(u) {
//first time seen
pushedNew = true
blocksToReport = append(blocksToReport, fillSideBlockResult(mainTip, u))
}
return false
})
if sideBlockBuffer.Insert(cur) {
//first time seen
pushedNew = true
blocksToReport = append(blocksToReport, fillSideBlockResult(mainTip, cur))
}
if !pushedNew {
break
}
}
}()
//sort for proper order
slices.SortFunc(blocksToReport, func(a, b *index.SideBlock) int {
if a.EffectiveHeight < b.EffectiveHeight {
return -1
} else if a.EffectiveHeight > b.EffectiveHeight {
return 1
}
if a.SideHeight < b.SideHeight {
return -1
} else if a.SideHeight > b.SideHeight {
return 1
}
//same height, sort by main id
return a.MainId.Compare(b.MainId)
})
func() {
listenerLock.RLock()
defer listenerLock.RUnlock()
for _, b := range blocksToReport {
buf, err := utils.MarshalJSON(&cmdutils.JSONEvent{
Type: cmdutils.JSONEventSideBlock,
SideBlock: b,
FoundBlock: nil,
MainCoinbaseOutputs: nil,
})
if err != nil {
continue
}
for _, l := range listeners {
select {
case <-l.Context.Done():
default:
l.Write(buf)
}
}
ts := time.Now().Unix()
// Send webhooks
go func(b *index.SideBlock) {
q, _ := indexDb.GetMinerWebHooks(b.Miner)
index.QueryIterate(q, func(_ int, w *index.MinerWebHook) (stop bool) {
if err := cmdutils.SendSideBlock(w, ts, b.MinerAddress, b); err != nil {
utils.Logf("WebHook", "Error sending %s webhook to %s: type %s, url %s: %s", cmdutils.JSONEventSideBlock, b.MinerAddress.ToBase58(), w.Type, w.Url, err)
}
return false
})
}(b)
}
}()
}
}()
go func() {
var blocksToReport []*index.FoundBlock
var unfoundBlocksToReport []*index.SideBlock
for range time.Tick(time.Second * 1) {
//reuse
blocksToReport = blocksToReport[:0]
unfoundBlocksToReport = unfoundBlocksToReport[:0]
func() {
sideBlocksLock.RLock()
defer sideBlocksLock.RUnlock()
mainTip := indexDb.GetMainBlockTip()
for _, m := range foundBlockBuffer.buf {
if m != nil && indexDb.GetMainBlockById(m.MainBlock.Id) == nil {
//unfound
if b := indexDb.GetSideBlockByMainId(m.MainBlock.Id); b != nil {
unfoundBlocksToReport = append(unfoundBlocksToReport, fillSideBlockResult(mainTip, indexDb.GetSideBlockByMainId(m.MainBlock.Id)))
foundBlockBuffer.Remove(m)
}
}
}
func() {
foundBlocks, _ := indexDb.GetFoundBlocks("", 5)
index.QueryIterate(foundBlocks, func(_ int, b *index.FoundBlock) (stop bool) {
if foundBlockBuffer.Insert(b) {
//first time seen
blocksToReport = append(blocksToReport, fillFoundBlockResult(b))
}
return false
})
}()
}()
//sort for proper order
slices.SortFunc(blocksToReport, func(a, b *index.FoundBlock) int {
if a.MainBlock.Height < b.MainBlock.Height {
return -1
} else if a.MainBlock.Height > b.MainBlock.Height {
return 1
}
if a.EffectiveHeight < b.EffectiveHeight {
return -1
} else if a.EffectiveHeight > b.EffectiveHeight {
return 1
}
if a.SideHeight < b.SideHeight {
return -1
} else if a.SideHeight > b.SideHeight {
return 1
}
//same height, sort by main id
return a.MainBlock.Id.Compare(b.MainBlock.Id)
})
//sort for proper order
slices.SortFunc(unfoundBlocksToReport, func(a, b *index.SideBlock) int {
if a.MainHeight < b.MainHeight {
return -1
} else if a.MainHeight > b.MainHeight {
return 1
}
return index.SortSideBlock(a, b)
})
func() {
listenerLock.RLock()
defer listenerLock.RUnlock()
for _, b := range unfoundBlocksToReport {
buf, err := utils.MarshalJSON(&cmdutils.JSONEvent{
Type: cmdutils.JSONEventOrphanedBlock,
SideBlock: b,
FoundBlock: nil,
MainCoinbaseOutputs: nil,
})
if err != nil {
continue
}
for _, l := range listeners {
select {
case <-l.Context.Done():
default:
l.Write(buf)
}
}
ts := time.Now().Unix()
// Send webhooks
go func(b *index.SideBlock) {
q, _ := indexDb.GetMinerWebHooks(b.Miner)
index.QueryIterate(q, func(_ int, w *index.MinerWebHook) (stop bool) {
if err := cmdutils.SendOrphanedBlock(w, ts, b.MinerAddress, b); err != nil {
utils.Logf("WebHook", "Error sending %s webhook to %s: type %s, url %s: %s", cmdutils.JSONEventOrphanedBlock, b.MinerAddress.ToBase58(), w.Type, w.Url, err)
}
return false
})
}(b)
}
for _, b := range blocksToReport {
coinbaseOutputs := func() index.MainCoinbaseOutputs {
mainOutputs, err := indexDb.GetMainCoinbaseOutputs(b.MainBlock.CoinbaseId)
if err != nil {
panic(err)
}
defer mainOutputs.Close()
return fillMainCoinbaseOutputs(index.IterateToSliceWithoutPointer[index.MainCoinbaseOutput](mainOutputs))
}()
if len(coinbaseOutputs) == 0 {
//report next time
foundBlockBuffer.Remove(b)
continue
}
buf, err := utils.MarshalJSON(&cmdutils.JSONEvent{
Type: cmdutils.JSONEventFoundBlock,
SideBlock: nil,
FoundBlock: b,
MainCoinbaseOutputs: coinbaseOutputs,
})
if err != nil {
continue
}
for _, l := range listeners {
select {
case <-l.Context.Done():
default:
l.Write(buf)
}
}
includingHeight := max(b.EffectiveHeight, b.SideHeight)
if uint64(b.WindowDepth) > includingHeight {
includingHeight = 0
} else {
includingHeight -= uint64(b.WindowDepth)
}
// Send webhooks on outputs
for _, o := range coinbaseOutputs {
payout := &index.Payout{
Miner: o.Miner,
TemplateId: b.MainBlock.SideTemplateId,
SideHeight: b.SideHeight,
UncleOf: b.UncleOf,
MainId: b.MainBlock.Id,
MainHeight: b.MainBlock.Height,
Timestamp: b.MainBlock.Timestamp,
CoinbaseId: b.MainBlock.CoinbaseId,
Reward: o.Value,
PrivateKey: b.MainBlock.CoinbasePrivateKey,
Index: uint64(o.Index),
GlobalOutputIndex: o.GlobalOutputIndex,
IncludingHeight: includingHeight,
}
addr := o.MinerAddress
ts := time.Now().Unix()
// One goroutine per entry
go func() {
q, _ := indexDb.GetMinerWebHooks(payout.Miner)
index.QueryIterate(q, func(_ int, w *index.MinerWebHook) (stop bool) {
if err := cmdutils.SendFoundBlock(w, ts, addr, b, coinbaseOutputs); err != nil {
utils.Logf("WebHook", "Error sending %s webhook to %s: type %s, url %s: %s", cmdutils.JSONEventFoundBlock, addr.ToBase58(), w.Type, w.Url, err)
}
if err := cmdutils.SendPayout(w, ts, addr, payout); err != nil {
utils.Logf("WebHook", "Error sending %s webhook to %s: type %s, url %s: %s", cmdutils.JSONEventPayout, addr.ToBase58(), w.Type, w.Url, err)
}
return false
})
}()
}
}
}()
}
}()
}

View file

@ -1,18 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/httputils
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
require git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/floatdrop/lru v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
golang.org/x/sys v0.17.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,12 +0,0 @@
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View file

@ -1,80 +0,0 @@
package httputils
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"io"
"net/http"
"strings"
)
func EncodeJson(r *http.Request, writer io.Writer, d any) error {
encoder := utils.NewJSONEncoder(writer)
if strings.Index(strings.ToLower(r.Header.Get("user-agent")), "mozilla") != -1 {
encoder.SetIndent("", " ")
}
return encoder.EncodeWithOption(d, utils.JsonEncodeOptions...)
}
// StreamJsonChan Streams a channel of values into a JSON list via a writer.
func StreamJsonChan[T any](r *http.Request, writer io.Writer, stream <-chan T) error {
encoder := utils.NewJSONEncoder(writer)
if strings.Index(strings.ToLower(r.Header.Get("user-agent")), "mozilla") != -1 {
encoder.SetIndent("", " ")
}
// Write start of JSON list
_, _ = writer.Write([]byte{'[', 0xa})
var count uint64
defer func() {
// Write end of JSON list
_, _ = writer.Write([]byte{0xa, ']'})
// Empty channel
for range stream {
}
}()
for v := range stream {
if count > 0 {
// Write separator between list fields
_, _ = writer.Write([]byte{',', 0xa})
}
if err := encoder.EncodeWithOption(v, utils.JsonEncodeOptions...); err != nil {
return err
}
count++
}
return nil
}
// StreamJsonIterator Streams an iterator of values into a JSON list via a writer.
func StreamJsonIterator[T any](r *http.Request, writer io.Writer, next func() (int, *T)) error {
encoder := utils.NewJSONEncoder(writer)
if strings.Index(strings.ToLower(r.Header.Get("user-agent")), "mozilla") != -1 {
encoder.SetIndent("", " ")
}
// Write start of JSON list
_, _ = writer.Write([]byte{'[', 0xa})
var count uint64
defer func() {
// Write end of JSON list
_, _ = writer.Write([]byte{0xa, ']'})
}()
for {
_, v := next()
if v == nil {
break
}
if count > 0 {
// Write separator between list fields
_, _ = writer.Write([]byte{',', 0xa})
}
if err := encoder.EncodeWithOption(v, utils.JsonEncodeOptions...); err != nil {
return err
}
count++
}
return nil
}

View file

@ -1,36 +0,0 @@
package index
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
)
type FoundBlock struct {
MainBlock MainBlock `json:"main_block"`
SideHeight uint64 `json:"side_height"`
Miner uint64 `json:"miner"`
UncleOf types.Hash `json:"uncle_of,omitempty"`
EffectiveHeight uint64 `json:"effective_height"`
WindowDepth uint32 `json:"window_depth"`
WindowOutputs uint32 `json:"window_outputs"`
TransactionCount uint32 `json:"transaction_count"`
Difficulty uint64 `json:"difficulty"`
CumulativeDifficulty types.Difficulty `json:"cumulative_difficulty"`
Inclusion BlockInclusion `json:"inclusion"`
// Extra information filled just for JSON purposes
MinerAddress *address.Address `json:"miner_address,omitempty"`
MinerAlias string `json:"miner_alias,omitempty"`
}
func (b *FoundBlock) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
if err := row.Scan(
&b.MainBlock.Id, &b.MainBlock.Height, &b.MainBlock.Timestamp, &b.MainBlock.Reward, &b.MainBlock.CoinbaseId, &b.MainBlock.CoinbasePrivateKey, &b.MainBlock.Difficulty, &b.MainBlock.SideTemplateId,
&b.SideHeight, &b.Miner, &b.UncleOf, &b.EffectiveHeight, &b.WindowDepth, &b.WindowOutputs, &b.TransactionCount, &b.Difficulty, &b.CumulativeDifficulty, &b.Inclusion,
); err != nil {
return err
}
return nil
}

View file

@ -1,12 +0,0 @@
CREATE OR REPLACE FUNCTION count_estimate(query text) RETURNS integer AS $$
DECLARE
rec record;
rows integer;
BEGIN
FOR rec IN EXECUTE 'EXPLAIN ' || query LOOP
rows := substring(rec."QUERY PLAN" FROM ' rows=([[:digit:]]+)');
EXIT WHEN rows IS NOT NULL;
END LOOP;
RETURN rows;
END;
$$ LANGUAGE plpgsql VOLATILE STRICT;

View file

@ -1,32 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a
github.com/floatdrop/lru v1.3.0
github.com/lib/pq v1.10.9
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,46 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,1448 +0,0 @@
package index
import (
"context"
"database/sql"
"embed"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"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/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/floatdrop/lru"
"github.com/lib/pq"
"io"
"path"
"reflect"
"regexp"
"slices"
"strings"
"sync"
)
type Index struct {
consensus *sidechain.Consensus
getDifficultyByHeight block.GetDifficultyByHeightFunc
getSeedByHeight block.GetSeedByHeightFunc
getByTemplateId sidechain.GetByTemplateIdFunc
derivationCache sidechain.DerivationCacheInterface
blockCache *lru.LRU[types.Hash, *sidechain.PoolBlock]
handle *sql.DB
statements struct {
GetMinerById *sql.Stmt
GetMinerByAddress *sql.Stmt
GetMinerByAlias *sql.Stmt
InsertMiner *sql.Stmt
TipSideBlock *sql.Stmt
TipSideBlocksTemplateId *sql.Stmt
InsertOrUpdateSideBlock *sql.Stmt
TipMainBlock *sql.Stmt
GetMainBlockByHeight *sql.Stmt
GetMainBlockById *sql.Stmt
GetSideBlockByMainId *sql.Stmt
GetSideBlockByUncleId *sql.Stmt
}
caches struct {
minerLock sync.RWMutex
miner map[uint64]*Miner
}
views map[string]string
}
//go:embed schema.sql
var dbSchema string
//go:embed functions/*.sql
var dbFunctions embed.FS
func OpenIndex(connStr string, consensus *sidechain.Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getSeedByHeight block.GetSeedByHeightFunc, getByTemplateId sidechain.GetByTemplateIdFunc) (index *Index, err error) {
index = &Index{
consensus: consensus,
getDifficultyByHeight: difficultyByHeight,
getSeedByHeight: getSeedByHeight,
getByTemplateId: getByTemplateId,
derivationCache: sidechain.NewDerivationLRUCache(),
blockCache: lru.New[types.Hash, *sidechain.PoolBlock](int(consensus.ChainWindowSize * 4)),
views: make(map[string]string),
}
if index.handle, err = sql.Open("postgres", connStr); err != nil {
return nil, err
}
index.handle.SetMaxIdleConns(8)
tx, err := index.handle.BeginTx(context.Background(), nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
viewMatch := regexp.MustCompile("CREATE (MATERIALIZED |)VIEW ([^ \n\t]+?)(_v[0-9]+)? AS\n")
immv := regexp.MustCompile("SELECT (create_immv)\\('([^ \n\t']+?)(_v[0-9]+)?',")
for _, statement := range strings.Split(dbSchema, ";") {
var matches []string
if matches = viewMatch.FindStringSubmatch(statement); matches == nil {
matches = immv.FindStringSubmatch(statement)
}
if matches != nil {
isMaterialized := matches[1] == "MATERIALIZED "
isImmv := matches[1] == "create_immv"
viewName := matches[2]
fullViewName := viewName
//base view for materialized one
if len(matches[3]) != 0 {
fullViewName = viewName + matches[3]
}
index.views[viewName] = fullViewName
if row, err := tx.Query(fmt.Sprintf("SELECT relname, relkind FROM pg_class WHERE relname LIKE '%s%%';", viewName)); err != nil {
return nil, err
} else {
var entries []struct{ n, kind string }
var exists bool
if err = func() error {
defer row.Close()
for row.Next() {
var n, kind string
if err = row.Scan(&n, &kind); err != nil {
return err
}
entries = append(entries, struct{ n, kind string }{n: n, kind: kind})
}
return row.Err()
}(); err != nil {
return nil, err
}
for _, e := range entries {
if e.kind == "m" && fullViewName != e.n {
if _, err := tx.Exec(fmt.Sprintf("DROP MATERIALIZED VIEW %s CASCADE;", e.n)); err != nil {
return nil, err
}
} else if e.kind == "v" && fullViewName != e.n {
if _, err := tx.Exec(fmt.Sprintf("DROP VIEW %s CASCADE;", e.n)); err != nil {
return nil, err
}
} else if e.kind == "r" && fullViewName != e.n {
if _, err := tx.Exec(fmt.Sprintf("DROP TABLE %s CASCADE;", e.n)); err != nil {
return nil, err
}
} else if e.kind != "i" {
exists = true
}
}
if !exists {
if _, err := tx.Exec(statement); err != nil {
return nil, err
}
//Do first refresh
if isMaterialized {
if _, err := tx.Exec(fmt.Sprintf("REFRESH MATERIALIZED VIEW %s;", fullViewName)); err != nil {
return nil, err
}
} else if isImmv {
if _, err := tx.Exec(fmt.Sprintf("SELECT refresh_immv('%s', true);", fullViewName)); err != nil {
return nil, err
}
}
continue
} else {
continue
}
}
}
if _, err := tx.Exec(statement); err != nil {
return nil, err
}
}
if dir, err := dbFunctions.ReadDir("functions"); err != nil {
return nil, err
} else {
for _, e := range dir {
f, err := dbFunctions.Open(path.Join("functions", e.Name()))
if err != nil {
return nil, err
}
var data []byte
func() {
defer f.Close()
data, err = io.ReadAll(f)
}()
if err != nil {
return nil, err
}
if _, err := tx.Exec(string(data)); err != nil {
return nil, err
}
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
if index.statements.GetMinerById, err = index.handle.Prepare("SELECT " + MinerSelectFields + " FROM miners WHERE id = $1;"); err != nil {
return nil, err
}
if index.statements.GetMinerByAddress, err = index.handle.Prepare("SELECT " + MinerSelectFields + " FROM miners WHERE spend_public_key = $1 AND view_public_key = $2;"); err != nil {
return nil, err
}
if index.statements.GetMinerByAlias, err = index.handle.Prepare("SELECT " + MinerSelectFields + " FROM miners WHERE alias = $1;"); err != nil {
return nil, err
}
if index.statements.InsertMiner, err = index.handle.Prepare("INSERT INTO miners (spend_public_key, view_public_key) VALUES ($1, $2) RETURNING " + MinerSelectFields + ";"); err != nil {
return nil, err
}
if index.statements.TipSideBlocksTemplateId, err = index.PrepareSideBlocksByQueryStatement("WHERE template_id = $1 AND effective_height = side_height AND inclusion = $2;"); err != nil {
return nil, err
}
if index.statements.InsertOrUpdateSideBlock, err = index.handle.Prepare("INSERT INTO side_blocks (" + SideBlockSelectFields + ") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) ON CONFLICT (main_id) DO UPDATE SET uncle_of = $7, effective_height = $8, inclusion = $21;"); err != nil {
return nil, err
}
if index.statements.GetMainBlockByHeight, err = index.PrepareMainBlocksByQueryStatement("WHERE height = $1;"); err != nil {
return nil, err
}
if index.statements.GetMainBlockById, err = index.PrepareMainBlocksByQueryStatement("WHERE id = $1;"); err != nil {
return nil, err
}
if index.statements.GetSideBlockByMainId, err = index.PrepareSideBlocksByQueryStatement("WHERE main_id = $1;"); err != nil {
return nil, err
}
if index.statements.GetSideBlockByUncleId, err = index.PrepareSideBlocksByQueryStatement("WHERE uncle_of = $1;"); err != nil {
return nil, err
}
if index.statements.TipSideBlock, err = index.PrepareSideBlocksByQueryStatement("WHERE inclusion = $1 ORDER BY side_height DESC LIMIT 1;"); err != nil {
return nil, err
}
if index.statements.TipMainBlock, err = index.PrepareMainBlocksByQueryStatement("ORDER BY height DESC LIMIT 1;"); err != nil {
return nil, err
}
index.caches.miner = make(map[uint64]*Miner)
return index, nil
}
func (i *Index) GetDifficultyByHeight(height uint64) types.Difficulty {
if mb := i.GetMainBlockByHeight(height); mb != nil {
return types.DifficultyFrom64(mb.Difficulty)
}
return i.getDifficultyByHeight(height)
}
func (i *Index) GetSeedByHeight(height uint64) types.Hash {
seedHeight := randomx.SeedHeight(height)
if mb := i.GetMainBlockByHeight(seedHeight); mb != nil {
return mb.Id
}
return i.getSeedByHeight(height)
}
func (i *Index) GetByTemplateId(id types.Hash) *sidechain.PoolBlock {
if v := i.blockCache.Get(id); v != nil {
return *v
} else {
if b := i.getByTemplateId(id); b != nil {
i.blockCache.Set(id, b)
return b
}
}
return nil
}
func (i *Index) CachePoolBlock(b *sidechain.PoolBlock) {
i.blockCache.Set(b.SideTemplateId(i.consensus), b)
}
func (i *Index) Consensus() *sidechain.Consensus {
return i.consensus
}
func (i *Index) GetMiner(miner uint64) *Miner {
if m := func() *Miner {
i.caches.minerLock.RLock()
defer i.caches.minerLock.RUnlock()
return i.caches.miner[miner]
}(); m != nil {
return m
} else if m = i.getMiner(miner); m != nil {
i.caches.minerLock.Lock()
defer i.caches.minerLock.Unlock()
i.caches.miner[miner] = m
return m
} else {
return nil
}
}
func (i *Index) getMiner(miner uint64) *Miner {
if rows, err := i.statements.GetMinerById.Query(miner); err != nil {
return nil
} else {
defer rows.Close()
return i.scanMiner(rows)
}
}
func (i *Index) GetMinerByAlias(alias string) *Miner {
if rows, err := i.statements.GetMinerByAlias.Query(alias); err != nil {
return nil
} else {
defer rows.Close()
return i.scanMiner(rows)
}
}
func (i *Index) GetMinerByStringAddress(addr string) *Miner {
minerAddr := address.FromBase58(addr)
if minerAddr != nil {
return i.GetMinerByAddress(minerAddr)
}
return nil
}
func (i *Index) GetMinerByAddress(addr *address.Address) *Miner {
if network, _ := i.consensus.NetworkType.AddressNetwork(); network != addr.Network {
return nil
}
spendPub, viewPub := addr.SpendPublicKey().AsBytes(), addr.ViewPublicKey().AsBytes()
if rows, err := i.statements.GetMinerByAddress.Query(&spendPub, &viewPub); err != nil {
return nil
} else {
defer rows.Close()
return i.scanMiner(rows)
}
}
func (i *Index) GetMinerByPackedAddress(addr address.PackedAddress) *Miner {
if rows, err := i.statements.GetMinerByAddress.Query(&addr[0], &addr[1]); err != nil {
return nil
} else {
defer rows.Close()
return i.scanMiner(rows)
}
}
func (i *Index) GetOrCreateMinerByAddress(addr *address.Address) *Miner {
if m := i.GetMinerByAddress(addr); m != nil {
return m
} else {
spendPub, viewPub := addr.SpendPublicKey().AsSlice(), addr.ViewPublicKey().AsSlice()
if rows, err := i.statements.InsertMiner.Query(&spendPub, &viewPub); err != nil {
return nil
} else {
defer rows.Close()
return i.scanMiner(rows)
}
}
}
func (i *Index) GetOrCreateMinerPackedAddress(addr address.PackedAddress) *Miner {
if m := i.GetMinerByPackedAddress(addr); m != nil {
return m
} else {
if rows, err := i.statements.InsertMiner.Query(&addr[0], &addr[1]); err != nil {
return nil
} else {
defer rows.Close()
return i.scanMiner(rows)
}
}
}
func (i *Index) SetMinerAlias(minerId uint64, alias string) error {
miner := i.GetMiner(minerId)
if miner == nil {
return nil
}
if alias == "" {
if err := i.Query("UPDATE miners SET alias = NULL WHERE id = $1;", nil, miner.Id()); err != nil {
return err
}
miner.alias.String = ""
miner.alias.Valid = false
} else {
if err := i.Query("UPDATE miners SET alias = $2 WHERE id = $1;", nil, miner.Id(), alias); err != nil {
return err
}
miner.alias.String = alias
miner.alias.Valid = true
}
return nil
}
func (i *Index) GetView(k string) string {
return i.views[k]
}
func (i *Index) Query(query string, callback func(row RowScanInterface) error, params ...any) error {
var parentError error
if stmt, err := i.handle.Prepare(query); err != nil {
parentError = err
} else {
defer stmt.Close()
parentError = i.QueryStatement(stmt, callback, params...)
}
return parentError
}
func (i *Index) QueryStatement(stmt *sql.Stmt, callback func(row RowScanInterface) error, params ...any) error {
if rows, err := stmt.Query(params...); err != nil {
return err
} else {
defer rows.Close()
for callback != nil && rows.Next() {
//callback will call sql.Rows.Scan
if err = callback(rows); err != nil {
return err
}
}
return nil
}
}
func (i *Index) PrepareSideBlocksByQueryStatement(where string) (stmt *sql.Stmt, err error) {
return i.handle.Prepare(fmt.Sprintf("SELECT "+SideBlockSelectFields+" FROM side_blocks %s;", where))
}
func (i *Index) GetSideBlocksByQuery(where string, params ...any) (QueryIterator[SideBlock], error) {
if stmt, err := i.PrepareSideBlocksByQueryStatement(where); err != nil {
return nil, err
} else {
return i.getSideBlocksByQueryStatement(stmt, params...)
}
}
func (i *Index) getSideBlocksByQueryStatement(stmt *sql.Stmt, params ...any) (QueryIterator[SideBlock], error) {
if r, err := queryStatement[SideBlock](i, stmt, params...); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
func (i *Index) GetSideBlocksByQueryStatement(stmt *sql.Stmt, params ...any) (QueryIterator[SideBlock], error) {
if r, err := queryStatement[SideBlock](i, stmt, params...); err != nil {
return nil, err
} else {
return r, nil
}
}
func (i *Index) PrepareMainBlocksByQueryStatement(where string) (stmt *sql.Stmt, err error) {
return i.handle.Prepare(fmt.Sprintf("SELECT "+MainBlockSelectFields+" FROM main_blocks %s;", where))
}
func (i *Index) GetShares(limit, minerId uint64, onlyBlocks bool, inclusion BlockInclusion) (QueryIterator[SideBlock], error) {
if limit == 0 {
if minerId != 0 {
if onlyBlocks {
return i.GetSideBlocksByQuery("WHERE miner = $1 AND uncle_of IS NULL AND inclusion = $2 ORDER BY side_height DESC;", minerId, inclusion)
} else {
return i.GetSideBlocksByQuery("WHERE miner = $1 AND inclusion = $2 ORDER BY side_height DESC, timestamp DESC;", minerId, inclusion)
}
} else {
if onlyBlocks {
return i.GetSideBlocksByQuery("WHERE uncle_of IS NULL AND inclusion = $1 ORDER BY side_height DESC;", inclusion)
} else {
return i.GetSideBlocksByQuery("ORDER BY side_height AND inclusion = $1 DESC, timestamp DESC;", inclusion)
}
}
} else {
if minerId != 0 {
if onlyBlocks {
return i.GetSideBlocksByQuery("WHERE miner = $1 AND uncle_of IS NULL AND inclusion = $3 ORDER BY side_height DESC LIMIT $2;", minerId, limit, inclusion)
} else {
return i.GetSideBlocksByQuery("WHERE miner = $1 AND inclusion = $3 ORDER BY side_height DESC, timestamp DESC LIMIT $2;", minerId, limit, inclusion)
}
} else {
if onlyBlocks {
return i.GetSideBlocksByQuery("WHERE uncle_of IS NULL AND inclusion = $2 ORDER BY side_height DESC LIMIT $1;", limit, inclusion)
} else {
return i.GetSideBlocksByQuery("WHERE inclusion = $2 ORDER BY side_height DESC, timestamp DESC LIMIT $1;", limit, inclusion)
}
}
}
}
func (i *Index) GetFoundBlocks(where string, limit uint64, params ...any) (QueryIterator[FoundBlock], error) {
if stmt, err := i.handle.Prepare(fmt.Sprintf("SELECT * FROM "+i.views["found_main_blocks"]+" %s ORDER BY main_height DESC LIMIT %d;", where, limit)); err != nil {
return nil, err
} else {
if r, err := queryStatement[FoundBlock](i, stmt, params...); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
}
func (i *Index) GetMainBlocksByQuery(where string, params ...any) (QueryIterator[MainBlock], error) {
if stmt, err := i.PrepareMainBlocksByQueryStatement(where); err != nil {
return nil, err
} else {
return i.getMainBlocksByQueryStatement(stmt, params...)
}
}
func (i *Index) getMainBlocksByQueryStatement(stmt *sql.Stmt, params ...any) (QueryIterator[MainBlock], error) {
if r, err := queryStatement[MainBlock](i, stmt, params...); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
func (i *Index) GetMainBlocksByQueryStatement(stmt *sql.Stmt, params ...any) (QueryIterator[MainBlock], error) {
if r, err := queryStatement[MainBlock](i, stmt, params...); err != nil {
return nil, err
} else {
return r, nil
}
}
func (i *Index) GetMainBlockById(id types.Hash) (b *MainBlock) {
if r, err := i.GetMainBlocksByQueryStatement(i.statements.GetMainBlockById, id[:]); err != nil {
utils.Print(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Print(r.Err())
}
}
return b
}
func (i *Index) GetMainBlockTip() (b *MainBlock) {
if r, err := i.GetMainBlocksByQueryStatement(i.statements.TipMainBlock); err != nil {
utils.Error(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Error(r.Err())
}
}
return b
}
func (i *Index) GetMainBlockByCoinbaseId(id types.Hash) (b *MainBlock) {
if r, err := i.GetMainBlocksByQuery("WHERE coinbase_id = $1;", id[:]); err != nil {
utils.Error(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Error(r.Err())
}
}
return b
}
func (i *Index) GetMainBlockByGlobalOutputIndex(globalOutputIndex uint64) (b *MainBlock) {
if r, err := i.GetMainBlocksByQuery("WHERE coinbase_id = (SELECT id FROM main_coinbase_outputs WHERE global_output_index = $1 LIMIT 1);", globalOutputIndex); err != nil {
utils.Error(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Error(r.Err())
}
}
return b
}
func (i *Index) GetMainBlockByHeight(height uint64) (b *MainBlock) {
if r, err := i.GetMainBlocksByQueryStatement(i.statements.GetMainBlockByHeight, height); err != nil {
utils.Error(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Error(r.Err())
}
}
return b
}
func (i *Index) GetSideBlockByMainId(id types.Hash) (b *SideBlock) {
if r, err := i.GetSideBlocksByQueryStatement(i.statements.GetSideBlockByMainId, id[:]); err != nil {
utils.Error(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Error(r.Err())
}
}
return b
}
func (i *Index) GetSideBlocksByTemplateId(id types.Hash) QueryIterator[SideBlock] {
if r, err := i.GetSideBlocksByQuery("WHERE template_id = $1;", id[:]); err != nil {
utils.Error(err)
} else {
return r
}
return nil
}
func (i *Index) GetSideBlocksByUncleOfId(id types.Hash) QueryIterator[SideBlock] {
if r, err := i.GetSideBlocksByQueryStatement(i.statements.GetSideBlockByUncleId, id[:]); err != nil {
utils.Error(err)
} else {
return r
}
return nil
}
func (i *Index) GetTipSideBlockByTemplateId(id types.Hash) (b *SideBlock) {
if r, err := i.GetSideBlocksByQueryStatement(i.statements.TipSideBlocksTemplateId, id[:], InclusionInVerifiedChain); err != nil {
utils.Error(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Error(r.Err())
}
}
return b
}
func (i *Index) GetSideBlocksByMainHeight(height uint64) QueryIterator[SideBlock] {
if r, err := i.GetSideBlocksByQuery("WHERE main_height = $1;", height); err != nil {
utils.Error(err)
} else {
return r
}
return nil
}
func (i *Index) GetSideBlocksByHeight(height uint64) QueryIterator[SideBlock] {
if r, err := i.GetSideBlocksByQuery("WHERE side_height = $1;", height); err != nil {
utils.Error(err)
} else {
return r
}
return nil
}
func (i *Index) GetTipSideBlockByHeight(height uint64) (b *SideBlock) {
if r, err := i.GetSideBlocksByQuery("WHERE side_height = $1 AND effective_height = $2 AND inclusion = $3;", height, height, InclusionInVerifiedChain); err != nil {
utils.Error(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Error(r.Err())
}
}
return b
}
func (i *Index) GetSideBlockTip() (b *SideBlock) {
if r, err := i.GetSideBlocksByQueryStatement(i.statements.TipSideBlock, InclusionInVerifiedChain); err != nil {
utils.Error(err)
} else {
defer r.Close()
if _, b = r.Next(); b == nil && r.Err() != nil {
utils.Error(r.Err())
}
}
return b
}
func (i *Index) GetSideBlocksInPPLNSWindow(tip *SideBlock) QueryIterator[SideBlock] {
return i.GetSideBlocksInWindow(tip.SideHeight, uint64(tip.WindowDepth))
}
func (i *Index) GetSideBlocksInWindow(startHeight, windowSize uint64) QueryIterator[SideBlock] {
if startHeight < windowSize {
windowSize = startHeight
}
if r, err := i.GetSideBlocksByQuery("WHERE effective_height <= $1 AND effective_height > $2 AND inclusion = $3 ORDER BY effective_height DESC, side_height DESC;", startHeight, startHeight-windowSize, InclusionInVerifiedChain); err != nil {
utils.Error(err)
} else {
return r
}
return nil
}
func (i *Index) GetSideBlocksByMinerIdInWindow(minerId, startHeight, windowSize uint64) QueryIterator[SideBlock] {
if startHeight < windowSize {
windowSize = startHeight
}
if r, err := i.GetSideBlocksByQuery("WHERE miner = $1 AND effective_height <= $2 AND effective_height > $3 AND inclusion = $4 ORDER BY effective_height DESC, side_height DESC;", minerId, startHeight, startHeight-windowSize, InclusionInVerifiedChain); err != nil {
utils.Error(err)
} else {
return r
}
return nil
}
func (i *Index) InsertOrUpdateSideBlock(b *SideBlock) error {
if b.IsTipOfHeight() {
if oldBlock := i.GetTipSideBlockByHeight(b.SideHeight); oldBlock != nil {
if oldBlock.MainId != b.MainId {
//conflict resolution, change other block status
if oldBlock.TemplateId == oldBlock.TemplateId {
oldBlock.Inclusion = InclusionAlternateInVerifiedChain
} else {
//mark as orphan if templates don't match. if uncle it will be adjusted later
oldBlock.Inclusion = InclusionOrphan
}
if err := i.InsertOrUpdateSideBlock(oldBlock); err != nil {
return err
}
}
}
}
var parentId any = &b.ParentTemplateId
if b.SideHeight == 0 {
parentId = types.ZeroHash[:]
}
return i.QueryStatement(
i.statements.InsertOrUpdateSideBlock,
nil,
&b.MainId,
b.MainHeight,
&b.TemplateId,
b.SideHeight,
parentId,
b.Miner,
&b.UncleOf,
b.EffectiveHeight,
b.Nonce,
b.ExtraNonce,
b.Timestamp,
b.SoftwareId,
b.SoftwareVersion,
b.WindowDepth,
b.WindowOutputs,
b.TransactionCount,
b.Difficulty,
&b.CumulativeDifficulty,
b.PowDifficulty,
&b.PowHash,
b.Inclusion,
)
}
func (i *Index) InsertOrUpdateMainBlock(b *MainBlock) error {
if oldBlock := i.GetMainBlockByHeight(b.Height); oldBlock != nil {
if oldBlock.Id != b.Id {
//conflict resolution
if tx, err := i.handle.BeginTx(context.Background(), nil); err != nil {
return err
} else {
defer tx.Rollback()
if _, err := tx.Exec("DELETE FROM main_coinbase_outputs WHERE id = $1;", oldBlock.CoinbaseId[:]); err != nil {
return err
}
if _, err = tx.Exec("DELETE FROM main_blocks WHERE id = $1;", oldBlock.Id[:]); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
}
}
}
metadataJson, _ := utils.MarshalJSON(b.Metadata)
if tx, err := i.handle.BeginTx(context.Background(), nil); err != nil {
return err
} else {
defer tx.Rollback()
if _, err := tx.Exec(
"INSERT INTO main_blocks (id, height, timestamp, reward, coinbase_id, difficulty, metadata, side_template_id, coinbase_private_key) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9) ON CONFLICT (id) DO UPDATE SET metadata = $7, side_template_id = $8, coinbase_private_key = $9;",
b.Id[:],
b.Height,
b.Timestamp,
b.Reward,
b.CoinbaseId[:],
b.Difficulty,
metadataJson,
&b.SideTemplateId,
&b.CoinbasePrivateKey,
); err != nil {
return err
}
return tx.Commit()
}
}
func (i *Index) GetPayoutsByMinerId(minerId uint64, limit uint64) (r QueryIterator[Payout], err error) {
var stmt *sql.Stmt
var params []any
if limit == 0 {
if stmt, err = i.handle.Prepare("SELECT * FROM " + i.views["payouts"] + " WHERE miner = $1 ORDER BY main_height DESC;"); err != nil {
return nil, err
}
params = []any{minerId}
} else {
if stmt, err = i.handle.Prepare("SELECT * FROM " + i.views["payouts"] + " WHERE miner = $1 ORDER BY main_height DESC LIMIT $2;"); err != nil {
return nil, err
}
params = []any{minerId, limit}
}
if r, err := queryStatement[Payout](i, stmt, params...); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
func (i *Index) GetPayoutsByMinerIdFromHeight(minerId uint64, height uint64) (QueryIterator[Payout], error) {
if stmt, err := i.handle.Prepare("SELECT * FROM " + i.views["payouts"] + " WHERE miner = $1 AND main_height >= $2 ORDER BY main_height DESC;"); err != nil {
return nil, err
} else {
if r, err := queryStatement[Payout](i, stmt, minerId, height); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
}
func (i *Index) GetPayoutsByMinerIdFromTimestamp(minerId uint64, timestamp uint64) (QueryIterator[Payout], error) {
if stmt, err := i.handle.Prepare("SELECT * FROM " + i.views["payouts"] + " WHERE miner = $1 AND timestamp >= $2 ORDER BY main_height DESC;"); err != nil {
return nil, err
} else {
if r, err := queryStatement[Payout](i, stmt, minerId, timestamp); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
}
func (i *Index) GetPayoutsBySideBlock(b *SideBlock) (QueryIterator[Payout], error) {
if stmt, err := i.handle.Prepare("SELECT * FROM " + i.views["payouts"] + " WHERE miner = $1 AND ((side_height >= $2 AND including_height <= $2) OR main_id = $3) ORDER BY main_height DESC;"); err != nil {
return nil, err
} else {
if r, err := queryStatement[Payout](i, stmt, b.Miner, b.EffectiveHeight, &b.MainId); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
}
func (i *Index) GetMainCoinbaseOutputs(coinbaseId types.Hash) (QueryIterator[MainCoinbaseOutput], error) {
if stmt, err := i.handle.Prepare("SELECT " + MainCoinbaseOutputSelectFields + " FROM main_coinbase_outputs WHERE id = $1 ORDER BY index ASC;"); err != nil {
return nil, err
} else {
if r, err := queryStatement[MainCoinbaseOutput](i, stmt, coinbaseId[:]); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
}
func (i *Index) GetMainCoinbaseOutputByIndex(coinbaseId types.Hash, index uint64) (o *MainCoinbaseOutput) {
if stmt, err := i.handle.Prepare("SELECT " + MainCoinbaseOutputSelectFields + " FROM main_coinbase_outputs WHERE id = $1 AND index = $2 ORDER BY index ASC;"); err != nil {
utils.Error(err)
return nil
} else {
if r, err := queryStatement[MainCoinbaseOutput](i, stmt, coinbaseId[:], index); err != nil {
utils.Error(err)
return nil
} else {
defer r.Close()
r.closer = func() {
stmt.Close()
}
defer r.Close()
if _, o = r.Next(); o == nil && r.Err() != nil {
utils.Error(r.Err())
}
return o
}
}
}
func (i *Index) GetMainCoinbaseOutputByGlobalOutputIndex(globalOutputIndex uint64) (o *MainCoinbaseOutput) {
if stmt, err := i.handle.Prepare("SELECT " + MainCoinbaseOutputSelectFields + " FROM main_coinbase_outputs WHERE global_output_index = $1 ORDER BY index ASC;"); err != nil {
utils.Error(err)
return nil
} else {
if r, err := queryStatement[MainCoinbaseOutput](i, stmt, globalOutputIndex); err != nil {
utils.Error(err)
return nil
} else {
defer r.Close()
r.closer = func() {
stmt.Close()
}
defer r.Close()
if _, o = r.Next(); o == nil && r.Err() != nil {
utils.Error(r.Err())
}
return o
}
}
}
func (i *Index) GetMainLikelySweepTransactions(limit uint64) (r QueryIterator[MainLikelySweepTransaction], err error) {
var stmt *sql.Stmt
var params []any
if limit > 0 {
if stmt, err = i.handle.Prepare("SELECT " + MainLikelySweepTransactionSelectFields + " FROM main_likely_sweep_transactions ORDER BY timestamp DESC LIMIT $1;"); err != nil {
return nil, err
}
params = []any{limit}
} else {
if stmt, err = i.handle.Prepare("SELECT " + MainLikelySweepTransactionSelectFields + " FROM main_likely_sweep_transactions ORDER BY timestamp DESC;"); err != nil {
return nil, err
}
params = []any{}
}
if r, err := queryStatement[MainLikelySweepTransaction](i, stmt, params...); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
func (i *Index) GetMainLikelySweepTransactionsByAddress(addr *address.Address, limit uint64) (r QueryIterator[MainLikelySweepTransaction], err error) {
var stmt *sql.Stmt
var params []any
spendPub, viewPub := addr.SpendPublicKey().AsSlice(), addr.ViewPublicKey().AsSlice()
if limit > 0 {
if stmt, err = i.handle.Prepare("SELECT " + MainLikelySweepTransactionSelectFields + " FROM main_likely_sweep_transactions WHERE miner_spend_public_key = $1 AND miner_view_public_key = $2 ORDER BY timestamp DESC LIMIT $3;"); err != nil {
return nil, err
}
params = []any{&spendPub, &viewPub, limit}
} else {
if stmt, err = i.handle.Prepare("SELECT " + MainLikelySweepTransactionSelectFields + " FROM main_likely_sweep_transactions WHERE miner_spend_public_key = $1 AND miner_view_public_key = $2 ORDER BY timestamp DESC;"); err != nil {
return nil, err
}
params = []any{&spendPub, &viewPub}
}
if r, err := queryStatement[MainLikelySweepTransaction](i, stmt, params...); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
type TransactionInputQueryResult struct {
Input client.TransactionInput `json:"input"`
MatchedOutputs []*MatchedOutput `json:"matched_outputs"`
}
type MatchedOutput struct {
Coinbase *MainCoinbaseOutput `json:"coinbase,omitempty"`
Sweep *MainLikelySweepTransaction `json:"sweep,omitempty"`
GlobalOutputIndex uint64 `json:"global_output_index"`
Timestamp uint64 `json:"timestamp"`
Address *address.Address `json:"address"`
}
type MinimalTransactionInputQueryResult struct {
Input client.TransactionInput `json:"input"`
MatchedOutputs []*MinimalMatchedOutput `json:"matched_outputs"`
}
type MinimalMatchedOutput struct {
Coinbase types.Hash `json:"coinbase,omitempty"`
Sweep types.Hash `json:"sweep,omitempty"`
GlobalOutputIndex uint64 `json:"global_output_index"`
Address *address.Address `json:"address"`
}
type MinimalTransactionInputQueryResults []MinimalTransactionInputQueryResult
type TransactionInputQueryResults []TransactionInputQueryResult
type TransactionInputQueryResultsMatch struct {
Address *address.Address `json:"address"`
Count uint64 `json:"count"`
SweepCount uint64 `json:"sweep_count"`
CoinbaseCount uint64 `json:"coinbase_count"`
CoinbaseAmount uint64 `json:"coinbase_amount"`
}
type TransactionInputQueryResultsMatches []TransactionInputQueryResultsMatch
func (r TransactionInputQueryResults) Match() (result TransactionInputQueryResultsMatches) {
//cannot have more than one of same miner outputs valid per input
//no miner outputs in whole input doesn't count
//cannot take vsame exact miner outputs on different inputs
//TODO
var zeroAddress address.PackedAddress
miners := make(map[address.PackedAddress]*TransactionInputQueryResultsMatch)
miners[zeroAddress] = &TransactionInputQueryResultsMatch{
Address: nil,
Count: 0,
SweepCount: 0,
CoinbaseCount: 0,
CoinbaseAmount: 0,
}
//TODO: handle same miner multiple times in same decoy
for _, matchResult := range r {
for _, o := range matchResult.MatchedOutputs {
if o != nil {
pA := o.Address.ToPackedAddress()
if _, ok := miners[pA]; !ok {
miners[pA] = &TransactionInputQueryResultsMatch{
Address: o.Address,
Count: 0,
SweepCount: 0,
CoinbaseCount: 0,
CoinbaseAmount: 0,
}
}
if o.Coinbase != nil {
miners[pA].CoinbaseCount++
miners[pA].CoinbaseAmount += o.Coinbase.Value
} else if o.Sweep != nil {
miners[pA].SweepCount++
}
miners[pA].Count++
} else {
miners[zeroAddress].Count++
}
}
}
result = make([]TransactionInputQueryResultsMatch, 0, len(miners))
for _, v := range miners {
result = append(result, *v)
}
slices.SortFunc(result, func(a, b TransactionInputQueryResultsMatch) int {
return int(b.Count) - int(a.Count)
})
return result
}
func (i *Index) QueryTransactionInputs(inputs []client.TransactionInput) TransactionInputQueryResults {
result := make(TransactionInputQueryResults, len(inputs))
for index, input := range inputs {
result[index].Input = input
result[index].MatchedOutputs = make([]*MatchedOutput, len(input.KeyOffsets))
if input.Amount != 0 {
continue
}
result[index].MatchedOutputs = i.QueryGlobalOutputIndices(input.KeyOffsets)
}
return result
}
func (i *Index) QueryGlobalOutputIndices(indices []uint64) []*MatchedOutput {
result := make([]*MatchedOutput, len(indices))
if err := i.Query("SELECT "+MainCoinbaseOutputSelectFields+" FROM main_coinbase_outputs WHERE global_output_index = ANY($1) ORDER BY index ASC;", func(row RowScanInterface) error {
var o MainCoinbaseOutput
if err := o.ScanFromRow(i.Consensus(), row); err != nil {
return err
}
if index := slices.Index(indices, o.GlobalOutputIndex); index != -1 {
result[index] = &MatchedOutput{
Coinbase: &o,
Sweep: nil,
GlobalOutputIndex: o.GlobalOutputIndex,
Address: i.GetMiner(o.Miner).Address(),
}
if mb := i.GetMainBlockByCoinbaseId(o.Id); mb != nil {
result[index].Timestamp = mb.Timestamp
}
}
return nil
}, pq.Array(indices)); err != nil {
return nil
}
if err := i.Query("SELECT "+MainLikelySweepTransactionSelectFields+" FROM main_likely_sweep_transactions WHERE $1::bigint[] && global_output_indices ORDER BY timestamp ASC;", func(row RowScanInterface) error {
var tx MainLikelySweepTransaction
if err := tx.ScanFromRow(i.Consensus(), row); err != nil {
return err
}
for _, globalOutputIndex := range tx.GlobalOutputIndices {
// fill all possible indices
if index := slices.Index(indices, globalOutputIndex); index != -1 {
if result[index] == nil {
result[index] = &MatchedOutput{
Coinbase: nil,
Sweep: &tx,
GlobalOutputIndex: globalOutputIndex,
Timestamp: tx.Timestamp,
Address: tx.Address,
}
}
}
}
return nil
}, pq.Array(indices)); err != nil {
return nil
}
return result
}
func (i *Index) GetMainLikelySweepTransactionBySpendingGlobalOutputIndices(globalOutputIndices ...uint64) [][]*MainLikelySweepTransaction {
entries := make([][]*MainLikelySweepTransaction, len(globalOutputIndices))
if err := i.Query("SELECT "+MainLikelySweepTransactionSelectFields+" FROM main_likely_sweep_transactions WHERE $1::bigint[] && spending_output_indices ORDER BY timestamp ASC;", func(row RowScanInterface) error {
var tx MainLikelySweepTransaction
if err := tx.ScanFromRow(i.Consensus(), row); err != nil {
return err
}
for _, globalOutputIndex := range tx.SpendingOutputIndices {
// fill all possible indices
if index := slices.Index(globalOutputIndices, globalOutputIndex); index != -1 {
entries[index] = append(entries[index], &tx)
}
}
return nil
}, pq.Array(globalOutputIndices)); err != nil {
return nil
}
return entries
}
func (i *Index) GetMainLikelySweepTransactionByGlobalOutputIndices(globalOutputIndices ...uint64) []*MainLikelySweepTransaction {
entries := make([]*MainLikelySweepTransaction, len(globalOutputIndices))
if err := i.Query("SELECT "+MainLikelySweepTransactionSelectFields+" FROM main_likely_sweep_transactions WHERE $1::bigint[] && global_output_indices ORDER BY timestamp ASC;", func(row RowScanInterface) error {
var tx MainLikelySweepTransaction
if err := tx.ScanFromRow(i.Consensus(), row); err != nil {
return err
}
for _, globalOutputIndex := range tx.GlobalOutputIndices {
// fill all possible indices
if index := slices.Index(globalOutputIndices, globalOutputIndex); index != -1 {
if entries[index] == nil {
entries[index] = &tx
}
}
}
return nil
}, pq.Array(globalOutputIndices)); err != nil {
return nil
}
return entries
}
func (i *Index) InsertOrUpdateMainLikelySweepTransaction(t *MainLikelySweepTransaction) error {
resultJson, _ := utils.MarshalJSON(t.Result)
matchJson, _ := utils.MarshalJSON(t.Match)
spendPub, viewPub := t.Address.SpendPublicKey().AsSlice(), t.Address.ViewPublicKey().AsSlice()
if _, err := i.handle.Exec(
"INSERT INTO main_likely_sweep_transactions (id, timestamp, result, match, value, spending_output_indices, global_output_indices, input_count, input_decoy_count, miner_count, other_miners_count, no_miner_count, miner_ratio, other_miners_ratio, no_miner_ratio, miner_spend_public_key, miner_view_public_key) VALUES ($1, $2, $3, $4, $5, $6::bigint[], $7::bigint[], $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) ON CONFLICT (id) DO UPDATE SET result = $3, match = $4, value = $5, miner_count = $10, other_miners_count = $11, no_miner_count = $12, miner_ratio = $13, other_miners_ratio = $14, no_miner_ratio = $15, miner_spend_public_key = $16, miner_view_public_key = $17;",
t.Id[:],
t.Timestamp,
resultJson,
matchJson,
t.Value,
pq.Array(t.SpendingOutputIndices),
pq.Array(t.GlobalOutputIndices),
t.InputCount,
t.InputDecoyCount,
t.MinerCount,
t.OtherMinersCount,
t.NoMinerCount,
t.MinerRatio,
t.OtherMinersRatio,
t.NoMinerRatio,
&spendPub,
&viewPub,
); err != nil {
return err
}
return nil
}
func (i *Index) GetMainCoinbaseOutputByMinerId(coinbaseId types.Hash, minerId uint64) *MainCoinbaseOutput {
var output MainCoinbaseOutput
if err := i.Query("SELECT "+MainCoinbaseOutputSelectFields+" FROM main_coinbase_outputs WHERE id = $1 AND miner = $2 ORDER BY index DESC;", func(row RowScanInterface) error {
if err := output.ScanFromRow(i.Consensus(), row); err != nil {
return err
}
return nil
}, coinbaseId[:], minerId); err != nil {
return nil
}
if output.Id == types.ZeroHash {
return nil
}
return &output
}
func (i *Index) InsertOrUpdateMainCoinbaseOutputs(outputs MainCoinbaseOutputs) error {
if len(outputs) == 0 {
return nil
}
for ix, o := range outputs[1:] {
if outputs[ix].Id != o.Id {
return errors.New("differing coinbase ids")
}
}
if tx, err := i.handle.BeginTx(context.Background(), nil); err != nil {
return err
} else {
defer tx.Rollback()
for _, o := range outputs {
if _, err := tx.Exec(
"INSERT INTO main_coinbase_outputs (id, index, global_output_index, miner, value) VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING;",
o.Id[:],
o.Index,
o.GlobalOutputIndex,
o.Miner,
o.Value,
); err != nil {
return err
}
}
return tx.Commit()
}
}
func (i *Index) Close() error {
//cleanup statements
v := reflect.ValueOf(i.statements)
for ix := 0; ix < v.NumField(); ix++ {
if stmt, ok := v.Field(ix).Interface().(*sql.Stmt); ok && stmt != nil {
//v.Field(i).Elem().Set(reflect.ValueOf((*sql.Stmt)(nil)))
stmt.Close()
}
}
return i.handle.Close()
}
func (i *Index) scanMiner(rows *sql.Rows) *Miner {
if rows.Next() {
m := &Miner{}
if m.ScanFromRow(i.Consensus(), rows) == nil {
return m
}
}
return nil
}
func (i *Index) GetSideBlockFromPoolBlock(b *sidechain.PoolBlock, inclusion BlockInclusion) (tip *SideBlock, uncles []*SideBlock, err error) {
if err = i.preProcessPoolBlock(b); err != nil {
return nil, nil, err
}
tip = &SideBlock{}
if err = tip.FromPoolBlock(i, b, i.GetSeedByHeight); err != nil {
return nil, nil, err
}
tip.EffectiveHeight = tip.SideHeight
tip.Inclusion = inclusion
if bottomHeight, err := sidechain.BlocksInPPLNSWindow(b, i.consensus, i.GetDifficultyByHeight, i.GetByTemplateId, func(b *sidechain.PoolBlock, weight types.Difficulty) {
}); err != nil {
// unknown
tip.WindowDepth = 0
} else {
tip.WindowDepth = uint32(tip.SideHeight - bottomHeight + 1)
}
for _, u := range b.Side.Uncles {
uncleBlock := i.GetTipSideBlockByTemplateId(u)
uncle := i.GetByTemplateId(u)
if err = i.preProcessPoolBlock(uncle); err != nil {
return nil, nil, err
}
if uncleBlock == nil && uncle != nil {
uncleBlock = &SideBlock{}
if err = uncleBlock.FromPoolBlock(i, uncle, i.GetSeedByHeight); err != nil {
return nil, nil, err
}
if tip.Inclusion == InclusionOrphan {
uncleBlock.Inclusion = InclusionOrphan
} else if tip.Inclusion == InclusionInVerifiedChain || tip.Inclusion == InclusionAlternateInVerifiedChain {
uncleBlock.Inclusion = InclusionInVerifiedChain
}
}
if uncleBlock == nil || uncle == nil {
return nil, nil, errors.New("nil uncle")
}
if tip.Inclusion == InclusionInVerifiedChain || tip.Inclusion == InclusionAlternateInVerifiedChain {
uncleBlock.UncleOf = tip.TemplateId
uncleBlock.EffectiveHeight = tip.EffectiveHeight
uncleBlock.Inclusion = InclusionInVerifiedChain
}
if uncleBottomHeight, err := sidechain.BlocksInPPLNSWindow(uncle, i.consensus, i.GetDifficultyByHeight, i.GetByTemplateId, func(b *sidechain.PoolBlock, weight types.Difficulty) {
}); err != nil {
// unknown
uncleBlock.WindowDepth = 0
} else {
uncleBlock.WindowDepth = uint32(uncleBlock.SideHeight - uncleBottomHeight + 1)
}
uncles = append(uncles, uncleBlock)
}
return tip, uncles, nil
}
func (i *Index) preProcessPoolBlock(b *sidechain.PoolBlock) error {
if b == nil {
return errors.New("nil block")
}
var preAllocatedShares sidechain.Shares
if len(b.Main.Coinbase.Outputs) == 0 {
//cannot use SideTemplateId() as it might not be proper to calculate yet. fetch from coinbase only here
if b2 := i.GetByTemplateId(types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId))); b2 != nil && len(b2.Main.Coinbase.Outputs) != 0 {
b.Main.Coinbase.Outputs = b2.Main.Coinbase.Outputs
} else {
preAllocatedShares = sidechain.PreAllocateShares(i.consensus.ChainWindowSize * 2)
}
}
_, err := b.PreProcessBlock(i.consensus, i.derivationCache, preAllocatedShares, i.GetDifficultyByHeight, i.GetByTemplateId)
return err
}
func (i *Index) DerivationCache() sidechain.DerivationCacheInterface {
return i.derivationCache
}
func (i *Index) InsertOrUpdatePoolBlock(b *sidechain.PoolBlock, inclusion BlockInclusion) error {
sideBlock, sideUncles, err := i.GetSideBlockFromPoolBlock(b, inclusion)
if err != nil {
return err
}
if err := i.InsertOrUpdateSideBlock(sideBlock); err != nil {
return err
}
for _, sideUncle := range sideUncles {
if err := i.InsertOrUpdateSideBlock(sideUncle); err != nil {
return err
}
}
return nil
}
func (i *Index) GetMinerWebHooks(minerId uint64) (QueryIterator[MinerWebHook], error) {
if stmt, err := i.handle.Prepare("SELECT miner, type, url, settings FROM miner_webhooks WHERE miner = $1;"); err != nil {
return nil, err
} else {
if r, err := queryStatement[MinerWebHook](i, stmt, minerId); err != nil {
defer stmt.Close()
return nil, err
} else {
r.closer = func() {
stmt.Close()
}
return r, nil
}
}
}
func (i *Index) DeleteMinerWebHook(minerId uint64, hookType WebHookType) error {
return i.Query("DELETE FROM miner_webhooks WHERE miner = $1 AND type = $2", func(row RowScanInterface) error {
return nil
}, minerId, hookType)
}
func (i *Index) InsertOrUpdateMinerWebHook(w *MinerWebHook) error {
metadataJson, _ := utils.MarshalJSON(w.Settings)
if w.Settings == nil {
metadataJson = []byte{'{', '}'}
}
if tx, err := i.handle.BeginTx(context.Background(), nil); err != nil {
return err
} else {
defer tx.Rollback()
if _, err := tx.Exec(
"INSERT INTO miner_webhooks (miner, type, url, settings) VALUES ($1, $2, $3, $4::jsonb) ON CONFLICT (miner, type) DO UPDATE SET url = $3, settings = $4;",
w.Miner,
w.Type,
w.Url,
metadataJson,
); err != nil {
return err
}
return tx.Commit()
}
}

View file

@ -1,66 +0,0 @@
package index
type IterateFunction[K, V any] func(key K, value V) (stop bool)
type IteratorFunction[K, V any] func(f IterateFunction[K, V]) (complete bool)
type Iterator[K, V any] interface {
All(f IterateFunction[K, V]) (complete bool)
}
func IteratorToIterator[K, V any](i Iterator[K, V], _ ...error) Iterator[K, V] {
return i
}
func IterateToSlice[T any](i Iterator[int, T], _ ...error) (s []T) {
i.All(func(key int, value T) (stop bool) {
s = append(s, value)
return false
})
return s
}
func IterateToSliceWithoutPointer[T any](i Iterator[int, *T], _ ...error) (s []T) {
i.All(func(key int, value *T) (stop bool) {
s = append(s, *value)
return false
})
return s
}
func SliceIterate[S ~[]T, T any](s S, f IterateFunction[int, T]) (complete bool) {
for i, v := range s {
if f(i, v) {
return (len(s) - 1) == i
}
}
return true
}
func ChanIterate[T any](c <-chan T, f IterateFunction[int, T]) (complete bool) {
var i int
for {
v, more := <-c
if !more {
return true
}
if f(i, v) {
return false
}
i++
}
}
func ChanConsume[T any](c <-chan T) {
for range c {
}
}
func ChanToSlice[S ~[]T, T any](c <-chan T) (s S) {
s = make(S, 0, len(c))
for v := range c {
s = append(s, v)
}
return s
}

View file

@ -1,47 +0,0 @@
package index
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
)
const MainBlockSelectFields = "id, height, timestamp, reward, coinbase_id, difficulty, metadata, side_template_id, coinbase_private_key"
type MainBlock struct {
Id types.Hash `json:"id"`
Height uint64 `json:"height"`
Timestamp uint64 `json:"timestamp"`
Reward uint64 `json:"reward"`
CoinbaseId types.Hash `json:"coinbase_id"`
Difficulty uint64 `json:"difficulty"`
// Metadata should be jsonb blob, can be NULL. metadata such as pool ownership, links to other p2pool networks, and other interesting data
Metadata map[string]any `json:"metadata,omitempty"`
// sidechain data for blocks we own
// SideTemplateId can be NULL
SideTemplateId types.Hash `json:"side_template_id,omitempty"`
// CoinbasePrivateKey private key for coinbase outputs we own (all 0x00 = not known, but should have one)
CoinbasePrivateKey crypto.PrivateKeyBytes `json:"coinbase_private_key,omitempty"`
}
func (b *MainBlock) GetMetadata(key string) any {
return b.Metadata[key]
}
func (b *MainBlock) SetMetadata(key string, v any) {
b.Metadata[key] = v
}
func (b *MainBlock) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
var metadataBuf []byte
b.Metadata = make(map[string]any)
if err := row.Scan(&b.Id, &b.Height, &b.Timestamp, &b.Reward, &b.CoinbaseId, &b.Difficulty, &metadataBuf, &b.SideTemplateId, &b.CoinbasePrivateKey); err != nil {
return err
} else if err = utils.UnmarshalJSON(metadataBuf, &b.Metadata); err != nil {
return err
}
return nil
}

View file

@ -1,37 +0,0 @@
package index
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
)
type MainCoinbaseOutputs []MainCoinbaseOutput
const MainCoinbaseOutputSelectFields = "id, index, global_output_index, miner, value"
type MainCoinbaseOutput struct {
// Id coinbase id
Id types.Hash `json:"id"`
// Index transaction output index
Index uint32 `json:"index"`
// Monero global output idx
GlobalOutputIndex uint64 `json:"global_output_index"`
// Miner owner of the output
Miner uint64 `json:"miner"`
Value uint64 `json:"value"`
// Extra information filled just for JSON purposes
MinerAddress *address.Address `json:"miner_address,omitempty"`
MinerAlias string `json:"miner_alias,omitempty"`
}
func (o *MainCoinbaseOutput) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
if err := row.Scan(&o.Id, &o.Index, &o.GlobalOutputIndex, &o.Miner, &o.Value); err != nil {
return err
}
return nil
}

View file

@ -1,66 +0,0 @@
package index
import (
"errors"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/lib/pq"
)
const MainLikelySweepTransactionSelectFields = "id, timestamp, result, match, value, spending_output_indices, global_output_indices, input_count, input_decoy_count, miner_count, other_miners_count, no_miner_count, miner_ratio, other_miners_ratio, no_miner_ratio, miner_spend_public_key, miner_view_public_key"
type MainLikelySweepTransaction struct {
// Id coinbase id
Id types.Hash `json:"id"`
Timestamp uint64 `json:"timestamp"`
Result MinimalTransactionInputQueryResults `json:"result"`
Match []TransactionInputQueryResultsMatch `json:"match"`
Value uint64 `json:"value"`
SpendingOutputIndices []uint64 `json:"spending_output_indices"`
GlobalOutputIndices []uint64 `json:"global_output_indices"`
InputCount int `json:"input_count"`
InputDecoyCount int `json:"input_decoy_count"`
MinerCount int `json:"miner_count"`
OtherMinersCount int `json:"other_miners_count"`
NoMinerCount int `json:"no_miner_count"`
MinerRatio float32 `json:"miner_ratio"`
OtherMinersRatio float32 `json:"other_miners_ratio"`
NoMinerRatio float32 `json:"no_miner_ratio"`
Address *address.Address `json:"address"`
}
func (t *MainLikelySweepTransaction) ScanFromRow(consensus *sidechain.Consensus, row RowScanInterface) error {
var spendPub, viewPub crypto.PublicKeyBytes
var resultBuf, matchBuf []byte
var spendingOutputIndices, globalOutputIndices pq.Int64Array
if err := row.Scan(&t.Id, &t.Timestamp, &resultBuf, &matchBuf, &t.Value, &spendingOutputIndices, &globalOutputIndices, &t.InputCount, &t.InputDecoyCount, &t.MinerCount, &t.OtherMinersCount, &t.NoMinerCount, &t.MinerRatio, &t.OtherMinersRatio, &t.NoMinerRatio, &spendPub, &viewPub); err != nil {
return err
} else if err = utils.UnmarshalJSON(resultBuf, &t.Result); err != nil {
return err
} else if err = utils.UnmarshalJSON(resultBuf, &t.Match); err != nil {
return err
}
t.SpendingOutputIndices = make([]uint64, len(spendingOutputIndices))
for j, ix := range spendingOutputIndices {
t.SpendingOutputIndices[j] = uint64(ix)
}
t.GlobalOutputIndices = make([]uint64, len(globalOutputIndices))
for j, ix := range globalOutputIndices {
t.GlobalOutputIndices[j] = uint64(ix)
}
network, err := consensus.NetworkType.AddressNetwork()
if err != nil {
return errors.New("unknown network type")
}
t.Address = address.FromRawAddress(network, &spendPub, &viewPub)
return nil
}

View file

@ -1,45 +0,0 @@
package index
import (
"database/sql"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
)
const MinerSelectFields = "id, alias, spend_public_key, view_public_key"
type Miner struct {
id uint64
addr address.Address
alias sql.NullString
}
func (m *Miner) Id() uint64 {
return m.id
}
func (m *Miner) Alias() string {
if m.alias.Valid {
return m.alias.String
}
return ""
}
func (m *Miner) Address() *address.Address {
return &m.addr
}
func (m *Miner) ScanFromRow(consensus *sidechain.Consensus, row RowScanInterface) error {
var spendPub, viewPub crypto.PublicKeyBytes
if err := row.Scan(&m.id, &m.alias, &spendPub, &viewPub); err != nil {
return err
}
network, err := consensus.NetworkType.AddressNetwork()
if err != nil {
return err
}
m.addr = *address.FromRawAddress(network, &spendPub, &viewPub)
return nil
}

View file

@ -1,124 +0,0 @@
package index
import (
"errors"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"net/url"
"slices"
"strings"
)
type MinerWebHook struct {
Miner uint64 `json:"miner"`
Type WebHookType `json:"type"`
Url string `json:"url"`
Settings map[string]string `json:"settings"`
Consensus *sidechain.Consensus `json:"-"`
}
type WebHookType string
const (
WebHookSlack WebHookType = "slack"
WebHookDiscord WebHookType = "discord"
WebHookTelegram WebHookType = "telegram"
WebHookMatrixHookshot WebHookType = "matrix-hookshot"
WebHookCustom WebHookType = "custom"
)
var disallowedCustomHookHosts = []string{
"hooks.slack.com",
"discord.com",
"api.telegram.org",
}
var disallowedCustomHookPorts = []string{}
func (w *MinerWebHook) ScanFromRow(consensus *sidechain.Consensus, row RowScanInterface) error {
var settingsBuf []byte
w.Consensus = consensus
w.Settings = make(map[string]string)
if err := row.Scan(&w.Miner, &w.Type, &w.Url, &settingsBuf); err != nil {
return err
} else if err = utils.UnmarshalJSON(settingsBuf, &w.Settings); err != nil {
return err
}
return nil
}
func (w *MinerWebHook) Verify() error {
uri, err := url.Parse(w.Url)
if err != nil {
return err
}
switch w.Type {
case WebHookSlack:
if uri.Scheme != "https" {
return errors.New("invalid URL scheme, expected https")
}
if uri.Host != "hooks.slack.com" {
return errors.New("invalid hook host, expected hooks.slack.com")
}
if uri.Port() != "" {
return errors.New("unexpected port")
}
if !strings.HasPrefix(uri.Path, "/services/") {
return errors.New("invalid hook path start")
}
case WebHookDiscord:
if uri.Scheme != "https" {
return errors.New("invalid URL scheme, expected https")
}
if uri.Host != "discord.com" {
return errors.New("invalid hook host, expected discord.com")
}
if uri.Port() != "" {
return errors.New("unexpected port")
}
if !strings.HasPrefix(uri.Path, "/api/webhooks/") {
return errors.New("invalid hook path start")
}
case WebHookTelegram:
if uri.Scheme != "https" {
return errors.New("invalid URL scheme, expected https")
}
if uri.Host != "api.telegram.org" {
return errors.New("invalid hook host, expected api.telegram.org")
}
if uri.Port() != "" {
return errors.New("unexpected port")
}
if !strings.HasPrefix(uri.Path, "/bot") {
return errors.New("invalid hook path start")
}
if !strings.HasSuffix(uri.Path, "/sendMessage") {
return errors.New("invalid hook path end")
}
case WebHookMatrixHookshot:
if uri.Scheme != "https" {
return errors.New("invalid URL scheme, expected https")
}
if slices.Contains(disallowedCustomHookHosts, strings.ToLower(uri.Hostname())) {
return errors.New("disallowed hook host")
}
if uri.Port() != "" {
return errors.New("unexpected port")
}
case WebHookCustom:
if uri.Scheme != "https" && uri.Scheme != "http" {
return errors.New("invalid URL scheme, expected http or https")
}
if slices.Contains(disallowedCustomHookHosts, strings.ToLower(uri.Hostname())) {
return errors.New("disallowed hook host")
}
if slices.Contains(disallowedCustomHookPorts, strings.ToLower(uri.Port())) {
return errors.New("disallowed hook port")
}
default:
return errors.New("unsupported hook type")
}
return nil
}

View file

@ -1,31 +0,0 @@
package index
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
)
type Payout struct {
Miner uint64 `json:"miner"`
TemplateId types.Hash `json:"template_id"`
SideHeight uint64 `json:"side_height"`
UncleOf types.Hash `json:"uncle_of,omitempty"`
MainId types.Hash `json:"main_id"`
MainHeight uint64 `json:"main_height"`
Timestamp uint64 `json:"timestamp"`
CoinbaseId types.Hash `json:"coinbase_id"`
Reward uint64 `json:"coinbase_reward"`
PrivateKey crypto.PrivateKeyBytes `json:"coinbase_private_key"`
Index uint64 `json:"coinbase_output_index"`
GlobalOutputIndex uint64 `json:"global_output_index"`
IncludingHeight uint64 `json:"including_height"`
}
func (p *Payout) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
if err := row.Scan(&p.Miner, &p.MainId, &p.MainHeight, &p.Timestamp, &p.CoinbaseId, &p.PrivateKey, &p.TemplateId, &p.SideHeight, &p.UncleOf, &p.Reward, &p.Index, &p.GlobalOutputIndex, &p.IncludingHeight); err != nil {
return err
}
return nil
}

View file

@ -1,174 +0,0 @@
package index
import (
"database/sql"
"errors"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
)
type RowScanInterface interface {
Scan(dest ...any) error
}
type Scannable interface {
ScanFromRow(consensus *sidechain.Consensus, row RowScanInterface) error
}
type QueryIterator[V any] interface {
All(f IterateFunction[int, *V]) (complete bool)
Next() (int, *V)
Close()
Err() error
}
func QueryIterate[V any](i QueryIterator[V], f IterateFunction[int, *V]) {
if i == nil {
return
}
defer i.Close()
i.All(f)
if i.Err() != nil {
panic(i.Err())
}
}
func QueryIterateToSlice[T any](i QueryIterator[T], err ...error) (s []*T) {
if len(err) > 0 {
if err[0] != nil {
panic(err)
}
}
QueryIterate(i, func(key int, value *T) (stop bool) {
s = append(s, value)
return false
})
return s
}
func QueryFirstResult[T any](i QueryIterator[T], err ...error) (v *T) {
if len(err) > 0 {
if err[0] != nil {
panic(err)
}
}
QueryIterate(i, func(_ int, value *T) (stop bool) {
v = value
return true
})
return
}
func QueryHasResults[T any](i QueryIterator[T], err ...error) bool {
if len(err) > 0 {
if err[0] != nil {
panic(err)
}
}
var hasValue bool
QueryIterate(i, func(key int, value *T) (stop bool) {
if value != nil {
hasValue = true
}
return true
})
return hasValue
}
// queryStatement Queries a provided sql.Stmt and returns a QueryIterator
// After results are read QueryIterator must be closed/freed
func queryStatement[V any](index *Index, stmt *sql.Stmt, params ...any) (*QueryResult[V], error) {
var testV *V
if _, ok := any(testV).(Scannable); !ok {
return nil, errors.New("unsupported type")
}
if rows, err := stmt.Query(params...); err != nil {
return nil, err
} else {
return &QueryResult[V]{
consensus: index.consensus,
rows: rows,
}, err
}
}
type FakeQueryResult[V any] struct {
NextFunction func() (int, *V)
}
func (r *FakeQueryResult[V]) All(f IterateFunction[int, *V]) (complete bool) {
for {
if i, v := r.Next(); v == nil {
return true
} else {
if f(i, v) {
// do not allow resuming
return true
}
}
}
}
func (r *FakeQueryResult[V]) Next() (int, *V) {
return r.NextFunction()
}
func (r *FakeQueryResult[V]) Err() error {
return nil
}
func (r *FakeQueryResult[V]) Close() {
}
type QueryResult[V any] struct {
consensus *sidechain.Consensus
rows *sql.Rows
closer func()
i int
err error
}
func (r *QueryResult[V]) All(f IterateFunction[int, *V]) (complete bool) {
for {
if i, v := r.Next(); v == nil {
return true
} else {
if f(i, v) {
// do not allow resuming
return true
}
}
}
}
func (r *QueryResult[V]) Next() (int, *V) {
if r.rows.Next() {
var v V
if r.err = any(&v).(Scannable).ScanFromRow(r.consensus, r.rows); r.err != nil {
return 0, nil
}
r.i++
return r.i - 1, &v
}
return 0, nil
}
func (r *QueryResult[V]) Err() error {
return r.err
}
func (r *QueryResult[V]) Close() {
if r.closer != nil {
r.closer()
r.closer = nil
}
if r.rows != nil {
r.rows.Close()
r.rows = nil
}
}

View file

@ -1,202 +0,0 @@
CREATE TABLE IF NOT EXISTS miners (
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
alias varchar UNIQUE DEFAULT NULL,
spend_public_key bytea NOT NULL,
view_public_key bytea NOT NULL,
UNIQUE (spend_public_key, view_public_key)
);
CREATE TABLE IF NOT EXISTS miner_webhooks (
miner bigint NOT NULL,
type varchar NOT NULL,
url varchar NOT NULL,
settings jsonb NOT NULL DEFAULT '{}', -- settings to know when to trigger or other required settings for the method
UNIQUE (miner, type),
FOREIGN KEY (miner) REFERENCES miners (id)
);
CREATE INDEX IF NOT EXISTS miner_webhooks_miner_idx ON miner_webhooks (miner);
CREATE TABLE IF NOT EXISTS side_blocks (
main_id bytea PRIMARY KEY, -- mainchain id, on Monero network
main_height bigint NOT NULL, -- mainchain height
template_id bytea NOT NULL, -- sidechain template id. Note multiple blocks can exist per template id, see inclusion
side_height bigint NOT NULL, -- sidechain height
parent_template_id bytea NOT NULL, -- previous sidechain template id
miner bigint NOT NULL, -- miner who contributed the block
-- uncle inclusion information
uncle_of bytea DEFAULT NULL, -- has been included under this parent block template id as an uncle. Can change after insert
effective_height bigint NOT NULL, -- has been included under this parent block height as an uncle, or is this height. Can change after insert
-- nonce data
nonce bigint NOT NULL, -- nonce on block header. requires bigint for unsigned int32
extra_nonce bigint NOT NULL, -- nonce on coinbase transaction extra data. requires bigint for unsigned int32
-- other not indexed data
timestamp bigint NOT NULL, -- mainchain timestamp
software_id bigint NOT NULL, -- Software used to generate this template. requires bigint for unsigned int32
software_version bigint NOT NULL, -- Software version used to generate this template. requires bigint for unsigned int32
window_depth int NOT NULL, -- PPLNS window depth, in blocks including this one
window_outputs int NOT NULL, -- number of outputs on coinbase transaction
transaction_count int NOT NULL, -- number of transactions included in the template
difficulty bigint NOT NULL, -- sidechain difficulty at height
cumulative_difficulty bytea NOT NULL, -- sidechain cumulative difficulty at height, binary
pow_difficulty bigint NOT NULL, -- difficulty of pow_hash
pow_hash bytea NOT NULL, -- result of PoW function as a hash (all 0x00 = not known)
inclusion int NOT NULL DEFAULT 1, -- how the block is included. Can change after insert:
-- 0 = orphan (was not included in-verified-chain)
-- 1 = in-verified-chain (uncle or main)
-- 2 = alternate in-verified-chain (uncle or main), for example when duplicate nonce happens
-- Higher values might specify forks or other custom additions
UNIQUE (template_id, nonce, extra_nonce), -- main id can only change when nonce / extra nonce is adjusted, as template_id hash does not include them
FOREIGN KEY (miner) REFERENCES miners (id)
);
CREATE INDEX IF NOT EXISTS side_blocks_miner_idx ON side_blocks (miner);
CREATE INDEX IF NOT EXISTS side_blocks_template_id_idx ON side_blocks (template_id);
CREATE INDEX IF NOT EXISTS side_blocks_main_height_idx ON side_blocks (main_height);
CREATE INDEX IF NOT EXISTS side_blocks_side_height_idx ON side_blocks (side_height);
CREATE INDEX IF NOT EXISTS side_blocks_parent_template_id_idx ON side_blocks (parent_template_id);
CREATE INDEX IF NOT EXISTS side_blocks_uncle_of_idx ON side_blocks (uncle_of);
CREATE INDEX IF NOT EXISTS side_blocks_effective_height_idx ON side_blocks (effective_height);
-- CLUSTER VERBOSE side_blocks USING side_blocks_effective_height_idx;
-- Cannot have non-unique constraints
-- ALTER TABLE side_blocks ADD CONSTRAINT fk_side_blocks_uncle_of FOREIGN KEY (uncle_of) REFERENCES side_blocks (template_id);
-- ALTER TABLE side_blocks ADD CONSTRAINT fk_side_blocks_effective_height FOREIGN KEY (effective_height) REFERENCES side_blocks (side_height);
CREATE TABLE IF NOT EXISTS main_blocks (
id bytea PRIMARY KEY,
height bigint UNIQUE NOT NULL,
timestamp bigint NOT NULL, -- timestamp as set in block
reward bigint NOT NULL,
coinbase_id bytea UNIQUE NOT NULL,
difficulty bigint NOT NULL, -- mainchain difficulty at height
metadata jsonb NOT NULL DEFAULT '{}', -- metadata such as pool ownership, links to other p2pool networks, and other interesting data
-- sidechain data for blocks we own
side_template_id bytea UNIQUE DEFAULT NULL,
coinbase_private_key bytea DEFAULT NULL -- private key for coinbase outputs (all 0x00 = not known, but should have one)
-- Cannot have non-unique constraints
-- FOREIGN KEY (side_template_id) REFERENCES side_blocks (template_id)
);
-- CLUSTER VERBOSE main_blocks USING main_blocks_height_key;
CREATE TABLE IF NOT EXISTS main_coinbase_outputs (
id bytea NOT NULL, -- coinbase id
index int NOT NULL, -- transaction output index
global_output_index bigint UNIQUE NOT NULL, -- Monero global output idx
miner bigint NOT NULL, -- owner of the output
value bigint NOT NULL,
PRIMARY KEY (id, index),
FOREIGN KEY (id) REFERENCES main_blocks (coinbase_id),
FOREIGN KEY (miner) REFERENCES miners (id)
);
CREATE INDEX IF NOT EXISTS main_coinbase_outputs_id_idx ON main_coinbase_outputs (id);
CREATE INDEX IF NOT EXISTS main_coinbase_outputs_miner_idx ON main_coinbase_outputs (miner);
CREATE TABLE IF NOT EXISTS main_likely_sweep_transactions (
id bytea PRIMARY KEY NOT NULL, -- transaction id
timestamp bigint NOT NULL, -- when the transaction was made / included in block
result jsonb NOT NULL, -- MinimalTransactionInputQueryResults
match jsonb NOT NULL, -- TransactionInputQueryResultsMatch
value bigint NOT NULL,
spending_output_indices bigint[] NOT NULL, -- global output indices consumed by this transaction (including decoys)
global_output_indices bigint[] NOT NULL, -- global output indices produced by this transaction
input_count integer NOT NULL, -- count of inputs
input_decoy_count integer NOT NULL, -- count of decoys per input
miner_count integer NOT NULL,
other_miners_count integer NOT NULL,
no_miner_count integer NOT NULL,
miner_ratio real NOT NULL,
other_miners_ratio real NOT NULL,
no_miner_ratio real NOT NULL,
miner_spend_public_key bytea NOT NULL, -- plausible owner of the transaction
miner_view_public_key bytea NOT NULL
);
CREATE INDEX IF NOT EXISTS main_likely_sweep_transactions_miner_idx ON main_likely_sweep_transactions (miner_spend_public_key, miner_view_public_key);
CREATE INDEX IF NOT EXISTS main_likely_sweep_transactions_spending_output_indexes_idx ON main_likely_sweep_transactions USING GIN (spending_output_indices);
CREATE INDEX IF NOT EXISTS main_likely_sweep_transactions_global_output_indexes_idx ON main_likely_sweep_transactions USING GIN (global_output_indices);
CREATE INDEX IF NOT EXISTS main_likely_sweep_transactions_timestamp_idx ON main_likely_sweep_transactions (timestamp);
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Views
CREATE EXTENSION IF NOT EXISTS pg_ivm;
SELECT create_immv('found_main_blocks_v4', $$
SELECT
m.id AS main_id,
m.height AS main_height,
m.timestamp AS timestamp,
m.reward AS reward,
m.coinbase_id AS coinbase_id,
m.coinbase_private_key AS coinbase_private_key,
m.difficulty AS main_difficulty,
m.side_template_id AS template_id,
s.side_height AS side_height,
s.miner AS miner,
s.uncle_of AS uncle_of,
s.effective_height AS effective_height,
s.window_depth AS window_depth,
s.window_outputs AS window_outputs,
s.transaction_count AS transaction_count,
s.difficulty AS side_difficulty,
s.cumulative_difficulty AS side_cumulative_difficulty,
s.inclusion AS side_inclusion
FROM
(SELECT * FROM main_blocks WHERE side_template_id IS NOT NULL) AS m
JOIN
(SELECT * FROM side_blocks) AS s ON s.main_id = m.id
$$);
CREATE UNIQUE INDEX IF NOT EXISTS found_main_blocks_main_id_idx ON found_main_blocks_v4 (main_id);
CREATE INDEX IF NOT EXISTS found_main_blocks_miner_idx ON found_main_blocks_v4 (miner);
CREATE INDEX IF NOT EXISTS found_main_blocks_side_height_idx ON found_main_blocks_v4 (side_height);
SELECT create_immv('payouts_v4', $$
SELECT
o.miner AS miner,
m.id AS main_id,
m.height AS main_height,
m.timestamp AS timestamp,
m.coinbase_id AS coinbase_id,
m.coinbase_private_key AS coinbase_private_key,
m.side_template_id AS template_id,
s.side_height AS side_height,
s.uncle_of AS uncle_of,
o.value AS value,
o.index AS index,
o.global_output_index AS global_output_index,
s.including_height AS including_height
FROM
(SELECT id, value, index, global_output_index, miner FROM main_coinbase_outputs) AS o
JOIN
(SELECT id, height, timestamp, side_template_id, coinbase_id, coinbase_private_key FROM main_blocks) AS m ON m.coinbase_id = o.id
JOIN
(SELECT template_id, main_id, side_height, uncle_of, GREATEST(0, GREATEST(effective_height, side_height) - window_depth) AS including_height FROM side_blocks) AS s ON s.main_id = m.id
$$);
CREATE UNIQUE INDEX IF NOT EXISTS found_main_blocks_global_output_index_idx ON payouts_v4 (global_output_index);
CREATE INDEX IF NOT EXISTS payouts_miner_idx ON payouts_v4 (miner);
CREATE INDEX IF NOT EXISTS payouts_main_id_idx ON payouts_v4 (main_id);
CREATE INDEX IF NOT EXISTS payouts_side_height_idx ON payouts_v4 (side_height);
CREATE INDEX IF NOT EXISTS payouts_main_height_idx ON payouts_v4 (main_height);

View file

@ -1,241 +0,0 @@
package index
import (
"bytes"
"encoding/binary"
"errors"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"slices"
"unsafe"
)
type BlockInclusion int
const (
// InclusionOrphan orphan (was not included in-verified-chain)
InclusionOrphan = BlockInclusion(iota)
// InclusionInVerifiedChain in-verified-chain (uncle or main)
InclusionInVerifiedChain
// InclusionAlternateInVerifiedChain alternate in-verified-chain (uncle or main), for example when duplicate nonce happens
InclusionAlternateInVerifiedChain
InclusionCount
)
const SideBlockSelectFields = "main_id, main_height, template_id, side_height, parent_template_id, miner, uncle_of, effective_height, nonce, extra_nonce, timestamp, software_id, software_version, window_depth, window_outputs, transaction_count, difficulty, cumulative_difficulty, pow_difficulty, pow_hash, inclusion"
type SideBlock struct {
// MainId mainchain id, on Monero network
MainId types.Hash `json:"main_id"`
MainHeight uint64 `json:"main_height"`
// TemplateId -- sidechain template id. Note multiple blocks can exist per template id, see Inclusion
TemplateId types.Hash `json:"template_id"`
SideHeight uint64 `json:"side_height"`
// ParentTemplateId previous sidechain template id
ParentTemplateId types.Hash `json:"parent_template_id"`
// Miner internal id of the miner who contributed the block
Miner uint64 `json:"miner"`
// Uncle inclusion information
// UncleOf has been included under this parent block TemplateId as an uncle
UncleOf types.Hash `json:"uncle_of,omitempty"`
// EffectiveHeight has been included under this parent block height as an uncle, or is this height
EffectiveHeight uint64 `json:"effective_height"`
// Nonce data
Nonce uint32 `json:"nonce"`
ExtraNonce uint32 `json:"extra_nonce"`
Timestamp uint64 `json:"timestamp"`
SoftwareId p2pooltypes.SoftwareId `json:"software_id"`
SoftwareVersion p2pooltypes.SoftwareVersion `json:"software_version"`
// WindowDepth PPLNS window depth, in blocks including this one
WindowDepth uint32 `json:"window_depth"`
WindowOutputs uint32 `json:"window_outputs"`
// Difficulty sidechain difficulty at height
Difficulty uint64 `json:"difficulty"`
CumulativeDifficulty types.Difficulty `json:"cumulative_difficulty"`
PowDifficulty uint64 `json:"pow_difficulty"`
// PowHash result of PoW function as a hash (all 0x00 = not known)
PowHash types.Hash `json:"pow_hash"`
Inclusion BlockInclusion `json:"inclusion"`
TransactionCount uint32 `json:"transaction_count"`
// Extra information filled just for JSON purposes
MinedMainAtHeight bool `json:"mined_main_at_height,omitempty"`
MinerAddress *address.Address `json:"miner_address,omitempty"`
MinerAlias string `json:"miner_alias,omitempty"`
Uncles []SideBlockUncleEntry `json:"uncles,omitempty"`
MainDifficulty uint64 `json:"main_difficulty,omitempty"`
}
type SideBlockUncleEntry struct {
TemplateId types.Hash `json:"template_id"`
Miner uint64 `json:"miner"`
SideHeight uint64 `json:"side_height"`
Difficulty uint64 `json:"difficulty"`
}
// FromPoolBlock block needs to be pre-processed for ids to be correct
// These fields need to be filled by caller to match needs:
// SideBlock.UncleOf
// SideBlock.EffectiveHeight
// SideBlock.WindowDepth
// SideBlock.Inclusion
func (b *SideBlock) FromPoolBlock(i *Index, block *sidechain.PoolBlock, getSeedByHeight mainblock.GetSeedByHeightFunc) error {
b.MainId = block.MainId()
b.TemplateId = block.SideTemplateId(i.consensus)
if b.MainId == types.ZeroHash {
return errors.New("invalid main id")
}
if b.TemplateId == types.ZeroHash || bytes.Compare(b.TemplateId[:], block.CoinbaseExtra(sidechain.SideTemplateId)) != 0 {
return errors.New("invalid template id")
}
b.MainHeight = block.Main.Coinbase.GenHeight
b.SideHeight = block.Side.Height
b.ParentTemplateId = block.Side.Parent
b.Miner = i.GetOrCreateMinerPackedAddress(block.GetAddress()).id
b.Nonce = block.Main.Nonce
b.ExtraNonce = block.ExtraNonce()
b.Timestamp = block.Main.Timestamp
b.SoftwareId = block.Side.ExtraBuffer.SoftwareId
b.SoftwareVersion = block.Side.ExtraBuffer.SoftwareVersion
b.WindowOutputs = uint32(len(block.Main.Coinbase.Outputs))
b.TransactionCount = uint32(len(block.Main.Transactions))
b.Difficulty = block.Side.Difficulty.Lo
b.CumulativeDifficulty = block.Side.CumulativeDifficulty
b.PowHash = block.PowHash(i.Consensus().GetHasher(), getSeedByHeight)
if b.PowHash == types.ZeroHash {
return errors.New("invalid pow hash")
}
b.PowDifficulty = types.DifficultyFromPoW(b.PowHash).Lo
return nil
}
func (b *SideBlock) SetUncleOf(block *SideBlock) error {
if block == nil {
b.UncleOf = types.ZeroHash
b.EffectiveHeight = b.SideHeight
return nil
}
if block.IsUncle() {
return errors.New("parent cannot be uncle")
}
if block.SideHeight <= b.SideHeight {
return errors.New("parent side height cannot be lower or equal")
}
b.UncleOf = block.TemplateId
b.EffectiveHeight = block.EffectiveHeight
return nil
}
// IsTipOfHeight whether this block is considered to be the "main" block within this height, not an uncle, or alternate
func (b *SideBlock) IsTipOfHeight() bool {
return b.SideHeight == b.EffectiveHeight && b.Inclusion == InclusionInVerifiedChain
}
func (b *SideBlock) FullId() sidechain.FullId {
var buf sidechain.FullId
copy(buf[:], b.TemplateId[:])
binary.LittleEndian.PutUint32(buf[types.HashSize:], b.Nonce)
binary.LittleEndian.PutUint32(buf[types.HashSize+unsafe.Sizeof(b.Nonce):], b.ExtraNonce)
return buf
}
func (b *SideBlock) IsUncle() bool {
return b.SideHeight != b.EffectiveHeight && b.UncleOf != types.ZeroHash
}
func (b *SideBlock) IsOrphan() bool {
return b.Inclusion == InclusionOrphan
}
func (b *SideBlock) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
if err := row.Scan(&b.MainId, &b.MainHeight, &b.TemplateId, &b.SideHeight, &b.ParentTemplateId, &b.Miner, &b.UncleOf, &b.EffectiveHeight, &b.Nonce, &b.ExtraNonce, &b.Timestamp, &b.SoftwareId, &b.SoftwareVersion, &b.WindowDepth, &b.WindowOutputs, &b.TransactionCount, &b.Difficulty, &b.CumulativeDifficulty, &b.PowDifficulty, &b.PowHash, &b.Inclusion); err != nil {
return err
}
return nil
}
//Utilities to be used by JSON decoded blocks
func (b *SideBlock) Weight(tipHeight, windowSize, consensusUnclePenalty uint64) (weight, parentWeight uint64) {
if (tipHeight - b.SideHeight) >= windowSize {
return 0, 0
}
if b.IsUncle() {
unclePenalty := types.DifficultyFrom64(b.Difficulty).Mul64(consensusUnclePenalty).Div64(100)
uncleWeight := b.Difficulty - unclePenalty.Lo
return uncleWeight, unclePenalty.Lo
} else {
weight = b.Difficulty
for _, u := range b.Uncles {
if (tipHeight - u.SideHeight) >= windowSize {
continue
}
weight += types.DifficultyFrom64(u.Difficulty).Mul64(consensusUnclePenalty).Div64(100).Lo
}
return weight, 0
}
}
func HashRatioSideBlocks(shares []*SideBlock, latest *SideBlock, consensusUnclePenalty uint64) (hashrate uint64, ratio float64, weight, total types.Difficulty) {
if len(shares) == 0 || latest == nil {
return 0, 0, types.ZeroDifficulty, types.ZeroDifficulty
}
shares = slices.Clone(shares)
slices.SortFunc(shares, SortSideBlock)
var totalWeight types.Difficulty
for _, s := range shares {
w, _ := s.Weight(s.SideHeight, uint64(s.WindowDepth), consensusUnclePenalty)
totalWeight = totalWeight.Add64(w)
}
cumWeight := latest.CumulativeDifficulty.Sub(shares[0].CumulativeDifficulty)
return totalWeight.Div64(latest.Timestamp - shares[0].Timestamp).Lo,
totalWeight.Float64() / cumWeight.Float64(),
totalWeight,
cumWeight
}
func SortSideBlock(a, b *SideBlock) int {
if a == b {
return 0
}
if a == nil {
return -1
} else if b == nil {
return 1
}
if a.EffectiveHeight < b.EffectiveHeight {
return -1
} else if a.EffectiveHeight > b.EffectiveHeight {
return 1
}
if a.SideHeight < b.SideHeight {
return -1
} else if a.SideHeight > b.SideHeight {
return 1
}
//same height, sort by main id
return a.MainId.Compare(b.MainId)
}

View file

@ -1,447 +0,0 @@
package index
import (
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"git.gammaspectra.live/P2Pool/sha3"
"slices"
)
type GetByTemplateIdFunc func(h types.Hash) *SideBlock
type GetUnclesByTemplateIdFunc func(h types.Hash) QueryIterator[SideBlock]
type SideBlockWindowAddWeightFunc func(b *SideBlock, weight types.Difficulty)
type SideBlockWindowSlot struct {
Block *SideBlock
// Uncles that count for the window weight
Uncles []*SideBlock
}
// IterateSideBlocksInPPLNSWindow
// Copy of sidechain.IterateBlocksInPPLNSWindow
func IterateSideBlocksInPPLNSWindow(tip *SideBlock, consensus *sidechain.Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, addWeightFunc SideBlockWindowAddWeightFunc, slotFunc func(slot SideBlockWindowSlot)) error {
cur := tip
var blockDepth uint64
var mainchainDiff types.Difficulty
if tip.ParentTemplateId != types.ZeroHash {
seedHeight := randomx.SeedHeight(tip.MainHeight)
mainchainDiff = difficultyByHeight(seedHeight)
if mainchainDiff == types.ZeroDifficulty {
return fmt.Errorf("couldn't get mainchain difficulty for height = %d", seedHeight)
}
}
// Dynamic PPLNS window starting from v2
// Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average)
sidechainVersion := sidechain.P2PoolShareVersion(consensus, tip.Timestamp)
maxPplnsWeight := types.MaxDifficulty
if sidechainVersion > sidechain.ShareVersion_V1 {
maxPplnsWeight = mainchainDiff.Mul64(2)
}
var pplnsWeight types.Difficulty
for {
curEntry := SideBlockWindowSlot{
Block: cur,
}
curWeight := types.DifficultyFrom64(cur.Difficulty)
QueryIterate(getUnclesByTemplateId(cur.TemplateId), func(_ int, uncle *SideBlock) (stop bool) {
//Needs to be added regardless - for other consumers
if !slices.ContainsFunc(curEntry.Uncles, func(sideBlock *SideBlock) bool {
return sideBlock.TemplateId == uncle.TemplateId
}) {
curEntry.Uncles = append(curEntry.Uncles, uncle)
}
// Skip uncles which are already out of PPLNS window
if (tip.SideHeight - uncle.SideHeight) >= consensus.ChainWindowSize {
return false
}
// Take some % of uncle's weight into this share
uncleWeight, unclePenalty := consensus.ApplyUnclePenalty(types.DifficultyFrom64(uncle.Difficulty))
newPplnsWeight := pplnsWeight.Add(uncleWeight)
// Skip uncles that push PPLNS weight above the limit
if newPplnsWeight.Cmp(maxPplnsWeight) > 0 {
return false
}
curWeight = curWeight.Add(unclePenalty)
if addWeightFunc != nil {
addWeightFunc(uncle, uncleWeight)
}
pplnsWeight = newPplnsWeight
return false
})
// Always add non-uncle shares even if PPLNS weight goes above the limit
slotFunc(curEntry)
if addWeightFunc != nil {
addWeightFunc(cur, curWeight)
}
pplnsWeight = pplnsWeight.Add(curWeight)
// One non-uncle share can go above the limit, but it will also guarantee that "shares" is never empty
if pplnsWeight.Cmp(maxPplnsWeight) > 0 {
break
}
blockDepth++
if blockDepth >= consensus.ChainWindowSize {
break
}
// Reached the genesis block so we're done
if cur.SideHeight == 0 {
break
}
parentId := cur.ParentTemplateId
cur = getByTemplateId(parentId)
if cur == nil {
return fmt.Errorf("could not find parent %s", parentId.String())
}
}
return nil
}
// BlocksInPPLNSWindow
// Copy of sidechain.BlocksInPPLNSWindow
func BlocksInPPLNSWindow(tip *SideBlock, consensus *sidechain.Consensus, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, addWeightFunc SideBlockWindowAddWeightFunc) (bottomHeight uint64, err error) {
cur := tip
var blockDepth uint64
var mainchainDiff types.Difficulty
if tip.ParentTemplateId != types.ZeroHash {
seedHeight := randomx.SeedHeight(tip.MainHeight)
mainchainDiff = difficultyByHeight(seedHeight)
if mainchainDiff == types.ZeroDifficulty {
return 0, fmt.Errorf("couldn't get mainchain difficulty for height = %d", seedHeight)
}
}
// Dynamic PPLNS window starting from v2
// Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average)
sidechainVersion := sidechain.P2PoolShareVersion(consensus, tip.Timestamp)
maxPplnsWeight := types.MaxDifficulty
if sidechainVersion > sidechain.ShareVersion_V1 {
maxPplnsWeight = mainchainDiff.Mul64(2)
}
var pplnsWeight types.Difficulty
for {
curEntry := SideBlockWindowSlot{
Block: cur,
}
curWeight := types.DifficultyFrom64(cur.Difficulty)
QueryIterate(getUnclesByTemplateId(cur.TemplateId), func(_ int, uncle *SideBlock) (stop bool) {
//Needs to be added regardless - for other consumers
if !slices.ContainsFunc(curEntry.Uncles, func(sideBlock *SideBlock) bool {
return sideBlock.TemplateId == uncle.TemplateId
}) {
curEntry.Uncles = append(curEntry.Uncles, uncle)
}
// Skip uncles which are already out of PPLNS window
if (tip.SideHeight - uncle.SideHeight) >= consensus.ChainWindowSize {
return false
}
// Take some % of uncle's weight into this share
uncleWeight, unclePenalty := consensus.ApplyUnclePenalty(types.DifficultyFrom64(uncle.Difficulty))
newPplnsWeight := pplnsWeight.Add(uncleWeight)
// Skip uncles that push PPLNS weight above the limit
if newPplnsWeight.Cmp(maxPplnsWeight) > 0 {
return false
}
curWeight = curWeight.Add(unclePenalty)
if addWeightFunc != nil {
addWeightFunc(uncle, uncleWeight)
}
pplnsWeight = newPplnsWeight
return false
})
// Always add non-uncle shares even if PPLNS weight goes above the limit
bottomHeight = cur.SideHeight
if addWeightFunc != nil {
addWeightFunc(cur, curWeight)
}
pplnsWeight = pplnsWeight.Add(curWeight)
// One non-uncle share can go above the limit, but it will also guarantee that "shares" is never empty
if pplnsWeight.Cmp(maxPplnsWeight) > 0 {
break
}
blockDepth++
if blockDepth >= consensus.ChainWindowSize {
break
}
// Reached the genesis block so we're done
if cur.SideHeight == 0 {
break
}
parentId := cur.ParentTemplateId
cur = getByTemplateId(parentId)
if cur == nil {
return 0, fmt.Errorf("could not find parent %s", parentId.String())
}
}
return bottomHeight, nil
}
func IterateSideBlocksInPPLNSWindowFast(indexDb *Index, tip *SideBlock, difficultyByHeight block.GetDifficultyByHeightFunc, addWeightFunc SideBlockWindowAddWeightFunc, slotFunc func(slot SideBlockWindowSlot)) error {
if tip == nil {
return errors.New("nil tip")
}
window := QueryIterateToSlice(indexDb.GetSideBlocksInPPLNSWindow(tip))
if len(window) == 0 {
return errors.New("nil window")
}
var hintIndex int
getByTemplateIdFull := func(h types.Hash) *SideBlock {
if i := slices.IndexFunc(window, func(e *SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex = i
return window[i]
}
return nil
}
getByTemplateId := func(h types.Hash) *SideBlock {
//fast lookup first
if i := slices.IndexFunc(window[hintIndex:], func(e *SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex += i
return window[hintIndex]
}
return getByTemplateIdFull(h)
}
getUnclesOf := func(h types.Hash) QueryIterator[SideBlock] {
parentEffectiveHeight := window[hintIndex].EffectiveHeight
if window[hintIndex].TemplateId != h {
parentEffectiveHeight = 0
}
startIndex := 0
return &FakeQueryResult[SideBlock]{
NextFunction: func() (int, *SideBlock) {
for _, b := range window[startIndex+hintIndex:] {
if b.UncleOf == h {
startIndex++
return startIndex, b
}
startIndex++
if parentEffectiveHeight != 0 && b.EffectiveHeight < parentEffectiveHeight {
//early exit
break
}
}
return 0, nil
},
}
}
return IterateSideBlocksInPPLNSWindow(tip, indexDb.Consensus(), difficultyByHeight, getByTemplateId, getUnclesOf, addWeightFunc, slotFunc)
}
func BlocksInPPLNSWindowFast(indexDb *Index, tip *SideBlock, difficultyByHeight block.GetDifficultyByHeightFunc, addWeightFunc SideBlockWindowAddWeightFunc) (bottomHeight uint64, err error) {
if tip == nil {
return 0, errors.New("nil tip")
}
window := QueryIterateToSlice(indexDb.GetSideBlocksInPPLNSWindow(tip))
if len(window) == 0 {
return 0, errors.New("nil window")
}
var hintIndex int
getByTemplateIdFull := func(h types.Hash) *SideBlock {
if i := slices.IndexFunc(window, func(e *SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex = i
return window[i]
}
return nil
}
getByTemplateId := func(h types.Hash) *SideBlock {
//fast lookup first
if i := slices.IndexFunc(window[hintIndex:], func(e *SideBlock) bool {
return e.TemplateId == h
}); i != -1 {
hintIndex += i
return window[hintIndex]
}
return getByTemplateIdFull(h)
}
getUnclesOf := func(h types.Hash) QueryIterator[SideBlock] {
parentEffectiveHeight := window[hintIndex].EffectiveHeight
if window[hintIndex].TemplateId != h {
parentEffectiveHeight = 0
}
startIndex := 0
return &FakeQueryResult[SideBlock]{
NextFunction: func() (int, *SideBlock) {
for _, b := range window[startIndex+hintIndex:] {
if b.UncleOf == h {
startIndex++
return startIndex, b
}
startIndex++
if parentEffectiveHeight != 0 && b.EffectiveHeight < parentEffectiveHeight {
//early exit
break
}
}
return 0, nil
},
}
}
return BlocksInPPLNSWindow(tip, indexDb.Consensus(), difficultyByHeight, getByTemplateId, getUnclesOf, addWeightFunc)
}
// GetSharesOrdered
// Copy of sidechain.GetSharesOrdered
func GetSharesOrdered(indexDb *Index, tip *SideBlock, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, preAllocatedShares sidechain.Shares) (shares sidechain.Shares, bottomHeight uint64) {
index := 0
l := len(preAllocatedShares)
if bottomHeight, err := BlocksInPPLNSWindow(tip, indexDb.Consensus(), difficultyByHeight, getByTemplateId, getUnclesByTemplateId, func(b *SideBlock, weight types.Difficulty) {
addr := indexDb.GetMiner(b.Miner).Address().ToPackedAddress()
if index < l {
preAllocatedShares[index].Address = addr
preAllocatedShares[index].Weight = weight
} else {
preAllocatedShares = append(preAllocatedShares, &sidechain.Share{
Address: addr,
Weight: weight,
})
}
index++
}); err != nil {
return nil, 0
} else {
shares = preAllocatedShares[:index]
//remove dupes
shares = shares.Compact()
return shares, bottomHeight
}
}
// GetShares
// Copy of sidechain.GetShares
func GetShares(indexDb *Index, tip *SideBlock, coinbasePrivateKeySeed types.Hash, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, preAllocatedShares sidechain.Shares) (shares sidechain.Shares, bottomHeight uint64) {
shares, bottomHeight = GetSharesOrdered(indexDb, tip, difficultyByHeight, getByTemplateId, getUnclesByTemplateId, preAllocatedShares)
if shares == nil {
return
}
//Shuffle shares
sidechain.ShuffleShares(shares, sidechain.P2PoolShareVersion(indexDb.Consensus(), tip.Timestamp), coinbasePrivateKeySeed)
return shares, bottomHeight
}
// CalculateOutputs
// Copy of sidechain.CalculateOutputs
func CalculateOutputs(indexDb *Index, block *SideBlock, transactionOutputType uint8, totalReward uint64, coinbasePrivateKey crypto.PrivateKey, coinbasePrivateKeySeed types.Hash, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId GetByTemplateIdFunc, getUnclesByTemplateId GetUnclesByTemplateIdFunc, derivationCache sidechain.DerivationCacheInterface, preAllocatedShares sidechain.Shares, preAllocatedRewards []uint64) (outputs transaction.Outputs, bottomHeight uint64) {
tmpShares, bottomHeight := GetShares(indexDb, block, coinbasePrivateKeySeed, difficultyByHeight, getByTemplateId, getUnclesByTemplateId, preAllocatedShares)
if preAllocatedRewards == nil {
preAllocatedRewards = make([]uint64, 0, len(tmpShares))
}
tmpRewards := sidechain.SplitReward(preAllocatedRewards, totalReward, tmpShares)
if tmpShares == nil || tmpRewards == nil || len(tmpRewards) != len(tmpShares) {
return nil, 0
}
n := uint64(len(tmpShares))
outputs = make(transaction.Outputs, n)
txType := transactionOutputType
txPrivateKeySlice := coinbasePrivateKey.AsSlice()
txPrivateKeyScalar := coinbasePrivateKey.AsScalar()
var hashers []*sha3.HasherState
defer func() {
for _, h := range hashers {
crypto.PutKeccak256Hasher(h)
}
}()
utils.SplitWork(-2, n, func(workIndex uint64, workerIndex int) error {
output := transaction.Output{
Index: workIndex,
Type: txType,
}
output.Reward = tmpRewards[output.Index]
output.EphemeralPublicKey, output.ViewTag = derivationCache.GetEphemeralPublicKey(&tmpShares[output.Index].Address, txPrivateKeySlice, txPrivateKeyScalar, output.Index, hashers[workerIndex])
outputs[output.Index] = output
return nil
}, func(routines, routineIndex int) error {
hashers = append(hashers, crypto.GetKeccak256Hasher())
return nil
}, nil)
return outputs, bottomHeight
}

View file

@ -1,35 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/legacytoarchive
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0 => ../../p2pool/cache
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0
github.com/floatdrop/lru v1.3.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
go.etcd.io/bbolt v1.3.9 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,47 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,153 +0,0 @@
package main
import (
"encoding/hex"
"flag"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/archive"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/floatdrop/lru"
"math"
"os"
"path"
)
func main() {
inputConsensus := flag.String("consensus", "config.json", "Input config.json consensus file")
inputFolder := flag.String("input", "", "Input legacy api folder with raw / failed blocks")
outputArchive := flag.String("output", "", "Output path for archive database")
flag.Parse()
cf, err := os.ReadFile(*inputConsensus)
consensus, err := sidechain.NewConsensusFromJSON(cf)
if err != nil {
utils.Panic(err)
}
archiveCache, err := archive.NewCache(*outputArchive, consensus, func(height uint64) types.Difficulty {
return types.ZeroDifficulty
})
if err != nil {
utils.Panic(err)
}
defer archiveCache.Close()
processed := make(map[types.Hash]bool)
totalStored := 0
derivationCache := sidechain.NewDerivationLRUCache()
blockCache := lru.New[types.Hash, *sidechain.PoolBlock](int(consensus.ChainWindowSize * 4 * 60))
loadBlock := func(id types.Hash) *sidechain.PoolBlock {
n := id.String()
fPath := path.Join(*inputFolder, "blocks", n[:1], n)
if buf, err := os.ReadFile(fPath); err != nil {
return nil
} else {
if hexBuf, err := hex.DecodeString(string(buf)); err != nil {
utils.Panic(err)
} else {
if block, err := sidechain.NewShareFromExportedBytes(hexBuf, consensus, derivationCache); err != nil {
utils.Error("", "error decoding block %s, %s", id.String(), err)
} else {
block.Depth.Store(math.MaxUint64)
return block
}
}
}
return nil
}
getByTemplateId := func(h types.Hash) *sidechain.PoolBlock {
if v := blockCache.Get(h); v == nil {
if b := loadBlock(h); b != nil {
b.Depth.Store(math.MaxUint64)
blockCache.Set(h, b)
return b
}
return nil
} else {
return *v
}
}
var storeBlock func(k types.Hash, b *sidechain.PoolBlock, depth uint64)
storeBlock = func(k types.Hash, b *sidechain.PoolBlock, depth uint64) {
if b == nil || processed[k] {
return
}
if depth >= consensus.ChainWindowSize*4*30 { //avoid infinite memory growth
return
}
if parent := getByTemplateId(b.Side.Parent); parent != nil {
storeBlock(b.Side.Parent, parent, depth+1)
topDepth := parent.Depth.Load()
b.FillTransactionParentIndices(parent)
if topDepth == math.MaxUint64 {
b.Depth.Store(consensus.ChainWindowSize * 2)
} else if topDepth == 0 {
b.Depth.Store(0)
} else {
b.Depth.Store(topDepth - 1)
}
} else {
b.Depth.Store(math.MaxUint64)
}
archiveCache.Store(b)
totalStored++
processed[k] = true
}
for i := 0; i <= 0xf; i++ {
n := hex.EncodeToString([]byte{byte(i)})
dPath := path.Join(*inputFolder, "blocks", n[1:])
utils.Logf("", "Reading directory %s", dPath)
if dir, err := os.ReadDir(dPath); err != nil {
utils.Panic(err)
} else {
for _, e := range dir {
h, _ := hex.DecodeString(e.Name())
id := types.HashFromBytes(h)
bb := getByTemplateId(id)
if bb != nil && !archiveCache.ExistsByMainId(bb.MainId()) {
storeBlock(id, bb, 0)
}
}
}
}
for i := 0; i <= 0xf; i++ {
n := hex.EncodeToString([]byte{byte(i)})
dPath := path.Join(*inputFolder, "failed_blocks", n[1:])
if dir, err := os.ReadDir(dPath); err != nil {
utils.Panic(err)
} else {
for _, e := range dir {
fPath := path.Join(dPath, e.Name())
utils.Logf("", "Processing %s", fPath)
if buf, err := os.ReadFile(path.Join(dPath, e.Name())); err != nil {
utils.Panic(err)
} else {
if hexBuf, err := hex.DecodeString(string(buf)); err != nil {
utils.Panic(err)
} else {
if block, err := sidechain.NewShareFromExportedBytes(hexBuf, consensus, derivationCache); err != nil {
utils.Panic(err)
} else {
block.Depth.Store(math.MaxUint64)
archiveCache.Store(block)
totalStored++
}
}
}
}
}
}
utils.Logf("", "total stored %d", totalStored)
}

View file

@ -1,892 +0,0 @@
package main
import (
"bytes"
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/httputils"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/p2p"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/gorilla/mux"
"io"
"math"
"net/http"
"net/netip"
"strconv"
"sync"
"time"
)
func getServerMux(instance *P2Pool) *mux.Router {
serveMux := mux.NewRouter()
archiveCache := instance.AddressableCache()
// ================================= Peering section =================================
serveMux.HandleFunc("/server/peers", func(writer http.ResponseWriter, request *http.Request) {
clients := instance.Server().Clients()
result := make([]p2pooltypes.P2PoolServerPeerResult, 0, len(clients))
for _, c := range clients {
if !c.IsGood() {
continue
}
result = append(result, p2pooltypes.P2PoolServerPeerResult{
Incoming: c.IsIncomingConnection,
Address: c.AddressPort.Addr().String(),
SoftwareId: c.VersionInformation.SoftwareId.String(),
SoftwareVersion: c.VersionInformation.SoftwareVersion.String(),
ProtocolVersion: c.VersionInformation.Protocol.String(),
ConnectionTime: uint64(c.ConnectionTime.Unix()),
ListenPort: c.ListenPort.Load(),
Latency: uint64(time.Duration(c.PingDuration.Load()).Milliseconds()),
PeerId: c.PeerId.Load(),
})
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
})
serveMux.HandleFunc("/server/connection_check/{addrPort:.+}", func(writer http.ResponseWriter, request *http.Request) {
addrPort, err := netip.ParseAddrPort(mux.Vars(request)["addrPort"])
if err != nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
buf, _ := utils.MarshalJSON(struct {
Error string `json:"error"`
}{
Error: err.Error(),
})
_, _ = writer.Write(buf)
return
}
var client *p2p.Client
var alreadyConnected bool
isBanned, banEntry := instance.Server().IsBanned(addrPort.Addr())
for _, c := range instance.Server().Clients() {
if c.AddressPort.Addr().Compare(addrPort.Addr()) == 0 && uint16(c.ListenPort.Load()) == addrPort.Port() {
client = c
alreadyConnected = true
break
}
}
if client == nil {
if client, err = instance.Server().DirectConnect(addrPort); err != nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
buf, _ := utils.MarshalJSON(struct {
Error string `json:"error"`
}{
Error: err.Error(),
})
_, _ = writer.Write(buf)
return
}
}
if client == nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusBadRequest)
buf, _ := utils.MarshalJSON(struct {
Error string `json:"error"`
}{
Error: "could not find client",
})
_, _ = writer.Write(buf)
return
}
for i := 0; i < 6; i++ {
if client.Closed.Load() || (client.IsGood() && client.PingDuration.Load() > 0 && client.LastKnownTip.Load() != nil) {
break
}
time.Sleep(time.Second * 1)
}
banError := ""
errorStr := ""
if err := client.BanError(); err != nil {
errorStr = err.Error()
}
if banEntry != nil && banEntry.Error != nil {
banError = banEntry.Error.Error()
}
info := p2pooltypes.P2PoolConnectionCheckInformation[*sidechain.PoolBlock]{
Address: client.AddressPort.Addr().Unmap().String(),
Port: client.AddressPort.Port(),
ListenPort: uint16(client.ListenPort.Load()),
PeerId: client.PeerId.Load(),
SoftwareId: client.VersionInformation.SoftwareId.String(),
SoftwareVersion: client.VersionInformation.SoftwareVersion.String(),
ProtocolVersion: client.VersionInformation.Protocol.String(),
ConnectionTime: uint64(client.ConnectionTime.Unix()),
Latency: uint64(time.Duration(client.PingDuration.Load()).Milliseconds()),
Incoming: client.IsIncomingConnection,
BroadcastTime: client.LastBroadcastTimestamp.Load(),
BroadcastHeight: client.BroadcastMaxHeight.Load(),
Tip: client.LastKnownTip.Load(),
Closed: client.Closed.Load(),
AlreadyConnected: alreadyConnected,
HandshakeComplete: client.HandshakeComplete.Load(),
LastActive: client.LastActiveTimestamp.Load(),
Banned: isBanned,
Error: errorStr,
BanError: banError,
}
if isBanned {
client.Close()
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, info)
})
serveMux.HandleFunc("/server/peerlist", func(writer http.ResponseWriter, request *http.Request) {
peers := instance.Server().PeerList()
var result []byte
for _, c := range peers {
result = append(result, []byte(c.AddressPort.String()+"\n")...)
}
writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write(result)
})
serveMux.HandleFunc("/server/status", func(writer http.ResponseWriter, request *http.Request) {
result := p2pooltypes.P2PoolServerStatusResult{
PeerId: instance.Server().PeerId(),
SoftwareId: instance.Server().VersionInformation().SoftwareId.String(),
SoftwareVersion: instance.Server().VersionInformation().SoftwareVersion.String(),
ProtocolVersion: instance.Server().VersionInformation().Protocol.String(),
ListenPort: instance.Server().ListenPort(),
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
})
// ================================= MainChain section =================================
serveMux.HandleFunc("/mainchain/header_by_height/{height:[0-9]+}", func(writer http.ResponseWriter, request *http.Request) {
if height, err := strconv.ParseUint(mux.Vars(request)["height"], 10, 0); err == nil {
result := instance.GetMinimalBlockHeaderByHeight(height)
if result == nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
}
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/mainchain/difficulty_by_height/{height:[0-9]+}", func(writer http.ResponseWriter, request *http.Request) {
if height, err := strconv.ParseUint(mux.Vars(request)["height"], 10, 0); err == nil {
result := instance.GetDifficultyByHeight(height)
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/mainchain/header_by_id/{id:[0-9a-f]+}", func(writer http.ResponseWriter, request *http.Request) {
if id, err := types.HashFromString(mux.Vars(request)["id"]); err == nil {
result := instance.GetMinimalBlockHeaderByHash(id)
if result == nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
}
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/mainchain/miner_data", func(writer http.ResponseWriter, request *http.Request) {
result := instance.GetMinerDataTip()
if result == nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
}
})
serveMux.HandleFunc("/mainchain/tip", func(writer http.ResponseWriter, request *http.Request) {
minerData := instance.GetMinerDataTip()
if minerData == nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
return
}
result := instance.GetMinimalBlockHeaderByHeight(minerData.Height - 1)
if result == nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
}
})
// ================================= SideChain section =================================
serveMux.HandleFunc("/sidechain/consensus", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, instance.Consensus())
})
serveMux.HandleFunc("/sidechain/blocks_by_height/{height:[0-9]+}", func(writer http.ResponseWriter, request *http.Request) {
if height, err := strconv.ParseUint(mux.Vars(request)["height"], 10, 0); err == nil {
result := make([]p2pooltypes.P2PoolBinaryBlockResult, 0, 1)
for _, b := range instance.SideChain().GetPoolBlocksByHeight(height) {
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("[]"))
}
})
serveMux.HandleFunc("/sidechain/block_by_template_id/{id:[0-9a-f]+}", func(writer http.ResponseWriter, request *http.Request) {
if templateId, err := types.HashFromString(mux.Vars(request)["id"]); err == nil {
var result p2pooltypes.P2PoolBinaryBlockResult
if b := instance.SideChain().GetPoolBlockByTemplateId(templateId); b != nil {
result.Version = int(b.ShareVersion())
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Error = err.Error()
} else {
result.Blob = blob
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("[]"))
}
})
serveMux.HandleFunc("/sidechain/state/short", func(writer http.ResponseWriter, request *http.Request) {
tip := instance.SideChain().GetChainTip()
if tip != nil {
tipId := instance.SideChain().GetChainTip().SideTemplateId(instance.Consensus())
chain, uncles := instance.SideChain().GetPoolBlocksFromTip(tipId)
result := p2pooltypes.P2PoolSideChainStateResult{
TipHeight: tip.Side.Height,
TipId: tipId,
Chain: make([]p2pooltypes.P2PoolBinaryBlockResult, 0, len(chain)),
Uncles: make([]p2pooltypes.P2PoolBinaryBlockResult, 0, len(uncles)),
}
for _, b := range chain {
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
for _, b := range uncles {
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/sidechain/state/tip", func(writer http.ResponseWriter, request *http.Request) {
tip := instance.SideChain().GetChainTip()
if tip != nil {
tipId := instance.SideChain().GetChainTip().SideTemplateId(instance.Consensus())
chain, uncles := instance.SideChain().GetPoolBlocksFromTip(tipId)
result := p2pooltypes.P2PoolSideChainStateResult{
TipHeight: tip.Side.Height,
TipId: tipId,
Chain: make([]p2pooltypes.P2PoolBinaryBlockResult, 0, len(chain)),
Uncles: make([]p2pooltypes.P2PoolBinaryBlockResult, 0, len(uncles)),
}
for _, b := range chain {
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
for _, b := range uncles {
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/sidechain/state/{id:[0-9a-f]+}", func(writer http.ResponseWriter, request *http.Request) {
if templateId, err := types.HashFromString(mux.Vars(request)["id"]); err == nil {
tip := instance.SideChain().GetPoolBlockByTemplateId(templateId)
if tip != nil {
tipId := tip.SideTemplateId(instance.Consensus())
chain, uncles := instance.SideChain().GetPoolBlocksFromTip(tipId)
result := p2pooltypes.P2PoolSideChainStateResult{
TipHeight: tip.Side.Height,
TipId: tipId,
Chain: make([]p2pooltypes.P2PoolBinaryBlockResult, 0, len(chain)),
Uncles: make([]p2pooltypes.P2PoolBinaryBlockResult, 0, len(uncles)),
}
for _, b := range chain {
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
for _, b := range uncles {
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("{}"))
}
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/sidechain/tip", func(writer http.ResponseWriter, request *http.Request) {
var result p2pooltypes.P2PoolBinaryBlockResult
if b := instance.SideChain().GetChainTip(); b != nil {
result.Version = int(b.ShareVersion())
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Error = err.Error()
} else {
result.Blob = blob
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
})
serveMux.HandleFunc("/sidechain/status", func(writer http.ResponseWriter, request *http.Request) {
result := p2pooltypes.P2PoolSideChainStatusResult{
Synchronized: instance.SideChain().PreCalcFinished(),
Blocks: instance.SideChain().GetPoolBlockCount(),
}
tip := instance.SideChain().GetHighestKnownTip()
if tip != nil {
result.Height = tip.Side.Height
result.Id = tip.SideTemplateId(instance.Consensus())
result.Difficulty = tip.Side.Difficulty
result.CumulativeDifficulty = tip.Side.CumulativeDifficulty
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
})
preAllocatedSharesPool := sidechain.NewPreAllocatedSharesPool(instance.Consensus().ChainWindowSize * 2)
// ================================= Archive section =================================
if archiveCache != nil {
serveMux.HandleFunc("/archive/window_from_template_id/{id:[0-9a-f]+}", func(writer http.ResponseWriter, request *http.Request) {
if templateId, err := types.HashFromString(mux.Vars(request)["id"]); err == nil {
tpls := archiveCache.LoadByTemplateId(templateId)
if len(tpls) > 0 {
tip := tpls[0]
mainDifficulty := instance.GetDifficultyByHeight(randomx.SeedHeight(tip.Main.Coinbase.GenHeight))
expectedBlocks := int(mainDifficulty.Mul64(2).Div(tip.Side.Difficulty).Lo) + 20
if expectedBlocks < 100 {
expectedBlocks = 100
}
if expectedBlocks > int(instance.Consensus().ChainWindowSize) {
expectedBlocks = int(instance.Consensus().ChainWindowSize)
}
if tip.ShareVersion() == sidechain.ShareVersion_V1 {
expectedBlocks = int(instance.Consensus().ChainWindowSize)
}
var windowLock sync.RWMutex
window := make(sidechain.UniquePoolBlockSlice, 0, expectedBlocks)
window = append(window, tip)
getByTemplateId := func(id types.Hash) *sidechain.PoolBlock {
if b := func() *sidechain.PoolBlock {
windowLock.RLock()
defer windowLock.RUnlock()
if b := window.Get(id); b != nil {
return b
}
return nil
}(); b != nil {
return b
}
windowLock.Lock()
defer windowLock.Unlock()
if bs := archiveCache.LoadByTemplateId(id); len(bs) > 0 {
window = append(window, bs[0])
return bs[0]
}
return nil
}
preAllocatedShares := preAllocatedSharesPool.Get()
defer preAllocatedSharesPool.Put(preAllocatedShares)
if _, err = tip.PreProcessBlock(instance.Consensus(), instance.SideChain().DerivationCache(), preAllocatedShares, instance.GetDifficultyByHeight, getByTemplateId); err == nil {
result := p2pooltypes.P2PoolSideChainStateResult{
TipHeight: tip.Side.Height,
TipId: tip.SideTemplateId(instance.Consensus()),
Chain: make([]p2pooltypes.P2PoolBinaryBlockResult, 0, expectedBlocks),
Uncles: make([]p2pooltypes.P2PoolBinaryBlockResult, 0, expectedBlocks/5),
}
var topError error
topError = sidechain.IterateBlocksInPPLNSWindow(tip, instance.Consensus(), instance.GetDifficultyByHeight, getByTemplateId, nil, func(e sidechain.PoolBlockWindowSlot) {
if _, err = e.Block.PreProcessBlock(instance.Consensus(), instance.SideChain().DerivationCache(), preAllocatedShares, instance.GetDifficultyByHeight, getByTemplateId); err != nil {
topError = err
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(e.Block.ShareVersion()),
Error: err.Error(),
})
} else if blob, err := e.Block.AppendBinaryFlags(make([]byte, 0, e.Block.BufferLength()), false, false); err != nil {
topError = err
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(e.Block.ShareVersion()),
Error: err.Error(),
})
} else {
result.Chain = append(result.Chain, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(e.Block.ShareVersion()),
Blob: blob,
})
}
for _, u := range e.Uncles {
if _, err = u.PreProcessBlock(instance.Consensus(), instance.SideChain().DerivationCache(), preAllocatedShares, instance.GetDifficultyByHeight, getByTemplateId); err != nil {
topError = err
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(u.ShareVersion()),
Error: err.Error(),
})
} else if blob, err := u.AppendBinaryFlags(make([]byte, 0, u.BufferLength()), false, false); err != nil {
topError = err
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(u.ShareVersion()),
Error: err.Error(),
})
} else {
result.Uncles = append(result.Uncles, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(u.ShareVersion()),
Blob: blob,
})
}
}
})
if topError == nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
return
}
}
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
_, _ = writer.Write([]byte("{}"))
})
serveMux.HandleFunc("/archive/store_alternate", func(writer http.ResponseWriter, request *http.Request) {
if buf, err := io.ReadAll(request.Body); err != nil {
return
} else {
b := &sidechain.PoolBlock{}
if err = b.UnmarshalBinary(instance.Consensus(), instance.SideChain().DerivationCache(), buf); err != nil {
return
}
if b.NeedsPreProcess() {
return
}
templateId := b.SideTemplateId(instance.Consensus())
if bytes.Compare(b.CoinbaseExtra(sidechain.SideTemplateId), templateId[:]) != 0 {
return
}
if archiveCache.LoadByMainId(b.MainId()) != nil {
return
}
existingBlock := archiveCache.LoadByTemplateId(templateId)
if len(existingBlock) == 0 {
return
}
tempData, _ := existingBlock[0].MarshalBinary()
newBlock := &sidechain.PoolBlock{}
if err = newBlock.UnmarshalBinary(instance.Consensus(), instance.SideChain().DerivationCache(), tempData); err != nil {
return
}
//set extra nonce and nonce
newBlock.Main.Coinbase.Extra[1] = b.Main.Coinbase.Extra[1]
newBlock.Main.Nonce = b.Main.Nonce
newBlock.Depth.Store(math.MaxUint64)
if !newBlock.IsProofHigherThanDifficulty(instance.Consensus().GetHasher(), func(height uint64) (hash types.Hash) {
seedHeight := randomx.SeedHeight(height)
if h := instance.GetMinimalBlockHeaderByHeight(seedHeight); h != nil {
return h.Id
} else {
return types.ZeroHash
}
}) {
return
}
//finally store alternate
archiveCache.Store(newBlock)
}
})
serveMux.HandleFunc("/archive/blocks_by_template_id/{id:[0-9a-f]+}", func(writer http.ResponseWriter, request *http.Request) {
if templateId, err := types.HashFromString(mux.Vars(request)["id"]); err == nil {
result := make([]p2pooltypes.P2PoolBinaryBlockResult, 0, 1)
for _, b := range archiveCache.LoadByTemplateId(templateId) {
if err := archiveCache.ProcessBlock(b); err != nil {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("[]"))
}
})
serveMux.HandleFunc("/archive/blocks_by_side_height/{height:[0-9]+}", func(writer http.ResponseWriter, request *http.Request) {
if height, err := strconv.ParseUint(mux.Vars(request)["height"], 10, 0); err == nil {
result := make([]p2pooltypes.P2PoolBinaryBlockResult, 0, 1)
for _, b := range archiveCache.LoadBySideChainHeight(height) {
if err := archiveCache.ProcessBlock(b); err != nil {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("[]"))
}
})
serveMux.HandleFunc("/archive/light_blocks_by_main_height/{height:[0-9]+}", func(writer http.ResponseWriter, request *http.Request) {
if height, err := strconv.ParseUint(mux.Vars(request)["height"], 10, 0); err == nil {
if blocks := archiveCache.LoadByMainChainHeight(height); len(blocks) > 0 {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, blocks)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
if blocks == nil {
blocks = make(sidechain.UniquePoolBlockSlice, 0)
}
_ = cmdutils.EncodeJson(request, writer, blocks)
}
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/archive/light_blocks_by_side_height/{height:[0-9]+}", func(writer http.ResponseWriter, request *http.Request) {
if height, err := strconv.ParseUint(mux.Vars(request)["height"], 10, 0); err == nil {
if blocks := archiveCache.LoadBySideChainHeight(height); len(blocks) > 0 {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, blocks)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
if blocks == nil {
blocks = make(sidechain.UniquePoolBlockSlice, 0)
}
_ = cmdutils.EncodeJson(request, writer, blocks)
}
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/archive/light_blocks_by_template_id/{id:[0-9a-f]+}", func(writer http.ResponseWriter, request *http.Request) {
if mainId, err := types.HashFromString(mux.Vars(request)["id"]); err == nil {
if blocks := archiveCache.LoadByTemplateId(mainId); len(blocks) > 0 {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, blocks)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
if blocks == nil {
blocks = make(sidechain.UniquePoolBlockSlice, 0)
}
_ = cmdutils.EncodeJson(request, writer, blocks)
}
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/archive/light_block_by_main_id/{id:[0-9a-f]+}", func(writer http.ResponseWriter, request *http.Request) {
params := request.URL.Query()
var templateIdHint types.Hash
if params.Has("templateIdHint") {
if h, err := types.HashFromString(params.Get("templateIdHint")); err == nil {
templateIdHint = h
}
}
if mainId, err := types.HashFromString(mux.Vars(request)["id"]); err == nil {
var b *sidechain.PoolBlock
if templateIdHint != types.ZeroHash {
// Fast lookup on sidechain
if b = instance.SideChain().GetPoolBlockByTemplateId(templateIdHint); b == nil || b.MainId() != mainId {
b = nil
}
}
// Fallback
if b == nil {
b = archiveCache.LoadByMainId(mainId)
}
if b != nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, b)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusNotFound)
_, _ = writer.Write([]byte("{}"))
}
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusInternalServerError)
_, _ = writer.Write([]byte("{}"))
}
})
serveMux.HandleFunc("/archive/block_by_main_id/{id:[0-9a-f]+}", func(writer http.ResponseWriter, request *http.Request) {
params := request.URL.Query()
var templateIdHint types.Hash
if params.Has("templateIdHint") {
if h, err := types.HashFromString(params.Get("templateIdHint")); err == nil {
templateIdHint = h
}
}
if mainId, err := types.HashFromString(mux.Vars(request)["id"]); err == nil {
var result p2pooltypes.P2PoolBinaryBlockResult
var b *sidechain.PoolBlock
if templateIdHint != types.ZeroHash {
// Fast lookup on sidechain
if b = instance.SideChain().GetPoolBlockByTemplateId(templateIdHint); b == nil || b.MainId() != mainId {
b = nil
}
}
// Fallback
if b == nil {
b = archiveCache.LoadByMainId(mainId)
}
if b != nil {
result.Version = int(b.ShareVersion())
if err := archiveCache.ProcessBlock(b); err != nil {
result.Error = err.Error()
} else {
if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result.Error = err.Error()
} else {
result.Blob = blob
}
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte(""))
}
})
serveMux.HandleFunc("/archive/blocks_by_main_height/{height:[0-9]+}", func(writer http.ResponseWriter, request *http.Request) {
if height, err := strconv.ParseUint(mux.Vars(request)["height"], 10, 0); err == nil {
result := make([]p2pooltypes.P2PoolBinaryBlockResult, 0, 1)
for _, b := range archiveCache.LoadByMainChainHeight(height) {
if err := archiveCache.ProcessBlock(b); err != nil {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else if blob, err := b.AppendBinaryFlags(make([]byte, 0, b.BufferLength()), false, false); err != nil {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Error: err.Error(),
})
} else {
result = append(result, p2pooltypes.P2PoolBinaryBlockResult{
Version: int(b.ShareVersion()),
Blob: blob,
})
}
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_ = cmdutils.EncodeJson(request, writer, result)
} else {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte("[]"))
}
})
}
return serveMux
}

Binary file not shown.

View file

@ -1,43 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/p2pool
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/httputils v0.0.0 => ../httputils
replace git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0 => ../../p2pool/cache
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/httputils v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0
github.com/gorilla/mux v1.8.1
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/floatdrop/lru v1.3.0 // indirect
github.com/go-zeromq/goczmq/v4 v4.2.2 // indirect
github.com/go-zeromq/zmq4 v0.16.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
go.etcd.io/bbolt v1.3.9 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,56 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/go-zeromq/goczmq/v4 v4.2.2 h1:HAJN+i+3NW55ijMJJhk7oWxHKXgAuSBkoFfvr8bYj4U=
github.com/go-zeromq/goczmq/v4 v4.2.2/go.mod h1:Sm/lxrfxP/Oxqs0tnHD6WAhwkWrx+S+1MRrKzcxoaYE=
github.com/go-zeromq/zmq4 v0.16.0 h1:D6oIPWSdkY/4DJu4tBUmo28P3WRq4F4Ji4/iQ/fJHc0=
github.com/go-zeromq/zmq4 v0.16.0/go.mod h1:8c3aXloJBRPba1AqWMJK4vypniM+yC+JKqi8KpRaDFc=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,318 +0,0 @@
package main
import (
"bufio"
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/p2p"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"net"
"net/http"
_ "net/http/pprof"
"net/netip"
"os"
"os/signal"
"runtime"
"runtime/debug"
"slices"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
func main() {
currentConsensus := sidechain.ConsensusDefault
//monerod related
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")
// consensus related
consensusConfigFile := flag.String("consensus-config", "", "Name of the p2pool consensus config file")
useMiniSidechain := flag.Bool("mini", false, "Connect to p2pool-mini sidechain. Note that it will also change default p2p port.")
//p2p peering related
p2pListen := flag.String("p2p", fmt.Sprintf("0.0.0.0:%d", currentConsensus.DefaultPort()), "IP:port for p2p server to listen on.")
p2pExternalPort := flag.Uint64("p2p-external-port", 0, "Port number that your router uses for mapping to your local p2p port. Use it if you are behind a NAT and still want to accept incoming connections")
outPeers := flag.Uint64("out-peers", 10, "Maximum number of outgoing connections for p2p server (any value between 10 and 450)")
inPeers := flag.Uint64("in-peers", 10, "Maximum number of incoming connections for p2p server (any value between 10 and 450)")
addPeers := flag.String("addpeers", "", "Comma-separated list of IP:port of other p2pool nodes to connect to")
addSelf := flag.Bool("add-self-peer", false, "Adds itself to the peer list regularly, based on found local interfaces for IPv4/IPv6")
peerList := flag.String("peer-list", "p2pool_peers.txt", "Either a path or an URL to obtain peer lists from. If it is a path, new peers will be saved to this path. Set to empty to disable")
//other settings
lightMode := flag.Bool("light-mode", false, "Don't allocate RandomX dataset, saves 2GB of RAM")
noDns := flag.Bool("no-dns", false, "Disable DNS queries, use only IP addresses to connect to peers (seed node DNS will be unavailable too)")
memoryLimitInGiB := flag.Uint64("memory-limit", 0, "Memory limit for go managed sections in GiB, set 0 to disable")
apiBind := flag.String("api-bind", "", "Bind to this address to serve blocks, and other utility methods. If -archive is specified, serve archived blocks.")
createArchive := flag.String("archive", "", "If specified, create an archive store of sidechain blocks on this path.")
blockCache := flag.String("block-cache", "p2pool.cache", "Block cache for faster startups. Set to empty to disable")
//testing settings
doDebug := flag.Bool("debug", false, "Log more details. Default false")
ipv6Only := flag.Bool("ipv6-only", false, "Use only IPv6. Default false")
debugListen := flag.String("debug-listen", "", "Provide a bind address and port to expose a pprof HTTP API on it.")
//TODO extend verbosity to debug flag
flag.Parse()
if *doDebug {
utils.LogFile = true
}
if buildInfo, _ := debug.ReadBuildInfo(); buildInfo != nil {
utils.Logf("P2Pool", "Consensus Software %s %s (go version %s)", types.CurrentSoftwareId, types.CurrentSoftwareVersion, buildInfo.GoVersion)
} else {
utils.Logf("P2Pool", "Consensus Software %s %s (go version %s)", types.CurrentSoftwareId, types.CurrentSoftwareVersion, runtime.Version())
}
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
debug.SetTraceback("all")
if *memoryLimitInGiB != 0 {
debug.SetMemoryLimit(int64(*memoryLimitInGiB) * 1024 * 1024 * 1024)
}
settings := make(map[string]string)
settings["listen"] = *p2pListen
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 = 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 != "" {
consensusData, err := os.ReadFile(*consensusConfigFile)
if err != nil {
utils.Panic(err)
}
if newConsensus, err := sidechain.NewConsensusFromJSON(consensusData); err != nil {
utils.Panic(err)
} else {
changeConsensus(newConsensus)
}
}
settings["out-peers"] = strconv.FormatUint(*outPeers, 10)
settings["in-peers"] = strconv.FormatUint(*inPeers, 10)
settings["external-port"] = strconv.FormatUint(*p2pExternalPort, 10)
if *ipv6Only {
settings["ipv6-only"] = "true"
}
if *blockCache != "" {
settings["cache"] = *blockCache
}
if *createArchive != "" {
settings["archive"] = *createArchive
}
if !*lightMode {
settings["full-mode"] = "true"
}
if instance, err := NewP2Pool(currentConsensus, settings); err != nil {
utils.Fatalf("Could not start p2pool: %s", err)
} else {
defer instance.Close(nil)
if *apiBind != "" {
serveMux := getServerMux(instance)
server := &http.Server{
Addr: *apiBind,
ReadTimeout: time.Second * 2,
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Method != "GET" && request.Method != "HEAD" && request.Method != "POST" {
writer.WriteHeader(http.StatusForbidden)
return
}
utils.Logf("API", "Handling %s %s", request.Method, request.URL.String())
serveMux.ServeHTTP(writer, request)
}),
}
go func() {
//TODO: context/wait
if err := server.ListenAndServe(); err != nil {
utils.Panic(err)
}
}()
}
if *debugListen != "" {
go func() {
if err := http.ListenAndServe(*debugListen, nil); err != nil {
utils.Panic(err)
}
}()
}
var connectList []netip.AddrPort
for _, peerAddr := range strings.Split(*addPeers, ",") {
if peerAddr == "" {
continue
}
//TODO: dns resolution of hosts
if addrPort, err := netip.ParseAddrPort(peerAddr); err != nil {
utils.Panic(err)
} else {
instance.Server().AddToPeerList(addrPort)
connectList = append(connectList, addrPort)
}
}
if !*noDns && currentConsensus.SeedNode() != "" {
utils.Logf("P2Pool", "Loading seed peers from %s", currentConsensus.SeedNode())
records, _ := net.LookupTXT(currentConsensus.SeedNode())
if len(records) > 0 {
for _, r := range records {
for _, e := range strings.Split(r, ",") {
if seedNodeAddr, err := netip.ParseAddrPort(e); err == nil {
instance.Server().AddToPeerList(seedNodeAddr)
}
}
}
} else {
ips, _ := net.LookupIP(currentConsensus.SeedNode())
for _, seedNodeIp := range ips {
seedNodeAddr := netip.AddrPortFrom(netip.MustParseAddr(seedNodeIp.String()), currentConsensus.DefaultPort())
instance.Server().AddToPeerList(seedNodeAddr)
}
}
}
if *peerList != "" {
utils.Logf("P2Pool", "Loading peers from %s", *peerList)
if len(*peerList) > 4 && (*peerList)[:4] == "http" {
func() {
r, err := http.DefaultClient.Get(*peerList)
if err == nil {
defer r.Body.Close()
scanner := bufio.NewScanner(r.Body)
for scanner.Scan() {
if addrPort, err := netip.ParseAddrPort(strings.TrimSpace(scanner.Text())); err == nil {
instance.Server().AddToPeerList(addrPort)
}
}
}
}()
} else {
func() {
f, err := os.Open(*peerList)
if err == nil {
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if addrPort, err := netip.ParseAddrPort(strings.TrimSpace(scanner.Text())); err == nil {
instance.Server().AddToPeerList(addrPort)
}
}
}
}()
go func() {
contents := make([]byte, 0, 4096)
for range utils.ContextTick(instance.Context(), time.Minute*1) {
contents = contents[:0]
peerListEntries := instance.Server().PeerList()
slices.SortFunc(peerListEntries, func(a, b *p2p.PeerListEntry) int {
return a.AddressPort.Compare(b.AddressPort)
})
for _, addrPort := range peerListEntries {
contents = append(contents, []byte(addrPort.AddressPort.String())...)
contents = append(contents, '\n')
}
if err := os.WriteFile(*peerList, contents, 0644); err != nil {
utils.Errorf("P2Pool", "error writing peer list %s: %s", *peerList, err.Error())
break
}
}
}()
}
}
if *addSelf {
if addrs, err := utils.GetOutboundIPv6(); err != nil {
utils.Errorf("P2Pool", "Could not get interface ipv6 addresses: %s", err)
} else {
for _, addr := range addrs {
if addr.Is6() {
//use own port directly?
instance.Server().AddToPeerList(netip.AddrPortFrom(addr, instance.Server().ListenPort()))
} else {
instance.Server().AddToPeerList(netip.AddrPortFrom(addr, instance.Server().ExternalListenPort()))
}
}
}
}
go func() {
for !instance.Started() {
time.Sleep(time.Second * 1)
}
var wg sync.WaitGroup
for _, addrPort := range connectList {
wg.Add(1)
go func(addrPort netip.AddrPort) {
defer wg.Done()
if err := instance.Server().Connect(addrPort); err != nil {
utils.Errorf("error connecting to initial peer %s: %s", addrPort.String(), err.Error())
}
}(addrPort)
}
wg.Wait()
instance.Server().UpdateClientConnections()
}()
sigHandler := make(chan os.Signal, 1)
signal.Notify(sigHandler, syscall.SIGINT)
go func() {
for s := range sigHandler {
if s == syscall.SIGKILL || s == syscall.SIGINT {
instance.Close(nil)
}
}
}()
if err := instance.Run(); err != nil {
instance.Close(err)
if instance.Started() {
instance.WaitUntilClosed()
}
if closeError := instance.CloseError(); closeError != nil {
utils.Panic(err)
} else {
os.Exit(0)
}
}
}
}

View file

@ -1,627 +0,0 @@
package main
import (
"context"
"encoding/binary"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/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/cache"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/archive"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/legacy"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/mainchain"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/mempool"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/p2p"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"slices"
"strconv"
"sync"
"sync/atomic"
"time"
)
type EventListener struct {
ListenerId uint64
Tip func(tip *sidechain.PoolBlock)
Broadcast func(b *sidechain.PoolBlock)
Found func(data *sidechain.ChainMain, b *sidechain.PoolBlock)
MainData func(data *sidechain.ChainMain)
MinerData func(data *p2pooltypes.MinerData)
MempoolData func(data mempool.Mempool)
}
type P2Pool struct {
consensus *sidechain.Consensus
sidechain *sidechain.SideChain
mainchain *mainchain.MainChain
archive cache.AddressableCache
cache cache.HeapCache
server *p2p.Server
ctx context.Context
ctxCancel context.CancelFunc
rpcClient *client.Client
zmqClient *zmq.Client
recentSubmittedBlocks *utils.CircularBuffer[types.Hash]
listenersLock sync.RWMutex
listeners []EventListener
nextListenerId uint64
started atomic.Bool
closeError error
closed chan struct{}
}
// AddListener Registers listener to several events produced centrally.
// Note that you should process events called as fast as possible, or spawn a new goroutine to not block
func (p *P2Pool) AddListener(tip func(tip *sidechain.PoolBlock), broadcast func(b *sidechain.PoolBlock), found func(data *sidechain.ChainMain, b *sidechain.PoolBlock), mainData func(data *sidechain.ChainMain), minerData func(data *p2pooltypes.MinerData), mempoolData func(data mempool.Mempool)) (listenerId uint64) {
p.listenersLock.Lock()
p.listenersLock.Unlock()
listenerId = p.nextListenerId
p.nextListenerId++
p.listeners = append(p.listeners, EventListener{
ListenerId: listenerId,
Tip: tip,
Broadcast: broadcast,
Found: found,
MainData: mainData,
MinerData: minerData,
MempoolData: mempoolData,
})
return listenerId
}
func (p *P2Pool) RemoveListener(listenerId uint64) bool {
p.listenersLock.Lock()
p.listenersLock.Unlock()
if i := slices.IndexFunc(p.listeners, func(listener EventListener) bool {
return listener.ListenerId == listenerId
}); i != -1 {
p.listeners = slices.Delete(p.listeners, i, i+1)
return true
}
return false
}
func (p *P2Pool) GetBlob(key []byte) (blob []byte, err error) {
return nil, errors.New("not found")
}
func (p *P2Pool) SetBlob(key, blob []byte) (err error) {
return nil
}
func (p *P2Pool) RemoveBlob(key []byte) (err error) {
return nil
}
func (p *P2Pool) AddressableCache() cache.AddressableCache {
return p.archive
}
func (p *P2Pool) Cache() cache.Cache {
return p.cache
}
func (p *P2Pool) CloseError() error {
return p.closeError
}
func (p *P2Pool) WaitUntilClosed() {
<-p.closed
}
func (p *P2Pool) Close(err error) {
started := p.started.Swap(false)
if started {
p.closeError = err
}
p.ctxCancel()
_ = p.zmqClient.Close()
if p.server != nil {
p.server.Close()
}
if p.cache != nil {
p.cache.Close()
}
if p.archive != nil {
p.archive.Close()
}
if started && err != nil {
close(p.closed)
utils.Panicf("[P2Pool] Closed due to error %s", err)
} else if started {
close(p.closed)
utils.Logf("P2Pool", "Closed")
} else if err != nil {
utils.Logf("P2Pool", "Received extra error during closing %s", err)
}
}
func NewP2Pool(consensus *sidechain.Consensus, settings map[string]string) (*P2Pool, error) {
if settings["full-mode"] == "true" {
if err := consensus.InitHasher(2, randomx.FlagSecure, randomx.FlagFullMemory); err != nil {
return nil, err
}
} else {
if err := consensus.InitHasher(2, randomx.FlagSecure); err != nil {
return nil, err
}
}
pool := &P2Pool{
consensus: consensus,
recentSubmittedBlocks: utils.NewCircularBuffer[types.Hash](8),
closed: make(chan struct{}),
}
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, zmq.TopicMinimalTxPoolAdd)
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 archivePath, ok := settings["archive"]; ok {
if pool.archive, err = archive.NewCache(archivePath, pool.consensus, pool.GetDifficultyByHeight); err != nil {
return nil, fmt.Errorf("could not create cache: %w", err)
}
}
if cachePath, ok := settings["cache"]; ok {
if pool.cache, err = legacy.NewCache(consensus, cachePath); err != nil {
return nil, fmt.Errorf("could not create cache: %w", err)
}
}
if addr, ok := settings["listen"]; ok {
listenAddress = addr
}
maxOutgoingPeers := uint64(10)
if outgoingPeers, ok := settings["out-peers"]; ok {
maxOutgoingPeers, _ = strconv.ParseUint(outgoingPeers, 10, 0)
}
maxIncomingPeers := uint64(450)
if incomingPeers, ok := settings["in-peers"]; ok {
maxIncomingPeers, _ = strconv.ParseUint(incomingPeers, 10, 0)
}
externalListenPort := uint64(0)
if externalPort, ok := settings["external-port"]; ok {
externalListenPort, _ = strconv.ParseUint(externalPort, 10, 0)
}
useIPv4, useIPv6 := true, true
if b, ok := settings["ipv6-only"]; ok && b == "true" {
useIPv4 = false
useIPv6 = true
}
if pool.server, err = p2p.NewServer(pool, listenAddress, uint16(externalListenPort), uint32(maxOutgoingPeers), uint32(maxIncomingPeers), useIPv4, useIPv6, pool.ctx); err != nil {
return nil, err
}
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()
}
func (p *P2Pool) GetMinerDataTip() *p2pooltypes.MinerData {
return p.mainchain.GetMinerDataTip()
}
// GetMinimalBlockHeaderByHeight Only Id / Height / Timestamp are assured
func (p *P2Pool) GetMinimalBlockHeaderByHeight(height uint64) *block.Header {
lowerThanTip := height <= p.mainchain.GetChainMainTip().Height
if chainMain := p.mainchain.GetChainMainByHeight(height); chainMain != nil && chainMain.Id != types.ZeroHash {
prev := p.mainchain.GetChainMainByHeight(height - 1)
if prev != nil {
return &block.Header{
Timestamp: chainMain.Timestamp,
Height: chainMain.Height,
Reward: chainMain.Reward,
Difficulty: chainMain.Difficulty,
Id: chainMain.Id,
PreviousId: prev.Id,
}
} else {
return &block.Header{
Timestamp: chainMain.Timestamp,
Height: chainMain.Height,
Reward: chainMain.Reward,
Difficulty: chainMain.Difficulty,
Id: chainMain.Id,
}
}
} else if lowerThanTip {
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),
}
return blockHeader
}
}
return nil
}
func (p *P2Pool) GetDifficultyByHeight(height uint64) types.Difficulty {
lowerThanTip := height <= p.mainchain.GetChainMainTip().Height
if chainMain := p.mainchain.GetChainMainByHeight(height); chainMain != nil && chainMain.Difficulty != types.ZeroDifficulty {
return chainMain.Difficulty
} else if lowerThanTip {
//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),
}
return blockHeader.Difficulty
}
}
return types.ZeroDifficulty
}
// 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 || header == nil {
return nil
} else {
prevHash, _ := types.HashFromString(header.PrevHash)
h, _ := types.HashFromString(header.Hash)
blockHeader := &block.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),
}
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) SideChain() *sidechain.SideChain {
return p.sidechain
}
func (p *P2Pool) MainChain() *mainchain.MainChain {
return p.mainchain
}
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
}
go func() {
//TODO: redo listen
if err := p.mainchain.Listen(); err != nil {
p.Close(err)
}
}()
//TODO: move peer list loading here
if p.cache != nil {
p.cache.LoadAll(p.Server())
}
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 {
utils.Logf("P2Pool", "monerod is busy syncing, trying again in 5 seconds")
time.Sleep(time.Second * 5)
return p.getInfo()
} else if !info.Synchronized {
utils.Logf("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
}
func (p *P2Pool) getVersion() error {
if version, err := p.ClientRPC().GetVersion(); err != nil {
return err
} else {
if version.Version < monero.RequiredMoneroVersion {
return fmt.Errorf("monerod RPC v%d.%d is incompatible, update to RPC >= v%d.%d (Monero %s or newer)", version.Version>>16, version.Version&0xffff, monero.RequiredMajor, monero.RequiredMinor, monero.RequiredMoneroString)
}
}
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)
data := &p2pooltypes.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(),
}
data.TxBacklog = make(mempool.Mempool, len(minerData.TxBacklog))
for i, e := range minerData.TxBacklog {
txId, _ := types.HashFromString(e.Id)
data.TxBacklog[i] = &mempool.MempoolEntry{
Id: txId,
BlobSize: e.BlobSize,
Weight: e.Weight,
Fee: e.Fee,
}
}
p.mainchain.HandleMinerData(data)
return p.mainchain.DownloadBlockHeaders(minerData.Height)
}
}
func (p *P2Pool) Consensus() *sidechain.Consensus {
return p.consensus
}
func (p *P2Pool) UpdateMainData(data *sidechain.ChainMain) {
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for i := range p.listeners {
if p.listeners[i].MainData != nil {
p.listeners[i].MainData(data)
}
}
}
func (p *P2Pool) UpdateMempoolData(data mempool.Mempool) {
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for i := range p.listeners {
if p.listeners[i].MempoolData != nil {
p.listeners[i].MempoolData(data)
}
}
}
func (p *P2Pool) UpdateMinerData(data *p2pooltypes.MinerData) {
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for i := range p.listeners {
if p.listeners[i].MinerData != nil {
p.listeners[i].MinerData(data)
}
}
}
func (p *P2Pool) UpdateBlockFound(data *sidechain.ChainMain, block *sidechain.PoolBlock) {
utils.Logf("P2Pool", "BLOCK FOUND: main chain block at height %d, id %s was mined by this p2pool", data.Height, data.Id)
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for i := range p.listeners {
if p.listeners[i].Found != nil {
p.listeners[i].Found(data, block)
}
}
}
func (p *P2Pool) SubmitBlock(b *block.Block) {
go func() {
//do not submit multiple monero blocks to monerod
if !p.recentSubmittedBlocks.PushUnique(b.Id()) {
return
}
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)
}
utils.Logf("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 {
utils.Logf("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" {
utils.Logf("P2Pool", "submit_block: BLOCK ACCEPTED at height = %d, template id = %s", b.Coinbase.GenHeight, templateId.String())
} else {
utils.Logf("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) Store(block *sidechain.PoolBlock) {
if p.cache != nil {
p.cache.Store(block)
}
if p.archive != nil {
p.archive.Store(block)
}
}
func (p *P2Pool) ClearCachedBlocks() {
p.server.ClearCachedBlocks()
}
func (p *P2Pool) Started() bool {
return p.started.Load()
}
func (p *P2Pool) UpdateTip(tip *sidechain.PoolBlock) {
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for i := range p.listeners {
if p.listeners[i].Tip != nil {
p.listeners[i].Tip(tip)
}
}
}
func (p *P2Pool) Broadcast(block *sidechain.PoolBlock) {
minerData := p.GetMinerDataTip()
if (block.Main.Coinbase.GenHeight)+2 < minerData.Height {
utils.Logf("P2Pool", "Trying to broadcast a stale block %s (mainchain height %d, current height is %d)", block.SideTemplateId(p.consensus), block.Main.Coinbase.GenHeight, minerData.Height)
return
}
if block.Main.Coinbase.GenHeight > (minerData.Height + 2) {
utils.Logf("P2Pool", "Trying to broadcast a block %s ahead on mainchain (mainchain height %d, current height is %d)", block.SideTemplateId(p.consensus), block.Main.Coinbase.GenHeight, minerData.Height)
return
}
p.server.Broadcast(block)
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for i := range p.listeners {
if p.listeners[i].Broadcast != nil {
p.listeners[i].Broadcast(block)
}
}
}

View file

@ -1,34 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/readcache
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0 => ../../p2pool/cache
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/floatdrop/lru v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,44 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,67 +0,0 @@
package main
import (
"flag"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache/legacy"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"os"
"path"
)
type loadee struct {
c *sidechain.Consensus
cb func(block *sidechain.PoolBlock)
}
func (l *loadee) Consensus() *sidechain.Consensus {
return l.c
}
func (l *loadee) AddCachedBlock(block *sidechain.PoolBlock) {
l.cb(block)
}
func main() {
inputConsensus := flag.String("consensus", "config.json", "Input config.json consensus file")
inputFile := flag.String("input", "p2pool.cache", "Input p2pool.cache path")
outputFolder := flag.String("output", "shares", "Output path for extracted shares")
flag.Parse()
cf, err := os.ReadFile(*inputConsensus)
consensus, err := sidechain.NewConsensusFromJSON(cf)
if err != nil {
utils.Panic(err)
}
cache, err := legacy.NewCache(consensus, *inputFile)
if err != nil {
utils.Panic(err)
}
defer cache.Close()
l := &loadee{
c: consensus,
cb: func(block *sidechain.PoolBlock) {
expectedBlockId := types.HashFromBytes(block.CoinbaseExtra(sidechain.SideTemplateId))
calculatedBlockId := block.SideTemplateId(consensus)
if expectedBlockId != calculatedBlockId {
utils.Errorf("", "block height %d, template id %s, expected %s", block.Side.Height, calculatedBlockId, expectedBlockId)
} else {
blob, err := block.MarshalBinary()
if err != nil {
utils.Panic(err)
}
utils.Logf("", "block height %d, template id %s, version %d", block.Side.Height, calculatedBlockId, block.ShareVersion())
_ = os.WriteFile(path.Join(*outputFolder, expectedBlockId.String()+".raw"), blob, 0664)
}
},
}
cache.LoadAll(l)
}

View file

@ -1,35 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/recoverpoolblock
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0 => ../../p2pool/cache
require (
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/cache v0.0.0
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a
github.com/floatdrop/lru v1.3.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
go.etcd.io/bbolt v1.3.9 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,47 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

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

View file

@ -1,107 +0,0 @@
package main
import (
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
)
type JsonBlock2 struct {
MinerMainId types.Hash `json:"miner_main_id"`
CoinbaseReward uint64 `json:"coinbase_reward,string"`
CoinbaseId types.Hash `json:"coinbase_id"`
Version uint64 `json:"version,string"`
Diff types.Difficulty `json:"diff"`
Wallet *address.Address `json:"wallet"`
MinerMainDiff types.Difficulty `json:"miner_main_diff"`
Id types.Hash `json:"id"`
Height uint64 `json:"height,string"`
PowHash types.Hash `json:"pow_hash"`
MainId types.Hash `json:"main_id"`
MainHeight uint64 `json:"main_height,string"`
Ts uint64 `json:"ts,string"`
PrevId types.Hash `json:"prev_id"`
CoinbasePriv crypto.PrivateKeyBytes `json:"coinbase_priv"`
Lts uint64 `json:"lts,string"`
MainFound string `json:"main_found,omitempty"`
Uncles []struct {
MinerMainId types.Hash `json:"miner_main_id"`
CoinbaseReward uint64 `json:"coinbase_reward,string"`
CoinbaseId types.Hash `json:"coinbase_id"`
Version uint64 `json:"version,string"`
Diff types.Difficulty `json:"diff"`
Wallet *address.Address `json:"wallet"`
MinerMainDiff types.Difficulty `json:"miner_main_diff"`
Id types.Hash `json:"id"`
Height uint64 `json:"height,string"`
PowHash types.Hash `json:"pow_hash"`
MainId types.Hash `json:"main_id"`
MainHeight uint64 `json:"main_height,string"`
Ts uint64 `json:"ts,string"`
PrevId types.Hash `json:"prev_id"`
CoinbasePriv crypto.PrivateKeyBytes `json:"coinbase_priv"`
Lts uint64 `json:"lts,string"`
MainFound string `json:"main_found,omitempty"`
} `json:"uncles,omitempty"`
}
type JsonBlock1 struct {
Wallet *address.Address `json:"wallet"`
Height uint64 `json:"height,string"`
MHeight uint64 `json:"mheight,string"`
PrevId types.Hash `json:"prev_id"`
Ts uint64 `json:"ts,string"`
PowHash types.Hash `json:"pow_hash"`
Id types.Hash `json:"id"`
PrevHash types.Hash `json:"prev_hash"`
Diff uint64 `json:"diff,string"`
TxCoinbase types.Hash `json:"tx_coinbase"`
Lts uint64 `json:"lts,string"`
MHash types.Hash `json:"mhash"`
TxPriv crypto.PrivateKeyBytes `json:"tx_priv"`
TxPub crypto.PublicKeyBytes `json:"tx_pub"`
BlockFound string `json:"main_found,omitempty"`
Uncles []struct {
Diff uint64 `json:"diff,string"`
PrevId types.Hash `json:"prev_id"`
Ts uint64 `json:"ts,string"`
MHeight uint64 `json:"mheight,string"`
PrevHash types.Hash `json:"prev_hash"`
Height uint64 `json:"height,string"`
Wallet *address.Address `json:"wallet"`
Id types.Hash `json:"id"`
} `json:"uncles,omitempty"`
}
type versionBlock struct {
Version uint64 `json:"version,string"`
}
func JSONFromTemplate(data []byte) (any, error) {
var version versionBlock
if err := utils.UnmarshalJSON(data, &version); err != nil {
return nil, err
} else {
if version.Version == 2 {
var b JsonBlock2
if err = utils.UnmarshalJSON(data, &b); err != nil {
return nil, err
}
return b, nil
} else if version.Version == 0 || version.Version == 1 {
var b JsonBlock1
if err = utils.UnmarshalJSON(data, &b); err != nil {
return nil, err
}
return b, nil
} else {
return nil, fmt.Errorf("unknown version %d", version.Version)
}
}
}

View file

@ -1,38 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/scansweeps
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0 => ../index
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0 => ../utils
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/floatdrop/lru v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,46 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,84 +0,0 @@
package main
import (
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
p2poolapi "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"time"
)
func main() {
moneroHost := flag.String("host", "127.0.0.1", "IP address of your Monero node")
moneroRpcPort := flag.Uint("rpc-port", 18081, "monerod RPC API port number")
startFromHeight := flag.Uint64("from", 0, "Start sweep from this main height")
dbString := flag.String("db", "", "")
p2poolApiHost := flag.String("api-host", "", "Host URL for p2pool go observer consensus")
flag.Parse()
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
client.GetDefaultClient().SetThrottle(1000)
p2api := p2poolapi.NewP2PoolApi(*p2poolApiHost)
if err := p2api.WaitSync(); err != nil {
utils.Panic(err)
}
indexDb, err := index.OpenIndex(*dbString, p2api.Consensus(), p2api.DifficultyByHeight, p2api.SeedByHeight, p2api.ByTemplateId)
if err != nil {
utils.Panic(err)
}
defer indexDb.Close()
startHeight := *startFromHeight
var tipHeight uint64
if err := indexDb.Query("SELECT MIN(height), MAX(height) FROM main_blocks WHERE height > 0;", func(row index.RowScanInterface) error {
var minHeight uint64
if err := row.Scan(&minHeight, &tipHeight); err != nil {
return err
}
if minHeight > startHeight {
startHeight = minHeight
}
return nil
}); err != nil {
utils.Panic(err)
}
stopHeight := tipHeight - monero.MinerRewardUnlockTime
if tipHeight < monero.MinerRewardUnlockTime {
stopHeight = tipHeight
}
utils.Logf("", "Starting at height %d, stopping at %d", startHeight, stopHeight)
isProcessedPrevious := true
for height := startHeight; height <= stopHeight; height++ {
b := indexDb.GetMainBlockByHeight(height)
if b == nil {
utils.Logf("", "Block at %d is nil", height)
continue
}
if isProcessed, ok := b.GetMetadata("processed").(bool); ok && isProcessed && isProcessedPrevious {
utils.Logf("", "Block %s at %d (%s) has already been processed", b.Id, b.Height, time.Unix(int64(b.Timestamp), 0).UTC().Format("02-01-2006 15:04:05 MST"))
continue
}
isProcessedPrevious = false
if err := cmdutils.ProcessFullBlock(b, indexDb); err != nil {
utils.Logf("", "error processing block %s at %d: %s", b.Id, b.Height, err)
}
}
}

View file

@ -1,213 +0,0 @@
package utils
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
types2 "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"strings"
"time"
)
const (
JSONEventSideBlock = "side_block"
JSONEventFoundBlock = "found_block"
JSONEventOrphanedBlock = "orphaned_block"
JSONEventPayout = "payout"
)
type JSONEvent struct {
Type string `json:"type"`
SideBlock *index.SideBlock `json:"side_block,omitempty"`
FoundBlock *index.FoundBlock `json:"found_block,omitempty"`
MainCoinbaseOutputs index.MainCoinbaseOutputs `json:"main_coinbase_outputs,omitempty"`
Payout *index.Payout `json:"payout,omitempty"`
}
type VersionInfo struct {
Version string `json:"version"`
Timestamp int64 `json:"timestamp"`
Link string `json:"link"`
CheckedTimestamp int64 `json:"-"`
}
func (v VersionInfo) ShortVersion() types2.SemanticVersion {
parts := strings.Split(v.Version, ".")
for len(parts) < 2 {
parts = append(parts, "0")
}
for len(parts) > 2 {
parts = parts[:len(parts)-1]
}
return types2.SemanticVersionFromString(strings.Join(parts, "."))
}
type ReleaseDataJson struct {
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
PublishedAt time.Time `json:"published_at"`
}
type SideChainVersionEntry struct {
Weight types.Difficulty `json:"weight"`
Share float64 `json:"share"`
Count int `json:"count"`
SoftwareId types2.SoftwareId `json:"software_id"`
SoftwareVersion types2.SoftwareVersion `json:"software_version"`
SoftwareString string `json:"software_string"`
}
type PoolInfoResult struct {
SideChain PoolInfoResultSideChain `json:"sidechain"`
MainChain PoolInfoResultMainChain `json:"mainchain"`
Versions struct {
P2Pool VersionInfo `json:"p2pool"`
Monero VersionInfo `json:"monero"`
} `json:"versions"`
}
type PoolInfoResultSideChain struct {
// Consensus Specifies the consensus parameters for the backing p2pool instance
Consensus *sidechain.Consensus `json:"consensus"`
// LastBlock Last sidechain block on database
LastBlock *index.SideBlock `json:"last_block"`
// SecondsSinceLastBlock
// Prefer using max(0, time.Now().Unix()-int64(LastBlock .Timestamp)) instead
SecondsSinceLastBlock int64 `json:"seconds_since_last_block"`
// LastFound Last sidechain block on database found and accepted on Monero network
LastFound *index.FoundBlock `json:"last_found"`
Effort PoolInfoResultSideChainEffort `json:"effort"`
Window PoolInfoResultSideChainWindow `json:"window"`
// Found Total count of found blocks in database
Found uint64 `json:"found"`
// Miners Total count of miners in database
Miners uint64 `json:"miners"`
// Id Available on LastBlock .TemplateId
// Deprecated
Id types.Hash `json:"id"`
// Height Available on LastBlock .SideHeight
// Deprecated
Height uint64 `json:"height"`
// Version Available via sidechain.P2PoolShareVersion
// Deprecated
Version sidechain.ShareVersion `json:"version"`
// Difficulty Available on LastBlock .Difficulty
// Deprecated
Difficulty types.Difficulty `json:"difficulty"`
// CumulativeDifficulty Available on LastBlock .CumulativeDifficulty
// Deprecated
CumulativeDifficulty types.Difficulty `json:"cumulative_difficulty"`
// Timestamp Available on LastBlock .Timestamp
// Deprecated
Timestamp uint64 `json:"timestamp"`
// WindowSize Available on Window .Blocks
// Deprecated
WindowSize int `json:"window_size"`
// MaxWindowSize Available on Consensus
// Deprecated
MaxWindowSize int `json:"max_window_size"`
// BlockTime Available on Consensus
// Deprecated
BlockTime int `json:"block_time"`
// UnclePenalty Available on Consensus
// Deprecated
UnclePenalty int `json:"uncle_penalty"`
}
type PoolInfoResultSideChainEffortLastEntry struct {
Id types.Hash `json:"id"`
Effort float64 `json:"effort"`
}
type PoolInfoResultSideChainEffort struct {
Current float64 `json:"current"`
Average10 float64 `json:"average10"`
Average50 float64 `json:"average"`
Average200 float64 `json:"average200"`
Last []PoolInfoResultSideChainEffortLastEntry `json:"last"`
}
type PoolInfoResultSideChainWindow struct {
// Miners Unique miners found in window
Miners int `json:"miners"`
// Blocks total count of blocks in the window, including uncles
Blocks int `json:"blocks"`
Uncles int `json:"uncles"`
// Top TemplateId of the window tip
Top types.Hash `json:"top"`
// Bottom TemplateId of the last non-uncle block in the window
Bottom types.Hash `json:"bottom"`
// BottomUncles TemplateId of the uncles included under the last block, if any
BottomUncles []types.Hash `json:"bottom_uncles,omitempty"`
Weight types.Difficulty `json:"weight"`
Versions []SideChainVersionEntry `json:"versions"`
}
type PoolInfoResultMainChainConsensus struct {
BlockTime uint64 `json:"block_time"`
TransactionUnlockTime uint64 `json:"transaction_unlock_time"`
MinerRewardUnlockTime uint64 `json:"miner_reward_unlock_time"`
// HardForkSupportedVersion
HardForkSupportedVersion uint8 `json:"hard_fork_supported_version"`
// HardForks HardFork information for Monero known hardfork by backing p2pool
HardForks []sidechain.HardFork `json:"hard_forks,omitempty"`
}
type PoolInfoResultMainChain struct {
Consensus PoolInfoResultMainChainConsensus `json:"consensus"`
Id types.Hash `json:"id"`
CoinbaseId types.Hash `json:"coinbase_id"`
Height uint64 `json:"height"`
Difficulty types.Difficulty `json:"difficulty"`
Reward uint64 `json:"reward"`
BaseReward uint64 `json:"base_reward"`
NextDifficulty types.Difficulty `json:"next_difficulty"`
// BlockTime included in Consensus
// Deprecated
BlockTime int `json:"block_time"`
}
type MinerInfoBlockData struct {
ShareCount uint64 `json:"shares"`
UncleCount uint64 `json:"uncles"`
LastShareHeight uint64 `json:"last_height"`
}
type MinerInfoResult struct {
Id uint64 `json:"id"`
Address *address.Address `json:"address"`
Alias string `json:"alias,omitempty"`
Shares [index.InclusionCount]MinerInfoBlockData `json:"shares"`
LastShareHeight uint64 `json:"last_share_height"`
LastShareTimestamp uint64 `json:"last_share_timestamp"`
}
type TransactionLookupResult struct {
Id types.Hash `json:"id"`
Inputs index.TransactionInputQueryResults `json:"inputs"`
Outs []client.Output `json:"outs"`
Match []index.TransactionInputQueryResultsMatch `json:"matches"`
}

View file

@ -1,302 +0,0 @@
package utils
import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/floatdrop/lru"
"math"
)
// keep last few main blocks
var mainBlockCache = lru.New[types.Hash, *block.Block](100)
func GetMainBlock(id types.Hash, client *client.Client) (*block.Block, error) {
if bb := mainBlockCache.Get(id); bb == nil {
blockData, err := client.GetBlock(id, context.Background())
if err != nil {
return nil, err
}
bufData, err := hex.DecodeString(blockData.Blob)
if err != nil {
return nil, err
}
mainBlock := &block.Block{}
err = mainBlock.UnmarshalBinary(bufData)
if err != nil {
return nil, err
}
if len(mainBlock.Coinbase.Outputs) == 0 {
//should never happen from monero itself
return nil, errors.New("outputs not filled")
}
mainBlockCache.Set(id, mainBlock)
return mainBlock, nil
} else {
return *bb, nil
}
}
func FindAndInsertMainHeader(h daemon.BlockHeader, indexDb *index.Index, storeFunc func(b *sidechain.PoolBlock), client *client.Client, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId sidechain.GetByTemplateIdFunc, getByMainId sidechain.GetByMainIdFunc, getByMainHeight sidechain.GetByMainHeightFunc, processBlock func(b *sidechain.PoolBlock) error) error {
mainId, _ := types.HashFromString(h.Hash)
coinbaseId, _ := types.HashFromString(h.MinerTxHash)
prevHash, _ := types.HashFromString(h.PrevHash)
var indexMain *index.MainBlock
indexMain = indexDb.GetMainBlockByHeight(h.Height)
if indexMain == nil {
indexMain = &index.MainBlock{}
}
if indexMain.Id != mainId {
//Zero data if they don't match to insert new height
indexMain.Metadata = make(map[string]any)
indexMain.SideTemplateId = types.ZeroHash
indexMain.CoinbasePrivateKey = crypto.PrivateKeyBytes{}
}
indexMain.Id = mainId
indexMain.Height = h.Height
indexMain.Timestamp = uint64(h.Timestamp)
indexMain.Reward = h.Reward
indexMain.CoinbaseId = coinbaseId
indexMain.Difficulty = h.Difficulty
// Find possible candidates that match on main height, main previous id, header nonce, timestamp, and count of transactions to reduce full block lookups
var candidates sidechain.UniquePoolBlockSlice
for _, b := range getByMainHeight(h.Height) {
if b.Main.PreviousId == prevHash && b.Main.Coinbase.TotalReward == h.Reward && b.Main.Timestamp == uint64(h.Timestamp) && len(b.Main.Transactions) == int(h.NumTxes) {
// candidate for checking
candidates = append(candidates, b)
}
}
if len(candidates) == 0 {
// no candidates, insert header as-is
if err := indexDb.InsertOrUpdateMainBlock(indexMain); err != nil {
return err
}
return nil
}
utils.Logf("Monero", "checking block %s at height %d: %d candidate(s)", mainId, h.Height, len(candidates))
mainBlock, err := GetMainBlock(mainId, client)
if err != nil {
utils.Errorf("Monero", "could not get main block: %e", err)
// insert errored block as-is
if err := indexDb.InsertOrUpdateMainBlock(indexMain); err != nil {
return err
}
return nil
}
var sideTemplateId types.Hash
var extraNonce []byte
// try to fetch extra nonce if existing
{
if t := mainBlock.Coinbase.Extra.GetTag(uint8(sidechain.SideExtraNonce)); t != nil {
if len(t.Data) >= sidechain.SideExtraNonceSize || len(t.Data) < sidechain.SideExtraNonceMaxSize {
extraNonce = t.Data
}
}
}
// get a merge tag if existing
{
if t := mainBlock.Coinbase.Extra.GetTag(uint8(sidechain.SideTemplateId)); t != nil {
if len(t.Data) == types.HashSize {
sideTemplateId = types.HashFromBytes(t.Data)
}
}
}
if sideTemplateId == types.ZeroHash || len(extraNonce) == 0 {
utils.Logf("Monero", "checking block %s at height %d: not a p2pool block", mainId, h.Height)
if err := indexDb.InsertOrUpdateMainBlock(indexMain); err != nil {
return err
}
return nil
}
if t := getByTemplateId(sideTemplateId); t != nil {
//found template
if err := processBlock(t); err != nil {
return fmt.Errorf("could not process template: %w", err)
}
if t.MainId() == mainId { //exact match, no need to re-arrange template inclusion
sideBlock := indexDb.GetSideBlockByMainId(t.MainId())
if sideBlock == nil {
//found deep orphan which found block, insert parents
cur := t
for cur != nil && !index.QueryHasResults(indexDb.GetSideBlocksByTemplateId(cur.SideTemplateId(indexDb.Consensus()))) {
orphanBlock, orphanUncles, err := indexDb.GetSideBlockFromPoolBlock(cur, index.InclusionOrphan)
if err != nil {
break
}
if index.QueryHasResults(indexDb.GetSideBlocksByTemplateId(orphanBlock.TemplateId)) {
continue
}
if err := indexDb.InsertOrUpdateSideBlock(orphanBlock); err != nil {
return fmt.Errorf("error inserting %s, %s at %d: %s", cur.SideTemplateId(indexDb.Consensus()), cur.MainId(), cur.Side.Height, err)
}
for _, orphanUncle := range orphanUncles {
if index.QueryHasResults(indexDb.GetSideBlocksByTemplateId(orphanUncle.TemplateId)) {
continue
}
if err := indexDb.InsertOrUpdateSideBlock(orphanUncle); err != nil {
return fmt.Errorf("error inserting uncle of %s, %s; %s, %s at %d: %s", cur.SideTemplateId(indexDb.Consensus()), cur.MainId(), orphanUncle.TemplateId, orphanUncle.MainId, cur.Side.Height, err)
}
}
cur = getByTemplateId(cur.Side.Parent)
if cur == nil {
break
}
if err := processBlock(cur); err != nil {
return fmt.Errorf("could not process cur template: %w", err)
}
}
sideBlock = indexDb.GetSideBlockByMainId(t.MainId())
if sideBlock == nil {
return errors.New("no block in database for orphan")
}
sideBlock.Inclusion = index.InclusionOrphan
} else if sideBlock.Inclusion == index.InclusionAlternateInVerifiedChain {
sideBlock.Inclusion = index.InclusionInVerifiedChain
}
if err := indexDb.InsertOrUpdateSideBlock(sideBlock); err != nil {
return fmt.Errorf("error inserting %s, %s at %d: %s", sideBlock.TemplateId, sideBlock.MainId, sideBlock.SideHeight, err)
}
t.Depth.Store(math.MaxUint64)
storeFunc(t)
indexMain.SideTemplateId = sideTemplateId
indexMain.CoinbasePrivateKey = t.Side.CoinbasePrivateKey
if err := indexDb.InsertOrUpdateMainBlock(indexMain); err != nil {
return err
}
utils.Logf("Monero", "INSERTED found block %s at height %d, template id %s", mainId, h.Height, sideTemplateId)
if err := FindAndInsertMainHeaderOutputs(indexMain, indexDb, client, difficultyByHeight, getByTemplateId, getByMainId, getByMainHeight, processBlock); err != nil {
utils.Errorf("", "error inserting coinbase outputs: %s", err)
}
return nil
} else {
data, _ := t.MarshalBinary()
newBlock := &sidechain.PoolBlock{}
if err := newBlock.UnmarshalBinary(indexDb.Consensus(), &sidechain.NilDerivationCache{}, data); err != nil {
utils.Panic(err)
}
copy(newBlock.CoinbaseExtra(sidechain.SideExtraNonce), extraNonce)
newBlock.Main.Nonce = uint32(h.Nonce)
newBlock.Depth.Store(math.MaxUint64)
if newBlock.MainId() == mainId {
//store into archive
storeFunc(newBlock)
//insert into db as well
var sideBlock *index.SideBlock
if indexDb.GetSideBlockByMainId(t.MainId()).Inclusion == index.InclusionOrphan {
sideBlock, _, err = indexDb.GetSideBlockFromPoolBlock(newBlock, index.InclusionOrphan)
} else {
sideBlock, _, err = indexDb.GetSideBlockFromPoolBlock(newBlock, index.InclusionInVerifiedChain)
}
if err != nil {
return fmt.Errorf("could not process %s, at side height %d, template id %s: %w", types.HashFromBytes(t.CoinbaseExtra(sidechain.SideTemplateId)), t.Side.Height, sideTemplateId, err)
}
if err := indexDb.InsertOrUpdateSideBlock(sideBlock); err != nil {
return fmt.Errorf("error inserting %s, %s at %d: %s", sideBlock.TemplateId, sideBlock.MainId, sideBlock.SideHeight, err)
}
indexMain.SideTemplateId = sideTemplateId
indexMain.CoinbasePrivateKey = newBlock.Side.CoinbasePrivateKey
if err := indexDb.InsertOrUpdateMainBlock(indexMain); err != nil {
return err
}
utils.Logf("", "INSERTED ALTERNATE found block %s at height %d, template id %s", mainId, h.Height, sideTemplateId)
if err := FindAndInsertMainHeaderOutputs(indexMain, indexDb, client, difficultyByHeight, getByTemplateId, getByMainId, getByMainHeight, processBlock); err != nil {
utils.Errorf("", "error inserting coinbase outputs: %s", err)
}
return nil
}
}
}
utils.Logf("", "checking block %s at height %d, template id %s: could not find matching template id", mainId, h.Height, sideTemplateId)
// could not find template, maybe it's other pool?
// fill template id for future reference
// TODO: do this for all blocks
indexMain.SetMetadata("merge_mining_tag", sideTemplateId)
indexMain.SetMetadata("extra_nonce", binary.LittleEndian.Uint32(extraNonce))
if err := indexDb.InsertOrUpdateMainBlock(indexMain); err != nil {
return err
}
return nil
}
func FindAndInsertMainHeaderOutputs(mb *index.MainBlock, indexDb *index.Index, client *client.Client, difficultyByHeight block.GetDifficultyByHeightFunc, getByTemplateId sidechain.GetByTemplateIdFunc, getByMainId sidechain.GetByMainIdFunc, getByMainHeight sidechain.GetByMainHeightFunc, processBlock func(b *sidechain.PoolBlock) error) error {
if !index.QueryHasResults(indexDb.GetMainCoinbaseOutputs(mb.CoinbaseId)) {
//fill information
utils.Logf("", "inserting coinbase outputs for %s, template id %s, coinbase id %s", mb.Id, mb.SideTemplateId, mb.CoinbaseId)
var t *sidechain.PoolBlock
if t = getByTemplateId(mb.SideTemplateId); t == nil || t.MainId() != mb.Id {
t = nil
}
if t == nil {
t = getByMainId(mb.Id)
}
if t != nil {
if err := processBlock(t); err != nil {
return fmt.Errorf("could not process block: %s", err)
}
if mb.CoinbaseId != t.CoinbaseId() {
return fmt.Errorf("not matching coinbase id: %s vs %s", mb.CoinbaseId, t.CoinbaseId())
}
indexes, err := client.GetOutputIndexes(mb.CoinbaseId)
if err != nil {
return fmt.Errorf("error getting output indexes: %w", err)
}
if len(indexes) != len(t.Main.Coinbase.Outputs) {
return fmt.Errorf("not matching indexes vs coinbase output len: %d vs %d", len(indexes), len(t.Main.Coinbase.Outputs))
}
preAllocatedShares := sidechain.PreAllocateShares(indexDb.Consensus().ChainWindowSize * 2)
shares, _ := sidechain.GetShares(t, indexDb.Consensus(), difficultyByHeight, getByTemplateId, preAllocatedShares)
if len(shares) != len(t.Main.Coinbase.Outputs) {
return fmt.Errorf("not matching shares vs coinbase output len: %d vs %d", len(shares), len(t.Main.Coinbase.Outputs))
}
outputs := make(index.MainCoinbaseOutputs, 0, len(indexes))
for _, o := range t.Main.Coinbase.Outputs {
output := index.MainCoinbaseOutput{
Id: mb.CoinbaseId,
Index: uint32(o.Index),
GlobalOutputIndex: indexes[o.Index],
Miner: indexDb.GetOrCreateMinerPackedAddress(shares[o.Index].Address).Id(),
Value: o.Reward,
}
outputs = append(outputs, output)
}
if err := indexDb.InsertOrUpdateMainCoinbaseOutputs(outputs); err != nil {
return err
}
} else {
return errors.New("nil template")
}
}
return nil
}

View file

@ -1,35 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0 => ../index
require (
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0
github.com/floatdrop/lru v1.3.0
github.com/goccy/go-json v0.10.2
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,46 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,140 +0,0 @@
package utils
type PositionChart struct {
totalItems uint64
bucket []uint64
perBucket int
idle byte
}
func (p *PositionChart) Add(index int, value uint64) {
if index < 0 || index >= int(p.totalItems) {
return
}
if len(p.bucket) == 1 {
p.bucket[0] += value
return
}
p.bucket[p.indexOf(index)] += value
}
func (p *PositionChart) indexOf(index int) int {
if len(p.bucket) == 1 {
return 0
}
i := (index*len(p.bucket) - 1) / int(p.totalItems-1)
return i
}
func (p *PositionChart) Total() (result uint64) {
for _, e := range p.bucket {
result += e
}
return
}
func (p *PositionChart) Size() uint64 {
return uint64(len(p.bucket))
}
func (p *PositionChart) Resolution() uint64 {
return p.totalItems / uint64(len(p.bucket))
}
func (p *PositionChart) SetIdle(idleChar byte) {
p.idle = idleChar
}
func (p *PositionChart) String() string {
position := make([]byte, 2*2+len(p.bucket))
position[0], position[1] = '[', '<'
position[len(position)-2], position[len(position)-1] = '<', ']'
//reverse
size := len(p.bucket) - 1
for i := len(p.bucket) - 1; i >= 0; i-- {
e := p.bucket[i]
if e > 0 {
if e > 9 {
position[2+size-i] = '+'
} else {
position[2+size-i] = 0x30 + byte(e)
}
} else {
position[2+size-i] = p.idle
}
}
return string(position)
}
func (p *PositionChart) StringWithoutDelimiters() string {
position := make([]byte, len(p.bucket))
//reverse
size := len(p.bucket) - 1
for i := len(p.bucket) - 1; i >= 0; i-- {
e := p.bucket[i]
if e > 0 {
if e > 9 {
position[size-i] = '+'
} else {
position[size-i] = 0x30 + byte(e)
}
} else {
position[size-i] = p.idle
}
}
return string(position)
}
func (p *PositionChart) StringWithSeparator(index int) string {
if index < 0 || index >= int(p.totalItems) {
return p.String()
}
separatorIndex := p.indexOf(index)
position := make([]byte, 1+2*2+len(p.bucket))
position[0], position[1] = '[', '<'
position[2+separatorIndex] = '|'
position[len(position)-2], position[len(position)-1] = '<', ']'
//reverse
size := len(p.bucket) - 1
for i := len(p.bucket) - 1; i >= 0; i-- {
e := p.bucket[i]
j := size - i
if j >= separatorIndex {
j++
}
if e > 0 {
if e > 9 {
position[2+j] = '+'
} else {
position[2+j] = 0x30 + byte(e)
}
} else {
position[2+j] = p.idle
}
}
return string(position)
}
func NewPositionChart(size uint64, totalItems uint64) *PositionChart {
if size < 1 {
size = 1
}
perBucket := int(totalItems / size)
if totalItems%size > 0 {
perBucket += 1
}
return &PositionChart{
totalItems: totalItems,
bucket: make([]uint64, size),
perBucket: perBucket,
idle: '.',
}
}

View file

@ -1,23 +0,0 @@
package utils
import (
"testing"
)
func TestChart(t *testing.T) {
p := NewPositionChart(32, 4096)
for i := 0; i < 4096; i++ {
p.Add(i, 1)
}
t.Log(p.String())
}
func TestChartIncrement(t *testing.T) {
p := NewPositionChart(32, 32)
for i := 0; i < 32; i++ {
p.Add(i, uint64(i))
}
t.Log(p.String())
}

View file

@ -1,119 +0,0 @@
package utils
import (
"bytes"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/goccy/go-json"
"slices"
)
type SignedActionEntry struct {
Key string `json:"key"`
Value string `json:"value"`
}
type SignedAction struct {
Action string `json:"action"`
Data []SignedActionEntry `json:"data"`
Realm string `json:"realm"`
}
func (a *SignedAction) Get(key string) (string, bool) {
if i := slices.IndexFunc(a.Data, func(entry SignedActionEntry) bool {
return entry.Key == key
}); i != -1 {
return a.Data[i].Value, true
}
return "", false
}
// String Creates a consistent string for signing
func (a *SignedAction) String() string {
if a.Data == nil {
a.Data = make([]SignedActionEntry, 0)
}
d, err := utils.MarshalJSON(a)
if err != nil {
panic(err)
}
buf := bytes.NewBuffer(make([]byte, 0, len(d)))
if err = json.Compact(buf, d); err != nil {
panic(err)
}
return buf.String()
}
func (a *SignedAction) Verify(realm string, addr address.Interface, signature string) address.SignatureVerifyResult {
if a.Realm != realm {
// Realm does not match
return address.ResultFail
}
message := a.String()
return address.VerifyMessage(addr, []byte(message), signature)
}
func (a *SignedAction) VerifyFallbackToZero(realm string, addr address.Interface, signature string) address.SignatureVerifyResult {
if a.Realm != realm {
// Realm does not match
return address.ResultFail
}
message := a.String()
return address.VerifyMessageFallbackToZero(addr, []byte(message), signature)
}
func SignedActionSetMinerAlias(realm, alias string) *SignedAction {
return &SignedAction{
Action: "set_miner_alias",
Data: []SignedActionEntry{
{
Key: "alias",
Value: alias,
},
},
Realm: realm,
}
}
func SignedActionUnsetMinerAlias(realm string) *SignedAction {
return &SignedAction{
Action: "unset_miner_alias",
Data: make([]SignedActionEntry, 0),
Realm: realm,
}
}
func SignedActionAddWebHook(realm, webhookType, webhookUrl string, other ...SignedActionEntry) *SignedAction {
return &SignedAction{
Action: "add_webhook",
Data: append([]SignedActionEntry{
{
Key: "type",
Value: webhookType,
},
{
Key: "url",
Value: webhookUrl,
},
}, other...),
Realm: realm,
}
}
func SignedActionRemoveWebHook(realm, webhookType, webhookUrlHash string, other ...SignedActionEntry) *SignedAction {
return &SignedAction{
Action: "remove_webhook",
Data: append([]SignedActionEntry{
{
Key: "type",
Value: webhookType,
},
{
Key: "url_hash",
Value: webhookUrlHash,
},
}, other...),
Realm: realm,
}
}

View file

@ -1,291 +0,0 @@
package utils
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
func LookupTransactions(requestOther func(ctx context.Context, indices []uint64) []*index.MatchedOutput, indexDb *index.Index, ctx context.Context, inputThreshold int, ids ...types.Hash) (results []index.TransactionInputQueryResults) {
txs, err := client.GetDefaultClient().GetTransactionInputs(ctx, ids...)
if err != nil || len(txs) != len(ids) {
return nil
}
decoys := make([]uint64, 0, len(txs)*16*16)
for _, tx := range txs {
if len(tx.Inputs) < inputThreshold {
continue
}
for _, i := range tx.Inputs {
decoys = append(decoys, i.KeyOffsets...)
}
}
var otherResult []*index.MatchedOutput
if requestOther != nil {
otherResult = requestOther(ctx, decoys)
if len(otherResult) != len(decoys) {
otherResult = nil
}
}
var otherIndex int
for _, tx := range txs {
if len(tx.Inputs) < inputThreshold {
queryResult := make(index.TransactionInputQueryResults, len(tx.Inputs))
for i := range queryResult {
queryResult[i].Input = tx.Inputs[i]
queryResult[i].MatchedOutputs = make([]*index.MatchedOutput, len(tx.Inputs[i].KeyOffsets))
}
results = append(results, queryResult)
continue
}
queryResult := indexDb.QueryTransactionInputs(tx.Inputs)
if otherResult != nil {
for i, input := range tx.Inputs {
for j := range input.KeyOffsets {
output := otherResult[otherIndex]
otherIndex++
if output == nil {
continue
}
if queryResult[i].MatchedOutputs[j] == nil { //todo: multiple matches??
queryResult[i].MatchedOutputs[j] = output
}
}
}
}
results = append(results, queryResult)
}
return results
}
var otherLookupHostFunc func(ctx context.Context, indices []uint64) []*index.MatchedOutput
func init() {
if os.Getenv("TRANSACTION_LOOKUP_OTHER") != "" {
otherLookupHostFunc = func(ctx context.Context, indices []uint64) (result []*index.MatchedOutput) {
data, _ := utils.MarshalJSON(indices)
result = make([]*index.MatchedOutput, len(indices))
for _, host := range strings.Split(os.Getenv("TRANSACTION_LOOKUP_OTHER"), ",") {
host = strings.TrimSpace(host)
if host == "" {
continue
}
uri, _ := url.Parse(host + "/api/global_indices_lookup")
if response, err := http.DefaultClient.Do(&http.Request{
Method: "POST",
URL: uri,
Body: io.NopCloser(bytes.NewReader(data)),
}); err == nil {
func() {
defer response.Body.Close()
if response.StatusCode == http.StatusOK {
if data, err := io.ReadAll(response.Body); err == nil {
r := make([]*index.MatchedOutput, 0, len(indices))
if utils.UnmarshalJSON(data, &r) == nil && len(r) == len(indices) {
for i := range r {
if result[i] == nil {
result[i] = r[i]
}
}
}
}
}
}()
}
}
return result
}
}
}
func ProcessFullBlock(b *index.MainBlock, indexDb *index.Index) error {
var sideTemplateId types.Hash
var extraNonce []byte
mainBlock, err := GetMainBlock(b.Id, client.GetDefaultClient())
if err != nil {
return fmt.Errorf("could not get main block for %s at %d: %w", b.Id, b.Height, err)
}
utils.Logf("", "Block %s at %d (%s), processing %d transactions", b.Id, b.Height, time.Unix(int64(b.Timestamp), 0).UTC().Format("02-01-2006 15:04:05 MST"), len(mainBlock.Transactions))
// try to fetch extra nonce if existing
{
if t := mainBlock.Coinbase.Extra.GetTag(uint8(sidechain.SideExtraNonce)); t != nil {
if len(t.Data) >= sidechain.SideExtraNonceSize || len(t.Data) < sidechain.SideExtraNonceMaxSize {
extraNonce = t.Data
}
}
}
// get a merge tag if existing
{
if t := mainBlock.Coinbase.Extra.GetTag(uint8(sidechain.SideTemplateId)); t != nil {
if len(t.Data) == types.HashSize {
sideTemplateId = types.HashFromBytes(t.Data)
}
}
}
if sideTemplateId != types.ZeroHash && len(extraNonce) != 0 {
b.SetMetadata("merge_mining_tag", sideTemplateId)
b.SetMetadata("extra_nonce", binary.LittleEndian.Uint32(extraNonce))
utils.Logf("", "p2pool tags found template id %s, extra nonce %d", sideTemplateId, binary.LittleEndian.Uint32(extraNonce))
}
const txInputThreshold = 4
const txInputThresholdForRatio = 8
if len(mainBlock.Transactions) > 0 {
results := LookupTransactions(otherLookupHostFunc, indexDb, context.Background(), txInputThreshold, mainBlock.Transactions...)
for i, inputs := range results {
txId := mainBlock.Transactions[i]
if len(inputs) < txInputThreshold {
//skip due to not enough matches later on
continue
}
matches := inputs.Match()
var topMiner *index.TransactionInputQueryResultsMatch
for j, m := range matches {
if m.Address == nil {
continue
} else if topMiner == nil {
topMiner = &matches[j]
} else {
if topMiner.Count <= 2 && topMiner.Count == m.Count {
//if count is not greater
topMiner = nil
}
break
}
}
if topMiner != nil {
var noMinerCount, minerCount, otherMinerCount uint64
for _, i := range inputs {
var isNoMiner, isMiner, isOtherMiner bool
for _, o := range i.MatchedOutputs {
if o == nil {
isNoMiner = true
} else if topMiner.Address.Compare(o.Address) == 0 {
isMiner = true
} else {
isOtherMiner = true
}
}
if isMiner {
minerCount++
} else if isOtherMiner {
otherMinerCount++
} else if isNoMiner {
noMinerCount++
}
}
minerRatio := float64(minerCount) / float64(len(inputs))
noMinerRatio := float64(noMinerCount) / float64(len(inputs))
otherMinerRatio := float64(otherMinerCount) / float64(len(inputs))
var likelyMiner bool
if (len(inputs) >= txInputThresholdForRatio && minerRatio >= noMinerRatio && minerRatio > otherMinerRatio) || (len(inputs) >= txInputThresholdForRatio && minerRatio > 0.35 && minerRatio > otherMinerRatio) || (len(inputs) >= txInputThreshold && minerRatio > 0.75) {
likelyMiner = true
}
if likelyMiner {
utils.Logf("", "transaction %s is LIKELY for %s: miner ratio %.02f (%d/%d), none %.02f (%d/%d), other %.02f (%d/%d); coinbase %d, sweep %d", txId, topMiner.Address.ToBase58(), minerRatio, minerCount, len(inputs), noMinerRatio, noMinerCount, len(inputs), otherMinerRatio, otherMinerCount, len(inputs), topMiner.CoinbaseCount, topMiner.SweepCount)
minimalInputs := make(index.MinimalTransactionInputQueryResults, len(inputs))
decoyCount := len(inputs[0].Input.KeyOffsets)
spendingOutputIndices := make([]uint64, 0, len(inputs)*decoyCount)
for j, input := range inputs {
spendingOutputIndices = append(spendingOutputIndices, input.Input.KeyOffsets...)
minimalInputs[j].Input = input.Input
minimalInputs[j].MatchedOutputs = make([]*index.MinimalMatchedOutput, len(input.MatchedOutputs))
for oi, o := range input.MatchedOutputs {
if o == nil {
continue
} else if o.Coinbase != nil {
minimalInputs[j].MatchedOutputs[oi] = &index.MinimalMatchedOutput{
Coinbase: o.Coinbase.Id,
GlobalOutputIndex: o.GlobalOutputIndex,
Address: o.Address,
}
} else if o.Sweep != nil {
minimalInputs[j].MatchedOutputs[oi] = &index.MinimalMatchedOutput{
Sweep: o.Sweep.Id,
GlobalOutputIndex: o.GlobalOutputIndex,
Address: o.Address,
}
}
}
}
outputIndexes, err := client.GetDefaultClient().GetOutputIndexes(txId)
if err != nil {
return err
}
tx := &index.MainLikelySweepTransaction{
Id: txId,
Timestamp: mainBlock.Timestamp,
Result: minimalInputs,
Match: matches,
Value: topMiner.CoinbaseAmount,
SpendingOutputIndices: spendingOutputIndices,
GlobalOutputIndices: outputIndexes,
InputCount: len(inputs),
InputDecoyCount: decoyCount,
MinerCount: int(minerCount),
OtherMinersCount: int(otherMinerCount),
NoMinerCount: int(noMinerCount),
MinerRatio: float32(minerRatio),
OtherMinersRatio: float32(otherMinerRatio),
NoMinerRatio: float32(noMinerRatio),
Address: topMiner.Address,
}
if err := indexDb.InsertOrUpdateMainLikelySweepTransaction(tx); err != nil {
return err
}
} else {
utils.Logf("", "transaction %s is NOT likely for %s: miner ratio %.02f (%d/%d), none %.02f (%d/%d), other %.02f (%d/%d); coinbase %d, sweep %d", txId, topMiner.Address.ToBase58(), minerRatio, topMiner.Count, len(inputs), noMinerRatio, noMinerCount, len(inputs), otherMinerRatio, otherMinerCount, len(inputs), topMiner.CoinbaseCount, topMiner.SweepCount)
}
} else {
//utils.Logf("", "transaction %s does not have enough matches, %d outputs", txId, len(inputs))
}
}
}
b.SetMetadata("processed", true)
if err = indexDb.InsertOrUpdateMainBlock(b); err != nil {
return err
}
return nil
}

View file

@ -1,88 +0,0 @@
package utils
type urlEntry struct {
Host string
Onion string
}
type siteKey int
const (
SiteKeyP2PoolIo = siteKey(iota)
SiteKeyLocalMonero
SiteKeyExploreMonero
SiteKeyMoneroCom
SiteKeyP2PoolObserver
SiteKeyP2PoolObserverMini
SiteKeyP2PoolObserverOld
SiteKeyP2PoolObserverOldMini
SiteKeyGitGammaspectraLive
SiteKeyXmrChainNet
SiteKeySethForPrivacy
SiteKeyXmrVsBeast
)
var existingUrl = map[siteKey]urlEntry{
SiteKeyP2PoolIo: {
Host: "https://p2pool.io",
Onion: "http://yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion",
},
SiteKeyLocalMonero: {
Host: "https://localmonero.co",
Onion: "http://nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion",
},
SiteKeyExploreMonero: {
Host: "https://www.exploremonero.com",
},
SiteKeyMoneroCom: {
Host: "https://monero.com",
},
SiteKeyP2PoolObserver: {
Host: "https://p2pool.observer",
Onion: "http://p2pool2giz2r5cpqicajwoazjcxkfujxswtk3jolfk2ubilhrkqam2id.onion",
},
SiteKeyP2PoolObserverMini: {
Host: "https://mini.p2pool.observer",
Onion: "http://p2pmin25k4ei5bp3l6bpyoap6ogevrc35c3hcfue7zfetjpbhhshxdqd.onion",
},
SiteKeyP2PoolObserverOld: {
Host: "https://old.p2pool.observer",
Onion: "http://temp2p7m2ddclcsqx2mrbqrmo7ccixpiu5s2cz2c6erxi2lppptdvxqd.onion",
},
SiteKeyP2PoolObserverOldMini: {
Host: "https://old-mini.p2pool.observer",
Onion: "http://temp2pbud6av2jx3lh3yovrj4mjjy2k4p5rxydviosp356ndzs4nd6yd.onion",
},
SiteKeyGitGammaspectraLive: {
Host: "https://git.gammaspectra.live",
Onion: "http://gitshn5x75sgs53q3pxwjva2z65ns5vadx3h7u3hrdssbxsova66cxid.onion",
},
SiteKeyXmrChainNet: {
Host: "https://xmrchain.net",
},
SiteKeySethForPrivacy: {
Host: "https://sethforprivacy.com",
Onion: "http://sfprivg7qec6tdle7u6hdepzjibin6fn3ivm6qlwytr235rh5vc6bfqd.onion",
},
SiteKeyXmrVsBeast: {
Host: "https://xmrvsbeast.com",
},
}
func GetSiteUrl(k siteKey, tryOnion bool) string {
e := existingUrl[k]
if tryOnion && e.Onion != "" {
return e.Onion
}
return e.Host
}
func GetSiteUrlByHost(host string, tryOnion bool) string {
findKey := "https://" + host
for k, e := range existingUrl {
if e.Host == findKey {
return GetSiteUrl(k, tryOnion)
}
}
return ""
}

View file

@ -1,401 +0,0 @@
package utils
import (
"bytes"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
type JSONWebHookDiscord struct {
Username string `json:"username,omitempty"`
AvatarUrl string `json:"avatar_url,omitempty"`
Content string `json:"content,omitempty"`
Embeds []JSONWebHookDiscordEmbed `json:"embeds,omitempty"`
Components []JSONWebHookDiscordComponentRow `json:"components,omitempty"`
TTS bool `json:"tts,omitempty"`
AllowedMentions *struct {
Parse []string `json:"parse,omitempty"`
Users []string `json:"users,omitempty"`
Roles []string `json:"roles,omitempty"`
} `json:"allowed_mentions,omitempty"`
}
func NewJSONWebHookDiscordComponent(b ...JSONWebHookDiscordComponentButton) JSONWebHookDiscordComponentRow {
return JSONWebHookDiscordComponentRow{
Type: 1,
Components: b,
}
}
func NewJSONWebHookDiscordComponentButton(label, url string) JSONWebHookDiscordComponentButton {
return JSONWebHookDiscordComponentButton{
Type: 2,
Style: 5,
Label: label,
Url: url,
}
}
type JSONWebHookDiscordComponentRow struct {
Type int `json:"type"`
Components []JSONWebHookDiscordComponentButton `json:"components"`
}
type JSONWebHookDiscordComponentButton struct {
Type int `json:"type"`
Style int `json:"style"`
Label string `json:"label,omitempty"`
Url string `json:"url,omitempty"`
}
type JSONWebHookDiscordEmbed struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Url string `json:"url,omitempty"`
// Timestamp Time in time.RFC3339 YYYY-MM-DDTHH:MM:SS.MSSZ format
Timestamp string `json:"timestamp,omitempty"`
Color *uint64 `json:"color,omitempty"`
Footer *struct {
Text string `json:"text,omitempty"`
} `json:"footer,omitempty"`
Image *struct {
Url string `json:"url,omitempty"`
} `json:"image,omitempty"`
Thumbnail *struct {
Url string `json:"url,omitempty"`
} `json:"thumbnail,omitempty"`
Provider *struct {
Name string `json:"name,omitempty"`
IconUrl string `json:"icon_url,omitempty"`
} `json:"provider,omitempty"`
Author *struct {
Name string `json:"name,omitempty"`
Url string `json:"url,omitempty"`
IconUrl string `json:"icon_url,omitempty"`
} `json:"author,omitempty"`
Fields []JSONWebHookDiscordEmbedField `json:"fields,omitempty"`
}
type JSONWebHookDiscordEmbedField struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Inline bool `json:"inline,omitempty"`
}
var (
DiscordColorGreen uint64 = 5763719
DiscordColorDarkGreen uint64 = 2067276
DiscordColorOrange uint64 = 15105570
)
var WebHookRateLimit = time.NewTicker(time.Second / 20)
var WebHookUserAgent string
var WebHookHost string
var WebHookVersion = fmt.Sprintf("%d.%d", types.CurrentSoftwareVersion.Major(), types.CurrentSoftwareVersion.Minor())
func init() {
WebHookHost = os.Getenv("NET_SERVICE_ADDRESS")
WebHookUserAgent = fmt.Sprintf("Mozilla/5.0 (compatible;GoObserver %s; +https://%s/api#webhooks)", types.CurrentSoftwareVersion.String(), WebHookHost)
}
var WebHookClient = http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
func SetWebHookProxy(hostPort string) {
WebHookClient.Transport = &http.Transport{
Proxy: http.ProxyURL(&url.URL{
Scheme: "socks5",
Host: hostPort,
}),
}
}
func SendSideBlock(w *index.MinerWebHook, ts int64, source *address.Address, block *index.SideBlock) error {
if !CanSendToHook(w, "side_blocks") {
return nil
}
blockValuation := func() string {
if block.IsOrphan() {
return "0%"
} else if block.IsUncle() {
return strconv.FormatUint(100-w.Consensus.UnclePenalty, 10) + "% (uncle)"
} else if block.IsUncle() {
return "100% + " + strconv.FormatUint(w.Consensus.UnclePenalty, 10) + "% of " + strconv.FormatUint(uint64(len(block.Uncles)), 10) + "% uncle(s)"
} else {
return "100%"
}
}
blockWeight, _ := block.Weight(block.EffectiveHeight, w.Consensus.ChainWindowSize, w.Consensus.UnclePenalty)
switch w.Type {
case index.WebHookDiscord:
var color = &DiscordColorGreen
fields := []JSONWebHookDiscordEmbedField{
{
Name: "Pool Height",
Value: strconv.FormatUint(block.SideHeight, 10),
Inline: true,
},
}
if block.IsUncle() {
color = &DiscordColorDarkGreen
fields = append(fields, JSONWebHookDiscordEmbedField{
Name: "Parent Height",
Value: "[" + strconv.FormatUint(block.EffectiveHeight, 10) + "](" + "https://" + WebHookHost + "/share/" + block.UncleOf.String() + ")",
Inline: true,
})
} else {
fields = append(fields, JSONWebHookDiscordEmbedField{
Name: "Monero Height",
Value: strconv.FormatUint(block.MainHeight, 10),
Inline: true,
})
}
fields = append(fields,
JSONWebHookDiscordEmbedField{
Name: "Found by",
Value: "[`" + string(utils.ShortenSlice(block.MinerAddress.ToBase58(), 24)) + "`](" + "https://" + WebHookHost + "/miner/" + string(block.MinerAddress.ToBase58()) + ")",
},
JSONWebHookDiscordEmbedField{
Name: "Template Id",
Value: "[`" + block.TemplateId.String() + "`](" + "https://" + WebHookHost + "/share/" + block.TemplateId.String() + ")",
},
JSONWebHookDiscordEmbedField{
Name: "Valuation",
Value: blockValuation(),
Inline: true,
},
JSONWebHookDiscordEmbedField{
Name: "Weight",
Value: utils.SiUnits(float64(blockWeight), 4),
Inline: true,
},
)
return SendJsonPost(w, ts, source, &JSONWebHookDiscord{
Embeds: []JSONWebHookDiscordEmbed{
{
Color: color,
Title: "**" + func() string {
if block.IsUncle() {
return "UNCLE "
} else {
return ""
}
}() + "SHARE FOUND** on P2Pool " + func() string {
if w.Consensus.IsDefault() {
return "Main"
} else if w.Consensus.IsMini() {
return "Mini"
} else {
return "Unknown"
}
}(),
Url: "https://" + WebHookHost + "/share/" + block.MainId.String(),
Description: "",
Fields: fields,
Footer: &struct {
Text string `json:"text,omitempty"`
}{
Text: "Share mined using " + block.SoftwareId.String() + " " + block.SoftwareVersion.String(),
},
Timestamp: time.Unix(int64(block.Timestamp), 0).UTC().Format(time.RFC3339),
},
},
})
case index.WebHookCustom:
return SendJsonPost(w, ts, source, &JSONEvent{
Type: JSONEventSideBlock,
SideBlock: block,
})
default:
return errors.New("unsupported hook type")
}
}
func SendFoundBlock(w *index.MinerWebHook, ts int64, source *address.Address, block *index.FoundBlock, outputs index.MainCoinbaseOutputs) error {
if !CanSendToHook(w, "found_blocks") {
return nil
}
switch w.Type {
case index.WebHookCustom:
return SendJsonPost(w, ts, source, &JSONEvent{
Type: JSONEventFoundBlock,
FoundBlock: block,
MainCoinbaseOutputs: outputs,
})
default:
return errors.New("unsupported hook type")
}
}
func CanSendToHook(w *index.MinerWebHook, key string) bool {
if v, ok := w.Settings["send_"+key]; ok && v == "false" {
return false
}
return true
}
func SendPayout(w *index.MinerWebHook, ts int64, source *address.Address, payout *index.Payout) error {
if !CanSendToHook(w, "payouts") {
return nil
}
switch w.Type {
case index.WebHookDiscord:
return SendJsonPost(w, ts, source, &JSONWebHookDiscord{
Embeds: []JSONWebHookDiscordEmbed{
{
Color: &DiscordColorOrange,
Title: "**RECEIVED PAYOUT** on P2Pool " + func() string {
if w.Consensus.IsDefault() {
return "Main"
} else if w.Consensus.IsMini() {
return "Mini"
} else {
return "Unknown"
}
}(),
Url: "https://" + WebHookHost + "/proof/" + payout.MainId.String() + "/" + strconv.FormatUint(payout.Index, 10),
Description: "",
Fields: []JSONWebHookDiscordEmbedField{
{
Name: "Pool Height",
Value: strconv.FormatUint(payout.SideHeight, 10),
Inline: true,
},
{
Name: "Monero Height",
Value: strconv.FormatUint(payout.MainHeight, 10),
Inline: true,
},
{
Name: "Monero Id",
Value: "[`" + payout.MainId.String() + "`](" + GetSiteUrl(SiteKeyP2PoolIo, false) + "/explorer/block/" + payout.MainId.String() + ")",
},
{
Name: "Template Id",
Value: "[`" + payout.TemplateId.String() + "`](" + "https://" + WebHookHost + "/share/" + payout.TemplateId.String() + ")",
},
{
Name: "Coinbase Id",
Value: "[`" + payout.CoinbaseId.String() + "`](" + GetSiteUrl(SiteKeyP2PoolIo, false) + "/explorer/tx/" + payout.CoinbaseId.String() + ")",
},
{
Name: "Coinbase Private Key",
Value: "[`" + payout.PrivateKey.String() + "`](" + "https://" + WebHookHost + "/proof/" + payout.MainId.String() + "/" + strconv.FormatUint(payout.Index, 10) + ")",
},
{
Name: "Payout address",
Value: "[`" + string(utils.ShortenSlice(source.ToBase58(), 24)) + "`](" + "https://" + WebHookHost + "/miner/" + string(source.ToBase58()) + ")",
},
{
Name: "Reward",
Value: "**" + utils.XMRUnits(payout.Reward) + " XMR" + "**",
Inline: true,
},
{
Name: "Global Output Index",
Value: strconv.FormatUint(payout.GlobalOutputIndex, 10),
Inline: true,
},
},
Timestamp: time.Unix(int64(payout.Timestamp), 0).UTC().Format(time.RFC3339),
},
},
})
case index.WebHookCustom:
return SendJsonPost(w, ts, source, &JSONEvent{
Type: JSONEventPayout,
Payout: payout,
})
default:
return errors.New("unsupported hook type")
}
}
func SendOrphanedBlock(w *index.MinerWebHook, ts int64, source *address.Address, block *index.SideBlock) error {
if !CanSendToHook(w, "orphaned_blocks") {
return nil
}
switch w.Type {
case index.WebHookCustom:
return SendJsonPost(w, ts, source, &JSONEvent{
Type: JSONEventOrphanedBlock,
SideBlock: block,
})
default:
return errors.New("unsupported hook type")
}
}
func SendJsonPost(w *index.MinerWebHook, ts int64, source *address.Address, data any) error {
uri, err := url.Parse(w.Url)
if err != nil {
return err
}
body, err := utils.MarshalJSON(data)
if err != nil {
return err
}
headers := make(http.Header)
headers.Set("Accept", "*/*")
headers.Set("User-Agent", WebHookUserAgent)
headers.Set("Content-Type", "application/json")
headers.Set("X-P2Pool-Observer-Timestamp", strconv.FormatInt(ts, 10))
headers.Set("X-P2Pool-Observer-Version", WebHookVersion)
headers.Set("X-P2Pool-Observer-Host", WebHookHost)
headers.Set("X-P2Pool-Observer-Consensus-ID", w.Consensus.Id.String())
headers.Set("X-P2Pool-Observer-Address", string(source.ToBase58()))
// apply rate limit
<-WebHookRateLimit.C
response, err := WebHookClient.Do(&http.Request{
Method: "POST",
URL: uri,
Header: headers,
Body: io.NopCloser(bytes.NewBuffer(body)),
ContentLength: int64(len(body)),
})
if err != nil {
return err
}
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNoContent {
data, _ := io.ReadAll(response.Body)
return fmt.Errorf("error code %d: %s", response.StatusCode, string(data))
}
return nil
}

View file

@ -1,177 +0,0 @@
package main
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"io"
"net/http"
"net/url"
"os"
"time"
)
func getTypeFromAPI[T any](method string, cacheTime ...int) *T {
cTime := 0
if len(cacheTime) > 0 {
cTime = cacheTime[0]
}
return cacheResult[*T](method, time.Second*time.Duration(cTime), func() *T {
uri, _ := url.Parse(os.Getenv("API_URL") + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return nil
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
var result T
decoder := utils.NewJSONDecoder(response.Body)
if decoder.Decode(&result) != nil {
return nil
} else {
return &result
}
} else {
return nil
}
}
})
}
func getSliceFromAPI[T any](method string, cacheTime ...int) []T {
cTime := 0
if len(cacheTime) > 0 {
cTime = cacheTime[0]
}
return cacheResult[[]T](method, time.Second*time.Duration(cTime), func() []T {
uri, _ := url.Parse(os.Getenv("API_URL") + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return nil
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
var result []T
decoder := utils.NewJSONDecoder(response.Body)
if decoder.Decode(&result) != nil {
return nil
} else {
return result
}
} else {
return nil
}
}
})
}
func getStreamFromAPI[T any](method string) <-chan T {
result := make(chan T, 1)
go func() {
defer close(result)
uri, _ := url.Parse(os.Getenv("API_URL") + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
var err error
// Read opening
var b [1]byte
for {
if _, err = response.Body.Read(b[:]); err != nil {
return
}
if b[0] == '[' {
break
} else if b[0] != ' ' && b[0] != 0xa {
return
}
}
decoder := utils.NewJSONDecoder(response.Body)
for decoder.More() {
var item T
if err := decoder.Decode(&item); err != nil {
return
} else {
result <- item
}
}
}
}
}()
return result
}
func getSideBlocksStreamFromAPI(method string) <-chan *index.SideBlock {
return getStreamFromAPI[*index.SideBlock](method)
}
func getSideBlocksFromAPI(method string, cacheTime ...int) []*index.SideBlock {
return getSliceFromAPI[*index.SideBlock](method, cacheTime...)
}
func getFromAPIRaw(method string, cacheTime ...int) []byte {
cTime := 0
if len(cacheTime) > 0 {
cTime = cacheTime[0]
}
return cacheResult[[]byte](method, time.Second*time.Duration(cTime), func() []byte {
uri, _ := url.Parse(os.Getenv("API_URL") + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return nil
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if response.StatusCode == http.StatusOK {
if data, err := io.ReadAll(response.Body); err != nil {
return nil
} else {
return data
}
} else {
return nil
}
}
})
}
func getFromAPI(method string) (statusCode int, data []byte) {
uri, _ := url.Parse(os.Getenv("API_URL") + method)
if response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: uri,
}); err != nil {
return 0, nil
} else {
defer response.Body.Close()
defer io.ReadAll(response.Body)
if data, err = io.ReadAll(response.Body); err != nil {
return response.StatusCode, nil
} else {
return response.StatusCode, data
}
}
}

View file

@ -1,47 +0,0 @@
package main
import (
"sync"
"time"
)
var cache = make(map[string]any)
var cacheLock sync.RWMutex
type cachedResult[T any] struct {
t time.Time
result T
}
func cacheResult[T any](k string, cacheTime time.Duration, result func() T) T {
var zeroVal T
if r, ok := func() (T, bool) {
if cacheTime > 0 {
cacheLock.RLock()
defer cacheLock.RUnlock()
if r := cache[k]; r != nil {
if r2, ok := r.(*cachedResult[T]); ok && r2 != nil && r2.t.Add(cacheTime).After(time.Now()) {
return r2.result, true
}
}
}
return zeroVal, false
}(); ok {
return r
}
r := result()
if cacheTime > 0 {
cacheLock.Lock()
defer cacheLock.Unlock()
cache[k] = &cachedResult[T]{
t: time.Now(),
result: r,
}
}
return r
}

Binary file not shown.

View file

@ -1,44 +0,0 @@
module git.gammaspectra.live/P2Pool/p2pool-observer/cmd/web
go 1.22
replace git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0 => ../../
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0 => ../index
replace git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0 => ../utils
require (
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0
github.com/goccy/go-json v0.10.2
github.com/gorilla/mux v1.8.1
github.com/mazznoer/colorgrad v0.9.1
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc
github.com/valyala/quicktemplate v1.7.0
)
require (
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 // indirect
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 // indirect
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 // indirect
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e // indirect
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 // indirect
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/floatdrop/lru v1.3.0 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/jxskiss/base62 v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mazznoer/csscolorparser v0.1.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
)
replace github.com/goccy/go-json => github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887

View file

@ -1,76 +0,0 @@
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33 h1:BPV7iIiv8T+X7gg9/JfNmEBoH4HXOkw8CR7FN6bBwB8=
git.gammaspectra.live/P2Pool/edwards25519 v0.0.0-20230701100949-027561bd2a33/go.mod h1:336HUKX25mQ1qUtzkwV9Wrqi153tTgUOKcIhpYuF2ts=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523 h1:oIJzm7kQyASS0xlJ79VSWRvvfXp2Qt7M05+E20o9gwE=
git.gammaspectra.live/P2Pool/go-monero v0.0.0-20230410011208-910450c4a523/go.mod h1:TAOAAV972JNDkCzyV5SkbYkKCRvcfhvvFa8LHH4Dg6g=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7 h1:bzHDuu1IgETKqPBOlIdCE2LaZIJ+ZpROSprNn+fnzd8=
git.gammaspectra.live/P2Pool/go-randomx v0.0.0-20221027085532-f46adfce03a7/go.mod h1:3kT0v4AMwT/OdorfH2gRWPwoOrUX/LV03HEeBsaXG1c=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e h1:ropqS9niQR/ZKCUrlmWe+uDH0fLIyAnCIjkEjyTDgA8=
git.gammaspectra.live/P2Pool/moneroutil v0.0.0-20230722215223-18ecc51ae61e/go.mod h1:Wn5QI7XIMHMpEu10pPspW9h3eGmXQPJwh/4/+Gi3G1U=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71 h1:MgeHHcF+GnCJBWMSzq8XAbc8p/UhNwFruEKCPPJ74YQ=
git.gammaspectra.live/P2Pool/randomx-go-bindings v0.0.0-20230514082649-9c5f18cd5a71/go.mod h1:KQaYHIxGXNHNMQELC7xGLu8xouwvP/dN7iGk681BXmk=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a h1:c24MHv/z+aBYpYNsQHcJqmFuaYInGVixJZgDCXA/4bs=
git.gammaspectra.live/P2Pool/sha3 v0.0.0-20230604092430-04fe7dc6439a/go.mod h1:6wZ0+whl+HZdcRve4R6Rq6jV1fmL1xCYO8Wty6lR008=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887 h1:P01nqSM+0b6zlPasOFYsqnQSP2dTRVZkanAnY9q/Bcc=
github.com/WeebDataHoarder/go-json v0.0.0-20230730135821-d8f6463bb887/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/floatdrop/lru v1.3.0 h1:83abtaKjXcWrPmtzTAk2Ggq8DUKqI29YzrTrB8+vu0c=
github.com/floatdrop/lru v1.3.0/go.mod h1:83zlXKA06Bm32JImNINCiTr0ldadvdAjUe5jSwIaw0s=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mazznoer/colorgrad v0.9.1 h1:MB80JYVndKWSMEM1beNqnuOowWGhoQc3DXWXkFp6JlM=
github.com/mazznoer/colorgrad v0.9.1/go.mod h1:WX2R9wt9B47+txJZVVpM9LY+LAGIdi4lTI5wIyreDH4=
github.com/mazznoer/csscolorparser v0.1.2/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
github.com/mazznoer/csscolorparser v0.1.3 h1:vug4zh6loQxAUxfU1DZEu70gTPufDPspamZlHAkKcxE=
github.com/mazznoer/csscolorparser v0.1.3/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=

View file

@ -1,2 +0,0 @@
*qtpl.go.tmp
*qtpl.go

View file

@ -1,1208 +0,0 @@
{% import cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils" %}
{% code
type ApiPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
PoolInfoExample cmdutils.PoolInfoResult
}
%}
{% func (p *ApiPage) Style() %}
{%= p.BasePage.Style() %}
pre {
padding: 2px;
border: 1px dashed #BBB;
}
{% endfunc %}
{% func (p *ApiPage) Title() %}
{%= p.BasePage.Title() %} - API Documentation
{% endfunc %}
{% func (p *ApiPage) Content() %}
<div class="center" style="min-width: 800px; width: 50%">
<div>
<h2>API Documentation</h2>
<p>This site and API is in development. As such, returned data could be different, or methods be deprecated / replaced anytime. Though it will be avoided if possible without affecting performance :)</p>
<p>All the data showed on this site comes from the API, P2Pool or Monero. So if new features are added on site, it's possible to do so via the API.</p>
<p>At the moment there is no broad rate-limiting. If abuse is detected, temporary rate limits can be put in place. You can always run your own instance of this site / API / consensus, see <a href="{%s p.Context().GetUrl("git.gammaspectra.live") %}/P2Pool/p2pool-observer">source code</a>.</p>
<p>Some historical data could be incomplete. Some block records do not exist.</p>
<p>In general, mandatory parameters are specified on the path, optional parameters on query.</p>
<p><strong>Knowledge of P2Pool internals is strongly advised.</strong> Feel free to ask questions on IRC or Matrix.</p>
<p>Unless specified, all returned data is JSON with status code 200. A not found error will issue a status code 404 and JSON of <code>{"error": "not_found"}</code> or similar.</p>
</div>
<div>
<h3>Common types and fields</h3>
<p>
Fields within JSON objects may be omitted if marked as <em>omitempty</em> on its definition within the source code. An empty/zero representation is assumed.
</br>
For example, a types.Hash marked as <em>omitempty</em> would represent the value <span class="smaller mono">0000000000000000000000000000000000000000000000000000000000000000</span>.
</br>
For pointers/structs, the zero or nil value is considered empty. In lists, an empty / length of zero is assumed when empty.
</br>
Refer to source code to check the struct definition when in doubt.
</p>
<h4>types.Hash</h4>
<p>Represents a 256-bit hash value. This can refer to a block template id, main id, parents, transaction ids, proof of work hashes or any other value.</p>
<p>This value is encoded as 64 hexadecimal characters.</p>
<h4>crypto.PublicKeyBytes / crypto.PrivateKeyBytes</h4>
<p>Represents an <em>edwards25519</em> Point or Scalar in binary form. Encoded as a 256-bit value. This can refer to a public keys on a block, transaction keys or any other value.</p>
<p>This value is encoded as 64 hexadecimal characters.</p>
<h4>address.Address</h4>
<p>Represents a Monero address. Includes valid checksums.</p>
<p>This value is encoded using special base58.</p>
<h4>types.Difficulty</h4>
<p>Represents a 128-bit unsigned integer value. This can refer to weight, block difficulty or cumulative difficulty.</p>
<p>This value is encoded as a 64-bit unsigned integer when the highest 64-bits are not set. Otherwise, it is encoded as 32 hexadecimal characters.</p>
</div>
<hr/>
<div>
<h3 class="mono">/api/pool_info</h3>
<p>Response contains general information on the current status of the P2Pool sidechain and Monero network, release versions, and other parameters.</p>
<pre class="smaller">
[Live result. Has been cut due to size, relevant portions shown]
curl --silent https://{%s p.Context().NetServiceAddress %}/api/pool_info
{%= encodeJson(p.PoolInfoExample, true) %}
</pre>
</div>
<hr/>
<div>
<h3 class="mono">/api/miner_info/&lt;id|address&gt;</h3>
<p>Response contains general information on a specific miner, referred by its internal <em>id</em> or known Monero <em>address</em>.</p>
<p>If you do not need the precise count of shares please pass the <em>?shareEstimates</em> parameter.</p>
<pre class="smaller">
curl --silent https://{%s p.Context().NetServiceAddress %}/api/miner_info/47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw
{
"id": 3,
"address": "47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"shares": [
{
"shares": 1022,
"uncles": 0,
"last_height": 4371641
},
{
"shares": 559294,
"uncles": 17861,
"last_height": 4868281
},
{
"shares": 69,
"uncles": 0,
"last_height": 4624653
}
],
"last_share_height": 4868281,
"last_share_timestamp": 1681256979
}
</pre>
</div>
<hr/>
<div>
<h3 class="mono">/api/side_blocks_in_window?window=[window_size]&from=[height][&noMainStatus][&noUncles][&noMiners]</h3>
<p>Response contains SideChain Blocks on specified range window. Optional parameters are <em>height</em>, denoting the Tip where the window gets measured from, and <em>window_size</em>, max SIDECHAIN_PPLNS_WINDOW * 4 * 7</p>
<p>By default it will return current window blocks, from current tip.</p>
<pre class="smaller">
[result has been cut due to size, relevant portions shown]
curl --silent https://{%s p.Context().NetServiceAddress %}/api/side_blocks_in_window
[
{
"main_id": "61c6463961c31584617f2ffd322fd32f91cef4a956b3ac431c128ab1ef8342c8",
"main_height": 2863488,
"template_id": "8782465759db40c11f64e78a26df4ec6221fb91202cc19a3beaef61acb8e1a50",
"side_height": 4882215,
"parent_template_id": "2ee8e55e2d0696d715049f5958c300c10b01d0a30b607c82afb8885a76f00d71",
"miner": 3,
"effective_height": 4882215,
"nonce": 3691022871,
"extra_nonce": 1665655721,
"timestamp": 1681402700,
"software_id": 0,
"software_version": 196610,
"window_depth": 294,
"window_outputs": 30,
"transaction_count": 2,
"difficulty": 2107950958,
"cumulative_difficulty": 7610896210501088,
"pow_difficulty": 3167867138,
"pow_hash": "3e19a8e6c1db97bae77fb19b68b5bf6c927e3b212063ee219e26155b01000000",
"inclusion": 1,
"miner_address": "47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"main_difficulty": 310626726029
},
{
"main_id": "30e8afa1c6961afae7686f4c9c2ee5ad77bd87bdcbf7da00958af53b3349264a",
"main_height": 2863488,
"template_id": "2ee8e55e2d0696d715049f5958c300c10b01d0a30b607c82afb8885a76f00d71",
"side_height": 4882214,
"parent_template_id": "e0b745e8e3004832d95ef7eb6aa5f43dd0f86a491b6edb8ccd8e8dfbf92f6768",
"miner": 2,
"effective_height": 4882214,
"nonce": 3791814950,
"extra_nonce": 610282123,
"timestamp": 1681402699,
"software_id": 0,
"software_version": 196610,
"window_depth": 293,
"window_outputs": 30,
"transaction_count": 2,
"difficulty": 2108162718,
"cumulative_difficulty": 7610894102550130,
"pow_difficulty": 5140029450,
"pow_hash": "591082e76b158724c0a1a3ea8ffdec93eaa33a8d371eda2ad85ae9d500000000",
"inclusion": 1,
"miner_address": "435QwkC3fJ4YeX929gmaBMeuUwR1wdmRrQBNyoKfpJdHKDbqnukHJeA768yt8ngnS7UTHRJ2yJmnWP8gCz6s3kkiBX8C363",
"uncles": [
{
"template_id": "4005ede80787679e83d269f09f6435ac4952d9c2cf2fadf999620ed81a948d40",
"miner": 77,
"side_height": 4882213,
"difficulty": 2109635914
}
],
"main_difficulty": 310626726029
},
{
"main_id": "6d1865f59d8cd38f5de6a498a2a5465040194f63122187b6e439743913aac202",
"main_height": 2863487,
"template_id": "4005ede80787679e83d269f09f6435ac4952d9c2cf2fadf999620ed81a948d40",
"side_height": 4882213,
"parent_template_id": "985d621e565da6da1f772dffef4b1154678cc724b6b18eff6edde5e68136a45d",
"miner": 77,
"uncle_of": "2ee8e55e2d0696d715049f5958c300c10b01d0a30b607c82afb8885a76f00d71",
"effective_height": 4882214,
"nonce": 3326443597,
"extra_nonce": 3803058762,
"timestamp": 1681402680,
"software_id": 0,
"software_version": 196610,
"window_depth": 294,
"window_outputs": 31,
"transaction_count": 0,
"difficulty": 2109635914,
"cumulative_difficulty": "0000000000000000001b0a106401568a",
"pow_difficulty": 2153295646,
"pow_hash": 7610889884751498,
"inclusion": 1,
"miner_address": "457fCKqWYSacjDRLjqah9kTga8KCQRNwL4WP7EfBNQKghYF3RZunjGHCoPSd6F7Y6mVRpT6eCgkEtin3cKvSCf11AqcP33g",
"main_difficulty": 310568847048
},
{
"main_id": "f5582e27b94a34713b607d0f47821142c8b88f8a3335d5b2b1d679c909a9651f",
"main_height": 2863486,
"template_id": "e0b745e8e3004832d95ef7eb6aa5f43dd0f86a491b6edb8ccd8e8dfbf92f6768",
"side_height": 4882213,
"parent_template_id": "985d621e565da6da1f772dffef4b1154678cc724b6b18eff6edde5e68136a45d",
"miner": 18,
"effective_height": 4882213,
"nonce": 3506931857,
"extra_nonce": 1625389436,
"timestamp": 1681402674,
"software_id": 0,
"software_version": 196610,
"window_depth": 294,
"window_outputs": 31,
"transaction_count": 115,
"difficulty": 2109635914,
"cumulative_difficulty": 7610889884751498,
"pow_difficulty": 3604028547,
"pow_hash": "1d01609ded322d6665fb96410042cc5cc9604be078b77967a818143101000000",
"inclusion": 1,
"miner_address": "4BBb6ZBPPPWivZnx2m3m7YUUGQv54F9HfjPWurf2Xy2UDQwwPyb956dGQ3p9whrxNJ568zc3Ygfr9McVT1UJyRKtLi1jcMb",
"main_difficulty": 310847993727
}
]
</pre>
</div>
<hr/>
<div>
<h3 class="mono">/api/side_blocks_in_window/&lt;id|address&gt;?window=[window_size]&from=[height][&noMainStatus][&noUncles][&noMiners]</h3>
<p>Response contains SideChain Blocks on specified range window for a specific miner. Optional parameters are <em>height</em>, denoting the Tip where the window gets measured from, and <em>window_size</em>, max SIDECHAIN_PPLNS_WINDOW * 4 * 7</p>
<p>By default it will return current window blocks, from current tip.</p>
<pre class="smaller">
[result has been cut due to size, relevant portions shown]
curl --silent https://{%s p.Context().NetServiceAddress %}/api/side_blocks_in_window/47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw
[
{
"main_id": "b41a9e78f11b9f7eaed30a4211f43ae7201f7c77031839f3e484b59c32088028",
"main_height": 2863492,
"template_id": "ce8de3ca5a9a066c0b4f5d5068783b17ace476f56236c65f5c949b9039c0ca3e",
"side_height": 4882250,
"parent_template_id": "a0cb288e467f5a4069603f4047c8607f1721a4b62fc8e2f794a5a942828fdf32",
"miner": 3,
"effective_height": 4882250,
"nonce": 2046825740,
"extra_nonce": 1477203696,
"timestamp": 1681403097,
"software_id": 0,
"software_version": 196610,
"window_depth": 296,
"window_outputs": 28,
"transaction_count": 75,
"difficulty": 2099479471,
"cumulative_difficulty": 7610976044280876,
"pow_difficulty": 2770364525,
"pow_hash": "ea3d41d8874fb1de05aee76a0fb6df7267b07e34ee95cdf1f924e28c01000000",
"inclusion": 1,
"miner_address": "47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"main_difficulty": 311336952550
},
{
"main_id": "fea4ee3769b350f2aad0571ff54eb555dbdf7f0cb811c19dea66bef286b041c4",
"main_height": 2863492,
"template_id": "74caf5d6ba885d05adbb4e4d611edabb55f0e05dcbb20cd3105d2adbcf4cdd1b",
"side_height": 4882248,
"parent_template_id": "3378e835d72a821b5157d8bebaf4f2193ebaf9cb86c93a106fc4fbbb1921c158",
"miner": 3,
"effective_height": 4882248,
"nonce": 3456175326,
"extra_nonce": 2803310737,
"timestamp": 1681403084,
"software_id": 0,
"software_version": 196610,
"window_depth": 296,
"window_outputs": 28,
"transaction_count": 73,
"difficulty": 2103245329,
"cumulative_difficulty": 7610971844410631,
"pow_difficulty": 5249306449,
"pow_hash": "4ab091d18ac65c212feff8f1a971aee1da50c177620827326b5d75d100000000",
"inclusion": 1,
"miner_address": "47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"main_difficulty": 311336952550
},
{
"main_id": "277a19bf024b4895553bbefc047eeb7733b95a97df6d8422759ec874d2d626c1",
"main_height": 2863492,
"template_id": "52ad4bb20d62b0e9d7762bab8d27de05563be6147cf3367d9bc79688f794217c",
"side_height": 4882246,
"parent_template_id": "40865b24eb4fbd1428ff1444d21b4337c8922b60b63a3ab6bdfef2586775390b",
"miner": 3,
"effective_height": 4882246,
"nonce": 754974898,
"extra_nonce": 2953378118,
"timestamp": 1681403082,
"software_id": 0,
"software_version": 196610,
"window_depth": 296,
"window_outputs": 28,
"transaction_count": 72,
"difficulty": 2104858109,
"cumulative_difficulty": 7610967638144376,
"pow_difficulty": 4514527717,
"pow_hash": "fc127598502b38c4977f7faef3f7db6bab11bab42353baed9ab58cf300000000",
"inclusion": 1,
"miner_address": "47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"uncles": [
{
"template_id": "ab07fe1e4a6a00ae799bedc637c1b642b4938d302ec16db5b513923937614abe",
"miner": 7,
"side_height": 4882244,
"difficulty": 2105207888
}
],
"main_difficulty": 311336952550
},
{
"main_id": "ff68eedc72252289e066d2dd59c209dc1502500d2cf4501fb69a91b69e2415b8",
"main_height": 2863492,
"template_id": "4764acc29d604f1491d600504d2802fea156a47ad691420b63d2c52924570925",
"side_height": 4882241,
"parent_template_id": "12f8b044e315669904f81caa055eac84f9a7d30d93bdd446cf304d38f18097cf",
"miner": 3,
"effective_height": 4882241,
"nonce": 4026633700,
"extra_nonce": 2755066571,
"timestamp": 1681403011,
"software_id": 0,
"software_version": 196610,
"window_depth": 296,
"window_outputs": 29,
"transaction_count": 47,
"difficulty": 2099621378,
"cumulative_difficulty": 7610955003350240,
"pow_difficulty": 5985480395,
"pow_hash": "6e0cc4b1e989a18a8f7a65c5aa5ad252a071cd57d877cac4f14bb2b700000000",
"inclusion": 1,
"miner_address": "47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"main_difficulty": 311336952550
}
]
</pre>
</div>
<hr/>
<div>
<h3 class="mono">/api/payouts/&lt;id|address&gt;?search_limit=[search_limit]</h3>
<p>Response contains payouts received by this miner due to blocks on Monero network. Optional parameters are <em>search_limit</em>, maximum amount of records to return, set 0 to return all. Defaults to 10</p>
<p><em>.[].coinbase_private_key</em> can be used to prove the payout is correct.</p>
<pre class="smaller">
[result has been cut due to size, relevant portions shown]
curl --silent https://{%s p.Context().NetServiceAddress %}/api/payouts/47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw
[
{
"miner": 3,
"template_id": "cebb0c11be67c43c1a01cf2ac43e6bafe745a92012bbbb04be03842f56c5cc78",
"side_height": 5428093,
"main_id": "8e0c7b08f92be1bdc5c4153f7a8c3e536f1353aff898684477cc6ba857b8510e",
"main_height": 2910747,
"timestamp": 1687085695,
"coinbase_id": "e1b20914f4ff768277c19daa65ef5f2ace7cda6f3f2586caf569c3008b7510b1",
"coinbase_reward": 3400854816,
"coinbase_private_key": "9438ef07d1bb10ae05e68cdf900938807fe8639acf7dab27feedc08119072b02",
"coinbase_output_index": 26,
"global_output_index": 75393989,
"including_height": 5427726
},
{
"miner": 3,
"template_id": "9bab936c12790d7e006ef1e97bf00572e85eb2f5647ce6320491fc93ba47f6a0",
"side_height": 5427897,
"main_id": "8bf0f5f17c53a68e5fb36a89781acd84c5936c4b06287aad09c038dac8b52860",
"main_height": 2910737,
"timestamp": 1687083786,
"coinbase_id": "413b6cac611c49d543d058a7d4ea908ba8a3774ced6781c2fdcdbbc0b187f24a",
"coinbase_reward": 103013108631,
"coinbase_private_key": "4344896bdacc7c58758090b955c7fd098f7414cdf773ec39fd12808f1fb4090f",
"coinbase_output_index": 11,
"global_output_index": 75393229,
"including_height": 5427531
},
{
"miner": 3,
"template_id": "566257bf73bc937a7b5bb6c3b6c1a2008db078a56d4fcbb2e34bde6f2b847d55",
"side_height": 5427764,
"main_id": "d74f9c0201c248911d6c306f7e6b5d58dea469ecf40ffd891e59dd0f6d082aea",
"main_height": 2910725,
"timestamp": 1687082281,
"coinbase_id": "561ed794fbdf5502fbd934fe7d7e4b9f4ccd4a8f7436fb93bc51c1e6dd431357",
"coinbase_reward": 170359596687,
"coinbase_private_key": "0332c944b6919f8e284993c35f810ce6b0c43cbf5f743f339fd4aaa197787109",
"coinbase_output_index": 14,
"global_output_index": 75392201,
"including_height": 5427402
}
]
</pre>
</div>
<hr/>
<div>
<h3 class="mono">/api/found_blocks?limit=[limit]&miner=[id|address]</h3>
<p>Response contains last found block records on the Monero network by P2Pool. Optional parameters are <em>limit</em>, maximum amount of records to return, defaults to 50, max 1000. Additionally, you can pass the <em>miner</em> parameter to select blocks from that miner only. <em>coinbase</em> parameter can also be used here to decode coinbase outputs.</p>
<pre class="smaller">
curl --silent https://{%s p.Context().NetServiceAddress %}/api/found_blocks?limit=2
[
{
"main_block": {
"id": "be284c7fc784ca83f92c2f1eac2e02524a99a63876179dae6c370a199d533d95",
"height": 2942191,
"timestamp": 1690866882,
"reward": 600373170000,
"coinbase_id": "691eb1e5b9222df2c667a481370acdd751c7f5f3cd0345ac9fc0f1feea396f7c",
"difficulty": 260315236919,
"side_template_id": "b76318caa4822826573ceebcabbd0309d2a710dd83700cb5d0cae206d9835cb4",
"coinbase_private_key": "5cdea4c89482c19588527238d32bdad97151c4e40800882183222d080ff5f203"
},
"side_height": 5789985,
"miner": 2915,
"effective_height": 5789985,
"window_depth": 438,
"window_outputs": 41,
"transaction_count": 2,
"difficulty": 1301336116,
"cumulative_difficulty": 9202373148639334,
"inclusion": 1,
"miner_address": "42HEEF3NM9cHkJoPpDhNyJHuZ6DFhdtymCohF9CwP5KPM1Mp3eH2RVXCPRrxe4iWRogT7299R8PP7drGvThE8bHmRDq1qWp"
},
{
"main_block": {
"id": "ac6ae3cfe0f241e22aae6b7a165e7254f380d50c5a690994aa462120c00c4f16",
"height": 2942190,
"timestamp": 1690866866,
"reward": 608015760000,
"coinbase_id": "a7f784cc724f9e172d3bea13a18569b339e96d218b3fa54f9189abacbeb349ae",
"difficulty": 260396879898,
"side_template_id": "be9e779f4f8b2cd96c6e30a94c28c6d36af261ecf7cc195394471f112b850c49",
"coinbase_private_key": "896cde95bf6713d60b7ed4bafa88018e7418cd55db9232db08dc2de174cebd0b"
},
"side_height": 5789983,
"miner": 2915,
"effective_height": 5789983,
"window_depth": 438,
"window_outputs": 41,
"transaction_count": 49,
"difficulty": 1301263505,
"cumulative_difficulty": 9202370546222535,
"inclusion": 1,
"miner_address": "42HEEF3NM9cHkJoPpDhNyJHuZ6DFhdtymCohF9CwP5KPM1Mp3eH2RVXCPRrxe4iWRogT7299R8PP7drGvThE8bHmRDq1qWp"
}
]
</pre>
</div>
<hr/>
<div>
<h3 class="mono">/api/shares?limit=[limit]&miner=[id|address][&onlyBlocks][&noMainStatus][&noUncles][&noMiners]</h3>
<p>Response contains last found SideChain block records on P2Pool. Optional parameters are <em>limit</em>, maximum amount of records to return, defaults to 50, max SIDECHAIN_PPLNS_WINDOW. Additionally, you can pass the <em>miner</em> parameter to select shares from that miner only. <em>onlyBlocks</em> can be set to not receive uncle share entries.</p>
<pre class="smaller">
curl --silent https://{%s p.Context().NetServiceAddress %}/api/shares?limit=2
[
{
"main_id": "7717e2e59fb345ce3e5c5585b51632f32db2e98bb0a69b64a0b91f7b370be3b7",
"main_height": 2942216,
"template_id": "7f0ce404ab3d576d97dc4affacb5e443fb4c1f36f1bb09feed5b9af3dcca0169",
"side_height": 5790240,
"parent_template_id": "7070785f6ac7c486ab2ae7df443e6afc31010f87206b44c90be1339cd2ecfc70",
"miner": 2915,
"effective_height": 5790240,
"nonce": 1660977225,
"extra_nonce": 2706560389,
"timestamp": 1690869415,
"software_id": 0,
"software_version": 196613,
"window_depth": 424,
"window_outputs": 40,
"difficulty": 1327149158,
"cumulative_difficulty": 9202726518962104,
"pow_difficulty": 1720691274,
"pow_hash": "36685a8106e8b6a7450820f97cba0fa3cd2aa017fb4119f3737ffe7e02000000",
"inclusion": 1,
"transaction_count": 9,
"miner_address": "42HEEF3NM9cHkJoPpDhNyJHuZ6DFhdtymCohF9CwP5KPM1Mp3eH2RVXCPRrxe4iWRogT7299R8PP7drGvThE8bHmRDq1qWp",
"main_difficulty": 260565791438
},
{
"main_id": "9ce6483c303ada746b9e969c65235bcba0433865f6afd11153e94fb0e6d41046",
"main_height": 2942216,
"template_id": "7070785f6ac7c486ab2ae7df443e6afc31010f87206b44c90be1339cd2ecfc70",
"side_height": 5790239,
"parent_template_id": "d7477275674bd90bba39e4604cf2539ae99460856567895369cda165370ba081",
"miner": 5695,
"effective_height": 5790239,
"nonce": 460496,
"extra_nonce": 346986124,
"timestamp": 1690869412,
"software_id": 0,
"software_version": 196612,
"window_depth": 425,
"window_outputs": 40,
"difficulty": 1326689571,
"cumulative_difficulty": 9202725191812946,
"pow_difficulty": 1929563612,
"pow_hash": "1ba49ad252103dc9e5eba606a3c1fac0875e01b6163201d6b6f0d23902000000",
"inclusion": 1,
"transaction_count": 9,
"miner_address": "48bSCfGTKE4Kb3Fe3YWYgmNjgxjGqxZsP4mwHEQDVUQST2HxRGEX2566LbfmBE5bW2JcByTnjsWZiQZWj6f8XSpX4EDE9Gj",
"main_difficulty": 260565791438
}
]
</pre>
</div>
<hr/>
<div>
<h3 class="mono">/api/block_by_id/&lt;blockId&gt;[/full|/light|/raw|/info|/payouts|/coinbase]</h3>
<p>Response contains a block/share record on P2Pool by Template, Main Id, or Coinbase Id. You can select the JSON version or the binary format, or the full/light JSON format of the raw share.</p>
<p>Using <em>/payouts</em> you can query all payouts that include weights by this share.</p>
<p>Using <em>/coinbase</em> you can query all possible outputs and ownership for found and not found blocks.</p>
<pre class="smaller">
curl --silent https://{%s p.Context().NetServiceAddress %}/api/block_by_id/179cf9d77c7d198dc4d9a5deb03561b6121336fe9ea72fbda00fbf6eea34a34d
{
"main_id": "abf18da3388c1c5c8ca372ac4804c6b1b2adb511d6a6cc5f781d883211568eb0",
"main_height": 2863113,
"template_id": "179cf9d77c7d198dc4d9a5deb03561b6121336fe9ea72fbda00fbf6eea34a34d",
"side_height": 4877733,
"parent_template_id": "ffd6a96ce3131f043f273582a69d1eaa42b7557f47f7b241140cb22af9fbf556",
"miner": 3,
"effective_height": 4877733,
"nonce": 1694662668,
"extra_nonce": 2797508428,
"timestamp": 1681355864,
"software_id": 0,
"software_version": 196610,
"window_depth": 327,
"window_outputs": 33,
"difficulty": 1956175240,
"cumulative_difficulty": 7601017513575704,
"pow_difficulty": 1301275836828,
"pow_hash": "602d39809af709405eb8660028fdfa5bf80e71e6a4ec1fbb924ed80000000000",
"inclusion": 1,
"transaction_count": 5,
"mined_main_at_height": true,
"miner_address": "47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"main_difficulty": 332971955880
}
</pre>
</div>
<hr/>
<div>
<h3 class="mono">/api/block_by_height/&lt;blockHeight&gt;[/full|/light|/raw|/info|/payouts|/coinbase]</h3>
<p>Response contains a block/share record on P2Pool by SideChain height. You can select the JSON version or the binary format, or the full/light JSON format of the raw share.</p>
<p>Using <em>/payouts</em> you can query all payouts that include weights by this share.</p>
<p>Using <em>/coinbase</em> you can query all possible outputs and ownership for found and not found blocks.</p>
<pre class="smaller">
curl --silent https://{%s p.Context().NetServiceAddress %}/api/block_by_height/4868351
{
"main_id": "5b468e062dc4aa1570f956e6314f6d0634a772aa8d2905d8ab9c83026a7ac6af",
"main_height": 2862309,
"template_id": "09d1974eea6de73d3cb95bc495b0b24a7b6f6405eaada53c4ec1ad3f513a4cc8",
"side_height": 4868351,
"parent_template_id": "b74acca03b7593386dd54b8132bf79522c9861b41f7797d0ce4f2af5f35cc170",
"miner": 1,
"effective_height": 4868351,
"nonce": 3271624915,
"extra_nonce": 1072029533,
"timestamp": 1681257577,
"software_id": 0,
"software_version": 196609,
"window_depth": 323,
"window_outputs": 30,
"difficulty": 2002581944,
"cumulative_difficulty": 7579715115168858,
"pow_difficulty": 3782914221,
"pow_hash": "fe471ad3fbd112fabe2abebafeccd2cb8eb0077864e7cfe939eaa62201000000",
"inclusion": 1,
"transaction_count": 28,
"miner_address": "4ApeMQzcRS7huBW8HEGkug2e5kQxhhC2Ub9y8F9bc7arKHsCktAV24n5ezKM9PjX5yS3Pv4PPNMXT8rTAFVuKCRi1jEqJZd",
"main_difficulty": 326506434325
}
</pre>
</div>
<hr/>
<div>
<h3><span class="mono">/api/events</span> WebSocket Event API</h3>
<p>
Connect to this using a WebSocket library. Produces a stream of JSON events for new blocks (type <em>side_block</em>), found blocks (type <em>found_block</em>), orphaned blocks (type <em>orphaned_block</em>).
<pre class="smaller">
[result has been cut due to size, relevant portions shown]
{
"type":"side_block",
"side_block":{
"main_id":"77d54ef67b566439c7df43614928ee6a503d82fb16f69720ed2ca0852b4ea5b6",
"main_height":2871492,
"template_id":"7c5db68e5af86e66df94d03688a8f61d7e7cd4544dbadc3f4d2f7972c0c72b17",
"side_height":4973833,
"parent_template_id":"10e2b50eb5c688bea4852f25b0d3550fe0e4e8192ea0602a373fb48454496e20",
"miner":19,
"uncle_of":"372ee3a80d8ab9131878a56e5e2802aa3f797a55d38f76871f1eeeafd60cd828",
"effective_height":4973834,
"nonce":4127591028,
"extra_nonce":3737539655,
"timestamp":1682358001,
"software_id":0,
"software_version":196610,
"window_depth":333,
"window_outputs":31,
"transaction_count":2,
"difficulty":1902041368,
"cumulative_difficulty":"0000000000000000001ba95c40814041",
"pow_difficulty":39725018671,
"pow_hash":"f6ddc7bdb80b6050fc7dd9fea1ad009434b7bcda3e3cb309a295ad1b00000000",
"inclusion":1,
"miner_address":"4B4mXWBqrFYGk9W8t1J3UifSdnU723HHKf8VaWGSZGzLZUncHRJetPCjbDUha1udPd2ZpJrwYCmPv1jzvg3NCf7Z1YViuoa",
"main_difficulty":344857075709
}
}
{
"type":"side_block",
"side_block":{
"main_id":"808c9c2a413b8480cc5b445575580a6edd5d237cc120f18a668c6f602dff9c1b",
"main_height":2871492,
"template_id":"372ee3a80d8ab9131878a56e5e2802aa3f797a55d38f76871f1eeeafd60cd828",
"side_height":4973834,
"parent_template_id":"8d691190875157d160c8bc8787ab140b1855c1afcc7b8f3076bc3366711bb085",
"miner":2,
"effective_height":4973834,
"nonce":402718979,
"extra_nonce":3611692200,
"timestamp":1682358008,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":2,
"difficulty":1900059444,
"cumulative_difficulty":"0000000000000000001ba95d2320b48d",
"pow_difficulty":4388053454,
"pow_hash":"727019817d63c52ac2ce2067652804cbfc8d3be778fe5548d0bf91fa00000000",
"inclusion":1,
"miner_address":"435QwkC3fJ4YeX929gmaBMeuUwR1wdmRrQBNyoKfpJdHKDbqnukHJeA768yt8ngnS7UTHRJ2yJmnWP8gCz6s3kkiBX8C363",
"uncles":[
{
"template_id":"7c5db68e5af86e66df94d03688a8f61d7e7cd4544dbadc3f4d2f7972c0c72b17",
"miner":19,
"side_height":4973833,
"difficulty":1902041368
}
],
"main_difficulty":344857075709
}
}
{
"type":"side_block",
"side_block":{
"main_id":"220534c981f71a7d2d935825b5cf479d960345177387eb7fcd196a762b018453",
"main_height":2871492,
"template_id":"320a1a1d8849d48243670938efa640a271b289fae392ea35eb1a7daf88d3e2c8",
"side_height":4973835,
"parent_template_id":"372ee3a80d8ab9131878a56e5e2802aa3f797a55d38f76871f1eeeafd60cd828",
"miner":3,
"effective_height":4973835,
"nonce":3825436994,
"extra_nonce":2417583792,
"timestamp":1682358009,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":3,
"difficulty":1899187725,
"cumulative_difficulty":"0000000000000000001ba95d9454029a",
"pow_difficulty":4870458263,
"pow_hash":"d6b5911b6b13e6258e2f1d23ab5256cf9b3c4a002e0722f15b4cc0e100000000",
"inclusion":1,
"miner_address":"47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"main_difficulty":344857075709
}
}
{
"type":"side_block",
"side_block":{
"main_id":"3e735ce653c2f1c312125309367ad6016e3f55c80834efd63fdb94d0929d017a",
"main_height":2871492,
"template_id":"3f089efbc7fd31eb98705665acbbe1e7d3b290866cb94f39a0d2e072f2a11f14",
"side_height":4973836,
"parent_template_id":"320a1a1d8849d48243670938efa640a271b289fae392ea35eb1a7daf88d3e2c8",
"miner":9,
"effective_height":4973836,
"nonce":132358,
"extra_nonce":1448692177,
"timestamp":1682358019,
"software_id":0,
"software_version":196609,
"window_depth":332,
"window_outputs":31,
"transaction_count":7,
"difficulty":1896666403,
"cumulative_difficulty":"0000000000000000001ba95e0560d7bd",
"pow_difficulty":15371353282,
"pow_hash":"cf9774d250b905729cb988b24437ac5f27ccc55d78d6388ca7a8874700000000",
"inclusion":1,
"miner_address":"4AxaapsBMRYMJHFV5uApnFJth7stf4GiK9sqnLAH8SizguBbZ8YYtE2C3aF3UGVbh3ATRKdtd7ai1Hi1zfzMkdqmAZLqaGN",
"main_difficulty":344857075709
}
}
{
"type":"side_block",
"side_block":{
"main_id":"3ecbfae652abf9ecfd043d8921b3c18f9acc6f08a8521bd4a06c46b8ff5181f9",
"main_height":2871492,
"template_id":"ba8514bd7403c16a61b7e20f34981d9cb72de09a7b0593d55ef14d55bff1908e",
"side_height":4973837,
"parent_template_id":"3f089efbc7fd31eb98705665acbbe1e7d3b290866cb94f39a0d2e072f2a11f14",
"miner":2,
"effective_height":4973837,
"nonce":1879082776,
"extra_nonce":1309685883,
"timestamp":1682358030,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":8,
"difficulty":1897117455,
"cumulative_difficulty":"0000000000000000001ba95e76748ecc",
"pow_difficulty":4828639998,
"pow_hash":"979dbacd27e0c15bfa93a72bda30f9e4e472ab86cfc9bc5f71ceb4e300000000",
"inclusion":1,
"miner_address":"435QwkC3fJ4YeX929gmaBMeuUwR1wdmRrQBNyoKfpJdHKDbqnukHJeA768yt8ngnS7UTHRJ2yJmnWP8gCz6s3kkiBX8C363",
"main_difficulty":344857075709
}
}
{
"type":"side_block",
"side_block":{
"main_id":"f7aab8fda85491a5b3847435fa16ae925d55b60791c01faf5a23c6d5365ef6ca",
"main_height":2871492,
"template_id":"19278cd3a3022a25edf4b91a5544a88b8c8f6f78f84e862b01781de493b562c8",
"side_height":4973838,
"parent_template_id":"ba8514bd7403c16a61b7e20f34981d9cb72de09a7b0593d55ef14d55bff1908e",
"miner":3,
"effective_height":4973838,
"nonce":2868937852,
"extra_nonce":869103007,
"timestamp":1682358036,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":8,
"difficulty":1894821734,
"cumulative_difficulty":"0000000000000000001ba95ee7653e32",
"pow_difficulty":2019716446,
"pow_hash":"2d03c8818798cb1bde4a6d2d64d0a59a04b8228e94aff841599c632002000000",
"inclusion":1,
"miner_address":"47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"main_difficulty":344857075709
}
}
{
"type":"side_block",
"side_block":{
"main_id":"877b4df53e2347e19d94ba52e04522601d461c0dc5d0439e8ec87a5ecb58a31f",
"main_height":2871492,
"template_id":"000c39fc469e4062fe4da6fc8444c3c990ad817397c94abe13c46720a4c9ab75",
"side_height":4973839,
"parent_template_id":"19278cd3a3022a25edf4b91a5544a88b8c8f6f78f84e862b01781de493b562c8",
"miner":2,
"effective_height":4973839,
"nonce":822086856,
"extra_nonce":1665899192,
"timestamp":1682358038,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":10,
"difficulty":1893298342,
"cumulative_difficulty":"0000000000000000001ba95f583eaed8",
"pow_difficulty":29696623614,
"pow_hash":"b1aa60a163d39b0524f7448954f770de8cbb873e346b63e37659062500000000",
"inclusion":1,
"miner_address":"435QwkC3fJ4YeX929gmaBMeuUwR1wdmRrQBNyoKfpJdHKDbqnukHJeA768yt8ngnS7UTHRJ2yJmnWP8gCz6s3kkiBX8C363",
"main_difficulty":344857075709
}
}
{
"type":"side_block",
"side_block":{
"main_id":"b6516743e3693824f0d8eceab7109c4242661f78e498f78e42a57bd602b7b167",
"main_height":2871493,
"template_id":"af88e464df1f31761996031f5c851f1a59d6ecb4b6e917407e1164ce6a569a07",
"side_height":4973840,
"parent_template_id":"000c39fc469e4062fe4da6fc8444c3c990ad817397c94abe13c46720a4c9ab75",
"miner":19,
"effective_height":4973840,
"nonce":1543602719,
"extra_nonce":2911703055,
"timestamp":1682358053,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":0,
"difficulty":1893967043,
"cumulative_difficulty":"0000000000000000001ba95fc922539b",
"pow_difficulty":1157101996504,
"pow_hash":"d855eed5d88837eb44c3d0a80457981fd39e9af67e8b58a03142f30000000000",
"inclusion":1,
"miner_address":"4B4mXWBqrFYGk9W8t1J3UifSdnU723HHKf8VaWGSZGzLZUncHRJetPCjbDUha1udPd2ZpJrwYCmPv1jzvg3NCf7Z1YViuoa",
"mined_main_at_height":true,
"main_difficulty":344633368046
}
}
{
"type":"found_block",
"found_block":{
"main_block":{
"id":"b6516743e3693824f0d8eceab7109c4242661f78e498f78e42a57bd602b7b167",
"height":2871493,
"timestamp":1682358053,
"reward":600000000000,
"coinbase_id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"difficulty":344633368046,
"side_template_id":"af88e464df1f31761996031f5c851f1a59d6ecb4b6e917407e1164ce6a569a07",
"coinbase_private_key":"1331ab860507192987e98497b971daac66e8804c51ad115ab6b2bf3d2abd270d"
},
"side_height":4973840,
"miner":19,
"effective_height":4973840,
"window_depth":332,
"window_outputs":31,
"transaction_count":0,
"difficulty":1893967043,
"cumulative_difficulty":"0000000000000000001ba95fc922539b",
"inclusion":1,
"miner_address":"4B4mXWBqrFYGk9W8t1J3UifSdnU723HHKf8VaWGSZGzLZUncHRJetPCjbDUha1udPd2ZpJrwYCmPv1jzvg3NCf7Z1YViuoa"
},
"main_coinbase_outputs":[
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":0,
"global_output_index":72132937,
"miner":30,
"value":1747977812,
"miner_address":"47J5KAEgevN8TKMhstn85mbASkhpZq5BtSt9afcseHPEVtDggZ8uBdxgoTRjdP2ooS4Px1y1pXeryhr9UFQqaykY5J4Ev85"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":1,
"global_output_index":72132938,
"miner":195,
"value":1736642632,
"miner_address":"47goCBjSdpqAGUFkc1nM9F7gNcSfSjtKuTqcLxGpEUn6DgboSZMpn2CMRkQ2JKWbxRJvHy2fbSJxtFqXVDbQqs5wPpxgbYD"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":2,
"global_output_index":72132939,
"miner":10,
"value":1737989471,
"miner_address":"46vavwU3QDmK2Jdw6CgezsB5538rbwFGi67EPWqkEBfvWuuiFTNaPVQPNSbxVc1UNbN6E7xakVkCxCnttvgyohaN4U9F7kf"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":3,
"global_output_index":72132940,
"miner":1,
"value":3139170892,
"miner_address":"4ApeMQzcRS7huBW8HEGkug2e5kQxhhC2Ub9y8F9bc7arKHsCktAV24n5ezKM9PjX5yS3Pv4PPNMXT8rTAFVuKCRi1jEqJZd"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":4,
"global_output_index":72132941,
"miner":47,
"value":1751320020,
"miner_address":"47Eqp7fsvVnPPSU4rsXrKJhyAme6LhDRZDzFky9xWsWUS9pd6FPjJCMDCNX1NnNiDzTwfbAgGMk2N6A1aucNcrkhLffta1p"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":5,
"global_output_index":72132942,
"miner":26,
"value":6966089461,
"miner_address":"42yAHAH5at9ZHvMXvb1xPV2aKnoibjZWCfTgQg2HdxumFx857g3DK75GbDKzmvu8QMW8CnWxuPAYyfQADjwKw6dy78x2gbp"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":6,
"global_output_index":72132943,
"miner":6,
"value":1739307539,
"miner_address":"4AzyJ87Tp3yLXjA3E5v9iAEnTTLu6UTeSdatiwto92n91z7NE11QmpV4YGatwS3ihvFsmvTfz85bfKMLBBAsZMGFKcJJ8jG"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":7,
"global_output_index":72132944,
"miner":18,
"value":11494230879,
"miner_address":"4BBb6ZBPPPWivZnx2m3m7YUUGQv54F9HfjPWurf2Xy2UDQwwPyb956dGQ3p9whrxNJ568zc3Ygfr9McVT1UJyRKtLi1jcMb"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":8,
"global_output_index":72132945,
"miner":19,
"value":39395535008,
"miner_address":"4B4mXWBqrFYGk9W8t1J3UifSdnU723HHKf8VaWGSZGzLZUncHRJetPCjbDUha1udPd2ZpJrwYCmPv1jzvg3NCf7Z1YViuoa"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":9,
"global_output_index":72132946,
"miner":842,
"value":3477271817,
"miner_address":"457Ht8LpLt2iq8y5HShzySduWTC58JvGLj6eWiURzUxNVXrDBWJc9jd3niHHLZ4o6hHYpufyd8zhL9EMktn3p3EdHQ6LAkj"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":10,
"global_output_index":72132947,
"miner":1104,
"value":1746268557,
"miner_address":"48LszLe98KgS1H4ELW9rmEH6dTar1uWdZDx7ttDHNSZvcANADw9PUbDU3KWREmgozxMbzofNmj8m5ZuieukEsj7aHjfo3in"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":11,
"global_output_index":72132948,
"miner":445,
"value":1748663264,
"miner_address":"44XavwVX8pP2U5NxtwGusU2obpoK55nNVBBqWLC2aJBX2yhAozsC1tvVG8NcC2PEQy2SRmBuB4WqTfVgfBCS3NkY1wYHZ3T"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":12,
"global_output_index":72132949,
"miner":156,
"value":1755605809,
"miner_address":"42FUiB6725E4UDkVXMJ3nZ21dhV9atHP9Z1yqxMGfNhnDkJUhdxDaTGaCej35H346eS5vcbEVY4kzdLVhKM63LjNJxFYMUF"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":13,
"global_output_index":72132950,
"miner":23,
"value":1749533284,
"miner_address":"46aSNdCK35w9fdX3iCwk3GLt3iZ4hnR34j2mJtxE14MWLXB5JRXxvzmjMUoz2mmFZk9mkqQRpCbPd2TbFqcQxqGkHxJPHUK"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":14,
"global_output_index":72132951,
"miner":61,
"value":3491378711,
"miner_address":"44YdBost8RfFjVT9W712c5KN1ZeTq5rPC6Uu12D4nBkUMBHxeb7TxKfGnAGLbfsvbaKtBvwzqKLqBWf7cpU7d2nZ55pXeCU"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":15,
"global_output_index":72132952,
"miner":647,
"value":24776511644,
"miner_address":"4AQ3YkqG2XdWsPHEgrDGdyQLq1qMMGFqWTFJfrVQW99qPmCzZKvJqzxgf5342KC17o9bchfJcUzLhVW9QgNKTYUBLg876Gt"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":16,
"global_output_index":72132953,
"miner":11,
"value":1739154107,
"miner_address":"42safVv7VKVLrGc1RWEgVqi9YJRV8XiUeFViStfLPWE8FAH1i4HvovcXntT8k5MvVuKkpYLcjELwL4Azrcb65MZC2AkoDoJ"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":17,
"global_output_index":72132954,
"miner":4,
"value":82984625360,
"miner_address":"4ADyLD6c7hzT5gCYxWnw7k2q5RPDxwqNTMsHcz9n91ir22YJbjBQ4LjGZgcLUVV7tyG33vXfB5SR1iCWaCjEQ7BNJUg6SyJ"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":18,
"global_output_index":72132955,
"miner":111,
"value":1741890162,
"miner_address":"45vMeqsvpW5UoQ4HQDjbUoLpmrLkhyVKgiSeR9WwuCpLV9egZtkwGGYPJ5HDvdAVtgF4bcu93dzEc6mLFbYRqVJzAQWHRnL"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":19,
"global_output_index":72132956,
"miner":8,
"value":5240626741,
"miner_address":"47oeGVdUWXDPSqjgFd2dQS57S3W4KPGnneq1o8hAdGE2ZwMZzKHeLFhiQoguKMFq4jScJ2qL7jrHoiP8xh2Su6SuFtxk3Da"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":20,
"global_output_index":72132957,
"miner":5283,
"value":1750270617,
"miner_address":"45j2xT1JbU27Mv8ScdumCC5bg3P9EU1HEE2DKZ9TzVje6wVE8kKqviwFuvwaoNywwzh7uiJoxfpWe4jp5HvYDZgyKdU2iEt"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":21,
"global_output_index":72132958,
"miner":179,
"value":1749846757,
"miner_address":"42TUuapMMbs4taYHYFnFTeFAM57WzVKDNXn4yeAQcJcgHq1m6q993EedsF2ebd7WP5Gjixw3NPu6ncH7WFWfsPDiPSWEnr9"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":22,
"global_output_index":72132959,
"miner":639,
"value":3497561198,
"miner_address":"49bNJ3mdVBfGjr3vWTVhXNig75gCxHQCwNG2FyvRErxs7U4A1c8K6Pn39Tq8oZAxtbXsfNsBtyKzT8apMkYD67QTS2v42oW"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":23,
"global_output_index":72132960,
"miner":15,
"value":5247959737,
"miner_address":"44N5jui2nbtQaxWjs9hoJBX5ELrCk5ftja7Zu3QCGTA1FftJhDKrEDL92Rw12j8FALHKnJEUb6KZ45SAncJ6iqKN6nShj2S"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":24,
"global_output_index":72132961,
"miner":2161,
"value":1750436503,
"miner_address":"44WHz1PGu8mYWxVE1p6vVtXDtvKtkd7eY23DdXncavA9Ybb6kP9i6BrhZschXpxGCHdTfBoLDznv55SYUqDhoCyJJxV84tD"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":25,
"global_output_index":72132962,
"miner":91,
"value":2100792894,
"miner_address":"48tdvJBtQN5Vt3jmLDTTYX7dpS9oDXATAdD8XFhuE1CshWUdShUsEif6UivD5iGsroKGEKqxGqA1aFA2XVx8rtmiFAqKT6E"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":26,
"global_output_index":72132963,
"miner":2,
"value":125897735171,
"miner_address":"435QwkC3fJ4YeX929gmaBMeuUwR1wdmRrQBNyoKfpJdHKDbqnukHJeA768yt8ngnS7UTHRJ2yJmnWP8gCz6s3kkiBX8C363"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":27,
"global_output_index":72132964,
"miner":5190,
"value":1741473336,
"miner_address":"49trSMSTsAmE2uMeCo1MmEUvfk6i9DWF15skLn9hut11ehcq1vLHkYi9sTujrcDG61dLFk61LeWMafa3EkamaGEUD1fSvRe"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":28,
"global_output_index":72132965,
"miner":9,
"value":50246935661,
"miner_address":"4AxaapsBMRYMJHFV5uApnFJth7stf4GiK9sqnLAH8SizguBbZ8YYtE2C3aF3UGVbh3ATRKdtd7ai1Hi1zfzMkdqmAZLqaGN"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":29,
"global_output_index":72132966,
"miner":4990,
"value":15680539194,
"miner_address":"4B4aCvEcZr6GcusVJfEds2LXixCeJ2dQBaDUCguWmzi5L7PW5tVXfAnE4cn1mQdiNzH6zWcEPMQTiYTsNcX44ryxCJWZKZH"
},
{
"id":"9ba3e0449f6ff3a108a877f2f37eedf0b87f811ff5725634e109ba0d2789d479",
"index":30,
"global_output_index":72132967,
"miner":3,
"value":190176655762,
"miner_address":"47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw"
}
]
}
{
"type":"side_block",
"side_block":{
"main_id":"5594278518615f55a7e911cca93280fd0c3d2dff1d1fd184db6602e0afb69e0b",
"main_height":2871494,
"template_id":"0ef3e5ddcb0c4f6398eb833361b3749f92fc232a049c52052c5da956c9bbc868",
"side_height":4973841,
"parent_template_id":"af88e464df1f31761996031f5c851f1a59d6ecb4b6e917407e1164ce6a569a07",
"miner":4,
"effective_height":4973841,
"nonce":2466322468,
"extra_nonce":893603150,
"timestamp":1682358055,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":0,
"difficulty":1894526953,
"cumulative_difficulty":"0000000000000000001ba9603a0e8384",
"pow_difficulty":11901446960,
"pow_hash":"bbe4a67f6a9bca4028ca1faa940d692f834978d10ed18e34d07b625c00000000",
"inclusion":1,
"miner_address":"4ADyLD6c7hzT5gCYxWnw7k2q5RPDxwqNTMsHcz9n91ir22YJbjBQ4LjGZgcLUVV7tyG33vXfB5SR1iCWaCjEQ7BNJUg6SyJ",
"main_difficulty":344205037143
}
}
{
"type":"side_block",
"side_block":{
"main_id":"6c50a9aa027a09b41c56f965f8e21041649c1638a956576fc3fc350441e0f4ab",
"main_height":2871494,
"template_id":"5f72a35b571746ac3835b204020e548e65a7350d84d5d7a7aa3a749dc058f7ad",
"side_height":4973842,
"parent_template_id":"0ef3e5ddcb0c4f6398eb833361b3749f92fc232a049c52052c5da956c9bbc868",
"miner":19,
"effective_height":4973842,
"nonce":1090620480,
"extra_nonce":162834581,
"timestamp":1682358073,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":1,
"difficulty":1893992163,
"cumulative_difficulty":"0000000000000000001ba960aaf28a67",
"pow_difficulty":4704880707,
"pow_hash":"bc26c47392a94004bc736822520f67bbf56dfba432e6d75aa12ab2e900000000",
"inclusion":1,
"miner_address":"4B4mXWBqrFYGk9W8t1J3UifSdnU723HHKf8VaWGSZGzLZUncHRJetPCjbDUha1udPd2ZpJrwYCmPv1jzvg3NCf7Z1YViuoa",
"main_difficulty":344205037143
}
}
{
"type":"side_block",
"side_block":{
"main_id":"617ea6fe4a47a3e7f51c8cc719a5158b88a4ca2583cd2f0cd5ed0b006bca3564",
"main_height":2871494,
"template_id":"b95dcd9d440fcd49a050c76f006189c01e1857d11bfc0b41f3fa560c15a4750f",
"side_height":4973843,
"parent_template_id":"5f72a35b571746ac3835b204020e548e65a7350d84d5d7a7aa3a749dc058f7ad",
"miner":3,
"effective_height":4973843,
"nonce":4026630147,
"extra_nonce":2353672144,
"timestamp":1682358101,
"software_id":0,
"software_version":196610,
"window_depth":332,
"window_outputs":31,
"transaction_count":15,
"difficulty":1895322145,
"cumulative_difficulty":"0000000000000000001ba9611beadc88",
"pow_difficulty":6105552648,
"pow_hash":"5c06f2ebd7fc22dca35bed2a0faa9e1aad7e36a5c89af94d587915b400000000",
"inclusion":1,
"miner_address":"47ab14EokGgCTX7RYoVhrNMjVA7GfW1jyMAmL7qBQz9fa4RZ6ZsBUgeRGuPWjqeM1wLptSJH5xuX2H4mAepMYvu6JqWMsGw",
"main_difficulty":344205037143
}
}
</pre>
</p>
</div>
<hr/>
<div>
<h3>Redirect APIs</h3>
<p>Several endpoints are available that redirect/forward your request via <em>Location</em> HTTP header to the relevant API listed above.</p>
<p>
<h4 class="mono">/api/last_found[/raw|...]?...</h4>
Redirects to the <em>/api/block_by_id/...</em> record for the last block found on the Monero network by P2Pool.
</p>
<p>
<h4 class="mono">/api/tip[/raw|...]?...</h4>
Redirects to the <em>/api/block_by_id/...</em> record for the last share found by P2Pool.
</p>
</div>
</div>
{% endfunc %}

View file

@ -1,77 +0,0 @@
{% import (
_ "embed"
) %}
{% code
//go:embed "css/style.css"
var styleCssContent string
%}
{% interface
Page {
Title()
Content()
Style()
}
%}
{% code
type ContextProviderPage interface {
Page
Context() *GlobalRequestContext
}
type ContextSetterPage interface {
ContextProviderPage
SetContext(ctx *GlobalRequestContext)
}
%}
Page prints a page implementing Page interface.
{% func PageTemplate(p ContextProviderPage) %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{%=h p.Title() %}</title>
<style>
{%= p.Style() %}
</style>
</head>
<body>
{%= Header(p.Context()) %}
<div class="content center">
{%= p.Content() %}
</div>
{%= Footer(p.Context()) %}
</body>
</html>
{% endfunc %}
Base page implementation. Other pages may inherit from it if they need
overriding only certain Page methods
{% code type BasePage struct {
ctx *GlobalRequestContext
} %}
{% func (p *BasePage) Title() %}
{% if p.Context().SiteTitle == "" %}
P2Pool Observer
{% else %}
{%s p.Context().SiteTitle %}
{% endif %}
{% endfunc %}
{% func (p *BasePage) Body() %}{% endfunc %}
{% code
func (p *BasePage) Context() *GlobalRequestContext {
return p.ctx
}
func (p *BasePage) SetContext(ctx *GlobalRequestContext) {
p.ctx = ctx
}
%}
{% func (p *BasePage) Style() %}
{%s= styleCssContent %}
{% endfunc %}

View file

@ -1,42 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address" %}
{% code
type BlocksPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Refresh int
FoundBlocks []*index.FoundBlock
Miner *address.Address
}
%}
{% func (p *BlocksPage) Title() %}
{% if p.Miner == nil %}
{%= p.BasePage.Title() %} - Recent Found Monero blocks
{% else %}
{%= p.BasePage.Title() %} - Found Monero blocks by {%z= p.Miner.ToBase58() %}
{% endif %}
{% endfunc %}
{% func (p *BlocksPage) Content() %}
{% if p.Miner == nil %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/blocks">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/blocks?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
{%endif %}
<div style="text-align: center">
{% if p.Miner == nil %}
<h2>Recent Found Monero blocks</h2>
{% else %}
<h2>{%= p.BasePage.Title() %} - Found Monero blocks by {%z= p.Miner.ToBase58() %}</h2>
{% endif %}
{%= TemplateFoundBlocks(p.Context(), p.FoundBlocks, p.Miner != nil) %}
</div>
{% endfunc %}

View file

@ -1,170 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/utils" %}
{% code
type CalculateShareTimePage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Hashrate float64
Magnitude float64
Efforts []CalculateShareTimePageEffortEntry
EstimatedRewardPerDay uint64
EstimatedSharesPerDay float64
EstimatedBlocksPerDay float64
}
%}
{% code
type CalculateShareTimePageEffortEntry struct {
Effort float64
Probability float64
Between float64
BetweenSolo float64
ShareProbabilities []float64
}
%}
{% func (p *CalculateShareTimePage) Title() %}
{%= p.BasePage.Title() %} - Average Share Time Calculator
{% endfunc %}
{% func (p *CalculateShareTimePage) Content() %}
<style type="text/css">
.bulk {
color: #00dddd;
}
.mode {
font-weight: bolder;
/* color: rgb(100,206,145); */
color: #00ffff;
}
</style>
<div style="text-align: center">
<form action="/calculate-share-time" method="get">
<h2>Average Share Time Calculator</h2>
<p>
This tool calculates your average expected time between shares on the pool at current difficulty.
<br/>
<small>Do note this time will vary (can get shares sooner, or later, even way later), depending on your luck, and changing difficulty of the pool due to increase / reductions in hashrate.</small>
<br/>
For lower hashrate, this average is what statistically will be achieved long term.
<br/>
The long term average per-share effort would be <span style="font-weight:bolder; color: {%s effort_color(100) %};">100%</span>
</p>
<div>
<label for="hashrate">Your Hashrate</label><br/>
<input type="numeric" name="hashrate" id="hashrate" placeholder="100" size="8" class="mono" value="{% if p.Hashrate > 0 %}{%s str(p.Hashrate) %}{% endif %}"/>
<select name="magnitude">
<option value="1"{% if p.Magnitude == 1 %} selected{% endif %}>H/s</option>
<option value="1000"{% if p.Magnitude == 1000 %} selected{% endif %}>KH/s</option>
<option value="1000000"{% if p.Magnitude == 1000000 %} selected{% endif %}>MH/s</option>
<option value="1000000000"{% if p.Magnitude == 1000000000 %} selected{% endif %}>GH/s</option>
</select>
</div>
<div style="margin-top: 10px">
<input type="submit" value="Calculate" style="width: 20em;"/>
</div>
</form>
</div>
{% if p.Hashrate > 0 %}
<hr/>
<div style="text-align: center">
<table class="center" style="max-width: calc(15em + 15em + 15em + 15em + 15em);">
{% code between := (float64(p.Context().Pool.SideChain.LastBlock.Difficulty) / (p.Hashrate * p.Magnitude)) %}
{% code between_solo := (float64(p.Context().Pool.MainChain.Difficulty.Lo) / (p.Hashrate * p.Magnitude)) %}
<tr style="line-height: 1.5;">
<td style="width: 15em"><strong>P2Pool Difficulty</strong><br/>{%s si_units(p.Context().Pool.SideChain.LastBlock.Difficulty, 2) %}</td>
<td style="width: 15em"><strong>P2Pool Hashrate</strong><br/>{%s si_units(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime), 2) %}H/s</td>
<td style="width: 15em"><strong>Your Hashrate</strong><br/>{%s si_units(p.Hashrate * p.Magnitude, 2) %}H/s</td>
<td title="Mean frequency between P2Pool shares" style="width: 15em; border: #ff6600 dashed 1px;"><strong>Your Share Mean<br/>{%s time_duration_long(between) %}</strong></td>
<td title="Mean P2Pool shares per day" style="width: 15em; border: #ff6600 dashed 1px;"><strong>Your Daily Mean Shares<br/>{%f.3 p.EstimatedSharesPerDay %} share(s)</strong></td>
</tr>
<tr><th colspan="5">&nbsp;</th></tr>
<tr style="line-height: 1.5;">
<td><strong>Monero Difficulty</strong><br/>{%s si_units(p.Context().Pool.MainChain.Difficulty.Lo, 2) %}</td>
<td><strong>Monero Hashrate</strong><br/>{%s si_units(diff_hashrate(p.Context().Pool.MainChain.Difficulty, uint64(p.Context().Pool.MainChain.Consensus.BlockTime)), 2) %}H/s</td>
<td title="Mean frequency between P2Pool finds Monero Blocks"><strong>P2Pool Block Mean</strong><br/><em>{%s time_duration_long(float64(p.Context().Pool.MainChain.Difficulty.Lo) / float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime))) %}</em></td>
<td title="Mean frequency between Solo Monero Blocks (without P2Pool)"><strong>Your Solo Block Mean</strong><br/><em>{%s time_duration_long(between_solo) %}</em></td>
<td title="Mean Solo Monero Blocks (without P2Pool) per day"><strong>Your Solo Daily Mean Blocks</strong><br/><em>{%f.3 p.EstimatedBlocksPerDay %} block(s)</em></td>
</tr>
<tr><th colspan="5">&nbsp;</th></tr>
<tr style="line-height: 1.5; font-size: 21px;"><td></td><td colspan="3" style="border: #ff6600 dashed 1px;"><strong>Your Estimated Daily Reward</strong><br/>{%s monero_to_xmr(p.EstimatedRewardPerDay) %} XMR</td><td></td></tr>
</table>
<br/>
<h3>Single Share Effort</h3>
<table class="center" style="max-width: calc(10em + 15em + 15em + 15em);">
<tr style="line-height: 1.5;">
<th>Share Effort</th>
<th>Found Probability</th>
<th>P2Pool Estimate</th>
<th>Solo Estimate</th>
</tr>
{% for _, e := range p.Efforts %}
<tr>
<td style="font-size: 17px; font-weight:bolder; color: {%s effort_color(e.Effort) %};">
{%f.0 e.Effort %}%
</td>
<td title="Chance share is not found before: {%f.5 100 - e.Probability %}%">
{%f.5 e.Probability %}%
</td>
<td>
{%s time_duration_long(e.Between) %}
</td>
<td>
{%s time_duration_long(e.BetweenSolo) %}
</td>
</tr>
{% endfor %}
</table>
<br/>
<h3>Cumulative Share Effort</h3>
<table class="center" style="max-width: calc(15em + 15em + {%d len(p.Efforts[0].ShareProbabilities)*2 %}em);">
<tr>
<th colspan="2"></th>
<th colspan="{%d len(p.Efforts[0].ShareProbabilities) %}">Number of shares</th>
</tr>
<tr style="line-height: 1.5;">
<th>Cumulative Effort</th>
<th>Cumulative Time</th>
{% for i := range p.Efforts[0].ShareProbabilities %}
<th>{%d i %}</th>
{% endfor %}
</tr>
{% for _, e := range p.Efforts %}
<tr>
<td style="font-size: 17px; font-weight:bolder; color: {%s effort_color(e.Effort) %};">
{%f.0 e.Effort %}%
</td>
<td>
{%s time_duration_long(e.Between) %}
</td>
{% code var mode = utils.ProbabilityMode(e.ShareProbabilities...) %}
{% for _, p := range e.ShareProbabilities %}
{% if p*100 > 1 %}
<td class="{% if mode == p %}mode{% else %}{% endif %}" title="{%f.5 p*100 %}%">{%f.0 p*100 %}%</td>
{% else %}
<td title="{%f.5 p*100 %}%"></td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
<tr>
<th colspan="2"></th>
{% for i := range p.Efforts[0].ShareProbabilities %}
<th>{%d i %}</th>
{% endfor %}
</tr>
</table>
</div>
{% endif %}
{% endfunc %}

View file

@ -1,128 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" %}
{% import p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types" %}
{% import "net/netip" %}
{% code
type ConnectivityCheckPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Address netip.AddrPort
YourTip *index.SideBlock
YourTipRaw *sidechain.PoolBlock
OurTip *index.SideBlock
Check *p2pooltypes.P2PoolConnectionCheckInformation[*sidechain.PoolBlock]
}
%}
{% func (p *ConnectivityCheckPage) Title() %}
{%= p.BasePage.Title() %} - Connectivity Check
{% endfunc %}
{% func (p *ConnectivityCheckPage) Content() %}
<div style="text-align: center">
<form action="/connectivity-check" method="get">
<h2>Connectivity Check</h2>
<p>
This tool tries to connect or looks up a running P2Pool node to verify ports have been properly opened.
<br/>
<small>You need to have external ports opened for this tool to work.</small>
</p>
<div>
<label for="address">IP Address and Port</label><br/>
<input type="text" name="address" id="address" placeholder="5.9.17.234:{%dul uint64(p.Context().Consensus.DefaultPort()) %}" size="20" class="mono" value="{% if p.Address.IsValid() %}{%s p.Address.String() %}{% endif %}"/>
</div>
<div style="margin-top: 10px">
<input type="submit" value="Check" style="width: 20em;"/>
</div>
</form>
</div>
{% if p.Address.IsValid() %}
<hr/>
<div style="text-align: center">
<h2>Connectivity Check Result</h2>
{% if p.Check == nil %}
<p style="color:#FF4433">Could not connect to host.</p>
{% elseif p.Check.Error != "" && p.Check.Address == "" %}
<p style="color:#FF4433">Error connecting to host: <code>{%s p.Check.Error %}</code>.</p>
{% if p.Check.Error == "disconnected before finishing handshake" %}
<p>This peer is likely running on a different SideChain or is running an incompatible P2Pool software version. Try a different Observer or verify your software.</p>
{% elseif p.Check.Error == "not_valid_ip" %}
<p>The IP Address you are trying to connect to is not valid or accepted on this tool.</p>
{% elseif p.Check.Error == "not_valid_port" %}
<p>The Port you are trying to connect to is not valid or accepted on this tool.</p>
{% endif %}
{% else %}
<table class="center" style="max-width: calc(8em + 28em + 8em + 28em + 20em)">
<tr style="line-height: 1.5;">
<td style="width: 10em"><strong>Peer Address</strong><br/>{%s p.Check.Address %}</td>
<td style="width: 10em"><strong>Peer Port</strong><br/>{%dul uint64(p.Check.Port) %}</td>
<td style="width: 10em"><strong>Listen Port</strong><br/>{% if p.Check.ListenPort > 0 %}{%dul uint64(p.Check.ListenPort) %}{% else %}-{% endif %}</td>
<td style="width: 10em"><strong>Peer Id</strong><br/>{% if p.Check.PeerId > 0 %}{%dul p.Check.PeerId %}{% else %}-{% endif %}</td>
<td style="width: 10em"><strong>Connection Direction</strong><br/>{% if p.Check.Incoming %}Incoming{% else %}Outgoing{% endif %}</td>
<td style="width: 10em"><strong>Connection Status</strong><br/>{% if p.Check.Closed %}Closed{% else %}Active{% endif %}{% if !p.Check.AlreadyConnected %}, new connection{% endif %}{% if p.Check.Banned %} (banned){% endif %}</td>
</tr>
{% if !p.Check.Closed || p.Check.PeerId > 0 %}
<tr style="line-height: 1.5;">
<td style="width: 10em"><strong>Peer Software</strong><br/>{%s p.Check.SoftwareId %} {%s p.Check.SoftwareVersion %}</td>
<td style="width: 10em"><strong>Peer Protocol</strong><br/>{%s p.Check.ProtocolVersion %}</td>
<td style="width: 10em"><strong>Handshake Complete</strong><br/>{% if p.Check.HandshakeComplete %}Yes{% else %}No{% endif %}</td>
<td style="width: 10em"><strong>Last Broadcast</strong><br/><span title="{%s utc_date(p.Check.BroadcastTime) %}">{%s time_elapsed_short(p.Check.BroadcastTime) %}</span></td>
<td style="width: 10em"><strong>Connection Duration</strong><br/><span title="{%s utc_date(p.Check.ConnectionTime) %}">{%s time_elapsed_short(p.Check.ConnectionTime) %}</span></td>
<td style="width: 10em"><strong>Connection Latency</strong><br/>{% if p.Check.Latency > 0 %}{%dul p.Check.Latency %}ms{% else %}-{% endif %}</td>
</tr>
{% endif %}
{% if p.Check.BanError != "" %}
<tr><th colspan="6">&nbsp;</th></tr>
<tr><td colspan="6" style="color:#FF4433"><strong>Previous Ban Error:</strong> <code>{%s p.Check.BanError %}</code></td></tr>
{% endif %}
{% if p.Check.Error != "" %}
<tr><th colspan="6">&nbsp;</th></tr>
<tr><td colspan="6" style="color:#FF4433"><strong>Error:</strong> <code>{%s p.Check.Error %}</code></td></tr>
{% if p.Check.Error == "disconnected before finishing handshake" %}
<tr><td colspan="6">This peer is likely running on a different SideChain or is running an incompatible P2Pool software version. Try a different Observer or verify your software.<br/>Alternatively this peer could have banned the Observer node and we cannot connect properly.</td></tr>
{% endif %}
{% endif %}
{% if p.Check.Tip != nil %}
<tr><th colspan="6">&nbsp;</th></tr>
<tr style="line-height: 1.5;">
<td style="width: 10em"><strong>Peer SideChain Height</strong><br/><a href="/share/{%= hex(p.Context(), p.YourTipRaw.SideTemplateId(p.Context().Consensus)) %}">{%dul p.YourTipRaw.Side.Height %}</a></td>
<td style="width: 10em"><strong>Peer SideChain Id</strong><br/><a href="/share/{%= hex(p.Context(), p.YourTipRaw.SideTemplateId(p.Context().Consensus)) %}" title="{%= hex(p.Context(), p.YourTipRaw.SideTemplateId(p.Context().Consensus)) %}">{%= shorten(p.Context(), p.YourTipRaw.SideTemplateId(p.Context().Consensus), 10) %}</a></td>
<td style="width: 10em"><strong>Peer MainChain Height</strong><br/>{%dul p.YourTipRaw.Main.Coinbase.GenHeight %}</td>
<td style="width: 10em"><strong>Peer Difficulty</strong><br/>{%s si_units(p.YourTipRaw.Side.Difficulty.Lo, 4) %}</td>
<td style="width: 10em"><strong>Peer Cumulative Difficulty</strong><br/>{%s si_units(p.YourTipRaw.Side.CumulativeDifficulty.Lo, 4) %}</td>
<td style="width: 10em"><strong>Peer Timestamp</strong><br/>{%s utc_date(p.YourTipRaw.Main.Timestamp) %}</td>
</tr>
<tr style="line-height: 1.5;">
<td style="width: 10em"><strong>Observer SideChain Height</strong><br/><a href="/share/{%= hex(p.Context(), p.OurTip.TemplateId) %}">{%dul p.OurTip.SideHeight %}</a></td>
<td style="width: 10em"><strong>Observer SideChain Id</strong><br/><a href="/share/{%= hex(p.Context(), p.OurTip.TemplateId) %}" title="{%= hex(p.Context(), p.OurTip.TemplateId) %}">{%= shorten(p.Context(), p.OurTip.TemplateId, 10) %}</a></td>
<td style="width: 10em"><strong>Observer MainChain Height</strong><br/>{%dul p.OurTip.MainHeight %}</td>
<td style="width: 10em"><strong>Observer Difficulty</strong><br/>{%s si_units(p.OurTip.Difficulty, 4) %}</td>
<td style="width: 10em"><strong>Observer Cumulative Difficulty</strong><br/>{%s si_units(p.OurTip.CumulativeDifficulty.Lo, 4) %}</td>
<td style="width: 10em"><strong>Observer Timestamp</strong><br/>{%s utc_date(p.OurTip.Timestamp) %}</td>
</tr>
<tr><th colspan="6">&nbsp;</th></tr>
{% if ((p.YourTipRaw.Main.Coinbase.GenHeight < (p.OurTip.MainHeight-3)) || (p.YourTipRaw.Main.Coinbase.GenHeight > (p.OurTip.MainHeight+3))) %}
<tr><td colspan="6" style="font-weight: bold; color:#FF4433">Peer Monero node is on a wildly different Monero height than Observer.<br/>Either peer node is lagging behind or your monerod is not up to sync.</td></tr>
{% endif %}
{% if p.YourTip == nil && ((p.YourTipRaw.Side.Height < (p.OurTip.SideHeight-3)) || (p.YourTipRaw.Side.Height > (p.OurTip.SideHeight+3))) %}
<tr><td colspan="6" style="font-weight: bold; color:#FF4433">Could not find Peer SideChain Tip on Observer.<br/>Either peer node is lagging behind or you are on a forked SideChain.</td></tr>
{% elseif (p.YourTipRaw.Side.Height < (p.OurTip.SideHeight-3)) || (p.YourTipRaw.Side.Height > (p.OurTip.SideHeight+3)) %}
<tr><td colspan="6" style="font-weight: bold">The peer is connectable and on the SideChain.<br/>Peer SideChain Tip is on a wildly different SideChain height than Observer.<br/>Either peer node is lagging behind or your p2pool is not up to sync.</td></tr>
{% else %}
<tr><td colspan="6" style="font-weight: bold">The peer is connectable and on the SideChain.<br/>{% if p.OurTip.TemplateId == p.YourTipRaw.SideTemplateId(p.Context().Consensus) %}The peer Tip matches exactly the Observer Tip.{% else %}Verify Peer SideChain Height against Observer SideChain Height, so it's not consistently different.{% endif %}</td></tr>
{% endif %}
<tr><th colspan="6">&nbsp;</th></tr>
<tr><td colspan="6">
{%= TemplatePoolBlock(p.Context(), p.YourTipRaw, "Peer Tip Share information") %}
</td></tr>
{% endif %}
{% endif %}
</table>
</div>
{% endif %}
{% endfunc %}

View file

@ -1,32 +0,0 @@
package views
import (
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
)
type GlobalRequestContext struct {
IsOnion bool
DonationAddress string
SiteTitle string
NetServiceAddress string
TorServiceAddress string
Consensus *sidechain.Consensus
Pool *cmdutils.PoolInfoResult
Socials struct {
Irc struct {
Title string
Link string
WebChat string
}
Matrix struct {
Link string
}
}
HexBuffer [types.HashSize * 2]byte
}
func (ctx *GlobalRequestContext) GetUrl(host string) string {
return cmdutils.GetSiteUrlByHost(host, ctx.IsOnion)
}

View file

@ -1,76 +0,0 @@
body {
margin: 0;
padding: 0;
color: #d1d1d1;
background-color: #4c4c4c;
font-family: Helvetica, Arial, sans-serif;
}
h1, h2, h3, h4, h5, h6 {
text-align: inherit;
}
ul{
list-style-position: inside;
}
.datatable tr, li, code.mono {
font-size: 12px;
}
.content {
margin-top: 15px;
margin-bottom: 30px;
}
.center {
margin-left: auto;
margin-right: auto;
width: 96%;
}
.hl-found{
background: rgba(0%, 75.3%, 0%, 0.5);
}
.mono {
font-family: "Lucida Console", Monaco, monospace;
white-space: nowrap;
line-break: strict;
}
a, a:hover, a:visited, a:link, a:active {
text-decoration: underline dotted #333;
color: inherit;
}
form {
display: inline-block;
text-align: center;
}
h1 {
color: #ff6600;
font-weight: bold;
}
h1 a {
text-decoration: none !important;
}
.small {
font-size: 80%;
}
.smaller {
font-size: 60%;
}
code.small {
font-size: 10px;
}
code.smaller {
font-size: 8px;
}

View file

@ -1,32 +0,0 @@
{% import "fmt" %}
{% code
type ErrorPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Code int
Message string
Error any
}
%}
{% func (p *ErrorPage) Title() %}
{%= p.BasePage.Title() %} - Error {%d p.Code %}
{% endfunc %}
{% func (p *ErrorPage) Content() %}
<div class="center" style="text-align: center">
<h2>Error {%d p.Code %} - {%s p.Message %}</h2>
{% if p.Error != nil %}
<div class="center" style="text-align: center">{%s fmt.Sprintf("%s", p.Error) %}</div>
{% endif %}
</div>
{% endfunc %}
{% code
func NewErrorPage(code int, message string, err any) *ErrorPage {
return &ErrorPage{Code: code, Message: message, Error: err}
}
%}

View file

@ -1,15 +0,0 @@
{% func Footer(ctx *GlobalRequestContext) %}
<div>
<footer class="center" style="text-align: center; margin-bottom: 20px;">
<a href="{%s ctx.GetUrl("git.gammaspectra.live") %}/P2Pool/p2pool-observer">source code</a>
&mdash;
<a href="https://{%s ctx.NetServiceAddress %}">{%s ctx.NetServiceAddress %}</a>
::
Tor <a href="http://{%s ctx.TorServiceAddress %}">{%s ctx.TorServiceAddress %}</a>
<br/>
Donate to P2Pool Observer developer on <a class="mono small" style="font-weight: bold" href="monero:{%s ctx.DonationAddress %}?tx_description=p2pool.observer">{%s ctx.DonationAddress %}</a> or OpenAlias <span class="mono">p2pool.observer</span> :: <a href="https://github.com/SChernykh/p2pool#donations">Donate to P2Pool Development</a>
<br/>
<strong>NOTE:</strong> This site is in development. You might find errors, incomplete or invalid data. :: <a target="_blank" href="{%s ctx.GetUrl("git.gammaspectra.live") %}/P2Pool/p2pool-observer/issues?state=open">Report any issues on tracker</a>, via IRC on <a target="_blank" href="ircs://irc.libera.chat:6697/#p2pool-observer">#p2pool-observer@libera.chat</a>, or via <a rel="nofollow" href="https://matrix.to/#/#p2pool-observer:monero.social">Matrix</a>
</footer>
</div>
{% endfunc %}

View file

@ -1,184 +0,0 @@
package views
import (
hex2 "encoding/hex"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/mazznoer/colorgrad"
"strconv"
"strings"
"time"
)
func utc_date[T int64 | uint64 | int | float64](v T) string {
return time.Unix(int64(v), 0).UTC().Format("02-01-2006 15:04:05 MST")
}
func time_elapsed_short[T int64 | uint64 | int | float64](v T) string {
diff := time.Since(time.Unix(int64(v), 0).UTC())
days := int64(diff.Hours() / 24)
hours := int64(diff.Hours()) % 24
minutes := int64(diff.Minutes()) % 60
seconds := int64(diff.Seconds()) % 60
var result []string
if days > 0 {
result = append(result, strconv.FormatInt(days, 10)+"d")
}
if hours > 0 {
result = append(result, strconv.FormatInt(hours, 10)+"h")
}
if minutes > 0 {
result = append(result, strconv.FormatInt(minutes, 10)+"m")
}
if seconds > 0 {
result = append(result, strconv.FormatInt(seconds, 10)+"s")
}
if v == 0 {
return "never"
} else if len(result) == 0 {
return "just now"
} else {
return strings.Join(result[0:1], " ") + " ago"
}
}
func side_block_weight(s *index.SideBlock, tipHeight, windowSize uint64, consensus *sidechain.Consensus) uint64 {
w, _ := s.Weight(tipHeight, windowSize, consensus.UnclePenalty)
return w
}
func si_units[T int64 | uint64 | int | float64](v T, n ...int) string {
if len(n) > 0 {
return utils.SiUnits(float64(v), n[0])
} else {
return utils.SiUnits(float64(v), 3)
}
}
func date_diff_short[T int64 | uint64 | int | float64](v T) string {
diff := time.Since(time.Unix(int64(v), 0).UTC())
s := fmt.Sprintf("%02d:%02d:%02d", int64(diff.Hours())%24, int64(diff.Minutes())%60, int64(diff.Seconds())%60)
days := int64(diff.Hours() / 24)
if days > 0 {
return strconv.FormatInt(days, 10) + ":" + s
}
return s
}
func time_duration_long[T int64 | uint64 | int | float64](v T) string {
diff := time.Second * time.Duration(v)
diff += time.Microsecond * time.Duration((float64(v)-float64(int64(v)))*1000000)
days := int64(diff.Hours() / 24)
hours := int64(diff.Hours()) % 24
minutes := int64(diff.Minutes()) % 60
seconds := int64(diff.Seconds()) % 60
ms := int64(diff.Milliseconds()) % 1000
var result []string
if days > 0 {
result = append(result, strconv.FormatInt(days, 10)+"d")
}
if hours > 0 {
result = append(result, strconv.FormatInt(hours, 10)+"h")
}
if minutes > 0 {
result = append(result, strconv.FormatInt(minutes, 10)+"m")
}
if seconds > 0 {
result = append(result, strconv.FormatInt(seconds, 10)+"s")
}
if len(result) == 0 || (len(result) == 1 && seconds > 0) {
result = append(result, strconv.FormatInt(ms, 10)+"ms")
}
return strings.Join(result, " ")
}
func benc(n uint64) string {
return utils.EncodeBinaryNumber(n)
}
func bencstr(val string) string {
if n, err := strconv.ParseUint(val, 10, 0); err == nil {
return utils.EncodeBinaryNumber(n)
} else {
panic(err)
}
}
func str(val any) string {
switch t := val.(type) {
case float32:
return strconv.FormatFloat(float64(t), 'f', -1, 64)
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
case uint32:
return strconv.FormatFloat(float64(t), 'f', -1, 64)
case uint64:
return strconv.FormatFloat(float64(t), 'f', -1, 64)
case int:
return strconv.FormatFloat(float64(t), 'f', -1, 64)
case fmt.Stringer:
return t.String()
default:
return fmt.Sprintf("%v", val)
}
}
func found_block_effort(b, previous *index.FoundBlock) float64 {
if previous == nil {
return 0
}
return float64(b.CumulativeDifficulty.Sub(previous.CumulativeDifficulty).Mul64(100).Lo) / float64(b.MainBlock.Difficulty)
}
var effortColorGradient = colorgrad.RdYlBu()
const effortRangeStart = 0.15
const effortRangeEnd = 0.85
func effort_color(effort float64) string {
probability := utils.ProbabilityEffort(effort)
// rescale
probability *= effortRangeEnd - effortRangeStart
probability += effortRangeStart
return effortColorGradient.At(1 - probability).Hex()
}
func monero_to_xmr(v uint64) string {
return utils.XMRUnits(v)
}
func diff_hashrate(v any, blockTime uint64) uint64 {
if strVal, ok := v.(string); ok {
if d, err := types.DifficultyFromString(strVal); err == nil {
return d.Div64(blockTime).Lo
}
} else if d, ok := v.(types.Difficulty); ok {
return d.Div64(blockTime).Lo
} else if d, ok := v.(uint64); ok {
return d / blockTime
}
return 0
}
func coinbase_extra(b *sidechain.PoolBlock) string {
coinbaseExtra := b.Main.Coinbase.Extra
var result []string
for _, t := range coinbaseExtra {
buf, _ := t.MarshalBinary()
result = append(result, hex2.EncodeToString(buf))
}
return strings.Join(result, " ")
}

View file

@ -1,180 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/types" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/utils" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% import p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types" %}
{% import "encoding/binary" %}
{% import "fmt" %}
{% import "slices" %}
{% import hex2 "encoding/hex" %}
{% import fasthex "github.com/tmthrgd/go-hex" %}
{% import qt "github.com/valyala/quicktemplate" %}
{% stripspace %}
{% code
func streamencodeJson(w *qt.Writer, val any, indent bool) {
encoder := utils.NewJSONEncoder(w.E())
if indent {
encoder.SetIndent("", " ")
}
if err := encoder.EncodeWithOption(val, utils.JsonEncodeOptions...); err != nil {
panic(err)
}
}
%}
{% func hex(ctx *GlobalRequestContext, val any) %}
{% switch s := val.(type) %}
{% case string %}
{%s s %}
{% case types.Difficulty %}
{% code
fasthex.Encode(ctx.HexBuffer[:types.DifficultySize*2], s.Bytes())
%}
{%z= ctx.HexBuffer[:types.DifficultySize*2] %}
{% case crypto.PrivateKeyBytes %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s[:])
%}
{%z= ctx.HexBuffer[:] %}
{% case crypto.PublicKeyBytes %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s[:])
%}
{%z= ctx.HexBuffer[:] %}
{% case crypto.PrivateKey %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s.AsSlice())
%}
{%z= ctx.HexBuffer[:] %}
{% case crypto.PublicKey %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s.AsSlice())
%}
{%z= ctx.HexBuffer[:] %}
{% case types.Hash %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s[:])
%}
{%z= ctx.HexBuffer[:] %}
{% case []byte %}
{%s= hex2.EncodeToString(s) %}
{% case uint32 %}
{% code
var buf [4]byte
binary.LittleEndian.PutUint32(buf[:], s)
fasthex.Encode(ctx.HexBuffer[:4*2], buf[:])
%}
{%z= ctx.HexBuffer[:4*2] %}
{% case uint64 %}
{% code
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], s)
fasthex.Encode(ctx.HexBuffer[:8*2], buf[:])
%}
{%z= ctx.HexBuffer[:8*2] %}
{% default %}
{%v val %}
{% endswitch %}
{% endfunc %}
{% func shorten(ctx *GlobalRequestContext, val any, n int) %}
{% switch s := val.(type) %}
{% case string %}
{%s utils.Shorten(s, n) %}
{% case []byte %}
{%z= utils.ShortenSlice(slices.Clone(s), n) %}
{% case crypto.PrivateKeyBytes %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s[:])
%}
{%z= utils.ShortenSlice(ctx.HexBuffer[:], n) %}
{% case crypto.PublicKeyBytes %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s[:])
%}
{%z= utils.ShortenSlice(ctx.HexBuffer[:], n) %}
{% case crypto.PrivateKey %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s.AsSlice())
%}
{%z= utils.ShortenSlice(ctx.HexBuffer[:], n) %}
{% case crypto.PublicKey %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s.AsSlice())
%}
{%z= utils.ShortenSlice(ctx.HexBuffer[:], n) %}
{% case types.Hash %}
{% code
fasthex.Encode(ctx.HexBuffer[:], s[:])
%}
{%z= utils.ShortenSlice(ctx.HexBuffer[:], n) %}
{% case fmt.Stringer %}
{%s utils.Shorten(s.String(), n) %}
{% default %}
{%s utils.Shorten(fmt.Sprintf("%v", val), n) %}
{% endswitch %}
{% endfunc %}
{% func henc(val any) %}
{% code
var buf [types.HashSize*2+1]byte
%}
{% switch s := val.(type) %}
{% case types.Hash %}
{% code
dst := utils.EncodeSliceBinaryNumber(buf[:], s[:])
%}
{%z= dst[:] %}
{% case crypto.PrivateKeyBytes %}
{% code
dst := utils.EncodeSliceBinaryNumber(buf[:], s[:])
%}
{%z= dst[:] %}
{% case string %}
{%s= utils.EncodeHexBinaryNumber(s) %}
{% case fmt.Stringer %}
{%s= utils.EncodeHexBinaryNumber(s.String()) %}
{% default %}
panic("type not allowed")
{% endswitch %}
{% endfunc %}
{% func software_info(softwareId p2pooltypes.SoftwareId, softwareVersion p2pooltypes.SoftwareVersion) %}
{% if softwareId == 0 && softwareVersion == 0 %}
Not present
{% else %}
{%s= softwareId.String() %}{%s= ` ` %}{%s= softwareVersion.String() %}
{% endif %}
{% endfunc %}
{% func side_block_valuation(b any, consensus *sidechain.Consensus) %}
{% switch block := b.(type) %}
{% case *index.SideBlock %}
{% if block.IsOrphan() %}
0%
{% elseif block.IsUncle() %}
{%dul 100-consensus.UnclePenalty %}%{%s= ` ` %}(uncle)
{% elseif len(block.Uncles) > 0 %}
100%{%s= ` ` %}+{%s= ` ` %}{%dul consensus.UnclePenalty %}%{%s= ` ` %}of{%s= ` ` %}{%d len(block.Uncles) %}{%s= ` ` %}uncle(s)
{% else %}
100%
{% endif %}
{% case *sidechain.PoolBlock %}
{% if len(block.Side.Uncles) > 0 %}
100%{%s= ` ` %}+{%s= ` ` %}{%dul consensus.UnclePenalty %}%{%s= ` ` %}of{%s= ` ` %}{%d len(block.Side.Uncles) %}{%s= ` ` %}uncle(s)
{% else %}
100%
{% endif %}
{% default %}
panic("type not allowed")
{% endswitch %}
{% endfunc %}
{% endstripspace %}

View file

@ -1,46 +0,0 @@
{% func Header(ctx *GlobalRequestContext) %}
<div>
<div class="center" style="text-align: center">
<h1 class="center"><a href="/">{% if ctx.SiteTitle == "" %}Monero P2Pool Observer{% else %}Monero {%s ctx.SiteTitle %}{% endif %}</a></h1>
<h4 style="font-size: 16px; margin: 0px">
<a href="https://gupax.io/" rel="nofollow">[Gupax] P2Pool plug and play Mining GUI, by <em>hinto-janai</em></a>
::
<a href="{%s ctx.GetUrl("p2pool.io") %}/#help" rel="nofollow">P2Pool Setup Help</a>
::
<a href="{%s ctx.GetUrl("p2pool.io") %}/#faq" rel="nofollow">P2Pool FAQ</a>
::
<a href="https://github.com/SChernykh/p2pool">What is P2Pool?</a>
::
<a href="/api">Observer API Documentation</a>
</h4>
{% if ctx.Socials.Irc.Link != "" %}
<h4 style="font-size: 16px; margin: 4px 0px 0px; font-weight: normal;">
Join chat to talk, send feedback, ask questions and be notified of changes on <a href="{%s ctx.Socials.Irc.Link %}" target="_blank"><strong>IRC on <code>{%s ctx.Socials.Irc.Title %}</code></strong></a>{% if ctx.Socials.Irc.WebChat != "" %}, on <a href="{%s ctx.Socials.Irc.WebChat %}" target="_blank"><strong>WebIRC</strong></a>{% endif %}{% if ctx.Socials.Matrix.Link != "" %}, on <a target="_blank" rel="nofollow" href="{%s ctx.Socials.Matrix.Link %}"><strong>Matrix</strong></a>{% endif %}
</h4>
{% endif %}
<h4 style="font-size: 13px; margin: 4px 0px 0px; font-weight: normal;">
<a href="https://github.com/SChernykh/p2pool#how-to-mine-on-p2pool">How to join P2Pool</a>
::
<a href="{%s ctx.GetUrl("sethforprivacy.com") %}/guides/run-a-p2pool-node/" rel="nofollow">Guide on how to run a Monero and P2Pool node by <em>Seth</em></a>
::
<a href="{%s ctx.GetUrl("xmrvsbeast.com") %}/p2pool/" rel="nofollow">P2Pool Bonus Hash Rate Raffle by <em>XMRvsBEAST</em></a>
</h4>
{% if pool := ctx.Pool; pool != nil %}
{% if uint64(pool.Versions.P2Pool.Timestamp+3600*24*30) > pool.SideChain.LastBlock.Timestamp || uint64(pool.Versions.Monero.Timestamp+3600*24*7) > pool.SideChain.LastBlock.Timestamp %}
<h4 style="font-size: 15px; margin: 4px 0px 0px; color:yellow;">
{% else %}
<h4 style="font-size: 13px; margin-top: 5px; ">
{% endif %}
Latest releases: <a href="{%s pool.Versions.P2Pool.Link %}" target="_blank">P2Pool {%s pool.Versions.P2Pool.Version %}</a> <em title="{%s utc_date(uint64(pool.Versions.P2Pool.Timestamp)) %}"><small>{%f.1 float64((pool.SideChain.LastBlock.Timestamp - uint64(pool.Versions.P2Pool.Timestamp))) / (3600*24) %} day(s) ago</small></em>
::
<a href="{%s pool.Versions.Monero.Link %}" target="_blank">Monero {%s pool.Versions.Monero.Version %}</a> <em title="{%s utc_date(uint64(pool.Versions.Monero.Timestamp)) %}"><small>{%f.1 float64((pool.SideChain.LastBlock.Timestamp - uint64(pool.Versions.Monero.Timestamp))) / (3600*24) %} day(s) ago</small></em>
</h4>
{% endif %}
</div>
</div>
{% endfunc %}

View file

@ -1,137 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% import cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils" %}
{% code
type IndexPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Refresh int
Positions struct {
BlocksFound *cmdutils.PositionChart
}
Shares []*index.SideBlock
FoundBlocks []*index.FoundBlock
}
%}
{% func (p *IndexPage) Content() %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
<div style="text-align: center">
<h2>P2Pool statistics</h2>
<table class="center" style="max-width: calc(15em + 15em + 15em + 15em)">
<tr>
<th style="width: 15em">P2Pool Height</th>
<th style="width: 15em">P2Pool Hashrate</th>
<th style="width: 15em">Monero Height</th>
<th style="width: 15em">Monero Hashrate</th>
</tr>
<tr>
<td title="{%= hex(p.Context(), p.Context().Pool.SideChain.LastBlock.TemplateId) %}"><a href="/share/{%= hex(p.Context(), p.Context().Pool.SideChain.LastBlock.TemplateId) %}">{%dul p.Context().Pool.SideChain.LastBlock.SideHeight %}</a></td>
<td>{%s si_units(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime), 2) %}H/s</td>
<td title="{%= hex(p.Context(), p.Context().Pool.MainChain.Id) %}"><a href="/b/{%s benc(p.Context().Pool.MainChain.Height) %}">{%dul p.Context().Pool.MainChain.Height %}</a></td>
<td>{%s si_units(diff_hashrate(p.Context().Pool.MainChain.Difficulty, uint64(p.Context().Pool.MainChain.Consensus.BlockTime)), 2) %}H/s</td>
</tr>
<tr><th colspan="4">&nbsp;</th></tr>
<tr>
<th>P2Pool Difficulty</th>
<th title="Percentage of Monero hashrate P2Pool has">P2Pool Monero %</th>
<th>Monero Difficulty</th>
<th title="This includes blocks the site knows about since it started observing. There might be more orphaned or produced by other sidechain not included here.">Blocks Found</th>
</tr>
<tr>
<td>{%s si_units(p.Context().Pool.SideChain.LastBlock.Timestamp, 2) %}</td>
<td>{%f.2 (float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime)) / float64(diff_hashrate(p.Context().Pool.MainChain.Difficulty, uint64(p.Context().Pool.MainChain.Consensus.BlockTime))))*100 %}%</td>
<td>{%s si_units(p.Context().Pool.MainChain.Difficulty.Lo, 2) %}</td>
<td>{%dul p.Context().Pool.SideChain.Found %}</td>
</tr>
<tr><th colspan="4">&nbsp;</th></tr>
<tr>
<th title="Miners that have ever mined a share on P2Pool">Miners Known</th>
<th>Average Effort</th>
<th title="Mean frequency between P2Pool finds Monero Blocks">Block Found Frequency</th>
<th>Last Found Block</th>
</tr>
<tr>
<td>{%dul p.Context().Pool.SideChain.Miners %}</td>
<td >
<span class="small" style="color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average10) %};" title="Last 10 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average10 %}%</span>
/
<span style="font-weight:bolder; color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average50) %};" title="Last 50 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average50 %}%</span>
/
<span class="small" style="color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average200) %};" title="Last 200 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average200 %}%</span>
</td>
<td>{%s time_duration_long(float64(p.Context().Pool.MainChain.Difficulty.Lo) / float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime))) %}</td>
{% if p.Context().Pool.SideChain.LastFound != nil %}
<td title="{%s utc_date(p.Context().Pool.SideChain.LastFound.MainBlock.Timestamp) %}">{%s time_elapsed_short(p.Context().Pool.SideChain.LastFound.MainBlock.Timestamp) %}</td>
{% else %}
<td>-</td>
{% endif %}
</tr>
<tr><th colspan="4">&nbsp;</th></tr>
<tr>
<th title="Current miners on P2Pool PPLNS window"><a href="/miners">Window Miners</a></th>
<th>Current Effort</th>
<th>Window Blocks</th>
<th>Last Share</th>
</tr>
<tr>
<td><a href="/miners">{%d p.Context().Pool.SideChain.Window.Miners %}</a></td>
<td style="font-weight:bolder; color: {%s effort_color(p.Context().Pool.SideChain.Effort.Current) %};">
{%f.2 p.Context().Pool.SideChain.Effort.Current %}%
</td>
<td>{%d p.Context().Pool.SideChain.Window.Blocks %} blocks (+{%d p.Context().Pool.SideChain.Window.Uncles %} uncles)</td>
<td title="{%s utc_date(p.Context().Pool.SideChain.LastBlock.Timestamp) %}">{%s time_elapsed_short(p.Context().Pool.SideChain.LastBlock.Timestamp) %}</td>
</tr>
</table>
</div>
<div style="text-align: center">
<form action="/miner" method="get">
<h2>Lookup miner statistics</h2>
<div>
<label for="miner-address">Payout Monero address or Miner Alias</label><br/>
<input type="text" name="address" id="miner-address" placeholder="{%s p.Context().DonationAddress %}" size="96" class="mono"/>
</div>
<div style="margin-top: 10px">
<input type="submit" value="Lookup" style="width: 20em;"/>
</div>
</form>
<p>
<a href="/calculate-share-time">[Average share time calculator]</a> :: <a href="/connectivity-check">[Connectivity Check]</a>
</p>
<p>
<a href="/miners">[Current Window Miners]</a> :: <a href="/miners?weekly">[Weekly Miners]</a>
</p>
<p>
<a href="/transaction-lookup">[Sweep Transaction Lookup]</a> :: <a href="/sweeps">[Recent Likely Sweep Transactions]</a>
</p>
</div>
<hr/>
<div style="text-align: center">
<h2>Recent Monero blocks found by P2Pool miners</h2>
{%= TemplateFoundBlocks(p.Context(), p.FoundBlocks, false) %}
<div class="center"><a href="/blocks">[show more found blocks]</a></div>
<h3>Blocks found during last day</h3>
<code class="mono">{%s p.Positions.BlocksFound.String() %}</code>
</div>
<hr/>
<div style="text-align: center">
<h2>Recent P2Pool shares found</h2>
{%= TemplateShares(p.Context(), p.Shares, false, nil) %}
</div>
{% endfunc %}

View file

@ -1,183 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% import cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils" %}
{% code
type MinerOptionsPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Miner *cmdutils.MinerInfoResult
SignedAction *cmdutils.SignedAction
WebHooks []*index.MinerWebHook
}
%}
{% func (p *MinerOptionsPage) Title() %}
{%= p.BasePage.Title() %} - Miner Options {%z= p.Miner.Address.ToBase58() %}
{% endfunc %}
{% func (p *MinerOptionsPage) Content() %}
<div style="text-align: center">
<h2 class="mono">Miner Options</h2>
<p><strong>Payout Address:</strong> <span class="mono small">{%z= p.Miner.Address.ToBase58() %}</span></p>
{% if p.Miner.Alias != "" %}
<p><strong>Miner Alias:</strong> <span class="mono">{%s p.Miner.Alias %}</span></p>
<p><small>Miner Alias is user content and not verified. This value should only be used for vanity purposes.</small></p>
{% endif %}
</div>
{% if p.SignedAction != nil %}
<div style="text-align: center;">
<form action="/miner-options/{%z= p.Miner.Address.ToBase58() %}/signed_action" method="get" target="_blank">
<h2>Sign and submit changes</h2>
<p>To submit any changes you will need to sign a message using your wallet to prove ownership.</p>
<p>On the Monero GUI, go to Advanced -> Sign/verify -> Select Mode "Message".</p>
<p>Enter the Message listed below in the "Message" field, then press Sign Message. Copy the Signature and paste it on the field below.</p>
<p>Do not modify the message, add spaces or add any new lines.</p>
<p><small>Signatures generated by a View-Only wallet are not supported. Monero GUI does not generate valid signatures for hardware wallets.</small></p>
<div>
<label for="message">Message</label><br/>
<input type="text" name="message" id="message" size="96" class="mono" value="{%s p.SignedAction.String() %}" style="user-select: all; -webkit-user-select: all;" readonly autofocus/>
</div>
<div style="margin-top: 10px">
<label for="signature">Signature</label><br/>
<input type="text" name="signature" id="signature" size="96" class="mono" placeholder="SigV2..." pattern="^SigV[12][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$" required/>
</div>
<div style="margin-top: 10px">
<input type="submit" value="Submit changes" style="width: 20em;"/>
</div>
</form>
</div>
{% else %}
<div style="text-align: center;">
<h2 id="mineralias">Miner Alias</h2>
<div>
<form action="/miner-options/{%z= p.Miner.Address.ToBase58() %}/set_miner_alias" method="get" target="_blank">
<h3>Set Miner Alias</h3>
<p><small>
Miner Alias must be 4 to 20 characters long, and only 0-9, a-z, A-Z, and _-. are allowed, and cannot start with a number or symbol. No spaces are allowed either.
</br>
Alias are unique for the observer instance, and can be removed or changed after adding one.
</small></p>
<div>
<label for="miner-alias">Miner Alias</label><br/>
<input type="text" name="alias" id="miner-alias" minlength="4" maxlength="20" size="20" class="mono" value="{%s p.Miner.Alias %}" pattern="^[a-zA-Z][0-9a-zA-Z_.\-]+$" required/>
</div>
<div style="margin-top: 10px">
<input type="submit" value="Set Alias" style="width: 20em;"/>
</div>
</form>
</div>
<div>
{% if p.Miner.Alias != "" %}
<form action="/miner-options/{%z= p.Miner.Address.ToBase58() %}/unset_miner_alias" method="get" target="_blank">
<h3>Unset Miner Alias</h3>
<p><small>You can remove your miner alias below if you don't want one set.</small></p>
<div style="margin-top: 10px">
<input type="submit" value="Unset Alias" style="width: 20em;"/>
</div>
</form>
{% endif %}
</div>
</div>
<div style="text-align: center;">
<h2 id="webhooks">Notification WebHooks</h2>
<div>
<form action="/miner-options/{%z= p.Miner.Address.ToBase58() %}/add_webhook" method="get" target="_blank">
<h3>Add WebHook</h3>
<p><small>
Configure notification URLs that can be used to post messages or other actions in other platforms or services.
</br>
Only one URL of each type is allowed. To adjust an existing hook settings you will need to add it with the new preference.
</small></p>
<div>
<label for="webhook-type">WebHook Type</label><br/>
<select type="text" name="type" id="webhook-type" required>
<option value=""></option>
<optgroup label="Chat Services">
<option value="{%s string(index.WebHookDiscord) %}">Discord</option>
<option value="{%s string(index.WebHookSlack) %}" disabled>Slack (coming soon)</option>
<option value="{%s string(index.WebHookTelegram) %}" disabled>Telegram (coming soon)</option>
<option value="{%s string(index.WebHookMatrixHookshot) %}" disabled>Matrix Hookshot (coming soon)</option>
</optgroup>
<optgroup label="Other">
<option value="{%s string(index.WebHookCustom) %}">Custom</option>
</optgroup>
</select>
</div>
</br>
<div>
<label for="webhook-url">WebHook URL</label><br/>
<input type="url" name="url" id="webhook-url" size="60" class="mono" value="" required/>
</div>
</br>
<fieldset>
<legend>WebHook Events</legend>
<div>
<input type="checkbox" id="webhook-side_blocks" name="side_blocks" checked />
<label for="webhook-side_blocks">Miner Side blocks</label>
</div>
<div>
<input type="checkbox" id="webhook-payouts" name="payouts" checked />
<label for="webhook-payouts">Miner Payouts</label>
</div>
<div>
<input type="checkbox" id="webhook-found_blocks" name="found_blocks" />
<label for="webhook-found_blocks">Found Monero blocks</label>
</div>
<div>
<input type="checkbox" id="webhook-orphaned_blocks" name="orphaned_blocks" />
<label for="webhook-orphaned_blocks">Orphaned Monero blocks</label>
</div>
<div>
<input type="checkbox" id="webhook-other" name="other" checked />
<label for="webhook-other">Other (notices, updates, events)</label>
</div>
</fieldset>
<div style="margin-top: 10px">
<input type="submit" value="Add WebHook" style="width: 20em;"/>
</div>
</form>
{% if len(p.WebHooks) > 0 %}
<h3>Current WebHooks</h3>
<table class="center datatable" style="max-width: calc(8em + 28em + 8em + 8em + 8em + 8em + 8em + 8em)">
<tr>
<th style="width: 8em;">Type</th>
<th style="width: 28em;" title="URL SHA-256 Hash">URL Hash</th>
<th style="width: 8em;">Shares</th>
<th style="width: 8em;">Payouts</th>
<th style="width: 8em;">Found Blocks</th>
<th style="width: 8em;">Orphaned Blocks</th>
<th style="width: 8em;">Other</th>
<th style="width: 8em;"></th>
</tr>
{% for _, w := range p.WebHooks %}
<tr>
<th>{%s string(w.Type) %}</th>
<td class="mono small">{%s w.Url %}</td>
<td>{% if cmdutils.CanSendToHook(w, "side_blocks") %}Yes{% else %}No{% endif %}</td>
<td>{% if cmdutils.CanSendToHook(w, "payouts") %}Yes{% else %}No{% endif %}</td>
<td>{% if cmdutils.CanSendToHook(w, "found_blocks") %}Yes{% else %}No{% endif %}</td>
<td>{% if cmdutils.CanSendToHook(w, "orphaned_blocks") %}Yes{% else %}No{% endif %}</td>
<td>{% if cmdutils.CanSendToHook(w, "other") %}Yes{% else %}No{% endif %}</td>
<td><a href="/miner-options/{%z= p.Miner.Address.ToBase58() %}/remove_webhook?type={%s string(w.Type) %}&url_hash={%s w.Url %}">[Remove]</a></td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</div>
{% endif %}
{% endfunc %}

View file

@ -1,277 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" %}
{% import p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types" %}
{% import cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils" %}
{% code
type MinerPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Refresh int
Positions struct {
Resolution int
ResolutionWindow int
SeparatorIndex int
Blocks *cmdutils.PositionChart
Uncles *cmdutils.PositionChart
BlocksInWindow *cmdutils.PositionChart
UnclesInWindow *cmdutils.PositionChart
Payouts *cmdutils.PositionChart
}
ExpectedRewardPerWindow uint64
ExpectedRewardPerDay uint64
WindowWeight uint64
Weight uint64
Miner *cmdutils.MinerInfoResult
LastPoolBlock *sidechain.PoolBlock
LastShares []*index.SideBlock
LastSharesEfforts []float64
LastOrphanedShares []*index.SideBlock
LastFound []*index.FoundBlock
LastPayouts []*index.Payout
LastSweeps <-chan *index.MainLikelySweepTransaction
HashrateSubmit bool
HashrateLocal float64
MagnitudeLocal float64
}
%}
{% func (p *MinerPage) Title() %}
{%= p.BasePage.Title() %} - Miner {%z= p.Miner.Address.ToBase58() %}
{% endfunc %}
{% func (p *MinerPage) Content() %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/miner/{%z= p.Miner.Address.ToBase58() %}">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/miner/{%z= p.Miner.Address.ToBase58() %}?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
<div style="text-align: center">
{% if p.Miner.LastShareTimestamp == 0 %}
<div style="border: #d1d1d1 1px dashed;">
<h3 style="color:#FF4433">No shares have been reported to this P2Pool network in the past for this miner.</h3>
<p>Finding shares is a random process based on your hashrate vs this P2Pool hashrate. This can take several days for low hashrate miners, and depending on your luck.</p>
<p>Please use the calculator below to find your average share time (with average luck).</p>
<p>You can also verify you are reachable by opening ports with the tool below.</p>
<br/>
<form action="/calculate-share-time" method="get">
<h3>Average Share Time Calculator</h3>
<label for="hashrate">Your Hashrate</label><br/>
<input type="numeric" name="hashrate" id="hashrate" placeholder="100" size="8" class="mono" value=""/>
<select name="magnitude">
<option value="1" selected>H/s</option>
<option value="1000">KH/s</option>
<option value="1000000">MH/s</option>
<option value="1000000000">GH/s</option>
</select>
<br/>
<input type="submit" value="Calculate" style="width: 20em; margin: 20px;"/>
</form>
<form action="/connectivity-check" method="get">
<h3>Connectivity Check</h3>
<div>
<label for="address">IP Address and Port</label><br/>
<input type="text" name="address" id="address" placeholder="5.9.17.234:{%dul uint64(p.Context().Consensus.DefaultPort()) %}" size="20" class="mono" value=""/>
</div>
<br/>
<div style="margin-top: 10px">
<input type="submit" value="Check" style="width: 20em;"/>
</div>
</form>
</div>
<hr/>
{% endif %}
{% if p.LastPoolBlock != nil && p.LastPoolBlock.ShareVersion() < sidechain.ShareVersion_V2 && p.LastPoolBlock.ShareVersionSignaling() != sidechain.ShareVersion_V2 %}
<div style="border: #d1d1d1 1px dashed;">
<h3 style="color:#FF4433" title="Share version {%s p.LastPoolBlock.ShareVersion().String() %}, signaling {%s p.LastPoolBlock.ShareVersionSignaling().String() %}">Recent shares indicate you are running an outdated version of P2Pool</h3>
<p>A new version of <a href="https://github.com/SChernykh/p2pool/releases/tag/v3.3">P2Pool (v3.0+)</a> has been released with several improvements, which requires a consensus change.</p>
<p>P2Pool (not Monero!) has hardforked to new consensus rules on <strong>March 18th, 2023 at 21:00 UTC</strong>. All versions before P2Pool v3.0 are incompatible. P2Pool v3.3+ is recommended.</p>
<p>If you keep using previous versions, you will keep mining as usual, but become almost a solo miner, as incompatible clients will mine on their own.</p>
<p>{% if p.Context().NetServiceAddress == "p2pool.observer" %}After the fork, you can check on <a href="{%s p.Context().GetUrl("old.p2pool.observer") %}/miner/{%z= p.Miner.Address.ToBase58() %}">OLD.P2Pool.Observer</a>.{% elseif p.Context().NetServiceAddress == "mini.p2pool.observer" %}After the fork, you can check on <a href="https://{%s p.Context().GetUrl("old-mini.p2pool.observer") %}/miner/{%z= p.Miner.Address.ToBase58() %}">OLD-MINI.P2Pool.Observer</a>.{% else %}Please check on an observer tracking the old chain.{% endif %}</p>
<p>After upgrading to a supported P2Pool version and mining a share, this message will be dismissed.</p>
<br/>
</div>
<hr/>
{% elseif p.LastPoolBlock != nil && p.LastPoolBlock.ShareVersion() > sidechain.ShareVersion_V1 && p.LastPoolBlock.Side.ExtraBuffer.SoftwareId == p2pooltypes.SoftwareIdP2Pool && p.LastPoolBlock.Side.ExtraBuffer.SoftwareVersion.String() != p.Context().Pool.Versions.P2Pool.ShortVersion().String() %}
<div style="border: #d1d1d1 1px dashed;">
<h3 style="color:#FF4433">Recent shares indicate you are running an older version of P2Pool</h3>
<p><a href="{%s p.Context().Pool.Versions.P2Pool.Link %}">P2Pool {%s p.Context().Pool.Versions.P2Pool.Version %}</a> has been released.</p>
<p>Your most recent share indicates are currently running {%= software_info(p.LastPoolBlock.Side.ExtraBuffer.SoftwareId, p.LastPoolBlock.Side.ExtraBuffer.SoftwareVersion) %}. It is recommended to upgrade.</p>
<p>After upgrading to this P2Pool version and mining a share, this message will be dismissed.</p>
<br/>
</div>
<hr/>
{% endif %}
<p><strong>Payout Address:</strong> <span class="mono small">{%z= p.Miner.Address.ToBase58() %}</span></p>
{% if p.Miner.Alias != "" %}
<p><strong>Miner Alias:</strong> <span class="mono">{%s p.Miner.Alias %}</span></p>
<p><small>Miner Alias is user content and not verified. This value should only be used for vanity purposes.</small></p>
{% endif %}
<p><small><a href="/miner-options/{%z= p.Miner.Address.ToBase58() %}#mineralias">[Change Miner Alias]</a> :: <a href="/miner-options/{%z= p.Miner.Address.ToBase58() %}#webhooks">[Configure WebHook notifications]</a></small></p>
<table class="center" style="max-width: calc(15em + 15em + 15em + 15em + 15em)">
<tr>
<th>Last Share</th>
<th>Current Shares</th>
<th>Estimated Hashrate</th>
<th>Pool Share %</th>
<th>Estimated Window Reward</th>
</tr>
<tr>
<td title="{%s utc_date(p.Miner.LastShareTimestamp) %}">{%s time_elapsed_short(p.Miner.LastShareTimestamp) %}</td>
<td>{%dul p.Positions.BlocksInWindow.Total() %} blocks (+{%dul p.Positions.UnclesInWindow.Total() %} uncles)</td>
{% code windowWeightRatio := float64(p.WindowWeight) / float64(p.Context().Pool.SideChain.Window.Weight.Lo) %}
<td>{%s si_units(windowWeightRatio * float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime)), 3) %}H/s</td>
<td>{%f.3 windowWeightRatio*100 %}%</td>
<td>{%s monero_to_xmr(p.ExpectedRewardPerWindow) %} XMR</td>
</tr>
<tr><td colspan="5">&nbsp;</td></tr>
<tr>
<th>Estimated Total Shares</th>
<th>Day Shares</th>
<th>Day Hashrate</th>
<th>Day Share %</th>
<th>Estimated Daily Reward</th>
</tr>
<tr>
<td>{% if p.LastPoolBlock != nil %}Around {%dul p.Miner.Shares[1].ShareCount %} blocks (+{%dul p.Miner.Shares[1].UncleCount %} uncles{% if p.Miner.Shares[0].ShareCount > 0 %}, +{%dul p.Miner.Shares[0].ShareCount %} orphans{% endif %}){% else %}No shares reported{% endif %}</td>
<td>{%dul p.Positions.Blocks.Total() %} blocks (+{%dul p.Positions.Uncles.Total() %} uncles)</td>
{% code weightRatio := (float64(p.Weight) / (float64(p.Context().Pool.SideChain.Window.Weight.Mul64(4).Lo) * (float64(p.Context().Pool.SideChain.Consensus.ChainWindowSize) / float64(p.Context().Pool.SideChain.Window.Blocks)))) %}
<td>{%s si_units(weightRatio * float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime)), 3) %}H/s</td>
<td>{%f.3 weightRatio*100 %}%</td>
<td>{%s monero_to_xmr(p.ExpectedRewardPerDay) %} XMR</td>
</tr>
</table>
</div>
<hr/>
<div style="text-align: center;">
<h2>Share positions</h2>
<p class="small">
Shares appear on the right, and get older towards the left.
<br/>
Number denotes the amount of shares per slice, with the plus (<code>+</code>) character being more than 9, and dot (<code>.</code>) being none.
<br/>
Each slice accounts for {%d p.Positions.Resolution %} P2Pool blocks, or around {%dul (uint64(p.Positions.Resolution) * p.Context().Consensus.TargetBlockTime) / 60 %} minutes.
</p>
<h3>Shares and uncles in PPLNS window</h3>
<p class="small">
Each slice accounts for {%d p.Positions.ResolutionWindow %} P2Pool block heights, or around {%dul (uint64(p.Positions.ResolutionWindow) * p.Context().Consensus.TargetBlockTime) / 60 %} minutes.
<br/>
Shares within the PPLNS window will be weighted towards receiving a payout when any Monero block is found by any P2Pool miner.
</p>
<code class="mono">{%s p.Positions.BlocksInWindow.String() %}</code>
<br/>
<code class="mono">{%s p.Positions.UnclesInWindow.String() %}</code>
<h3>Shares and uncles during last day</h3>
<p class="small">
Each slice accounts for {%d p.Positions.Resolution %} P2Pool block heights, or around {%dul (uint64(p.Positions.Resolution) * p.Context().Consensus.TargetBlockTime) / 60 %} minutes.
<br/>
The pipe (<code>|</code>) character denotes roughly the PPLNS window end.
</p>
<code class="mono">{%s p.Positions.Blocks.StringWithSeparator(p.Positions.SeparatorIndex) %}</code>
<br/>
<code class="mono">{%s p.Positions.Uncles.StringWithSeparator(p.Positions.SeparatorIndex) %}</code>
{% if p.Positions.Payouts.Total() > 0 %}
<h3>Payouts during last day</h3>
<code class="mono">{%s p.Positions.Payouts.StringWithSeparator(p.Positions.SeparatorIndex) %}</code>
{% endif %}
</div>
<br/>
<br/>
<hr/>
<div style="text-align: center">
<h2>Most recent payouts</h2>
{%= TemplatePayoutsSlice(p.Context(), p.LastPayouts) %}
<div class="center"><a href="/payouts/{%z= p.Miner.Address.ToBase58() %}">[show all historical payouts]</a></div>
</div>
<hr/>
<div style="text-align: center">
<h2>Most recent shares</h2>
{% if p.HashrateSubmit || (p.Positions.Blocks.Total() + p.Positions.Uncles.Total()) > 2 %}
{%= TemplateShares(p.Context(), p.LastShares, true, &p.LastSharesEfforts) %}
{% else %}
{%= TemplateShares(p.Context(), p.LastShares, true, nil) %}
{% endif %}
</div>
<hr/>
{% if len(p.LastOrphanedShares) > 0 %}
<div style="text-align: center">
<h2>Most recent orphaned shares</h2>
{%= TemplateShares(p.Context(), p.LastOrphanedShares, true, nil) %}
</div>
<hr/>
{% endif %}
<div style="text-align: center">
<h2>Most recent Monero blocks found</h2>
{%= TemplateFoundBlocks(p.Context(), p.LastFound, true) %}
<div class="center"><a href="/blocks?miner={%z= p.Miner.Address.ToBase58() %}">[show more found blocks]</a></div>
</div>
<hr/>
<div style="text-align: center">
<h2>Most recent likely sweeps</h2>
{%= TemplateSweeps(p.Context(), p.LastSweeps, true) %}
<div class="center"><a href="/sweeps?miner={%z= p.Miner.Address.ToBase58() %}">[show more likely sweeps]</a></div>
</div>
{% if p.Miner.LastShareTimestamp != 0 %}
<hr/>
<div style="text-align: center">
<form action="/calculate-share-time" method="get">
<h3>Effort Calculation</h3>
<p>Local hashrate of each P2Pool miner is not known by the network. A guess is calculated based on daily estimation. If you provide a value here, it will be more accurate for effort calculation.</p>
<p>This data will not be saved.</p>
<label for="hashrate_local">Your Local Hashrate</label><br/>
<input type="numeric" name="hashrate" id="hashrate_local" placeholder="100" size="8" class="mono" value="{% if p.HashrateLocal > 0 %}{%s str(p.HashrateLocal) %}{% endif %}"/>
<select name="magnitude">
<option value="1"{% if p.MagnitudeLocal == 1 %} selected{% endif %}>H/s</option>
<option value="1000"{% if p.MagnitudeLocal == 1000 %} selected{% endif %}>KH/s</option>
<option value="1000000"{% if p.MagnitudeLocal == 1000000 %} selected{% endif %}>MH/s</option>
<option value="1000000000"{% if p.MagnitudeLocal == 1000000000 %} selected{% endif %}>GH/s</option>
</select>
<br/>
<input type="submit" value="Calculate" style="width: 20em; margin: 20px;"/>
</form>
</div>
{% endif %}
{% endfunc %}

View file

@ -1,100 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address" %}
{% import cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils" %}
{% import types "git.gammaspectra.live/P2Pool/p2pool-observer/types" %}
{% import p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types" %}
{% code
type MinersPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Refresh int
Weekly bool
Miners []*MinersPageMinerEntry
WindowWeight types.Difficulty
}
%}
{% code
type MinersPageMinerEntry struct {
Address *address.Address
Alias string
SoftwareId p2pooltypes.SoftwareId
SoftwareVersion p2pooltypes.SoftwareVersion
Weight types.Difficulty
Shares *cmdutils.PositionChart
Uncles *cmdutils.PositionChart
}
%}
{% func (p *MinersPage) Title() %}
{% if p.Weekly %}
{%= p.BasePage.Title() %} - Current Window Miners
{% else %}
{%= p.BasePage.Title() %} - Weekly Miners
{% endif %}
{% endfunc %}
{% func (p *MinersPage) Content() %}
{% if p.Weekly %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/miners?week">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/blocks?week&refresh">Autorefresh is OFF</a>
{% endif %}
</div>
{% else %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/miners">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/blocks?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
{% endif %}
<div style="text-align: center">
{% if p.Weekly %}
<h2>Weekly Miners</h2>
<p>This is a list of the miners that have shares in the last 28 full windows, or about seven days.</p>
<p class="small">Entries are sorted by current window "weight". There are more total miners currently active, but without a share to show at the moment.</p>
<p class="small">Pool share % is relative to whole pool hashrate. Miners can join or leave anytime and there is no ceiling limit. </p>
<table class="center datatable" style="border-collapse: collapse; max-width: calc(4em + 12em + 10em + 8em + 8em + 12em + 32em)">
{% else %}
<h2>Current Window Miners</h2>
<p>This is a list of the miners that have shares in the current window, and would be rewarded when a Monero block is found within the window.</p>
<p class="small">Entries are sorted by current window "weight". There are more total miners currently active, but without a share to show at the moment.</p>
<p class="small">Pool share % is relative to whole pool hashrate. Miners can join or leave anytime and there is no ceiling limit. </p>
<table class="center datatable" style="border-collapse: collapse; max-width: calc(4em + 12em + 10em + 8em + 8em + 12em + 24em)">
{% endif %}
<tr>
<th style="width: 4em;">#</th>
<th style="width: 12em;">Miner</th>
<th style="width: 10em;">Software</th>
<th style="width: 8em;">Pool share %</th>
<th style="width: 8em;">Estimated Hashrate</th>
<th style="width: 12em;">Shares found</th>
<th style="width: {% if p.Weekly %}32{% else %}24{% endif %}em;">Shares position</th>
</tr>
{% for i, m := range p.Miners %}
<tr style="padding-bottom: 10px; border-bottom: solid #aaa 1px">
<td style="vertical-align: middle">{%d i+1 %}</td>
{%= TemplateRowMinerWithTag(p.Context(), m.Address, m.Alias, "th") %}
<td style="vertical-align: middle">{%= software_info(m.SoftwareId, m.SoftwareVersion) %}</td>
{% code minerRatio := float64(m.Weight.Lo) / float64(p.WindowWeight.Lo) %}
<td style="vertical-align: middle">{%f.3 minerRatio*100 %}%</td>
<td style="vertical-align: middle">{%s si_units(minerRatio * (float64(p.Context().Pool.SideChain.LastBlock.Difficulty) / float64(p.Context().Consensus.TargetBlockTime)), 3) %}H/s</td>
<td style="vertical-align: middle">{%dul m.Shares.Total() %} block(s) +{%dul m.Uncles.Total() %} uncle(s)</td>
<td>
<code class="mono small">{%s m.Shares.String() %}</code>
<br/>
<code class="mono small">{%s m.Uncles.String() %}</code>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endfunc %}

View file

@ -1,40 +0,0 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address" %}
{% code
type PayoutsPage struct {
// inherit from base page, so its' title is used in error page.
BasePage
Refresh int
Miner *address.Address
Payouts <-chan *index.Payout
}
%}
{% func (p *PayoutsPage) Title() %}
{%= p.BasePage.Title() %} - Historical miner payouts {%z= p.Miner.ToBase58() %}
{% endfunc %}
{% func (p *PayoutsPage) Content() %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/payouts/{%z= p.Miner.ToBase58() %}">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/payouts/{%z= p.Miner.ToBase58() %}?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
<div style="text-align: center">
<h2>Historical miner payouts</h2>
<p><strong>Payout Address:</strong> <a href="/miner/{%z= p.Miner.ToBase58() %}"><span class="mono small">{%z= p.Miner.ToBase58() %}</span></a></p>
{% code var total uint64 %}
{%= TemplatePayouts(p.Context(), p.Payouts, &total) %}
<p><strong>Estimated total:</strong> {%s monero_to_xmr(total) %} XMR</p>
</div>
{% endfunc %}

Some files were not shown because too many files have changed in this diff Show more