From 37feb92cde9a9dcfbeb0fe91e855583fe305642d Mon Sep 17 00:00:00 2001 From: jholdstock Date: Fri, 3 Jul 2026 14:52:37 +0800 Subject: [PATCH 1/3] rpcserver: Bump version to 9.0.0. This bumps the major version of the JSON RPC server in anticipation of upcoming changes. --- internal/rpcserver/rpcserver.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go index 5a3013bc18..af1d6e1c03 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 ) From 348da73101f65d18b72d8643455eeea44361620c Mon Sep 17 00:00:00 2001 From: jholdstock Date: Thu, 2 Jul 2026 15:30:47 +0800 Subject: [PATCH 2/3] multi: Remove GetMixPairRequests RPC. The GetMixPairRequests RPC, which was already explicitly flagged experimental/do-not-use, simply returned a raw hex-dump of pair requests, which was at odds with the rest of the RPC calls which typically return nicely structured JSON. --- docs/json_rpc_api.mediawiki | 26 -------------------------- internal/rpcserver/rpcserver.go | 25 ------------------------- internal/rpcserver/rpcserverhelp.go | 7 +------ rpc/jsonrpc/types/chainsvrcmds.go | 10 ---------- rpc/jsonrpc/types/chainsvrcmds_test.go | 11 ----------- 5 files changed, 1 insertion(+), 78 deletions(-) diff --git a/docs/json_rpc_api.mediawiki b/docs/json_rpc_api.mediawiki index f545a37ae3..fa0e24e036 100644 --- a/docs/json_rpc_api.mediawiki +++ b/docs/json_rpc_api.mediawiki @@ -272,10 +272,6 @@ 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]] -|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! -|- |[[#getnettotals|getnettotals]] |Y |Returns a JSON object containing network traffic statistics. @@ -1549,28 +1545,6 @@ of the best block. ---- -====getmixpairrequests==== -{| -!Method -|getmixpairrequests -|- -!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 -|(json array of string) hex-encoded mixing pair request messages. -|- -!Example Return -|["a1d1d8c6ffc20a51ad9a5aa093c31bdeb06a0f627af95...",...] -|} - ----- - ====getnettotals==== {| !Method diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go index af1d6e1c03..046f67b63e 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -210,7 +210,6 @@ var rpcHandlersBeforeInit = map[types.Method]commandHandler{ "getmempoolinfo": handleGetMempoolInfo, "getmininginfo": handleGetMiningInfo, "getmixmessage": handleGetMixMessage, - "getmixpairrequests": handleGetMixPairRequests, "getnettotals": handleGetNetTotals, "getnetworkhashps": handleGetNetworkHashPS, "getnetworkinfo": handleGetNetworkInfo, @@ -380,7 +379,6 @@ var rpcLimited = map[string]struct{}{ "getheaders": {}, "getinfo": {}, "getmixmessage": {}, - "getmixpairrequests": {}, "getnettotals": {}, "getnetworkhashps": {}, "getnetworkinfo": {}, @@ -2666,29 +2664,6 @@ 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) { - mp := s.cfg.MixPooler - - prs := mp.MixPRs() - - buf := new(strings.Builder) - res := make([]string, 0, len(prs)) - - const pver = wire.MixVersion - for _, pr := range prs { - err := pr.BtcEncode(hex.NewEncoder(buf), pver) - if err != nil { - return nil, err - } - res = append(res, buf.String()) - buf.Reset() - } - - return res, nil -} - // handleGetNetTotals implements the getnettotals command. func handleGetNetTotals(_ context.Context, s *Server, _ any) (any, error) { totalBytesRecv, totalBytesSent := s.cfg.ConnMgr.NetTotals() diff --git a/internal/rpcserver/rpcserverhelp.go b/internal/rpcserver/rpcserverhelp.go index b3a1f9b8ad..636bc2f575 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,10 +522,6 @@ 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.", - // GetNetworkHashPSCmd help. "getnetworkhashps--synopsis": "Returns the estimated network hashes per second for the block heights provided by the parameters.", "getnetworkhashps-blocks": "The number of blocks or -1 for the default number of blocks", @@ -993,7 +989,6 @@ var rpcResultTypes = map[types.Method][]any{ "getmempoolinfo": {(*types.GetMempoolInfoResult)(nil)}, "getmininginfo": {(*types.GetMiningInfoResult)(nil)}, "getmixmessage": {(*types.GetMixMessageResult)(nil)}, - "getmixpairrequests": {(*[]string)(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..c3209f3cd9 100644 --- a/rpc/jsonrpc/types/chainsvrcmds.go +++ b/rpc/jsonrpc/types/chainsvrcmds.go @@ -565,15 +565,6 @@ func NewGetMixMessageCmd(hash string) *GetMixMessageCmd { } } -// GetMixPairRequestsCmd defines the getmixpairrequests JSON-RPC command. -type GetMixPairRequestsCmd struct{} - -// NewGetMixPairRequestsCmd returns a new instance which can be used to issue a -// getmixpairrequests JSON-RPC command. -func NewGetMixPairRequestsCmd() *GetMixPairRequestsCmd { - return &GetMixPairRequestsCmd{} -} - // GetNetworkInfoCmd defines the getnetworkinfo JSON-RPC command. type GetNetworkInfoCmd struct{} @@ -1184,7 +1175,6 @@ 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("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..9df189fb7d 100644 --- a/rpc/jsonrpc/types/chainsvrcmds_test.go +++ b/rpc/jsonrpc/types/chainsvrcmds_test.go @@ -464,17 +464,6 @@ func TestChainSvrCmds(t *testing.T) { marshalled: `{"jsonrpc":"1.0","method":"getmixmessage","params":["123"],"id":1}`, unmarshalled: &GetMixMessageCmd{Hash: "123"}, }, - { - name: "getmixpairrequests", - newCmd: func() (interface{}, error) { - return dcrjson.NewCmd(Method("getmixpairrequests")) - }, - staticCmd: func() interface{} { - return NewGetMixPairRequestsCmd() - }, - marshalled: `{"jsonrpc":"1.0","method":"getmixpairrequests","params":[],"id":1}`, - unmarshalled: &GetMixPairRequestsCmd{}, - }, { name: "getnetworkinfo", newCmd: func() (interface{}, error) { From 63fe84823f138274ec8f73259c78264203a67e2e Mon Sep 17 00:00:00 2001 From: jholdstock Date: Tue, 28 Jul 2026 11:36:20 +0800 Subject: [PATCH 3/3] multi: Add GetMixpoolInfo RPC. This adds a new RPC named GetMixpoolInfo which returns the current state of the mixpool including timing of the next mix epoch and pending pair requests. PRs are grouped by mixing compatibility, i.e. if they share a common MixAmount, ScriptClass, TxVersion, LockTime, and PairingFlags. Available to limited users because it only retrieves public information and performs no sensitive actions. --- docs/json_rpc_api.mediawiki | 52 +++++ internal/rpcserver/interface.go | 3 + internal/rpcserver/rpcserver.go | 139 +++++++++++--- internal/rpcserver/rpcserverhandlers_test.go | 188 +++++++++++++++++++ internal/rpcserver/rpcserverhelp.go | 34 ++++ rpc/jsonrpc/types/chainsvrcmds.go | 10 + rpc/jsonrpc/types/chainsvrcmds_test.go | 11 ++ rpc/jsonrpc/types/chainsvrresults.go | 42 ++++- 8 files changed, 453 insertions(+), 26 deletions(-) diff --git a/docs/json_rpc_api.mediawiki b/docs/json_rpc_api.mediawiki index fa0e24e036..8c1a1784f2 100644 --- a/docs/json_rpc_api.mediawiki +++ b/docs/json_rpc_api.mediawiki @@ -272,6 +272,10 @@ 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. |- +|[[#getmixpoolinfo|getmixpoolinfo]] +|Y +|Returns the current state of the mixpool including timing of the next mix epoch and pending pair requests. +|- |[[#getnettotals|getnettotals]] |Y |Returns a JSON object containing network traffic statistics. @@ -1545,6 +1549,54 @@ of the best block. ---- +====getmixpoolinfo==== +{| +!Method +|getmixpoolinfo +|- +!Parameters +|None +|- +!Description +| +: Returns the current state of the mixpool including timing of the next mix epoch and pending pair requests. +|- +!Returns +|(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 object) Unspent transaction outputs contributed by the pair request. +:::: txid: (string) The hash of the origin transaction. +:::: vout: (numeric) The index of the output being contributed. +:::: tree: (numeric) The tree of the output being contributed. +:::: amountin: (numeric) The amount of the output, in DCR. +:::: blockheight: (numeric) The height of the block containing the origin transaction. +:::: scriptPubKey: (json object) The public key script used to pay coins. +::::: asm: (string) Disassembly of the script. +::::: hex: (string) Hex-encoded bytes of the script. +::::: reqSigs: (numeric) The number of required signatures. +::::: type: (string) The type of the script (e.g. 'pubkeyhash'). +::::: addresses: (json array of string) The Decred addresses associated with this output. +::::: version: (numeric) The script version. +::: expiry: (numeric) Block height at which the pair request expires. +|- +!Example Return +|{"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":[{"txid":"a1d1d8c6...","vout":0,"tree":0,"amountin":1.2,"blockheight":987000,"scriptPubKey":{"asm":"OP_DUP OP_HASH160 2da3aaa402b110247f08c3ea2300a0567de77a5b OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9142da3aaa402b110247f08c3ea2300a0567de77a5b88ac","reqSigs":1,"type":"pubkeyhash","addresses":["DsV8DxpEUFyZtcAwpWfejmWRpR5FEsrM6Yo"],"version":0}}],"expiry":987654}]}]} +|} + +---- + ====getnettotals==== {| !Method 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 046f67b63e..e7fdf97067 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -210,6 +210,7 @@ var rpcHandlersBeforeInit = map[types.Method]commandHandler{ "getmempoolinfo": handleGetMempoolInfo, "getmininginfo": handleGetMiningInfo, "getmixmessage": handleGetMixMessage, + "getmixpoolinfo": handleGetMixpoolInfo, "getnettotals": handleGetNetTotals, "getnetworkhashps": handleGetNetworkHashPS, "getnetworkinfo": handleGetNetworkInfo, @@ -379,6 +380,7 @@ var rpcLimited = map[string]struct{}{ "getheaders": {}, "getinfo": {}, "getmixmessage": {}, + "getmixpoolinfo": {}, "getnettotals": {}, "getnetworkhashps": {}, "getnetworkinfo": {}, @@ -1264,6 +1266,38 @@ func createVinList(mtx *wire.MsgTx, isTreasuryEnabled bool) []types.Vin { return vinList } +// createScriptPubKeyResult returns a JSON object describing the passed public +// key script information. +func createScriptPubKeyResult(scriptVersion uint16, pkScript []byte, + chainParams *chaincfg.Params) types.ScriptPubKeyResult { + + // Disassemble script into single line printable format. The disassembled + // string will contain [error] inline if the script doesn't fully parse, so + // ignore the error here. + script := pkScript + disbuf, _ := txscript.DisasmString(script) + + // Attempt to extract known addresses associated with the script. + scriptType, addrs := stdscript.ExtractAddrs(scriptVersion, script, + chainParams) + addresses := make([]string, len(addrs)) + for i, addr := range addrs { + addresses[i] = addr.String() + } + + // Determine the number of required signatures for known standard types. + reqSigs := stdscript.DetermineRequiredSigs(scriptVersion, script) + + return types.ScriptPubKeyResult{ + Asm: disbuf, + Hex: hex.EncodeToString(script), + ReqSigs: int32(reqSigs), + Type: scriptType.String(), + Addresses: addresses, + Version: scriptVersion, + } +} + // createVoutList returns a slice of JSON objects for the outputs of the passed // transaction. func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params, @@ -2664,6 +2698,84 @@ func handleGetMixMessage(_ context.Context, s *Server, cmd any) (any, error) { return &result, nil } +// 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() + + // 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 { + pairing, err := pr.Pairing() + if err != nil { + return nil, rpcInternalErr(err, "Failed to generate PR pairing") + } + + 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, min(len(prs), mixing.MaxPeers)), + } + groups[key] = group + } + + // The pair request only provides the hash, index and tree of the UTXO, + // so amountIn, blockHeight and scriptPubKey are retrieved from the + // local UTXO set. + utxos := make([]types.PairRequestUTXO, len(pr.UTXOs)) + for i := range pr.UTXOs { + op := pr.UTXOs[i].OutPoint + + entry, err := s.cfg.Chain.FetchUtxoEntry(op) + if entry == nil || err != nil { + return nil, rpcNoTxInfoError(&op.Hash) + } + + utxos[i].Txid = op.Hash.String() + utxos[i].Vout = op.Index + utxos[i].Tree = op.Tree + utxos[i].AmountIn = dcrutil.Amount(entry.Amount()).ToCoin() + utxos[i].BlockHeight = uint32(entry.BlockHeight()) + utxos[i].ScriptPubKey = createScriptPubKeyResult( + entry.ScriptVersion(), entry.PkScript(), s.cfg.ChainParams) + } + + 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, + }) + } + + // 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. func handleGetNetTotals(_ context.Context, s *Server, _ any) (any, error) { totalBytesRecv, totalBytesSent := s.cfg.ConnMgr.NetTotals() @@ -3727,35 +3839,12 @@ func handleGetTxOut(_ context.Context, s *Server, cmd any) (any, error) { isCoinbase = entry.IsCoinBase() } - // Disassemble script into single line printable format. The - // disassembled string will contain [error] inline if the script - // doesn't fully parse, so ignore the error here. - script := pkScript - disbuf, _ := txscript.DisasmString(script) - - // Attempt to extract known addresses associated with the script. - scriptType, addrs := stdscript.ExtractAddrs(scriptVersion, script, - s.cfg.ChainParams) - addresses := make([]string, len(addrs)) - for i, addr := range addrs { - addresses[i] = addr.String() - } - - // Determine the number of required signatures for known standard types. - reqSigs := stdscript.DetermineRequiredSigs(scriptVersion, script) - txOutReply := &types.GetTxOutResult{ BestBlock: bestBlockHash, Confirmations: confirmations, Value: dcrutil.Amount(value).ToUnit(dcrutil.AmountCoin), - ScriptPubKey: types.ScriptPubKeyResult{ - Asm: disbuf, - Hex: hex.EncodeToString(pkScript), - ReqSigs: int32(reqSigs), - Type: scriptType.String(), - Addresses: addresses, - Version: scriptVersion, - }, + ScriptPubKey: createScriptPubKeyResult(scriptVersion, pkScript, + s.cfg.ChainParams), Coinbase: isCoinbase, } return txOutReply, nil diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go index fbd8716fee..e26e90b197 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,188 @@ func TestHandleGetMempoolInfo(t *testing.T) { }}) } +func TestHandleGetMixpoolInfo(t *testing.T) { + t.Parallel() + + epoch := 10 * time.Minute + now := time.Unix(1700000000, 0) + + // Construct the mock UtxoEntry which will be returned by FetchUtxoEntry. + script := hexToBytes("76a9142da3aaa402b110247f08c3ea2300a0567de77a5b88ac") + const scriptVersion = uint16(0) + mockUtxo := &testRPCUtxoEntry{ + amount: 500000000, + height: 12345, + pkScript: script, + scriptVersion: scriptVersion, + } + + // Construct the representation of the script expected to be returned by the + // RPC. + disbuf, _ := txscript.DisasmString(script) + scriptType, addrs := stdscript.ExtractAddrs(scriptVersion, script, + defaultChainParams) + addresses := make([]string, len(addrs)) + for i, addr := range addrs { + addresses[i] = addr.String() + } + reqSigs := stdscript.DetermineRequiredSigs(scriptVersion, script) + scriptPubKey := types.ScriptPubKeyResult{ + Asm: disbuf, + Hex: hex.EncodeToString(script), + ReqSigs: int32(reqSigs), + Type: scriptType.String(), + Addresses: addresses, + Version: scriptVersion, + } + + // Construct two UTXOs for use in the pair requests, and the representation + // of them expected to be returned by the RPC. + utxo1 := wire.OutPoint{Hash: chainhash.Hash{0x01}, Index: 2, Tree: 0} + utxo1Result := types.PairRequestUTXO{ + Txid: "0000000000000000000000000000000000000000000000000000000000000001", + Vout: 2, + Tree: 0, + AmountIn: 5.00, + BlockHeight: 12345, + ScriptPubKey: scriptPubKey, + } + + utxo2 := wire.OutPoint{Hash: chainhash.Hash{0x02}, Index: 3, Tree: 1} + utxo2Result := types.PairRequestUTXO{ + Txid: "0000000000000000000000000000000000000000000000000000000000000002", + Vout: 3, + Tree: 1, + AmountIn: 5.00, + BlockHeight: 12345, + ScriptPubKey: scriptPubKey, + } + + // 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}, + mockChain: func() *testRPCChain { + chain := defaultMockRPCChain() + chain.fetchUtxoEntry = mockUtxo + return chain + }(), + 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: []types.PairRequestUTXO{utxo1Result}, + Expiry: 100, + }, { + Hash: pr2.Hash().String(), + Identity: hex.EncodeToString(pr2.Identity[:]), + MessageCount: 1, + InputValue: 0.015, + UTXOs: []types.PairRequestUTXO{utxo2Result}, + Expiry: 200, + }}, + }}, + }, + }, { + name: "handleGetMixpoolInfo: utxo not found in the utxo set", + handler: handleGetMixpoolInfo, + cmd: &types.GetMixpoolInfoCmd{}, + mockClock: &testClock{now: now}, + mockChain: func() *testRPCChain { + chain := defaultMockRPCChain() + chain.fetchUtxoEntry = nil + return chain + }(), + mockMixPooler: func() *testMixPooler { + mp := defaultMockMixPooler() + mp.epoch = epoch + mp.mixPRs = []*wire.MsgMixPairReq{pr1} + return mp + }(), + wantErr: true, + errCode: dcrjson.ErrRPCNoTxInfo, + }, { + name: "handleGetMixpoolInfo: utxo set lookup failure", + handler: handleGetMixpoolInfo, + cmd: &types.GetMixpoolInfoCmd{}, + mockClock: &testClock{now: now}, + mockChain: func() *testRPCChain { + chain := defaultMockRPCChain() + chain.fetchUtxoEntryErr = errors.New("utxo entry error") + return chain + }(), + mockMixPooler: func() *testMixPooler { + mp := defaultMockMixPooler() + mp.epoch = epoch + mp.mixPRs = []*wire.MsgMixPairReq{pr1} + return mp + }(), + wantErr: true, + errCode: dcrjson.ErrRPCNoTxInfo, + }}) +} + func TestHandleGetMiningInfo(t *testing.T) { t.Parallel() diff --git a/internal/rpcserver/rpcserverhelp.go b/internal/rpcserver/rpcserverhelp.go index 636bc2f575..0925408323 100644 --- a/internal/rpcserver/rpcserverhelp.go +++ b/internal/rpcserver/rpcserverhelp.go @@ -522,6 +522,39 @@ var helpDescsEnUS = map[string]string{ "getmixmessageresult-type": "Command type of the message", "getmixmessageresult-message": "Serialized message in hex encoding", + // 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", + "pairrequest-expiry": "Block height at which the pair request expires", + + // PairRequestUTXO help. + "pairrequestutxo-txid": "The hash of the origin transaction", + "pairrequestutxo-vout": "The index of the output being contributed", + "pairrequestutxo-tree": "The tree of the output being contributed", + "pairrequestutxo-amountin": "The amount of the output, in DCR", + "pairrequestutxo-blockheight": "The height of the block containing the origin transaction", + "pairrequestutxo-scriptPubKey": "The public key script used to pay coins as a JSON object", + // GetNetworkHashPSCmd help. "getnetworkhashps--synopsis": "Returns the estimated network hashes per second for the block heights provided by the parameters.", "getnetworkhashps-blocks": "The number of blocks or -1 for the default number of blocks", @@ -989,6 +1022,7 @@ var rpcResultTypes = map[types.Method][]any{ "getmempoolinfo": {(*types.GetMempoolInfoResult)(nil)}, "getmininginfo": {(*types.GetMiningInfoResult)(nil)}, "getmixmessage": {(*types.GetMixMessageResult)(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 c3209f3cd9..e4bfc25ee3 100644 --- a/rpc/jsonrpc/types/chainsvrcmds.go +++ b/rpc/jsonrpc/types/chainsvrcmds.go @@ -565,6 +565,15 @@ func NewGetMixMessageCmd(hash string) *GetMixMessageCmd { } } +// GetMixpoolInfoCmd defines the getmixpoolinfo JSON-RPC command. +type GetMixpoolInfoCmd struct{} + +// 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. type GetNetworkInfoCmd struct{} @@ -1175,6 +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("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 9df189fb7d..36af5a3dc7 100644 --- a/rpc/jsonrpc/types/chainsvrcmds_test.go +++ b/rpc/jsonrpc/types/chainsvrcmds_test.go @@ -464,6 +464,17 @@ func TestChainSvrCmds(t *testing.T) { marshalled: `{"jsonrpc":"1.0","method":"getmixmessage","params":["123"],"id":1}`, unmarshalled: &GetMixMessageCmd{Hash: "123"}, }, + { + name: "getmixpoolinfo", + newCmd: func() (interface{}, error) { + return dcrjson.NewCmd(Method("getmixpoolinfo")) + }, + staticCmd: func() interface{} { + return NewGetMixpoolInfoCmd() + }, + marshalled: `{"jsonrpc":"1.0","method":"getmixpoolinfo","params":[],"id":1}`, + unmarshalled: &GetMixpoolInfoCmd{}, + }, { name: "getnetworkinfo", newCmd: func() (interface{}, error) { diff --git a/rpc/jsonrpc/types/chainsvrresults.go b/rpc/jsonrpc/types/chainsvrresults.go index d18f9a6804..10a2d39355 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,46 @@ 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 []PairRequestUTXO `json:"utxos"` + Expiry uint32 `json:"expiry"` +} + +// PairRequestUTXO models an unspent transaction output contributed to a mixing +// pair request. +type PairRequestUTXO struct { + Txid string `json:"txid"` + Vout uint32 `json:"vout"` + Tree int8 `json:"tree"` + AmountIn float64 `json:"amountin"` + BlockHeight uint32 `json:"blockheight"` + ScriptPubKey ScriptPubKeyResult `json:"scriptPubKey"` +} + // LocalAddressesResult models the localaddresses data from the getnetworkinfo // command. type LocalAddressesResult struct {