consensus/merge_mining/client.go
2024-04-10 03:10:55 +02:00

253 lines
6.6 KiB
Go

package merge_mining
import (
"bytes"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/consensus/v3/monero/crypto"
"git.gammaspectra.live/P2Pool/consensus/v3/types"
"git.gammaspectra.live/P2Pool/consensus/v3/utils"
"io"
"net/http"
"net/url"
)
type Client interface {
GetChainId() (id types.Hash, err error)
GetJob(chainAddress string, auxiliaryHash types.Hash, height uint64, prevId types.Hash) (job AuxiliaryJob, same bool, err error)
SubmitSolution(job AuxiliaryJob, blob []byte, proof crypto.MerkleProof) (status string, err error)
}
type GenericClient struct {
Address *url.URL
Client *http.Client
}
func NewGenericClient(address string, client *http.Client) (*GenericClient, error) {
uri, err := url.Parse(address)
if err != nil {
return nil, err
}
if client == nil {
client = http.DefaultClient
}
return &GenericClient{
Address: uri,
Client: client,
}, nil
}
type RPCJSON struct {
JSONRPC string `json:"jsonrpc"`
Id string `json:"id"`
Method string `json:"method"`
Params any `json:"params,omitempty"`
}
type MergeMiningGetChainIdResult struct {
Result struct {
ChainID types.Hash `json:"chain_id"`
} `json:"result"`
Error string `json:"error"`
}
type MergeMiningGetJobJSON struct {
// Address A wallet address on the merge mined chain
Address string `json:"address"`
// AuxiliaryHash Merge mining job that is currently being used
AuxiliaryHash types.Hash `json:"aux_hash"`
// Height Monero height
Height uint64 `json:"height"`
// PreviousId Hash of the previous Monero block
PreviousId types.Hash `json:"prev_id"`
}
type MergeMiningGetJobResult struct {
Result AuxiliaryJob `json:"result"`
Error string `json:"error"`
}
type MergeMiningSubmitSolutionJSON struct {
// AuxiliaryBlob blob of data returned by merge_mining_get_job.
AuxiliaryBlob types.Bytes `json:"aux_blob"`
// AuxiliaryHash A 32-byte hex-encoded hash of the aux_blob - the same value that was returned by merge_mining_get_job.
AuxiliaryHash types.Hash `json:"aux_hash"`
// Blob Monero block template that has enough PoW to satisfy difficulty returned by merge_mining_get_job.
// It also must have a merge mining tag in tx_extra of the coinbase transaction.
Blob types.Bytes `json:"blob"`
// MerkleProof A proof that aux_hash was included when calculating Merkle root hash from the merge mining tag
MerkleProof crypto.MerkleProof `json:"merkle_proof"`
}
type MergeMiningSubmitSolutionResult struct {
Result struct {
Status string `json:"status"`
} `json:"result"`
Error string `json:"error"`
}
func (c *GenericClient) GetChainId() (id types.Hash, err error) {
data, err := utils.MarshalJSON(RPCJSON{
JSONRPC: "2.0",
Id: "0",
Method: "merge_mining_get_chain_id",
})
if err != nil {
return types.ZeroHash, err
}
response, err := c.Client.Do(&http.Request{
Method: "POST",
URL: c.Address,
Header: http.Header{
"Content-Type": []string{"application/json-rpc"},
},
Body: io.NopCloser(bytes.NewBuffer(data)),
ContentLength: int64(len(data)),
})
if err != nil {
return types.ZeroHash, err
}
defer io.ReadAll(response.Body)
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return types.ZeroHash, fmt.Errorf("unexpected status code: %d", response.StatusCode)
}
resultJSON, err := io.ReadAll(response.Body)
if err != nil {
return types.ZeroHash, err
}
var result MergeMiningGetChainIdResult
if err := utils.UnmarshalJSON(resultJSON, &result); err != nil {
return types.ZeroHash, err
}
if result.Error != "" {
return types.ZeroHash, errors.New(result.Error)
}
return result.Result.ChainID, nil
}
func (c *GenericClient) GetJob(chainAddress string, auxiliaryHash types.Hash, height uint64, prevId types.Hash) (job AuxiliaryJob, same bool, err error) {
data, err := utils.MarshalJSON(RPCJSON{
JSONRPC: "2.0",
Id: "0",
Method: "merge_mining_get_job",
Params: MergeMiningGetJobJSON{
Address: chainAddress,
AuxiliaryHash: auxiliaryHash,
Height: height,
PreviousId: prevId,
},
})
if err != nil {
return AuxiliaryJob{}, false, err
}
response, err := c.Client.Do(&http.Request{
Method: "POST",
URL: c.Address,
Header: http.Header{
"Content-Type": []string{"application/json-rpc"},
},
Body: io.NopCloser(bytes.NewBuffer(data)),
ContentLength: int64(len(data)),
})
if err != nil {
return AuxiliaryJob{}, false, err
}
defer io.ReadAll(response.Body)
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return AuxiliaryJob{}, false, fmt.Errorf("unexpected status code: %d", response.StatusCode)
}
resultJSON, err := io.ReadAll(response.Body)
if err != nil {
return AuxiliaryJob{}, false, err
}
var result MergeMiningGetJobResult
if err := utils.UnmarshalJSON(resultJSON, &result); err != nil {
return AuxiliaryJob{}, false, err
}
if result.Error != "" {
return AuxiliaryJob{}, false, errors.New(result.Error)
}
// If aux_hash is the same as in the request, all other fields will be ignored by P2Pool, so they don't have to be included in the response.
if result.Result.Hash == auxiliaryHash {
return AuxiliaryJob{}, true, nil
}
// Moreover, empty response will be interpreted as a response having the same aux_hash as in the request. This enables an efficient polling.
// TODO: properly check for emptiness
if result.Result.Hash == types.ZeroHash {
return AuxiliaryJob{}, true, nil
}
return result.Result, false, nil
}
func (c *GenericClient) SubmitSolution(job AuxiliaryJob, blob []byte, proof crypto.MerkleProof) (status string, err error) {
data, err := utils.MarshalJSON(RPCJSON{
JSONRPC: "2.0",
Id: "0",
Method: "merge_mining_submit_solution",
Params: MergeMiningSubmitSolutionJSON{
AuxiliaryBlob: job.Blob,
AuxiliaryHash: job.Hash,
Blob: blob,
MerkleProof: proof,
},
})
if err != nil {
return "", err
}
response, err := c.Client.Do(&http.Request{
Method: "POST",
URL: c.Address,
Header: http.Header{
"Content-Type": []string{"application/json-rpc"},
},
Body: io.NopCloser(bytes.NewBuffer(data)),
ContentLength: int64(len(data)),
})
if err != nil {
return "", err
}
defer io.ReadAll(response.Body)
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code: %d", response.StatusCode)
}
resultJSON, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
var result MergeMiningSubmitSolutionResult
if err := utils.UnmarshalJSON(resultJSON, &result); err != nil {
return "", err
}
if result.Error != "" {
return "", errors.New(result.Error)
}
return result.Result.Status, nil
}