package btcwire import ( "encoding/binary" "fmt" "io" "math/big" "git.smesh.lol/nostrino/pkg/crypto/chainhash" ) // BlockHeaderSize is the serialized size of a block header in bytes. const BlockHeaderSize = 80 // targetMaxBits is the number of bits a proof-of-work target may occupy. const targetMaxBits = 256 // BlockHeader is the 80-byte Bitcoin block header. It commits to the previous // block, the merkle root of its transactions, and the proof-of-work solution. type BlockHeader struct { // Version is the block version. Version int32 // PrevBlock is the hash of the previous block. PrevBlock chainhash.Hash // MerkleRoot is the root of the block's transaction merkle tree. MerkleRoot chainhash.Hash // Timestamp is the block's time in seconds since the Unix epoch. Timestamp uint32 // Bits is the compact encoding of the proof-of-work target. Bits uint32 // Nonce is the proof-of-work nonce. Nonce uint32 } // Serialize returns the 80-byte wire encoding of the header. func (h *BlockHeader) Serialize() [BlockHeaderSize]byte { var b [BlockHeaderSize]byte binary.LittleEndian.PutUint32(b[0:4], uint32(h.Version)) copy(b[4:36], h.PrevBlock[:]) copy(b[36:68], h.MerkleRoot[:]) binary.LittleEndian.PutUint32(b[68:72], h.Timestamp) binary.LittleEndian.PutUint32(b[72:76], h.Bits) binary.LittleEndian.PutUint32(b[76:80], h.Nonce) return b } // Deserialize replaces h with the header decoded from the first 80 bytes of b. // It returns an error if b is shorter than BlockHeaderSize. func (h *BlockHeader) Deserialize(b []byte) error { if len(b) < BlockHeaderSize { return fmt.Errorf( "btcwire: block header needs %d bytes, got %d: %w", BlockHeaderSize, len(b), io.ErrUnexpectedEOF, ) } h.Version = int32(binary.LittleEndian.Uint32(b[0:4])) copy(h.PrevBlock[:], b[4:36]) copy(h.MerkleRoot[:], b[36:68]) h.Timestamp = binary.LittleEndian.Uint32(b[68:72]) h.Bits = binary.LittleEndian.Uint32(b[72:76]) h.Nonce = binary.LittleEndian.Uint32(b[76:80]) return nil } // BlockHash returns the double-SHA256 hash of the serialized header, in // internal byte order. func (h *BlockHeader) BlockHash() chainhash.Hash { b := h.Serialize() return chainhash.DoubleHashH(b[:]) } // Target returns the proof-of-work target encoded by the header's Bits field. // Negative and out-of-range compact encodings are rejected. func (h *BlockHeader) Target() (*big.Int, error) { target, err := CompactToBig(h.Bits) if err != nil { return nil, fmt.Errorf("btcwire: target for bits 0x%08x: %w", h.Bits, err) } return target, nil } // CheckProofOfWork reports whether the header hash satisfies the target // encoded in its Bits field. It returns false for negative, zero, or // out-of-range targets. func (h *BlockHeader) CheckProofOfWork() bool { target, err := h.Target() if err != nil || target.Sign() <= 0 { return false } hash := h.BlockHash() return HashToBig(&hash).Cmp(target) <= 0 } // CompactToBig converts a compact target (the encoding used in a block // header's Bits field) to a big integer. It rejects negative encodings and // targets wider than 256 bits. func CompactToBig(compact uint32) (*big.Int, error) { mantissa := compact & 0x007fffff negative := compact&0x00800000 != 0 exponent := uint(compact >> 24) if negative { return nil, fmt.Errorf("%w: compact 0x%08x", ErrTargetNegative, compact) } target := new(big.Int).SetUint64(uint64(mantissa)) if exponent <= 3 { target.Rsh(target, 8*(3-exponent)) return target, nil } shift := 8 * (exponent - 3) if target.Sign() != 0 && target.BitLen()+int(shift) > targetMaxBits { return nil, fmt.Errorf("%w: compact 0x%08x", ErrTargetOverflow, compact) } target.Lsh(target, shift) return target, nil } // HashToBig interprets h as a little-endian 256-bit unsigned integer, the form // in which block hashes are compared against the proof-of-work target. Hashes // are stored in internal byte order, so this reverses the bytes before // interpreting them. func HashToBig(h *chainhash.Hash) *big.Int { var reversed [chainhash.HashSize]byte for i := 0; i < chainhash.HashSize; i++ { reversed[i] = h[chainhash.HashSize-1-i] } return new(big.Int).SetBytes(reversed[:]) }