Skip to content

feat: support for decoding Handshake domain record data #327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/handshake/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func TestDecodeHandshakeBlock(t *testing.T) {
Version: 0,
Hash: decodeHex("5ad99a3052017938562ede6e228b68ca50c14663"),
},
Covenant: handshake.Covenant{
Covenant: handshake.GenericCovenant{
Type: 8,
Items: [][]byte{
decodeHex("c89c49ce327748244702f481f35097199cca2f7c2549a33ecacbdf973690e534"),
Expand Down
149 changes: 149 additions & 0 deletions internal/handshake/covenant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2025 Blink Labs Software
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package handshake

import (
"encoding/binary"
"errors"
"io"
)

// Covenant types
const (
CovenantTypeNone = 0
CovenantTypeClaim = 1
CovenantTypeOpen = 2
CovenantTypeBid = 3
CovenantTypeReveal = 4
CovenantTypeRedeem = 5
CovenantTypeRegister = 6
CovenantTypeUpdate = 7
CovenantTypeRenew = 8
CovenantTypeTransfer = 9
CovenantTypeFinalize = 10
CovenantTypeRevoke = 11
)

type Covenant interface {
isCovenant()
}

type GenericCovenant struct {
Type uint8
Items [][]byte
}

func (*GenericCovenant) isCovenant() {}

func (c *GenericCovenant) Decode(r io.Reader) error {
if err := binary.Read(r, binary.LittleEndian, &c.Type); err != nil {
return err
}
itemCount, err := binary.ReadUvarint(r.(io.ByteReader))
if err != nil {
return err
}
for i := uint64(0); i < itemCount; i++ {
itemLength, err := binary.ReadUvarint(r.(io.ByteReader))
if err != nil {
return err
}
item := make([]byte, itemLength)
if err := binary.Read(r, binary.LittleEndian, &item); err != nil {
return err
}
c.Items = append(c.Items, item)
}
return nil
}

func (c *GenericCovenant) Covenant() Covenant {
switch c.Type {
case CovenantTypeRegister:
ret, err := NewRegisterCovenantFromGeneric(c)
if err != nil {
panic("can't convert generic covenant to Register")
}
return ret
case CovenantTypeUpdate:
ret, err := NewUpdateCovenantFromGeneric(c)
if err != nil {
panic("can't convert generic covenant to Update")
}
return ret
}
// Return generic covenant (ourselves)
return c
}

type RegisterCovenant struct {
NameHash []byte
Height uint32
ResourceData DomainResourceData
BlockHash []byte
}

func (RegisterCovenant) isCovenant() {}

func NewRegisterCovenantFromGeneric(gc *GenericCovenant) (*RegisterCovenant, error) {
if gc.Type != CovenantTypeRegister {
return nil, errors.New("wrong covenant type")
}
if len(gc.Items) != 4 {
return nil, errors.New("incorrect items length")
}
ret := &RegisterCovenant{
NameHash: make([]byte, len(gc.Items[0])),
BlockHash: make([]byte, len(gc.Items[3])),
}
// Copy hashes
copy(ret.NameHash, gc.Items[0])
copy(ret.BlockHash, gc.Items[3])
// Decode height from bytes
ret.Height = binary.LittleEndian.Uint32(gc.Items[1])
// Decode resource data
tmpData, err := NewDomainResourceDataFromBytes(gc.Items[2])
if err != nil {
return nil, err
}
ret.ResourceData = *tmpData
return ret, nil
}

type UpdateCovenant struct {
NameHash []byte
Height uint32
ResourceData DomainResourceData
BlockHash []byte
}

func (UpdateCovenant) isCovenant() {}

func NewUpdateCovenantFromGeneric(gc *GenericCovenant) (*UpdateCovenant, error) {
if gc.Type != CovenantTypeUpdate {
return nil, errors.New("wrong covenant type")
}
if len(gc.Items) != 4 {
return nil, errors.New("incorrect items length")
}
ret := &UpdateCovenant{
NameHash: make([]byte, len(gc.Items[0])),
BlockHash: make([]byte, len(gc.Items[3])),
}
// Copy hashes
copy(ret.NameHash, gc.Items[0])
copy(ret.BlockHash, gc.Items[3])
// Decode height from bytes
ret.Height = binary.LittleEndian.Uint32(gc.Items[1])
// Decode resource data
tmpData, err := NewDomainResourceDataFromBytes(gc.Items[2])
if err != nil {
return nil, err
}
ret.ResourceData = *tmpData
return ret, nil
}
147 changes: 147 additions & 0 deletions internal/handshake/covenant_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2025 Blink Labs Software
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package handshake_test

import (
"net"
"reflect"
"testing"

"github.com/blinklabs-io/cdnsd/internal/handshake"
)

func TestCovenantRegisterFromGeneric(t *testing.T) {
// This data comes from mainnet TX 63ba84b6362724aa8fd484d3616c8d1bdea68240c8e0cd6a104fcf85a35d52fb
testGenericCovenant := &handshake.GenericCovenant{
Type: handshake.CovenantTypeRegister,
Items: [][]byte{
decodeHex("62a90ce374b1499d0b67b1e4e6164b18acacbd16be905a6ffee593d48d4e0a82"),
decodeHex("8d1e0400"),
decodeHex("0002036e73310a69727677696c6c69616d002ce706b701c00202036e7332c00636d688f601c01a00d5580d0114402ed0125506f35ba249265f39b988d7028a28c300d5580d02200c6c45064c26b529b4ac074dff5de60a99d6025d5b0d7f32c2b8c7d40ec8b3de00d5580d043071cb0417852b08b965413f3b871b033996159d121a585e35111a335d4cfb79b67e49a99c3829f6a1f42e100f7f33d7d9"),
decodeHex("0000000000000000153c62dbcabb762c254fb4104ab7cdd779926b79b34601fc"),
},
}
expectedCovenant := &handshake.RegisterCovenant{
NameHash: decodeHex("62a90ce374b1499d0b67b1e4e6164b18acacbd16be905a6ffee593d48d4e0a82"),
Height: 269965,
ResourceData: handshake.DomainResourceData{
Version: 0,
Records: []handshake.DomainRecord{
&handshake.Glue4DomainRecord{
Name: "ns1.irvwilliam.",
Address: net.ParseIP("44.231.6.183").To4(),
},
&handshake.NsDomainRecord{
Name: "ns1.irvwilliam.",
},
&handshake.Glue4DomainRecord{
Name: "ns2.irvwilliam.",
Address: net.ParseIP("54.214.136.246").To4(),
},
&handshake.NsDomainRecord{
Name: "ns2.irvwilliam.",
},
&handshake.DsDomainRecord{
KeyTag: 54616,
Algorithm: 13,
DigestType: 1,
Digest: decodeHex("402ed0125506f35ba249265f39b988d7028a28c3"),
},
&handshake.DsDomainRecord{
KeyTag: 54616,
Algorithm: 13,
DigestType: 2,
Digest: decodeHex("0c6c45064c26b529b4ac074dff5de60a99d6025d5b0d7f32c2b8c7d40ec8b3de"),
},
&handshake.DsDomainRecord{
KeyTag: 54616,
Algorithm: 13,
DigestType: 4,
Digest: decodeHex("71cb0417852b08b965413f3b871b033996159d121a585e35111a335d4cfb79b67e49a99c3829f6a1f42e100f7f33d7d9"),
},
},
},
BlockHash: decodeHex("0000000000000000153c62dbcabb762c254fb4104ab7cdd779926b79b34601fc"),
}
tmpCovenant, err := handshake.NewRegisterCovenantFromGeneric(testGenericCovenant)
if err != nil {
t.Fatalf("unexpected error creating RegisterCovenant from GenericCovenant: %s", err)
}
if !reflect.DeepEqual(tmpCovenant, expectedCovenant) {
t.Fatalf(
"did not get expected covenant:\n got: %#v\n wanted: %#v",
tmpCovenant,
expectedCovenant,
)
}
}

func TestCovenantUpdateFromGeneric(t *testing.T) {
// This data comes from mainnet TX 63ba84b6362724aa8fd484d3616c8d1bdea68240c8e0cd6a104fcf85a35d52fb
testGenericCovenant := &handshake.GenericCovenant{
Type: handshake.CovenantTypeUpdate,
Items: [][]byte{
decodeHex("62a90ce374b1499d0b67b1e4e6164b18acacbd16be905a6ffee593d48d4e0a82"),
decodeHex("8d1e0400"),
decodeHex("0002036e73310a69727677696c6c69616d002ce706b701c00202036e7332c00636d688f601c01a00d5580d0114402ed0125506f35ba249265f39b988d7028a28c300d5580d02200c6c45064c26b529b4ac074dff5de60a99d6025d5b0d7f32c2b8c7d40ec8b3de00d5580d043071cb0417852b08b965413f3b871b033996159d121a585e35111a335d4cfb79b67e49a99c3829f6a1f42e100f7f33d7d9"),
decodeHex("0000000000000000153c62dbcabb762c254fb4104ab7cdd779926b79b34601fc"),
},
}
expectedCovenant := &handshake.UpdateCovenant{
NameHash: decodeHex("62a90ce374b1499d0b67b1e4e6164b18acacbd16be905a6ffee593d48d4e0a82"),
Height: 269965,
ResourceData: handshake.DomainResourceData{
Version: 0,
Records: []handshake.DomainRecord{
&handshake.Glue4DomainRecord{
Name: "ns1.irvwilliam.",
Address: net.ParseIP("44.231.6.183").To4(),
},
&handshake.NsDomainRecord{
Name: "ns1.irvwilliam.",
},
&handshake.Glue4DomainRecord{
Name: "ns2.irvwilliam.",
Address: net.ParseIP("54.214.136.246").To4(),
},
&handshake.NsDomainRecord{
Name: "ns2.irvwilliam.",
},
&handshake.DsDomainRecord{
KeyTag: 54616,
Algorithm: 13,
DigestType: 1,
Digest: decodeHex("402ed0125506f35ba249265f39b988d7028a28c3"),
},
&handshake.DsDomainRecord{
KeyTag: 54616,
Algorithm: 13,
DigestType: 2,
Digest: decodeHex("0c6c45064c26b529b4ac074dff5de60a99d6025d5b0d7f32c2b8c7d40ec8b3de"),
},
&handshake.DsDomainRecord{
KeyTag: 54616,
Algorithm: 13,
DigestType: 4,
Digest: decodeHex("71cb0417852b08b965413f3b871b033996159d121a585e35111a335d4cfb79b67e49a99c3829f6a1f42e100f7f33d7d9"),
},
},
},
BlockHash: decodeHex("0000000000000000153c62dbcabb762c254fb4104ab7cdd779926b79b34601fc"),
}
tmpCovenant, err := handshake.NewUpdateCovenantFromGeneric(testGenericCovenant)
if err != nil {
t.Fatalf("unexpected error creating UpdateCovenant from GenericCovenant: %s", err)
}
if !reflect.DeepEqual(tmpCovenant, expectedCovenant) {
t.Fatalf(
"did not get expected covenant:\n got: %#v\n wanted: %#v",
tmpCovenant,
expectedCovenant,
)
}
}
Loading
Loading