Implement Miner Options page, add webhook notifications
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
6fee8b6d62
commit
e2885687b2
158
cmd/api/api.go
158
cmd/api/api.go
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
@ -639,6 +640,47 @@ func main() {
|
|||
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/api/miner_webhooks/{miner:4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+}", func(writer http.ResponseWriter, request *http.Request) {
|
||||
minerId := mux.Vars(request)["miner"]
|
||||
miner := indexDb.GetMinerByStringAddress(minerId)
|
||||
|
||||
if miner == nil {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "miner_not_found",
|
||||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
wh, _ := indexDb.GetMinerWebHooks(miner.Id())
|
||||
defer wh.Close()
|
||||
_ = httputils.StreamJsonIterator(request, writer, func() (int, *index.MinerWebHook) {
|
||||
i, w := wh.Next()
|
||||
if w == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
newHook := &index.MinerWebHook{
|
||||
Miner: w.Miner,
|
||||
Type: w.Type,
|
||||
}
|
||||
// Sanitize private data by hashing
|
||||
newHook.Url = types.Hash(sha256.Sum256([]byte(w.Url))).String()
|
||||
settings := make(map[string]string)
|
||||
for k, v := range w.Settings {
|
||||
if strings.HasPrefix("send_", k) {
|
||||
settings[k] = v
|
||||
}
|
||||
}
|
||||
newHook.Settings = settings
|
||||
return i, newHook
|
||||
})
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/api/miner_signed_action/{miner:4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+}", func(writer http.ResponseWriter, request *http.Request) {
|
||||
minerId := mux.Vars(request)["miner"]
|
||||
miner := indexDb.GetMinerByStringAddress(minerId)
|
||||
|
@ -682,21 +724,8 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
result := signedAction.Verify(miner.Address(), sig)
|
||||
result := signedAction.VerifyFallbackToZero(os.Getenv("NET_SERVICE_ADDRESS"), miner.Address(), sig)
|
||||
if result == address.ResultFail {
|
||||
// check again with zero keys
|
||||
zeroResult := signedAction.Verify(&address.ZeroPrivateKeyAddress, sig)
|
||||
if zeroResult == address.ResultSuccessSpend || zeroResult == address.ResultSuccessView {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusUnauthorized)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "signature_verify_fail_zero_private_key",
|
||||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusUnauthorized)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
|
@ -706,6 +735,26 @@ func main() {
|
|||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
} else if result == address.ResultFailZeroSpend || result == address.ResultFailZeroView {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusUnauthorized)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "signature_verify_fail_zero_private_key",
|
||||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
} else if result != address.ResultSuccessSpend && result != address.ResultSuccessView {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusUnauthorized)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "signature_verify_fail_unknown",
|
||||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
switch signedAction.Action {
|
||||
|
@ -797,6 +846,85 @@ func main() {
|
|||
writer.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
case "add_webhook":
|
||||
whType, _ := signedAction.Get("type")
|
||||
whUrl, _ := signedAction.Get("url")
|
||||
wh := &index.MinerWebHook{
|
||||
Miner: miner.Id(),
|
||||
Type: index.WebHookType(whType),
|
||||
Url: whUrl,
|
||||
Settings: make(map[string]string),
|
||||
}
|
||||
for _, e := range signedAction.Data {
|
||||
if strings.HasPrefix(e.Key, "send_") {
|
||||
wh.Settings[e.Key] = e.Value
|
||||
}
|
||||
}
|
||||
|
||||
err := wh.Verify()
|
||||
if err != nil {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: err.Error(),
|
||||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
}
|
||||
err = indexDb.InsertOrUpdateMinerWebHook(wh)
|
||||
if err != nil {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: err.Error(),
|
||||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
return
|
||||
case "remove_webhook":
|
||||
urlHash, ok := signedAction.Get("url_hash")
|
||||
urlType, ok2 := signedAction.Get("type")
|
||||
if !ok || !ok2 || urlHash == "" {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "invalid_url",
|
||||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
it, err := indexDb.GetMinerWebHooks(miner.Id())
|
||||
if err != nil {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
buf, _ := utils.MarshalJSON(struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "internal_error",
|
||||
})
|
||||
_, _ = writer.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
it.All(func(key int, value *index.MinerWebHook) (stop bool) {
|
||||
if string(value.Type) == urlType && types.Hash(sha256.Sum256([]byte(value.Url))).String() == urlHash {
|
||||
_ = indexDb.DeleteMinerWebHook(miner.Id(), value.Type)
|
||||
}
|
||||
return false
|
||||
})
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
return
|
||||
default:
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
|
@ -1791,7 +1919,7 @@ func main() {
|
|||
Miners uint64 `json:"miners"`
|
||||
}
|
||||
|
||||
serveMux.HandleFunc("/api/stats/{kind:difficulty|miner_seen|miner_seen_window$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||
serveMux.HandleFunc("/api/stats/{kind:difficulty|miner_seen|miner_seen_window|version_seen$}", func(writer http.ResponseWriter, request *http.Request) {
|
||||
switch mux.Vars(request)["kind"] {
|
||||
case "difficulty":
|
||||
result := make(chan *difficultyStatResult)
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"fmt"
|
||||
"git.gammaspectra.live/P2Pool/go-monero/pkg/rpc/daemon"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
|
||||
utils2 "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
|
||||
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
|
||||
|
@ -37,10 +37,15 @@ func main() {
|
|||
p2poolApiHost := flag.String("api-host", "", "Host URL for p2pool go observer consensus")
|
||||
fullMode := flag.Bool("full-mode", false, "Allocate RandomX dataset, uses 2GB of RAM")
|
||||
debugListen := flag.String("debug-listen", "", "Provide a bind address and port to expose a pprof HTTP API on it.")
|
||||
hookProxy := flag.String("hook-proxy", "", "socks5 proxy host:port for webhook requests")
|
||||
flag.Parse()
|
||||
|
||||
client.SetDefaultClientSettings(fmt.Sprintf("http://%s:%d", *moneroHost, *moneroRpcPort))
|
||||
|
||||
if *hookProxy != "" {
|
||||
cmdutils.SetWebHookProxy(*hookProxy)
|
||||
}
|
||||
|
||||
p2api := p2poolapi.NewP2PoolApi(*p2poolApiHost)
|
||||
|
||||
if err := p2api.WaitSync(); err != nil {
|
||||
|
@ -164,7 +169,7 @@ func main() {
|
|||
ctx := context.Background()
|
||||
|
||||
scanHeader := func(h daemon.BlockHeader) error {
|
||||
if err := utils2.FindAndInsertMainHeader(h, indexDb, func(b *sidechain.PoolBlock) {
|
||||
if err := cmdutils.FindAndInsertMainHeader(h, indexDb, func(b *sidechain.PoolBlock) {
|
||||
p2api.InsertAlternate(b)
|
||||
}, client.GetDefaultClient(), indexDb.GetDifficultyByHeight, indexDb.GetByTemplateId, p2api.ByMainId, p2api.LightByMainHeight, func(b *sidechain.PoolBlock) error {
|
||||
_, err := b.PreProcessBlock(p2api.Consensus(), &sidechain.NilDerivationCache{}, sidechain.PreAllocateShares(p2api.Consensus().ChainWindowSize*2), indexDb.GetDifficultyByHeight, indexDb.GetByTemplateId)
|
||||
|
@ -275,7 +280,7 @@ func main() {
|
|||
continue
|
||||
}
|
||||
|
||||
if err := utils2.ProcessFullBlock(b, indexDb); err != nil {
|
||||
if err := cmdutils.ProcessFullBlock(b, indexDb); err != nil {
|
||||
log.Printf("error processing block %s at %d: %s", b.Id, b.Height, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
|
||||
utils2 "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
|
||||
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api"
|
||||
types2 "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
|
||||
|
@ -325,8 +325,8 @@ func setupEventHandler(p2api *api.P2PoolApi, indexDb *index.Index) {
|
|||
listenerLock.RLock()
|
||||
defer listenerLock.RUnlock()
|
||||
for _, b := range blocksToReport {
|
||||
buf, err := utils.MarshalJSON(&utils2.JSONEvent{
|
||||
Type: utils2.JSONEventSideBlock,
|
||||
buf, err := utils.MarshalJSON(&cmdutils.JSONEvent{
|
||||
Type: cmdutils.JSONEventSideBlock,
|
||||
SideBlock: b,
|
||||
FoundBlock: nil,
|
||||
MainCoinbaseOutputs: nil,
|
||||
|
@ -341,6 +341,19 @@ func setupEventHandler(p2api *api.P2PoolApi, indexDb *index.Index) {
|
|||
l.Write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
ts := time.Now().Unix()
|
||||
|
||||
// Send webhooks
|
||||
go func(b *index.SideBlock) {
|
||||
q, _ := indexDb.GetMinerWebHooks(b.Miner)
|
||||
index.QueryIterate(q, func(_ int, w *index.MinerWebHook) (stop bool) {
|
||||
if err := cmdutils.SendSideBlock(w, ts, b.MinerAddress, b); err != nil {
|
||||
log.Printf("[WebHook] Error sending %s webhook to %s: type %s, url %s: %s", cmdutils.JSONEventSideBlock, b.MinerAddress.ToBase58(), w.Type, w.Url, err)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}(b)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -426,8 +439,8 @@ func setupEventHandler(p2api *api.P2PoolApi, indexDb *index.Index) {
|
|||
listenerLock.RLock()
|
||||
defer listenerLock.RUnlock()
|
||||
for _, b := range unfoundBlocksToReport {
|
||||
buf, err := utils.MarshalJSON(&utils2.JSONEvent{
|
||||
Type: utils2.JSONEventOrphanedBlock,
|
||||
buf, err := utils.MarshalJSON(&cmdutils.JSONEvent{
|
||||
Type: cmdutils.JSONEventOrphanedBlock,
|
||||
SideBlock: b,
|
||||
FoundBlock: nil,
|
||||
MainCoinbaseOutputs: nil,
|
||||
|
@ -442,6 +455,19 @@ func setupEventHandler(p2api *api.P2PoolApi, indexDb *index.Index) {
|
|||
l.Write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
ts := time.Now().Unix()
|
||||
|
||||
// Send webhooks
|
||||
go func(b *index.SideBlock) {
|
||||
q, _ := indexDb.GetMinerWebHooks(b.Miner)
|
||||
index.QueryIterate(q, func(_ int, w *index.MinerWebHook) (stop bool) {
|
||||
if err := cmdutils.SendOrphanedBlock(w, ts, b.MinerAddress, b); err != nil {
|
||||
log.Printf("[WebHook] Error sending %s webhook to %s: type %s, url %s: %s", cmdutils.JSONEventOrphanedBlock, b.MinerAddress.ToBase58(), w.Type, w.Url, err)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}(b)
|
||||
}
|
||||
for _, b := range blocksToReport {
|
||||
coinbaseOutputs := func() index.MainCoinbaseOutputs {
|
||||
|
@ -458,8 +484,8 @@ func setupEventHandler(p2api *api.P2PoolApi, indexDb *index.Index) {
|
|||
foundBlockBuffer.Remove(b)
|
||||
continue
|
||||
}
|
||||
buf, err := utils.MarshalJSON(&utils2.JSONEvent{
|
||||
Type: utils2.JSONEventFoundBlock,
|
||||
buf, err := utils.MarshalJSON(&cmdutils.JSONEvent{
|
||||
Type: cmdutils.JSONEventFoundBlock,
|
||||
SideBlock: nil,
|
||||
FoundBlock: b,
|
||||
MainCoinbaseOutputs: coinbaseOutputs,
|
||||
|
@ -474,6 +500,51 @@ func setupEventHandler(p2api *api.P2PoolApi, indexDb *index.Index) {
|
|||
l.Write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
includingHeight := max(b.EffectiveHeight, b.SideHeight)
|
||||
if uint64(b.WindowDepth) > includingHeight {
|
||||
includingHeight = 0
|
||||
} else {
|
||||
includingHeight -= uint64(b.WindowDepth)
|
||||
}
|
||||
|
||||
// Send webhooks on outputs
|
||||
for _, o := range coinbaseOutputs {
|
||||
payout := &index.Payout{
|
||||
Miner: o.Miner,
|
||||
TemplateId: b.MainBlock.SideTemplateId,
|
||||
SideHeight: b.SideHeight,
|
||||
UncleOf: b.UncleOf,
|
||||
MainId: b.MainBlock.Id,
|
||||
MainHeight: b.MainBlock.Height,
|
||||
Timestamp: b.MainBlock.Timestamp,
|
||||
CoinbaseId: b.MainBlock.CoinbaseId,
|
||||
Reward: o.Value,
|
||||
PrivateKey: b.MainBlock.CoinbasePrivateKey,
|
||||
Index: uint64(o.Index),
|
||||
GlobalOutputIndex: o.GlobalOutputIndex,
|
||||
IncludingHeight: includingHeight,
|
||||
}
|
||||
|
||||
addr := o.MinerAddress
|
||||
|
||||
ts := time.Now().Unix()
|
||||
|
||||
// One goroutine per entry
|
||||
go func() {
|
||||
q, _ := indexDb.GetMinerWebHooks(payout.Miner)
|
||||
index.QueryIterate(q, func(_ int, w *index.MinerWebHook) (stop bool) {
|
||||
if err := cmdutils.SendFoundBlock(w, ts, addr, b, coinbaseOutputs); err != nil {
|
||||
log.Printf("[WebHook] Error sending %s webhook to %s: type %s, url %s: %s", cmdutils.JSONEventFoundBlock, addr.ToBase58(), w.Type, w.Url, err)
|
||||
}
|
||||
|
||||
if err := cmdutils.SendPayout(w, ts, addr, payout); err != nil {
|
||||
log.Printf("[WebHook] Error sending %s webhook to %s: type %s, url %s: %s", cmdutils.JSONEventPayout, addr.ToBase58(), w.Type, w.Url, err)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package index
|
|||
|
||||
import (
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||
)
|
||||
|
||||
|
@ -24,7 +25,7 @@ type FoundBlock struct {
|
|||
MinerAlias string `json:"miner_alias,omitempty"`
|
||||
}
|
||||
|
||||
func (b *FoundBlock) ScanFromRow(i *Index, row RowScanInterface) error {
|
||||
func (b *FoundBlock) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
|
||||
if err := row.Scan(
|
||||
&b.MainBlock.Id, &b.MainBlock.Height, &b.MainBlock.Timestamp, &b.MainBlock.Reward, &b.MainBlock.CoinbaseId, &b.MainBlock.CoinbasePrivateKey, &b.MainBlock.Difficulty, &b.MainBlock.SideTemplateId,
|
||||
&b.SideHeight, &b.Miner, &b.UncleOf, &b.EffectiveHeight, &b.WindowDepth, &b.WindowOutputs, &b.TransactionCount, &b.Difficulty, &b.CumulativeDifficulty, &b.Inclusion,
|
||||
|
|
|
@ -1112,7 +1112,7 @@ func (i *Index) QueryGlobalOutputIndices(indices []uint64) []*MatchedOutput {
|
|||
|
||||
if err := i.Query("SELECT "+MainCoinbaseOutputSelectFields+" FROM main_coinbase_outputs WHERE global_output_index = ANY($1) ORDER BY index ASC;", func(row RowScanInterface) error {
|
||||
var o MainCoinbaseOutput
|
||||
if err := o.ScanFromRow(i, row); err != nil {
|
||||
if err := o.ScanFromRow(i.Consensus(), row); err != nil {
|
||||
return err
|
||||
}
|
||||
if index := slices.Index(indices, o.GlobalOutputIndex); index != -1 {
|
||||
|
@ -1133,7 +1133,7 @@ func (i *Index) QueryGlobalOutputIndices(indices []uint64) []*MatchedOutput {
|
|||
|
||||
if err := i.Query("SELECT "+MainLikelySweepTransactionSelectFields+" FROM main_likely_sweep_transactions WHERE $1::bigint[] && global_output_indices ORDER BY timestamp ASC;", func(row RowScanInterface) error {
|
||||
var tx MainLikelySweepTransaction
|
||||
if err := tx.ScanFromRow(i, row); err != nil {
|
||||
if err := tx.ScanFromRow(i.Consensus(), row); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, globalOutputIndex := range tx.GlobalOutputIndices {
|
||||
|
@ -1161,7 +1161,7 @@ func (i *Index) GetMainLikelySweepTransactionBySpendingGlobalOutputIndices(globa
|
|||
entries := make([][]*MainLikelySweepTransaction, len(globalOutputIndices))
|
||||
if err := i.Query("SELECT "+MainLikelySweepTransactionSelectFields+" FROM main_likely_sweep_transactions WHERE $1::bigint[] && spending_output_indices ORDER BY timestamp ASC;", func(row RowScanInterface) error {
|
||||
var tx MainLikelySweepTransaction
|
||||
if err := tx.ScanFromRow(i, row); err != nil {
|
||||
if err := tx.ScanFromRow(i.Consensus(), row); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, globalOutputIndex := range tx.SpendingOutputIndices {
|
||||
|
@ -1181,7 +1181,7 @@ func (i *Index) GetMainLikelySweepTransactionByGlobalOutputIndices(globalOutputI
|
|||
entries := make([]*MainLikelySweepTransaction, len(globalOutputIndices))
|
||||
if err := i.Query("SELECT "+MainLikelySweepTransactionSelectFields+" FROM main_likely_sweep_transactions WHERE $1::bigint[] && global_output_indices ORDER BY timestamp ASC;", func(row RowScanInterface) error {
|
||||
var tx MainLikelySweepTransaction
|
||||
if err := tx.ScanFromRow(i, row); err != nil {
|
||||
if err := tx.ScanFromRow(i.Consensus(), row); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, globalOutputIndex := range tx.GlobalOutputIndices {
|
||||
|
@ -1234,7 +1234,7 @@ func (i *Index) InsertOrUpdateMainLikelySweepTransaction(t *MainLikelySweepTrans
|
|||
func (i *Index) GetMainCoinbaseOutputByMinerId(coinbaseId types.Hash, minerId uint64) *MainCoinbaseOutput {
|
||||
var output MainCoinbaseOutput
|
||||
if err := i.Query("SELECT "+MainCoinbaseOutputSelectFields+" FROM main_coinbase_outputs WHERE id = $1 AND miner = $2 ORDER BY index DESC;", func(row RowScanInterface) error {
|
||||
if err := output.ScanFromRow(i, row); err != nil {
|
||||
if err := output.ScanFromRow(i.Consensus(), row); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -1293,7 +1293,7 @@ func (i *Index) Close() error {
|
|||
func (i *Index) scanMiner(rows *sql.Rows) *Miner {
|
||||
if rows.Next() {
|
||||
m := &Miner{}
|
||||
if m.ScanFromRow(i, rows) == nil {
|
||||
if m.ScanFromRow(i.Consensus(), rows) == nil {
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
@ -1400,3 +1400,50 @@ func (i *Index) InsertOrUpdatePoolBlock(b *sidechain.PoolBlock, inclusion BlockI
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Index) GetMinerWebHooks(minerId uint64) (QueryIterator[MinerWebHook], error) {
|
||||
if stmt, err := i.handle.Prepare("SELECT miner, type, url, settings FROM miner_webhooks WHERE miner = $1;"); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if r, err := queryStatement[MinerWebHook](i, stmt, minerId); err != nil {
|
||||
defer stmt.Close()
|
||||
return nil, err
|
||||
} else {
|
||||
r.closer = func() {
|
||||
stmt.Close()
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Index) DeleteMinerWebHook(minerId uint64, hookType WebHookType) error {
|
||||
return i.Query("DELETE FROM miner_webhooks WHERE miner = $1 AND type = $2", func(row RowScanInterface) error {
|
||||
return nil
|
||||
}, minerId, hookType)
|
||||
}
|
||||
|
||||
func (i *Index) InsertOrUpdateMinerWebHook(w *MinerWebHook) error {
|
||||
metadataJson, _ := utils.MarshalJSON(w.Settings)
|
||||
|
||||
if w.Settings == nil {
|
||||
metadataJson = []byte{'{', '}'}
|
||||
}
|
||||
|
||||
if tx, err := i.handle.BeginTx(context.Background(), nil); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(
|
||||
"INSERT INTO miner_webhooks (miner, type, url, settings) VALUES ($1, $2, $3, $4::jsonb) ON CONFLICT (miner, type) DO UPDATE SET url = $3, settings = $4;",
|
||||
w.Miner,
|
||||
w.Type,
|
||||
w.Url,
|
||||
metadataJson,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package index
|
|||
|
||||
import (
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
|
||||
)
|
||||
|
@ -34,7 +35,7 @@ func (b *MainBlock) SetMetadata(key string, v any) {
|
|||
b.Metadata[key] = v
|
||||
}
|
||||
|
||||
func (b *MainBlock) ScanFromRow(i *Index, row RowScanInterface) error {
|
||||
func (b *MainBlock) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
|
||||
var metadataBuf []byte
|
||||
b.Metadata = make(map[string]any)
|
||||
if err := row.Scan(&b.Id, &b.Height, &b.Timestamp, &b.Reward, &b.CoinbaseId, &b.Difficulty, &metadataBuf, &b.SideTemplateId, &b.CoinbasePrivateKey); err != nil {
|
||||
|
|
|
@ -2,6 +2,7 @@ package index
|
|||
|
||||
import (
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||
)
|
||||
|
||||
|
@ -28,7 +29,7 @@ type MainCoinbaseOutput struct {
|
|||
MinerAlias string `json:"miner_alias,omitempty"`
|
||||
}
|
||||
|
||||
func (o *MainCoinbaseOutput) ScanFromRow(i *Index, row RowScanInterface) error {
|
||||
func (o *MainCoinbaseOutput) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
|
||||
if err := row.Scan(&o.Id, &o.Index, &o.GlobalOutputIndex, &o.Miner, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
|
||||
"github.com/lib/pq"
|
||||
|
@ -35,7 +36,7 @@ type MainLikelySweepTransaction struct {
|
|||
Address *address.Address `json:"address"`
|
||||
}
|
||||
|
||||
func (t *MainLikelySweepTransaction) ScanFromRow(i *Index, row RowScanInterface) error {
|
||||
func (t *MainLikelySweepTransaction) ScanFromRow(consensus *sidechain.Consensus, row RowScanInterface) error {
|
||||
var spendPub, viewPub crypto.PublicKeyBytes
|
||||
var resultBuf, matchBuf []byte
|
||||
var spendingOutputIndices, globalOutputIndices pq.Int64Array
|
||||
|
@ -55,7 +56,7 @@ func (t *MainLikelySweepTransaction) ScanFromRow(i *Index, row RowScanInterface)
|
|||
t.GlobalOutputIndices[j] = uint64(ix)
|
||||
}
|
||||
|
||||
network, err := i.consensus.NetworkType.AddressNetwork()
|
||||
network, err := consensus.NetworkType.AddressNetwork()
|
||||
if err != nil {
|
||||
return errors.New("unknown network type")
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"database/sql"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
||||
)
|
||||
|
||||
const MinerSelectFields = "id, alias, spend_public_key, view_public_key"
|
||||
|
@ -29,13 +30,13 @@ func (m *Miner) Address() *address.Address {
|
|||
return &m.addr
|
||||
}
|
||||
|
||||
func (m *Miner) ScanFromRow(i *Index, row RowScanInterface) error {
|
||||
func (m *Miner) ScanFromRow(consensus *sidechain.Consensus, row RowScanInterface) error {
|
||||
var spendPub, viewPub crypto.PublicKeyBytes
|
||||
if err := row.Scan(&m.id, &m.alias, &spendPub, &viewPub); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
network, err := i.consensus.NetworkType.AddressNetwork()
|
||||
network, err := consensus.NetworkType.AddressNetwork()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
124
cmd/index/miner_webhook.go
Normal file
124
cmd/index/miner_webhook.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package index
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MinerWebHook struct {
|
||||
Miner uint64 `json:"miner"`
|
||||
Type WebHookType `json:"type"`
|
||||
Url string `json:"url"`
|
||||
Settings map[string]string `json:"settings"`
|
||||
Consensus *sidechain.Consensus `json:"-"`
|
||||
}
|
||||
|
||||
type WebHookType string
|
||||
|
||||
const (
|
||||
WebHookSlack WebHookType = "slack"
|
||||
WebHookDiscord WebHookType = "discord"
|
||||
WebHookTelegram WebHookType = "telegram"
|
||||
WebHookMatrixHookshot WebHookType = "matrix-hookshot"
|
||||
WebHookCustom WebHookType = "custom"
|
||||
)
|
||||
|
||||
var disallowedCustomHookHosts = []string{
|
||||
"hooks.slack.com",
|
||||
"discord.com",
|
||||
"api.telegram.org",
|
||||
}
|
||||
|
||||
var disallowedCustomHookPorts = []string{}
|
||||
|
||||
func (w *MinerWebHook) ScanFromRow(consensus *sidechain.Consensus, row RowScanInterface) error {
|
||||
var settingsBuf []byte
|
||||
|
||||
w.Consensus = consensus
|
||||
w.Settings = make(map[string]string)
|
||||
|
||||
if err := row.Scan(&w.Miner, &w.Type, &w.Url, &settingsBuf); err != nil {
|
||||
return err
|
||||
} else if err = utils.UnmarshalJSON(settingsBuf, &w.Settings); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *MinerWebHook) Verify() error {
|
||||
uri, err := url.Parse(w.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch w.Type {
|
||||
case WebHookSlack:
|
||||
if uri.Scheme != "https" {
|
||||
return errors.New("invalid URL scheme, expected https")
|
||||
}
|
||||
if uri.Host != "hooks.slack.com" {
|
||||
return errors.New("invalid hook host, expected hooks.slack.com")
|
||||
}
|
||||
if uri.Port() != "" {
|
||||
return errors.New("unexpected port")
|
||||
}
|
||||
if !strings.HasPrefix(uri.Path, "/services/") {
|
||||
return errors.New("invalid hook path start")
|
||||
}
|
||||
case WebHookDiscord:
|
||||
if uri.Scheme != "https" {
|
||||
return errors.New("invalid URL scheme, expected https")
|
||||
}
|
||||
if uri.Host != "discord.com" {
|
||||
return errors.New("invalid hook host, expected discord.com")
|
||||
}
|
||||
if uri.Port() != "" {
|
||||
return errors.New("unexpected port")
|
||||
}
|
||||
if !strings.HasPrefix(uri.Path, "/api/webhooks/") {
|
||||
return errors.New("invalid hook path start")
|
||||
}
|
||||
case WebHookTelegram:
|
||||
if uri.Scheme != "https" {
|
||||
return errors.New("invalid URL scheme, expected https")
|
||||
}
|
||||
if uri.Host != "api.telegram.org" {
|
||||
return errors.New("invalid hook host, expected api.telegram.org")
|
||||
}
|
||||
if uri.Port() != "" {
|
||||
return errors.New("unexpected port")
|
||||
}
|
||||
if !strings.HasPrefix(uri.Path, "/bot") {
|
||||
return errors.New("invalid hook path start")
|
||||
}
|
||||
if !strings.HasSuffix(uri.Path, "/sendMessage") {
|
||||
return errors.New("invalid hook path end")
|
||||
}
|
||||
case WebHookMatrixHookshot:
|
||||
if uri.Scheme != "https" {
|
||||
return errors.New("invalid URL scheme, expected https")
|
||||
}
|
||||
if slices.Contains(disallowedCustomHookHosts, strings.ToLower(uri.Hostname())) {
|
||||
return errors.New("disallowed hook host")
|
||||
}
|
||||
if uri.Port() != "" {
|
||||
return errors.New("unexpected port")
|
||||
}
|
||||
case WebHookCustom:
|
||||
if uri.Scheme != "https" && uri.Scheme != "http" {
|
||||
return errors.New("invalid URL scheme, expected http or https")
|
||||
}
|
||||
if slices.Contains(disallowedCustomHookHosts, strings.ToLower(uri.Hostname())) {
|
||||
return errors.New("disallowed hook host")
|
||||
}
|
||||
if slices.Contains(disallowedCustomHookPorts, strings.ToLower(uri.Port())) {
|
||||
return errors.New("disallowed hook port")
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported hook type")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -2,6 +2,7 @@ package index
|
|||
|
||||
import (
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||
)
|
||||
|
||||
|
@ -21,7 +22,7 @@ type Payout struct {
|
|||
IncludingHeight uint64 `json:"including_height"`
|
||||
}
|
||||
|
||||
func (p *Payout) ScanFromRow(i *Index, row RowScanInterface) error {
|
||||
func (p *Payout) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
|
||||
if err := row.Scan(&p.Miner, &p.MainId, &p.MainHeight, &p.Timestamp, &p.CoinbaseId, &p.PrivateKey, &p.TemplateId, &p.SideHeight, &p.UncleOf, &p.Reward, &p.Index, &p.GlobalOutputIndex, &p.IncludingHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package index
|
|||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
|
||||
)
|
||||
|
||||
type RowScanInterface interface {
|
||||
|
@ -10,7 +11,7 @@ type RowScanInterface interface {
|
|||
}
|
||||
|
||||
type Scannable interface {
|
||||
ScanFromRow(i *Index, row RowScanInterface) error
|
||||
ScanFromRow(consensus *sidechain.Consensus, row RowScanInterface) error
|
||||
}
|
||||
|
||||
type QueryIterator[V any] interface {
|
||||
|
@ -21,6 +22,9 @@ type QueryIterator[V any] interface {
|
|||
}
|
||||
|
||||
func QueryIterate[V any](i QueryIterator[V], f IterateFunction[int, *V]) {
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
defer i.Close()
|
||||
i.All(f)
|
||||
|
||||
|
@ -86,8 +90,8 @@ func queryStatement[V any](index *Index, stmt *sql.Stmt, params ...any) (*QueryR
|
|||
return nil, err
|
||||
} else {
|
||||
return &QueryResult[V]{
|
||||
index: index,
|
||||
rows: rows,
|
||||
consensus: index.consensus,
|
||||
rows: rows,
|
||||
}, err
|
||||
}
|
||||
}
|
||||
|
@ -122,11 +126,11 @@ func (r *FakeQueryResult[V]) Close() {
|
|||
}
|
||||
|
||||
type QueryResult[V any] struct {
|
||||
index *Index
|
||||
rows *sql.Rows
|
||||
closer func()
|
||||
i int
|
||||
err error
|
||||
consensus *sidechain.Consensus
|
||||
rows *sql.Rows
|
||||
closer func()
|
||||
i int
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *QueryResult[V]) All(f IterateFunction[int, *V]) (complete bool) {
|
||||
|
@ -145,7 +149,7 @@ func (r *QueryResult[V]) All(f IterateFunction[int, *V]) (complete bool) {
|
|||
func (r *QueryResult[V]) Next() (int, *V) {
|
||||
if r.rows.Next() {
|
||||
var v V
|
||||
if r.err = any(&v).(Scannable).ScanFromRow(r.index, r.rows); r.err != nil {
|
||||
if r.err = any(&v).(Scannable).ScanFromRow(r.consensus, r.rows); r.err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
r.i++
|
||||
|
|
|
@ -6,6 +6,16 @@ CREATE TABLE IF NOT EXISTS miners (
|
|||
UNIQUE (spend_public_key, view_public_key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS miner_webhooks (
|
||||
miner bigint NOT NULL,
|
||||
type varchar NOT NULL,
|
||||
url varchar NOT NULL,
|
||||
settings jsonb NOT NULL DEFAULT '{}', -- settings to know when to trigger or other required settings for the method
|
||||
UNIQUE (miner, type),
|
||||
FOREIGN KEY (miner) REFERENCES miners (id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS miner_webhooks_miner_idx ON miner_webhooks (miner);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS side_blocks (
|
||||
main_id bytea PRIMARY KEY, -- mainchain id, on Monero network
|
||||
|
|
|
@ -162,7 +162,7 @@ func (b *SideBlock) IsOrphan() bool {
|
|||
return b.Inclusion == InclusionOrphan
|
||||
}
|
||||
|
||||
func (b *SideBlock) ScanFromRow(i *Index, row RowScanInterface) error {
|
||||
func (b *SideBlock) ScanFromRow(_ *sidechain.Consensus, row RowScanInterface) error {
|
||||
if err := row.Scan(&b.MainId, &b.MainHeight, &b.TemplateId, &b.SideHeight, &b.ParentTemplateId, &b.Miner, &b.UncleOf, &b.EffectiveHeight, &b.Nonce, &b.ExtraNonce, &b.Timestamp, &b.SoftwareId, &b.SoftwareVersion, &b.WindowDepth, &b.WindowOutputs, &b.TransactionCount, &b.Difficulty, &b.CumulativeDifficulty, &b.PowDifficulty, &b.PowHash, &b.Inclusion); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ const (
|
|||
JSONEventSideBlock = "side_block"
|
||||
JSONEventFoundBlock = "found_block"
|
||||
JSONEventOrphanedBlock = "orphaned_block"
|
||||
JSONEventPayout = "payout"
|
||||
)
|
||||
|
||||
type JSONEvent struct {
|
||||
|
@ -22,6 +23,7 @@ type JSONEvent struct {
|
|||
SideBlock *index.SideBlock `json:"side_block,omitempty"`
|
||||
FoundBlock *index.FoundBlock `json:"found_block,omitempty"`
|
||||
MainCoinbaseOutputs index.MainCoinbaseOutputs `json:"main_coinbase_outputs,omitempty"`
|
||||
Payout *index.Payout `json:"payout,omitempty"`
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
|
|
|
@ -11,6 +11,7 @@ require (
|
|||
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
|
||||
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0
|
||||
github.com/floatdrop/lru v1.3.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -22,7 +23,6 @@ require (
|
|||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/dolthub/maphash v0.1.0 // indirect
|
||||
github.com/dolthub/swiss v0.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/holiman/uint256 v1.2.3 // indirect
|
||||
github.com/jxskiss/base62 v1.1.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
|
|
|
@ -16,6 +16,7 @@ type SignedActionEntry struct {
|
|||
type SignedAction struct {
|
||||
Action string `json:"action"`
|
||||
Data []SignedActionEntry `json:"data"`
|
||||
Realm string `json:"realm"`
|
||||
}
|
||||
|
||||
func (a *SignedAction) Get(key string) (string, bool) {
|
||||
|
@ -44,12 +45,25 @@ func (a *SignedAction) String() string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
func (a *SignedAction) Verify(addr address.Interface, signature string) address.SignatureVerifyResult {
|
||||
func (a *SignedAction) Verify(realm string, addr address.Interface, signature string) address.SignatureVerifyResult {
|
||||
if a.Realm != realm {
|
||||
// Realm does not match
|
||||
return address.ResultFail
|
||||
}
|
||||
message := a.String()
|
||||
return address.VerifyMessage(addr, []byte(message), signature)
|
||||
}
|
||||
|
||||
func SignedActionSetMinerAlias(alias string) *SignedAction {
|
||||
func (a *SignedAction) VerifyFallbackToZero(realm string, addr address.Interface, signature string) address.SignatureVerifyResult {
|
||||
if a.Realm != realm {
|
||||
// Realm does not match
|
||||
return address.ResultFail
|
||||
}
|
||||
message := a.String()
|
||||
return address.VerifyMessageFallbackToZero(addr, []byte(message), signature)
|
||||
}
|
||||
|
||||
func SignedActionSetMinerAlias(realm, alias string) *SignedAction {
|
||||
return &SignedAction{
|
||||
Action: "set_miner_alias",
|
||||
Data: []SignedActionEntry{
|
||||
|
@ -58,17 +72,19 @@ func SignedActionSetMinerAlias(alias string) *SignedAction {
|
|||
Value: alias,
|
||||
},
|
||||
},
|
||||
Realm: realm,
|
||||
}
|
||||
}
|
||||
|
||||
func SignedActionUnsetMinerAlias() *SignedAction {
|
||||
func SignedActionUnsetMinerAlias(realm string) *SignedAction {
|
||||
return &SignedAction{
|
||||
Action: "unset_miner_alias",
|
||||
Data: make([]SignedActionEntry, 0),
|
||||
Realm: realm,
|
||||
}
|
||||
}
|
||||
|
||||
func SignedActionAddWebHook(webhookType, webhookUrl string, other ...SignedActionEntry) *SignedAction {
|
||||
func SignedActionAddWebHook(realm, webhookType, webhookUrl string, other ...SignedActionEntry) *SignedAction {
|
||||
return &SignedAction{
|
||||
Action: "add_webhook",
|
||||
Data: append([]SignedActionEntry{
|
||||
|
@ -81,10 +97,11 @@ func SignedActionAddWebHook(webhookType, webhookUrl string, other ...SignedActio
|
|||
Value: webhookUrl,
|
||||
},
|
||||
}, other...),
|
||||
Realm: realm,
|
||||
}
|
||||
}
|
||||
|
||||
func SignedActionRemoveWebHook(webhookType, webhookUrl string, other ...SignedActionEntry) *SignedAction {
|
||||
func SignedActionRemoveWebHook(realm, webhookType, webhookUrlHash string, other ...SignedActionEntry) *SignedAction {
|
||||
return &SignedAction{
|
||||
Action: "remove_webhook",
|
||||
Data: append([]SignedActionEntry{
|
||||
|
@ -93,9 +110,10 @@ func SignedActionRemoveWebHook(webhookType, webhookUrl string, other ...SignedAc
|
|||
Value: webhookType,
|
||||
},
|
||||
{
|
||||
Key: "url",
|
||||
Value: webhookUrl,
|
||||
Key: "url_hash",
|
||||
Value: webhookUrlHash,
|
||||
},
|
||||
}, other...),
|
||||
Realm: realm,
|
||||
}
|
||||
}
|
||||
|
|
401
cmd/utils/webhook.go
Normal file
401
cmd/utils/webhook.go
Normal file
|
@ -0,0 +1,401 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JSONWebHookDiscord struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
AvatarUrl string `json:"avatar_url,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Embeds []JSONWebHookDiscordEmbed `json:"embeds,omitempty"`
|
||||
Components []JSONWebHookDiscordComponentRow `json:"components,omitempty"`
|
||||
TTS bool `json:"tts,omitempty"`
|
||||
AllowedMentions *struct {
|
||||
Parse []string `json:"parse,omitempty"`
|
||||
Users []string `json:"users,omitempty"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
} `json:"allowed_mentions,omitempty"`
|
||||
}
|
||||
|
||||
func NewJSONWebHookDiscordComponent(b ...JSONWebHookDiscordComponentButton) JSONWebHookDiscordComponentRow {
|
||||
return JSONWebHookDiscordComponentRow{
|
||||
Type: 1,
|
||||
Components: b,
|
||||
}
|
||||
}
|
||||
|
||||
func NewJSONWebHookDiscordComponentButton(label, url string) JSONWebHookDiscordComponentButton {
|
||||
return JSONWebHookDiscordComponentButton{
|
||||
Type: 2,
|
||||
Style: 5,
|
||||
Label: label,
|
||||
Url: url,
|
||||
}
|
||||
}
|
||||
|
||||
type JSONWebHookDiscordComponentRow struct {
|
||||
Type int `json:"type"`
|
||||
Components []JSONWebHookDiscordComponentButton `json:"components"`
|
||||
}
|
||||
|
||||
type JSONWebHookDiscordComponentButton struct {
|
||||
Type int `json:"type"`
|
||||
Style int `json:"style"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
type JSONWebHookDiscordEmbed struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
// Timestamp Time in time.RFC3339 YYYY-MM-DDTHH:MM:SS.MSSZ format
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
Color *uint64 `json:"color,omitempty"`
|
||||
|
||||
Footer *struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
} `json:"footer,omitempty"`
|
||||
|
||||
Image *struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
} `json:"image,omitempty"`
|
||||
Thumbnail *struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
} `json:"thumbnail,omitempty"`
|
||||
|
||||
Provider *struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
IconUrl string `json:"icon_url,omitempty"`
|
||||
} `json:"provider,omitempty"`
|
||||
|
||||
Author *struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
IconUrl string `json:"icon_url,omitempty"`
|
||||
} `json:"author,omitempty"`
|
||||
|
||||
Fields []JSONWebHookDiscordEmbedField `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type JSONWebHookDiscordEmbedField struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Inline bool `json:"inline,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
DiscordColorGreen uint64 = 5763719
|
||||
DiscordColorDarkGreen uint64 = 2067276
|
||||
DiscordColorOrange uint64 = 15105570
|
||||
)
|
||||
|
||||
var WebHookRateLimit = time.NewTicker(time.Second / 20)
|
||||
var WebHookUserAgent string
|
||||
|
||||
var WebHookHost string
|
||||
|
||||
var WebHookVersion = fmt.Sprintf("%d.%d", types.CurrentSoftwareVersion.Major(), types.CurrentSoftwareVersion.Minor())
|
||||
|
||||
func init() {
|
||||
WebHookHost = os.Getenv("NET_SERVICE_ADDRESS")
|
||||
WebHookUserAgent = fmt.Sprintf("Mozilla/5.0 (compatible;GoObserver %s; +https://%s/api#webhooks)", types.CurrentSoftwareVersion.String(), WebHookHost)
|
||||
}
|
||||
|
||||
var WebHookClient = http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
func SetWebHookProxy(hostPort string) {
|
||||
WebHookClient.Transport = &http.Transport{
|
||||
Proxy: http.ProxyURL(&url.URL{
|
||||
Scheme: "socks5",
|
||||
Host: hostPort,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func SendSideBlock(w *index.MinerWebHook, ts int64, source *address.Address, block *index.SideBlock) error {
|
||||
if !CanSendToHook(w, "side_blocks") {
|
||||
return nil
|
||||
}
|
||||
|
||||
blockValuation := func() string {
|
||||
if block.IsOrphan() {
|
||||
return "0%"
|
||||
} else if block.IsUncle() {
|
||||
return strconv.FormatUint(100-w.Consensus.UnclePenalty, 10) + "% (uncle)"
|
||||
} else if block.IsUncle() {
|
||||
return "100% + " + strconv.FormatUint(w.Consensus.UnclePenalty, 10) + "% of " + strconv.FormatUint(uint64(len(block.Uncles)), 10) + "% uncle(s)"
|
||||
} else {
|
||||
return "100%"
|
||||
}
|
||||
}
|
||||
|
||||
blockWeight, _ := block.Weight(block.EffectiveHeight, w.Consensus.ChainWindowSize, w.Consensus.UnclePenalty)
|
||||
|
||||
switch w.Type {
|
||||
case index.WebHookDiscord:
|
||||
|
||||
var color = &DiscordColorGreen
|
||||
fields := []JSONWebHookDiscordEmbedField{
|
||||
{
|
||||
Name: "Pool Height",
|
||||
Value: strconv.FormatUint(block.SideHeight, 10),
|
||||
Inline: true,
|
||||
},
|
||||
}
|
||||
if block.IsUncle() {
|
||||
color = &DiscordColorDarkGreen
|
||||
fields = append(fields, JSONWebHookDiscordEmbedField{
|
||||
Name: "Parent Height",
|
||||
Value: "[" + strconv.FormatUint(block.EffectiveHeight, 10) + "](" + "https://" + WebHookHost + "/share/" + block.UncleOf.String() + ")",
|
||||
Inline: true,
|
||||
})
|
||||
} else {
|
||||
fields = append(fields, JSONWebHookDiscordEmbedField{
|
||||
Name: "Monero Height",
|
||||
Value: strconv.FormatUint(block.MainHeight, 10),
|
||||
Inline: true,
|
||||
})
|
||||
}
|
||||
fields = append(fields,
|
||||
JSONWebHookDiscordEmbedField{
|
||||
Name: "Found by",
|
||||
Value: "[`" + string(utils.ShortenSlice(block.MinerAddress.ToBase58(), 24)) + "`](" + "https://" + WebHookHost + "/miner/" + string(block.MinerAddress.ToBase58()) + ")",
|
||||
},
|
||||
JSONWebHookDiscordEmbedField{
|
||||
Name: "Template Id",
|
||||
Value: "[`" + block.TemplateId.String() + "`](" + "https://" + WebHookHost + "/share/" + block.TemplateId.String() + ")",
|
||||
},
|
||||
JSONWebHookDiscordEmbedField{
|
||||
Name: "Valuation",
|
||||
Value: blockValuation(),
|
||||
Inline: true,
|
||||
},
|
||||
JSONWebHookDiscordEmbedField{
|
||||
Name: "Weight",
|
||||
Value: utils.SiUnits(float64(blockWeight), 4),
|
||||
Inline: true,
|
||||
},
|
||||
)
|
||||
|
||||
return SendJsonPost(w, ts, source, &JSONWebHookDiscord{
|
||||
Embeds: []JSONWebHookDiscordEmbed{
|
||||
{
|
||||
Color: color,
|
||||
Title: "**" + func() string {
|
||||
if block.IsUncle() {
|
||||
return "UNCLE "
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}() + "SHARE FOUND** on P2Pool " + func() string {
|
||||
if w.Consensus.IsDefault() {
|
||||
return "Main"
|
||||
} else if w.Consensus.IsMini() {
|
||||
return "Mini"
|
||||
} else {
|
||||
return "Unknown"
|
||||
}
|
||||
}(),
|
||||
Url: "https://" + WebHookHost + "/share/" + block.MainId.String(),
|
||||
Description: "",
|
||||
Fields: fields,
|
||||
Footer: &struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
}{
|
||||
Text: "Share mined using " + block.SoftwareId.String() + " " + block.SoftwareVersion.String(),
|
||||
},
|
||||
Timestamp: time.Unix(int64(block.Timestamp), 0).UTC().Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
})
|
||||
case index.WebHookCustom:
|
||||
return SendJsonPost(w, ts, source, &JSONEvent{
|
||||
Type: JSONEventSideBlock,
|
||||
SideBlock: block,
|
||||
})
|
||||
default:
|
||||
return errors.New("unsupported hook type")
|
||||
}
|
||||
}
|
||||
|
||||
func SendFoundBlock(w *index.MinerWebHook, ts int64, source *address.Address, block *index.FoundBlock, outputs index.MainCoinbaseOutputs) error {
|
||||
if !CanSendToHook(w, "found_blocks") {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch w.Type {
|
||||
case index.WebHookCustom:
|
||||
return SendJsonPost(w, ts, source, &JSONEvent{
|
||||
Type: JSONEventFoundBlock,
|
||||
FoundBlock: block,
|
||||
MainCoinbaseOutputs: outputs,
|
||||
})
|
||||
default:
|
||||
return errors.New("unsupported hook type")
|
||||
}
|
||||
}
|
||||
|
||||
func CanSendToHook(w *index.MinerWebHook, key string) bool {
|
||||
if v, ok := w.Settings["send_"+key]; ok && v == "false" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SendPayout(w *index.MinerWebHook, ts int64, source *address.Address, payout *index.Payout) error {
|
||||
if !CanSendToHook(w, "payouts") {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch w.Type {
|
||||
case index.WebHookDiscord:
|
||||
return SendJsonPost(w, ts, source, &JSONWebHookDiscord{
|
||||
Embeds: []JSONWebHookDiscordEmbed{
|
||||
{
|
||||
Color: &DiscordColorOrange,
|
||||
Title: "**RECEIVED PAYOUT** on P2Pool " + func() string {
|
||||
if w.Consensus.IsDefault() {
|
||||
return "Main"
|
||||
} else if w.Consensus.IsMini() {
|
||||
return "Mini"
|
||||
} else {
|
||||
return "Unknown"
|
||||
}
|
||||
}(),
|
||||
Url: "https://" + WebHookHost + "/proof/" + payout.MainId.String() + "/" + strconv.FormatUint(payout.Index, 10),
|
||||
Description: "",
|
||||
Fields: []JSONWebHookDiscordEmbedField{
|
||||
{
|
||||
Name: "Pool Height",
|
||||
Value: strconv.FormatUint(payout.SideHeight, 10),
|
||||
Inline: true,
|
||||
},
|
||||
{
|
||||
Name: "Monero Height",
|
||||
Value: strconv.FormatUint(payout.MainHeight, 10),
|
||||
Inline: true,
|
||||
},
|
||||
{
|
||||
Name: "Monero Id",
|
||||
Value: "[`" + payout.MainId.String() + "`](" + GetSiteUrl(SiteKeyP2PoolIo, false) + "/explorer/block/" + payout.MainId.String() + ")",
|
||||
},
|
||||
{
|
||||
Name: "Template Id",
|
||||
Value: "[`" + payout.TemplateId.String() + "`](" + "https://" + WebHookHost + "/share/" + payout.TemplateId.String() + ")",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "Coinbase Id",
|
||||
Value: "[`" + payout.CoinbaseId.String() + "`](" + GetSiteUrl(SiteKeyP2PoolIo, false) + "/explorer/tx/" + payout.CoinbaseId.String() + ")",
|
||||
},
|
||||
{
|
||||
Name: "Coinbase Private Key",
|
||||
Value: "[`" + payout.PrivateKey.String() + "`](" + "https://" + WebHookHost + "/proof/" + payout.MainId.String() + "/" + strconv.FormatUint(payout.Index, 10) + ")",
|
||||
},
|
||||
{
|
||||
Name: "Payout address",
|
||||
Value: "[`" + string(utils.ShortenSlice(source.ToBase58(), 24)) + "`](" + "https://" + WebHookHost + "/miner/" + string(source.ToBase58()) + ")",
|
||||
},
|
||||
{
|
||||
Name: "Reward",
|
||||
Value: "**" + utils.XMRUnits(payout.Reward) + " XMR" + "**",
|
||||
Inline: true,
|
||||
},
|
||||
{
|
||||
Name: "Global Output Index",
|
||||
Value: strconv.FormatUint(payout.GlobalOutputIndex, 10),
|
||||
Inline: true,
|
||||
},
|
||||
},
|
||||
Timestamp: time.Unix(int64(payout.Timestamp), 0).UTC().Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
})
|
||||
case index.WebHookCustom:
|
||||
return SendJsonPost(w, ts, source, &JSONEvent{
|
||||
Type: JSONEventPayout,
|
||||
Payout: payout,
|
||||
})
|
||||
default:
|
||||
return errors.New("unsupported hook type")
|
||||
}
|
||||
}
|
||||
|
||||
func SendOrphanedBlock(w *index.MinerWebHook, ts int64, source *address.Address, block *index.SideBlock) error {
|
||||
if !CanSendToHook(w, "orphaned_blocks") {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch w.Type {
|
||||
case index.WebHookCustom:
|
||||
return SendJsonPost(w, ts, source, &JSONEvent{
|
||||
Type: JSONEventOrphanedBlock,
|
||||
SideBlock: block,
|
||||
})
|
||||
default:
|
||||
return errors.New("unsupported hook type")
|
||||
}
|
||||
}
|
||||
|
||||
func SendJsonPost(w *index.MinerWebHook, ts int64, source *address.Address, data any) error {
|
||||
uri, err := url.Parse(w.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := utils.MarshalJSON(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := make(http.Header)
|
||||
headers.Set("Accept", "*/*")
|
||||
headers.Set("User-Agent", WebHookUserAgent)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("X-P2Pool-Observer-Timestamp", strconv.FormatInt(ts, 10))
|
||||
headers.Set("X-P2Pool-Observer-Version", WebHookVersion)
|
||||
headers.Set("X-P2Pool-Observer-Host", WebHookHost)
|
||||
headers.Set("X-P2Pool-Observer-Consensus-ID", w.Consensus.Id.String())
|
||||
headers.Set("X-P2Pool-Observer-Address", string(source.ToBase58()))
|
||||
|
||||
// apply rate limit
|
||||
<-WebHookRateLimit.C
|
||||
|
||||
response, err := WebHookClient.Do(&http.Request{
|
||||
Method: "POST",
|
||||
URL: uri,
|
||||
Header: headers,
|
||||
Body: io.NopCloser(bytes.NewBuffer(body)),
|
||||
ContentLength: int64(len(body)),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
defer io.ReadAll(response.Body)
|
||||
|
||||
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNoContent {
|
||||
data, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("error code %d: %s", response.StatusCode, string(data))
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -25,6 +25,7 @@ func getTypeFromAPI[T any](method string, cacheTime ...int) *T {
|
|||
return nil
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
defer io.ReadAll(response.Body)
|
||||
if response.StatusCode == http.StatusOK {
|
||||
var result T
|
||||
decoder := utils.NewJSONDecoder(response.Body)
|
||||
|
@ -55,6 +56,7 @@ func getSliceFromAPI[T any](method string, cacheTime ...int) []T {
|
|||
return nil
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
defer io.ReadAll(response.Body)
|
||||
if response.StatusCode == http.StatusOK {
|
||||
var result []T
|
||||
decoder := utils.NewJSONDecoder(response.Body)
|
||||
|
@ -141,6 +143,7 @@ func getFromAPIRaw(method string, cacheTime ...int) []byte {
|
|||
return nil
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
defer io.ReadAll(response.Body)
|
||||
if response.StatusCode == http.StatusOK {
|
||||
if data, err := io.ReadAll(response.Body); err != nil {
|
||||
return nil
|
||||
|
@ -153,3 +156,22 @@ func getFromAPIRaw(method string, cacheTime ...int) []byte {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func getFromAPI(method string) (statusCode int, data []byte) {
|
||||
uri, _ := url.Parse(os.Getenv("API_URL") + method)
|
||||
if response, err := http.DefaultClient.Do(&http.Request{
|
||||
Method: "GET",
|
||||
URL: uri,
|
||||
}); err != nil {
|
||||
return 0, nil
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
defer io.ReadAll(response.Body)
|
||||
|
||||
if data, err = io.ReadAll(response.Body); err != nil {
|
||||
return response.StatusCode, nil
|
||||
} else {
|
||||
return response.StatusCode, data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ require (
|
|||
git.gammaspectra.live/P2Pool/p2pool-observer v0.0.0
|
||||
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index v0.0.0
|
||||
git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils v0.0.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc
|
||||
github.com/valyala/quicktemplate v1.7.0
|
||||
|
@ -28,7 +29,6 @@ require (
|
|||
github.com/dolthub/maphash v0.1.0 // indirect
|
||||
github.com/dolthub/swiss v0.1.0 // indirect
|
||||
github.com/floatdrop/lru v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/holiman/uint256 v1.2.3 // indirect
|
||||
github.com/jxskiss/base62 v1.1.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
|
|
183
cmd/web/views/miner-options.qtpl
Normal file
183
cmd/web/views/miner-options.qtpl
Normal file
|
@ -0,0 +1,183 @@
|
|||
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
|
||||
{% import cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils" %}
|
||||
|
||||
{% code
|
||||
type MinerOptionsPage struct {
|
||||
// inherit from base page, so its' title is used in error page.
|
||||
BasePage
|
||||
Miner *cmdutils.MinerInfoResult
|
||||
SignedAction *cmdutils.SignedAction
|
||||
WebHooks []*index.MinerWebHook
|
||||
}
|
||||
%}
|
||||
|
||||
{% func (p *MinerOptionsPage) Title() %}
|
||||
{%= p.BasePage.Title() %} - Miner Options {%z= p.Miner.Address.ToBase58() %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func (p *MinerOptionsPage) Content() %}
|
||||
|
||||
<div style="text-align: center">
|
||||
<h2 class="mono">Miner Options</h2>
|
||||
<p><strong>Payout Address:</strong> <span class="mono small">{%z= p.Miner.Address.ToBase58() %}</span></p>
|
||||
|
||||
{% if p.Miner.Alias != "" %}
|
||||
<p><strong>Miner Alias:</strong> <span class="mono">{%s p.Miner.Alias %}</span></p>
|
||||
<p><small>Miner Alias is user content and not verified. This value should only be used for vanity purposes.</small></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if p.SignedAction != nil %}
|
||||
|
||||
<div style="text-align: center;">
|
||||
|
||||
<form action="/miner-options/{%z= p.Miner.Address.ToBase58() %}/signed_action" method="get" target="_blank">
|
||||
<h2>Sign and submit changes</h2>
|
||||
<p>To submit any changes you will need to sign a message using your wallet to prove ownership.</p>
|
||||
<p>On the Monero GUI, go to Advanced -> Sign/verify -> Select Mode "Message".</p>
|
||||
<p>Enter the Message listed below in the "Message" field, then press Sign Message. Copy the Signature and paste it on the field below.</p>
|
||||
<p>Do not modify the message, add spaces or add any new lines.</p>
|
||||
<p><small>Signatures generated by a View-Only wallet are not supported. Monero GUI does not generate valid signatures for hardware wallets.</small></p>
|
||||
<div>
|
||||
<label for="message">Message</label><br/>
|
||||
<input type="text" name="message" id="message" size="96" class="mono" value="{%s p.SignedAction.String() %}" style="user-select: all; -webkit-user-select: all;" readonly autofocus/>
|
||||
</div>
|
||||
<div style="margin-top: 10px">
|
||||
<label for="signature">Signature</label><br/>
|
||||
<input type="text" name="signature" id="signature" size="96" class="mono" placeholder="SigV2..." pattern="^SigV[12][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$" required/>
|
||||
</div>
|
||||
<div style="margin-top: 10px">
|
||||
<input type="submit" value="Submit changes" style="width: 20em;"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div style="text-align: center;">
|
||||
<h2 id="mineralias">Miner Alias</h2>
|
||||
|
||||
<div>
|
||||
<form action="/miner-options/{%z= p.Miner.Address.ToBase58() %}/set_miner_alias" method="get" target="_blank">
|
||||
<h3>Set Miner Alias</h3>
|
||||
<p><small>
|
||||
Miner Alias must be 4 to 20 characters long, and only 0-9, a-z, A-Z, and _-. are allowed, and cannot start with a number or symbol. No spaces are allowed either.
|
||||
</br>
|
||||
Alias are unique for the observer instance, and can be removed or changed after adding one.
|
||||
</small></p>
|
||||
<div>
|
||||
<label for="miner-alias">Miner Alias</label><br/>
|
||||
<input type="text" name="alias" id="miner-alias" minlength="4" maxlength="20" size="20" class="mono" value="{%s p.Miner.Alias %}" pattern="^[a-zA-Z][0-9a-zA-Z_.\-]+$" required/>
|
||||
</div>
|
||||
<div style="margin-top: 10px">
|
||||
<input type="submit" value="Set Alias" style="width: 20em;"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{% if p.Miner.Alias != "" %}
|
||||
<form action="/miner-options/{%z= p.Miner.Address.ToBase58() %}/unset_miner_alias" method="get" target="_blank">
|
||||
<h3>Unset Miner Alias</h3>
|
||||
<p><small>You can remove your miner alias below if you don't want one set.</small></p>
|
||||
<div style="margin-top: 10px">
|
||||
<input type="submit" value="Unset Alias" style="width: 20em;"/>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="text-align: center;">
|
||||
<h2 id="webhooks">Notification WebHooks</h2>
|
||||
|
||||
<div>
|
||||
<form action="/miner-options/{%z= p.Miner.Address.ToBase58() %}/add_webhook" method="get" target="_blank">
|
||||
<h3>Add WebHook</h3>
|
||||
<p><small>
|
||||
Configure notification URLs that can be used to post messages or other actions in other platforms or services.
|
||||
</br>
|
||||
Only one URL of each type is allowed. To adjust an existing hook settings you will need to add it with the new preference.
|
||||
</small></p>
|
||||
<div>
|
||||
<label for="webhook-type">WebHook Type</label><br/>
|
||||
<select type="text" name="type" id="webhook-type" required>
|
||||
<option value=""></option>
|
||||
<optgroup label="Chat Services">
|
||||
<option value="{%s string(index.WebHookDiscord) %}">Discord</option>
|
||||
<option value="{%s string(index.WebHookSlack) %}" disabled>Slack (coming soon)</option>
|
||||
<option value="{%s string(index.WebHookTelegram) %}" disabled>Telegram (coming soon)</option>
|
||||
<option value="{%s string(index.WebHookMatrixHookshot) %}" disabled>Matrix Hookshot (coming soon)</option>
|
||||
</optgroup>
|
||||
<optgroup label="Other">
|
||||
<option value="{%s string(index.WebHookCustom) %}">Custom</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</br>
|
||||
<div>
|
||||
<label for="webhook-url">WebHook URL</label><br/>
|
||||
<input type="url" name="url" id="webhook-url" size="60" class="mono" value="" required/>
|
||||
</div>
|
||||
</br>
|
||||
<fieldset>
|
||||
<legend>WebHook Events</legend>
|
||||
<div>
|
||||
<input type="checkbox" id="webhook-side_blocks" name="side_blocks" checked />
|
||||
<label for="webhook-side_blocks">Miner Side blocks</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="webhook-payouts" name="payouts" checked />
|
||||
<label for="webhook-payouts">Miner Payouts</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="webhook-found_blocks" name="found_blocks" />
|
||||
<label for="webhook-found_blocks">Found Monero blocks</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="webhook-orphaned_blocks" name="orphaned_blocks" />
|
||||
<label for="webhook-orphaned_blocks">Orphaned Monero blocks</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="webhook-other" name="other" checked />
|
||||
<label for="webhook-other">Other (notices, updates, events)</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div style="margin-top: 10px">
|
||||
<input type="submit" value="Add WebHook" style="width: 20em;"/>
|
||||
</div>
|
||||
</form>
|
||||
{% if len(p.WebHooks) > 0 %}
|
||||
|
||||
<h3>Current WebHooks</h3>
|
||||
|
||||
<table class="center datatable" style="max-width: calc(8em + 28em + 8em + 8em + 8em + 8em + 8em + 8em)">
|
||||
<tr>
|
||||
<th style="width: 8em;">Type</th>
|
||||
<th style="width: 28em;" title="URL SHA-256 Hash">URL Hash</th>
|
||||
<th style="width: 8em;">Shares</th>
|
||||
<th style="width: 8em;">Payouts</th>
|
||||
<th style="width: 8em;">Found Blocks</th>
|
||||
<th style="width: 8em;">Orphaned Blocks</th>
|
||||
<th style="width: 8em;">Other</th>
|
||||
<th style="width: 8em;"></th>
|
||||
</tr>
|
||||
{% for _, w := range p.WebHooks %}
|
||||
<tr>
|
||||
<th>{%s string(w.Type) %}</th>
|
||||
<td class="mono small">{%s w.Url %}</td>
|
||||
<td>{% if cmdutils.CanSendToHook(w, "side_blocks") %}Yes{% else %}No{% endif %}</td>
|
||||
<td>{% if cmdutils.CanSendToHook(w, "payouts") %}Yes{% else %}No{% endif %}</td>
|
||||
<td>{% if cmdutils.CanSendToHook(w, "found_blocks") %}Yes{% else %}No{% endif %}</td>
|
||||
<td>{% if cmdutils.CanSendToHook(w, "orphaned_blocks") %}Yes{% else %}No{% endif %}</td>
|
||||
<td>{% if cmdutils.CanSendToHook(w, "other") %}Yes{% else %}No{% endif %}</td>
|
||||
<td><a href="/miner-options/{%z= p.Miner.Address.ToBase58() %}/remove_webhook?type={%s string(w.Type) %}&url_hash={%s w.Url %}">[Remove]</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endfunc %}
|
|
@ -128,6 +128,8 @@ type MinerPage struct {
|
|||
<p><small>Miner Alias is user content and not verified. This value should only be used for vanity purposes.</small></p>
|
||||
{% endif %}
|
||||
|
||||
<p><small><a href="/miner-options/{%z= p.Miner.Address.ToBase58() %}#mineralias">[Change Miner Alias]</a> :: <a href="/miner-options/{%z= p.Miner.Address.ToBase58() %}#webhooks">[Configure WebHook notifications]</a></small></p>
|
||||
|
||||
<table class="center" style="max-width: calc(15em + 15em + 15em + 15em + 15em)">
|
||||
<tr>
|
||||
<th>Last Share</th>
|
||||
|
@ -271,28 +273,5 @@ type MinerPage struct {
|
|||
<input type="submit" value="Calculate" style="width: 20em; margin: 20px;"/>
|
||||
</form>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
<div style="text-align: center">
|
||||
<form action="/api/miner_alias/{%z= p.Miner.Address.ToBase58() %}" method="get" target="_blank">
|
||||
<h3>Set Miner Alias</h3>
|
||||
<p>To set a miner alias you will need to sign a message using your wallet. On the Monero GUI, go to Advanced -> Sign/verify -> Select Mode "Message"</p>
|
||||
<p>Enter the desired Miner Alias in Message, then press Sign Message. Copy the Signature and paste it on the field below, along with your chosen Miner Alias.</p>
|
||||
<p><small>Miner Alias must be 4 to 20 characters long, and only 0-9, a-z, A-Z, and _-. are allowed, and cannot start with a number or symbol. No spaces are allowed either. Alias are unique for the observer instance.</small></p>
|
||||
<p><small>Signatures generated by a View-Only wallet are not supported. Monero GUI does not generate valid signatures for hardware wallets.</small></p>
|
||||
<p><small>You can remove your miner alias by using "REMOVE_MINER_ALIAS" as an alias.</small></p>
|
||||
<div>
|
||||
<label for="miner-alias">Miner Alias</label><br/>
|
||||
<input type="text" name="message" id="miner-alias" size="20" class="mono"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="signature">Signature</label><br/>
|
||||
<input type="text" name="signature" id="signature" size="96" class="mono"/>
|
||||
</div>
|
||||
<div style="margin-top: 10px">
|
||||
<input type="submit" value="Set Alias" style="width: 20em;"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfunc %}
|
||||
|
|
106
cmd/web/web.go
106
cmd/web/web.go
|
@ -14,6 +14,7 @@ import (
|
|||
types2 "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
|
||||
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/valyala/quicktemplate"
|
||||
"io"
|
||||
|
@ -153,19 +154,23 @@ func main() {
|
|||
switch strings.Split(humanHost, ":")[0] {
|
||||
case "irc.libera.chat":
|
||||
if len(splitChan) > 1 {
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s", splitChan[0], splitChan[1])
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s?via=matrix.org&via=%s", splitChan[0], splitChan[1], splitChan[1])
|
||||
webchatLink = fmt.Sprintf("https://web.libera.chat/?nick=Guest?#%s", splitChan[0])
|
||||
} else {
|
||||
humanHost = "libera.chat"
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s", ircUrl.Fragment, humanHost)
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s?via=matrix.org", ircUrl.Fragment, humanHost)
|
||||
webchatLink = fmt.Sprintf("https://web.libera.chat/?nick=Guest%%3F#%s", ircUrl.Fragment)
|
||||
}
|
||||
case "irc.hackint.org":
|
||||
if len(splitChan) > 1 {
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s", splitChan[0], splitChan[1])
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s?via=matrix.org&via=%s", splitChan[0], splitChan[1], splitChan[1])
|
||||
} else {
|
||||
humanHost = "hackint.org"
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s", ircUrl.Fragment, humanHost)
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s?via=matrix.org", ircUrl.Fragment, humanHost)
|
||||
}
|
||||
default:
|
||||
if len(splitChan) > 1 {
|
||||
matrixLink = fmt.Sprintf("https://matrix.to/#/#%s:%s?via=matrix.org&via=%s", splitChan[0], splitChan[1], splitChan[1])
|
||||
}
|
||||
}
|
||||
ircLinkTitle = fmt.Sprintf("#%s@%s", splitChan[0], humanHost)
|
||||
|
@ -1063,6 +1068,99 @@ func main() {
|
|||
renderPage(request, writer, minerPage, poolInfo)
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/miner-options/{miner:[^ ]+}/signed_action", func(writer http.ResponseWriter, request *http.Request) {
|
||||
params := request.URL.Query()
|
||||
|
||||
address := mux.Vars(request)["miner"]
|
||||
miner := getTypeFromAPI[cmdutils.MinerInfoResult](fmt.Sprintf("miner_info/%s?noShares", address))
|
||||
if miner == nil || miner.Address == nil {
|
||||
renderPage(request, writer, views.NewErrorPage(http.StatusNotFound, "Address Not Found", "You need to have mined at least one share in the past. Come back later :)"))
|
||||
return
|
||||
}
|
||||
|
||||
var signedAction *cmdutils.SignedAction
|
||||
|
||||
if err := json.Unmarshal([]byte(params.Get("message")), &signedAction); err != nil || signedAction == nil {
|
||||
renderPage(request, writer, views.NewErrorPage(http.StatusBadRequest, "Invalid Message", nil))
|
||||
return
|
||||
}
|
||||
|
||||
values := make(url.Values)
|
||||
values.Set("signature", params.Get("signature"))
|
||||
values.Set("message", signedAction.String())
|
||||
|
||||
var jsonErr struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
statusCode, buf := getFromAPI("miner_signed_action/" + string(miner.Address.ToBase58()) + "?" + values.Encode())
|
||||
_ = json.Unmarshal(buf, &jsonErr)
|
||||
if statusCode != http.StatusOK {
|
||||
renderPage(request, writer, views.NewErrorPage(http.StatusBadRequest, "Could not verify message", jsonErr.Error))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/miner-options/{miner:[^ ]+}/{action:set_miner_alias|unset_miner_alias|add_webhook|remove_webhook}", func(writer http.ResponseWriter, request *http.Request) {
|
||||
params := request.URL.Query()
|
||||
|
||||
address := mux.Vars(request)["miner"]
|
||||
miner := getTypeFromAPI[cmdutils.MinerInfoResult](fmt.Sprintf("miner_info/%s?noShares", address))
|
||||
if miner == nil || miner.Address == nil {
|
||||
renderPage(request, writer, views.NewErrorPage(http.StatusNotFound, "Address Not Found", "You need to have mined at least one share in the past. Come back later :)"))
|
||||
return
|
||||
}
|
||||
|
||||
var signedAction *cmdutils.SignedAction
|
||||
|
||||
action := mux.Vars(request)["action"]
|
||||
switch action {
|
||||
case "set_miner_alias":
|
||||
signedAction = cmdutils.SignedActionSetMinerAlias(baseContext.NetServiceAddress, params.Get("alias"))
|
||||
case "unset_miner_alias":
|
||||
signedAction = cmdutils.SignedActionUnsetMinerAlias(baseContext.NetServiceAddress)
|
||||
case "add_webhook":
|
||||
signedAction = cmdutils.SignedActionAddWebHook(baseContext.NetServiceAddress, params.Get("type"), params.Get("url"))
|
||||
for _, s := range []string{"side_blocks", "payouts", "found_blocks", "orphaned_blocks", "other"} {
|
||||
value := "false"
|
||||
if params.Get(s) == "on" {
|
||||
value = "true"
|
||||
}
|
||||
signedAction.Data = append(signedAction.Data, cmdutils.SignedActionEntry{
|
||||
Key: "send_" + s,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
case "remove_webhook":
|
||||
signedAction = cmdutils.SignedActionRemoveWebHook(baseContext.NetServiceAddress, params.Get("type"), params.Get("url_hash"))
|
||||
default:
|
||||
renderPage(request, writer, views.NewErrorPage(http.StatusNotFound, "Invalid Action", nil))
|
||||
return
|
||||
}
|
||||
|
||||
renderPage(request, writer, &views.MinerOptionsPage{
|
||||
Miner: miner,
|
||||
SignedAction: signedAction,
|
||||
WebHooks: getSliceFromAPI[*index.MinerWebHook](fmt.Sprintf("miner_webhooks/%s", address)),
|
||||
})
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/miner-options/{miner:[^ ]+}", func(writer http.ResponseWriter, request *http.Request) {
|
||||
//params := request.URL.Query()
|
||||
|
||||
address := mux.Vars(request)["miner"]
|
||||
miner := getTypeFromAPI[cmdutils.MinerInfoResult](fmt.Sprintf("miner_info/%s?noShares", address))
|
||||
if miner == nil || miner.Address == nil {
|
||||
renderPage(request, writer, views.NewErrorPage(http.StatusNotFound, "Address Not Found", "You need to have mined at least one share in the past. Come back later :)"))
|
||||
return
|
||||
}
|
||||
|
||||
renderPage(request, writer, &views.MinerOptionsPage{
|
||||
Miner: miner,
|
||||
SignedAction: nil,
|
||||
WebHooks: getSliceFromAPI[*index.MinerWebHook](fmt.Sprintf("miner_webhooks/%s", address)),
|
||||
})
|
||||
})
|
||||
|
||||
serveMux.HandleFunc("/miner", func(writer http.ResponseWriter, request *http.Request) {
|
||||
params := request.URL.Query()
|
||||
if params.Get("address") == "" {
|
||||
|
|
|
@ -31,6 +31,15 @@ services:
|
|||
- p2pool
|
||||
networks:
|
||||
- p2pool-observer
|
||||
tor-proxy:
|
||||
image: goldy/tor-hidden-service:v0.4.7.12-54c0e54
|
||||
tmpfs:
|
||||
- /tmp
|
||||
restart: always
|
||||
environment:
|
||||
TOR_SOCKS_PORT: 9050
|
||||
networks:
|
||||
- p2pool-observer
|
||||
site:
|
||||
build:
|
||||
context: ./docker/nginx
|
||||
|
@ -93,7 +102,6 @@ services:
|
|||
- /run
|
||||
- /run/postgresql
|
||||
|
||||
|
||||
p2pool:
|
||||
build:
|
||||
context: ./
|
||||
|
@ -132,6 +140,7 @@ services:
|
|||
- GOPROXY=${GOPROXY}
|
||||
restart: always
|
||||
environment:
|
||||
- NET_SERVICE_ADDRESS=${NET_SERVICE_ADDRESS}
|
||||
- TOR_SERVICE_ADDRESS=${TOR_SERVICE_ADDRESS}
|
||||
- TRANSACTION_LOOKUP_OTHER=${TRANSACTION_LOOKUP_OTHER}
|
||||
depends_on:
|
||||
|
@ -185,9 +194,11 @@ services:
|
|||
restart: always
|
||||
environment:
|
||||
- TOR_SERVICE_ADDRESS=${TOR_SERVICE_ADDRESS}
|
||||
- NET_SERVICE_ADDRESS=${NET_SERVICE_ADDRESS}
|
||||
- TRANSACTION_LOOKUP_OTHER=${TRANSACTION_LOOKUP_OTHER}
|
||||
depends_on:
|
||||
- db
|
||||
- tor-proxy
|
||||
- p2pool
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
@ -201,6 +212,7 @@ services:
|
|||
-host ${MONEROD_HOST}
|
||||
-rpc-port ${MONEROD_RPC_PORT}
|
||||
-api-host "http://p2pool:3131"
|
||||
-hook-proxy "tor-proxy:9050"
|
||||
-db="postgres:///p2pool?host=/var/run/postgresql&user=p2pool&password=p2pool&sslmode=disable"
|
||||
|
||||
pgo-p2pool:
|
||||
|
|
|
@ -92,7 +92,11 @@ func GetTxProofV1(a Interface, txId types.Hash, txKey crypto.PrivateKey, message
|
|||
type SignatureVerifyResult int
|
||||
|
||||
const (
|
||||
ResultFail SignatureVerifyResult = iota
|
||||
ResultFailZeroSpend SignatureVerifyResult = -2
|
||||
ResultFailZeroView SignatureVerifyResult = -1
|
||||
)
|
||||
const (
|
||||
ResultFail = SignatureVerifyResult(iota)
|
||||
ResultSuccessSpend
|
||||
ResultSuccessView
|
||||
)
|
||||
|
@ -130,6 +134,11 @@ func VerifyMessage(a Interface, message []byte, signature string) SignatureVerif
|
|||
return ResultSuccessSpend
|
||||
}
|
||||
|
||||
// Special mode: view wallets in Monero GUI could generate signatures with spend public key proper, with message hash of spend wallet mode, but zero spend private key
|
||||
if crypto.VerifyMessageSignatureSplit(hash, a.SpendPublicKey(), ZeroPrivateKeyAddress.SpendPublicKey(), sig) {
|
||||
return ResultFailZeroSpend
|
||||
}
|
||||
|
||||
if strings.HasPrefix(signature, "SigV2") {
|
||||
hash = GetMessageHash(a, message, 1)
|
||||
}
|
||||
|
@ -140,3 +149,47 @@ func VerifyMessage(a Interface, message []byte, signature string) SignatureVerif
|
|||
|
||||
return ResultFail
|
||||
}
|
||||
|
||||
// VerifyMessageFallbackToZero Check for Monero GUI behavior to generate wrong signatures on view-only wallets
|
||||
func VerifyMessageFallbackToZero(a Interface, message []byte, signature string) SignatureVerifyResult {
|
||||
var hash types.Hash
|
||||
|
||||
if strings.HasPrefix(signature, "SigV1") {
|
||||
hash = crypto.Keccak256(message)
|
||||
} else if strings.HasPrefix(signature, "SigV2") {
|
||||
hash = GetMessageHash(a, message, 0)
|
||||
} else {
|
||||
return ResultFail
|
||||
}
|
||||
raw := moneroutil.DecodeMoneroBase58([]byte(signature[5:]))
|
||||
|
||||
sig := crypto.NewSignatureFromBytes(raw)
|
||||
|
||||
if sig == nil {
|
||||
return ResultFail
|
||||
}
|
||||
|
||||
if crypto.VerifyMessageSignature(hash, a.SpendPublicKey(), sig) {
|
||||
return ResultSuccessSpend
|
||||
}
|
||||
|
||||
// Special mode: view wallets in Monero GUI could generate signatures with spend public key proper, with message hash of spend wallet mode, but zero spend private key
|
||||
if crypto.VerifyMessageSignatureSplit(hash, a.SpendPublicKey(), ZeroPrivateKeyAddress.SpendPublicKey(), sig) {
|
||||
return ResultFailZeroSpend
|
||||
}
|
||||
|
||||
if strings.HasPrefix(signature, "SigV2") {
|
||||
hash = GetMessageHash(a, message, 1)
|
||||
}
|
||||
|
||||
if crypto.VerifyMessageSignature(hash, a.ViewPublicKey(), sig) {
|
||||
return ResultSuccessView
|
||||
}
|
||||
|
||||
// Special mode
|
||||
if crypto.VerifyMessageSignatureSplit(hash, a.ViewPublicKey(), ZeroPrivateKeyAddress.ViewPublicKey(), sig) {
|
||||
return ResultFailZeroView
|
||||
}
|
||||
|
||||
return ResultFail
|
||||
}
|
||||
|
|
|
@ -80,3 +80,45 @@ func TestDerivePublicKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testThrowawayAddress = FromBase58("42ecNLuoGtn1qC9SPSD9FPMNfsv35RE66Eu8WJJtyEHKfEsEiALVVp7GBCeAYFb7PHcSZmz9sDUtRMnKk2as1KfuLuTQJ3i")
|
||||
var signatureMessage = []byte("test_message")
|
||||
var signatureMessage2 = []byte("test_message2")
|
||||
|
||||
const (
|
||||
SignatureSpendSuccess = "SigV2DnQYF11xZ1ahLJxddohCroiEJRnUe1tgwD5ksmFMzQ9NcRdbxLPrEdQW3e8w4sLpqhSup5tU9igQqeAR8j7r7Sty"
|
||||
SignatureSpendWrongMessage = "SigV26RKRd31efizGHrWHwtYG6EN2MmwvF1rjU4ygZQuDmSxvCJnky1GJTzaM49naQeKvXbaGcnpZ1b3k8gVQLaFMFiBJ"
|
||||
SignatureViewSuccess = "SigV2b7LaAuXrFvPAXwU11SJwHbcXJoKfQ5aBJ9FwMJNxvMTu78AebqNUCWPH1BVfNRvy1f3GCTLjHfWvuRJMZtSHu5uj"
|
||||
SignatureViewWrongMessage = "SigV2AxWUATswZvnHSR5mMRsn9GcJe2gSCv3SbFwHv6J8THkj5KvmR8gUnTidHovZVyxgNHcUuiunM2dfVhbZvBTS6sZZ"
|
||||
)
|
||||
|
||||
func TestSignature(t *testing.T) {
|
||||
result := VerifyMessage(testThrowawayAddress, signatureMessage, SignatureSpendSuccess)
|
||||
if result != ResultSuccessSpend {
|
||||
t.Fatalf("unexpected %d", result)
|
||||
}
|
||||
result = VerifyMessage(testThrowawayAddress, signatureMessage, SignatureSpendWrongMessage)
|
||||
if result != ResultFail {
|
||||
t.Fatalf("unexpected %d", result)
|
||||
}
|
||||
result = VerifyMessage(testThrowawayAddress, signatureMessage2, SignatureSpendSuccess)
|
||||
if result != ResultFail {
|
||||
t.Fatalf("unexpected %d", result)
|
||||
}
|
||||
result = VerifyMessage(testThrowawayAddress, signatureMessage, SignatureViewSuccess)
|
||||
if result != ResultFailZeroSpend {
|
||||
t.Fatalf("unexpected %d", result)
|
||||
}
|
||||
result = VerifyMessage(testThrowawayAddress, signatureMessage, SignatureViewWrongMessage)
|
||||
if result != ResultFail {
|
||||
t.Fatalf("unexpected %d", result)
|
||||
}
|
||||
result = VerifyMessage(testThrowawayAddress, signatureMessage2, SignatureViewSuccess)
|
||||
if result != ResultFail {
|
||||
t.Fatalf("unexpected %d", result)
|
||||
}
|
||||
result = VerifyMessage(&ZeroPrivateKeyAddress, signatureMessage, SignatureViewSuccess)
|
||||
if result != ResultFail {
|
||||
t.Fatalf("unexpected %d", result)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,13 +80,18 @@ func CreateMessageSignature(prefixHash types.Hash, key PrivateKey) *Signature {
|
|||
}
|
||||
|
||||
func VerifyMessageSignature(prefixHash types.Hash, publicKey PublicKey, signature *Signature) bool {
|
||||
return VerifyMessageSignatureSplit(prefixHash, publicKey, publicKey, signature)
|
||||
}
|
||||
|
||||
// VerifyMessageSignatureSplit Allows specifying a different signer key than for the rest. Use VerifyMessageSignature in all other cases
|
||||
func VerifyMessageSignatureSplit(prefixHash types.Hash, commPublicKey, signPublicKey PublicKey, signature *Signature) bool {
|
||||
buf := &SignatureComm{}
|
||||
buf.Hash = prefixHash
|
||||
buf.Key = publicKey
|
||||
buf.Key = commPublicKey
|
||||
|
||||
ok, _ := signature.Verify(func(r PublicKey) []byte {
|
||||
buf.Comm = r
|
||||
return buf.Bytes()
|
||||
}, publicKey)
|
||||
}, signPublicKey)
|
||||
return ok
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue