diff --git a/parser/block.go b/parser/block.go index e693f58..1160dbd 100644 --- a/parser/block.go +++ b/parser/block.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "log" + "github.com/gtank/ctxd/parser/internal/bytestring" "github.com/pkg/errors" ) @@ -80,12 +81,37 @@ func (hdr *rawBlockHeader) UnmarshalBinary(data []byte) error { return nil } +type blockHeaderDecoder struct { + in *bytestring.String +} + +func NewBlockHeaderDecoder(in *bytestring.String) Decoder { + return &blockHeaderDecoder{in} +} + +func (dec *blockHeaderDecoder) Decode(out Serializable) error { + hdr, ok := out.(*BlockHeader) + if !ok { + return errors.New("unexpected Serializable for BlockHeader decoder") + } + + if hdr.rawBlockHeader == nil { + hdr.rawBlockHeader = new(rawBlockHeader) + } + + err := binary.Read(dec.in, binary.LittleEndian, hdr.rawBlockHeader) + if err != nil { + return errors.Wrap(err, "parsing block header") + } + return nil +} + type BlockHeader struct { *rawBlockHeader cachedHash []byte } -func (hdr *BlockHeader) GetBlockHeaderHash() []byte { +func (hdr *BlockHeader) GetHash() []byte { if hdr.cachedHash != nil { return hdr.cachedHash } @@ -103,16 +129,3 @@ func (hdr *BlockHeader) GetBlockHeaderHash() []byte { hdr.cachedHash = digest[:] return hdr.cachedHash } - -func (hdr *BlockHeader) GetSerializedSize() int { - // TODO: Make this dynamic. Low priority; it's unlikely to change. - return SER_BLOCK_HEADER_SIZE -} - -func ReadBlockHeader(blockHeader *BlockHeader, data []byte) error { - if blockHeader.rawBlockHeader == nil { - blockHeader.rawBlockHeader = new(rawBlockHeader) - } - return blockHeader.UnmarshalBinary(data) -} - diff --git a/parser/block_test.go b/parser/block_test.go index 5b25fcf..4dd03d9 100644 --- a/parser/block_test.go +++ b/parser/block_test.go @@ -6,6 +6,8 @@ import ( "encoding/hex" "os" "testing" + + "github.com/gtank/ctxd/parser/internal/bytestring" ) func TestBlockHeader(t *testing.T) { @@ -26,14 +28,21 @@ func TestBlockHeader(t *testing.T) { continue } - // Try to read the header - blockHeader := &BlockHeader{} - err = ReadBlockHeader(blockHeader, decodedBlockData) + s := bytestring.String(decodedBlockData) + startLength := len(s) + dec := NewBlockHeaderDecoder(&s) + + blockHeader := BlockHeader{} + err = dec.Decode(&blockHeader) if err != nil { t.Error(err) continue } + if (startLength - len(s)) != SER_BLOCK_HEADER_SIZE { + t.Error("did not advance underlying bytestring") + } + // Some basic sanity checks if blockHeader.Version != 4 { t.Error("Read wrong version in a test block.") @@ -73,7 +82,7 @@ func TestBlockHeader(t *testing.T) { break } - hash := blockHeader.GetBlockHeaderHash() + hash := blockHeader.GetHash() // This is not necessarily true for anything but our current test cases. for _, b := range hash[28:] { diff --git a/parser/internal/bytestring/bytestring.go b/parser/internal/bytestring/bytestring.go new file mode 100644 index 0000000..9f5a228 --- /dev/null +++ b/parser/internal/bytestring/bytestring.go @@ -0,0 +1,150 @@ +// Package bytestring provides a cryptobyte-inspired API specialized to the +// needs of parsing Zcash transactions. +package bytestring + +import ( + "errors" + "io" +) + +const MAX_COMPACT_SIZE uint64 = 0x02000000 + +// String represents a string of bytes and provides methods for parsing values +// from it. +type String []byte + +// read advances the string by n bytes and returns them. If fewer than n bytes +// remain, it returns nil. +func (s *String) read(n int) []byte { + if len(*s) < n { + return nil + } + + out := (*s)[:n] + (*s) = (*s)[n:] + return out +} + +// Read reads the next len(p) bytes from the string, or the remainder of the +// string if len(*s) < len(p). It returns the number of bytes read as n. If the +// string is empty it returns an io.EOF error, or a nil error if len(p) == 0. +// Read satisfies io.Reader. +func (s *String) Read(p []byte) (n int, err error) { + if s.Empty() { + if len(p) == 0 { + return 0, nil + } + return 0, io.EOF + } + + n = copy(p, *s) + if !s.Skip(n) { + return 0, errors.New("unexpected end of bytestring read") + } + return n, nil +} + +// Empty reports whether or not the string is empty. +func (s *String) Empty() bool { + return len(*s) == 0 +} + +// Skip advances the string by n bytes and reports whether it was successful. +func (s *String) Skip(n int) bool { + return s.read(n) != nil +} + +// ReadBytes reads n bytes into out and advances over them. It reports if the +// read was successful. +func (s *String) ReadBytes(out *[]byte, n int) bool { + v := s.read(n) + if v == nil { + return false + } + *out = v + return true +} + +// ReadCompactSize reads and interprets a Bitcoin-custom compact integer +// encoding used for length-prefixing and count values. If the values fall +// outside the expected canonical ranges, it returns false. +func (s *String) ReadCompactSize(size *uint64) bool { + lenBytes := s.read(1) + if lenBytes == nil { + return false + } + lenByte := lenBytes[0] + + var lenLen int + var length, minSize uint64 + + switch { + case lenByte < 253: + length = uint64(lenByte) + case lenByte == 253: + lenLen = 2 + minSize = 253 + case lenByte == 254: + lenLen = 4 + minSize = 0x10000 + case lenByte == 255: + lenLen = 8 + minSize = 0x100000000 + } + + if lenLen > 0 { + // expect little endian uint of varying size + lenBytes := s.read(lenLen) + for i := lenLen - 1; i >= 0; i-- { + length <<= 8 + length = length | uint64(lenBytes[i]) + } + } + + if length > MAX_COMPACT_SIZE || length < minSize { + return false + } + + *size = length + return true +} + +// ReadCompactLengthPrefixed reads data prefixed by a CompactSize-encoded +// length field into out. It reports whether the read was successful. +func (s *String) ReadCompactLengthPrefixed(out *String) bool { + var length uint64 + if ok := s.ReadCompactSize(&length); !ok { + return false + } + + v := s.read(int(length)) + if v == nil { + return false + } + + *out = v + return true +} + +// ReadUint32 decodes a little-endian, 32-bit value into out and advances over +// it. It reports whether the read was successful. +func (s *String) ReadUint32(out *uint32) bool { + v := s.read(4) + if v == nil { + return false + } + *out = uint32(v[0]) | uint32(v[1])<<8 | uint32(v[2])<<16 | uint32(v[3])<<24 + return true +} + +// ReadUint64 decodes a little-endian, 64-bit value into out and advances over +// it. It reports whether the read was successful. +func (s *String) ReadUint64(out *uint64) bool { + v := s.read(8) + if v == nil { + return false + } + *out = uint64(v[0]) | uint64(v[1])<<8 | uint64(v[2])<<16 | uint64(v[3])<<24 | + uint64(v[4])<<32 | uint64(v[5])<<40 | uint64(v[6])<<48 | uint64(v[7])<<56 + return true +} diff --git a/parser/serializable.go b/parser/serializable.go new file mode 100644 index 0000000..0416a11 --- /dev/null +++ b/parser/serializable.go @@ -0,0 +1,12 @@ +package parser + +import "encoding" + +type Serializable interface { + encoding.BinaryMarshaler + encoding.BinaryUnmarshaler +} + +type Decoder interface { + Decode(v Serializable) error +}