Skip to content

Commit

Permalink
tbcapi: split block-headers-by-height into raw and serialised (#73)
Browse files Browse the repository at this point in the history
* tbcapi: add raw and 'cooked' version of block-headers-by-height

* tbc: add test coverage for raw/cooked block-headers-by-height

* fixed expected response

* tbc: make utility name plural

---------

Co-authored-by: ClaytonNorthey92 <[email protected]>
  • Loading branch information
joshuasing and ClaytonNorthey92 authored Apr 15, 2024
1 parent 9d38928 commit e08522b
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 60 deletions.
68 changes: 34 additions & 34 deletions api/tbcapi/tbcapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const (
CmdPingRequest = "tbcapi-ping-request"
CmdPingResponse = "tbcapi-ping-response"

CmdBlockHeadersByHeightRawRequest = "tbcapi-block-headers-by-height-raw-request"
CmdBlockHeadersByHeightRawResponse = "tbcapi-block-headers-by-height-raw-response"

CmdBlockHeadersByHeightRequest = "tbcapi-block-headers-by-height-request"
CmdBlockHeadersByHeightResponse = "tbcapi-block-headers-by-height-response"

Expand Down Expand Up @@ -55,35 +58,30 @@ type (
PingResponse protocol.PingResponse
)

type Header struct {
Version uint32 `json:"version"`

// hex encoded byte array
PrevHash string `json:"prev_hash"`

// hex encoded byte array
type BlockHeader struct {
Version int32 `json:"version"`
PrevHash string `json:"prev_hash"`
MerkleRoot string `json:"merkle_root"`

Timestamp uint64 `json:"timestamp"`

// hex encoded int
Bits string `json:"bits"`

Nonce uint32 `json:"nonce"`
Timestamp int64 `json:"timestamp"`
Bits string `json:"bits"`
Nonce uint32 `json:"nonce"`
}

type BlockHeader struct {
type BlockHeadersByHeightRawRequest struct {
Height uint32 `json:"height"`
NumTx uint32 `json:"num_tx"`
Header Header `json:"header"`
}

type BlockHeadersByHeightRawResponse struct {
BlockHeaders []api.ByteSlice `json:"block_headers"`
Error *protocol.Error `json:"error,omitempty"`
}

type BlockHeadersByHeightRequest struct {
Height uint32 `json:"height"`
}

type BlockHeadersByHeightResponse struct {
BlockHeaders []api.ByteSlice `json:"block_headers"`
BlockHeaders []*BlockHeader `json:"block_headers"`
Error *protocol.Error `json:"error,omitempty"`
}

Expand Down Expand Up @@ -177,22 +175,24 @@ type Tx struct {
}

var commands = map[protocol.Command]reflect.Type{
CmdPingRequest: reflect.TypeOf(PingRequest{}),
CmdPingResponse: reflect.TypeOf(PingResponse{}),
CmdBlockHeadersByHeightRequest: reflect.TypeOf(BlockHeadersByHeightRequest{}),
CmdBlockHeadersByHeightResponse: reflect.TypeOf(BlockHeadersByHeightResponse{}),
CmdBlockHeadersBestRequest: reflect.TypeOf(BlockHeadersBestRequest{}),
CmdBlockHeadersBestResponse: reflect.TypeOf(BlockHeadersBestResponse{}),
CmdBalanceByAddressRequest: reflect.TypeOf(BalanceByAddressRequest{}),
CmdBalanceByAddressResponse: reflect.TypeOf(BalanceByAddressResponse{}),
CmdUtxosByAddressRawRequest: reflect.TypeOf(UtxosByAddressRawRequest{}),
CmdUtxosByAddressRawResponse: reflect.TypeOf(UtxosByAddressRawResponse{}),
CmdUtxosByAddressRequest: reflect.TypeOf(UtxosByAddressRequest{}),
CmdUtxosByAddressResponse: reflect.TypeOf(UtxosByAddressResponse{}),
CmdTxByIdRawRequest: reflect.TypeOf(TxByIdRawRequest{}),
CmdTxByIdRawResponse: reflect.TypeOf(TxByIdRawResponse{}),
CmdTxByIdRequest: reflect.TypeOf(TxByIdRequest{}),
CmdTxByIdResponse: reflect.TypeOf(TxByIdResponse{}),
CmdPingRequest: reflect.TypeOf(PingRequest{}),
CmdPingResponse: reflect.TypeOf(PingResponse{}),
CmdBlockHeadersByHeightRawRequest: reflect.TypeOf(BlockHeadersByHeightRawRequest{}),
CmdBlockHeadersByHeightRawResponse: reflect.TypeOf(BlockHeadersByHeightRawResponse{}),
CmdBlockHeadersByHeightRequest: reflect.TypeOf(BlockHeadersByHeightRequest{}),
CmdBlockHeadersByHeightResponse: reflect.TypeOf(BlockHeadersByHeightResponse{}),
CmdBlockHeadersBestRequest: reflect.TypeOf(BlockHeadersBestRequest{}),
CmdBlockHeadersBestResponse: reflect.TypeOf(BlockHeadersBestResponse{}),
CmdBalanceByAddressRequest: reflect.TypeOf(BalanceByAddressRequest{}),
CmdBalanceByAddressResponse: reflect.TypeOf(BalanceByAddressResponse{}),
CmdUtxosByAddressRawRequest: reflect.TypeOf(UtxosByAddressRawRequest{}),
CmdUtxosByAddressRawResponse: reflect.TypeOf(UtxosByAddressRawResponse{}),
CmdUtxosByAddressRequest: reflect.TypeOf(UtxosByAddressRequest{}),
CmdUtxosByAddressResponse: reflect.TypeOf(UtxosByAddressResponse{}),
CmdTxByIdRawRequest: reflect.TypeOf(TxByIdRawRequest{}),
CmdTxByIdRawResponse: reflect.TypeOf(TxByIdRawResponse{}),
CmdTxByIdRequest: reflect.TypeOf(TxByIdRequest{}),
CmdTxByIdResponse: reflect.TypeOf(TxByIdResponse{}),
}

type tbcAPI struct{}
Expand Down
57 changes: 45 additions & 12 deletions service/tbc/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ func (s *Server) handleWebsocketRead(ctx context.Context, ws *tbcWs) {
return s.handleBlockHeadersByHeightRequest(ctx, req)
}

go s.handleRequest(ctx, ws, id, cmd, handler)
case tbcapi.CmdBlockHeadersByHeightRawRequest:
handler := func(ctx context.Context) (any, error) {
req := payload.(*tbcapi.BlockHeadersByHeightRawRequest)
return s.handleBlockHeadersByHeightRawRequest(ctx, req)
}

go s.handleRequest(ctx, ws, id, cmd, handler)
case tbcapi.CmdBlockHeadersBestRequest:
handler := func(ctx context.Context) (any, error) {
Expand Down Expand Up @@ -178,7 +185,7 @@ func (s *Server) handleBlockHeadersByHeightRequest(ctx context.Context, req *tbc
log.Tracef("handleBtcBlockHeadersByHeightRequest")
defer log.Tracef("handleBtcBlockHeadersByHeightRequest exit")

blockHeaders, err := s.BlockHeadersByHeight(ctx, uint64(req.Height))
wireBlockHeaders, err := s.BlockHeadersByHeight(ctx, uint64(req.Height))
if err != nil {
if errors.Is(err, database.ErrNotFound) {
return &tbcapi.BlockHeadersByHeightResponse{
Expand All @@ -192,20 +199,31 @@ func (s *Server) handleBlockHeadersByHeightRequest(ctx context.Context, req *tbc
}, e
}

var encodedBlockHeaders []api.ByteSlice
for _, bh := range blockHeaders {
bytes, err := header2Bytes(&bh)
if err != nil {
e := protocol.NewInternalError(err)
return &tbcapi.BlockHeadersByHeightResponse{
Error: e.ProtocolError(),
}, e
return &tbcapi.BlockHeadersByHeightResponse{
BlockHeaders: wireBlockHeadersToTBC(wireBlockHeaders),
}, nil
}

func (s *Server) handleBlockHeadersByHeightRawRequest(ctx context.Context, req *tbcapi.BlockHeadersByHeightRawRequest) (any, error) {
log.Tracef("handleBtcBlockHeadersByHeightRawRequest")
defer log.Tracef("handleBtcBlockHeadersByHeightRawRequest exit")

rawBlockHeaders, err := s.RawBlockHeadersByHeight(ctx, uint64(req.Height))
if err != nil {
if errors.Is(err, database.ErrNotFound) {
return &tbcapi.BlockHeadersByHeightRawResponse{
Error: protocol.RequestErrorf("block headers not found at height %d", req.Height),
}, nil
}
encodedBlockHeaders = append(encodedBlockHeaders, bytes)

e := protocol.NewInternalError(err)
return &tbcapi.BlockHeadersByHeightRawResponse{
Error: e.ProtocolError(),
}, e
}

return tbcapi.BlockHeadersByHeightResponse{
BlockHeaders: encodedBlockHeaders,
return &tbcapi.BlockHeadersByHeightRawResponse{
BlockHeaders: rawBlockHeaders,
}, nil
}

Expand Down Expand Up @@ -468,6 +486,21 @@ func (s *Server) deleteSession(id string) {
}
}

func wireBlockHeadersToTBC(w []*wire.BlockHeader) []*tbcapi.BlockHeader {
blockHeaders := make([]*tbcapi.BlockHeader, len(w))
for i, bh := range w {
blockHeaders[i] = &tbcapi.BlockHeader{
Version: bh.Version,
PrevHash: bh.PrevBlock.String(),
MerkleRoot: bh.MerkleRoot.String(),
Timestamp: bh.Timestamp.Unix(),
Bits: fmt.Sprintf("%x", bh.Bits),
Nonce: bh.Nonce,
}
}
return blockHeaders
}

func wireTxToTbcapiTx(w *wire.MsgTx) *tbcapi.Tx {
a := &tbcapi.Tx{
Version: w.Version,
Expand Down
34 changes: 26 additions & 8 deletions service/tbc/tbc.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/syndtr/goleveldb/leveldb"

"github.com/hemilabs/heminetwork/api"
"github.com/hemilabs/heminetwork/api/tbcapi"
"github.com/hemilabs/heminetwork/database"
"github.com/hemilabs/heminetwork/database/tbcd"
Expand Down Expand Up @@ -1330,24 +1331,41 @@ func (s *Server) blockHeadersByHeight(ctx context.Context, height uint64) ([]tbc
return bhs, nil
}

func (s *Server) BlockHeadersByHeight(ctx context.Context, height uint64) ([]wire.BlockHeader, error) {
func (s *Server) RawBlockHeadersByHeight(ctx context.Context, height uint64) ([]api.ByteSlice, error) {
log.Tracef("RawBlockHeadersByHeight")
defer log.Tracef("RawBlockHeadersByHeight exit")

bhs, err := s.blockHeadersByHeight(ctx, height)
if err != nil {
return nil, err
}

var headers []api.ByteSlice
for _, bh := range bhs {
headers = append(headers, []byte(bh.Header))
}

return headers, nil
}

func (s *Server) BlockHeadersByHeight(ctx context.Context, height uint64) ([]*wire.BlockHeader, error) {
log.Tracef("BlockHeadersByHeight")
defer log.Tracef("BlockHeadersByHeight exit")

bhs, err := s.blockHeadersByHeight(ctx, height)
blockHeaders, err := s.blockHeadersByHeight(ctx, height)
if err != nil {
return nil, err
}

bhsw := make([]wire.BlockHeader, 0, len(bhs))
for k := range bhs {
bhw, err := bytes2Header(bhs[k].Header)
wireBlockHeaders := make([]*wire.BlockHeader, 0, len(blockHeaders))
for _, bh := range blockHeaders {
w, err := bh.Wire()
if err != nil {
return nil, fmt.Errorf("bytes to header: %w", err)
return nil, err
}
bhsw = append(bhsw, *bhw)
wireBlockHeaders = append(wireBlockHeaders, w)
}
return bhsw, nil
return wireBlockHeaders, nil
}

// BlockHeadersBest returns the headers for the best known blocks.
Expand Down
100 changes: 94 additions & 6 deletions service/tbc/tbc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func skipIfNoDocker(t *testing.T) {
}
}

func TestBtcBlockHeadersByHeight(t *testing.T) {
func TestBtcBlockHeadersByHeightRaw(t *testing.T) {
skipIfNoDocker(t)

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
Expand All @@ -84,15 +84,15 @@ func TestBtcBlockHeadersByHeight(t *testing.T) {
}

var lastErr error
var response tbcapi.BlockHeadersByHeightResponse
var response tbcapi.BlockHeadersByHeightRawResponse
for {
select {
case <-time.After(1 * time.Second):
case <-ctx.Done():
t.Fatal(ctx.Err())
}
lastErr = nil
err = tbcapi.Write(ctx, tws.conn, "someid", tbcapi.BlockHeadersByHeightRequest{
err = tbcapi.Write(ctx, tws.conn, "someid", tbcapi.BlockHeadersByHeightRawRequest{
Height: 55,
})
if err != nil {
Expand All @@ -107,7 +107,7 @@ func TestBtcBlockHeadersByHeight(t *testing.T) {
continue
}

if v.Header.Command == tbcapi.CmdBlockHeadersByHeightResponse {
if v.Header.Command == tbcapi.CmdBlockHeadersByHeightRawResponse {
if err := json.Unmarshal(v.Payload, &response); err != nil {
t.Fatal(err)
}
Expand All @@ -129,6 +129,73 @@ func TestBtcBlockHeadersByHeight(t *testing.T) {

t.Logf(spew.Sdump(bh))

cliBtcBlock := blockAtHeight(ctx, t, bitcoindContainer, 55)
expected := cliBlockToRawResponse(cliBtcBlock, t)
if diff := deep.Equal(expected, response); len(diff) > 0 {
t.Fatalf("unexpected diff: %s", diff)
}
}

func TestBtcBlockHeadersByHeight(t *testing.T) {
skipIfNoDocker(t)

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

bitcoindContainer, mappedPeerPort := createBitcoindWithInitialBlocks(ctx, t, 100, "")
_, tbcUrl := createTbcServer(ctx, t, mappedPeerPort)

c, _, err := websocket.Dial(ctx, tbcUrl, nil)
if err != nil {
t.Fatal(err)
}
defer c.CloseNow()

assertPing(ctx, t, c, tbcapi.CmdPingRequest)

tws := &tbcWs{
conn: protocol.NewWSConn(c),
}

var lastErr error
var response tbcapi.BlockHeadersByHeightResponse
for {
select {
case <-time.After(1 * time.Second):
case <-ctx.Done():
t.Fatal(ctx.Err())
}
lastErr = nil
err = tbcapi.Write(ctx, tws.conn, "someid", tbcapi.BlockHeadersByHeightRequest{
Height: 55,
})
if err != nil {
lastErr = err
continue
}

var v protocol.Message
err = wsjson.Read(ctx, c, &v)
if err != nil {
lastErr = err
continue
}

if v.Header.Command == tbcapi.CmdBlockHeadersByHeightResponse {
if err := json.Unmarshal(v.Payload, &response); err != nil {
t.Fatal(err)
}
break
} else {
lastErr = fmt.Errorf("received unexpected command: %s", v.Header.Command)
}

}

if lastErr != nil {
t.Fatal(lastErr)
}

cliBtcBlock := blockAtHeight(ctx, t, bitcoindContainer, 55)
expected := cliBlockToResponse(cliBtcBlock, t)
if diff := deep.Equal(expected, response); len(diff) > 0 {
Expand Down Expand Up @@ -1695,7 +1762,7 @@ type BtcCliBlockHeader struct {
NextBlockHash string `json:"nextblockhash"`
}

func cliBlockToResponse(btcCliBlockHeader BtcCliBlockHeader, t *testing.T) tbcapi.BlockHeadersByHeightResponse {
func cliBlockToRawResponse(btcCliBlockHeader BtcCliBlockHeader, t *testing.T) tbcapi.BlockHeadersByHeightRawResponse {
prevBlockHash, err := chainhash.NewHashFromStr(btcCliBlockHeader.PreviousBlockHash)
if err != nil {
t.Fatal(err)
Expand All @@ -1715,11 +1782,32 @@ func cliBlockToResponse(btcCliBlockHeader BtcCliBlockHeader, t *testing.T) tbcap
if err != nil {
t.Fatal(err)
}
return tbcapi.BlockHeadersByHeightResponse{
return tbcapi.BlockHeadersByHeightRawResponse{
BlockHeaders: []api.ByteSlice{bytes},
}
}

func cliBlockToResponse(btcCliBlockHeader BtcCliBlockHeader, t *testing.T) tbcapi.BlockHeadersByHeightResponse {
prevBlockHash, err := chainhash.NewHashFromStr(btcCliBlockHeader.PreviousBlockHash)
if err != nil {
t.Fatal(err)
}
merkleRoot, err := chainhash.NewHashFromStr(btcCliBlockHeader.MerkleRoot)
if err != nil {
t.Fatal(err)
}
bits, err := strconv.ParseUint(btcCliBlockHeader.Bits, 16, 64)
if err != nil {
t.Fatal(err)
}
bh := wire.NewBlockHeader(int32(btcCliBlockHeader.Version), prevBlockHash, merkleRoot, uint32(bits), uint32(btcCliBlockHeader.Nonce))
bh.Timestamp = time.Unix(int64(btcCliBlockHeader.Time), 0)
t.Logf(spew.Sdump(bh))
return tbcapi.BlockHeadersByHeightResponse{
BlockHeaders: wireBlockHeadersToTBC([]*wire.BlockHeader{bh}),
}
}

func blockAtHeight(ctx context.Context, t *testing.T, bitcoindContainer testcontainers.Container, height uint64) BtcCliBlockHeader {
blockHash, err := runBitcoinCommand(
ctx,
Expand Down

0 comments on commit e08522b

Please sign in to comment.