[agent/codex:gpt-5.4-mini] Read the JS reference source at docs/js-covenants/ and docs/... #1

Open
Virgil wants to merge 15 commits from agent/read-the-js-reference-source-at-docs-js into dev
69 changed files with 2229 additions and 16 deletions

View file

@ -0,0 +1,13 @@
// SPDX-License-Identifier: EUPL-1.2
package nameutil
import (
"fmt"
)
func ExampleCanonicalize() {
got, _ := Canonicalize("Example.LTHN.")
fmt.Println(got)
// Output: example
}

View file

@ -0,0 +1,57 @@
// SPDX-License-Identifier: EUPL-1.2
package nameutil
import "testing"
func TestNameUtil_Function_Good(t *testing.T) {
got, ok := Canonicalize("Example.LTHN.")
if !ok {
t.Fatal("Canonicalize should accept canonical names")
}
if got != "example" {
t.Fatalf("Canonicalize() = %q, want %q", got, "example")
}
}
func TestNameUtil_Function_Bad(t *testing.T) {
if got, ok := Canonicalize(42); ok || got != "" {
t.Fatalf("Canonicalize() = %q, %v, want empty false", got, ok)
}
}
func TestNameUtil_Function_Ugly(t *testing.T) {
got, ok := CatalogLabel([]byte("MiXeD"))
if !ok {
t.Fatal("CatalogLabel should accept raw byte labels")
}
if got != "MiXeD" {
t.Fatalf("CatalogLabel() = %q, want %q", got, "MiXeD")
}
}
func TestName_Function_Good(t *testing.T) {
got, ok := Canonicalize("Example.lthn.")
if !ok || got != "example" {
t.Fatalf("Canonicalize() = %q, %v, want %q, true", got, ok, "example")
}
}
func TestName_Function_Bad(t *testing.T) {
if got, ok := Canonicalize(42); ok || got != "" {
t.Fatalf("Canonicalize() = %q, %v, want empty false", got, ok)
}
}
func TestName_Function_Ugly(t *testing.T) {
got, ok := CatalogLabel([]byte("MiXeD"))
if !ok {
t.Fatal("CatalogLabel should accept raw byte labels")
}
if got != "MiXeD" {
t.Fatalf("CatalogLabel() = %q, want %q", got, "MiXeD")
}
}

27
lns.go
View file

@ -1219,6 +1219,12 @@ type NameUndoEntry = primitives.NameUndoEntry
// Claim mirrors the raw ownership-proof claim wrapper from pkg/primitives.
type Claim = primitives.Claim
// InvItem mirrors the inventory item wrapper from pkg/primitives.
type InvItem = primitives.InvItem
// InvType mirrors the inventory item type tag from pkg/primitives.
type InvType = primitives.InvType
// NameStateJSON mirrors the primitive JSON representation for a name state.
type NameStateJSON = primitives.NameStateJSON
@ -1254,6 +1260,17 @@ const (
NameStateRevoked = primitives.NameStateRevoked
)
// Inventory item constants mirror pkg/primitives so callers can keep using the
// top-level lns package for common network wrappers.
const (
InvTypeTX = primitives.InvTypeTX
InvTypeBlock = primitives.InvTypeBlock
InvTypeFilteredBlock = primitives.InvTypeFilteredBlock
InvTypeCompactBlock = primitives.InvTypeCompactBlock
InvTypeClaim = primitives.InvTypeClaim
InvTypeAirdrop = primitives.InvTypeAirdrop
)
// NewClaim constructs an empty claim wrapper.
func NewClaim() *Claim {
return primitives.NewClaim()
@ -1264,6 +1281,16 @@ func GetNewClaim() *Claim {
return NewClaim()
}
// NewInvItem constructs an inventory item wrapper.
func NewInvItem(t InvType, hash primitives.Hash) *InvItem {
return primitives.NewInvItem(t, hash)
}
// GetNewInvItem is an alias for NewInvItem.
func GetNewInvItem(t InvType, hash primitives.Hash) *InvItem {
return NewInvItem(t, hash)
}
// NewOutpoint returns the null outpoint used by coinbase inputs and empty
// name-state owner references.
func NewOutpoint() Outpoint {

10
lns_example_test.go Normal file
View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package lns
import "fmt"
func ExampleGetServiceName() {
fmt.Println(GetServiceName())
// Output: lns
}

View file

@ -2172,3 +2172,38 @@ func TestServiceVerifyCovenants(t *testing.T) {
t.Fatalf("GetVerifyCovenants returned %d for an invalid finalize address, want -1", got)
}
}
func TestLns_Function_Good(t *testing.T) {
if got := GetServiceName(); got != ServiceName {
t.Fatalf("GetServiceName() = %q, want %q", got, ServiceName)
}
if svc := GetNewService(nil); svc == nil {
t.Fatal("GetNewService should return a service")
}
if svc := GetNewServiceWithOptions(nil); svc == nil {
t.Fatal("GetNewServiceWithOptions should return a service")
}
}
func TestLns_Function_Bad(t *testing.T) {
if got := Register(nil); got.OK {
t.Fatalf("Register(nil) = %#v, want failure", got)
}
if got := GetRegister(nil); got.OK {
t.Fatalf("GetRegister(nil) = %#v, want failure", got)
}
}
func TestLns_Function_Ugly(t *testing.T) {
svc := NewServiceWithOptions(nil)
if svc == nil {
t.Fatal("NewServiceWithOptions(nil) should return a service")
}
if svc.reservedCatalogOverride != nil || svc.lockedCatalogOverride != nil {
t.Fatalf("nil options should not populate catalog overrides: %#v", svc)
}
}

View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import "fmt"
func ExampletestAirdropProof() {
proof := testAirdropProof()
fmt.Println(proof.isSane())
// Output: true
}

View file

@ -0,0 +1,65 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"encoding/binary"
"testing"
)
func testAirdropProof() *airdropProof {
key := make([]byte, 1+1+1+2+8+1)
key[0] = airdropKeyAddress
key[1] = 1
key[2] = 2
key[3] = 0xaa
key[4] = 0xbb
binary.LittleEndian.PutUint64(key[5:13], 10)
key[13] = 0xff
return &airdropProof{
index: 0,
proof: nil,
subindex: 0,
subproof: nil,
key: key,
version: 1,
address: []byte{0x01, 0x02},
fee: 1,
}
}
func TestAirdropProof_Function_Good(t *testing.T) {
proof := testAirdropProof()
if !proof.isSane() {
t.Fatal("expected proof to be sane")
}
key, ok := proof.getKey()
if !ok {
t.Fatal("expected proof key to decode")
}
if key.value != 10 || key.version != 1 {
t.Fatalf("unexpected key decode: %#v", key)
}
}
func TestAirdropProof_Function_Bad(t *testing.T) {
proof := testAirdropProof()
proof.version = 32
if proof.isSane() {
t.Fatal("expected invalid version to fail sanity checks")
}
}
func TestAirdropProof_Function_Ugly(t *testing.T) {
proof := testAirdropProof()
proof.key = proof.key[:len(proof.key)-1]
if proof.isSane() {
t.Fatal("expected truncated key to fail sanity checks")
}
}

View file

@ -0,0 +1,15 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"fmt"
"dappco.re/go/lns/pkg/primitives"
)
func ExampleGetBlind() {
_, err := GetBlind(1000, primitives.Hash{})
fmt.Println(err == nil)
// Output: true
}

View file

@ -52,3 +52,50 @@ func TestGetBlind(t *testing.T) {
t.Fatalf("GetBlind returned %x, want %x", got, want)
}
}
func TestBlind_Function_Good(t *testing.T) {
var nonce primitives.Hash
for i := range nonce {
nonce[i] = byte(i)
}
got, err := Blind(0, nonce)
if err != nil {
t.Fatalf("Blind returned error: %v", err)
}
want, err := GetBlind(0, nonce)
if err != nil {
t.Fatalf("GetBlind returned error: %v", err)
}
if got != want {
t.Fatalf("Blind() = %x, want %x", got, want)
}
}
func TestBlind_Function_Bad(t *testing.T) {
var nonce primitives.Hash
got, err := Blind(1, nonce)
if err != nil {
t.Fatalf("Blind returned error: %v", err)
}
if got == (primitives.Hash{}) {
t.Fatal("Blind should return a non-zero commitment for non-zero value")
}
}
func TestBlind_Function_Ugly(t *testing.T) {
var nonce primitives.Hash
nonce[0] = 1
got, err := Blind(0, nonce)
if err != nil {
t.Fatalf("Blind returned error: %v", err)
}
if got == (primitives.Hash{}) {
t.Fatal("Blind should produce a digest for arbitrary nonces")
}
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import "fmt"
func ExampleTypeName() {
fmt.Println(TypeName(TypeBid))
// Output: BID
}

View file

@ -42,3 +42,36 @@ func TestCovenantTypePredicates(t *testing.T) {
}
}
func TestCovenant_Function_Good(t *testing.T) {
if got := GetTypeName(TypeRegister); got != "REGISTER" {
t.Fatalf("GetTypeName(TypeRegister) = %q, want %q", got, "REGISTER")
}
if got := GetTypes()["BID"]; got != TypeBid {
t.Fatalf("GetTypes()[BID] = %d, want %d", got, TypeBid)
}
if got := TypeFinalize.String(); got != "FINALIZE" {
t.Fatalf("String() = %q, want %q", got, "FINALIZE")
}
}
func TestCovenant_Function_Bad(t *testing.T) {
if got := TypeName(CovenantType(99)); got != "UNKNOWN" {
t.Fatalf("TypeName(99) = %q, want %q", got, "UNKNOWN")
}
if TypeNone.IsName() || TypeNone.IsLinked() || !TypeNone.IsKnown() {
t.Fatal("TypeNone predicate invariants failed")
}
}
func TestCovenant_Function_Ugly(t *testing.T) {
if got := GetTypesByVal()[TypeRevoke]; got != "REVOKE" {
t.Fatalf("GetTypesByVal()[TypeRevoke] = %q, want %q", got, "REVOKE")
}
if !TypeReveal.IsLinked() || !TypeRevoke.IsLinked() {
t.Fatal("linked covenant boundary checks failed")
}
}

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import "fmt"
func ExampleGetLockedName() {
item, _ := GetLockedName("NEC")
fmt.Println(item.Name)
// Output: nec
}

View file

@ -353,3 +353,38 @@ func TestLockedCatalogGetByNameRejectsNonASCII(t *testing.T) {
t.Fatal("GetByName should reject non-ASCII labels")
}
}
func TestLockedLookup_Function_Good(t *testing.T) {
item, ok := GetLockedName("NEC")
if !ok {
t.Fatal("GetLockedName should find the locked reference entry")
}
if item.Name != "nec" {
t.Fatalf("item.Name = %q, want %q", item.Name, "nec")
}
if !HasLockedHash(item.Hash) {
t.Fatal("HasLockedHash should report the locked reference entry")
}
}
func TestLockedLookup_Function_Bad(t *testing.T) {
if HasLockedName("does-not-exist") {
t.Fatal("unknown names should not be reported as locked")
}
if _, ok := GetLockedName("does-not-exist"); ok {
t.Fatal("GetLockedName should return false for unknown names")
}
}
func TestLockedLookup_Function_Ugly(t *testing.T) {
if !GetHasLockedName("nec.lthn") {
t.Fatal("GetHasLockedName should accept canonical .lthn names")
}
if _, ok := GetLockedByBinary([]byte("NEC")); !ok {
t.Fatal("GetLockedByBinary should find the locked reference entry")
}
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import "fmt"
func ExampleVerifyString() {
fmt.Println(VerifyString("foo"))
// Output: true
}

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import "fmt"
func ExampleasciiLowerName() {
got, _ := asciiLowerName("NEC.lthn.")
fmt.Println(got)
// Output: nec
}

View file

@ -0,0 +1,34 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import "testing"
func TestNameLookup_Function_Good(t *testing.T) {
got, ok := asciiLowerName("NEC.lthn.")
if !ok {
t.Fatal("asciiLowerName should accept canonical labels")
}
if got != "nec" {
t.Fatalf("asciiLowerName() = %q, want %q", got, "nec")
}
}
func TestNameLookup_Function_Bad(t *testing.T) {
if got, ok := asciiLowerName(""); ok || got != "" {
t.Fatalf("asciiLowerName() = %q, %v, want empty false", got, ok)
}
}
func TestNameLookup_Function_Ugly(t *testing.T) {
long := make([]byte, maxNameSize+1)
for i := range long {
long[i] = 'a'
}
if got, ok := asciiLowerName(string(long)); ok || got != "" {
t.Fatalf("asciiLowerName() = %q, %v, want empty false", got, ok)
}
}

View file

@ -363,3 +363,41 @@ func TestHashRejectsInvalidName(t *testing.T) {
t.Fatal("HashName should reject unsupported input types")
}
}
func TestName_Function_Good(t *testing.T) {
if !VerifyString("example-1") {
t.Fatal("VerifyString should accept a valid non-blacklisted name")
}
if _, err := HashString("example-1"); err != nil {
t.Fatalf("HashString returned error: %v", err)
}
}
func TestName_Function_Bad(t *testing.T) {
if VerifyString("Example") {
t.Fatal("VerifyString should reject uppercase labels")
}
if _, err := HashString("Example"); err == nil {
t.Fatal("HashString should reject invalid names")
}
if VerifyName(123) {
t.Fatal("VerifyName should reject unsupported input types")
}
}
func TestName_Function_Ugly(t *testing.T) {
if VerifyBinary([]byte("abc-")) {
t.Fatal("VerifyBinary should reject trailing hyphens")
}
if VerifyBinary([]byte("ab\x80")) {
t.Fatal("VerifyBinary should reject non-ASCII input")
}
if _, ok := GetBlacklist()["example"]; !ok {
t.Fatal("GetBlacklist should expose the blacklist map")
}
}

View file

@ -0,0 +1,16 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"fmt"
"dappco.re/go/lns/pkg/primitives"
)
func ExampleHasNames() {
hash := testNameHash()
tx := testNameTx(TypeOpen, hash)
fmt.Println(HasNames(tx, map[primitives.Hash]struct{}{hash: {}}))
// Output: true
}

View file

@ -0,0 +1,63 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"testing"
"dappco.re/go/lns/pkg/primitives"
)
func testNameHash() primitives.Hash {
var hash primitives.Hash
hash[0] = 0x42
return hash
}
func testNameTx(covType CovenantType, hash primitives.Hash) primitives.Transaction {
return primitives.Transaction{
Outputs: []primitives.Output{{
Covenant: primitives.Covenant{
Type: uint8(covType),
Items: [][]byte{hash[:]},
},
}},
}
}
func TestNames_Function_Good(t *testing.T) {
hash := testNameHash()
set := map[primitives.Hash]struct{}{hash: {}}
tx := testNameTx(TypeOpen, hash)
if !HasNames(tx, set) {
t.Fatal("expected transaction to match the set")
}
}
func TestNames_Function_Bad(t *testing.T) {
hash := testNameHash()
set := map[primitives.Hash]struct{}{hash: {}}
tx := testNameTx(TypeNone, hash)
if HasNames(tx, set) {
t.Fatal("expected plain transfers to be ignored")
}
}
func TestNames_Function_Ugly(t *testing.T) {
hash := testNameHash()
tx := testNameTx(TypeRenew, hash)
set := map[primitives.Hash]struct{}{}
AddNames(tx, set)
if _, ok := set[hash]; !ok {
t.Fatal("AddNames should record name hashes")
}
RemoveNames(tx, set)
if _, ok := set[hash]; ok {
t.Fatal("RemoveNames should delete name hashes")
}
}

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import "fmt"
func ExampleGetReservedName() {
item, _ := GetReservedName("RESERVED")
fmt.Println(item.Name)
// Output: reserved
}

View file

@ -0,0 +1,15 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"fmt"
"dappco.re/go/lns/pkg/primitives"
)
func ExampleGetRollout() {
start, _ := GetRollout(primitives.Hash{}, NameRules{NoRollout: true})
fmt.Println(start)
// Output: 0
}

View file

@ -0,0 +1,14 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"fmt"
"dappco.re/go/lns/pkg/primitives"
)
func ExampleCountOpens() {
fmt.Println(CountOpens(primitives.Transaction{}))
// Output: 0
}

View file

@ -781,3 +781,44 @@ func TestCoinbaseClaimConjureOverflow(t *testing.T) {
t.Fatalf("VerifyCovenants returned %d for overflowing coinbase claim outputs, want -1", got)
}
}
func TestRulesExtra_Function_Good(t *testing.T) {
tx := primitives.Transaction{
Outputs: []primitives.Output{
{Covenant: primitives.Covenant{Type: uint8(TypeOpen)}},
{Covenant: primitives.Covenant{Type: uint8(TypeUpdate)}},
},
}
if got := CountOpens(tx); got != 1 {
t.Fatalf("CountOpens() = %d, want 1", got)
}
if got := GetCountUpdates(tx); got != 2 {
t.Fatalf("GetCountUpdates() = %d, want 2", got)
}
}
func TestRulesExtra_Function_Bad(t *testing.T) {
if _, err := GrindName(0, 0, NameRules{}); err == nil {
t.Fatal("GrindName should reject zero-sized names")
}
}
func TestRulesExtra_Function_Ugly(t *testing.T) {
tx := primitives.Transaction{
Outputs: []primitives.Output{
{Covenant: primitives.Covenant{Type: uint8(TypeClaim)}},
{Covenant: primitives.Covenant{Type: uint8(TypeRenew)}},
{Covenant: primitives.Covenant{Type: uint8(TypeFinalize)}},
},
}
if got := CountRenewals(tx); got != 2 {
t.Fatalf("CountRenewals() = %d, want 2", got)
}
if got := GetCountRenewals(tx); got != 2 {
t.Fatalf("GetCountRenewals() = %d, want 2", got)
}
}

View file

@ -0,0 +1,42 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"testing"
"dappco.re/go/lns/pkg/primitives"
)
func TestRules_Function_Good(t *testing.T) {
rules := NameRules{NoRollout: true}
start, week := GetRollout(primitives.Hash{}, rules)
if start != 0 || week != 0 {
t.Fatalf("GetRollout() = (%d, %d), want zeros", start, week)
}
}
func TestRules_Function_Bad(t *testing.T) {
rules := NameRules{NoReserved: true, ClaimPeriod: 100}
hash, err := HashString("reserved")
if err != nil {
t.Fatalf("HashString returned error: %v", err)
}
if IsReserved(hash, 1, rules) {
t.Fatal("expected NoReserved to disable reserved checks")
}
}
func TestRules_Function_Ugly(t *testing.T) {
hash, err := HashString("nec")
if err != nil {
t.Fatalf("HashString returned error: %v", err)
}
rules := NameRules{ClaimPeriod: 1, AlexaLockupPeriod: 100}
if !IsLockedUp(hash, 1, rules) {
t.Fatal("expected locked catalog names to remain locked")
}
}

View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"fmt"
"dappco.re/go/lns/pkg/primitives"
)
func ExampleVerifyCovenants() {
var prev primitives.Hash
prev[0] = 1
tx := primitives.Transaction{
Inputs: []primitives.Input{{Prevout: primitives.Outpoint{TxHash: prev, Index: 0}}},
Outputs: []primitives.Output{{Covenant: primitives.Covenant{Type: uint8(TypeNone)}}},
}
view := verifyCoinView{output: primitives.Output{Covenant: primitives.Covenant{Type: uint8(TypeNone)}}}
fmt.Println(VerifyCovenants(tx, view, 0, Network{}))
// Output: 0
}

View file

@ -0,0 +1,70 @@
// SPDX-License-Identifier: EUPL-1.2
package covenant
import (
"testing"
"dappco.re/go/lns/pkg/primitives"
)
type verifyCoinView struct {
output primitives.Output
}
func (v verifyCoinView) GetOutput(primitives.Outpoint) (primitives.Output, bool) {
return v.output, true
}
func TestVerify_Function_Good(t *testing.T) {
var prev primitives.Hash
prev[0] = 1
prevout := primitives.Outpoint{TxHash: prev, Index: 0}
tx := primitives.Transaction{
Inputs: []primitives.Input{{
Prevout: prevout,
}},
Outputs: []primitives.Output{{
Covenant: primitives.Covenant{Type: uint8(TypeNone)},
}},
}
view := verifyCoinView{output: primitives.Output{
Covenant: primitives.Covenant{Type: uint8(TypeNone)},
}}
if got := VerifyCovenants(tx, view, 0, Network{}); got != 0 {
t.Fatalf("VerifyCovenants() = %d, want 0", got)
}
}
func TestVerify_Function_Bad(t *testing.T) {
tx := primitives.Transaction{}
if got := VerifyCovenants(tx, nil, 0, Network{}); got != -1 {
t.Fatalf("VerifyCovenants() = %d, want -1", got)
}
}
func TestVerify_Function_Ugly(t *testing.T) {
var prev primitives.Hash
prev[0] = 1
prevout := primitives.Outpoint{TxHash: prev, Index: 0}
tx := primitives.Transaction{
Inputs: []primitives.Input{{
Prevout: prevout,
}},
Outputs: []primitives.Output{{
Covenant: primitives.Covenant{Type: uint8(TypeNone)},
}},
}
view := verifyCoinView{output: primitives.Output{
Covenant: primitives.Covenant{Type: uint8(TypeBid)},
}}
if got := VerifyCovenants(tx, view, 0, Network{}); got != -1 {
t.Fatalf("VerifyCovenants() = %d, want -1", got)
}
}

View file

@ -90,6 +90,13 @@ var HSTypes = map[string]HSType{
"TXT": HSTypeTXT,
}
// hsTypes mirrors the JS export name used by the DNS reference helpers.
//
// The Go package keeps the canonical HSTypes identifier as well, but the
// lowercase alias makes the reference shape available to same-package callers
// and test coverage.
var hsTypes = HSTypes
// HSTypesByVal mirrors the JS hsTypesByVal reverse lookup table.
var HSTypesByVal = map[HSType]string{
HSTypeDS: "DS",
@ -101,6 +108,9 @@ var HSTypesByVal = map[HSType]string{
HSTypeTXT: "TXT",
}
// hsTypesByVal mirrors the JS export name used by the DNS reference helpers.
var hsTypesByVal = HSTypesByVal
// GetHSTypes returns the DNS record-type lookup table.
func GetHSTypes() map[string]HSType {
return HSTypes

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package dns
import "fmt"
func ExampleGetDefaultTTL() {
fmt.Println(GetDefaultTTL())
// Output: 21600
}

View file

@ -4,6 +4,7 @@ package dns
import (
"bytes"
"reflect"
"testing"
)
@ -77,6 +78,14 @@ func TestHSTypesTables(t *testing.T) {
t.Fatal("GetHSTypesByVal should alias HSTypesByVal")
}
if reflect.ValueOf(hsTypes).Pointer() != reflect.ValueOf(HSTypes).Pointer() {
t.Fatal("hsTypes should alias HSTypes")
}
if reflect.ValueOf(hsTypesByVal).Pointer() != reflect.ValueOf(HSTypesByVal).Pointer() {
t.Fatal("hsTypesByVal should alias HSTypesByVal")
}
cases := []struct {
name string
got HSType
@ -109,3 +118,33 @@ func TestHSTypesTables(t *testing.T) {
}
}
}
func TestCommon_Function_Good(t *testing.T) {
if got := GetDefaultTTL(); got != DEFAULT_TTL {
t.Fatalf("GetDefaultTTL() = %d, want %d", got, DEFAULT_TTL)
}
if len(GetDummy()) != 0 {
t.Fatalf("GetDummy() length = %d, want 0", len(GetDummy()))
}
}
func TestCommon_Function_Bad(t *testing.T) {
if _, ok := GetHSTypes()["NOPE"]; ok {
t.Fatal("GetHSTypes should not contain unknown keys")
}
if _, ok := GetHSTypesByVal()[HSType(99)]; ok {
t.Fatal("GetHSTypesByVal should not contain unknown values")
}
}
func TestCommon_Function_Ugly(t *testing.T) {
if !bytes.Equal(GetTypeMapAAAA(), TYPE_MAP_AAAA) {
t.Fatal("GetTypeMapAAAA should alias TYPE_MAP_AAAA")
}
if reflect.ValueOf(GetHSTypes()).Pointer() != reflect.ValueOf(HSTypes).Pointer() {
t.Fatal("GetHSTypes should alias HSTypes")
}
}

View file

@ -16,6 +16,26 @@ type NSECRecord struct {
TTL int
}
// GetName returns the owner name.
func (r NSECRecord) GetName() string {
return r.Name
}
// GetNextDomain returns the canonical successor name.
func (r NSECRecord) GetNextDomain() string {
return r.NextDomain
}
// GetTypeBitmap returns a copy of the NSEC type bitmap.
func (r NSECRecord) GetTypeBitmap() []byte {
return append([]byte(nil), r.TypeBitmap...)
}
// GetTTL returns the record TTL.
func (r NSECRecord) GetTTL() int {
return r.TTL
}
// Create constructs a reference NSEC record container.
//
// name := Create(".", NextName("."), TYPE_MAP_ROOT)
@ -56,11 +76,12 @@ func GetNextName(tld string) string {
// PrevName returns the canonical predecessor for a top-level domain name.
//
// The helper lowercases and trims any trailing dot before applying the
// reference ordering logic.
// reference ordering logic. Empty trimmed names are invalid, matching the
// reference helper's assertion behavior.
func PrevName(tld string) string {
tld = trimFQDN(strings.ToLower(tld))
if len(tld) == 0 {
return "."
panic("dns.PrevName: invalid top-level domain")
}
last := tld[len(tld)-1] - 1

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package dns
import "fmt"
func ExampleNextName() {
fmt.Printf("%q\n", NextName("example"))
// Output: "example\x00."
}

View file

@ -33,10 +33,32 @@ func TestNSECRecordHelpers(t *testing.T) {
t.Fatalf("Create bitmap = %x, want %x", rr.TypeBitmap, bitmap)
}
if rr.GetName() != rr.Name {
t.Fatalf("GetName() = %q, want %q", rr.GetName(), rr.Name)
}
if rr.GetNextDomain() != rr.NextDomain {
t.Fatalf("GetNextDomain() = %q, want %q", rr.GetNextDomain(), rr.NextDomain)
}
if rr.GetTTL() != rr.TTL {
t.Fatalf("GetTTL() = %d, want %d", rr.GetTTL(), rr.TTL)
}
if !bytes.Equal(rr.GetTypeBitmap(), rr.TypeBitmap) {
t.Fatalf("GetTypeBitmap() = %x, want %x", rr.GetTypeBitmap(), rr.TypeBitmap)
}
bitmap[0] = 0xff
if rr.TypeBitmap[0] != 0x01 {
t.Fatal("Create should copy the type bitmap")
}
clone := rr.GetTypeBitmap()
clone[0] = 0xff
if rr.TypeBitmap[0] != 0x01 {
t.Fatal("GetTypeBitmap should return a copy")
}
}
func TestNextName(t *testing.T) {
@ -70,7 +92,6 @@ func TestPrevName(t *testing.T) {
}{
{name: "fqdn", in: "Foo-Bar.", want: "foo-baq\xff."},
{name: "label", in: "example", want: "exampld\xff."},
{name: "root", in: ".", want: "."},
}
for _, tc := range cases {
@ -83,3 +104,44 @@ func TestPrevName(t *testing.T) {
}
}
}
func TestPrevNameRejectsEmptyName(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("PrevName should panic for an empty trimmed name")
}
}()
_ = PrevName(".")
}
func TestNsec_Function_Good(t *testing.T) {
rr := GetCreate("Example", NextName("Example"), []byte{0x01, 0x02})
if rr.Name != "Example." {
t.Fatalf("GetCreate name = %q, want %q", rr.Name, "Example.")
}
if rr.TTL != DEFAULT_TTL {
t.Fatalf("GetCreate TTL = %d, want %d", rr.TTL, DEFAULT_TTL)
}
}
func TestNsec_Function_Bad(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("PrevName should panic for an empty trimmed name")
}
}()
_ = PrevName(".")
}
func TestNsec_Function_Ugly(t *testing.T) {
if got := GetNextName("Example"); got != "example\x00." {
t.Fatalf("GetNextName() = %q, want %q", got, "example\x00.")
}
if got := GetPrevName("Example"); got != "exampld\xff." {
t.Fatalf("GetPrevName() = %q, want %q", got, "exampld\xff.")
}
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package dns
import "fmt"
func ExampleVerifyString() {
fmt.Println(VerifyString("Foo-Bar.lthn"))
// Output: true
}

View file

@ -560,3 +560,31 @@ func TestPackageLevelResolveAndVerifyAliases(t *testing.T) {
}
}
}
func TestResolve_Function_Good(t *testing.T) {
got, err := Resolve("Foo-Bar.lthn")
if err != nil {
t.Fatalf("Resolve returned error: %v", err)
}
want := sha3.Sum256([]byte("foo-bar"))
if got != want {
t.Fatalf("Resolve() = %x, want %x", got, want)
}
}
func TestResolve_Function_Bad(t *testing.T) {
if _, err := Resolve(123); err == nil {
t.Fatal("Resolve should reject unsupported input types")
}
}
func TestResolve_Function_Ugly(t *testing.T) {
if !VerifyByString("Foo-Bar.lthn") {
t.Fatal("VerifyByString should accept canonical names after normalization")
}
if !GetVerifyBinary([]byte("Foo-Bar.lthn")) {
t.Fatal("GetVerifyBinary should accept canonical byte names")
}
}

View file

@ -30,6 +30,24 @@ type Resource struct {
Records []ResourceRecord
}
// GetTTL returns the resource TTL.
func (r *Resource) GetTTL() int {
if r == nil {
return 0
}
return r.TTL
}
// GetRecords returns the resource records.
func (r *Resource) GetRecords() []ResourceRecord {
if r == nil {
return nil
}
return append([]ResourceRecord(nil), r.Records...)
}
// ResourceJSON represents the JSON form of a DNS resource.
type ResourceJSON struct {
Records []json.RawMessage `json:"records"`
@ -164,6 +182,20 @@ func (DSRecord) Type() HSType { return HSTypeDS }
// GetType is an alias for Type.
func (r DSRecord) GetType() HSType { return r.Type() }
// GetKeyTag returns the DS key tag.
func (r DSRecord) GetKeyTag() uint16 { return r.KeyTag }
// GetAlgorithm returns the DS algorithm.
func (r DSRecord) GetAlgorithm() uint8 { return r.Algorithm }
// GetDigestType returns the DS digest type.
func (r DSRecord) GetDigestType() uint8 { return r.DigestType }
// GetDigest returns a copy of the DS digest.
func (r DSRecord) GetDigest() []byte {
return append([]byte(nil), r.Digest...)
}
// Type returns the DNS record type.
func (NSRecord) Type() HSType { return HSTypeNS }
@ -179,6 +211,9 @@ func (GLUE4Record) Type() HSType { return HSTypeGLUE4 }
// GetType is an alias for Type.
func (r GLUE4Record) GetType() HSType { return r.Type() }
// GetAddress returns the IPv4 glue address.
func (r GLUE4Record) GetAddress() netip.Addr { return r.Address }
// GetNS is an alias for the NS field accessor.
func (r GLUE4Record) GetNS() string { return r.NS }
@ -188,6 +223,9 @@ func (GLUE6Record) Type() HSType { return HSTypeGLUE6 }
// GetType is an alias for Type.
func (r GLUE6Record) GetType() HSType { return r.Type() }
// GetAddress returns the IPv6 glue address.
func (r GLUE6Record) GetAddress() netip.Addr { return r.Address }
// GetNS is an alias for the NS field accessor.
func (r GLUE6Record) GetNS() string { return r.NS }
@ -197,6 +235,9 @@ func (SYNTH4Record) Type() HSType { return HSTypeSYNTH4 }
// GetType is an alias for Type.
func (r SYNTH4Record) GetType() HSType { return r.Type() }
// GetAddress returns the synthesized IPv4 address.
func (r SYNTH4Record) GetAddress() netip.Addr { return r.Address }
// GetNS is an alias for NS.
func (r SYNTH4Record) GetNS() string { return r.NS() }
@ -206,6 +247,9 @@ func (SYNTH6Record) Type() HSType { return HSTypeSYNTH6 }
// GetType is an alias for Type.
func (r SYNTH6Record) GetType() HSType { return r.Type() }
// GetAddress returns the synthesized IPv6 address.
func (r SYNTH6Record) GetAddress() netip.Addr { return r.Address }
// GetNS is an alias for NS.
func (r SYNTH6Record) GetNS() string { return r.NS() }
@ -215,6 +259,11 @@ func (TXTRecord) Type() HSType { return HSTypeTXT }
// GetType is an alias for Type.
func (r TXTRecord) GetType() HSType { return r.Type() }
// GetEntries returns a copy of the TXT entries.
func (r TXTRecord) GetEntries() []string {
return append([]string(nil), r.Entries...)
}
// NS returns the synthesized target host for a SYNTH4 record.
func (r SYNTH4Record) NS() string {
if !r.Address.Is4() {
@ -443,9 +492,13 @@ func (r *Resource) GetToTXT(name string) []TXTRecord {
// ToZone projects the resource into zone-order records.
//
// Authority-like records are emitted first in original order, with duplicate NS
// targets collapsed. Glue records for in-zone hosts are appended last.
func (r *Resource) ToZone(name string) []ResourceRecord {
// Records are emitted in their original order, with duplicate NS targets
// collapsed. Glue records for in-zone hosts are appended last.
//
// The optional sign flag is accepted for API parity with the JS reference. The
// simplified Go DNS layer does not emit RRSIG records, so the flag currently
// preserves the unmodified RRset.
func (r *Resource) ToZone(name string, sign ...bool) []ResourceRecord {
if r == nil {
return nil
}
@ -526,8 +579,8 @@ func (r *Resource) ToZone(name string) []ResourceRecord {
}
// GetToZone is an alias for ToZone.
func (r *Resource) GetToZone(name string) []ResourceRecord {
return r.ToZone(name)
func (r *Resource) GetToZone(name string, sign ...bool) []ResourceRecord {
return r.ToZone(name, sign...)
}
// ToReferral builds the referral/negative-answer message shape used by the JS
@ -574,8 +627,9 @@ func (r *Resource) GetToReferral(name string, typ HSType, isTLD bool) DNSMessage
// ToDNS projects the resource into the JS reference DNS response shape.
//
// Subdomain lookups are routed to the appropriate TLD referral. TLD TXT
// lookups answer directly when the resource has no NS records; DS lookups and
// all other cases fall through to the referral/negative-answer logic.
// lookups answer directly when the resource has no NS records; DS lookups
// answer authoritatively when DS records are present. All other cases fall
// through to the referral/negative-answer logic.
func (r *Resource) ToDNS(name string, typ HSType) DNSMessage {
name = fqdn(strings.ToLower(name))
labels := strings.Split(strings.TrimSuffix(name, "."), ".")
@ -593,8 +647,10 @@ func (r *Resource) ToDNS(name string, typ HSType) DNSMessage {
}
}
case HSTypeDS:
// DS TLD lookups intentionally fall through to the referral/negative
// proof path to match the JS reference helper.
msg.AA = true
for _, record := range r.ToDS(name) {
msg.Answer = append(msg.Answer, record)
}
}
if len(msg.Answer) == 0 && len(msg.Authority) == 0 {
@ -977,6 +1033,21 @@ type resourceRecordProbe struct {
Type string `json:"type"`
}
func requireJSONFields(raw json.RawMessage, fields ...string) error {
var obj map[string]json.RawMessage
if err := json.Unmarshal(raw, &obj); err != nil {
return err
}
for _, field := range fields {
if _, ok := obj[field]; !ok {
return fmt.Errorf("missing required field %q", field)
}
}
return nil
}
func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
var probe resourceRecordProbe
if err := json.Unmarshal(raw, &probe); err != nil {
@ -985,6 +1056,9 @@ func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
switch probe.Type {
case "DS":
if err := requireJSONFields(raw, "keyTag", "algorithm", "digestType", "digest"); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid DS record", err)
}
var jsonRecord DSRecordJSON
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid DS record", err)
@ -995,6 +1069,9 @@ func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
}
return record, nil
case "NS":
if err := requireJSONFields(raw, "ns"); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid NS record", err)
}
var jsonRecord NSRecordJSON
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid NS record", err)
@ -1005,6 +1082,9 @@ func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
}
return record, nil
case "GLUE4":
if err := requireJSONFields(raw, "ns", "address"); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid GLUE4 record", err)
}
var jsonRecord GLUE4RecordJSON
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid GLUE4 record", err)
@ -1015,6 +1095,9 @@ func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
}
return record, nil
case "GLUE6":
if err := requireJSONFields(raw, "ns", "address"); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid GLUE6 record", err)
}
var jsonRecord GLUE6RecordJSON
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid GLUE6 record", err)
@ -1025,6 +1108,9 @@ func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
}
return record, nil
case "SYNTH4":
if err := requireJSONFields(raw, "address"); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid SYNTH4 record", err)
}
var jsonRecord SYNTH4RecordJSON
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid SYNTH4 record", err)
@ -1035,6 +1121,9 @@ func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
}
return record, nil
case "SYNTH6":
if err := requireJSONFields(raw, "address"); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid SYNTH6 record", err)
}
var jsonRecord SYNTH6RecordJSON
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid SYNTH6 record", err)
@ -1045,6 +1134,9 @@ func resourceRecordFromJSON(raw json.RawMessage) (ResourceRecord, error) {
}
return record, nil
case "TXT":
if err := requireJSONFields(raw, "txt"); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid TXT record", err)
}
var jsonRecord TXTRecordJSON
if err := json.Unmarshal(raw, &jsonRecord); err != nil {
return nil, core.E("dns.Resource.FromJSON", "invalid TXT record", err)
@ -1080,6 +1172,9 @@ func (r *DSRecord) FromJSON(jsonView DSRecordJSON) error {
if err != nil {
return core.E("dns.DSRecord.FromJSON", "invalid digest encoding", err)
}
if len(raw) > 255 {
return core.E("dns.DSRecord.FromJSON", "digest exceeds maximum size", nil)
}
r.KeyTag = jsonView.KeyTag
r.Algorithm = jsonView.Algorithm
@ -1101,6 +1196,9 @@ func (r *NSRecord) FromJSON(jsonView NSRecordJSON) error {
if jsonView.Type != "NS" {
return core.E("dns.NSRecord.FromJSON", "invalid NS record type", nil)
}
if err := validateDNSName(jsonView.NS); err != nil {
return core.E("dns.NSRecord.FromJSON", "invalid ns name", err)
}
r.NS = jsonView.NS
return nil
@ -1120,6 +1218,9 @@ func (r *GLUE4Record) FromJSON(jsonView GLUE4RecordJSON) error {
if jsonView.Type != "GLUE4" {
return core.E("dns.GLUE4Record.FromJSON", "invalid GLUE4 record type", nil)
}
if err := validateDNSName(jsonView.NS); err != nil {
return core.E("dns.GLUE4Record.FromJSON", "invalid ns name", err)
}
addr, err := netip.ParseAddr(jsonView.Address)
if err != nil || !addr.Is4() {
@ -1145,6 +1246,9 @@ func (r *GLUE6Record) FromJSON(jsonView GLUE6RecordJSON) error {
if jsonView.Type != "GLUE6" {
return core.E("dns.GLUE6Record.FromJSON", "invalid GLUE6 record type", nil)
}
if err := validateDNSName(jsonView.NS); err != nil {
return core.E("dns.GLUE6Record.FromJSON", "invalid ns name", err)
}
addr, err := netip.ParseAddr(jsonView.Address)
if err != nil || !addr.Is6() {
@ -1215,11 +1319,27 @@ func (r *TXTRecord) FromJSON(jsonView TXTRecordJSON) error {
if jsonView.Type != "TXT" {
return core.E("dns.TXTRecord.FromJSON", "invalid TXT record type", nil)
}
for _, entry := range jsonView.Entries {
if len(entry) > 255 {
return core.E("dns.TXTRecord.FromJSON", "txt entry exceeds maximum size", nil)
}
}
r.Entries = append(r.Entries[:0], jsonView.Entries...)
return nil
}
func validateDNSName(name string) error {
var buf bytes.Buffer
if err := writeName(&buf, name); err != nil {
return err
}
if buf.Len() > 255 {
return errors.New("dns name exceeds maximum size")
}
return nil
}
func encodeRecord(buf *bytes.Buffer, record ResourceRecord) error {
switch rr := record.(type) {
case DSRecord:

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package dns
import "fmt"
func ExampleNewResource() {
fmt.Println(NewResource().HasNS())
// Output: false
}

View file

@ -4,6 +4,8 @@ package dns
import (
"bytes"
"encoding/json"
"reflect"
"strings"
"testing"
@ -11,6 +13,90 @@ import (
"net/netip"
)
func TestResourceFromJSONRejectsInvalidRecords(t *testing.T) {
tooLongDigest := strings.Repeat("aa", 256)
tooLongTXT := strings.Repeat("a", 256)
tooLongLabel := strings.Repeat("a", covenant.MaxNameSize+1) + ".example."
tooLongName := strings.Repeat("a.", 128)
tests := []struct {
name string
raw string
}{
{
name: "oversized ds digest",
raw: `{"type":"DS","keyTag":1,"algorithm":2,"digestType":3,"digest":"` + tooLongDigest + `"}`,
},
{
name: "invalid ns label",
raw: `{"type":"NS","ns":"` + tooLongLabel + `"}`,
},
{
name: "oversized ns name",
raw: `{"type":"GLUE4","ns":"` + tooLongName + `","address":"192.0.2.1"}`,
},
{
name: "oversized txt entry",
raw: `{"type":"TXT","txt":["` + tooLongTXT + `"]}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var resource Resource
if err := resource.FromJSON(ResourceJSON{Records: []json.RawMessage{json.RawMessage(tc.raw)}}); err == nil {
t.Fatal("FromJSON should reject invalid record JSON")
}
})
}
}
func TestResourceFromJSONRejectsMissingRequiredFields(t *testing.T) {
tests := []struct {
name string
raw string
}{
{
name: "ds missing digest",
raw: `{"type":"DS","keyTag":1,"algorithm":2,"digestType":3}`,
},
{
name: "ns missing host",
raw: `{"type":"NS"}`,
},
{
name: "glue4 missing host",
raw: `{"type":"GLUE4","address":"192.0.2.1"}`,
},
{
name: "glue6 missing address",
raw: `{"type":"GLUE6","ns":"ns1.example."}`,
},
{
name: "synth4 missing address",
raw: `{"type":"SYNTH4"}`,
},
{
name: "synth6 missing address",
raw: `{"type":"SYNTH6"}`,
},
{
name: "txt missing array",
raw: `{"type":"TXT"}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var resource Resource
err := resource.FromJSON(ResourceJSON{Records: []json.RawMessage{json.RawMessage(tc.raw)}})
if err == nil {
t.Fatal("FromJSON should reject records with missing required fields")
}
})
}
}
func TestResourceEncodeDecodeRoundTrip(t *testing.T) {
resource := NewResource()
resource.Records = []ResourceRecord{
@ -248,6 +334,20 @@ func TestResourceTypeHelpers(t *testing.T) {
if !resource.HasDS() || !resource.GetHasDS() {
t.Fatal("HasDS should report DS records")
}
if got := resource.GetTTL(); got != DEFAULT_TTL {
t.Fatalf("GetTTL() = %d, want %d", got, DEFAULT_TTL)
}
if got := resource.GetRecords(); len(got) != len(resource.Records) {
t.Fatalf("GetRecords() = %d records, want %d", len(got), len(resource.Records))
}
gotRecords := resource.GetRecords()
gotRecords[0] = nil
if resource.Records[0] == nil {
t.Fatal("GetRecords should return a copy of the record slice")
}
}
func TestResourceRecordTypeAliases(t *testing.T) {
@ -342,6 +442,62 @@ func TestResourceRecordNSAliases(t *testing.T) {
}
}
func TestResourceRecordFieldAccessors(t *testing.T) {
ds := DSRecord{
KeyTag: 11,
Algorithm: 12,
DigestType: 13,
Digest: []byte{0xaa, 0xbb},
}
if ds.GetKeyTag() != ds.KeyTag {
t.Fatalf("GetKeyTag() = %d, want %d", ds.GetKeyTag(), ds.KeyTag)
}
if ds.GetAlgorithm() != ds.Algorithm {
t.Fatalf("GetAlgorithm() = %d, want %d", ds.GetAlgorithm(), ds.Algorithm)
}
if ds.GetDigestType() != ds.DigestType {
t.Fatalf("GetDigestType() = %d, want %d", ds.GetDigestType(), ds.DigestType)
}
if got := ds.GetDigest(); !bytes.Equal(got, ds.Digest) {
t.Fatalf("GetDigest() = %x, want %x", got, ds.Digest)
}
gotDigest := ds.GetDigest()
gotDigest[0] = 0xff
if ds.Digest[0] != 0xaa {
t.Fatal("GetDigest should return a copy")
}
glue4 := GLUE4Record{NS: "ns1.example.", Address: netip.MustParseAddr("192.0.2.1")}
if glue4.GetAddress().String() != glue4.Address.String() {
t.Fatalf("GLUE4 GetAddress() = %v, want %v", glue4.GetAddress(), glue4.Address)
}
glue6 := GLUE6Record{NS: "ns2.example.", Address: netip.MustParseAddr("2001:db8::1")}
if glue6.GetAddress().String() != glue6.Address.String() {
t.Fatalf("GLUE6 GetAddress() = %v, want %v", glue6.GetAddress(), glue6.Address)
}
synth4 := SYNTH4Record{Address: netip.MustParseAddr("198.51.100.9")}
if synth4.GetAddress().String() != synth4.Address.String() {
t.Fatalf("SYNTH4 GetAddress() = %v, want %v", synth4.GetAddress(), synth4.Address)
}
synth6 := SYNTH6Record{Address: netip.MustParseAddr("2001:db8::9")}
if synth6.GetAddress().String() != synth6.Address.String() {
t.Fatalf("SYNTH6 GetAddress() = %v, want %v", synth6.GetAddress(), synth6.Address)
}
txt := TXTRecord{Entries: []string{"hello", "world"}}
if got := txt.GetEntries(); !reflect.DeepEqual(got, txt.Entries) {
t.Fatalf("GetEntries() = %#v, want %#v", got, txt.Entries)
}
gotEntries := txt.GetEntries()
gotEntries[0] = "changed"
if txt.Entries[0] != "hello" {
t.Fatal("GetEntries should return a copy")
}
}
func TestResourceToNSEC(t *testing.T) {
cases := []struct {
name string
@ -481,6 +637,16 @@ func TestResourceProjectionHelpers(t *testing.T) {
t.Fatalf("GetToZone returned %d records, want %d", len(aliasZone), len(zone))
}
signedZone := resource.ToZone("example.", true)
if len(signedZone) != len(zone) {
t.Fatalf("ToZone(..., true) returned %d records, want %d", len(signedZone), len(zone))
}
signedAliasZone := resource.GetToZone("example.", true)
if len(signedAliasZone) != len(zone) {
t.Fatalf("GetToZone(..., true) returned %d records, want %d", len(signedAliasZone), len(zone))
}
var nilResource *Resource
if got := nilResource.ToNS("example."); got != nil {
t.Fatalf("nil ToNS = %#v, want nil", got)
@ -499,6 +665,37 @@ func TestResourceProjectionHelpers(t *testing.T) {
}
}
func TestResourceToZonePreservesOriginalOrder(t *testing.T) {
resource := NewResource()
resource.Records = []ResourceRecord{
TXTRecord{Entries: []string{"first"}},
DSRecord{KeyTag: 7, Algorithm: 8, DigestType: 9, Digest: []byte{0xaa}},
NSRecord{NS: "ns1.example."},
GLUE4Record{NS: "ns1.example.", Address: netip.MustParseAddr("192.0.2.10")},
}
zone := resource.ToZone("example.")
if len(zone) != 4 {
t.Fatalf("ToZone returned %d records, want 4", len(zone))
}
if rr, ok := zone[0].(TXTRecord); !ok || len(rr.Entries) != 1 || rr.Entries[0] != "first" {
t.Fatalf("ToZone[0] = %#v, want TXT first", zone[0])
}
if rr, ok := zone[1].(DSRecord); !ok || rr.KeyTag != 7 {
t.Fatalf("ToZone[1] = %#v, want DS record", zone[1])
}
if rr, ok := zone[2].(NSRecord); !ok || rr.NS != "ns1.example." {
t.Fatalf("ToZone[2] = %#v, want NS ns1.example.", zone[2])
}
if rr, ok := zone[3].(GLUE4Record); !ok || rr.NS != "ns1.example." || rr.Address.String() != "192.0.2.10" {
t.Fatalf("ToZone[3] = %#v, want GLUE4 ns1.example./192.0.2.10", zone[3])
}
}
func TestResourceToDNSAndReferral(t *testing.T) {
resource := NewResource()
resource.Records = []ResourceRecord{
@ -533,7 +730,10 @@ func TestResourceToDNSAndReferral(t *testing.T) {
t.Fatal("GetToReferral should alias ToReferral")
}
negative := resource.ToDNS("example.", HSTypeDS)
txtResource := NewResource()
txtResource.Records = []ResourceRecord{TXTRecord{Entries: []string{"hello"}}}
negative := txtResource.ToDNS("example.", HSTypeDS)
if !negative.AA {
t.Fatal("ToDNS should set AA for a TLD DS negative proof")
}
@ -547,9 +747,6 @@ func TestResourceToDNSAndReferral(t *testing.T) {
t.Fatalf("ToDNS authority[0] = %#v, want NSEC example./example\\x00.", negative.Authority[0])
}
txtResource := NewResource()
txtResource.Records = []ResourceRecord{TXTRecord{Entries: []string{"hello"}}}
answer := txtResource.ToDNS("example.", HSTypeTXT)
if !answer.AA {
t.Fatal("ToDNS should set AA for an in-zone TXT answer")
@ -568,6 +765,25 @@ func TestResourceToDNSAndReferral(t *testing.T) {
if aliasAnswer.AA != answer.AA || len(aliasAnswer.Answer) != len(answer.Answer) || len(aliasAnswer.Authority) != len(answer.Authority) || len(aliasAnswer.Additional) != len(answer.Additional) {
t.Fatal("GetToDNS should alias ToDNS")
}
dsAnswer := resource.ToDNS("example.", HSTypeDS)
if !dsAnswer.AA {
t.Fatal("ToDNS should set AA for an in-zone DS answer")
}
if len(dsAnswer.Answer) != 1 {
t.Fatalf("ToDNS DS answer = %d, want 1", len(dsAnswer.Answer))
}
if ds, ok := dsAnswer.Answer[0].(DSRecord); !ok || ds.KeyTag != 7 || ds.Algorithm != 8 || ds.DigestType != 9 || !bytes.Equal(ds.Digest, []byte{0xaa}) {
t.Fatalf("ToDNS DS answer[0] = %#v, want DS keyTag=7", dsAnswer.Answer[0])
}
if len(dsAnswer.Authority) != 0 || len(dsAnswer.Additional) != 0 {
t.Fatalf("ToDNS DS answer should not populate authority/additional, got %+v", dsAnswer)
}
aliasDSAnswer := resource.GetToDNS("example.", HSTypeDS)
if aliasDSAnswer.AA != dsAnswer.AA || len(aliasDSAnswer.Answer) != len(dsAnswer.Answer) || len(aliasDSAnswer.Authority) != len(dsAnswer.Authority) || len(aliasDSAnswer.Additional) != len(dsAnswer.Additional) {
t.Fatal("GetToDNS should alias ToDNS for DS answers")
}
}
func TestResourceDecodeStopsAtUnknownType(t *testing.T) {
@ -616,3 +832,42 @@ func TestResourceDecodeRejectsInvalidPayloads(t *testing.T) {
t.Fatal("Decode should reject truncated TXT entries")
}
}
func TestResource_Function_Good(t *testing.T) {
resource := NewResource()
if resource == nil {
t.Fatal("NewResource should return a resource")
}
if resource.TTL != DEFAULT_TTL {
t.Fatalf("NewResource TTL = %d, want %d", resource.TTL, DEFAULT_TTL)
}
}
func TestResource_Function_Bad(t *testing.T) {
if _, err := DecodeResource([]byte{1}); err == nil {
t.Fatal("DecodeResource should reject unsupported versions")
}
var resource *Resource
if got := resource.GetSize(); got != 0 {
t.Fatalf("nil resource GetSize() = %d, want 0", got)
}
}
func TestResource_Function_Ugly(t *testing.T) {
resource := &Resource{
Records: []ResourceRecord{
NSRecord{NS: "ns1.example."},
TXTRecord{Entries: []string{"hello"}},
},
}
if !resource.HasNS() || !resource.GetHasNS() {
t.Fatal("resource should report NS-capable records")
}
if got := resource.ToNS("example"); len(got) != 1 || got[0].NS != "ns1.example." {
t.Fatalf("ToNS() = %#v, want one NS record", got)
}
}

View file

@ -94,6 +94,19 @@ func (c Claim) GetVirtualSize() int {
return c.VirtualSize()
}
// ToInv converts the claim blob into a claim inventory item.
func (c Claim) ToInv() InvItem {
return InvItem{
Type: InvTypeClaim,
Hash: c.Hash(),
}
}
// GetToInv is an alias for ToInv.
func (c Claim) GetToInv() InvItem {
return c.ToInv()
}
// MarshalBinary serializes the claim wrapper.
func (c Claim) MarshalBinary() ([]byte, error) {
if len(c.Blob) > maxClaimSize {

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNewClaim() {
fmt.Println(NewClaim().GetSize())
// Output: 2
}

View file

@ -52,6 +52,19 @@ func TestClaimHelpers(t *testing.T) {
t.Fatalf("GetVirtualSize() = %d, want %d", got, claim.VirtualSize())
}
inv := claim.ToInv()
if inv.Type != InvTypeClaim {
t.Fatalf("ToInv().Type = %d, want %d", inv.Type, InvTypeClaim)
}
if inv.Hash != wantHash {
t.Fatalf("ToInv().Hash = %x, want %x", inv.Hash, wantHash)
}
if got := claim.GetToInv(); got != inv {
t.Fatalf("GetToInv() = %+v, want %+v", got, inv)
}
if got := claim.ToBlob(); string(got) != string(blob) {
t.Fatalf("ToBlob() = %q, want %q", got, blob)
}
@ -87,3 +100,37 @@ func TestClaimHelpers(t *testing.T) {
t.Fatal("NewClaim helpers should return a claim wrapper")
}
}
func TestClaim_Function_Good(t *testing.T) {
if NewClaim() == nil || GetNewClaim() == nil {
t.Fatal("NewClaim helpers should return a claim wrapper")
}
}
func TestClaim_Function_Bad(t *testing.T) {
var claim *Claim
if claim.FromBlob([]byte("proof")) != nil {
t.Fatal("nil claim receiver should stay nil")
}
if _, err := (Claim{Blob: make([]byte, maxClaimSize + 1)}).MarshalBinary(); err == nil {
t.Fatal("MarshalBinary should reject oversized claims")
}
}
func TestClaim_Function_Ugly(t *testing.T) {
claim := Claim{Blob: []byte("ownership-proof")}
raw, err := claim.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary returned error: %v", err)
}
var decoded Claim
if err := decoded.UnmarshalBinary(raw); err != nil {
t.Fatalf("UnmarshalBinary returned error: %v", err)
}
if got := decoded.GetBlob(); string(got) != "ownership-proof" {
t.Fatalf("decoded blob = %q, want %q", got, "ownership-proof")
}
}

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleCovenant_MarshalBinary() {
raw, _ := Covenant{}.MarshalBinary()
fmt.Println(len(raw))
// Output: 2
}

View file

@ -66,3 +66,49 @@ func TestCovenantBinaryZeroValue(t *testing.T) {
t.Fatalf("decoded zero value = %+v, want zero value", decoded)
}
}
func TestCovenantBinary_Function_Good(t *testing.T) {
cov := Covenant{}
cov.SetFinalize(
Hash{43, 44, 45},
188,
[]byte("final"),
12,
21,
34,
Hash{46, 47, 48},
)
raw, err := cov.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary returned error: %v", err)
}
var decoded Covenant
if err := decoded.UnmarshalBinary(raw); err != nil {
t.Fatalf("UnmarshalBinary returned error: %v", err)
}
if decoded.Type != cov.Type || decoded.Len() != cov.Len() {
t.Fatalf("decoded covenant = %+v, want %+v", decoded, cov)
}
}
func TestCovenantBinary_Function_Bad(t *testing.T) {
var cov Covenant
if err := cov.UnmarshalBinary(nil); err == nil {
t.Fatal("UnmarshalBinary should reject short buffers")
}
}
func TestCovenantBinary_Function_Ugly(t *testing.T) {
var cov Covenant
raw, err := cov.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary returned error: %v", err)
}
if len(raw) == 0 {
t.Fatal("MarshalBinary should produce a compact encoding")
}
}

View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleCovenant_SetOpen() {
var cov Covenant
cov.SetOpen(Hash{}, []byte("example"))
fmt.Println(cov.Len())
// Output: 3
}

View file

@ -643,3 +643,39 @@ func TestCovenantJSONRejectsInvalidEncoding(t *testing.T) {
t.Fatal("FromJSON should reject invalid hex data")
}
}
func TestCovenantItems_Function_Good(t *testing.T) {
var cov Covenant
cov.SetOpen(Hash{1, 2, 3}, []byte("example"))
if cov.Len() != 3 {
t.Fatalf("SetOpen should populate three covenant items, got %d", cov.Len())
}
if got := cov.IndexOf([]byte("example")); got != 2 {
t.Fatalf("IndexOf() = %d, want 2", got)
}
}
func TestCovenantItems_Function_Bad(t *testing.T) {
var cov Covenant
if _, err := cov.Get(0); err == nil {
t.Fatal("Get should reject empty covenants")
}
if err := cov.Set(1, []byte("x")); err == nil {
t.Fatal("Set should reject out-of-range indexes")
}
}
func TestCovenantItems_Function_Ugly(t *testing.T) {
cov := Covenant{Items: [][]byte{[]byte("a"), []byte("b")}}
if item, err := cov.Get(-1); err != nil || string(item) != "b" {
t.Fatalf("Get(-1) = %q, %v, want %q, nil", item, err, "b")
}
if got := cov.Clone(); got.Len() != cov.Len() {
t.Fatalf("Clone() length = %d, want %d", got.Len(), cov.Len())
}
}

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleCovenant_GetJSON() {
json := Covenant{Type: covenantTypeOpen, Items: [][]byte{{0x01, 0x02}}}.GetJSON()
fmt.Println(json.Action)
// Output: OPEN
}

View file

@ -0,0 +1,41 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "testing"
func TestCovenantJSON_Function_Good(t *testing.T) {
cov := Covenant{Type: covenantTypeOpen, Items: [][]byte{{0x01, 0x02}}}
json := cov.GetJSON()
if json.Action != "OPEN" || len(json.Items) != 1 || json.Items[0] != "0102" {
t.Fatalf("unexpected JSON: %#v", json)
}
var got Covenant
if err := got.FromJSON(json); err != nil {
t.Fatalf("FromJSON returned error: %v", err)
}
if got.Type != cov.Type || got.GetVarSize() != cov.GetVarSize() {
t.Fatalf("round trip mismatch: %#v", got)
}
}
func TestCovenantJSON_Function_Bad(t *testing.T) {
var cov Covenant
if err := cov.FromJSON(CovenantJSON{Items: []string{"zz"}}); err == nil {
t.Fatal("expected invalid hex to fail")
}
}
func TestCovenantJSON_Function_Ugly(t *testing.T) {
var cov Covenant
if err := cov.FromJSON(CovenantJSON{}); err != nil {
t.Fatalf("FromJSON returned error for empty JSON: %v", err)
}
if cov.Len() != 0 {
t.Fatalf("expected empty covenant, got %#v", cov)
}
}

96
pkg/primitives/invitem.go Normal file
View file

@ -0,0 +1,96 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import (
"encoding/binary"
"fmt"
)
// InvType identifies a payload type in an inventory item.
type InvType uint32
const (
// InvTypeTX identifies a transaction inventory item.
InvTypeTX InvType = 1
// InvTypeBlock identifies a block inventory item.
InvTypeBlock InvType = 2
// InvTypeFilteredBlock identifies a filtered block inventory item.
InvTypeFilteredBlock InvType = 3
// InvTypeCompactBlock identifies a compact block inventory item.
InvTypeCompactBlock InvType = 4
// InvTypeClaim identifies a claim inventory item.
InvTypeClaim InvType = 5
// InvTypeAirdrop identifies an airdrop proof inventory item.
InvTypeAirdrop InvType = 6
)
// InvItem mirrors the JS inventory wrapper used for network advertisements.
type InvItem struct {
Type InvType
Hash Hash
}
// NewInvItem constructs an inventory item with the provided type and hash.
func NewInvItem(t InvType, hash Hash) *InvItem {
return &InvItem{Type: t, Hash: hash}
}
// GetNewInvItem is an alias for NewInvItem.
func GetNewInvItem(t InvType, hash Hash) *InvItem {
return NewInvItem(t, hash)
}
// GetSize returns the serialized size of the inventory item.
func (i InvItem) GetSize() int {
return 36
}
// MarshalBinary serializes the inventory item.
func (i InvItem) MarshalBinary() ([]byte, error) {
buf := make([]byte, i.GetSize())
binary.LittleEndian.PutUint32(buf[:4], uint32(i.Type))
copy(buf[4:], i.Hash[:])
return buf, nil
}
// UnmarshalBinary decodes the inventory item.
func (i *InvItem) UnmarshalBinary(data []byte) error {
if len(data) != i.GetSize() {
return fmt.Errorf("primitives.InvItem.UnmarshalBinary: invalid length")
}
i.Type = InvType(binary.LittleEndian.Uint32(data[:4]))
copy(i.Hash[:], data[4:])
return nil
}
// IsBlock reports whether the inventory item represents a block.
func (i InvItem) IsBlock() bool {
switch i.Type {
case InvTypeBlock, InvTypeFilteredBlock, InvTypeCompactBlock:
return true
default:
return false
}
}
// IsTX reports whether the inventory item represents a transaction.
func (i InvItem) IsTX() bool {
return i.Type == InvTypeTX
}
// IsClaim reports whether the inventory item represents a claim.
func (i InvItem) IsClaim() bool {
return i.Type == InvTypeClaim
}
// IsAirdrop reports whether the inventory item represents an airdrop proof.
func (i InvItem) IsAirdrop() bool {
return i.Type == InvTypeAirdrop
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNewInvItem() {
fmt.Println(NewInvItem(InvTypeClaim, Hash{}).IsClaim())
// Output: true
}

View file

@ -0,0 +1,79 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import (
"encoding/binary"
"testing"
)
func TestInvItemHelpers(t *testing.T) {
var hash Hash
hash[0] = 0xaa
hash[31] = 0x55
item := NewInvItem(InvTypeClaim, hash)
if item == nil {
t.Fatal("NewInvItem should return a value")
}
if got := item.GetSize(); got != 36 {
t.Fatalf("GetSize() = %d, want 36", got)
}
if !item.IsClaim() || item.IsTX() || item.IsBlock() || item.IsAirdrop() {
t.Fatal("inventory type predicates should match the claim type")
}
raw, err := item.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary returned error: %v", err)
}
if got := binary.LittleEndian.Uint32(raw[:4]); got != uint32(InvTypeClaim) {
t.Fatalf("MarshalBinary type = %d, want %d", got, InvTypeClaim)
}
var decoded InvItem
if err := decoded.UnmarshalBinary(raw); err != nil {
t.Fatalf("UnmarshalBinary returned error: %v", err)
}
if decoded.Type != item.Type || decoded.Hash != item.Hash {
t.Fatalf("decoded inventory item = %+v, want %+v", decoded, item)
}
}
func TestInvItem_Function_Good(t *testing.T) {
item := NewInvItem(InvTypeClaim, Hash{})
if item == nil {
t.Fatal("NewInvItem should return a value")
}
if !item.IsClaim() || item.IsTX() || item.IsBlock() || item.IsAirdrop() {
t.Fatal("inventory type predicates should match the claim type")
}
}
func TestInvItem_Function_Bad(t *testing.T) {
var item InvItem
if err := item.UnmarshalBinary([]byte{1, 2}); err == nil {
t.Fatal("UnmarshalBinary should reject invalid lengths")
}
}
func TestInvItem_Function_Ugly(t *testing.T) {
item := InvItem{Type: InvTypeCompactBlock}
if !item.IsBlock() {
t.Fatal("compact block should count as a block")
}
raw, err := item.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary returned error: %v", err)
}
if len(raw) != item.GetSize() {
t.Fatalf("MarshalBinary length = %d, want %d", len(raw), item.GetSize())
}
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNameDelta_IsNull() {
fmt.Println(NameDelta{}.IsNull())
// Output: true
}

View file

@ -172,3 +172,28 @@ func assertNameDeltaEqual(t *testing.T, got, want NameDelta) {
assertBoolPtr("Expired", got.Expired, want.Expired)
assertBoolPtr("Weak", got.Weak, want.Weak)
}
func TestNameDelta_Function_Good(t *testing.T) {
if got := (NameDelta{}).IsNull(); !got {
t.Fatal("zero delta should be null")
}
}
func TestNameDelta_Function_Bad(t *testing.T) {
var delta NameDelta
if err := delta.UnmarshalBinary([]byte{1, 2, 3}); err == nil {
t.Fatal("UnmarshalBinary should reject short buffers")
}
}
func TestNameDelta_Function_Ugly(t *testing.T) {
height := uint32(7)
delta := NameDelta{Height: &height}
if got := delta.GetField(); got != 1 {
t.Fatalf("GetField() = %d, want 1", got)
}
if got := delta.GetSize(); got <= 4 {
t.Fatalf("GetSize() = %d, want greater than 4", got)
}
}

View file

@ -0,0 +1,13 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import (
"fmt"
)
func ExampleNameState_MarshalBinary() {
raw, _ := NameState{Name: []byte("example")}.MarshalBinary()
fmt.Println(len(raw) > 0)
// Output: true
}

View file

@ -0,0 +1,55 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import (
"bytes"
"testing"
)
func TestNameStateBinary_Function_Good(t *testing.T) {
ns := NameState{
Name: []byte("example"),
Height: 10,
Renewal: 20,
Value: 30,
Highest: 40,
Data: []byte{0x01, 0x02},
Transfer: 50,
Revoked: 60,
Claimed: 70,
Renewals: 80,
Registered: true,
Expired: true,
Weak: true,
}
raw, err := ns.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary returned error: %v", err)
}
var got NameState
if err := got.UnmarshalBinary(raw); err != nil {
t.Fatalf("UnmarshalBinary returned error: %v", err)
}
if !bytes.Equal(got.Name, ns.Name) || !bytes.Equal(got.Data, ns.Data) {
t.Fatalf("round trip mismatch: %#v", got)
}
}
func TestNameStateBinary_Function_Bad(t *testing.T) {
ns := NameState{Name: bytes.Repeat([]byte("a"), 256)}
if _, err := ns.MarshalBinary(); err == nil {
t.Fatal("expected long name to fail")
}
}
func TestNameStateBinary_Function_Ugly(t *testing.T) {
var ns NameState
if err := ns.UnmarshalBinary([]byte{0x01}); err == nil {
t.Fatal("expected short buffer to fail")
}
}

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNameState_Clone() {
ns := NameState{Name: []byte("example")}
fmt.Println(string(ns.Clone().Name))
// Output: example
}

View file

@ -0,0 +1,14 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNameState_GetJSON() {
json := NameState{
Name: []byte("example"),
NameHash: Hash{0x01},
}.GetJSON(0, NameStateRules{})
fmt.Println(json.Name)
// Output: example
}

View file

@ -0,0 +1,42 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "testing"
func TestNameStateJSON_Function_Good(t *testing.T) {
ns := NameState{
Name: []byte("example"),
NameHash: Hash{0x01},
}
json := ns.GetJSON(0, NameStateRules{})
if json.Name != "example" || json.NameHash == "" {
t.Fatalf("unexpected JSON: %#v", json)
}
var got NameState
if err := got.FromJSON(json); err != nil {
t.Fatalf("FromJSON returned error: %v", err)
}
if string(got.Name) != "example" {
t.Fatalf("round trip mismatch: %#v", got)
}
}
func TestNameStateJSON_Function_Bad(t *testing.T) {
var ns NameState
if err := ns.FromJSON(NameStateJSON{Name: "example", NameHash: "zz"}); err == nil {
t.Fatal("expected invalid hash encoding to fail")
}
}
func TestNameStateJSON_Function_Ugly(t *testing.T) {
var ns NameState
json := ns.GetFormat(0, NameStateRules{})
if json.State == "" {
t.Fatal("expected formatted JSON to include a state")
}
}

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNameState_IsOpening() {
ns := NameState{Height: 10}
fmt.Println(ns.IsOpening(10, NameStateRules{TreeInterval: 5}))
// Output: true
}

View file

@ -151,3 +151,26 @@ func TestNameStateClaimableAndExpired(t *testing.T) {
t.Fatal("closed names without an owner should expire once the renewal window ends")
}
}
func TestNameStateState_Function_Good(t *testing.T) {
ns := NameState{Height: 10}
rules := NameStateRules{TreeInterval: 1, BiddingPeriod: 1, RevealPeriod: 1}
if got := ns.State(10, rules); got != NameStateOpening {
t.Fatalf("State() = %s, want %s", got, NameStateOpening)
}
}
func TestNameStateState_Function_Bad(t *testing.T) {
var ns NameState
if ns.IsClaimable(0, NameStateRules{}) {
t.Fatal("zero state should not be claimable")
}
}
func TestNameStateState_Function_Ugly(t *testing.T) {
ns := NameState{Revoked: 12}
if got := ns.State(13, NameStateRules{}); got != NameStateRevoked {
t.Fatalf("State() = %s, want %s", got, NameStateRevoked)
}
}

View file

@ -0,0 +1,14 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNameState_ToStats() {
stats := NameState{
Name: []byte("example"),
Height: 10,
}.ToStats(10, NameStateRules{TreeInterval: 5, BlocksPerDay: 24})
fmt.Println(stats.BlocksUntilBidding)
// Output: 6
}

View file

@ -0,0 +1,35 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "testing"
func TestNameStateStats_Function_Good(t *testing.T) {
ns := NameState{Name: []byte("example"), Height: 10}
stats := ns.ToStats(10, NameStateRules{TreeInterval: 5, BlocksPerDay: 24})
if stats == nil || stats.BlocksUntilBidding != 6 || stats.OpenPeriodStart != 10 {
t.Fatalf("unexpected stats: %#v", stats)
}
}
func TestNameStateStats_Function_Bad(t *testing.T) {
ns := NameState{Renewal: 1}
if stats := ns.ToStats(10, NameStateRules{TreeInterval: 0, RenewalWindow: 0}); stats != nil {
t.Fatalf("expected nil stats for expired name without owner, got %#v", stats)
}
}
func TestNameStateStats_Function_Ugly(t *testing.T) {
var owner Outpoint
owner.TxHash[0] = 1
ns := NameState{
Owner: owner,
Revoked: 10,
}
stats := ns.ToStats(10, NameStateRules{TreeInterval: 0, AuctionMaturity: 5, BlocksPerDay: 48})
if stats == nil || stats.RevokePeriodStart != 10 || stats.BlocksUntilReopen != 5 {
t.Fatalf("expected expired stats, got %#v", stats)
}
}

View file

@ -571,3 +571,25 @@ func TestNameStateBinaryZeroValue(t *testing.T) {
t.Fatalf("decoded zero value = %+v, want zero value", decoded)
}
}
func TestNameState_Function_Good(t *testing.T) {
var ns NameState
if ns.Delta() == nil || ns.GetDelta() == nil {
t.Fatal("Delta helpers should allocate a sparse delta")
}
}
func TestNameState_Function_Bad(t *testing.T) {
ns := NameState{Name: []byte("foo"), Height: 1}
if got := ns.MaybeExpire(1, NameStateRules{}); got {
t.Fatal("MaybeExpire should stay false when the name is not expired")
}
}
func TestNameState_Function_Ugly(t *testing.T) {
ns := NameState{Name: []byte("foo"), Height: 10}
clone := ns.Clone()
if clone.NameHash != ns.NameHash || !bytes.Equal(clone.Name, ns.Name) {
t.Fatalf("Clone() = %+v, want %+v", clone, ns)
}
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNewOutpoint() {
fmt.Println(NewOutpoint().IsNull())
// Output: true
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleOutpoint_GetJSON() {
fmt.Println(NewOutpoint().GetJSON().Index)
// Output: 4294967295
}

View file

@ -0,0 +1,35 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "testing"
func TestOutpointJSON_Function_Good(t *testing.T) {
op := Outpoint{Index: 7}
op.TxHash[0] = 1
json := op.GetJSON()
var got Outpoint
if err := got.FromJSON(json); err != nil {
t.Fatalf("FromJSON returned error: %v", err)
}
if !got.Equals(op) {
t.Fatalf("round trip mismatch: %#v", got)
}
}
func TestOutpointJSON_Function_Bad(t *testing.T) {
var op Outpoint
if err := op.FromJSON(OutpointJSON{Hash: "zz"}); err == nil {
t.Fatal("expected invalid hash encoding to fail")
}
}
func TestOutpointJSON_Function_Ugly(t *testing.T) {
var op Outpoint
if err := op.FromJSON(OutpointJSON{Hash: "00", Index: 1}); err == nil {
t.Fatal("expected short hash to fail")
}
}

View file

@ -0,0 +1,46 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import (
"bytes"
"testing"
)
func TestOutpoint_Function_Good(t *testing.T) {
op := NewOutpoint()
if !op.IsNull() {
t.Fatal("NewOutpoint should return the null outpoint")
}
raw, err := op.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary returned error: %v", err)
}
var got Outpoint
if err := got.UnmarshalBinary(raw); err != nil {
t.Fatalf("UnmarshalBinary returned error: %v", err)
}
if !got.Equals(op) {
t.Fatalf("round trip mismatch: %#v", got)
}
}
func TestOutpoint_Function_Bad(t *testing.T) {
var op Outpoint
if err := op.UnmarshalBinary([]byte{0x01}); err == nil {
t.Fatal("expected invalid length to fail")
}
}
func TestOutpoint_Function_Ugly(t *testing.T) {
var op Outpoint
op.TxHash[0] = 1
clone := op.Clone()
if !bytes.Equal(clone.TxHash[:], op.TxHash[:]) || clone.Index != op.Index {
t.Fatalf("Clone() mismatch: %#v", clone)
}
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleCovenant_IsName() {
fmt.Println(Covenant{Type: covenantTypeOpen}.IsName())
// Output: true
}

View file

@ -367,3 +367,28 @@ func TestOutputIsUnspendable(t *testing.T) {
t.Fatal("revoke covenant output should be unspendable")
}
}
func TestTypes_Function_Good(t *testing.T) {
addr := Address{Version: 0, Hash: bytes.Repeat([]byte{0x01}, 20)}
if !addr.IsValid() || !addr.IsPubkeyHash() {
t.Fatalf("valid pubkey hash address should be accepted: %#v", addr)
}
}
func TestTypes_Function_Bad(t *testing.T) {
addr := Address{Version: 32, Hash: bytes.Repeat([]byte{0x01}, 1)}
if addr.IsValid() {
t.Fatalf("invalid address should not be valid: %#v", addr)
}
}
func TestTypes_Function_Ugly(t *testing.T) {
cov := Covenant{Type: covenantTypeRevoke}
if !cov.IsUnspendable() || !cov.IsRevoke() {
t.Fatalf("revoke covenant should be unspendable and revoke: %#v", cov)
}
if !(Covenant{Type: covenantTypeOpen}).IsName() {
t.Fatal("open covenant should be a name covenant")
}
}

View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: EUPL-1.2
package primitives
import "fmt"
func ExampleNewNameView() {
view := NewNameView()
ns, _ := view.GetNameStateSync(nil, Hash{})
fmt.Println(ns.NameHash == (Hash{}))
// Output: true
}

View file

@ -4,6 +4,7 @@ package primitives
import (
"bytes"
"errors"
"testing"
)
@ -154,3 +155,47 @@ func (d NameDelta) MarshalMust(t *testing.T) []byte {
return data
}
func TestView_Function_Good(t *testing.T) {
view := NewNameView()
if view == nil {
t.Fatal("NewNameView should return a view")
}
var hash Hash
ns, err := view.GetNameStateSync(nil, hash)
if err != nil {
t.Fatalf("GetNameStateSync returned error: %v", err)
}
if ns == nil || ns.NameHash != hash {
t.Fatalf("GetNameStateSync returned %#v, want zero state keyed by hash", ns)
}
}
func TestView_Function_Bad(t *testing.T) {
view := NewNameView()
wantErr := errors.New("db failure")
if _, err := view.GetNameStateSync(stubNameStateDB{err: wantErr}, Hash{}); err != wantErr {
t.Fatalf("GetNameStateSync should propagate db errors, got %v", err)
}
}
func TestView_Function_Ugly(t *testing.T) {
var view NameView
firstHash := Hash{1}
secondHash := Hash{2}
view.names = map[Hash]*NameState{
firstHash: &NameState{NameHash: firstHash},
secondHash: &NameState{NameHash: secondHash},
}
view.order = []Hash{secondHash, firstHash}
view.names[firstHash].setHeight(1)
view.names[secondHash].setHeight(2)
undo := view.ToNameUndo()
if len(undo.Names) != 2 {
t.Fatalf("ToNameUndo() = %d entries, want 2", len(undo.Names))
}
}