Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 5 additions & 5 deletions blockchain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,12 @@ func (b *BlockChain) calcSequenceLock(node *blockNode, tx *hnsutil.Tx, utxoView
// any given height or time.
sequenceLock := &SequenceLock{Seconds: -1, BlockHeight: -1}

// Handshake sequence-lock semantics are active consensus rules for
// version 2+ transactions. They are not gated behind Bitcoin's BIP9 CSV
// deployment. Sequence locks also do not apply to coinbase transactions.
// Handshake sequence-lock semantics are active consensus rules for all
// transaction versions. They are not gated behind Bitcoin's BIP9 CSV
// deployment or the inherited Bitcoin tx version >= 2 rule. Sequence locks
// also do not apply to coinbase transactions.
mTx := tx.MsgTx()
sequenceLockActive := uint32(mTx.Version) >= 2
if !sequenceLockActive || IsCoinBase(tx) {
if IsCoinBase(tx) {
return sequenceLock, nil
}

Expand Down
8 changes: 3 additions & 5 deletions blockchain/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,10 @@ func TestCalcSequenceLock(t *testing.T) {
mempool bool
want *SequenceLock
}{
// A transaction of version one should disable sequence locks
// as the new sequence number semantics only apply to
// transactions version 2 or higher.
// Handshake applies sequence locks to version 0 transactions.
{
tx: &wire.MsgTx{
Version: 1,
Version: 0,
TxIn: []*wire.TxIn{{
PreviousOutPoint: utxo,
Sequence: LockTimeToSequence(false, 3),
Expand All @@ -237,7 +235,7 @@ func TestCalcSequenceLock(t *testing.T) {
view: utxoView,
want: &SequenceLock{
Seconds: -1,
BlockHeight: -1,
BlockHeight: prevUtxoHeight + 2,
},
},
// A transaction with a single input with max sequence number.
Expand Down
25 changes: 24 additions & 1 deletion blockchain/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const (
maxNameSize = 63
maxResourceSize = 512

maxCovenantItems = 1000
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
maxCovenantSize = 1 + 32 + 1 + 4 + 2 + maxResourceSize + 1 + 32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for maxCovenantSize or maxResourceSize definitions and usage
rg -nP 'maxCovenantSize|maxResourceSize' blockchain/names.go
rg -nP 'maxCovenantItems' blockchain/ wire/

Repository: blinklabs-io/handshake-node

Length of output: 1251


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant logic in the local repository.
sed -n '1,120p' blockchain/names.go
printf '\n---\n'
sed -n '860,910p' blockchain/names.go
printf '\n---\n'
sed -n '1,220p' wire/covenant.go

Repository: blinklabs-io/handshake-node

Length of output: 9338


🌐 Web query:

hsd covenant size limit maxCovenantSize maxResourceSize covenantItemsSize consensus constant

💡 Result:

In the hsd implementation of the Handshake protocol, covenant size limits are governed by constants defined in lib/covenants/rules.js [1][2]. The relevant consensus-related constraints are: MAX_RESOURCE_SIZE: This is set to 512 bytes [1][2]. It defines the maximum allowed size for record data within a covenant (specifically used for items[2] in a covenant) [1][2][3]. MAX_COVENANT_SIZE: This represents the maximum total serialized size of a covenant [1][2]. It is calculated as a constant expression: 2 + MAX_RESOURCE_SIZE + 1 + 32 [1][2]. Given MAX_RESOURCE_SIZE is 512, this results in a maximum size of 547 bytes [1][2]. These rules are enforced during transaction validation to ensure that covenant data, such as DNS resource records, remains within the defined protocol constraints [1][2][3]. While consensus.js manages network-wide constants like block size (1,000,000 bytes) [4][5], the covenant-specific limits are handled within the covenants module to maintain naming system integrity [6][7].

Citations:


🌐 Web query:

hsd lib/covenants/rules.js MAX_COVENANT_SIZE MAX_RESOURCE_SIZE formula unknown covenant items size

💡 Result:

In the hsd library, the file lib/covenants/rules.js manages protocol limits for Handshake name covenants [1][2]. The constants MAX_COVENANT_SIZE and MAX_RESOURCE_SIZE are defined to enforce consensus rules regarding the size of covenants and their embedded resource data [1][3]. MAX_RESOURCE_SIZE is the limit on the size of record data within a covenant. Historically, this limit was 512 bytes [1][3]. Following a protocol hard fork (as noted in recent updates), this limit has been increased to 8,192 bytes to accommodate richer zone files [3][4]. The calculation for MAX_COVENANT_SIZE is defined in the source code as: rules.MAX_COVENANT_SIZE = (0 + 1 + 32 + 2 + rules.MAX_RESOURCE_SIZE + 1 + 32); In this formula, the components represent: - 1: A byte for the covenant type. - 32: A 32-byte hash (typically the name hash). - 2: An overhead allowance (or specific protocol field allocation). - rules.MAX_RESOURCE_SIZE: The maximum size permitted for the resource record data [1][5]. - 1 + 32: Additional structural or metadata padding used in the covenant serialization [1][6]. Essentially, the total covenant size limit is derived from the sum of these fixed-size fields plus the current maximum allowed resource size [1]. Enforcement of these limits occurs during block validation, where the system checks both the length of specific items (like record data) and the total size of the covenant object using these defined constants [1][3].

Citations:


Align maxCovenantSize with hsd's current consensus bound. hsd now derives MAX_COVENANT_SIZE from 1 + 32 + 2 + MAX_RESOURCE_SIZE + 1 + 32, and MAX_RESOURCE_SIZE is 8192 upstream, so the local 1 + 32 + 1 + 4 + 2 + maxResourceSize + 1 + 32 bound is stale and can diverge from consensus for unknown covenants. Also, maxCovenantItems = 1000 is duplicated in wire/covenant.go; add a cross-reference or centralize it to keep the two packages in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blockchain/names.go` around lines 24 - 26, Update maxCovenantSize in names.go
to derive from hsd’s current consensus formula, using the existing
maxResourceSize value and removing the stale extra fields. Address the
duplicated maxCovenantItems definition between names.go and wire/covenant.go by
centralizing the constant or adding a clear cross-reference so both packages
remain synchronized.

maxBlockNameOpens = 300
maxBlockNameUpdates = 600
maxBlockNameRenewals = 600
Expand Down Expand Up @@ -868,13 +871,33 @@ func checkCovenantSanity(tx *hnsutil.Tx) error {
return err
}
default:
return badCovenant("unknown covenant type")
if err := checkUnknownCovenant(covenant); err != nil {
return err
}
}
}

return nil
}

func checkUnknownCovenant(covenant wire.Covenant) error {
if len(covenant.Items) > maxCovenantItems {
return badCovenant("unknown covenant has too many items")
}
if covenantItemsSize(covenant) > maxCovenantSize {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
return badCovenant("unknown covenant is too large")
}
return nil
}

func covenantItemsSize(covenant wire.Covenant) int {
size := 0
for _, item := range covenant.Items {
size += wire.VarIntSerializeSize(uint64(len(item))) + len(item)
}
return size
}

func checkTransactionNameLimits(tx *hnsutil.Tx) error {
var opens, updates, renewals int

Expand Down
86 changes: 86 additions & 0 deletions blockchain/names_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,67 @@ func TestCheckCovenantSanityOpenRejectsHashMismatch(t *testing.T) {
}
}

func TestCheckCovenantSanityUnknownCovenants(t *testing.T) {
t.Run("allows hsd max item size", func(t *testing.T) {
tx := wire.NewMsgTx(1)
tx.AddTxIn(wire.NewTxIn(testOutPoint(1), math.MaxUint32,
nil))
tx.AddTxOut(wire.NewTxOut(1, wire.Address{}, wire.Covenant{
Type: wire.CovenantRevoke + 1,
Items: [][]byte{
make([]byte, maxCovenantSize-
wire.VarIntSerializeSize(uint64(maxCovenantSize))),
},
}))

if err := CheckTransactionSanity(hnsutil.NewTx(tx)); err != nil {
t.Fatalf("CheckTransactionSanity: %v", err)
}
})

t.Run("allows hsd max empty item count by size", func(t *testing.T) {
items := make([][]byte, maxCovenantSize)
for i := range items {
items[i] = []byte{}
}

tx := wire.NewMsgTx(1)
tx.AddTxIn(wire.NewTxIn(testOutPoint(2), math.MaxUint32,
nil))
tx.AddTxOut(wire.NewTxOut(1, wire.Address{}, wire.Covenant{
Type: wire.CovenantRevoke + 1,
Items: items,
}))

if err := CheckTransactionSanity(hnsutil.NewTx(tx)); err != nil {
t.Fatalf("CheckTransactionSanity: %v", err)
}
})

t.Run("rejects over hsd covenant size", func(t *testing.T) {
tx := wire.NewMsgTx(1)
tx.AddTxIn(wire.NewTxIn(testOutPoint(3), math.MaxUint32,
nil))
tx.AddTxOut(wire.NewTxOut(1, wire.Address{}, wire.Covenant{
Type: wire.CovenantRevoke + 1,
Items: [][]byte{
make([]byte, maxCovenantSize),
},
}))

err := CheckTransactionSanity(hnsutil.NewTx(tx))
if err == nil {
t.Fatal("CheckTransactionSanity: expected size error")
}
if ruleErr, ok := err.(RuleError); !ok ||
ruleErr.ErrorCode != ErrInvalidCovenant {

t.Fatalf("CheckTransactionSanity error = %T %v, want ErrInvalidCovenant",
err, err)
}
})
}

func TestCheckBlockNameLimitsDuplicateAcrossTransactions(t *testing.T) {
block := &wire.MsgBlock{}
for i := 0; i < 2; i++ {
Expand Down Expand Up @@ -276,6 +337,31 @@ func TestVerifyCovenantSpendBidRevealBlind(t *testing.T) {
}
}

func TestVerifyCovenantSpendUnknownCovenant(t *testing.T) {
prev := namePrevOutput{
outpoint: *testOutPoint(1),
amount: 10,
covenant: wire.Covenant{
Type: wire.CovenantRevoke + 1,
Items: [][]byte{
[]byte("future"),
},
},
}

if err := verifyCovenantSpend(prev,
wire.NewTxOut(1, wire.Address{}, wire.Covenant{})); err != nil {

t.Fatalf("verifyCovenantSpend unknown to NONE: %v", err)
}

if err := verifyCovenantSpend(prev,
wire.NewTxOut(1, wire.Address{}, openCovenant("known"))); err == nil {

t.Fatal("verifyCovenantSpend: expected unknown to known name rejection")
}
}

func TestNameBlockViewOpenLifecycle(t *testing.T) {
params := chaincfg.RegressionNetParams
params.NameNoRollout = true
Expand Down
36 changes: 36 additions & 0 deletions txscript/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,42 @@ func TestStandardVerifyFlagsExcludeTaproot(t *testing.T) {
}
}

func TestCheckSequenceVerifyAllowsVersionZero(t *testing.T) {
t.Parallel()

pkScript, err := NewScriptBuilder().
AddInt64(1).
AddOp(OP_CHECKSEQUENCEVERIFY).
AddOp(OP_TRUE).
Script()
if err != nil {
t.Fatalf("script build: %v", err)
}

tx := &wire.MsgTx{
Version: 0,
TxIn: []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{0x01},
Index: 0,
},
Sequence: 1,
}},
TxOut: []*wire.TxOut{{
Value: 1000000000,
}},
}

vm, err := NewEngine(pkScript, tx, 0, ScriptVerifyCheckSequenceVerify,
nil, nil, -1, nil)
if err != nil {
t.Fatalf("NewEngine: %v", err)
}
if err := vm.Execute(); err != nil {
t.Fatalf("Execute: %v", err)
}
}

// TestCheckPubKeyEncoding ensures the internal checkPubKeyEncoding function
// works as expected.
func TestCheckPubKeyEncoding(t *testing.T) {
Expand Down
8 changes: 0 additions & 8 deletions txscript/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -1181,14 +1181,6 @@ func opcodeCheckSequenceVerify(op *opcode, data []byte, vm *Engine) error {
return nil
}

// Transaction version numbers not high enough to trigger CSV rules must
// fail.
if uint32(vm.tx.Version) < 2 {
str := fmt.Sprintf("invalid transaction version: %d",
vm.tx.Version)
return scriptError(ErrUnsatisfiedLockTime, str)
}

// Sequence numbers with their most significant bit set are not
// consensus constrained. Testing that the transaction's sequence
// number does not have this bit set prevents using this property
Expand Down
36 changes: 18 additions & 18 deletions wire/covenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,17 @@ const (
// CovenantRevoke represents revoking a name.
CovenantRevoke uint8 = 11

// maxCovenantType is the highest valid covenant type value.
// maxCovenantType is the highest known covenant type value.
maxCovenantType = CovenantRevoke

// maxCovenantItems is the maximum number of items a covenant can have.
// This is based on the Handshake protocol's maximum of 255 items.
maxCovenantItems = 255
// This matches hsd's consensus MAX_SCRIPT_STACK bound.
maxCovenantItems = 1000

// maxCovenantItemSize is the maximum size of a single covenant item.
// This limits memory usage during deserialization.
maxCovenantItemSize = 520
// It is large enough to admit every hsd-sane unknown covenant while still
// bounding memory during deserialization.
maxCovenantItemSize = 585
)

// covenantTypeNames maps covenant types to their human-readable names.
Expand Down Expand Up @@ -89,13 +90,6 @@ type Covenant struct {
func validateCovenantFields(covenantType uint8, itemCount int, items [][]byte,
op string) error {

if covenantType > maxCovenantType {
return messageError(op, fmt.Sprintf(
"covenant type %d exceeds max %d",
covenantType, maxCovenantType,
))
}

if itemCount > maxCovenantItems {
str := fmt.Sprintf("covenant item count is too large "+
"[count %d, max %d]", itemCount, maxCovenantItems)
Expand Down Expand Up @@ -151,12 +145,6 @@ func (c *Covenant) Decode(r io.Reader) error {
return err
}
c.Type = typ
if typ > maxCovenantType {
return messageError("Covenant.Decode", fmt.Sprintf(
"covenant type %d exceeds max %d",
typ, maxCovenantType,
))
}

itemCount, err := ReadVarInt(r, 0)
if err != nil {
Expand Down Expand Up @@ -208,6 +196,18 @@ func (c *Covenant) String() string {
return fmt.Sprintf("UNKNOWN(%d)", c.Type)
}

// IsKnown returns whether the covenant type is one of the currently defined
// Handshake covenant types.
func (c *Covenant) IsKnown() bool {
return c.Type <= maxCovenantType
}

// IsUnknown returns whether the covenant type is reserved for a future
// Handshake covenant extension.
func (c *Covenant) IsUnknown() bool {
return !c.IsKnown()
}

// NewCovenant returns a new Covenant with the given type and items.
func NewCovenant(covenantType uint8, items [][]byte) *Covenant {
return &Covenant{
Expand Down
18 changes: 18 additions & 0 deletions wire/covenant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ func TestCovenantEncodeDecode(t *testing.T) {
name: "NONE covenant with empty items slice",
covenant: NewCovenant(CovenantNone, [][]byte{}),
},
{
name: "unknown covenant with hsd-sized item",
covenant: NewCovenant(CovenantRevoke+1, [][]byte{
make([]byte, 582),
}),
},
{
name: "REGISTER covenant with empty item",
covenant: NewCovenant(CovenantRegister, [][]byte{
Expand Down Expand Up @@ -240,6 +246,18 @@ func TestCovenantString(t *testing.T) {
}
}

func TestCovenantKnownUnknown(t *testing.T) {
known := NewCovenant(CovenantRevoke, nil)
if !known.IsKnown() || known.IsUnknown() {
t.Fatal("REVOKE covenant should be known")
}

unknown := NewCovenant(CovenantRevoke+1, nil)
if unknown.IsKnown() || !unknown.IsUnknown() {
t.Fatal("covenant above REVOKE should be unknown")
}
}

// TestAddressEncodeDecode tests round-trip encoding and decoding of addresses
// with both 20-byte and 32-byte hashes at version 0.
func TestAddressEncodeDecode(t *testing.T) {
Expand Down
Loading