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