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 {