BNS: resolvers read covenant-held names
A name whose certificate sits in the registry covenant has an owner and an
expiry that are hashed into its address, not written on the chain. Until
now a resolver saw only "held by some script address", forever.
The transaction that moves the certificate now says the state (`c` in the
payload) and the resolver checks it against the address, using the
covenant's code from the network table. Nobody is trusted about who owns
such a name or until when.
Entering is the one step script cannot police - a certificate can be sent
to a covenant address with any expiry - so resolvers set the price: the
entering transaction pays the fund for whole years and may claim no more
than it paid for. Once a name is inside, only a verified state speaks for
it; otherwise an owner could announce their way back to a name that never
expires.
A name past its grace period stays taken but resolves to nothing, judged
at the chain tip (the indexer now answers what that is) or, failing that,
at the newest block in the beacon history, so a stale index lapses a name
late and never early.
Same rules in the gateway, the Ariadne resolver and the Android resolver;
the builders announce every move, and there is one to put an existing
name into the covenant. Chipnet's fund and steward are the operator key.
2026-10-04 18:08:00 +02:00
// Resolvers reading covenant-held names.
// (Decentralized.DNS/PROTOCOL-ADDENDUM-covenant-names.md)
//
// A covenant-held name's owner and expiry are hashed into its address. The
// transaction says what they are; the resolver checks the claim against the
// address, decides whether an entry was paid for, and stops resolving a name
// whose grace period has passed.
//
// Every transaction here is built by the real builders and, where it spends
// the covenant, accepted by the VM first — then turned into the verbose form
// an electrum server returns and fed to all three JavaScript reducers, which
// must agree.
import test from "node:test" ;
BNS: commit-reveal for first registrations
A registration shows its name to the mempool before it is mined, and
"first REG wins" gives the name to whoever copies it with a better fee.
A registrant can now publish a hash first (CMT) and reveal a block later:
the REG carries the salt, and counts only if a commit of
sha256(salt, name, certificate lock) is already 1..144 blocks deep.
Someone who learns the name from the reveal has no such commit.
Who wins a contested name: the first valid REG opens a window of six
blocks, and within it the REG with the oldest commit takes the name; a REG
without a commit ranks by its own block. After the window the registration
is final. The addendum first let an older commit win for as long as the
commit lived, which left every registration open for a week and let a
standing commit beat any buyer; and a commit now lives a day, so keeping
one alive costs more than owning the name.
Optional on chipnet, where plain registrations keep working exactly as
before (the live registry reduces to the same index). Required on mainnet,
which starts empty. A registration straight into the covenant commits to
the covenant address, fixing owner and expiry at commit time.
One implementation for the JavaScript reducers; the Android reducer is now
a pure function and agrees with it on every history the tests build.
2026-10-05 22:29:38 +02:00
import { writeFileSync } from "node:fs" ;
BNS: resolvers read covenant-held names
A name whose certificate sits in the registry covenant has an owner and an
expiry that are hashed into its address, not written on the chain. Until
now a resolver saw only "held by some script address", forever.
The transaction that moves the certificate now says the state (`c` in the
payload) and the resolver checks it against the address, using the
covenant's code from the network table. Nobody is trusted about who owns
such a name or until when.
Entering is the one step script cannot police - a certificate can be sent
to a covenant address with any expiry - so resolvers set the price: the
entering transaction pays the fund for whole years and may claim no more
than it paid for. Once a name is inside, only a verified state speaks for
it; otherwise an owner could announce their way back to a name that never
expires.
A name past its grace period stays taken but resolves to nothing, judged
at the chain tip (the indexer now answers what that is) or, failing that,
at the newest block in the beacon history, so a stale index lapses a name
late and never early.
Same rules in the gateway, the Ariadne resolver and the Android resolver;
the builders announce every move, and there is one to put an existing
name into the covenant. Chipnet's fund and steward are the operator key.
2026-10-04 18:08:00 +02:00
import assert from "node:assert/strict" ;
import * as L from "@bitauth/libauth" ;
import {
NETWORKS , buildIndex , buildIndexFromSnapshot , covenantLockHex , covenantTierIndex , covenantClaim , covenantPhase ,
pkhToAddress , addressToLockHex , historyHeight ,
} from "../src/lib/resolver-web.js" ;
import * as legacy from "../src/lib/bns.js" ;
import { covenantTable } from "../src/generate-covenant-table.mjs" ;
import {
registryTerms , registryLock , commitBoxLock , commitHashFor , covenantClaimOf , registryAnnouncement ,
buildRegistryEnterTx , buildRegistryRenewTx , buildRegistryTransferTx , buildRegistryTakeoverTx , buildRegistryReleaseTx ,
} from "../src/lib/registry-covenant.js" ;
import { TIERS , tierForName , BLOCKS _PER _YEAR , GRACE _BLOCKS } from "../src/lib/registry-lifecycle.js" ;
import { lockToAddress } from "../src/lib/covenant.js" ;
import { encodePayload , opReturnScript } from "../src/lib/register-tx.js" ;
const { hexToBin , binToHex , hash160 , secp256k1 , encodeLockingBytecodeP2pkh , generatePrivateKey } = L ;
const NET = NETWORKS . chipnet ;
const COV = NET . covenant ;
const VM = L . createVirtualMachineBch2025 ( ) ;
// ---------- cast ----------
const person = ( ) => {
const priv = generatePrivateKey ( ( ) => crypto . getRandomValues ( new Uint8Array ( 32 ) ) ) ;
const pub = secp256k1 . derivePublicKeyCompressed ( priv ) ;
const pkh = hash160 ( pub ) ;
const lock = encodeLockingBytecodeP2pkh ( pkh ) ;
return { priv , pub , pkh : binToHex ( pkh ) , lock , lockHex : binToHex ( lock ) , address : lockToAddress ( lock , NET . prefix , false ) } ;
} ;
const owner = person ( ) , buyer = person ( ) , taker = person ( ) , stranger = person ( ) ;
const keyring = ( ... people ) => ( lockHex ) => { const p = people . find ( ( x ) => x . lockHex === lockHex ) ; return p ? { privateKey : p . priv , publicKey : p . pub } : null ; } ;
const KEYS = keyring ( owner , buyer , taker , stranger ) ;
const fundLock = hexToBin ( COV . fundLock ) ;
const beaconLock = hexToBin ( addressToLockHex ( NET . beacon ) ) ;
const termsFor = ( name ) => registryTerms ( { tier : tierForName ( name ) , fundLock , stewardPk : hexToBin ( COV . steward ) } ) ;
let coinSeq = 100 ;
const coin = ( who , sats = 50_000_000 n ) => ( { txid : ( coinSeq ++ ) . toString ( 16 ) . padStart ( 64 , "0" ) , vout : 0 , satoshis : sats , lockingBytecode : who . lock } ) ;
// ---------- libauth transaction -> what an electrum server returns ----------
const verbose = ( built ) => ( {
txid : built . txid ,
vin : built . transaction . inputs . map ( ( i ) => ( { txid : binToHex ( i . outpointTransactionHash ) , vout : i . outpointIndex } ) ) ,
vout : built . transaction . outputs . map ( ( o , n ) => {
const hex = binToHex ( o . lockingBytecode ) ;
let address ;
try { address = lockToAddress ( o . lockingBytecode , NET . prefix , false ) ; } catch { address = undefined ; }
return {
n , value : Number ( o . valueSatoshis ) / 1e8 ,
scriptPubKey : { hex , ... ( address ? { address } : { } ) } ,
... ( o . token ? { tokenData : { category : binToHex ( o . token . category ) , amount : String ( o . token . amount ? ? 0 n ) , ... ( o . token . nft ? { nft : { capability : o . token . nft . capability , commitment : binToHex ( o . token . nft . commitment ) } } : { } ) } } : { } ) ,
} ;
} ) ,
} ) ;
const vmAccepts = ( built , what ) => assert . equal ( VM . verify ( { transaction : built . transaction , sourceOutputs : built . sourceOutputs } ) , true , ` ${ what } : the VM must accept it ` ) ;
const nextCov = ( built , token ) => ( { txid : built . txid , vout : 0 , satoshis : built . transaction . outputs [ 0 ] . valueSatoshis , lockingBytecode : built . transaction . outputs [ 0 ] . lockingBytecode , token } ) ;
// A plain registration, hand-made (no contract involved).
let catSeq = 1 ;
function registration ( name , who , records = { u : "https://first.example" } ) {
const category = ( catSeq ++ ) . toString ( 16 ) . padStart ( 2 , "0" ) . repeat ( 32 ) ;
const token = { category , amount : 0 n , nft : { capability : "none" , commitment : binToHex ( new TextEncoder ( ) . encode ( name ) ) } } ;
const txid = "f" + ( catSeq ) . toString ( 16 ) . padStart ( 63 , "0" ) ;
const tx = {
txid , vin : [ { txid : category , vout : 0 } ] ,
vout : [
{ n : 0 , value : 0.00001 , scriptPubKey : { hex : who . lockHex , address : who . address } , tokenData : { category , amount : "0" , nft : { capability : "none" , commitment : token . nft . commitment } } } ,
{ n : 1 , value : 0 , scriptPubKey : { hex : binToHex ( opReturnScript ( encodePayload ( "REG" , name , records ) ) ) } } ,
{ n : 2 , value : 0.000006 , scriptPubKey : { hex : binToHex ( beaconLock ) , address : NET . beacon } } ,
] ,
} ;
return { tx , cert : { txid , vout : 0 , satoshis : 1000 n , lockingBytecode : who . lock , token } } ;
}
const announce = ( name , records ) => ( { name , records , beaconLock } ) ;
BNS: commit-reveal for first registrations
A registration shows its name to the mempool before it is mined, and
"first REG wins" gives the name to whoever copies it with a better fee.
A registrant can now publish a hash first (CMT) and reveal a block later:
the REG carries the salt, and counts only if a commit of
sha256(salt, name, certificate lock) is already 1..144 blocks deep.
Someone who learns the name from the reveal has no such commit.
Who wins a contested name: the first valid REG opens a window of six
blocks, and within it the REG with the oldest commit takes the name; a REG
without a commit ranks by its own block. After the window the registration
is final. The addendum first let an older commit win for as long as the
commit lived, which left every registration open for a week and let a
standing commit beat any buyer; and a commit now lives a day, so keeping
one alive costs more than owning the name.
Optional on chipnet, where plain registrations keep working exactly as
before (the live registry reduces to the same index). Required on mainnet,
which starts empty. A registration straight into the covenant commits to
the covenant address, fixing owner and expiry at commit time.
One implementation for the JavaScript reducers; the Android reducer is now
a pure function and agrees with it on every history the tests build.
2026-10-05 22:29:38 +02:00
// BNS_DUMP=<file>: write every reduction as a vector, so the Android reducer
// can be run against the same histories on the desktop (see the memory note).
const DUMPED = [ ] ;
const dump = ( history , txs , height , idx ) => {
if ( ! process . env . BNS _DUMP ) return ;
DUMPED . push ( { history , txs , height , expect : [ ... idx ] . map ( ( [ k , e ] ) => ( { name : k , category : e . category , txid : e . txid , records : e . records , ownerPkh : e . covenant ? . ownerPkh ? ? null , expiry : e . covenant ? . expiry ? ? 0 , status : e . status ? ? null } ) ) } ) ;
} ;
process . on ( "exit" , ( ) => { if ( process . env . BNS _DUMP && DUMPED . length ) writeFileSync ( process . env . BNS _DUMP , JSON . stringify ( { vectors : DUMPED } ) ) ; } ) ;
BNS: resolvers read covenant-held names
A name whose certificate sits in the registry covenant has an owner and an
expiry that are hashed into its address, not written on the chain. Until
now a resolver saw only "held by some script address", forever.
The transaction that moves the certificate now says the state (`c` in the
payload) and the resolver checks it against the address, using the
covenant's code from the network table. Nobody is trusted about who owns
such a name or until when.
Entering is the one step script cannot police - a certificate can be sent
to a covenant address with any expiry - so resolvers set the price: the
entering transaction pays the fund for whole years and may claim no more
than it paid for. Once a name is inside, only a verified state speaks for
it; otherwise an owner could announce their way back to a name that never
expires.
A name past its grace period stays taken but resolves to nothing, judged
at the chain tip (the indexer now answers what that is) or, failing that,
at the newest block in the beacon history, so a stale index lapses a name
late and never early.
Same rules in the gateway, the Ariadne resolver and the Android resolver;
the builders announce every move, and there is one to put an existing
name into the covenant. Chipnet's fund and steward are the operator key.
2026-10-04 18:08:00 +02:00
// ---------- the three reducers, which must agree ----------
async function reduce ( entries , height ) {
const history = entries . map ( ( [ tx , h ] ) => ( { tx _hash : tx . txid , height : h } ) ) ;
const txs = Object . fromEntries ( entries . map ( ( [ tx ] ) => [ tx . txid , tx ] ) ) ;
const snap = buildIndexFromSnapshot ( { snapshot : { history , txs } , height } ) ;
const el = {
call : async ( method , [ a ] ) => method === "blockchain.scripthash.get_history" ? history . map ( ( x ) => ( { ... x } ) )
: method === "blockchain.headers.subscribe" ? { height }
: txs [ a ] ,
} ;
const live = await buildIndex ( { client : el , cosignGate : false } ) ;
const old = await legacy . buildIndex ( { getHistory : async ( ) => history . map ( ( x ) => ( { ... x } ) ) , getRawTransactionObject : async ( id ) => txs [ id ] } , { cosignGate : false , height } ) ;
const shape = ( idx ) => [ ... idx ] . map ( ( [ k , e ] ) => JSON . stringify ( [ k , e . records , e . covenant ? ? null , e . status ? ? null , e . expiry ? ? null , e . lapsedRecords ? ? null ] ) ) . sort ( ) ;
assert . deepEqual ( shape ( live ) , shape ( snap ) , "buildIndex and buildIndexFromSnapshot disagree" ) ;
assert . deepEqual ( shape ( old ) , shape ( snap ) , "bns.js and resolver-web.js disagree" ) ;
assert . equal ( snap . height , height ) ;
BNS: commit-reveal for first registrations
A registration shows its name to the mempool before it is mined, and
"first REG wins" gives the name to whoever copies it with a better fee.
A registrant can now publish a hash first (CMT) and reveal a block later:
the REG carries the salt, and counts only if a commit of
sha256(salt, name, certificate lock) is already 1..144 blocks deep.
Someone who learns the name from the reveal has no such commit.
Who wins a contested name: the first valid REG opens a window of six
blocks, and within it the REG with the oldest commit takes the name; a REG
without a commit ranks by its own block. After the window the registration
is final. The addendum first let an older commit win for as long as the
commit lived, which left every registration open for a week and let a
standing commit beat any buyer; and a commit now lives a day, so keeping
one alive costs more than owning the name.
Optional on chipnet, where plain registrations keep working exactly as
before (the live registry reduces to the same index). Required on mainnet,
which starts empty. A registration straight into the covenant commits to
the covenant address, fixing owner and expiry at commit time.
One implementation for the JavaScript reducers; the Android reducer is now
a pure function and agrees with it on every history the tests build.
2026-10-05 22:29:38 +02:00
dump ( history , txs , height , snap ) ;
BNS: resolvers read covenant-held names
A name whose certificate sits in the registry covenant has an owner and an
expiry that are hashed into its address, not written on the chain. Until
now a resolver saw only "held by some script address", forever.
The transaction that moves the certificate now says the state (`c` in the
payload) and the resolver checks it against the address, using the
covenant's code from the network table. Nobody is trusted about who owns
such a name or until when.
Entering is the one step script cannot police - a certificate can be sent
to a covenant address with any expiry - so resolvers set the price: the
entering transaction pays the fund for whole years and may claim no more
than it paid for. Once a name is inside, only a verified state speaks for
it; otherwise an owner could announce their way back to a name that never
expires.
A name past its grace period stays taken but resolves to nothing, judged
at the chain tip (the indexer now answers what that is) or, failing that,
at the newest block in the beacon history, so a stale index lapses a name
late and never early.
Same rules in the gateway, the Ariadne resolver and the Android resolver;
the builders announce every move, and there is one to put an existing
name into the covenant. Chipnet's fund and steward are the operator key.
2026-10-04 18:08:00 +02:00
return snap ;
}
// ---------- the table ----------
test ( "the network table holds the contract's code, byte for byte" , ( ) => {
assert . deepEqual ( covenantTable ( { fundLock , stewardPk : hexToBin ( COV . steward ) } ) , COV , "regenerate: node src/generate-covenant-table.mjs" ) ;
for ( const t of TIERS ) {
const terms = registryTerms ( { tier : t . tier , fundLock , stewardPk : hexToBin ( COV . steward ) } ) ;
const state = { ownerPkh : owner . pkh , expiry : 123456 + t . tier } ;
assert . equal ( covenantLockHex ( COV , t . tier - 1 , state . ownerPkh , state . expiry ) , binToHex ( registryLock ( terms , state ) ) , ` tier ${ t . tier } ` ) ;
}
for ( const name of [ "a.x" , "ab.x" , "abc.x" , "abcd.x" , "abcde.x" , "abcdefg.x" , "abcdefgh.x" , "a-much-longer-label.test" , "bare" ] ) {
assert . equal ( covenantTierIndex ( name , COV ) + 1 , tierForName ( name ) , name ) ;
}
assert . equal ( NETWORKS . mainnet . covenant , null , "mainnet is not pinned" ) ;
} ) ;
test ( "pkhToAddress is the inverse of addressToLockHex" , ( ) => {
for ( const p of [ owner , buyer , taker ] ) {
assert . equal ( pkhToAddress ( p . pkh , NET . prefix ) , p . address ) ;
assert . equal ( addressToLockHex ( pkhToAddress ( p . pkh , NET . prefix ) ) , p . lockHex ) ;
}
assert . equal ( pkhToAddress ( owner . pkh , "bitcoincash" ) , lockToAddress ( owner . lock , "bitcoincash" , false ) ) ;
} ) ;
// ---------- entering ----------
const NAME = "holdme.x" ;
const H _REG = 1000 , TIP = 1999 , H _ENTER = 2000 ;
function entered ( { name = NAME , who = owner , years = 1 , height = TIP , records = { u : "https://held.example" } } = { } ) {
const reg = registration ( name , who ) ;
const terms = termsFor ( name ) ;
const enter = buildRegistryEnterTx ( { cert : reg . cert , terms , name , records , ownerPkh : who . pkh , height , years , beaconLock , coins : [ coin ( who ) ] , changeLock : who . lock , keyFor : KEYS } ) ;
return { reg , terms , enter , cov : nextCov ( enter , reg . cert . token ) , state : enter . state } ;
}
test ( "a paid entry makes the name covenant-held: owner and expiry come from the verified claim" , async ( ) => {
const { reg , enter , state } = entered ( ) ;
const idx = await reduce ( [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] ] , H _ENTER ) ;
const e = idx . get ( NAME ) ;
assert . deepEqual ( e . covenant , { ownerPkh : owner . pkh , expiry : TIP + BLOCKS _PER _YEAR , tier : 3 , version : 1 } ) ;
assert . equal ( e . owner , owner . address , "the owner is the key in the state, not the covenant's address" ) ;
assert . deepEqual ( e . records , { u : "https://held.example" } ) ;
assert . equal ( e . status , "active" ) ;
assert . equal ( e . expiry , state . expiry ) ;
assert . equal ( e . idleEnd , state . expiry + GRACE _BLOCKS ) ;
} ) ;
test ( "the lifecycle: resolves while active and idle, not once open — and stays taken" , async ( ) => {
const { reg , enter , state } = entered ( ) ;
const at = async ( h ) => ( await reduce ( [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] ] , h ) ) . get ( NAME ) ;
assert . equal ( ( await at ( state . expiry - 1 ) ) . status , "active" ) ;
const idle = await at ( state . expiry ) ;
assert . equal ( idle . status , "idle" ) ;
assert . deepEqual ( idle . records , { u : "https://held.example" } , "the owner keeps the records through the grace period" ) ;
assert . equal ( ( await at ( state . expiry + GRACE _BLOCKS - 1 ) ) . status , "idle" ) ;
const open = await at ( state . expiry + GRACE _BLOCKS ) ;
assert . equal ( open . status , "open" ) ;
assert . deepEqual ( open . records , { } , "a lapsed name resolves to nothing" ) ;
assert . deepEqual ( open . lapsedRecords , { u : "https://held.example" } ) ;
// Its certificate still exists: a fresh registration of the same name is refused.
const again = registration ( NAME , stranger , { u : "https://squatter.example" } ) ;
const idx = await reduce ( [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] , [ again . tx , state . expiry + GRACE _BLOCKS + 5 ] ] , state . expiry + GRACE _BLOCKS + 10 ) ;
assert . equal ( idx . get ( NAME ) . category , reg . cert . token . category ) ;
assert . deepEqual ( idx . get ( NAME ) . records , { } ) ;
assert . deepEqual ( covenantPhase ( open . covenant , state . expiry + GRACE _BLOCKS ) , { status : "open" , expiry : state . expiry , idleEnd : state . expiry + GRACE _BLOCKS } ) ;
assert . equal ( covenantPhase ( null , 5 ) , null ) ;
} ) ;
test ( "an entry that was not paid for leaves an ordinary name" , async ( ) => {
const plain = async ( enter , height , why ) => {
const idx = await reduce ( [ [ enter . reg . tx , H _REG ] , [ enter . tx , height ] ] , Math . max ( height , H _ENTER ) ) ;
const e = idx . get ( enter . name ? ? NAME ) ;
assert . equal ( e . covenant , undefined , why ) ;
assert . equal ( e . status , undefined , why ) ;
assert . deepEqual ( e . records , { u : "https://held.example" } , ` ${ why } : it is still a valid update of an ordinary name ` ) ;
return e ;
} ;
const base = entered ( ) ;
const fundIndex = verbose ( base . enter ) . vout . findIndex ( ( v ) => v . scriptPubKey . hex === COV . fundLock && ! v . tokenData ) ;
assert . equal ( fundIndex , 1 ) ;
// one satoshi short
const short = verbose ( base . enter ) ;
short . vout [ 1 ] . value = ( COV . tiers [ 2 ] . renewalSats - 1 ) / 1e8 ;
await plain ( { reg : base . reg , tx : short } , H _ENTER , "one satoshi short of a year" ) ;
// paid to somebody else
const elsewhere = verbose ( base . enter ) ;
elsewhere . vout [ 1 ] . scriptPubKey = { hex : stranger . lockHex , address : stranger . address } ;
await plain ( { reg : base . reg , tx : elsewhere } , H _ENTER , "paid to another address" ) ;
// a token output to the fund is not a payment
const tokenPay = verbose ( base . enter ) ;
tokenPay . vout [ 1 ] . tokenData = { category : "ee" . repeat ( 32 ) , amount : "5" } ;
await plain ( { reg : base . reg , tx : tokenPay } , H _ENTER , "the fund output carries a token" ) ;
// one year paid, two claimed
const far = entered ( { height : TIP + BLOCKS _PER _YEAR } ) ;
await plain ( { reg : far . reg , tx : verbose ( far . enter ) } , H _ENTER , "expiry further out than was paid for" ) ;
// not in a block yet
const e = await plain ( { reg : base . reg , tx : verbose ( base . enter ) } , 0 , "unconfirmed" ) ;
assert . notEqual ( e . owner , owner . address , "until it confirms, the holder is just the address the certificate sits at" ) ;
// two years paid, two claimed: fine
const two = entered ( { years : 2 } ) ;
const idx = await reduce ( [ [ two . reg . tx , H _REG ] , [ verbose ( two . enter ) , H _ENTER ] ] , H _ENTER ) ;
assert . equal ( idx . get ( NAME ) . covenant . expiry , TIP + 2 * BLOCKS _PER _YEAR ) ;
} ) ;
test ( "a claim that does not hash to the address is not a claim" , async ( ) => {
const base = entered ( ) ;
const holder = verbose ( base . enter ) . vout [ 0 ] ;
const good = covenantClaimOf ( base . state ) ;
assert . ok ( covenantClaim ( { c : good } , holder , NAME ) ) ;
const bad = [
{ ... good , o : buyer . pkh } , { ... good , e : good . e + 1 } , { ... good , e : String ( good . e ) } , { ... good , o : good . o . toUpperCase ( ) } ,
{ o : good . o } , { e : good . e } , null , "x" , { ... good , e : 500000000 } , { ... good , e : 0 } , { ... good , e : good . e + 0.5 } ,
] ;
for ( const c of bad ) assert . equal ( covenantClaim ( { c } , holder , NAME ) , null , JSON . stringify ( c ) ) ;
// the right state under another tier's prices is another address
assert . equal ( covenantClaim ( { c : good } , holder , "ab.x" ) , null ) ;
assert . equal ( covenantClaim ( { c : good } , holder , "a-long-label.x" ) , null ) ;
// a network with no covenant pinned knows no covenant-held names
assert . equal ( covenantClaim ( { c : good } , holder , NAME , NETWORKS . mainnet ) , null ) ;
// In the index: a short name entered at the cheap tier's address stays ordinary.
const reg = registration ( "ab.x" , owner ) ;
const cheap = registryTerms ( { tier : 4 , fundLock , stewardPk : hexToBin ( COV . steward ) } ) ;
const enter = buildRegistryEnterTx ( { cert : reg . cert , terms : cheap , name : "ab.x" , records : { u : "https://held.example" } , ownerPkh : owner . pkh , height : TIP , beaconLock , coins : [ coin ( owner ) ] , changeLock : owner . lock , keyFor : KEYS } ) ;
const idx = await reduce ( [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] ] , H _ENTER ) ;
assert . equal ( idx . get ( "ab.x" ) . covenant , undefined ) ;
} ) ;
// ---------- inside ----------
test ( "inside the covenant: updates, transfers and renewals are read from announced states" , async ( ) => {
const { reg , terms , enter , cov , state } = entered ( ) ;
const chain = [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] ] ;
// records update: a transfer to the same key
const upd = buildRegistryTransferTx ( { cov , terms , state , newOwnerPkh : owner . pkh , announce : announce ( NAME , { u : "https://second.example" } ) , coins : [ coin ( owner ) ] , changeLock : owner . lock , keyFor : KEYS } ) ;
vmAccepts ( upd , "records update" ) ;
chain . push ( [ verbose ( upd ) , 2100 ] ) ;
let e = ( await reduce ( chain , 2100 ) ) . get ( NAME ) ;
assert . deepEqual ( e . records , { u : "https://second.example" } ) ;
assert . equal ( e . owner , owner . address ) ;
// transfer to the buyer
const xfer = buildRegistryTransferTx ( { cov : nextCov ( upd , cov . token ) , terms , state : upd . state , newOwnerPkh : buyer . pkh , announce : announce ( NAME , { u : "https://buyer.example" } ) , coins : [ coin ( owner ) ] , changeLock : owner . lock , keyFor : KEYS } ) ;
vmAccepts ( xfer , "transfer" ) ;
chain . push ( [ verbose ( xfer ) , 2200 ] ) ;
e = ( await reduce ( chain , 2200 ) ) . get ( NAME ) ;
assert . equal ( e . owner , buyer . address ) ;
assert . equal ( e . covenant . expiry , state . expiry , "a transfer does not move the expiry" ) ;
// renewal by the buyer
const renew = buildRegistryRenewTx ( { cov : nextCov ( xfer , cov . token ) , terms , state : xfer . state , years : 2 , announce : announce ( NAME , { u : "https://buyer.example" } ) , coins : [ coin ( buyer ) ] , changeLock : buyer . lock , keyFor : KEYS } ) ;
vmAccepts ( renew , "renew" ) ;
chain . push ( [ verbose ( renew ) , 2300 ] ) ;
e = ( await reduce ( chain , state . expiry + GRACE _BLOCKS + 1 ) ) . get ( NAME ) ;
assert . equal ( e . covenant . expiry , state . expiry + 2 * BLOCKS _PER _YEAR ) ;
assert . equal ( e . status , "active" , "the renewal was announced, so the name did not lapse" ) ;
// an unannounced renewal is valid on chain and invisible here: the name lapses in the index
const quiet = ( await reduce ( chain . slice ( 0 , - 1 ) , state . expiry + GRACE _BLOCKS + 1 ) ) . get ( NAME ) ;
assert . equal ( quiet . status , "open" ) ;
assert . deepEqual ( quiet . records , { } ) ;
} ) ;
test ( "once inside, a transaction without a verified state changes nothing" , async ( ) => {
const { reg , terms , enter , cov , state } = entered ( ) ;
const base = [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] ] ;
const real = buildRegistryTransferTx ( { cov , terms , state , newOwnerPkh : owner . pkh , announce : announce ( NAME , { u : "https://hijack.example" } ) , coins : [ coin ( owner ) ] , changeLock : owner . lock , keyFor : KEYS } ) ;
const payloadAt = verbose ( real ) . vout . findIndex ( ( v ) => v . scriptPubKey . hex . startsWith ( "6a" ) ) ;
const withPayload = ( payload ) => { const t = verbose ( real ) ; t . vout [ payloadAt ] . scriptPubKey . hex = binToHex ( opReturnScript ( payload ) ) ; return t ; } ;
const cases = [
[ "no claim at all — the way back to a name that never expires" , encodePayload ( "UPD" , NAME , { u : "https://hijack.example" } ) ] ,
[ "a claim for another owner" , encodePayload ( "UPD" , NAME , { u : "https://hijack.example" } , { o : stranger . pkh , e : state . expiry } ) ] ,
[ "a claim for a later expiry" , encodePayload ( "UPD" , NAME , { u : "https://hijack.example" } , { o : owner . pkh , e : state . expiry + BLOCKS _PER _YEAR } ) ] ,
] ;
for ( const [ what , payload ] of cases ) {
const e = ( await reduce ( [ ... base , [ withPayload ( payload ) , 2100 ] ] , 2100 ) ) . get ( NAME ) ;
assert . deepEqual ( e . records , { u : "https://held.example" } , what ) ;
assert . equal ( e . covenant . expiry , state . expiry , what ) ;
assert . equal ( e . owner , owner . address , what ) ;
}
// and the honest announcement of the same transaction is accepted
assert . deepEqual ( ( await reduce ( [ ... base , [ verbose ( real ) , 2100 ] ] , 2100 ) ) . get ( NAME ) . records , { u : "https://hijack.example" } ) ;
} ) ;
// ---------- leaving ----------
test ( "a takeover brings a lapsed name back, for its new owner" , async ( ) => {
const { reg , terms , enter , cov , state } = entered ( ) ;
const openAt = state . expiry + GRACE _BLOCKS + 100 ;
const salt = crypto . getRandomValues ( new Uint8Array ( 32 ) ) ;
const box = { txid : "b0" . repeat ( 32 ) , vout : 0 , satoshis : 1000 n , lockingBytecode : commitBoxLock ( { ownerPkh : taker . pkh , commitHash : commitHashFor ( NAME , salt ) } ) } ;
const take = buildRegistryTakeoverTx ( { cov , terms , state , name : NAME , height : openAt , newOwnerPkh : taker . pkh , salt , box , announce : announce ( NAME , { u : "https://taker.example" } ) , coins : [ coin ( taker ) ] , changeLock : taker . lock , keyFor : KEYS } ) ;
vmAccepts ( take , "takeover" ) ;
const before = ( await reduce ( [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] ] , openAt ) ) . get ( NAME ) ;
assert . deepEqual ( before . records , { } ) ;
const after = ( await reduce ( [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] , [ verbose ( take ) , openAt + 1 ] ] , openAt + 1 ) ) . get ( NAME ) ;
assert . equal ( after . owner , taker . address ) ;
assert . deepEqual ( after . records , { u : "https://taker.example" } ) ;
assert . equal ( after . status , "active" ) ;
assert . equal ( after . covenant . expiry , openAt + BLOCKS _PER _YEAR ) ;
assert . equal ( after . category , reg . cert . token . category , "the same certificate: a takeover is not a new registration" ) ;
} ) ;
test ( "a release from the covenant frees the name like any other" , async ( ) => {
const { reg , terms , enter , cov , state } = entered ( ) ;
const rel = buildRegistryReleaseTx ( { cov , terms , state , announce : announce ( NAME ) , coins : [ coin ( owner ) ] , changeLock : owner . lock , keyFor : KEYS } ) ;
vmAccepts ( rel , "release" ) ;
const chain = [ [ reg . tx , H _REG ] , [ verbose ( enter ) , H _ENTER ] , [ verbose ( rel ) , 2100 ] ] ;
const idx = await reduce ( chain , 2100 ) ;
assert . equal ( idx . has ( NAME ) , false ) ;
assert . equal ( idx . released . get ( NAME ) , 2100 ) ;
const again = registration ( NAME , stranger , { u : "https://next.example" } ) ;
assert . equal ( ( await reduce ( [ ... chain , [ again . tx , 2100 ] ] , 2101 ) ) . has ( NAME ) , false , "not in the release's own block" ) ;
assert . deepEqual ( ( await reduce ( [ ... chain , [ again . tx , 2101 ] ] , 2101 ) ) . get ( NAME ) . records , { u : "https://next.example" } ) ;
} ) ;
// ---------- registering straight in ----------
test ( "a registration can go straight into the covenant, on the same terms as an entry" , async ( ) => {
const name = "straight.x" ;
const terms = termsFor ( name ) ;
const state = { ownerPkh : owner . pkh , expiry : TIP + BLOCKS _PER _YEAR } ;
const make = ( paid ) => {
const r = registration ( name , owner , { u : "https://straight.example" } ) ;
r . tx . vout [ 0 ] . scriptPubKey = { hex : binToHex ( registryLock ( terms , state ) ) } ;
r . tx . vout [ 1 ] . scriptPubKey . hex = binToHex ( registryAnnouncement ( { name , records : { u : "https://straight.example" } , beaconLock } , state , "REG" ) [ 0 ] . lockingBytecode ) ;
if ( paid ) r . tx . vout . push ( { n : 3 , value : COV . tiers [ covenantTierIndex ( name , COV ) ] . renewalSats / 1e8 , scriptPubKey : { hex : COV . fundLock } } ) ;
return r . tx ;
} ;
const held = ( await reduce ( [ [ make ( true ) , H _ENTER ] ] , H _ENTER ) ) . get ( name ) ;
assert . equal ( held . covenant . expiry , state . expiry ) ;
assert . equal ( held . owner , owner . address ) ;
const unpaid = ( await reduce ( [ [ make ( false ) , H _ENTER ] ] , H _ENTER ) ) . get ( name ) ;
assert . equal ( unpaid . covenant , undefined , "registered, but an ordinary name" ) ;
assert . deepEqual ( unpaid . records , { u : "https://straight.example" } ) ;
} ) ;
// ---------- which height ----------
test ( "expiry is judged at the tip when known, else at the newest block in the history — never ahead of the chain" , async ( ) => {
const { reg , enter , state } = entered ( ) ;
const history = [ { tx _hash : reg . tx . txid , height : H _REG } , { tx _hash : enter . txid , height : H _ENTER } ] ;
const txs = { [ reg . tx . txid ] : reg . tx , [ enter . txid ] : verbose ( enter ) } ;
assert . equal ( historyHeight ( history ) , H _ENTER ) ;
assert . equal ( historyHeight ( [ { height : 0 } , { height : - 1 } ] ) , 0 ) ;
const noTip = buildIndexFromSnapshot ( { snapshot : { history , txs } } ) ;
assert . equal ( noTip . height , H _ENTER ) ;
assert . equal ( noTip . get ( NAME ) . status , "active" ) ;
const stamped = buildIndexFromSnapshot ( { snapshot : { history , txs , height : state . expiry + GRACE _BLOCKS } } ) ;
assert . equal ( stamped . get ( NAME ) . status , "open" ) ;
// a server that will not say the tip: fall back to the history
const el = { call : async ( m , [ a ] ) => { if ( m === "blockchain.scripthash.get_history" ) return history . map ( ( x ) => ( { ... x } ) ) ; if ( m === "blockchain.headers.subscribe" ) throw new Error ( "unsupported" ) ; return txs [ a ] ; } } ;
const live = await buildIndex ( { client : el , cosignGate : false } ) ;
assert . equal ( live . height , H _ENTER ) ;
// an explicit height wins
assert . equal ( ( await buildIndex ( { client : el , cosignGate : false , height : state . expiry } ) ) . get ( NAME ) . status , "idle" ) ;
} ) ;
test ( "ordinary names are untouched: no status, no expiry, same entry as before" , async ( ) => {
const reg = registration ( "plain.x" , owner ) ;
const e = ( await reduce ( [ [ reg . tx , H _REG ] ] , 10_000_000 ) ) . get ( "plain.x" ) ;
assert . deepEqual ( Object . keys ( e ) . sort ( ) , [ "category" , "height" , "name" , "owner" , "records" , "txid" , "updatedTxid" ] ) ;
} ) ;