Skip to content

Commit b879210

Browse files
committed
feat: support for decoding Handshake domain record data
This adds support for parsing generic covenant data into specific covenant types. It also adds support for parsing the domain resource data included with the Update and Register covenant types Fixes #258 Signed-off-by: Aurora Gaffney <[email protected]>
1 parent 09a7701 commit b879210

File tree

6 files changed

+724
-29
lines changed

6 files changed

+724
-29
lines changed

internal/handshake/block_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func TestDecodeHandshakeBlock(t *testing.T) {
9595
Version: 0,
9696
Hash: decodeHex("5ad99a3052017938562ede6e228b68ca50c14663"),
9797
},
98-
Covenant: handshake.Covenant{
98+
Covenant: handshake.GenericCovenant{
9999
Type: 8,
100100
Items: [][]byte{
101101
decodeHex("c89c49ce327748244702f481f35097199cca2f7c2549a33ecacbdf973690e534"),

internal/handshake/covenant.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2025 Blink Labs Software
2+
//
3+
// Use of this source code is governed by an MIT-style
4+
// license that can be found in the LICENSE file or at
5+
// https://opensource.org/licenses/MIT.
6+
7+
package handshake
8+
9+
import (
10+
"encoding/binary"
11+
"errors"
12+
"io"
13+
)
14+
15+
// Covenant types
16+
const (
17+
CovenantTypeNone = 0
18+
CovenantTypeClaim = 1
19+
CovenantTypeOpen = 2
20+
CovenantTypeBid = 3
21+
CovenantTypeReveal = 4
22+
CovenantTypeRedeem = 5
23+
CovenantTypeRegister = 6
24+
CovenantTypeUpdate = 7
25+
CovenantTypeRenew = 8
26+
CovenantTypeTransfer = 9
27+
CovenantTypeFinalize = 10
28+
CovenantTypeRevoke = 11
29+
)
30+
31+
type Covenant interface {
32+
isCovenant()
33+
}
34+
35+
type GenericCovenant struct {
36+
Type uint8
37+
Items [][]byte
38+
}
39+
40+
func (*GenericCovenant) isCovenant() {}
41+
42+
func (c *GenericCovenant) Decode(r io.Reader) error {
43+
if err := binary.Read(r, binary.LittleEndian, &c.Type); err != nil {
44+
return err
45+
}
46+
itemCount, err := binary.ReadUvarint(r.(io.ByteReader))
47+
if err != nil {
48+
return err
49+
}
50+
for i := uint64(0); i < itemCount; i++ {
51+
itemLength, err := binary.ReadUvarint(r.(io.ByteReader))
52+
if err != nil {
53+
return err
54+
}
55+
item := make([]byte, itemLength)
56+
if err := binary.Read(r, binary.LittleEndian, &item); err != nil {
57+
return err
58+
}
59+
c.Items = append(c.Items, item)
60+
}
61+
return nil
62+
}
63+
64+
func (c *GenericCovenant) Covenant() Covenant {
65+
switch c.Type {
66+
case CovenantTypeRegister:
67+
ret, err := NewRegisterCovenantFromGeneric(c)
68+
if err != nil {
69+
panic("can't convert generic covenant to Register")
70+
}
71+
return ret
72+
case CovenantTypeUpdate:
73+
ret, err := NewUpdateCovenantFromGeneric(c)
74+
if err != nil {
75+
panic("can't convert generic covenant to Update")
76+
}
77+
return ret
78+
}
79+
// Return generic covenant (ourselves)
80+
return c
81+
}
82+
83+
type RegisterCovenant struct {
84+
NameHash []byte
85+
Height uint32
86+
ResourceData DomainResourceData
87+
BlockHash []byte
88+
}
89+
90+
func (RegisterCovenant) isCovenant() {}
91+
92+
func NewRegisterCovenantFromGeneric(gc *GenericCovenant) (*RegisterCovenant, error) {
93+
if gc.Type != CovenantTypeRegister {
94+
return nil, errors.New("wrong covenant type")
95+
}
96+
if len(gc.Items) != 4 {
97+
return nil, errors.New("incorrect items length")
98+
}
99+
ret := &RegisterCovenant{
100+
NameHash: make([]byte, len(gc.Items[0])),
101+
BlockHash: make([]byte, len(gc.Items[3])),
102+
}
103+
// Copy hashes
104+
copy(ret.NameHash, gc.Items[0])
105+
copy(ret.BlockHash, gc.Items[3])
106+
// Decode height from bytes
107+
ret.Height = binary.LittleEndian.Uint32(gc.Items[1])
108+
// Decode resource data
109+
tmpData, err := NewDomainResourceDataFromBytes(gc.Items[2])
110+
if err != nil {
111+
return nil, err
112+
}
113+
ret.ResourceData = *tmpData
114+
return ret, nil
115+
}
116+
117+
type UpdateCovenant struct {
118+
NameHash []byte
119+
Height uint32
120+
ResourceData DomainResourceData
121+
BlockHash []byte
122+
}
123+
124+
func (UpdateCovenant) isCovenant() {}
125+
126+
func NewUpdateCovenantFromGeneric(gc *GenericCovenant) (*UpdateCovenant, error) {
127+
if gc.Type != CovenantTypeUpdate {
128+
return nil, errors.New("wrong covenant type")
129+
}
130+
if len(gc.Items) != 4 {
131+
return nil, errors.New("incorrect items length")
132+
}
133+
ret := &UpdateCovenant{
134+
NameHash: make([]byte, len(gc.Items[0])),
135+
BlockHash: make([]byte, len(gc.Items[3])),
136+
}
137+
// Copy hashes
138+
copy(ret.NameHash, gc.Items[0])
139+
copy(ret.BlockHash, gc.Items[3])
140+
// Decode height from bytes
141+
ret.Height = binary.LittleEndian.Uint32(gc.Items[1])
142+
// Decode resource data
143+
tmpData, err := NewDomainResourceDataFromBytes(gc.Items[2])
144+
if err != nil {
145+
return nil, err
146+
}
147+
ret.ResourceData = *tmpData
148+
return ret, nil
149+
}

internal/handshake/covenant_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2025 Blink Labs Software
2+
//
3+
// Use of this source code is governed by an MIT-style
4+
// license that can be found in the LICENSE file or at
5+
// https://opensource.org/licenses/MIT.
6+
7+
package handshake_test
8+
9+
import (
10+
"net"
11+
"reflect"
12+
"testing"
13+
14+
"github.com/blinklabs-io/cdnsd/internal/handshake"
15+
)
16+
17+
func TestCovenantRegisterFromGeneric(t *testing.T) {
18+
// This data comes from mainnet TX 63ba84b6362724aa8fd484d3616c8d1bdea68240c8e0cd6a104fcf85a35d52fb
19+
testGenericCovenant := &handshake.GenericCovenant{
20+
Type: handshake.CovenantTypeRegister,
21+
Items: [][]byte{
22+
decodeHex("62a90ce374b1499d0b67b1e4e6164b18acacbd16be905a6ffee593d48d4e0a82"),
23+
decodeHex("8d1e0400"),
24+
decodeHex("0002036e73310a69727677696c6c69616d002ce706b701c00202036e7332c00636d688f601c01a00d5580d0114402ed0125506f35ba249265f39b988d7028a28c300d5580d02200c6c45064c26b529b4ac074dff5de60a99d6025d5b0d7f32c2b8c7d40ec8b3de00d5580d043071cb0417852b08b965413f3b871b033996159d121a585e35111a335d4cfb79b67e49a99c3829f6a1f42e100f7f33d7d9"),
25+
decodeHex("0000000000000000153c62dbcabb762c254fb4104ab7cdd779926b79b34601fc"),
26+
},
27+
}
28+
expectedCovenant := &handshake.RegisterCovenant{
29+
NameHash: decodeHex("62a90ce374b1499d0b67b1e4e6164b18acacbd16be905a6ffee593d48d4e0a82"),
30+
Height: 269965,
31+
ResourceData: handshake.DomainResourceData{
32+
Version: 0,
33+
Records: []handshake.DomainRecord{
34+
&handshake.Glue4DomainRecord{
35+
Name: "ns1.irvwilliam.",
36+
Address: net.ParseIP("44.231.6.183").To4(),
37+
},
38+
&handshake.NsDomainRecord{
39+
Name: "ns1.irvwilliam.",
40+
},
41+
&handshake.Glue4DomainRecord{
42+
Name: "ns2.irvwilliam.",
43+
Address: net.ParseIP("54.214.136.246").To4(),
44+
},
45+
&handshake.NsDomainRecord{
46+
Name: "ns2.irvwilliam.",
47+
},
48+
&handshake.DsDomainRecord{
49+
KeyTag: 54616,
50+
Algorithm: 13,
51+
DigestType: 1,
52+
Digest: decodeHex("402ed0125506f35ba249265f39b988d7028a28c3"),
53+
},
54+
&handshake.DsDomainRecord{
55+
KeyTag: 54616,
56+
Algorithm: 13,
57+
DigestType: 2,
58+
Digest: decodeHex("0c6c45064c26b529b4ac074dff5de60a99d6025d5b0d7f32c2b8c7d40ec8b3de"),
59+
},
60+
&handshake.DsDomainRecord{
61+
KeyTag: 54616,
62+
Algorithm: 13,
63+
DigestType: 4,
64+
Digest: decodeHex("71cb0417852b08b965413f3b871b033996159d121a585e35111a335d4cfb79b67e49a99c3829f6a1f42e100f7f33d7d9"),
65+
},
66+
},
67+
},
68+
BlockHash: decodeHex("0000000000000000153c62dbcabb762c254fb4104ab7cdd779926b79b34601fc"),
69+
}
70+
tmpCovenant, err := handshake.NewRegisterCovenantFromGeneric(testGenericCovenant)
71+
if err != nil {
72+
t.Fatalf("unexpected error creating RegisterCovenant from GenericCovenant: %s", err)
73+
}
74+
if !reflect.DeepEqual(tmpCovenant, expectedCovenant) {
75+
t.Fatalf(
76+
"did not get expected covenant:\n got: %#v\n wanted: %#v",
77+
tmpCovenant,
78+
expectedCovenant,
79+
)
80+
}
81+
}
82+
83+
func TestCovenantUpdateFromGeneric(t *testing.T) {
84+
// This data comes from mainnet TX 63ba84b6362724aa8fd484d3616c8d1bdea68240c8e0cd6a104fcf85a35d52fb
85+
testGenericCovenant := &handshake.GenericCovenant{
86+
Type: handshake.CovenantTypeUpdate,
87+
Items: [][]byte{
88+
decodeHex("62a90ce374b1499d0b67b1e4e6164b18acacbd16be905a6ffee593d48d4e0a82"),
89+
decodeHex("8d1e0400"),
90+
decodeHex("0002036e73310a69727677696c6c69616d002ce706b701c00202036e7332c00636d688f601c01a00d5580d0114402ed0125506f35ba249265f39b988d7028a28c300d5580d02200c6c45064c26b529b4ac074dff5de60a99d6025d5b0d7f32c2b8c7d40ec8b3de00d5580d043071cb0417852b08b965413f3b871b033996159d121a585e35111a335d4cfb79b67e49a99c3829f6a1f42e100f7f33d7d9"),
91+
decodeHex("0000000000000000153c62dbcabb762c254fb4104ab7cdd779926b79b34601fc"),
92+
},
93+
}
94+
expectedCovenant := &handshake.UpdateCovenant{
95+
NameHash: decodeHex("62a90ce374b1499d0b67b1e4e6164b18acacbd16be905a6ffee593d48d4e0a82"),
96+
Height: 269965,
97+
ResourceData: handshake.DomainResourceData{
98+
Version: 0,
99+
Records: []handshake.DomainRecord{
100+
&handshake.Glue4DomainRecord{
101+
Name: "ns1.irvwilliam.",
102+
Address: net.ParseIP("44.231.6.183").To4(),
103+
},
104+
&handshake.NsDomainRecord{
105+
Name: "ns1.irvwilliam.",
106+
},
107+
&handshake.Glue4DomainRecord{
108+
Name: "ns2.irvwilliam.",
109+
Address: net.ParseIP("54.214.136.246").To4(),
110+
},
111+
&handshake.NsDomainRecord{
112+
Name: "ns2.irvwilliam.",
113+
},
114+
&handshake.DsDomainRecord{
115+
KeyTag: 54616,
116+
Algorithm: 13,
117+
DigestType: 1,
118+
Digest: decodeHex("402ed0125506f35ba249265f39b988d7028a28c3"),
119+
},
120+
&handshake.DsDomainRecord{
121+
KeyTag: 54616,
122+
Algorithm: 13,
123+
DigestType: 2,
124+
Digest: decodeHex("0c6c45064c26b529b4ac074dff5de60a99d6025d5b0d7f32c2b8c7d40ec8b3de"),
125+
},
126+
&handshake.DsDomainRecord{
127+
KeyTag: 54616,
128+
Algorithm: 13,
129+
DigestType: 4,
130+
Digest: decodeHex("71cb0417852b08b965413f3b871b033996159d121a585e35111a335d4cfb79b67e49a99c3829f6a1f42e100f7f33d7d9"),
131+
},
132+
},
133+
},
134+
BlockHash: decodeHex("0000000000000000153c62dbcabb762c254fb4104ab7cdd779926b79b34601fc"),
135+
}
136+
tmpCovenant, err := handshake.NewUpdateCovenantFromGeneric(testGenericCovenant)
137+
if err != nil {
138+
t.Fatalf("unexpected error creating UpdateCovenant from GenericCovenant: %s", err)
139+
}
140+
if !reflect.DeepEqual(tmpCovenant, expectedCovenant) {
141+
t.Fatalf(
142+
"did not get expected covenant:\n got: %#v\n wanted: %#v",
143+
tmpCovenant,
144+
expectedCovenant,
145+
)
146+
}
147+
}

0 commit comments

Comments
 (0)