profile.go raw

   1  package chain
   2  
   3  import (
   4  	"encoding/hex"
   5  	"encoding/json"
   6  	"errors"
   7  	"fmt"
   8  	"os"
   9  	"strconv"
  10  	"strings"
  11  
  12  	"git.smesh.lol/nostrino/pkg/btcwire"
  13  	"git.smesh.lol/nostrino/pkg/crypto/chainhash"
  14  )
  15  
  16  // ProfileJSON is the on-disk form of a chain profile. Hashes are written in
  17  // the usual display order and the genesis header as 160 hex characters.
  18  //
  19  // A checkpoint pins both the block and the compact filter header at a height,
  20  // which is what lets a first sync start near the tip. It must come from a
  21  // source the operator trusts; a wrong filter header is detected on the first
  22  // filter header response, not silently accepted.
  23  type ProfileJSON struct {
  24  	Name             string          `json:"name"`
  25  	Magic            string          `json:"magic"`
  26  	GenesisHeader    string          `json:"genesis_header"`
  27  	GenesisHash      string          `json:"genesis_hash"`
  28  	P2PPort          string          `json:"p2p_port,omitempty"`
  29  	Bech32HRP        string          `json:"bech32_hrp,omitempty"`
  30  	CoinType         uint32          `json:"coin_type,omitempty"`
  31  	PowLimitBits     string          `json:"pow_limit_bits,omitempty"`
  32  	TargetSpacing    int64           `json:"target_spacing,omitempty"`
  33  	TargetTimespan   int64           `json:"target_timespan,omitempty"`
  34  	RetargetInterval uint32          `json:"retarget_interval,omitempty"`
  35  	MinDifficulty    bool            `json:"min_difficulty,omitempty"`
  36  	NoRetarget       bool            `json:"no_retarget,omitempty"`
  37  	Checkpoint       *CheckpointJSON `json:"checkpoint,omitempty"`
  38  }
  39  
  40  // CheckpointJSON is the checkpoint form of ProfileJSON.
  41  type CheckpointJSON struct {
  42  	Height       uint32 `json:"height"`
  43  	BlockHash    string `json:"block_hash"`
  44  	FilterHeader string `json:"filter_header"`
  45  }
  46  
  47  // ParseProfile parses a profile from JSON.
  48  func ParseProfile(raw []byte) (*Profile, error) {
  49  	var pj ProfileJSON
  50  	dec := json.NewDecoder(strings.NewReader(string(raw)))
  51  	dec.DisallowUnknownFields()
  52  	if err := dec.Decode(&pj); err != nil {
  53  		return nil, fmt.Errorf("chain: profile json: %w", err)
  54  	}
  55  	return profileFromJSON(&pj)
  56  }
  57  
  58  // LoadProfile reads a profile file.
  59  func LoadProfile(path string) (*Profile, error) {
  60  	raw, err := os.ReadFile(path)
  61  	if err != nil {
  62  		return nil, fmt.Errorf("chain: read profile: %w", err)
  63  	}
  64  	p, err := ParseProfile(raw)
  65  	if err != nil {
  66  		return nil, fmt.Errorf("chain: %s: %w", path, err)
  67  	}
  68  	return p, nil
  69  }
  70  
  71  func profileFromJSON(pj *ProfileJSON) (*Profile, error) {
  72  	if strings.TrimSpace(pj.Name) == "" {
  73  		return nil, errors.New("chain: profile has no name")
  74  	}
  75  	magicText := strings.TrimSpace(pj.Magic)
  76  	magicText = strings.TrimPrefix(magicText, "0x")
  77  	if len(magicText) != 8 {
  78  		return nil, fmt.Errorf("chain: magic %q must be 4 bytes of hex", pj.Magic)
  79  	}
  80  	magic, err := strconv.ParseUint(magicText, 16, 32)
  81  	if err != nil {
  82  		return nil, fmt.Errorf("chain: magic: %w", err)
  83  	}
  84  	hdrText := strings.TrimSpace(pj.GenesisHeader)
  85  	hdrBytes, err := hex.DecodeString(hdrText)
  86  	if err != nil {
  87  		return nil, fmt.Errorf("chain: genesis header is not hex: %w", err)
  88  	}
  89  	if len(hdrBytes) != btcwire.BlockHeaderSize {
  90  		return nil, fmt.Errorf("chain: genesis header is %d bytes, want %d", len(hdrBytes), btcwire.BlockHeaderSize)
  91  	}
  92  	var header btcwire.BlockHeader
  93  	if err := header.Deserialize(hdrBytes); err != nil {
  94  		return nil, fmt.Errorf("chain: genesis header: %w", err)
  95  	}
  96  	hash, err := chainhash.NewHashFromStr(strings.TrimSpace(pj.GenesisHash))
  97  	if err != nil {
  98  		return nil, fmt.Errorf("chain: genesis hash: %w", err)
  99  	}
 100  	p := &Profile{
 101  		Name:             strings.ToLower(strings.TrimSpace(pj.Name)),
 102  		Magic:            uint32(magic),
 103  		GenesisHash:      *hash,
 104  		GenesisHeader:    header,
 105  		P2PPort:          pj.P2PPort,
 106  		Bech32HRP:        pj.Bech32HRP,
 107  		CoinType:         pj.CoinType,
 108  		TargetSpacing:    pj.TargetSpacing,
 109  		TargetTimespan:   pj.TargetTimespan,
 110  		RetargetInterval: pj.RetargetInterval,
 111  		MinDifficulty:    pj.MinDifficulty,
 112  		NoRetarget:       pj.NoRetarget,
 113  	}
 114  	if p.TargetSpacing == 0 {
 115  		p.TargetSpacing = 600
 116  	}
 117  	if p.TargetTimespan == 0 {
 118  		p.TargetTimespan = 1209600
 119  	}
 120  	if p.RetargetInterval == 0 {
 121  		p.RetargetInterval = 2016
 122  	}
 123  	if strings.TrimSpace(pj.PowLimitBits) != "" {
 124  		bits, err := parseBits(pj.PowLimitBits)
 125  		if err != nil {
 126  			return nil, err
 127  		}
 128  		p.PowLimitBits = bits
 129  	} else {
 130  		p.PowLimitBits = header.Bits
 131  	}
 132  	if pj.Checkpoint != nil {
 133  		bh, err := chainhash.NewHashFromStr(strings.TrimSpace(pj.Checkpoint.BlockHash))
 134  		if err != nil {
 135  			return nil, fmt.Errorf("chain: checkpoint block hash: %w", err)
 136  		}
 137  		fh, err := chainhash.NewHashFromStr(strings.TrimSpace(pj.Checkpoint.FilterHeader))
 138  		if err != nil {
 139  			return nil, fmt.Errorf("chain: checkpoint filter header: %w", err)
 140  		}
 141  		p.Checkpoint = &Checkpoint{
 142  			Height:       pj.Checkpoint.Height,
 143  			BlockHash:    *bh,
 144  			FilterHeader: *fh,
 145  		}
 146  	}
 147  	if err := p.Validate(); err != nil {
 148  		return nil, err
 149  	}
 150  	return p, nil
 151  }
 152  
 153  func parseBits(raw string) (uint32, error) {
 154  	text := strings.TrimPrefix(strings.TrimSpace(raw), "0x")
 155  	if len(text) != 8 {
 156  		return 0, fmt.Errorf("chain: pow_limit_bits %q must be 4 bytes of hex", raw)
 157  	}
 158  	v, err := strconv.ParseUint(text, 16, 32)
 159  	if err != nil {
 160  		return 0, fmt.Errorf("chain: pow_limit_bits: %w", err)
 161  	}
 162  	return uint32(v), nil
 163  }
 164  
 165  // ParseCheckpoint parses the three checkpoint values an operator supplies.
 166  func ParseCheckpoint(height uint32, blockHash, filterHeader string) (*Checkpoint, error) {
 167  	bh, err := chainhash.NewHashFromStr(strings.TrimSpace(blockHash))
 168  	if err != nil {
 169  		return nil, fmt.Errorf("chain: checkpoint block hash: %w", err)
 170  	}
 171  	fh, err := chainhash.NewHashFromStr(strings.TrimSpace(filterHeader))
 172  	if err != nil {
 173  		return nil, fmt.Errorf("chain: checkpoint filter header: %w", err)
 174  	}
 175  	return &Checkpoint{Height: height, BlockHash: *bh, FilterHeader: *fh}, nil
 176  }
 177