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 }