decred.org/dcrdex@v1.0.5/server/asset/firo/firo.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package firo
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/binary"
     9  	"fmt"
    10  	"io"
    11  	"math"
    12  	"time"
    13  
    14  	"decred.org/dcrdex/dex"
    15  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    16  	dexfiro "decred.org/dcrdex/dex/networks/firo"
    17  	"decred.org/dcrdex/server/asset"
    18  	"decred.org/dcrdex/server/asset/btc"
    19  	"github.com/btcsuite/btcd/chaincfg"
    20  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    21  	"github.com/btcsuite/btcd/wire"
    22  )
    23  
    24  // Driver implements asset.Driver.
    25  type Driver struct{}
    26  
    27  // Setup creates the DGB backend. Start the backend with its Run method.
    28  func (d *Driver) Setup(cfg *asset.BackendConfig) (asset.Backend, error) {
    29  	return NewBackend(cfg)
    30  }
    31  
    32  // DecodeCoinID creates a human-readable representation of a coin ID for
    33  // DigiByte.
    34  func (d *Driver) DecodeCoinID(coinID []byte) (string, error) {
    35  	// Digibyte and Bitcoin have the same tx hash and output format.
    36  	return (&btc.Driver{}).DecodeCoinID(coinID)
    37  }
    38  
    39  // Version returns the Backend implementation's version number.
    40  func (d *Driver) Version() uint32 {
    41  	return version
    42  }
    43  
    44  // UnitInfo returns the dex.UnitInfo for the asset.
    45  func (d *Driver) UnitInfo() dex.UnitInfo {
    46  	return dexfiro.UnitInfo
    47  }
    48  
    49  // MinBondSize calculates the minimum bond size for a given fee rate that avoids
    50  // dust outputs on the bond and refund txs, assuming the maxFeeRate doesn't
    51  // change.
    52  func (d *Driver) MinBondSize(maxFeeRate uint64) uint64 {
    53  	return dexbtc.MinBondSize(maxFeeRate, false)
    54  }
    55  
    56  // MinLotSize calculates the minimum bond size for a given fee rate that avoids
    57  // dust outputs on the swap and refund txs, assuming the maxFeeRate doesn't
    58  // change.
    59  func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 {
    60  	return dexbtc.MinLotSize(maxFeeRate, false)
    61  }
    62  
    63  // Name is the asset's name.
    64  func (d *Driver) Name() string {
    65  	return "Firo"
    66  }
    67  
    68  func init() {
    69  	asset.Register(BipID, &Driver{})
    70  }
    71  
    72  const (
    73  	version   = 0
    74  	BipID     = 136 // Zcoin XZC
    75  	assetName = "firo"
    76  )
    77  
    78  // NewBackend generates the network parameters and creates a dgb backend as a
    79  // btc clone using an asset/btc helper function.
    80  func NewBackend(cfg *asset.BackendConfig) (asset.Backend, error) {
    81  	var params *chaincfg.Params
    82  	switch cfg.Net {
    83  	case dex.Mainnet:
    84  		params = dexfiro.MainNetParams
    85  	case dex.Testnet:
    86  		params = dexfiro.TestNetParams
    87  	case dex.Regtest:
    88  		params = dexfiro.RegressionNetParams
    89  	default:
    90  		return nil, fmt.Errorf("unknown network ID %v", cfg.Net)
    91  	}
    92  
    93  	// Designate the clone ports.
    94  	ports := dexbtc.NetPorts{
    95  		Mainnet: "8888",
    96  		Testnet: "18888",
    97  		Simnet:  "28888",
    98  	}
    99  
   100  	configPath := cfg.ConfigPath
   101  	if configPath == "" {
   102  		configPath = dexbtc.SystemConfigPath("firo")
   103  	}
   104  
   105  	return btc.NewBTCClone(&btc.BackendCloneConfig{
   106  		Name:        assetName,
   107  		Segwit:      false,
   108  		ConfigPath:  configPath,
   109  		Logger:      cfg.Logger,
   110  		Net:         cfg.Net,
   111  		ChainParams: params,
   112  		Ports:       ports,
   113  		// 2 blocks should be enough - Firo has masternode 1 block finalize
   114  		// confirms with Instasend (see also: Dash instasend)
   115  		// Also 'estimatefee 2' usually returns 0.00001000
   116  		FeeConfs: 2,
   117  		// Firo mainnet blocks are rarely full so this should be the most
   118  		// common fee rate .. as can be seen from looking at the explorer
   119  		NoCompetitionFeeRate: 1,    // 1 Sats/B
   120  		ManualMedianFee:      true, // no getblockstats
   121  		// BlockFeeTransactions: testnet never returns a result for estimatefee,
   122  		// apparently, and we don't have getblockstats, so we need to scan
   123  		// blocks on testnet.
   124  		BlockFeeTransactions: func(rc *btc.RPCClient, blockHash *chainhash.Hash) ([]btc.FeeTx, chainhash.Hash, error) {
   125  			return btcBlockFeeTransactions(rc, blockHash, cfg.Net)
   126  		},
   127  		MaxFeeBlocks:       16, // failsafe
   128  		BooleanGetBlockRPC: true,
   129  		// DumbFeeEstimates: Firo actually has estimatesmartfee, but with a big
   130  		// warning
   131  		// 'WARNING: This interface is unstable and may disappear or change!'
   132  		// It also doesn't accept an estimate_mode argument.
   133  		// Neither estimatesmartfee or estimatefee work on testnet v0.14.12.4,
   134  		// but estimatefee works on simnet, and on mainnet v0.14.12.1.
   135  		DumbFeeEstimates: true,
   136  		RelayAddr:        cfg.RelayAddr,
   137  	})
   138  }
   139  
   140  // Firo v0.14.12.1 defaults:
   141  // -fallbackfee= (default: 20000) 	wallet.h: DEFAULT_FALLBACK_FEE unused afaics
   142  // -mintxfee= (default: 1000)  		for tx creation
   143  // -maxtxfee= (default: 1000000000) 10 FIRO .. also looks unused
   144  // -minrelaytxfee= (default: 1000) 	0.00001 firo,
   145  // -blockmintxfee= (default: 1000)
   146  
   147  type FiroBlock struct {
   148  	wire.MsgBlock
   149  	// ProgPOW
   150  	Height  uint32
   151  	Nonce64 uint64
   152  	MixHash chainhash.Hash
   153  	// MTP
   154  	NVersionMTP  int32
   155  	MTPHashValue chainhash.Hash
   156  	Reserved     [2]chainhash.Hash
   157  	HashRootMTP  [16]byte
   158  	// Discarding MTP proofs
   159  
   160  	mtpTime     uint32 // Params::nMTPSwitchTime
   161  	progPowTime uint32 // Params::nPPSwitchTime
   162  }
   163  
   164  func NewFiroBlock(net dex.Network) *FiroBlock {
   165  	switch net {
   166  	case dex.Mainnet:
   167  		return &FiroBlock{mtpTime: 1544443200, progPowTime: 1635228000} // block 419_269
   168  	case dex.Testnet:
   169  		return &FiroBlock{mtpTime: 1539172800, progPowTime: 1630069200} // block 37_310
   170  	default:
   171  		return &FiroBlock{mtpTime: math.MaxUint32, progPowTime: math.MaxUint32}
   172  	}
   173  }
   174  
   175  // BlockFeeTransactions is for the manual median-fee backup method when
   176  // estimatefee doesn't return a result. This may only be a problem on testnet
   177  // (v0.14.12.4). In testing, estimatefee returns non-negative and reasonable
   178  // values on both mainnet (v0.14.12.1) and simnet (v0.14.12.4).
   179  func btcBlockFeeTransactions(rc *btc.RPCClient, blockHash *chainhash.Hash, net dex.Network) (feeTxs []btc.FeeTx, prevBlock chainhash.Hash, err error) {
   180  	blockB, err := rc.GetRawBlock(blockHash)
   181  	if err != nil {
   182  		return nil, chainhash.Hash{}, fmt.Errorf("GetRawBlock error: %w", err)
   183  	}
   184  
   185  	firoBlock, err := deserializeFiroBlock(blockB, net)
   186  	if err != nil {
   187  		return nil, chainhash.Hash{}, fmt.Errorf("deserializeFiroBlock error for block %q: %w", blockHash, err)
   188  	}
   189  
   190  	if len(firoBlock.Transactions) == 0 {
   191  		return nil, chainhash.Hash{}, fmt.Errorf("no transactions?")
   192  	}
   193  
   194  	feeTxs = make([]btc.FeeTx, len(firoBlock.Transactions)-1)
   195  	for i, msgTx := range firoBlock.Transactions[1:] { // skip coinbase
   196  		feeTxs[i] = &btc.BTCFeeTx{MsgTx: msgTx}
   197  	}
   198  	return feeTxs, firoBlock.Header.PrevBlock, nil
   199  }
   200  
   201  func deserializeFiroBlock(b []byte, net dex.Network) (*FiroBlock, error) {
   202  	r := bytes.NewReader(b)
   203  	blk := NewFiroBlock(net)
   204  	if err := blk.readBlockHeader(r); err != nil {
   205  		return nil, fmt.Errorf("readBlockHeader error: %v", err)
   206  	}
   207  
   208  	const pver = 0
   209  
   210  	txCount, err := wire.ReadVarInt(r, pver)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	blk.Transactions = make([]*wire.MsgTx, 0, txCount)
   216  	for i := uint64(0); i < txCount; i++ {
   217  		tx := new(wire.MsgTx)
   218  
   219  		if err := tx.BtcDecode(r, pver, wire.BaseEncoding); err != nil {
   220  			return nil, fmt.Errorf("error decoding tx at index %d: %w", i, err)
   221  		}
   222  
   223  		// https://github.com/firoorg/firo/blob/26da759ee79b1f65fad42beda0b614bb9c2ab756/src/primitives/transaction.h#L332
   224  		// nVersion := tx.Version & 0xffff
   225  		nType := (tx.Version >> 16) & 0xffff
   226  		const txTypeNormal = 0
   227  		if nType != txTypeNormal {
   228  			// vExtraPayload
   229  			sz, err := wire.ReadVarInt(r, pver)
   230  			if err != nil {
   231  				return nil, fmt.Errorf("ReadVarInt error: %v", err)
   232  			}
   233  			if _, err = io.CopyN(io.Discard, r, int64(sz)); err != nil {
   234  				return nil, fmt.Errorf("error discarding vExtraPayload: %v", err)
   235  			}
   236  		}
   237  
   238  		blk.Transactions = append(blk.Transactions, tx)
   239  	}
   240  
   241  	return blk, nil
   242  
   243  }
   244  
   245  // https://github.com/firoorg/firo/blob/26da759ee79b1f65fad42beda0b614bb9c2ab756/src/primitives/block.h#L157-L194
   246  func (blk *FiroBlock) readBlockHeader(r io.Reader) error {
   247  	hdr := &blk.Header
   248  
   249  	nVersion, err := readUint32(r)
   250  	if err != nil {
   251  		return err
   252  	}
   253  	hdr.Version = int32(nVersion)
   254  
   255  	if _, err = io.ReadFull(r, hdr.PrevBlock[:]); err != nil {
   256  		return err
   257  	}
   258  
   259  	if _, err := io.ReadFull(r, hdr.MerkleRoot[:]); err != nil {
   260  		return err
   261  	}
   262  
   263  	nTime, err := readUint32(r)
   264  	if err != nil {
   265  		return err
   266  	}
   267  	hdr.Timestamp = time.Unix(int64(nTime), 0)
   268  
   269  	if hdr.Bits, err = readUint32(r); err != nil {
   270  		return err
   271  	}
   272  
   273  	if nTime >= blk.progPowTime {
   274  		if blk.Height, err = readUint32(r); err != nil {
   275  			return err
   276  		}
   277  
   278  		if blk.Nonce64, err = readUint64(r); err != nil {
   279  			return err
   280  		}
   281  
   282  		if _, err = io.ReadFull(r, blk.MixHash[:]); err != nil {
   283  			return err
   284  		}
   285  	} else {
   286  		if hdr.Nonce, err = readUint32(r); err != nil {
   287  			return err
   288  		}
   289  		if nTime >= blk.mtpTime {
   290  			nVersionMTP, err := readUint32(r)
   291  			if err != nil {
   292  				return err
   293  			}
   294  			blk.NVersionMTP = int32(nVersionMTP)
   295  
   296  			if _, err := io.ReadFull(r, blk.MTPHashValue[:]); err != nil {
   297  				return err
   298  			}
   299  
   300  			if _, err := io.ReadFull(r, blk.Reserved[0][:]); err != nil {
   301  				return err
   302  			}
   303  
   304  			if _, err := io.ReadFull(r, blk.Reserved[1][:]); err != nil {
   305  				return err
   306  			}
   307  
   308  			if _, err := io.ReadFull(r, blk.HashRootMTP[:]); err != nil {
   309  				return err
   310  			}
   311  
   312  			if blk.HashRootMTP != [16]byte{} {
   313  				// I have scanned the pre-ProgPOW sections of both testnet
   314  				// and mainnet and have yet to find non-zero HashRootMTP. I am
   315  				// speculating that the MTP proof data was purged in an upgrade
   316  				// some time after the switch to ProgPOW.
   317  				// This entire block could maybe be deleted, and is untested.
   318  				// Based on https://github.com/firoorg/firo/blob/26da759ee79b1f65fad42beda0b614bb9c2ab756/src/primitives/block.h#L94-L112
   319  				const mtpL = 64
   320  				for i := 0; i < mtpL*3; i++ {
   321  					var numberOfProofBlocksArr [1]byte
   322  					if _, err := io.ReadFull(r, numberOfProofBlocksArr[:]); err != nil {
   323  						return err
   324  					}
   325  					numberOfProofBlocks := int64(numberOfProofBlocksArr[0])
   326  					proofSize := 16 * numberOfProofBlocks
   327  					if _, err = io.CopyN(io.Discard, r, proofSize); err != nil {
   328  						return err
   329  					}
   330  				}
   331  			}
   332  		}
   333  	}
   334  	return nil
   335  }
   336  
   337  // readUint32 reads a little-endian encoded uint32 from the Reader.
   338  func readUint32(r io.Reader) (uint32, error) {
   339  	b := make([]byte, 4)
   340  	if _, err := io.ReadFull(r, b); err != nil {
   341  		return 0, err
   342  	}
   343  	return binary.LittleEndian.Uint32(b), nil
   344  }
   345  
   346  // readUint64 reads a little-endian encoded uint64 from the Reader.
   347  func readUint64(r io.Reader) (uint64, error) {
   348  	b := make([]byte, 8)
   349  	if _, err := io.ReadFull(r, b); err != nil {
   350  		return 0, err
   351  	}
   352  	return binary.LittleEndian.Uint64(b), nil
   353  }