Skip to content

Commit

Permalink
client/asset/ltc: work with and require v0.21.2
Browse files Browse the repository at this point in the history
This updates the client's supported Litecoin Core version to v0.21.2

This is the modern Litecoin Core, with the MWEB patch set.

In addition to bumping the version, this means:
- getbalances is supported now
- sendrawtransaction has the new syntax with maxfeerate not allowHighFee

This also adds the missing switches on wallet Type to both ltc and bch.

To support MWEB and MW txns, this defines custom block and transaction
deserializers.  This is based on the LIPS:
https://github.com/litecoin-project/lips/blob/master/lip-0002.mediawiki
https://github.com/litecoin-project/lips/blob/master/lip-0003.mediawiki

On LTC regtest/regnet, the mweb soft fork is not active at genesis.
It is necessary to mine to block 431, send to a mweb address in a
"peg-in" transaction, and then mine the block so that the pegin
and the required hogex may be created when mweb activates at 432.
As such this also regenerates the litecoin harness chain tarball.

As of Bitcoin Core 0.21, the default "" wallet is not automatically
created.
https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-0.21.0.md#automatic-wallet-creation-removed
This adds a CREATE_DEFAULT_WALLET variable to the harness script
to explicitly create this wallet if needed.  The command also creates it
encrypted to start.
  • Loading branch information
chappjc authored Jun 16, 2022
1 parent f6cccaa commit d57bbb5
Show file tree
Hide file tree
Showing 28 changed files with 1,154 additions and 11 deletions.
6 changes: 6 additions & 0 deletions client/asset/bch/bch.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network)
return nil, fmt.Errorf("unknown network ID %v", network)
}

switch cfg.Type {
case walletTypeRPC, walletTypeLegacy:
default:
return nil, fmt.Errorf("unknown wallet type %q", cfg.Type)
}

// Designate the clone ports. These will be overwritten by any explicit
// settings in the configuration file. Bitcoin Cash uses the same default
// ports as Bitcoin.
Expand Down
4 changes: 4 additions & 0 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3172,6 +3172,7 @@ func (btc *baseWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes)

var blockHeight int32
if blockHash != nil {
btc.log.Infof("FindRedemption - Checking block %v for swap %v", blockHash, outPt)
blockHeight, err = btc.checkRedemptionBlockDetails(outPt, blockHash, pkScript)
if err != nil {
return exitError("GetBlockHeight for redemption %s error: %v", outPt, err)
Expand Down Expand Up @@ -3311,13 +3312,16 @@ func (btc *baseWallet) tryRedemptionRequests(ctx context.Context, startBlock *ch
continue
}

btc.log.Debugf("tryRedemptionRequests - Checking block %v for redemptions...", iHash)
discovered := btc.node.searchBlockForRedemptions(ctx, validReqs, *iHash)
for outPt, res := range discovered {
req, found := undiscovered[outPt]
if !found {
btc.log.Critical("Request not found in undiscovered map. This shouldn't be possible.")
continue
}
redeemTxID, redeemTxInput, _ := decodeCoinID(res.redemptionCoinID)
btc.log.Debugf("Found redemption %s:%d", redeemTxID, redeemTxInput)
req.success(res)
delete(undiscovered, outPt)
}
Expand Down
13 changes: 10 additions & 3 deletions client/asset/ltc/ltc.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const (
defaultFee = 10
// defaultFeeRateLimit is the default value for the feeratelimit.
defaultFeeRateLimit = 100
minNetworkVersion = 180100
minNetworkVersion = 210201
walletTypeRPC = "litecoindRPC"
walletTypeLegacy = ""
)
Expand Down Expand Up @@ -150,6 +150,12 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network)
return nil, fmt.Errorf("unknown network ID %v", network)
}

switch cfg.Type {
case walletTypeRPC, walletTypeLegacy:
default:
return nil, fmt.Errorf("unknown wallet type %q", cfg.Type)
}

// Designate the clone ports. These will be overwritten by any explicit
// settings in the configuration file.
cloneCFG := &btc.BTCCloneCFG{
Expand All @@ -163,9 +169,10 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network)
Ports: NetPorts,
DefaultFallbackFee: defaultFee,
DefaultFeeRateLimit: defaultFeeRateLimit,
LegacyBalance: true,
LegacyRawFeeLimit: true,
LegacyBalance: false,
LegacyRawFeeLimit: false,
Segwit: true,
BlockDeserializer: dexltc.DeserializeBlockBytes,
}

return btc.BTCCloneWallet(cloneCFG)
Expand Down
4 changes: 4 additions & 0 deletions client/core/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -2284,6 +2284,10 @@ func (t *trackedTrade) findMakersRedemption(match *matchTracker) {
t.dc.log.Errorf("waitForRedemptions: error storing match info in database: %v", err)
}

t.dc.log.Infof("Found redemption of contract %s (%s) for order %s, match %s. Redeem: %v",
coinIDString(fromAsset.ID, swapCoinID), fromAsset.Symbol, t.ID(), match,
coinIDString(fromAsset.ID, redemptionCoinID))

subject, details := t.formatDetails(TopicMatchRecovered,
fromAsset.Symbol, coinIDString(fromAsset.ID, redemptionCoinID), match)
t.notify(newOrderNote(TopicMatchRecovered, subject, details, db.Poke, t.coreOrderInternal()))
Expand Down
132 changes: 132 additions & 0 deletions dex/networks/ltc/block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org

package ltc

import (
"bytes"
"fmt"
"io"

"github.com/btcsuite/btcd/wire"
)

const (
// mwebVer is the bit of the block header's version that indicates the
// presence of a MWEB.
mwebVer = 0x20000000 // 1 << 29
)

func parseMWEB(blk io.Reader) error {
dec := newDecoder(blk)
// src/mweb/mweb_models.h - struct Block
// "A convenience wrapper around a possibly-null extension block.""
// OptionalPtr around a mw::Block. Read the option byte:
hasMWEB, err := dec.readByte()
if err != nil {
return fmt.Errorf("failed to check MWEB option byte: %w", err)
}
if hasMWEB == 0 {
return nil
}

// src/libmw/include/mw/models/block/Block.h - class Block
// (1) Header and (2) TxBody

// src/libmw/include/mw/models/block/Header.h - class Header
// height
if _, err = dec.readVLQ(); err != nil {
return fmt.Errorf("failed to decode MWEB height: %w", err)
}

// 3x Hash + 2x BlindingFactor
if err = dec.discardBytes(32*3 + 32*2); err != nil {
return fmt.Errorf("failed to decode MWEB junk: %w", err)
}

// Number of TXOs: outputMMRSize
if _, err = dec.readVLQ(); err != nil {
return fmt.Errorf("failed to decode TXO count: %w", err)
}

// Number of kernels: kernelMMRSize
if _, err = dec.readVLQ(); err != nil {
return fmt.Errorf("failed to decode kernel count: %w", err)
}

// TxBody
_, err = dec.readMWTXBody()
if err != nil {
return fmt.Errorf("failed to decode MWEB tx: %w", err)
}
// if len(kern0) > 0 {
// mwebTxID := chainhash.Hash(blake3.Sum256(kern0))
// fmt.Println(mwebTxID.String())
// }

return nil
}

// DeserializeBlock decodes the bytes of a serialized Litecoin block. This
// function exists because MWEB changes both the block and transaction
// serializations. Blocks may have a MW "extension block" for "peg-out"
// transactions, and this EB is after all the transactions in the regular LTC
// block. After the canonical transactions in the regular block, there may be
// zero or more "peg-in" transactions followed by one integration transaction
// (also known as a HogEx transaction), all still in the regular LTC block. The
// peg-in txns decode correctly, but the integration tx is a special transaction
// with the witness tx flag with bit 3 set (8), which prevents correct
// wire.MsgTx deserialization.
// Refs:
// https://github.com/litecoin-project/lips/blob/master/lip-0002.mediawiki#PegOut_Transactions
// https://github.com/litecoin-project/lips/blob/master/lip-0003.mediawiki#Specification
// https://github.com/litecoin-project/litecoin/commit/9d1f530a5fa6d16871fdcc3b506be42b593d3ce4
// https://github.com/litecoin-project/litecoin/commit/8c82032f45e644f413ec5c91e121a31c993aa831
// (src/libmw/include/mw/models/tx/Transaction.h for the `mweb_tx` field of the
// `CTransaction` in the "primitives" commit).
func DeserializeBlock(blk io.Reader) (*wire.MsgBlock, error) {
// Block header
hdr := &wire.BlockHeader{}
err := hdr.Deserialize(blk)
if err != nil {
return nil, fmt.Errorf("failed to deserialize block header: %w", err)
}

// This block's transactions
txnCount, err := wire.ReadVarInt(blk, 0)
if err != nil {
return nil, fmt.Errorf("failed to parse transaction count: %w", err)
}

// We can only decode the canonical txns, not the mw peg-in txs in the EB.
var hasHogEx bool
txns := make([]*wire.MsgTx, 0, int(txnCount))
for i := 0; i < cap(txns); i++ {
msgTx, err := DeserializeTx(blk)
if err != nil {
return nil, fmt.Errorf("failed to deserialize transaction %d of %d in block %v: %w",
i+1, txnCount, hdr.BlockHash(), err)
}
txns = append(txns, msgTx.MsgTx) // txns = append(txns, msgTx)
hasHogEx = msgTx.IsHogEx // hogex is the last txn
}

// The mwebVer mask indicates it may contain a MWEB after a HogEx.
// src/primitives/block.h: SERIALIZE_NO_MWEB
if hdr.Version&mwebVer != 0 && hasHogEx {
if err = parseMWEB(blk); err != nil {
return nil, err
}
}

return &wire.MsgBlock{
Header: *hdr,
Transactions: txns,
}, nil
}

// DeserializeBlockBytes wraps DeserializeBlock using bytes.NewReader for
// convenience.
func DeserializeBlockBytes(blk []byte) (*wire.MsgBlock, error) {
return DeserializeBlock(bytes.NewReader(blk))
}
Binary file added dex/networks/ltc/block2215586testnet4.dat
Binary file not shown.
134 changes: 134 additions & 0 deletions dex/networks/ltc/block_online_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org

//go:build ltclive

package ltc

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os/user"
"path/filepath"
"strings"
"testing"

"decred.org/dcrdex/dex/config"

"github.com/btcsuite/btcd/btcjson"
"github.com/decred/dcrd/chaincfg/chainhash" // just for the array type, not its methods
"github.com/decred/dcrd/rpcclient/v7"
)

// TestOnlineDeserializeBlock attempts to deserialize every testnet4 LTC block
// from 1000 blocks prior to mweb to the chain tip.
func TestOnlineDeserializeBlock(t *testing.T) {
cfg := struct {
RPCUser string `ini:"rpcuser"`
RPCPass string `ini:"rpcpassword"`
}{}
usr, _ := user.Current()
if err := config.ParseInto(filepath.Join(usr.HomeDir, ".litecoin", "litecoin.conf"), &cfg); err != nil {
t.Fatalf("config.ParseInto error: %v", err)
}
client, err := rpcclient.New(&rpcclient.ConnConfig{
HTTPPostMode: true,
DisableTLS: true,
Host: "127.0.0.1:19332", // testnet4, mainnet is 9332
User: cfg.RPCUser,
Pass: cfg.RPCPass,
}, nil)
if err != nil {
t.Fatalf("error creating RPC client: %v", err)
}

msg, err := client.RawRequest(context.Background(), "getblockchaininfo", nil)
if err != nil {
t.Fatalf("getblockchaininfo: %v", err)
}
gbci := &struct {
Chain string `json:"chain"`
}{}
err = json.Unmarshal(msg, &gbci)
if err != nil {
t.Fatalf("Unmarshal: %v", err)
}
isTestNet := gbci.Chain == "test"
t.Log("Network:", gbci.Chain)

makeParams := func(args ...interface{}) []json.RawMessage {
params := make([]json.RawMessage, 0, len(args))
for i := range args {
p, err := json.Marshal(args[i])
if err != nil {
t.Fatal(err)
}
params = append(params, p)
}
return params
}

getBlkStr := func(hash *chainhash.Hash) string {
params := makeParams(hash.String(), 0) // not verbose
msg, err := client.RawRequest(context.Background(), "getblock", params)
if err != nil {
t.Fatalf("RawRequest: %v", err)
}
var blkStr string
err = json.Unmarshal(msg, &blkStr)
if err != nil {
t.Fatalf("Unmarshal: %v", err)
}
return blkStr
}

getBlkVerbose := func(hash *chainhash.Hash) *btcjson.GetBlockVerboseResult {
params := makeParams(hash.String(), 1) // verbose
msg, err := client.RawRequest(context.Background(), "getblock", params)
if err != nil {
t.Fatalf("RawRequest: %v", err)
}
var res btcjson.GetBlockVerboseResult
err = json.Unmarshal(msg, &res)
if err != nil {
t.Fatalf("Unmarshal: %v", err)
}
return &res
}

// start 1000 blocks prior to mweb testnet blocks
// 2215586 for mainnet, 2214584 for testnet4
var startBlk, numBlocks int64 = 2214584, 2000
if isTestNet {
numBlocks = 12000 // testnet blocks are empty and fast
}
for iBlk := startBlk; iBlk <= startBlk+numBlocks; iBlk++ {
hash, err := client.GetBlockHash(context.Background(), iBlk)
if err != nil {
if strings.Contains(err.Error(), "height out of range") {
break
}
t.Fatal(err) // hint: 401 means unauthorized (check user/pass above)
}
blkStr := getBlkStr(hash)
blk, err := DeserializeBlock(hex.NewDecoder(strings.NewReader(blkStr)))
if err != nil {
t.Fatalf("Unmarshal (%d): %v", iBlk, err)
}
if iBlk%500 == 0 {
fmt.Println(iBlk)
}
gbv := getBlkVerbose(hash)
if len(blk.Transactions) != len(gbv.Tx) {
t.Fatalf("block %v has %d but decoded %d txns", hash, len(gbv.Tx), len(blk.Transactions))
}
for i, tx := range blk.Transactions {
txid := tx.TxHash().String()
if txid != gbv.Tx[i] {
t.Errorf("got txid %v, wanted %v", txid, gbv.Tx[i])
}
}
}
}
Loading

0 comments on commit d57bbb5

Please sign in to comment.