Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions docs/json_rpc_api.mediawiki
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
|<code>(json array of string)</code> hex-encoded mixing pair request messages.
|<code>(json object)</code>
: <code>epoch</code>: <code>(numeric)</code> Duration between mix epochs, in seconds.
: <code>nextepoch</code>: <code>(numeric)</code> Unix timestamp of the next mix epoch.
: <code>pairings</code>: <code>(json array of object)</code> Pending pair requests grouped by mixing compatibility.
:: <code>mixamount</code>: <code>(numeric)</code> Amount of each mixed output, in DCR.
:: <code>scriptclass</code>: <code>(string)</code> Script class of the mixed outputs.
:: <code>txversion</code>: <code>(numeric)</code> Transaction version of the mix transaction.
:: <code>locktime</code>: <code>(numeric)</code> Lock time of the mix transaction.
:: <code>pairingflags</code>: <code>(numeric)</code> Pairing flags of the pair request.
:: <code>pairrequests</code>: <code>(json array of object)</code> The pair requests matching these mixing parameters.
::: <code>hash</code>: <code>(string)</code> Hash of the pair request message.
::: <code>identity</code>: <code>(string)</code> Participant ephemeral public key identity as a hex string.
::: <code>messagecount</code>: <code>(numeric)</code> Number of mixed outputs, each of value mixamount, the pair request is creating.
::: <code>inputvalue</code>: <code>(numeric)</code> Total value of inputs contributed by the pair request, in DCR.
::: <code>utxos</code>: <code>(json array of string)</code> Unspent transaction outputs contributed by the pair request, each as a "hash:index:tree" string.
::: <code>expiry</code>: <code>(numeric)</code> Block height at which the pair request expires.
|-
!Example Return
|<code>["a1d1d8c6ffc20a51ad9a5aa093c31bdeb06a0f627af95...",...]</code>
|<code>{"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}]}]}</code>
|}

----
Expand Down
3 changes: 3 additions & 0 deletions internal/rpcserver/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 58 additions & 17 deletions internal/rpcserver/rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ import (

// API version constants.
const (
jsonrpcSemverMajor = 8
jsonrpcSemverMinor = 3
jsonrpcSemverMajor = 9
jsonrpcSemverMinor = 0
jsonrpcSemverPatch = 0
)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -380,7 +380,7 @@ var rpcLimited = map[string]struct{}{
"getheaders": {},
"getinfo": {},
"getmixmessage": {},
"getmixpairrequests": {},
"getmixpoolinfo": {},
"getnettotals": {},
"getnetworkhashps": {},
"getnetworkinfo": {},
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally rather see this, at the very least, match types.Vin.ScriptSig so it has the actual script hex and decoded asm. Particularly because, and I hadn't noticed this until now, the string the wire message uses is inconsistent with the naming used by stdscript and isn't really descriptive enough either. P2PKH-secp256k1-v0 doesn't tell you one of the most important parts -- the signature scheme (ecdsa). Notice stdscript calls them p2pkh-ecdsa-secp256k1 and p2pkh-schnorr-secp256k1 specifically to differentiate.

More generally, I think maybe even the entire types.Vin struct would make sense for each utxo. There are some cases that would obviously not apply (e.g. coinbase, stakebase, treasurybase, treasuryspend), but it has a bunch of details about the referenced utxo like its transaction hash, output index, tree, amountin, blockheight, blockindex, scriptSig.asm, and scriptSig.hex.

That would also make it more consistent with getrawtransaction, decoderawtransaction, etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be uncovering a wider bug because that string representation is not only used in the wire message but also in the compatibility matching (CompatiblePRs matches using Pairing).

Given that the Pairing.ScriptClass field of the new RPC response is intended to describe the values used for compatibility matching, and not the individual UTXOs necessarily, I believe it is correct as-is.

In order to give more detail to the returned UTXOs we could include the values from wire.MixPairReqUTXO quite easily?

TxVersion: pr.TxVersion,
LockTime: pr.LockTime,
PairingFlags: pr.PairingFlags,
Comment thread
jholdstock marked this conversation as resolved.
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.
Expand Down
103 changes: 103 additions & 0 deletions internal/rpcserver/rpcserverhandlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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()

Expand Down
31 changes: 26 additions & 5 deletions internal/rpcserver/rpcserverhelp.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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)},
Expand Down
14 changes: 7 additions & 7 deletions rpc/jsonrpc/types/chainsvrcmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Loading