diff --git a/docs/json_rpc_api.mediawiki b/docs/json_rpc_api.mediawiki index f545a37ae3..8af7012a00 100644 --- a/docs/json_rpc_api.mediawiki +++ b/docs/json_rpc_api.mediawiki @@ -272,9 +272,9 @@ the method name for further details such as parameter and return information. |Y |Returns a mix message and its message type by its hash if it is accepted by and currently in the mixpool. |- -|[[#getmixpairrequests|getmixpairrequests]] +|[[#getmixpoolinfo|getmixpoolinfo]] |Y -|Returns the current set of mixing pair request messages from the mixpool. WARNING: This is experimental and will very likely be removed in the next version. Do not use! +|Returns the current state of the mixpool including timing of the next mix epoch and pending pair requests. |- |[[#getnettotals|getnettotals]] |Y @@ -1549,24 +1549,38 @@ of the best block. ---- -====getmixpairrequests==== +====getmixpoolinfo==== {| !Method -|getmixpairrequests +|getmixpoolinfo |- !Parameters |None |- !Description | -: Returns the current set of mixing pair request messages from the mixpool. -: WARNING: This is experimental and will very likely be removed in the next version. Do not use! +: Returns the current state of the mixpool including timing of the next mix epoch and pending pair requests. |- !Returns -|(json array of string) hex-encoded mixing pair request messages. +|(json object) +: epoch: (numeric) Duration between mix epochs, in seconds. +: nextepoch: (numeric) Unix timestamp of the next mix epoch. +: pairings: (json array of object) Pending pair requests grouped by mixing compatibility. +:: mixamount: (numeric) Amount of each mixed output, in DCR. +:: scriptclass: (string) Script class of the mixed outputs. +:: txversion: (numeric) Transaction version of the mix transaction. +:: locktime: (numeric) Lock time of the mix transaction. +:: pairingflags: (numeric) Pairing flags of the pair request. +:: pairrequests: (json array of object) The pair requests matching these mixing parameters. +::: hash: (string) Hash of the pair request message. +::: identity: (string) Participant ephemeral public key identity as a hex string. +::: messagecount: (numeric) Number of mixed outputs, each of value mixamount, the pair request is creating. +::: inputvalue: (numeric) Total value of inputs contributed by the pair request, in DCR. +::: utxos: (json array of string) Unspent transaction outputs contributed by the pair request, each as a "hash:index:tree" string. +::: expiry: (numeric) Block height at which the pair request expires. |- !Example Return -|["a1d1d8c6ffc20a51ad9a5aa093c31bdeb06a0f627af95...",...] +|{"epoch":600,"nextepoch":1751049600,"pairings":[{"mixamount":1,"scriptclass":"P2PKH-secp256k1-v0","txversion":1,"locktime":0,"pairingflags":1,"pairrequests":[{"hash":"a1d1d8c6...","identity":"02d1a...","messagecount":1,"inputvalue":1.2,"utxos":["a1d1d8c6...:0:0"],"expiry":987654}]}]} |} ---- diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go index 64c9443b84..0db36e6ce6 100644 --- a/internal/rpcserver/interface.go +++ b/internal/rpcserver/interface.go @@ -665,6 +665,9 @@ type MixPooler interface { // Message searches the mixing pool for a message by its hash. Message(query *chainhash.Hash) (mixing.Message, error) + + // Epoch returns the duration between mix epochs. + Epoch() time.Duration } // TxIndexer provides an interface for retrieving details for a given diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go index 295f66599b..33fe809b50 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -62,8 +62,8 @@ import ( // API version constants. const ( - jsonrpcSemverMajor = 8 - jsonrpcSemverMinor = 3 + jsonrpcSemverMajor = 9 + jsonrpcSemverMinor = 0 jsonrpcSemverPatch = 0 ) @@ -210,7 +210,7 @@ var rpcHandlersBeforeInit = map[types.Method]commandHandler{ "getmempoolinfo": handleGetMempoolInfo, "getmininginfo": handleGetMiningInfo, "getmixmessage": handleGetMixMessage, - "getmixpairrequests": handleGetMixPairRequests, + "getmixpoolinfo": handleGetMixpoolInfo, "getnettotals": handleGetNetTotals, "getnetworkhashps": handleGetNetworkHashPS, "getnetworkinfo": handleGetNetworkInfo, @@ -380,7 +380,7 @@ var rpcLimited = map[string]struct{}{ "getheaders": {}, "getinfo": {}, "getmixmessage": {}, - "getmixpairrequests": {}, + "getmixpoolinfo": {}, "getnettotals": {}, "getnetworkhashps": {}, "getnetworkinfo": {}, @@ -2666,27 +2666,68 @@ func handleGetMixMessage(_ context.Context, s *Server, cmd any) (any, error) { return &result, nil } -// handleGetMixPairRequests implements the getmixpairrequests command, -// returning all current mixing pair requests messages from mixpool. -func handleGetMixPairRequests(_ context.Context, s *Server, _ any) (any, error) { +// handleGetMixpoolInfo implements the getmixpoolinfo command, returning the +// timing of the next mix epoch and the pending pair requests grouped by pairing +// description. +func handleGetMixpoolInfo(_ context.Context, s *Server, _ any) (any, error) { mp := s.cfg.MixPooler - prs := mp.MixPRs() - buf := new(strings.Builder) - res := make([]string, 0, len(prs)) - - const pver = wire.MixVersion + // Use a map to group PRs by their pairing description. This is converted to + // a slice for JSON marshalling later. + groups := make(map[string]*types.Pairing) for _, pr := range prs { - err := pr.BtcEncode(hex.NewEncoder(buf), pver) + pairing, err := pr.Pairing() if err != nil { - return nil, err + return nil, rpcInternalErr(err, "Failed to generate PR pairing") } - res = append(res, buf.String()) - buf.Reset() + + key := string(pairing) + group, ok := groups[key] + if !ok { + group = &types.Pairing{ + MixAmount: dcrutil.Amount(pr.MixAmount).ToCoin(), + ScriptClass: pr.ScriptClass, + TxVersion: pr.TxVersion, + LockTime: pr.LockTime, + PairingFlags: pr.PairingFlags, + PairRequests: make([]types.PairRequest, 0, minInt(len(prs), mixing.MaxPeers)), + } + groups[key] = group + } + + // Get string representation of the UTXOs in this PR ("hash:idx:tree"). + utxos := make([]string, len(pr.UTXOs)) + for i := range pr.UTXOs { + op := pr.UTXOs[i].OutPoint + utxos[i] = fmt.Sprintf("%s:%d:%d", op.Hash, op.Index, op.Tree) + } + + group.PairRequests = append(group.PairRequests, types.PairRequest{ + Hash: pr.Hash().String(), + Identity: hex.EncodeToString(pr.Identity[:]), + MessageCount: pr.MessageCount, + InputValue: dcrutil.Amount(pr.InputValue).ToCoin(), + UTXOs: utxos, + Expiry: pr.Expiry, + }) } - return res, nil + // Convert map to slice for JSON marshalling. + pairings := make([]types.Pairing, 0, len(groups)) + for _, group := range groups { + pairings = append(pairings, *group) + } + + epoch := mp.Epoch() + nextEpoch := s.cfg.Clock.Now().Truncate(epoch).Add(epoch) + + result := types.GetMixpoolInfoResult{ + Epoch: int64(epoch.Seconds()), + NextEpoch: nextEpoch.Unix(), + Pairings: pairings, + } + return &result, nil } // handleGetNetTotals implements the getnettotals command. diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go index d86b0f5372..384271309a 100644 --- a/internal/rpcserver/rpcserverhandlers_test.go +++ b/internal/rpcserver/rpcserverhandlers_test.go @@ -1184,6 +1184,7 @@ type testMixPooler struct { mixPRs []*wire.MsgMixPairReq message mixing.Message messageErr error + epoch time.Duration } // MixPRs returns a mocked slice of MixPR messages. @@ -1196,6 +1197,11 @@ func (mp *testMixPooler) Message(query *chainhash.Hash) (mixing.Message, error) return mp.message, mp.messageErr } +// Epoch returns a mocked duration between mix epochs. +func (mp *testMixPooler) Epoch() time.Duration { + return mp.epoch +} + // testNtfnManager provides a mock notification manager by implementing the // NtfnManager interface. type testNtfnManager struct { @@ -4606,6 +4612,103 @@ func TestHandleGetMempoolInfo(t *testing.T) { }}) } +func TestHandleGetMixpoolInfo(t *testing.T) { + t.Parallel() + + epoch := 10 * time.Minute + now := time.Unix(1700000000, 0) + + // Construct two UTXOs for use in the pair requests and the string + // representation of them expected to be returned by the RPC. + utxo1 := wire.OutPoint{Hash: chainhash.Hash{0x01}, Index: 2, Tree: 0} + utxo1Str := "0000000000000000000000000000000000000000000000000000000000000001:2:0" + utxo2 := wire.OutPoint{Hash: chainhash.Hash{0x02}, Index: 3, Tree: 1} + utxo2Str := "0000000000000000000000000000000000000000000000000000000000000002:3:1" + + // Construct two pair requests each contributing a distinct UTXO, and + // sharing the same pairing description so they are grouped together. + pr1 := &wire.MsgMixPairReq{ + Identity: [33]byte{0x02, 0xaa}, + Expiry: 100, + MixAmount: 1000000, + ScriptClass: string(mixing.ScriptClassP2PKHv0), + TxVersion: 1, + MessageCount: 2, + InputValue: 2500000, + UTXOs: []wire.MixPairReqUTXO{{OutPoint: utxo1}}, + PairingFlags: 2, + } + pr2 := &wire.MsgMixPairReq{ + Identity: [33]byte{0x03, 0xbb}, + Expiry: 200, + MixAmount: 1000000, + ScriptClass: string(mixing.ScriptClassP2PKHv0), + TxVersion: 1, + MessageCount: 1, + InputValue: 1500000, + UTXOs: []wire.MixPairReqUTXO{{OutPoint: utxo2}}, + PairingFlags: 2, + } + + hasher := blake256.NewHasher256() + pr1.WriteHash(hasher) + pr2.WriteHash(hasher) + + testRPCServerHandler(t, []rpcTest{{ + name: "handleGetMixpoolInfo: no pending pair requests", + handler: handleGetMixpoolInfo, + cmd: &types.GetMixpoolInfoCmd{}, + mockClock: &testClock{now: now}, + mockMixPooler: func() *testMixPooler { + mp := defaultMockMixPooler() + mp.epoch = epoch + return mp + }(), + result: &types.GetMixpoolInfoResult{ + Epoch: 600, // 10 minutes + NextEpoch: 1700000400, + Pairings: []types.Pairing{}, + }, + }, { + name: "handleGetMixpoolInfo: pending pair requests grouped by pairing", + handler: handleGetMixpoolInfo, + cmd: &types.GetMixpoolInfoCmd{}, + mockClock: &testClock{now: now}, + mockMixPooler: func() *testMixPooler { + mp := defaultMockMixPooler() + mp.epoch = epoch + mp.mixPRs = []*wire.MsgMixPairReq{pr1, pr2} + return mp + }(), + result: &types.GetMixpoolInfoResult{ + Epoch: 600, // 10 minutes + NextEpoch: 1700000400, + Pairings: []types.Pairing{{ + MixAmount: 0.01, + ScriptClass: string(mixing.ScriptClassP2PKHv0), + TxVersion: 1, + LockTime: 0, + PairingFlags: 2, + PairRequests: []types.PairRequest{{ + Hash: pr1.Hash().String(), + Identity: hex.EncodeToString(pr1.Identity[:]), + MessageCount: 2, + InputValue: 0.025, + UTXOs: []string{utxo1Str}, + Expiry: 100, + }, { + Hash: pr2.Hash().String(), + Identity: hex.EncodeToString(pr2.Identity[:]), + MessageCount: 1, + InputValue: 0.015, + UTXOs: []string{utxo2Str}, + Expiry: 200, + }}, + }}, + }, + }}) +} + func TestHandleGetMiningInfo(t *testing.T) { t.Parallel() diff --git a/internal/rpcserver/rpcserverhelp.go b/internal/rpcserver/rpcserverhelp.go index b3a1f9b8ad..df3b90af7b 100644 --- a/internal/rpcserver/rpcserverhelp.go +++ b/internal/rpcserver/rpcserverhelp.go @@ -1,5 +1,5 @@ // Copyright (c) 2015 The btcsuite developers -// Copyright (c) 2015-2025 The Decred developers +// Copyright (c) 2015-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -522,9 +522,30 @@ var helpDescsEnUS = map[string]string{ "getmixmessageresult-type": "Command type of the message", "getmixmessageresult-message": "Serialized message in hex encoding", - // GetMixPairRequests help. - "getmixpairrequests--synopsis": "Returns current set of mixing pair request messages from mixpool.", - "getmixpairrequests--result0": "JSON array of hex-encoded mixing pair request messages.", + // GetMixpoolInfo help. + "getmixpoolinfo--synopsis": "Returns the current state of the mixpool including timing of the next mix epoch and pending pair requests.", + "getmixpoolinfo--result0": "JSON object describing current mixpool state.", + + // GetMixpoolInfoResult help. + "getmixpoolinforesult-epoch": "Duration between mix epochs, in seconds", + "getmixpoolinforesult-nextepoch": "Unix timestamp of the next mix epoch", + "getmixpoolinforesult-pairings": "Pending pair requests grouped by mixing compatibility", + + // Pairing help. + "pairing-mixamount": "Amount of each mixed output, in DCR", + "pairing-scriptclass": "Script class of the mixed outputs", + "pairing-txversion": "Transaction version of the mix transaction", + "pairing-locktime": "Lock time of the mix transaction", + "pairing-pairingflags": "Pairing flags", + "pairing-pairrequests": "The pair requests matching these mixing parameters", + + // PairRequest help. + "pairrequest-hash": "Hash of the pair request message", + "pairrequest-identity": "Participant ephemeral public key identity as a hex string", + "pairrequest-messagecount": "Number of mixed outputs, each of value mixamount, the pair request is creating", + "pairrequest-inputvalue": "Total value of inputs contributed by the pair request, in DCR", + "pairrequest-utxos": "Unspent transaction outputs contributed by the pair request, each as a \"hash:index:tree\" string", + "pairrequest-expiry": "Block height at which the pair request expires", // GetNetworkHashPSCmd help. "getnetworkhashps--synopsis": "Returns the estimated network hashes per second for the block heights provided by the parameters.", @@ -993,7 +1014,7 @@ var rpcResultTypes = map[types.Method][]any{ "getmempoolinfo": {(*types.GetMempoolInfoResult)(nil)}, "getmininginfo": {(*types.GetMiningInfoResult)(nil)}, "getmixmessage": {(*types.GetMixMessageResult)(nil)}, - "getmixpairrequests": {(*[]string)(nil)}, + "getmixpoolinfo": {(*types.GetMixpoolInfoResult)(nil)}, "getnettotals": {(*types.GetNetTotalsResult)(nil)}, "getnetworkhashps": {(*int64)(nil)}, "getnetworkinfo": {(*[]types.GetNetworkInfoResult)(nil)}, diff --git a/rpc/jsonrpc/types/chainsvrcmds.go b/rpc/jsonrpc/types/chainsvrcmds.go index 276f939264..e4bfc25ee3 100644 --- a/rpc/jsonrpc/types/chainsvrcmds.go +++ b/rpc/jsonrpc/types/chainsvrcmds.go @@ -565,13 +565,13 @@ func NewGetMixMessageCmd(hash string) *GetMixMessageCmd { } } -// GetMixPairRequestsCmd defines the getmixpairrequests JSON-RPC command. -type GetMixPairRequestsCmd struct{} +// GetMixpoolInfoCmd defines the getmixpoolinfo JSON-RPC command. +type GetMixpoolInfoCmd struct{} -// NewGetMixPairRequestsCmd returns a new instance which can be used to issue a -// getmixpairrequests JSON-RPC command. -func NewGetMixPairRequestsCmd() *GetMixPairRequestsCmd { - return &GetMixPairRequestsCmd{} +// NewGetMixpoolInfoCmd returns a new instance which can be used to issue a +// getmixpoolinfo JSON-RPC command. +func NewGetMixpoolInfoCmd() *GetMixpoolInfoCmd { + return &GetMixpoolInfoCmd{} } // GetNetworkInfoCmd defines the getnetworkinfo JSON-RPC command. @@ -1184,7 +1184,7 @@ func init() { dcrjson.MustRegister(Method("getmempoolinfo"), (*GetMempoolInfoCmd)(nil), flags) dcrjson.MustRegister(Method("getmininginfo"), (*GetMiningInfoCmd)(nil), flags) dcrjson.MustRegister(Method("getmixmessage"), (*GetMixMessageCmd)(nil), flags) - dcrjson.MustRegister(Method("getmixpairrequests"), (*GetMixPairRequestsCmd)(nil), flags) + dcrjson.MustRegister(Method("getmixpoolinfo"), (*GetMixpoolInfoCmd)(nil), flags) dcrjson.MustRegister(Method("getnetworkinfo"), (*GetNetworkInfoCmd)(nil), flags) dcrjson.MustRegister(Method("getnettotals"), (*GetNetTotalsCmd)(nil), flags) dcrjson.MustRegister(Method("getnetworkhashps"), (*GetNetworkHashPSCmd)(nil), flags) diff --git a/rpc/jsonrpc/types/chainsvrcmds_test.go b/rpc/jsonrpc/types/chainsvrcmds_test.go index a33f15869c..36af5a3dc7 100644 --- a/rpc/jsonrpc/types/chainsvrcmds_test.go +++ b/rpc/jsonrpc/types/chainsvrcmds_test.go @@ -465,15 +465,15 @@ func TestChainSvrCmds(t *testing.T) { unmarshalled: &GetMixMessageCmd{Hash: "123"}, }, { - name: "getmixpairrequests", + name: "getmixpoolinfo", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd(Method("getmixpairrequests")) + return dcrjson.NewCmd(Method("getmixpoolinfo")) }, staticCmd: func() interface{} { - return NewGetMixPairRequestsCmd() + return NewGetMixpoolInfoCmd() }, - marshalled: `{"jsonrpc":"1.0","method":"getmixpairrequests","params":[],"id":1}`, - unmarshalled: &GetMixPairRequestsCmd{}, + marshalled: `{"jsonrpc":"1.0","method":"getmixpoolinfo","params":[],"id":1}`, + unmarshalled: &GetMixpoolInfoCmd{}, }, { name: "getnetworkinfo", diff --git a/rpc/jsonrpc/types/chainsvrresults.go b/rpc/jsonrpc/types/chainsvrresults.go index d18f9a6804..8a4c08e6b9 100644 --- a/rpc/jsonrpc/types/chainsvrresults.go +++ b/rpc/jsonrpc/types/chainsvrresults.go @@ -1,5 +1,5 @@ // Copyright (c) 2014 The btcsuite developers -// Copyright (c) 2015-2024 The Decred developers +// Copyright (c) 2015-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -240,6 +240,35 @@ type GetMixMessageResult struct { Message string `json:"message"` } +// GetMixpoolInfoResult models the data from the getmixpoolinfo command. +type GetMixpoolInfoResult struct { + Epoch int64 `json:"epoch"` + NextEpoch int64 `json:"nextepoch"` + Pairings []Pairing `json:"pairings"` +} + +// Pairing models a group of pair requests which share the same mix parameters +// (i.e. MixAmount, ScriptClass, TxVersion, LockTime, and PairingFlags) and are +// thus compatible to mix together. +type Pairing struct { + MixAmount float64 `json:"mixamount"` + ScriptClass string `json:"scriptclass"` + TxVersion uint16 `json:"txversion"` + LockTime uint32 `json:"locktime"` + PairingFlags uint8 `json:"pairingflags"` + PairRequests []PairRequest `json:"pairrequests"` +} + +// PairRequest models a single pair request. +type PairRequest struct { + Hash string `json:"hash"` + Identity string `json:"identity"` + MessageCount uint32 `json:"messagecount"` + InputValue float64 `json:"inputvalue"` + UTXOs []string `json:"utxos"` + Expiry uint32 `json:"expiry"` +} + // LocalAddressesResult models the localaddresses data from the getnetworkinfo // command. type LocalAddressesResult struct {