diff --git a/blockchain/stake/staketx.go b/blockchain/stake/staketx.go index 51444e26..7c10d7b7 100644 --- a/blockchain/stake/staketx.go +++ b/blockchain/stake/staketx.go @@ -176,6 +176,13 @@ var ( rangeLimitMax = uint16(63) ) +// VoteBits is a field representing the mandatory 2-byte field of voteBits along +// with the optional 73-byte extended field for votes. +type VoteBits struct { + Bits uint16 + ExtendedBits []byte +} + // -------------------------------------------------------------------------------- // Accessory Stake Functions // -------------------------------------------------------------------------------- diff --git a/dcrjson/dcrwalletextcmds.go b/dcrjson/dcrwalletextcmds.go index f3b58ccb..a2ec380e 100644 --- a/dcrjson/dcrwalletextcmds.go +++ b/dcrjson/dcrwalletextcmds.go @@ -569,6 +569,22 @@ func NewSetTicketVoteBitsCmd(txHash string, voteBits uint16, voteBitsExt *string } } +// SetTicketsVoteBitsCmd is a type handling custom marshaling and +// unmarshaling of setticketsvotebits JSON RPC commands. +type SetTicketsVoteBitsCmd struct { + TxHashes string + VoteBitsBytes string +} + +// NewSetTicketsVoteBitsCmd creates a new instance of the setticketsvotebits +// command. +func NewSetTicketsVoteBitsCmd(txHashes string, voteBitsBytes string) *SetTicketsVoteBitsCmd { + return &SetTicketsVoteBitsCmd{ + TxHashes: txHashes, + VoteBitsBytes: voteBitsBytes, + } +} + // SignRawTransactionsCmd defines the signrawtransactions JSON-RPC command. type SignRawTransactionsCmd struct { RawTxs []string @@ -651,6 +667,7 @@ func init() { MustRegisterCmd("setticketfee", (*SetTicketFeeCmd)(nil), flags) MustRegisterCmd("setticketmaxprice", (*SetTicketMaxPriceCmd)(nil), flags) MustRegisterCmd("setticketvotebits", (*SetTicketVoteBitsCmd)(nil), flags) + MustRegisterCmd("setticketsvotebits", (*SetTicketsVoteBitsCmd)(nil), flags) MustRegisterCmd("signrawtransactions", (*SignRawTransactionsCmd)(nil), flags) MustRegisterCmd("stakepooluserinfo", (*StakePoolUserInfoCmd)(nil), flags) MustRegisterCmd("walletinfo", (*WalletInfoCmd)(nil), flags) diff --git a/dcrjson/parse.go b/dcrjson/parse.go index 07bb8d68..61dae0a5 100644 --- a/dcrjson/parse.go +++ b/dcrjson/parse.go @@ -5,8 +5,11 @@ package dcrjson import ( + "encoding/binary" "encoding/hex" + "fmt" + "github.com/decred/dcrd/blockchain/stake" "github.com/decred/dcrd/chaincfg/chainhash" ) @@ -44,3 +47,87 @@ func DecodeConcatenatedHashes(hashes string) ([]chainhash.Hash, error) { } return decoded, nil } + +// EncodeConcatenatedVoteBits encodes a slice of VoteBits into a serialized byte +// slice. The entirety of the voteBits are encoded individually in series as +// follows: +// +// Size Description +// 1 byte Length of the concatenated voteBits in bytes +// 2 bytes Vote bits +// up to 73 bytes Extended vote bits +// +// The result may be concatenated into a slice and then passed to callers +func EncodeConcatenatedVoteBits(voteBitsSlice []stake.VoteBits) (string, error) { + length := 0 + for i := range voteBitsSlice { + if len(voteBitsSlice[i].ExtendedBits) > stake.MaxSingleBytePushLength-2 { + return "", fmt.Errorf("extended votebits too long (got %v, want "+ + "%v max", len(voteBitsSlice[i].ExtendedBits), + stake.MaxSingleBytePushLength-2) + } + + length += 1 + 2 + len(voteBitsSlice[i].ExtendedBits) + } + + vbBytes := make([]byte, length) + offset := 0 + for i := range voteBitsSlice { + vbBytes[offset] = 2 + uint8(len(voteBitsSlice[i].ExtendedBits)) + offset++ + + binary.LittleEndian.PutUint16(vbBytes[offset:offset+2], + voteBitsSlice[i].Bits) + offset += 2 + + copy(vbBytes[offset:], voteBitsSlice[i].ExtendedBits[:]) + offset += len(voteBitsSlice[i].ExtendedBits) + } + + return hex.EncodeToString(vbBytes), nil +} + +// DecodeConcatenatedVoteBits decodes a string encoded as a slice of concatenated +// voteBits and extended voteBits, and returns the slice of DecodedVoteBits to +// the caller. +func DecodeConcatenatedVoteBits(voteBitsString string) ([]stake.VoteBits, error) { + asBytes, err := hex.DecodeString(voteBitsString) + if err != nil { + return nil, err + } + + var dvbs []stake.VoteBits + cursor := 0 + for { + var dvb stake.VoteBits + length := int(asBytes[cursor]) + if length < 2 { + return nil, &RPCError{ + Code: ErrRPCInvalidParameter, + Message: "invalid length byte for votebits (short)", + } + } + + if cursor+length >= len(asBytes) { + return nil, &RPCError{ + Code: ErrRPCInvalidParameter, + Message: "cursor read past memory when decoding " + + "votebits", + } + } + cursor++ + + dvb.Bits = binary.LittleEndian.Uint16(asBytes[cursor : cursor+2]) + cursor += 2 + + dvb.ExtendedBits = asBytes[cursor : cursor+length-2] + cursor += length - 2 + + dvbs = append(dvbs, dvb) + if cursor == len(asBytes) { + break + } + } + + return dvbs, nil +} diff --git a/dcrjson/parse_test.go b/dcrjson/parse_test.go index 1a6b0294..17a50189 100644 --- a/dcrjson/parse_test.go +++ b/dcrjson/parse_test.go @@ -5,9 +5,12 @@ package dcrjson_test import ( + "bytes" "encoding/hex" + "reflect" "testing" + "github.com/decred/dcrd/blockchain/stake" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrjson" ) @@ -46,3 +49,113 @@ func TestDecodeConcatenatedHashes(t *testing.T) { } } } + +func TestEncodeConcatenatedVoteBits(t *testing.T) { + testVbs := []stake.VoteBits{ + stake.VoteBits{Bits: 0, ExtendedBits: []byte{}}, + stake.VoteBits{Bits: 0, ExtendedBits: []byte{0x00}}, + stake.VoteBits{Bits: 0x1223, ExtendedBits: []byte{0x01, 0x02, 0x03, 0x04}}, + stake.VoteBits{Bits: 0xaaaa, ExtendedBits: []byte{0x01, 0x02, 0x03, 0x04, 0x05}}, + } + encodedResults, err := dcrjson.EncodeConcatenatedVoteBits(testVbs) + if err != nil { + t.Fatalf("Encode failed: %v", err) + } + + expectedEncoded := []byte{ + 0x02, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x06, + 0x23, 0x12, 0x01, 0x02, + 0x03, 0x04, 0x07, 0xaa, + 0xaa, 0x01, 0x02, 0x03, + 0x04, 0x05, + } + + encodedResultsStr, _ := hex.DecodeString(encodedResults) + if !bytes.Equal(expectedEncoded, encodedResultsStr) { + t.Fatalf("Encoded votebits `%x` does not match expected `%x`", + encodedResults, expectedEncoded) + } + + // Test too long voteBits extended. + testVbs = []stake.VoteBits{ + stake.VoteBits{Bits: 0, ExtendedBits: bytes.Repeat([]byte{0x00}, 74)}, + } + _, err = dcrjson.EncodeConcatenatedVoteBits(testVbs) + if err == nil { + t.Fatalf("expected too long error") + } +} + +func TestDecodeConcatenatedVoteBits(t *testing.T) { + encodedBytes := []byte{ + 0x03, 0x00, 0x00, 0x00, + 0x06, 0x23, 0x12, 0x01, + 0x02, 0x03, 0x04, 0x07, + 0xaa, 0xaa, 0x01, 0x02, + 0x03, 0x04, 0x05, + } + encodedBytesStr := hex.EncodeToString(encodedBytes) + + expectedVbs := []stake.VoteBits{ + stake.VoteBits{Bits: 0, ExtendedBits: []byte{0x00}}, + stake.VoteBits{Bits: 0x1223, ExtendedBits: []byte{0x01, 0x02, 0x03, 0x04}}, + stake.VoteBits{Bits: 0xaaaa, ExtendedBits: []byte{0x01, 0x02, 0x03, 0x04, 0x05}}, + } + + decodedSlice, err := + dcrjson.DecodeConcatenatedVoteBits(encodedBytesStr) + if err != nil { + t.Fatalf("unexpected error decoding votebits: %v", err.Error()) + } + + if !reflect.DeepEqual(expectedVbs, decodedSlice) { + t.Fatalf("Decoded votebits `%v` does not match expected `%v`", + decodedSlice, expectedVbs) + } + + // Test short read. + encodedBytes = []byte{ + 0x03, 0x00, 0x00, 0x00, + 0x06, 0x23, 0x12, 0x01, + 0x02, 0x03, 0x04, 0x07, + 0xaa, 0xaa, 0x01, 0x02, + 0x03, 0x04, + } + encodedBytesStr = hex.EncodeToString(encodedBytes) + + decodedSlice, err = dcrjson.DecodeConcatenatedVoteBits(encodedBytesStr) + if err == nil { + t.Fatalf("expected short read error") + } + + // Test too long read. + encodedBytes = []byte{ + 0x03, 0x00, 0x00, 0x00, + 0x06, 0x23, 0x12, 0x01, + 0x02, 0x03, 0x04, 0x07, + 0xaa, 0xaa, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, + } + encodedBytesStr = hex.EncodeToString(encodedBytes) + + decodedSlice, err = dcrjson.DecodeConcatenatedVoteBits(encodedBytesStr) + if err == nil { + t.Fatalf("expected corruption error") + } + + // Test invalid length. + encodedBytes = []byte{ + 0x01, 0x00, 0x00, 0x00, + 0x06, 0x23, 0x12, 0x01, + 0x02, 0x03, 0x04, 0x07, + 0xaa, 0xaa, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, + } + encodedBytesStr = hex.EncodeToString(encodedBytes) + + decodedSlice, err = dcrjson.DecodeConcatenatedVoteBits(encodedBytesStr) + if err == nil { + t.Fatalf("expected corruption error") + } +}