diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 02768a7d6..aae134996 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -25,6 +25,7 @@ const CoinView = require('../coins/coinview'); const Script = require('../script/script'); const {VerifyError} = require('../protocol/errors'); const {OwnershipProof} = require('../covenants/ownership'); +const Address = require('../primitives/address'); const AirdropProof = require('../primitives/airdropproof'); const {CriticalError} = require('../errors'); const thresholdStates = common.thresholdStates; @@ -918,6 +919,12 @@ class Chain extends AsyncEmitter { // Make sure the miner isn't trying to conjure more coins. reward += consensus.getReward(height, interval); + // Add reallocation amount at hard fork height. + if (height === this.network.reallocationHeight + && consensus.AIRDROP_REALLOCATION > 0) { + reward += consensus.AIRDROP_REALLOCATION; + } + if (block.getClaimed() > reward) { throw new VerifyError(block, 'invalid', @@ -925,6 +932,34 @@ class Chain extends AsyncEmitter { 0); } + // Validate reallocation output at hard fork height. + if (height === this.network.reallocationHeight + && this.network.reallocationAddress + && consensus.AIRDROP_REALLOCATION > 0) { + const cb = block.txs[0]; + const addr = Address.fromString( + this.network.reallocationAddress, + this.network + ); + + let found = false; + + for (const output of cb.outputs) { + if (output.value === consensus.AIRDROP_REALLOCATION + && output.address.equals(addr)) { + found = true; + break; + } + } + + if (!found) { + throw new VerifyError(block, + 'invalid', + 'bad-cb-reallocation', + 100); + } + } + // Push onto verification queue. const jobs = []; for (let i = 0; i < block.txs.length; i++) { diff --git a/lib/covenants/rules.js b/lib/covenants/rules.js index dbb763d92..1b4f27d63 100644 --- a/lib/covenants/rules.js +++ b/lib/covenants/rules.js @@ -113,7 +113,7 @@ rules.MAX_NAME_SIZE = 63; * @default */ -rules.MAX_RESOURCE_SIZE = 512; +rules.MAX_RESOURCE_SIZE = 8192; /** * Consensus name verification flags (used for block validation). diff --git a/lib/dns/common.js b/lib/dns/common.js index 6037cc176..c865eaa6e 100644 --- a/lib/dns/common.js +++ b/lib/dns/common.js @@ -29,12 +29,6 @@ exports.TYPE_MAP_NS = Buffer.from('0006200000000003', 'hex'); // TXT RRSIG NSEC exports.TYPE_MAP_TXT = Buffer.from('0006000080000003', 'hex'); -// A RRSIG NSEC -exports.TYPE_MAP_A = Buffer.from('0006400000000003', 'hex'); - -// AAAA RRSIG NSEC -exports.TYPE_MAP_AAAA = Buffer.from('0006000000080003', 'hex'); - exports.hsTypes = { DS: 0, NS: 1, @@ -42,7 +36,26 @@ exports.hsTypes = { GLUE6: 3, SYNTH4: 4, SYNTH6: 5, - TXT: 6 + TXT: 6, + A: 7, + AAAA: 8, + CNAME: 9, + DNAME: 10, + MX: 11, + SRV: 12, + TLSA: 13, + SSHFP: 14, + CAA: 15, + SOA: 16, + PTR: 17, + NAPTR: 18, + SMIMEA: 19, + OPENPGPKEY: 20, + URI: 21, + LOC: 22, + RP: 23, + LABEL: 24, + RAW: 25 }; exports.hsTypesByVal = { @@ -52,5 +65,24 @@ exports.hsTypesByVal = { [exports.hsTypes.GLUE6]: 'GLUE6', [exports.hsTypes.SYNTH4]: 'SYNTH4', [exports.hsTypes.SYNTH6]: 'SYNTH6', - [exports.hsTypes.TXT]: 'TXT' + [exports.hsTypes.TXT]: 'TXT', + [exports.hsTypes.A]: 'A', + [exports.hsTypes.AAAA]: 'AAAA', + [exports.hsTypes.CNAME]: 'CNAME', + [exports.hsTypes.DNAME]: 'DNAME', + [exports.hsTypes.MX]: 'MX', + [exports.hsTypes.SRV]: 'SRV', + [exports.hsTypes.TLSA]: 'TLSA', + [exports.hsTypes.SSHFP]: 'SSHFP', + [exports.hsTypes.CAA]: 'CAA', + [exports.hsTypes.SOA]: 'SOA', + [exports.hsTypes.PTR]: 'PTR', + [exports.hsTypes.NAPTR]: 'NAPTR', + [exports.hsTypes.SMIMEA]: 'SMIMEA', + [exports.hsTypes.OPENPGPKEY]: 'OPENPGPKEY', + [exports.hsTypes.URI]: 'URI', + [exports.hsTypes.LOC]: 'LOC', + [exports.hsTypes.RP]: 'RP', + [exports.hsTypes.LABEL]: 'LABEL', + [exports.hsTypes.RAW]: 'RAW' }; diff --git a/lib/dns/resource.js b/lib/dns/resource.js index 1478d0466..c6c1e0826 100644 --- a/lib/dns/resource.js +++ b/lib/dns/resource.js @@ -43,9 +43,97 @@ const { NSRecord, TXTRecord, DSRecord, + CNAMERecord, + DNAMERecord, + MXRecord, + SRVRecord, + TLSARecord, + SSHFPRecord, + CAARecord, + SOARecord, + PTRRecord, + NAPTRRecord, + SMIMEARecord, + OPENPGPKEYRecord, + URIRecord, + LOCRecord, + RPRecord, + NSECRecord, + UNKNOWNRecord, types } = wire; +/** + * Map hsType to DNS wire type. + * @param {Number} hsType + * @returns {Number|null} + */ + +function hsToDNSType(hsType) { + switch (hsType) { + case hsTypes.DS: + return types.DS; + case hsTypes.NS: + case hsTypes.GLUE4: + case hsTypes.GLUE6: + case hsTypes.SYNTH4: + case hsTypes.SYNTH6: + return types.NS; + case hsTypes.TXT: + return types.TXT; + case hsTypes.A: + return types.A; + case hsTypes.AAAA: + return types.AAAA; + case hsTypes.CNAME: + return types.CNAME; + case hsTypes.DNAME: + return types.DNAME; + case hsTypes.MX: + return types.MX; + case hsTypes.SRV: + return types.SRV; + case hsTypes.TLSA: + return types.TLSA; + case hsTypes.SSHFP: + return types.SSHFP; + case hsTypes.CAA: + return types.CAA; + case hsTypes.SOA: + return types.SOA; + case hsTypes.PTR: + return types.PTR; + case hsTypes.NAPTR: + return types.NAPTR; + case hsTypes.SMIMEA: + return types.SMIMEA; + case hsTypes.OPENPGPKEY: + return types.OPENPGPKEY; + case hsTypes.URI: + return types.URI; + case hsTypes.LOC: + return types.LOC; + case hsTypes.RP: + return types.RP; + default: + return null; + } +} + +/** + * Get the label-qualified FQDN for a record. + * @param {String} name - The TLD FQDN (e.g. "example.") + * @param {String} label - The subdomain label (e.g. "www" or "_443._tcp") + * @returns {String} FQDN + */ + +function labelName(name, label) { + if (!label) + return name; + + return `${label}.${name}`; +} + /** * Resource * @extends {Struct} @@ -84,17 +172,55 @@ class Resource extends Struct { return this.hasType(hsTypes.DS); } + /** + * Check if any records exist for a given subdomain label. + * @param {String} label + * @returns {Boolean} + */ + + hasLabel(label) { + for (const record of this.records) { + if ((record.label || '') === label) + return true; + } + + return false; + } + + /** + * Get all distinct labels used in records. + * @returns {Set} + */ + + getLabels() { + const labels = new Set(); + + for (const record of this.records) + labels.add(record.label || ''); + + return labels; + } + encode() { - const bw = bio.write(512); + const bw = bio.write(8192); this.write(bw, new Map()); return bw.slice(); } getSize(map) { - let size = 1; + let size = 1; // version byte + let currentLabel = ''; + + for (const rr of this.records) { + const label = rr.label || ''; + + if (label !== currentLabel) { + size += 1 + sizeString(label); // LABEL type + string + currentLabel = label; + } - for (const rr of this.records) size += 1 + rr.getSize(map); + } return size; } @@ -102,7 +228,17 @@ class Resource extends Struct { write(bw, map) { bw.writeU8(0); + let currentLabel = ''; + for (const rr of this.records) { + const label = rr.label || ''; + + if (label !== currentLabel) { + bw.writeU8(hsTypes.LABEL); + writeStringBW(bw, label); + currentLabel = label; + } + bw.writeU8(rr.type); rr.write(bw, map); } @@ -116,19 +252,54 @@ class Resource extends Struct { if (version !== 0) throw new Error(`Unknown serialization version: ${version}.`); + let currentLabel = ''; + while (br.left()) { - const RD = typeToClass(br.readU8()); + const type = br.readU8(); + + // Handle LABEL pseudo-record. + if (type === hsTypes.LABEL) { + currentLabel = readStringBR(br); + continue; + } + + const RD = typeToClass(type); // Break at unknown records. if (!RD) break; - this.records.push(RD.read(br)); + const record = RD.read(br); + record.label = currentLabel; + this.records.push(record); } return this; } + /** + * Filter records by type and optional label. + * @param {Number} hsType + * @param {String} [label=''] + * @returns {Array} + */ + + recordsByType(hsType, label = '') { + const result = []; + + for (const record of this.records) { + if (record.type !== hsType) + continue; + + if ((record.label || '') !== label) + continue; + + result.push(record); + } + + return result; + } + toNS(name) { const authority = []; const set = new Set(); @@ -181,10 +352,101 @@ class Resource extends Struct { } toDS(name) { + return this._toDNSByType(hsTypes.DS, name); + } + + toTXT(name) { + return this._toDNSByType(hsTypes.TXT, name); + } + + toA(name, label = '') { + return this._toDNSByType(hsTypes.A, name, label); + } + + toAAAA(name, label = '') { + return this._toDNSByType(hsTypes.AAAA, name, label); + } + + toCNAME(name, label = '') { + return this._toDNSByType(hsTypes.CNAME, name, label); + } + + toDNAME(name, label = '') { + return this._toDNSByType(hsTypes.DNAME, name, label); + } + + toMX(name, label = '') { + return this._toDNSByType(hsTypes.MX, name, label); + } + + toSRV(name, label = '') { + return this._toDNSByType(hsTypes.SRV, name, label); + } + + toTLSA(name, label = '') { + return this._toDNSByType(hsTypes.TLSA, name, label); + } + + toSSHFP(name, label = '') { + return this._toDNSByType(hsTypes.SSHFP, name, label); + } + + toCAA(name, label = '') { + return this._toDNSByType(hsTypes.CAA, name, label); + } + + toSOA(name, label = '') { + return this._toDNSByType(hsTypes.SOA, name, label); + } + + toPTR(name, label = '') { + return this._toDNSByType(hsTypes.PTR, name, label); + } + + toNAPTR(name, label = '') { + return this._toDNSByType(hsTypes.NAPTR, name, label); + } + + toSMIMEA(name, label = '') { + return this._toDNSByType(hsTypes.SMIMEA, name, label); + } + + toOPENPGPKEY(name, label = '') { + return this._toDNSByType(hsTypes.OPENPGPKEY, name, label); + } + + toURI(name, label = '') { + return this._toDNSByType(hsTypes.URI, name, label); + } + + toLOC(name, label = '') { + return this._toDNSByType(hsTypes.LOC, name, label); + } + + toRP(name, label = '') { + return this._toDNSByType(hsTypes.RP, name, label); + } + + toRAW(name, label = '') { + return this._toDNSByType(hsTypes.RAW, name, label); + } + + /** + * Convert records of a given hsType (and label) to DNS records. + * @param {Number} hsType + * @param {String} name - FQDN + * @param {String} [label=''] + * @returns {Array} + */ + + _toDNSByType(hsType, name, label = '') { const answer = []; for (const record of this.records) { - if (record.type !== hsTypes.DS) + if (record.type !== hsType) + continue; + + if ((record.label || '') !== label) continue; answer.push(record.toDNS(name, this.ttl)); @@ -193,11 +455,18 @@ class Resource extends Struct { return answer; } - toTXT(name) { + /** + * Get all DNS records for a given label (subdomain). + * @param {String} name - FQDN for the subdomain + * @param {String} label - subdomain label + * @returns {Array} + */ + + _allDNSForLabel(name, label) { const answer = []; for (const record of this.records) { - if (record.type !== hsTypes.TXT) + if ((record.label || '') !== label) continue; answer.push(record.toDNS(name, this.ttl)); @@ -211,7 +480,8 @@ class Resource extends Struct { const set = new Set(); for (const record of this.records) { - const rr = record.toDNS(name, this.ttl); + const recordName = labelName(name, record.label); + const rr = record.toDNS(recordName, this.ttl); if (rr.type === types.NS) { if (set.has(rr.data.ns)) @@ -283,15 +553,37 @@ class Resource extends Struct { return res; } - toNSEC(name) { - let typeMap = TYPE_MAP_EMPTY; + /** + * Generate NSEC bitmap for records at a given label. + * @param {String} name - FQDN + * @param {String} [label=''] - subdomain label + * @returns {Record} + */ + + toNSEC(name, label = '') { + const dnsTypes = [types.RRSIG, types.NSEC]; + + for (const record of this.records) { + if ((record.label || '') !== label) + continue; + + // For RAW records, use the stored DNS type directly. + if (record.type === hsTypes.RAW) { + if (!dnsTypes.includes(record.dnsType)) + dnsTypes.push(record.dnsType); + continue; + } + + const dnsType = hsToDNSType(record.type); - if (this.hasNS()) - typeMap = TYPE_MAP_NS; - else if (this.hasType(hsTypes.TXT)) - typeMap = TYPE_MAP_TXT; + if (dnsType && !dnsTypes.includes(dnsType)) + dnsTypes.push(dnsType); + } + + const rd = new NSECRecord(); + rd.setTypes(dnsTypes); - return nsec.create(name, nsec.nextName(name), typeMap); + return nsec.create(name, nsec.nextName(name), rd.typeBitmap); } toDNS(name, type) { @@ -300,19 +592,44 @@ class Resource extends Struct { const labels = util.split(name); - // Referral. + // Multi-label query (e.g. "www.example." or "_443._tcp.example."). if (labels.length > 1) { const tld = util.from(name, labels, -1); + + // If we have NS delegation, always refer (except for on-chain SLD). + if (!this.hasNS()) { + // Extract subdomain prefix. + const tldPos = name.length - tld.length; + const prefix = tldPos > 0 + ? name.slice(0, tldPos - 1) + : ''; + + if (prefix && this.hasLabel(prefix)) + return this._resolveLabel(tld, prefix, name, type); + + // No matching on-chain records for this subdomain. + // Return authoritative negative answer (NODATA/NXDOMAIN). + const res = new Message(); + res.aa = true; + res.authority.push(this.toNSEC(name, prefix)); + key.signZSK(res.authority, types.NSEC); + return res; + } + return this.toReferral(tld, type, false); } - // Potentially an answer. + // TLD-level query. const res = new Message(); - // TLDs are authoritative over their own NS & TXT records. - // The NS records in the root zone are just "hints" - // and therefore are not signed by the root ZSK. - // The only records root is authoritative over is DS. + // If a CNAME exists, return it for any query type (per RFC 1034). + if (!this.hasNS() && this.hasType(hsTypes.CNAME) && type !== types.CNAME) { + res.aa = true; + res.answer = this.toCNAME(name); + key.signZSK(res.answer, types.CNAME); + return res; + } + switch (type) { case types.TXT: if (!this.hasNS()) { @@ -326,9 +643,141 @@ class Resource extends Struct { res.answer = this.toDS(name); key.signZSK(res.answer, types.DS); break; + case types.A: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toA(name); + key.signZSK(res.answer, types.A); + } + break; + case types.AAAA: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toAAAA(name); + key.signZSK(res.answer, types.AAAA); + } + break; + case types.CNAME: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toCNAME(name); + key.signZSK(res.answer, types.CNAME); + } + break; + case types.DNAME: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toDNAME(name); + key.signZSK(res.answer, types.DNAME); + } + break; + case types.MX: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toMX(name); + key.signZSK(res.answer, types.MX); + } + break; + case types.SRV: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toSRV(name); + key.signZSK(res.answer, types.SRV); + } + break; + case types.TLSA: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toTLSA(name); + key.signZSK(res.answer, types.TLSA); + } + break; + case types.SSHFP: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toSSHFP(name); + key.signZSK(res.answer, types.SSHFP); + } + break; + case types.CAA: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toCAA(name); + key.signZSK(res.answer, types.CAA); + } + break; + case types.SOA: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toSOA(name); + key.signZSK(res.answer, types.SOA); + } + break; + case types.PTR: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toPTR(name); + key.signZSK(res.answer, types.PTR); + } + break; + case types.NAPTR: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toNAPTR(name); + key.signZSK(res.answer, types.NAPTR); + } + break; + case types.SMIMEA: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toSMIMEA(name); + key.signZSK(res.answer, types.SMIMEA); + } + break; + case types.OPENPGPKEY: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toOPENPGPKEY(name); + key.signZSK(res.answer, types.OPENPGPKEY); + } + break; + case types.URI: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toURI(name); + key.signZSK(res.answer, types.URI); + } + break; + case types.LOC: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toLOC(name); + key.signZSK(res.answer, types.LOC); + } + break; + case types.RP: + if (!this.hasNS()) { + res.aa = true; + res.answer = this.toRP(name); + key.signZSK(res.answer, types.RP); + } + break; + default: + // Handle RAW and any unknown query types. + if (!this.hasNS()) { + const raws = this.toRAW(name); + const matching = raws.filter(rr => rr.type === type); + + if (matching.length > 0) { + res.aa = true; + res.answer = matching; + key.signZSK(res.answer, type); + } + } + break; } - // Nope, we may need a referral + // Nope, we may need a referral. if (res.answer.length === 0 && res.authority.length === 0) { return this.toReferral(name, type, true); } @@ -336,11 +785,74 @@ class Resource extends Struct { return res; } + /** + * Resolve a query for a subdomain label. + * @param {String} tld - TLD FQDN (e.g. "example.") + * @param {String} label - subdomain prefix (e.g. "www" or "_443._tcp") + * @param {String} fqdn - full query name (e.g. "www.example.") + * @param {Number} type - DNS query type + * @returns {Message} + */ + + _resolveLabel(tld, label, fqdn, type) { + const res = new Message(); + res.aa = true; + + // Collect matching records for this label and type. + const answer = []; + + for (const record of this.records) { + if ((record.label || '') !== label) + continue; + + // For RAW, check if the DNS type matches. + if (record.type === hsTypes.RAW) { + const rr = record.toDNS(fqdn, this.ttl); + if (rr.type === type) + answer.push(rr); + continue; + } + + const dnsType = hsToDNSType(record.type); + + if (dnsType === type) + answer.push(record.toDNS(fqdn, this.ttl)); + } + + // Check for CNAME at this label (returned for any query type). + if (answer.length === 0 && type !== types.CNAME) { + const cnames = this._toDNSByType(hsTypes.CNAME, fqdn, label); + + if (cnames.length > 0) { + res.answer = cnames; + key.signZSK(res.answer, types.CNAME); + return res; + } + } + + if (answer.length > 0) { + res.answer = answer; + key.signZSK(res.answer, type); + } else { + // NODATA: type doesn't exist at this label. + res.authority.push(this.toNSEC(fqdn, label)); + key.signZSK(res.authority, types.NSEC); + } + + return res; + } + getJSON(name) { const json = { records: [] }; - for (const record of this.records) - json.records.push(record.getJSON()); + for (const record of this.records) { + const rj = record.getJSON(); + + if (record.label) + rj.name = record.label; + + json.records.push(rj); + } return json; } @@ -357,9 +869,25 @@ class Resource extends Struct { if (!RD) throw new Error(`Unknown type: ${item.type}.`); - this.records.push(RD.fromJSON(item)); + const record = RD.fromJSON(item); + record.label = item.name || ''; + this.records.push(record); } + // Sort: unlabeled (TLD) records first for backwards compatibility. + // Old nodes break on first unknown type (LABEL), so TLD records + // must precede any labeled records. + this.records.sort((a, b) => { + const la = a.label || ''; + const lb = b.label || ''; + + if (la === '' && lb !== '') return -1; + if (la !== '' && lb === '') return 1; + if (la < lb) return -1; + if (la > lb) return 1; + return 0; + }); + return this; } } @@ -848,6 +1376,1464 @@ class TXT extends Struct { } } +/** + * A + * @extends {Struct} + */ + +class A extends Struct { + constructor() { + super(); + this.address = '0.0.0.0'; + } + + get type() { + return hsTypes.A; + } + + getSize() { + return 4; + } + + write(bw) { + writeIP(bw, this.address, 4); + return this; + } + + read(br) { + this.address = readIP(br, 4); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + return createA(name, ttl, this.address); + } + + getJSON() { + return { + type: 'A', + address: this.address + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid A record.'); + assert(json.type === 'A', + 'Invalid A record. Type must be "A".'); + assert(IP.isIPv4String(json.address), + 'Invalid A record. Address must be a valid IPv4 address.'); + + this.address = IP.normalize(json.address); + + return this; + } +} + +/** + * AAAA + * @extends {Struct} + */ + +class AAAA extends Struct { + constructor() { + super(); + this.address = '::'; + } + + get type() { + return hsTypes.AAAA; + } + + getSize() { + return 16; + } + + write(bw) { + writeIP(bw, this.address, 16); + return this; + } + + read(br) { + this.address = readIP(br, 16); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + return createAAAA(name, ttl, this.address); + } + + getJSON() { + return { + type: 'AAAA', + address: this.address + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid AAAA record.'); + assert(json.type === 'AAAA', + 'Invalid AAAA record. Type must be "AAAA".'); + assert(IP.isIPv6String(json.address), + 'Invalid AAAA record. Address must be a valid IPv6 address.'); + + this.address = IP.normalize(json.address); + + return this; + } +} + +/** + * CNAME + * @extends {Struct} + */ + +class CNAME extends Struct { + constructor() { + super(); + this.target = '.'; + } + + get type() { + return hsTypes.CNAME; + } + + getSize(map) { + return sizeName(this.target, map); + } + + write(bw, map) { + writeNameBW(bw, this.target, map); + return this; + } + + read(br) { + this.target = readNameBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new CNAMERecord(); + + rr.name = name; + rr.type = types.CNAME; + rr.ttl = ttl; + rr.data = rd; + rd.target = this.target; + + return rr; + } + + getJSON() { + return { + type: 'CNAME', + target: this.target + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid CNAME record.'); + assert(json.type === 'CNAME', + 'Invalid CNAME record. Type must be "CNAME".'); + assert(isName(json.target), + 'Invalid CNAME record. target must be a valid name.'); + + this.target = json.target; + + return this; + } +} + +/** + * DNAME + * @extends {Struct} + */ + +class DNAME extends Struct { + constructor() { + super(); + this.target = '.'; + } + + get type() { + return hsTypes.DNAME; + } + + getSize(map) { + return sizeName(this.target, map); + } + + write(bw, map) { + writeNameBW(bw, this.target, map); + return this; + } + + read(br) { + this.target = readNameBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new DNAMERecord(); + + rr.name = name; + rr.type = types.DNAME; + rr.ttl = ttl; + rr.data = rd; + rd.target = this.target; + + return rr; + } + + getJSON() { + return { + type: 'DNAME', + target: this.target + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid DNAME record.'); + assert(json.type === 'DNAME', + 'Invalid DNAME record. Type must be "DNAME".'); + assert(isName(json.target), + 'Invalid DNAME record. target must be a valid name.'); + + this.target = json.target; + + return this; + } +} + +/** + * PTR + * @extends {Struct} + */ + +class PTR extends Struct { + constructor() { + super(); + this.target = '.'; + } + + get type() { + return hsTypes.PTR; + } + + getSize(map) { + return sizeName(this.target, map); + } + + write(bw, map) { + writeNameBW(bw, this.target, map); + return this; + } + + read(br) { + this.target = readNameBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new PTRRecord(); + + rr.name = name; + rr.type = types.PTR; + rr.ttl = ttl; + rr.data = rd; + rd.target = this.target; + + return rr; + } + + getJSON() { + return { + type: 'PTR', + target: this.target + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid PTR record.'); + assert(json.type === 'PTR', + 'Invalid PTR record. Type must be "PTR".'); + assert(isName(json.target), + 'Invalid PTR record. target must be a valid name.'); + + this.target = json.target; + + return this; + } +} + +/** + * MX + * @extends {Struct} + */ + +class MX extends Struct { + constructor() { + super(); + this.preference = 0; + this.mx = '.'; + } + + get type() { + return hsTypes.MX; + } + + getSize(map) { + return 2 + sizeName(this.mx, map); + } + + write(bw, map) { + bw.writeU16BE(this.preference); + writeNameBW(bw, this.mx, map); + return this; + } + + read(br) { + this.preference = br.readU16BE(); + this.mx = readNameBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new MXRecord(); + + rr.name = name; + rr.type = types.MX; + rr.ttl = ttl; + rr.data = rd; + rd.preference = this.preference; + rd.mx = this.mx; + + return rr; + } + + getJSON() { + return { + type: 'MX', + preference: this.preference, + mx: this.mx + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid MX record.'); + assert(json.type === 'MX', + 'Invalid MX record. Type must be "MX".'); + assert((json.preference & 0xffff) === json.preference, + 'Invalid MX record. preference must be a uint16.'); + assert(isName(json.mx), + 'Invalid MX record. mx must be a valid name.'); + + this.preference = json.preference; + this.mx = json.mx; + + return this; + } +} + +/** + * SRV + * @extends {Struct} + */ + +class SRV extends Struct { + constructor() { + super(); + this.priority = 0; + this.weight = 0; + this.port = 0; + this.target = '.'; + } + + get type() { + return hsTypes.SRV; + } + + getSize(map) { + return 6 + sizeName(this.target, map); + } + + write(bw, map) { + bw.writeU16BE(this.priority); + bw.writeU16BE(this.weight); + bw.writeU16BE(this.port); + writeNameBW(bw, this.target, map); + return this; + } + + read(br) { + this.priority = br.readU16BE(); + this.weight = br.readU16BE(); + this.port = br.readU16BE(); + this.target = readNameBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new SRVRecord(); + + rr.name = name; + rr.type = types.SRV; + rr.ttl = ttl; + rr.data = rd; + rd.priority = this.priority; + rd.weight = this.weight; + rd.port = this.port; + rd.target = this.target; + + return rr; + } + + getJSON() { + return { + type: 'SRV', + priority: this.priority, + weight: this.weight, + port: this.port, + target: this.target + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid SRV record.'); + assert(json.type === 'SRV', + 'Invalid SRV record. Type must be "SRV".'); + assert((json.priority & 0xffff) === json.priority, + 'Invalid SRV record. priority must be a uint16.'); + assert((json.weight & 0xffff) === json.weight, + 'Invalid SRV record. weight must be a uint16.'); + assert((json.port & 0xffff) === json.port, + 'Invalid SRV record. port must be a uint16.'); + assert(isName(json.target), + 'Invalid SRV record. target must be a valid name.'); + + this.priority = json.priority; + this.weight = json.weight; + this.port = json.port; + this.target = json.target; + + return this; + } +} + +/** + * TLSA + * @extends {Struct} + */ + +class TLSA extends Struct { + constructor() { + super(); + this.usage = 0; + this.selector = 0; + this.matchingType = 0; + this.certificate = DUMMY; + } + + get type() { + return hsTypes.TLSA; + } + + getSize() { + return 4 + this.certificate.length; + } + + write(bw) { + bw.writeU8(this.usage); + bw.writeU8(this.selector); + bw.writeU8(this.matchingType); + bw.writeU8(this.certificate.length); + bw.writeBytes(this.certificate); + return this; + } + + read(br) { + this.usage = br.readU8(); + this.selector = br.readU8(); + this.matchingType = br.readU8(); + this.certificate = br.readBytes(br.readU8()); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new TLSARecord(); + + rr.name = name; + rr.type = types.TLSA; + rr.ttl = ttl; + rr.data = rd; + rd.usage = this.usage; + rd.selector = this.selector; + rd.matchingType = this.matchingType; + rd.certificate = this.certificate; + + return rr; + } + + getJSON() { + return { + type: 'TLSA', + usage: this.usage, + selector: this.selector, + matchingType: this.matchingType, + certificate: this.certificate.toString('hex') + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid TLSA record.'); + assert(json.type === 'TLSA', + 'Invalid TLSA record. Type must be "TLSA".'); + assert((json.usage & 0xff) === json.usage, + 'Invalid TLSA record. usage must be a uint8.'); + assert((json.selector & 0xff) === json.selector, + 'Invalid TLSA record. selector must be a uint8.'); + assert((json.matchingType & 0xff) === json.matchingType, + 'Invalid TLSA record. matchingType must be a uint8.'); + assert(typeof json.certificate === 'string', + 'Invalid TLSA record. certificate must be a String.'); + assert((json.certificate.length >>> 1) <= 255, + 'Invalid TLSA record. certificate is too large.'); + + this.usage = json.usage; + this.selector = json.selector; + this.matchingType = json.matchingType; + this.certificate = util.parseHex(json.certificate); + + return this; + } +} + +/** + * SSHFP + * @extends {Struct} + */ + +class SSHFP extends Struct { + constructor() { + super(); + this.algorithm = 0; + this.digestType = 0; + this.fingerprint = DUMMY; + } + + get type() { + return hsTypes.SSHFP; + } + + getSize() { + return 3 + this.fingerprint.length; + } + + write(bw) { + bw.writeU8(this.algorithm); + bw.writeU8(this.digestType); + bw.writeU8(this.fingerprint.length); + bw.writeBytes(this.fingerprint); + return this; + } + + read(br) { + this.algorithm = br.readU8(); + this.digestType = br.readU8(); + this.fingerprint = br.readBytes(br.readU8()); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new SSHFPRecord(); + + rr.name = name; + rr.type = types.SSHFP; + rr.ttl = ttl; + rr.data = rd; + rd.algorithm = this.algorithm; + rd.digestType = this.digestType; + rd.fingerprint = this.fingerprint; + + return rr; + } + + getJSON() { + return { + type: 'SSHFP', + algorithm: this.algorithm, + digestType: this.digestType, + fingerprint: this.fingerprint.toString('hex') + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid SSHFP record.'); + assert(json.type === 'SSHFP', + 'Invalid SSHFP record. Type must be "SSHFP".'); + assert((json.algorithm & 0xff) === json.algorithm, + 'Invalid SSHFP record. algorithm must be a uint8.'); + assert((json.digestType & 0xff) === json.digestType, + 'Invalid SSHFP record. digestType must be a uint8.'); + assert(typeof json.fingerprint === 'string', + 'Invalid SSHFP record. fingerprint must be a String.'); + assert((json.fingerprint.length >>> 1) <= 255, + 'Invalid SSHFP record. fingerprint is too large.'); + + this.algorithm = json.algorithm; + this.digestType = json.digestType; + this.fingerprint = util.parseHex(json.fingerprint); + + return this; + } +} + +/** + * CAA + * @extends {Struct} + */ + +class CAA extends Struct { + constructor() { + super(); + this.flags = 0; + this.tag = ''; + this.value = ''; + } + + get type() { + return hsTypes.CAA; + } + + getSize() { + return 1 + sizeString(this.tag) + sizeString(this.value); + } + + write(bw) { + bw.writeU8(this.flags); + writeStringBW(bw, this.tag); + writeStringBW(bw, this.value); + return this; + } + + read(br) { + this.flags = br.readU8(); + this.tag = readStringBR(br); + this.value = readStringBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new CAARecord(); + + rr.name = name; + rr.type = types.CAA; + rr.ttl = ttl; + rr.data = rd; + rd.flags = this.flags; + rd.tag = this.tag; + rd.value = this.value; + + return rr; + } + + getJSON() { + return { + type: 'CAA', + flags: this.flags, + tag: this.tag, + value: this.value + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid CAA record.'); + assert(json.type === 'CAA', + 'Invalid CAA record. Type must be "CAA".'); + assert((json.flags & 0xff) === json.flags, + 'Invalid CAA record. flags must be a uint8.'); + assert(typeof json.tag === 'string', + 'Invalid CAA record. tag must be a String.'); + assert(json.tag.length <= 255, + 'Invalid CAA record. tag is too large.'); + assert(typeof json.value === 'string', + 'Invalid CAA record. value must be a String.'); + assert(json.value.length <= 255, + 'Invalid CAA record. value is too large.'); + + this.flags = json.flags; + this.tag = json.tag; + this.value = json.value; + + return this; + } +} + +/** + * SOA + * @extends {Struct} + */ + +class SOA extends Struct { + constructor() { + super(); + this.ns = '.'; + this.mbox = '.'; + this.serial = 0; + this.refresh = 0; + this.retry = 0; + this.expire = 0; + this.minimum = 0; + } + + get type() { + return hsTypes.SOA; + } + + getSize(map) { + return sizeName(this.ns, map) + sizeName(this.mbox, map) + 20; + } + + write(bw, map) { + writeNameBW(bw, this.ns, map); + writeNameBW(bw, this.mbox, map); + bw.writeU32BE(this.serial); + bw.writeU32BE(this.refresh); + bw.writeU32BE(this.retry); + bw.writeU32BE(this.expire); + bw.writeU32BE(this.minimum); + return this; + } + + read(br) { + this.ns = readNameBR(br); + this.mbox = readNameBR(br); + this.serial = br.readU32BE(); + this.refresh = br.readU32BE(); + this.retry = br.readU32BE(); + this.expire = br.readU32BE(); + this.minimum = br.readU32BE(); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new SOARecord(); + + rr.name = name; + rr.type = types.SOA; + rr.ttl = ttl; + rr.data = rd; + rd.ns = this.ns; + rd.mbox = this.mbox; + rd.serial = this.serial; + rd.refresh = this.refresh; + rd.retry = this.retry; + rd.expire = this.expire; + rd.minimum = this.minimum; + + return rr; + } + + getJSON() { + return { + type: 'SOA', + ns: this.ns, + mbox: this.mbox, + serial: this.serial, + refresh: this.refresh, + retry: this.retry, + expire: this.expire, + minimum: this.minimum + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid SOA record.'); + assert(json.type === 'SOA', + 'Invalid SOA record. Type must be "SOA".'); + assert(isName(json.ns), + 'Invalid SOA record. ns must be a valid name.'); + assert(isName(json.mbox), + 'Invalid SOA record. mbox must be a valid name.'); + assert((json.serial >>> 0) === json.serial, + 'Invalid SOA record. serial must be a uint32.'); + assert((json.refresh >>> 0) === json.refresh, + 'Invalid SOA record. refresh must be a uint32.'); + assert((json.retry >>> 0) === json.retry, + 'Invalid SOA record. retry must be a uint32.'); + assert((json.expire >>> 0) === json.expire, + 'Invalid SOA record. expire must be a uint32.'); + assert((json.minimum >>> 0) === json.minimum, + 'Invalid SOA record. minimum must be a uint32.'); + + this.ns = json.ns; + this.mbox = json.mbox; + this.serial = json.serial; + this.refresh = json.refresh; + this.retry = json.retry; + this.expire = json.expire; + this.minimum = json.minimum; + + return this; + } +} + +/** + * NAPTR + * @extends {Struct} + */ + +class NAPTR extends Struct { + constructor() { + super(); + this.order = 0; + this.preference = 0; + this.flags = ''; + this.service = ''; + this.regexp = ''; + this.replacement = '.'; + } + + get type() { + return hsTypes.NAPTR; + } + + getSize(map) { + return 4 + + sizeString(this.flags) + + sizeString(this.service) + + sizeString(this.regexp) + + sizeName(this.replacement, map); + } + + write(bw, map) { + bw.writeU16BE(this.order); + bw.writeU16BE(this.preference); + writeStringBW(bw, this.flags); + writeStringBW(bw, this.service); + writeStringBW(bw, this.regexp); + writeNameBW(bw, this.replacement, map); + return this; + } + + read(br) { + this.order = br.readU16BE(); + this.preference = br.readU16BE(); + this.flags = readStringBR(br); + this.service = readStringBR(br); + this.regexp = readStringBR(br); + this.replacement = readNameBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new NAPTRRecord(); + + rr.name = name; + rr.type = types.NAPTR; + rr.ttl = ttl; + rr.data = rd; + rd.order = this.order; + rd.preference = this.preference; + rd.flags = this.flags; + rd.service = this.service; + rd.regexp = this.regexp; + rd.replacement = this.replacement; + + return rr; + } + + getJSON() { + return { + type: 'NAPTR', + order: this.order, + preference: this.preference, + flags: this.flags, + service: this.service, + regexp: this.regexp, + replacement: this.replacement + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid NAPTR record.'); + assert(json.type === 'NAPTR', + 'Invalid NAPTR record. Type must be "NAPTR".'); + assert((json.order & 0xffff) === json.order, + 'Invalid NAPTR record. order must be a uint16.'); + assert((json.preference & 0xffff) === json.preference, + 'Invalid NAPTR record. preference must be a uint16.'); + assert(typeof json.flags === 'string', + 'Invalid NAPTR record. flags must be a String.'); + assert(json.flags.length <= 255, + 'Invalid NAPTR record. flags is too large.'); + assert(typeof json.service === 'string', + 'Invalid NAPTR record. service must be a String.'); + assert(json.service.length <= 255, + 'Invalid NAPTR record. service is too large.'); + assert(typeof json.regexp === 'string', + 'Invalid NAPTR record. regexp must be a String.'); + assert(json.regexp.length <= 255, + 'Invalid NAPTR record. regexp is too large.'); + assert(isName(json.replacement), + 'Invalid NAPTR record. replacement must be a valid name.'); + + this.order = json.order; + this.preference = json.preference; + this.flags = json.flags; + this.service = json.service; + this.regexp = json.regexp; + this.replacement = json.replacement; + + return this; + } +} + +/** + * SMIMEA + * @extends {Struct} + */ + +class SMIMEA extends Struct { + constructor() { + super(); + this.usage = 0; + this.selector = 0; + this.matchingType = 0; + this.certificate = DUMMY; + } + + get type() { + return hsTypes.SMIMEA; + } + + getSize() { + return 4 + this.certificate.length; + } + + write(bw) { + bw.writeU8(this.usage); + bw.writeU8(this.selector); + bw.writeU8(this.matchingType); + bw.writeU8(this.certificate.length); + bw.writeBytes(this.certificate); + return this; + } + + read(br) { + this.usage = br.readU8(); + this.selector = br.readU8(); + this.matchingType = br.readU8(); + this.certificate = br.readBytes(br.readU8()); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new SMIMEARecord(); + + rr.name = name; + rr.type = types.SMIMEA; + rr.ttl = ttl; + rr.data = rd; + rd.usage = this.usage; + rd.selector = this.selector; + rd.matchingType = this.matchingType; + rd.certificate = this.certificate; + + return rr; + } + + getJSON() { + return { + type: 'SMIMEA', + usage: this.usage, + selector: this.selector, + matchingType: this.matchingType, + certificate: this.certificate.toString('hex') + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid SMIMEA record.'); + assert(json.type === 'SMIMEA', + 'Invalid SMIMEA record. Type must be "SMIMEA".'); + assert((json.usage & 0xff) === json.usage, + 'Invalid SMIMEA record. usage must be a uint8.'); + assert((json.selector & 0xff) === json.selector, + 'Invalid SMIMEA record. selector must be a uint8.'); + assert((json.matchingType & 0xff) === json.matchingType, + 'Invalid SMIMEA record. matchingType must be a uint8.'); + assert(typeof json.certificate === 'string', + 'Invalid SMIMEA record. certificate must be a String.'); + assert((json.certificate.length >>> 1) <= 255, + 'Invalid SMIMEA record. certificate is too large.'); + + this.usage = json.usage; + this.selector = json.selector; + this.matchingType = json.matchingType; + this.certificate = util.parseHex(json.certificate); + + return this; + } +} + +/** + * OPENPGPKEY + * @extends {Struct} + */ + +class OPENPGPKEY extends Struct { + constructor() { + super(); + this.publicKey = DUMMY; + } + + get type() { + return hsTypes.OPENPGPKEY; + } + + getSize() { + return 2 + this.publicKey.length; + } + + write(bw) { + bw.writeU16BE(this.publicKey.length); + bw.writeBytes(this.publicKey); + return this; + } + + read(br) { + this.publicKey = br.readBytes(br.readU16BE()); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new OPENPGPKEYRecord(); + + rr.name = name; + rr.type = types.OPENPGPKEY; + rr.ttl = ttl; + rr.data = rd; + rd.publicKey = this.publicKey; + + return rr; + } + + getJSON() { + return { + type: 'OPENPGPKEY', + publicKey: this.publicKey.toString('hex') + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid OPENPGPKEY record.'); + assert(json.type === 'OPENPGPKEY', + 'Invalid OPENPGPKEY record. Type must be "OPENPGPKEY".'); + assert(typeof json.publicKey === 'string', + 'Invalid OPENPGPKEY record. publicKey must be a String.'); + assert((json.publicKey.length >>> 1) <= 65535, + 'Invalid OPENPGPKEY record. publicKey is too large.'); + + this.publicKey = util.parseHex(json.publicKey); + + return this; + } +} + +/** + * URI + * @extends {Struct} + */ + +class URI extends Struct { + constructor() { + super(); + this.priority = 0; + this.weight = 0; + this.target = ''; + } + + get type() { + return hsTypes.URI; + } + + getSize() { + return 4 + sizeString(this.target); + } + + write(bw) { + bw.writeU16BE(this.priority); + bw.writeU16BE(this.weight); + writeStringBW(bw, this.target); + return this; + } + + read(br) { + this.priority = br.readU16BE(); + this.weight = br.readU16BE(); + this.target = readStringBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new URIRecord(); + + rr.name = name; + rr.type = types.URI; + rr.ttl = ttl; + rr.data = rd; + rd.priority = this.priority; + rd.weight = this.weight; + rd.target = this.target; + + return rr; + } + + getJSON() { + return { + type: 'URI', + priority: this.priority, + weight: this.weight, + target: this.target + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid URI record.'); + assert(json.type === 'URI', + 'Invalid URI record. Type must be "URI".'); + assert((json.priority & 0xffff) === json.priority, + 'Invalid URI record. priority must be a uint16.'); + assert((json.weight & 0xffff) === json.weight, + 'Invalid URI record. weight must be a uint16.'); + assert(typeof json.target === 'string', + 'Invalid URI record. target must be a String.'); + assert(json.target.length <= 255, + 'Invalid URI record. target is too large.'); + + this.priority = json.priority; + this.weight = json.weight; + this.target = json.target; + + return this; + } +} + +/** + * LOC + * @extends {Struct} + */ + +class LOC extends Struct { + constructor() { + super(); + this.version = 0; + this.size = 0; + this.horizPre = 0; + this.vertPre = 0; + this.latitude = 0; + this.longitude = 0; + this.altitude = 0; + } + + get type() { + return hsTypes.LOC; + } + + getSize() { + return 16; + } + + write(bw) { + bw.writeU8(this.version); + bw.writeU8(this.size); + bw.writeU8(this.horizPre); + bw.writeU8(this.vertPre); + bw.writeU32BE(this.latitude); + bw.writeU32BE(this.longitude); + bw.writeU32BE(this.altitude); + return this; + } + + read(br) { + this.version = br.readU8(); + this.size = br.readU8(); + this.horizPre = br.readU8(); + this.vertPre = br.readU8(); + this.latitude = br.readU32BE(); + this.longitude = br.readU32BE(); + this.altitude = br.readU32BE(); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new LOCRecord(); + + rr.name = name; + rr.type = types.LOC; + rr.ttl = ttl; + rr.data = rd; + rd.version = this.version; + rd.size = this.size; + rd.horizPre = this.horizPre; + rd.vertPre = this.vertPre; + rd.latitude = this.latitude; + rd.longitude = this.longitude; + rd.altitude = this.altitude; + + return rr; + } + + getJSON() { + return { + type: 'LOC', + version: this.version, + size: this.size, + horizPre: this.horizPre, + vertPre: this.vertPre, + latitude: this.latitude, + longitude: this.longitude, + altitude: this.altitude + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid LOC record.'); + assert(json.type === 'LOC', + 'Invalid LOC record. Type must be "LOC".'); + assert((json.version & 0xff) === json.version, + 'Invalid LOC record. version must be a uint8.'); + assert((json.size & 0xff) === json.size, + 'Invalid LOC record. size must be a uint8.'); + assert((json.horizPre & 0xff) === json.horizPre, + 'Invalid LOC record. horizPre must be a uint8.'); + assert((json.vertPre & 0xff) === json.vertPre, + 'Invalid LOC record. vertPre must be a uint8.'); + assert((json.latitude >>> 0) === json.latitude, + 'Invalid LOC record. latitude must be a uint32.'); + assert((json.longitude >>> 0) === json.longitude, + 'Invalid LOC record. longitude must be a uint32.'); + assert((json.altitude >>> 0) === json.altitude, + 'Invalid LOC record. altitude must be a uint32.'); + + this.version = json.version; + this.size = json.size; + this.horizPre = json.horizPre; + this.vertPre = json.vertPre; + this.latitude = json.latitude; + this.longitude = json.longitude; + this.altitude = json.altitude; + + return this; + } +} + +/** + * RP + * @extends {Struct} + */ + +class RP extends Struct { + constructor() { + super(); + this.mbox = '.'; + this.txt = '.'; + } + + get type() { + return hsTypes.RP; + } + + getSize(map) { + return sizeName(this.mbox, map) + sizeName(this.txt, map); + } + + write(bw, map) { + writeNameBW(bw, this.mbox, map); + writeNameBW(bw, this.txt, map); + return this; + } + + read(br) { + this.mbox = readNameBR(br); + this.txt = readNameBR(br); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new RPRecord(); + + rr.name = name; + rr.type = types.RP; + rr.ttl = ttl; + rr.data = rd; + rd.mbox = this.mbox; + rd.txt = this.txt; + + return rr; + } + + getJSON() { + return { + type: 'RP', + mbox: this.mbox, + txt: this.txt + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid RP record.'); + assert(json.type === 'RP', + 'Invalid RP record. Type must be "RP".'); + assert(isName(json.mbox), + 'Invalid RP record. mbox must be a valid name.'); + assert(isName(json.txt), + 'Invalid RP record. txt must be a valid name.'); + + this.mbox = json.mbox; + this.txt = json.txt; + + return this; + } +} + +/** + * RAW - Generic record for arbitrary DNS types. + * Stores raw DNS rdata for any record type, including + * future types not yet defined. + * @extends {Struct} + */ + +class RAW extends Struct { + constructor() { + super(); + this.dnsType = 0; + this.data = DUMMY; + } + + get type() { + return hsTypes.RAW; + } + + getSize() { + return 4 + this.data.length; + } + + write(bw) { + bw.writeU16BE(this.dnsType); + bw.writeU16BE(this.data.length); + bw.writeBytes(this.data); + return this; + } + + read(br) { + this.dnsType = br.readU16BE(); + this.data = br.readBytes(br.readU16BE()); + return this; + } + + toDNS(name = '.', ttl = DEFAULT_TTL) { + assert(util.isFQDN(name)); + assert((ttl >>> 0) === ttl); + + const rr = new Record(); + const rd = new UNKNOWNRecord(); + + rr.name = name; + rr.type = this.dnsType; + rr.ttl = ttl; + rr.data = rd; + rd.data = this.data; + + return rr; + } + + getJSON() { + return { + type: 'RAW', + dnsType: this.dnsType, + data: this.data.toString('hex') + }; + } + + fromJSON(json) { + assert(json && typeof json === 'object', 'Invalid RAW record.'); + assert(json.type === 'RAW', + 'Invalid RAW record. Type must be "RAW".'); + assert((json.dnsType & 0xffff) === json.dnsType, + 'Invalid RAW record. dnsType must be a uint16.'); + assert(typeof json.data === 'string', + 'Invalid RAW record. data must be a hex String.'); + assert((json.data.length >>> 1) <= 65535, + 'Invalid RAW record. data is too large.'); + + this.dnsType = json.dnsType; + this.data = util.parseHex(json.data); + + return this; + } +} + /* * Helpers */ @@ -869,6 +2855,42 @@ function typeToClass(type) { return SYNTH6; case hsTypes.TXT: return TXT; + case hsTypes.A: + return A; + case hsTypes.AAAA: + return AAAA; + case hsTypes.CNAME: + return CNAME; + case hsTypes.DNAME: + return DNAME; + case hsTypes.MX: + return MX; + case hsTypes.SRV: + return SRV; + case hsTypes.TLSA: + return TLSA; + case hsTypes.SSHFP: + return SSHFP; + case hsTypes.CAA: + return CAA; + case hsTypes.SOA: + return SOA; + case hsTypes.PTR: + return PTR; + case hsTypes.NAPTR: + return NAPTR; + case hsTypes.SMIMEA: + return SMIMEA; + case hsTypes.OPENPGPKEY: + return OPENPGPKEY; + case hsTypes.URI: + return URI; + case hsTypes.LOC: + return LOC; + case hsTypes.RP: + return RP; + case hsTypes.RAW: + return RAW; default: return null; } @@ -946,3 +2968,21 @@ exports.GLUE6 = GLUE6; exports.SYNTH4 = SYNTH4; exports.SYNTH6 = SYNTH6; exports.TXT = TXT; +exports.A = A; +exports.AAAA = AAAA; +exports.CNAME = CNAME; +exports.DNAME = DNAME; +exports.MX = MX; +exports.SRV = SRV; +exports.TLSA = TLSA; +exports.SSHFP = SSHFP; +exports.CAA = CAA; +exports.SOA = SOA; +exports.PTR = PTR; +exports.NAPTR = NAPTR; +exports.SMIMEA = SMIMEA; +exports.OPENPGPKEY = OPENPGPKEY; +exports.URI = URI; +exports.LOC = LOC; +exports.RP = RP; +exports.RAW = RAW; diff --git a/lib/dns/server.js b/lib/dns/server.js index 71ac275cb..121fc4426 100644 --- a/lib/dns/server.js +++ b/lib/dns/server.js @@ -26,9 +26,7 @@ const { DEFAULT_TTL, TYPE_MAP_ROOT, TYPE_MAP_EMPTY, - TYPE_MAP_NS, - TYPE_MAP_A, - TYPE_MAP_AAAA + TYPE_MAP_NS } = require('./common'); const { @@ -45,6 +43,7 @@ const { AAAARecord, NSRecord, SOARecord, + NSECRecord, types, codes } = wire; @@ -346,8 +345,9 @@ class RootServer extends DNSServer { // Query must be for the correct synth version if (type !== synthType) { // SYNTH4/6 proof: - const typeMap = synthType === types.A ? TYPE_MAP_A : TYPE_MAP_AAAA; - res.authority.push(nsec.create(name, '\\000.' + name, typeMap)); + const rd = new NSECRecord(); + rd.setTypes([synthType, types.RRSIG, types.NSEC]); + res.authority.push(nsec.create(name, '\\000.' + name, rd.typeBitmap)); key.signZSK(res.authority, types.NSEC); res.authority.push(this.toSOA()); diff --git a/lib/mining/miner.js b/lib/mining/miner.js index 9a8913c78..ec56dff91 100644 --- a/lib/mining/miner.js +++ b/lib/mining/miner.js @@ -151,7 +151,8 @@ class Miner extends EventEmitter { coinbaseFlags: this.options.coinbaseFlags, interval: this.network.halvingInterval, weight: this.options.reservedWeight, - sigops: this.options.reservedSigops + sigops: this.options.reservedSigops, + network: this.network }); this.assemble(attempt); diff --git a/lib/mining/template.js b/lib/mining/template.js index 41dfa7d91..9f33103c4 100644 --- a/lib/mining/template.js +++ b/lib/mining/template.js @@ -75,6 +75,7 @@ class BlockTemplate { this.items = []; this.claims = []; this.airdrops = []; + this.network = null; if (options) this.fromOptions(options); @@ -203,6 +204,9 @@ class BlockTemplate { this.airdrops = options.airdrops; } + if (options.network != null) + this.network = options.network; + return this; } @@ -313,6 +317,20 @@ class BlockTemplate { cb.outputs.push(output); } + // Add reallocation output at hard fork height. + if (this.network + && this.height === this.network.reallocationHeight + && this.network.reallocationAddress + && consensus.AIRDROP_REALLOCATION > 0) { + const reOutput = new Output(); + reOutput.value = consensus.AIRDROP_REALLOCATION; + reOutput.address = Address.fromString( + this.network.reallocationAddress, + this.network + ); + cb.outputs.push(reOutput); + } + // Add any airdrop proofs. for (const proof of this.airdrops) { const input = new Input(); diff --git a/lib/protocol/consensus.js b/lib/protocol/consensus.js index 808129653..fa0bd3f50 100644 --- a/lib/protocol/consensus.js +++ b/lib/protocol/consensus.js @@ -80,6 +80,18 @@ exports.MAX_CA_NAMING = 102e6 * exports.COIN; exports.MAX_AIRDROP = 0.952e9 * exports.COIN; +/** + * Reallocation of unclaimed airdrop and name claim tokens + * to the Handshake Foundation multisig wallet (consensus). + * Unclaimed airdrops: 686,773,848.08 HNS + * Unclaimed name claims: 187,954,986.77 HNS + * Total: 874,728,834.85 HNS + * @const {Amount} + * @default + */ + +exports.AIRDROP_REALLOCATION = 874728834850000; + /** * Maximum initial supply in dollarydoos (consensus). * @const {Amount} diff --git a/lib/protocol/network.js b/lib/protocol/network.js index 307a8e8ef..c6e963889 100644 --- a/lib/protocol/network.js +++ b/lib/protocol/network.js @@ -65,6 +65,8 @@ class Network { this.requestMempool = options.requestMempool; this.claimPrefix = options.claimPrefix; this.deflationHeight = options.deflationHeight; + this.reallocationHeight = options.reallocationHeight; + this.reallocationAddress = options.reallocationAddress; this.time = new TimeData(); this.txStart = options.txStart; diff --git a/lib/protocol/networks.js b/lib/protocol/networks.js index cd209ab05..7316579a1 100644 --- a/lib/protocol/networks.js +++ b/lib/protocol/networks.js @@ -362,7 +362,7 @@ main.names = { * @const {Number} */ - treeInterval: main.pow.blocksPerDay >>> 2, + treeInterval: 1, /** * Amount of time transfers are locked up for. @@ -640,6 +640,22 @@ main.claimPrefix = 'hns-claim:'; main.deflationHeight = 61043; +/** + * Activation height for airdrop reallocation hard fork. + * At this height, unclaimed airdrop tokens are reallocated + * to the Handshake Foundation multisig wallet. + * @const {Number} + */ + +main.reallocationHeight = 0; // TBD + +/** + * Handshake Foundation multisig address for airdrop reallocation. + * @const {String} + */ + +main.reallocationAddress = null; // TBD + /* * Testnet */ @@ -703,7 +719,7 @@ testnet.names = { claimFrequency: 2 * testnet.pow.blocksPerDay, biddingPeriod: 1 * testnet.pow.blocksPerDay, revealPeriod: 2 * testnet.pow.blocksPerDay, - treeInterval: testnet.pow.blocksPerDay >>> 2, + treeInterval: 1, transferLockup: 2 * testnet.pow.blocksPerDay, auctionMaturity: (1 + 2 + 4) * testnet.pow.blocksPerDay, noRollout: false, @@ -810,6 +826,9 @@ testnet.claimPrefix = 'hns-testnet:'; testnet.deflationHeight = 0; +testnet.reallocationHeight = 0; // TBD +testnet.reallocationAddress = null; // TBD + /* * Regtest */ @@ -868,7 +887,7 @@ regtest.names = { claimFrequency: 0, biddingPeriod: 5, revealPeriod: 10, - treeInterval: 5, + treeInterval: 1, transferLockup: 10, auctionMaturity: 5 + 10 + 50, noRollout: false, @@ -978,6 +997,9 @@ regtest.claimPrefix = 'hns-regtest:'; regtest.deflationHeight = 200; +regtest.reallocationHeight = 0; // TBD +regtest.reallocationAddress = null; // TBD + /* * Simnet */ @@ -1037,7 +1059,7 @@ simnet.names = { claimFrequency: 0, biddingPeriod: 25, revealPeriod: 50, - treeInterval: 2, + treeInterval: 1, transferLockup: 5, auctionMaturity: 25 + 50 + 25, noRollout: false, @@ -1147,6 +1169,9 @@ simnet.claimPrefix = 'hns-simnet:'; simnet.deflationHeight = 0; +simnet.reallocationHeight = 0; // TBD +simnet.reallocationAddress = null; // TBD + /* * Expose */ diff --git a/test/ns-test.js b/test/ns-test.js index 68842e7fa..c72659a4e 100644 --- a/test/ns-test.js +++ b/test/ns-test.js @@ -14,9 +14,7 @@ const { TYPE_MAP_ROOT, TYPE_MAP_EMPTY, TYPE_MAP_NS, - TYPE_MAP_TXT, - TYPE_MAP_A, - TYPE_MAP_AAAA + TYPE_MAP_TXT } = require('../lib/dns/common'); describe('RootServer', function() { @@ -370,12 +368,6 @@ describe('RootServer DNSSEC', function () { wire.types.RRSIG, wire.types.NSEC]], [TYPE_MAP_TXT, [wire.types.TXT, - wire.types.RRSIG, - wire.types.NSEC]], - [TYPE_MAP_A, [wire.types.A, - wire.types.RRSIG, - wire.types.NSEC]], - [TYPE_MAP_AAAA, [wire.types.AAAA, wire.types.RRSIG, wire.types.NSEC]] ]; @@ -461,7 +453,7 @@ describe('RootServer DNSSEC', function () { assert.strictEqual(set.length, 1); const proof = set[0]; - assert.strictEqual(proof.data.typeBitmap, TYPE_MAP_ROOT); + assert.bufferEqual(proof.data.typeBitmap, TYPE_MAP_ROOT); assert(cover('.', proof.name, proof.data.nextDomain)); assert(!cover('0.', proof.name, proof.data.nextDomain)); assert(!cover('aa.', proof.name, proof.data.nextDomain)); @@ -548,7 +540,7 @@ describe('RootServer DNSSEC', function () { const proof = set[0]; // NSEC must be exact match assert.strictEqual(qname, proof.name); - assert.strictEqual(proof.data.typeBitmap, TYPE_MAP_NS); + assert.bufferEqual(proof.data.typeBitmap, TYPE_MAP_NS); } }); @@ -582,7 +574,7 @@ describe('RootServer DNSSEC', function () { const set = util.extractSet(res.authority, tld, wire.types.NSEC); assert.strictEqual(set.length, 1); - assert.strictEqual(set[0].data.typeBitmap, TYPE_MAP_NS); + assert.bufferEqual(set[0].data.typeBitmap, TYPE_MAP_NS); } }); @@ -612,7 +604,7 @@ describe('RootServer DNSSEC', function () { const set = util.extractSet(res.authority, query.name, wire.types.NSEC); assert.strictEqual(set.length, 1); const proof = set[0]; - assert.strictEqual(proof.data.typeBitmap, query.bitmap); + assert.bufferEqual(proof.data.typeBitmap, query.bitmap); } }); @@ -634,11 +626,20 @@ describe('RootServer DNSSEC', function () { const set = util.extractSet(res.authority, query.name, wire.types.NSEC); assert.strictEqual(set.length, 1); - assert.strictEqual(set[0].data.typeBitmap, TYPE_MAP_EMPTY); + assert.bufferEqual(set[0].data.typeBitmap, TYPE_MAP_EMPTY); } }); it('should prove non-existence of a type for _synth records', async () => { + // Generate expected bitmaps dynamically + const nsecA = new wire.NSECRecord(); + nsecA.setTypes([wire.types.A, wire.types.RRSIG, wire.types.NSEC]); + const TYPE_MAP_A = nsecA.typeBitmap; + + const nsecAAAA = new wire.NSECRecord(); + nsecAAAA.setTypes([wire.types.AAAA, wire.types.RRSIG, wire.types.NSEC]); + const TYPE_MAP_AAAA = nsecAAAA.typeBitmap; + const queries = [ {name: '_040g208._synth.', type: wire.types.AAAA, bitmap: TYPE_MAP_A}, {name: '_040g208._synth.', type: wire.types.TXT, bitmap: TYPE_MAP_A}, @@ -656,7 +657,7 @@ describe('RootServer DNSSEC', function () { const set = util.extractSet(res.authority, query.name, wire.types.NSEC); assert.strictEqual(set.length, 1); const proof = set[0]; - assert.strictEqual(proof.data.typeBitmap, query.bitmap); + assert.bufferEqual(proof.data.typeBitmap, query.bitmap); } }); }); diff --git a/test/resource-test.js b/test/resource-test.js index 29b54291a..91ba9423b 100644 --- a/test/resource-test.js +++ b/test/resource-test.js @@ -3,6 +3,7 @@ const assert = require('bsert'); const {wire} = require('bns'); const {Resource} = require('../lib/dns/resource'); +const rules = require('../lib/covenants/rules'); const {types} = wire; describe('Resource', function() { @@ -64,7 +65,6 @@ describe('Resource', function() { assert(msg.answer.length === 0); assert(msg.authority.length === 8); - // Notice that the glue for `ns3.some-other-domain.` is omitted assert(msg.additional.length === 4); const [ns1, ns2, ns3, ns4, synth4, synth6, ds, rrsig] = msg.authority; @@ -116,12 +116,7 @@ describe('Resource', function() { it('should return TXT from root zone if NS is not present', () => { const res = Resource.fromJSON({ - records: [ - { - type: 'TXT', - txt: ['hello world'] - } - ] + records: [{type: 'TXT', txt: ['hello world']}] }); const msg = res.toDNS('hns.', types.TXT); @@ -160,4 +155,446 @@ describe('Resource', function() { ) ); }); + + // --- New record type round-trip tests --- + + it('should serialize A record', () => { + const j = {records: [{type: 'A', address: '1.2.3.4'}]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + assert.strictEqual(res2.toJSON().records[0].address, '1.2.3.4'); + }); + + it('should serialize AAAA record', () => { + const j = {records: [{type: 'AAAA', address: '::1'}]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize CNAME record', () => { + const j = {records: [{type: 'CNAME', target: 'example.com.'}]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize DNAME record', () => { + const j = {records: [{type: 'DNAME', target: 'example.com.'}]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize PTR record', () => { + const j = {records: [{type: 'PTR', target: 'host.example.com.'}]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize MX record', () => { + const j = {records: [{type: 'MX', preference: 10, mx: 'mail.example.com.'}]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize SRV record', () => { + const j = {records: [{ + type: 'SRV', priority: 10, weight: 60, port: 5060, + target: 'sip.example.com.' + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize TLSA record', () => { + const j = {records: [{ + type: 'TLSA', usage: 3, selector: 1, + matchingType: 1, certificate: 'aabbccdd' + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize SSHFP record', () => { + const j = {records: [{ + type: 'SSHFP', algorithm: 1, digestType: 1, fingerprint: 'aabbccdd' + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize CAA record', () => { + const j = {records: [{ + type: 'CAA', flags: 0, tag: 'issue', value: 'letsencrypt.org' + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize SOA record', () => { + const j = {records: [{ + type: 'SOA', ns: 'ns1.example.com.', mbox: 'admin.example.com.', + serial: 2021010101, refresh: 3600, retry: 900, + expire: 604800, minimum: 86400 + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize NAPTR record', () => { + const j = {records: [{ + type: 'NAPTR', order: 100, preference: 10, flags: 'u', + service: 'E2U+sip', regexp: '!^.*$!sip:info@example.com!', + replacement: '.' + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize SMIMEA record', () => { + const j = {records: [{ + type: 'SMIMEA', usage: 3, selector: 1, + matchingType: 1, certificate: 'deadbeef' + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize OPENPGPKEY record', () => { + const j = {records: [{type: 'OPENPGPKEY', publicKey: 'deadbeefcafe'}]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize URI record', () => { + const j = {records: [{ + type: 'URI', priority: 10, weight: 1, target: 'https://example.com/' + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize LOC record', () => { + const j = {records: [{ + type: 'LOC', version: 0, size: 18, horizPre: 22, vertPre: 19, + latitude: 2160199054, longitude: 2147549195, altitude: 10000000 + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + it('should serialize RP record', () => { + const j = {records: [{ + type: 'RP', mbox: 'admin.example.com.', txt: 'txt.example.com.' + }]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + }); + + // --- RAW record --- + + it('should serialize RAW record', () => { + const j = {records: [{type: 'RAW', dnsType: 999, data: 'deadbeef'}]}; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + assert.strictEqual(res2.toJSON().records[0].dnsType, 999); + assert.strictEqual(res2.toJSON().records[0].data, 'deadbeef'); + }); + + it('should convert RAW record to DNS wire format', () => { + const res = Resource.fromJSON({ + records: [{type: 'RAW', dnsType: 999, data: 'deadbeef'}] + }); + const rr = res.records[0].toDNS('example.', 3600); + assert.strictEqual(rr.type, 999); + assert.strictEqual(rr.name, 'example.'); + assert.bufferEqual(rr.data.data, Buffer.from('deadbeef', 'hex')); + }); + + // --- LABEL / SLD compression --- + + it('should serialize records with labels', () => { + const j = { + records: [ + {type: 'A', address: '1.2.3.4'}, + {type: 'A', name: 'www', address: '5.6.7.8'}, + {type: 'TLSA', name: '_443._tcp', usage: 3, selector: 1, + matchingType: 1, certificate: 'aabb'} + ] + }; + const res1 = Resource.fromJSON(j); + const res2 = Resource.decode(res1.encode()); + assert.deepStrictEqual(res1.toJSON(), res2.toJSON()); + + // Verify the labels survived round-trip. + assert.strictEqual(res2.records[0].label, ''); + assert.strictEqual(res2.records[1].label, '_443._tcp'); + assert.strictEqual(res2.records[2].label, 'www'); + }); + + it('should sort TLD records before labeled records', () => { + // Input has labeled record first. + const j = { + records: [ + {type: 'A', name: 'www', address: '5.6.7.8'}, + {type: 'A', address: '1.2.3.4'} + ] + }; + const res = Resource.fromJSON(j); + + // TLD record should be first after sorting. + assert.strictEqual(res.records[0].label, ''); + assert.strictEqual(res.records[1].label, 'www'); + }); + + it('should backwards-compat: old decoder reads TLD records before LABEL', () => { + // Simulate: resource with TLD A + labeled TLSA. + const j = { + records: [ + {type: 'A', address: '1.2.3.4'}, + {type: 'TLSA', name: '_443._tcp', usage: 3, selector: 1, + matchingType: 1, certificate: 'aabb'} + ] + }; + const res = Resource.fromJSON(j); + const encoded = res.encode(); + + // Old decoder (simulated): read until unknown type. + // The A record is at the TLD level (before any LABEL marker). + // An old node would read: version(0), type(7=A), A data, type(24=LABEL)→break + const bio2 = require('bufio'); + const br = bio2.read(encoded); + const version = br.readU8(); + assert.strictEqual(version, 0); + + const firstType = br.readU8(); + assert.strictEqual(firstType, 7); // hsTypes.A + + // Old node can read the A record data (4 bytes). + const ip = br.readBytes(4); + assert.bufferEqual(ip, Buffer.from([1, 2, 3, 4])); + + // Next byte is the LABEL marker (24) — old node doesn't know it, breaks. + const nextType = br.readU8(); + assert.strictEqual(nextType, 24); // hsTypes.LABEL + }); + + // --- DNS resolution tests --- + + it('should return authoritative A answer when no NS present', () => { + const res = Resource.fromJSON({ + records: [{type: 'A', address: '93.184.216.34'}] + }); + const msg = res.toDNS('example.', types.A); + + assert(msg.aa); + assert(msg.answer.length === 2); + assert.strictEqual(msg.answer[0].type, types.A); + assert.strictEqual(msg.answer[0].data.address, '93.184.216.34'); + assert.strictEqual(msg.answer[1].type, types.RRSIG); + }); + + it('should return CNAME for any query type (RFC 1034)', () => { + const res = Resource.fromJSON({ + records: [{type: 'CNAME', target: 'other.example.com.'}] + }); + const msg = res.toDNS('alias.', types.A); + assert(msg.aa); + assert(msg.answer.length === 2); + assert.strictEqual(msg.answer[0].type, types.CNAME); + assert.strictEqual(msg.answer[0].data.target, 'other.example.com.'); + }); + + it('should not return A from root zone if NS is present', () => { + const res = Resource.fromJSON({ + records: [ + {type: 'NS', ns: 'ns1.example.com.'}, + {type: 'A', address: '1.2.3.4'} + ] + }); + const msg = res.toDNS('example.', types.A); + assert(!msg.aa); + assert(msg.answer.length === 0); + }); + + // --- Subdomain resolution via LABEL --- + + it('should resolve subdomain A record via label', () => { + const res = Resource.fromJSON({ + records: [ + {type: 'A', address: '1.2.3.4'}, + {type: 'A', name: 'www', address: '5.6.7.8'} + ] + }); + + // Query for www.example. + const msg = res.toDNS('www.example.', types.A); + assert(msg.aa); + assert(msg.answer.length === 2); + assert.strictEqual(msg.answer[0].type, types.A); + assert.strictEqual(msg.answer[0].name, 'www.example.'); + assert.strictEqual(msg.answer[0].data.address, '5.6.7.8'); + }); + + it('should resolve TLSA at _443._tcp subdomain', () => { + const res = Resource.fromJSON({ + records: [ + {type: 'A', address: '1.2.3.4'}, + {type: 'TLSA', name: '_443._tcp', usage: 3, selector: 1, + matchingType: 1, certificate: 'aabbccdd'} + ] + }); + + const msg = res.toDNS('_443._tcp.example.', types.TLSA); + assert(msg.aa); + assert(msg.answer.length === 2); + assert.strictEqual(msg.answer[0].type, types.TLSA); + assert.strictEqual(msg.answer[0].name, '_443._tcp.example.'); + }); + + it('should return NSEC for non-existent type at subdomain', () => { + const res = Resource.fromJSON({ + records: [ + {type: 'A', address: '1.2.3.4'}, + {type: 'A', name: 'www', address: '5.6.7.8'} + ] + }); + + // Query for MX at www.example. — only A exists there. + const msg = res.toDNS('www.example.', types.MX); + assert(msg.aa); + assert(msg.answer.length === 0); + + let nsecRR = null; + for (const rr of msg.authority) { + if (rr.type === types.NSEC) { + nsecRR = rr; + break; + } + } + assert(nsecRR); + + // NSEC should list A + RRSIG + NSEC for the www label. + const expected = new wire.NSECRecord(); + expected.setTypes([types.A, types.RRSIG, types.NSEC]); + assert.bufferEqual(nsecRR.data.typeBitmap, expected.typeBitmap); + }); + + it('should still refer when NS records exist (even with labels)', () => { + const res = Resource.fromJSON({ + records: [ + {type: 'NS', ns: 'ns1.example.com.'}, + {type: 'A', name: 'www', address: '5.6.7.8'} + ] + }); + + // NS exists so subdomain queries should be referrals. + const msg = res.toDNS('www.example.', types.A); + assert(!msg.aa); + assert(msg.answer.length === 0); + assert(msg.authority.length > 0); + }); + + // --- Dynamic NSEC bitmap --- + + it('should produce dynamic NSEC bitmap for A record', () => { + const res = Resource.fromJSON({ + records: [{type: 'A', address: '1.2.3.4'}] + }); + const msg = res.toDNS('example.', types.MX); + assert(msg.aa); + assert(msg.answer.length === 0); + + let nsecRR = null; + for (const rr of msg.authority) { + if (rr.type === types.NSEC) { + nsecRR = rr; + break; + } + } + assert(nsecRR); + + const expected = new wire.NSECRecord(); + expected.setTypes([types.A, types.RRSIG, types.NSEC]); + assert.bufferEqual(nsecRR.data.typeBitmap, expected.typeBitmap); + }); + + it('should produce dynamic NSEC bitmap for multiple types', () => { + const res = Resource.fromJSON({ + records: [ + {type: 'A', address: '1.2.3.4'}, + {type: 'MX', preference: 10, mx: 'mail.example.com.'}, + {type: 'TXT', txt: ['hello']} + ] + }); + const msg = res.toDNS('example.', types.AAAA); + assert(msg.aa); + assert(msg.answer.length === 0); + + let nsecRR = null; + for (const rr of msg.authority) { + if (rr.type === types.NSEC) { + nsecRR = rr; + break; + } + } + assert(nsecRR); + + const expected = new wire.NSECRecord(); + expected.setTypes([types.A, types.MX, types.TXT, types.RRSIG, types.NSEC]); + assert.bufferEqual(nsecRR.data.typeBitmap, expected.typeBitmap); + }); + + // --- Size limit --- + + it('should accept resource up to 8192 bytes', () => { + const records = []; + for (let i = 0; i < 15; i++) { + records.push({type: 'TXT', txt: ['a'.repeat(255)]}); + } + const res = Resource.fromJSON({records}); + const encoded = res.encode(); + assert(encoded.length <= 8192); + const decoded = Resource.decode(encoded); + assert.deepStrictEqual(res.toJSON(), decoded.toJSON()); + }); + + // --- toZone with labels --- + + it('should generate zone records with correct subdomain names', () => { + const res = Resource.fromJSON({ + records: [ + {type: 'A', address: '1.2.3.4'}, + {type: 'A', name: 'www', address: '5.6.7.8'}, + {type: 'MX', preference: 10, mx: 'mail.example.'} + ] + }); + const zone = res.toZone('example.'); + + assert.strictEqual(zone.length, 3); + // Unlabeled (TLD) records come first due to sorting + assert.strictEqual(zone[0].name, 'example.'); + assert.strictEqual(zone[0].type, types.A); + assert.strictEqual(zone[1].name, 'example.'); + assert.strictEqual(zone[1].type, types.MX); + // Labeled (subdomain) records come after + assert.strictEqual(zone[2].name, 'www.example.'); + assert.strictEqual(zone[2].type, types.A); + }); });