UNPKG

hsd

Version:
1,495 lines (1,183 loc) 33.4 kB
/*! * rules.js - covenant rules for hsd * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const bio = require('bufio'); const {BufferSet} = require('buffer-map'); const blake2b = require('bcrypto/lib/blake2b'); const sha3 = require('bcrypto/lib/sha3'); const consensus = require('../protocol/consensus'); const reserved = require('./reserved'); const {locked} = require('./locked'); const OwnershipProof = require('./ownership').OwnershipProof; const AirdropProof = require('../primitives/airdropproof'); const rules = exports; /** @typedef {import('../types').Amount} AmountValue */ /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('../types').NameFlags} NameFlags */ /** @typedef {import('../protocol/network')} Network */ /** @typedef {import('../primitives/tx')} TX */ /** @typedef {import('../coins/coinview')} CoinView */ /** @typedef {import('./ownership').OwnershipProof} OwnershipProof */ /* * Constants */ const NAME_BUFFER = Buffer.allocUnsafe(63); const CHARSET = new Uint8Array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 4, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0 ]); /** * Covenant Types. * @enum {Number} * @default */ rules.types = { NONE: 0, CLAIM: 1, OPEN: 2, BID: 3, REVEAL: 4, REDEEM: 5, REGISTER: 6, UPDATE: 7, RENEW: 8, TRANSFER: 9, FINALIZE: 10, REVOKE: 11 }; const types = rules.types; /** * Covenant types by value. * @const {Object} */ rules.typesByVal = { [types.NONE]: 'NONE', [types.CLAIM]: 'CLAIM', [types.OPEN]: 'OPEN', [types.BID]: 'BID', [types.REVEAL]: 'REVEAL', [types.REDEEM]: 'REDEEM', [types.REGISTER]: 'REGISTER', [types.UPDATE]: 'UPDATE', [types.RENEW]: 'RENEW', [types.TRANSFER]: 'TRANSFER', [types.FINALIZE]: 'FINALIZE', [types.REVOKE]: 'REVOKE' }; /** * Blacklisted names. * @const {Set} */ rules.blacklist = new Set([ 'example', // ICANN reserved 'invalid', // ICANN reserved 'local', // mDNS 'localhost', // ICANN reserved 'test' // ICANN reserved ]); /** * Maximum name size for a TLD. * @const {Number} * @default */ rules.MAX_NAME_SIZE = 63; /** * Maximum resource size. * @const {Number} * @default */ rules.MAX_RESOURCE_SIZE = 512; /** * Consensus name verification flags (used for block validation). * @enum {Number} * @default */ rules.nameFlags = { VERIFY_COVENANTS_NONE: 0, // Activated when hardening soft fork activates. VERIFY_COVENANTS_HARDENED: 1 << 0, // Activated when ICANN lockup soft fork activates. VERIFY_COVENANTS_LOCKUP: 1 << 1 }; /** * Standard verify flags for covenants. * @const {rules.NameFlags} * @default */ rules.MANDATORY_VERIFY_COVENANT_FLAGS = 0; /** * Maximum covenant size. * @const {Number} * @default */ rules.MAX_COVENANT_SIZE = (0 + 1 + 32 + 1 + 4 + 2 + rules.MAX_RESOURCE_SIZE + 1 + 32); /** * Hash a domain name. * @param {String|Buffer} name * @returns {Hash} */ rules.hashName = function hashName(name) { if (Buffer.isBuffer(name)) return rules.hashBinary(name); return rules.hashString(name); }; /** * Hash a domain name. * @param {String} name * @returns {Hash} */ rules.hashString = function hashString(name) { assert(typeof name === 'string'); assert(rules.verifyString(name)); const slab = NAME_BUFFER; const written = slab.write(name, 0, slab.length, 'ascii'); assert(name.length === written); const buf = slab.slice(0, written); return rules.hashBinary(buf); }; /** * Hash a domain name. * @param {Buffer} name * @returns {Buffer} */ rules.hashBinary = function hashBinary(name) { assert(Buffer.isBuffer(name)); assert(rules.verifyBinary(name)); return sha3.digest(name); }; /** * Verify a domain name meets handshake requirements. * @param {String|Buffer} name * @returns {Boolean} */ rules.verifyName = function verifyName(name) { if (Buffer.isBuffer(name)) return rules.verifyBinary(name); return rules.verifyString(name); }; /** * Verify a domain name meets handshake requirements. * @param {String} str * @returns {Boolean} */ rules.verifyString = function verifyString(str) { assert(typeof str === 'string'); if (str.length === 0) return false; if (str.length > rules.MAX_NAME_SIZE) return false; for (let i = 0; i < str.length; i++) { const ch = str.charCodeAt(i); // No unicode characters. if (ch & 0xff80) return false; const type = CHARSET[ch]; switch (type) { case 0: // non-printable return false; case 1: // 0-9 break; case 2: // A-Z return false; case 3: // a-z break; case 4: // - and _ // Do not allow at end or beginning. if (i === 0 || i === str.length - 1) return false; break; } } if (rules.blacklist.has(str)) return false; return true; }; /** * Verify a domain name meets handshake requirements. * @param {Buffer} buf * @returns {Boolean} */ rules.verifyBinary = function verifyBinary(buf) { assert(Buffer.isBuffer(buf)); if (buf.length === 0) return false; if (buf.length > rules.MAX_NAME_SIZE) return false; for (let i = 0; i < buf.length; i++) { const ch = buf[i]; // No unicode characters. if (ch & 0x80) return false; const type = CHARSET[ch]; switch (type) { case 0: // non-printable return false; case 1: // 0-9 break; case 2: // A-Z return false; case 3: // a-z break; case 4: // - and _ // Do not allow at end or beginning. if (i === 0 || i === buf.length - 1) return false; break; } } const str = buf.toString('binary'); if (rules.blacklist.has(str)) return false; return true; }; /** * Get height and week of name hash rollout. * @param {Buffer} nameHash * @param {Network} network * @returns {Array} [height, week] */ rules.getRollout = function getRollout(nameHash, network) { assert(Buffer.isBuffer(nameHash) && nameHash.length === 32); assert(network && network.names); if (network.names.noRollout) return [0, 0]; // Modulo the hash by 52 to get week number. const week = modBuffer(nameHash, 52); // Multiply result by a number of blocks-per-week. const height = week * network.names.rolloutInterval; // Add the auction start height to the rollout height. return [network.names.auctionStart + height, week]; }; /** * Verify a name hash meets the rollout. * @param {Buffer} hash * @param {Number} height * @param {Network} network * @returns {Boolean} */ rules.hasRollout = function hasRollout(hash, height, network) { assert((height >>> 0) === height); assert(network); const [start] = rules.getRollout(hash, network); if (height < start) return false; return true; }; /** * Grind a name for rollout. * Used for testing. * @param {Number} size * @param {Number} height * @param {Network} network * @returns {String} */ rules.grindName = function grindName(size, height, network) { assert((size >>> 0) === size); assert((height >>> 0) === height); assert(network && network.names); if (height < network.names.auctionStart) height = network.names.auctionStart; for (let i = 0; i < 500000; i++) { const name = randomString(size); if (rules.blacklist.has(name)) continue; const hash = rules.hashName(name); const [start] = rules.getRollout(hash, network); if (height < start) continue; if (reserved.has(hash)) continue; return name; } throw new Error('Could not find available name.'); }; /** * Test whether a name is reserved. * @param {Buffer} nameHash * @param {Number} height * @param {Network} network * @returns {Boolean} */ rules.isReserved = function isReserved(nameHash, height, network) { assert(Buffer.isBuffer(nameHash)); assert((height >>> 0) === height); assert(network && network.names); if (network.names.noReserved) return false; if (height >= network.names.claimPeriod) return false; return reserved.has(nameHash); }; /** * Test whether a name is locked up. * ICANNLOCKUP soft fork. * @param {Buffer} nameHash * @param {Number} height * @param {Network} network * @returns {Boolean} */ rules.isLockedUp = function isLockedUp(nameHash, height, network) { assert(Buffer.isBuffer(nameHash)); assert((height >>> 0) === height); assert(network && network.names); if (network.names.noReserved) return false; if (height < network.names.claimPeriod) return false; const item = locked.get(nameHash); if (!item) return false; // ICANN Names are always LOCKED. if (item.root) return true; // Alexa names will expire after 4 years, after claim period ends. if (height < network.names.alexaLockupPeriod) return true; return false; }; /** * Create a blind bid hash from a value and nonce. * @param {AmountValue} value * @param {Buffer} nonce * @returns {Buffer} */ rules.blind = function blind(value, nonce) { assert(Number.isSafeInteger(value) && value >= 0); assert(Buffer.isBuffer(nonce) && nonce.length === 32); const bw = bio.write(40); bw.writeU64(value); bw.writeBytes(nonce); return blake2b.digest(bw.render()); }; /** * Test whether a transaction has * names contained in the set. * @param {TX} tx * @param {BufferSet} set * @returns {Boolean} */ rules.hasNames = function hasNames(tx, set) { assert(tx); assert(set instanceof BufferSet); for (const {covenant} of tx.outputs) { if (!covenant.isName()) continue; const nameHash = covenant.getHash(0); switch (covenant.type) { case types.CLAIM: case types.OPEN: if (set.has(nameHash)) return true; break; case types.BID: case types.REVEAL: case types.REDEEM: break; case types.REGISTER: case types.UPDATE: case types.RENEW: case types.TRANSFER: case types.FINALIZE: case types.REVOKE: if (set.has(nameHash)) return true; break; } } return false; }; /** * Add names to set. * @param {TX} tx * @param {BufferSet} set */ rules.addNames = function addNames(tx, set) { assert(tx); assert(set instanceof BufferSet); for (const {covenant} of tx.outputs) { if (!covenant.isName()) continue; const nameHash = covenant.getHash(0); switch (covenant.type) { case types.CLAIM: case types.OPEN: set.add(nameHash); break; case types.BID: case types.REVEAL: case types.REDEEM: break; case types.REGISTER: case types.UPDATE: case types.RENEW: case types.TRANSFER: case types.FINALIZE: case types.REVOKE: set.add(nameHash); break; } } }; /** * Remove names from set. * @param {TX} tx * @param {BufferSet} set */ rules.removeNames = function removeNames(tx, set) { assert(tx); assert(set instanceof BufferSet); for (const {covenant} of tx.outputs) { if (!covenant.isName()) continue; const nameHash = covenant.getHash(0); switch (covenant.type) { case types.CLAIM: case types.OPEN: set.delete(nameHash); break; case types.BID: case types.REVEAL: case types.REDEEM: break; case types.REGISTER: case types.UPDATE: case types.RENEW: case types.TRANSFER: case types.FINALIZE: case types.REVOKE: set.delete(nameHash); break; } } }; /** * Count name opens. * @param {TX} tx * @returns {Number} */ rules.countOpens = function countOpens(tx) { assert(tx); let total = 0; for (const {covenant} of tx.outputs) { if (covenant.isOpen()) total += 1; } return total; }; /** * Count name updates. * @param {TX} tx * @returns {Number} */ rules.countUpdates = function countUpdates(tx) { assert(tx); let total = 0; for (const {covenant} of tx.outputs) { switch (covenant.type) { case types.NONE: break; case types.CLAIM: case types.OPEN: total += 1; break; case types.BID: case types.REVEAL: case types.REDEEM: break; case types.REGISTER: break; case types.UPDATE: total += 1; break; case types.RENEW: break; case types.TRANSFER: total += 1; break; case types.FINALIZE: break; case types.REVOKE: total += 1; break; } } return total; }; /** * Count name renewals. * @param {TX} tx * @returns {Number} */ rules.countRenewals = function countRenewals(tx) { assert(tx); let total = 0; for (const {covenant} of tx.outputs) { switch (covenant.type) { case types.REGISTER: total += 1; break; case types.RENEW: total += 1; break; case types.FINALIZE: total += 1; break; } } return total; }; /** * Check covenant sanity (called from `tx.checkSanity()`). * @param {TX} tx * @returns {Boolean} */ rules.hasSaneCovenants = function hasSaneCovenants(tx) { assert(tx); // Coinbases are only capable of creating claims. if (tx.isCoinbase()) { if (tx.inputs.length > tx.outputs.length) return false; for (let i = 0; i < tx.outputs.length; i++) { const output = tx.outputs[i]; const {covenant} = output; switch (covenant.type) { case types.NONE: { // Just a regular payment. if (covenant.items.length !== 0) return false; // Airdrop proof. if (i > 0 && i < tx.inputs.length) { const input = tx.inputs[i]; const {witness} = input; // Must have exactly 1 witness item. if (witness.items.length !== 1) return false; let proof; try { proof = AirdropProof.decode(witness.items[0]); } catch (e) { return false; } if (!proof.isSane()) return false; } break; } case types.CLAIM: { // Must not be the first input/output. if (i === 0) return false; // Must be linked. if (i >= tx.inputs.length) return false; const input = tx.inputs[i]; const {witness} = input; // Must have exactly 1 witness item. if (witness.items.length !== 1) return false; // Should contain a name hash, height, // name, flags, block hash, and block height. if (covenant.items.length !== 6) return false; // Name hash is 32 bytes. if (covenant.items[0].length !== 32) return false; // Height must be 4 bytes. if (covenant.items[1].length !== 4) return false; // Name must be valid. if (!rules.verifyName(covenant.items[2])) return false; // Flags must be 1 byte. if (covenant.items[3].length !== 1) return false; // Block hash must be 32 bytes. if (covenant.items[4].length !== 32) return false; // Block height must be 4 bytes. if (covenant.items[5].length !== 4) return false; // Must be a reserved name. const nameHash = covenant.items[0]; if (!reserved.has(nameHash)) return false; // Must match the hash. const name = covenant.items[2]; const key = rules.hashName(name); if (!key.equals(nameHash)) return false; /** @type {OwnershipProof} */ let proof; try { proof = OwnershipProof.decode(witness.items[0]); } catch (e) { return false; } if (!proof.isSane()) return false; if (proof.getName() !== name.toString('binary')) return false; const flags = covenant.getU8(3); const weak = (flags & 1) !== 0; if (proof.isWeak() !== weak) return false; break; } default: { return false; } } } return true; } for (let i = 0; i < tx.outputs.length; i++) { const output = tx.outputs[i]; const {covenant} = output; switch (covenant.type) { case types.NONE: { // Just a regular payment. // Can come from a NONE or a REDEEM. if (covenant.items.length !== 0) return false; break; } case types.CLAIM: { // Cannot exist in a non-coinbase. return false; } case types.OPEN: { // Has to come from NONE or REDEEM. // Should contain a name hash, zero height, and name. if (covenant.items.length !== 3) return false; // Name hash is 32 bytes. if (covenant.items[0].length !== 32) return false; // Height is 4 bytes. if (covenant.items[1].length !== 4) return false; // Height must be zero. if (covenant.getU32(1) !== 0) return false; // Name must be valid. if (!rules.verifyName(covenant.items[2])) return false; const key = rules.hashName(covenant.items[2]); if (!key.equals(covenant.items[0])) return false; break; } case types.BID: { // Has to come from NONE or REDEEM. // Should contain a name hash, name, height, and hash. if (covenant.items.length !== 4) return false; // Name hash is 32 bytes. if (covenant.items[0].length !== 32) return false; // Height must be 4 bytes. if (covenant.items[1].length !== 4) return false; // Name must be valid. if (!rules.verifyName(covenant.items[2])) return false; // Hash must be 32 bytes. if (covenant.items[3].length !== 32) return false; const key = rules.hashName(covenant.items[2]); if (!key.equals(covenant.items[0])) return false; break; } case types.REVEAL: { // Has to come from a BID. if (i >= tx.inputs.length) return false; // Should contain name hash, height, and nonce. if (covenant.items.length !== 3) return false; // Name hash must be valid. if (covenant.items[0].length !== 32) return false; // Height must be 4 bytes. if (covenant.items[1].length !== 4) return false; // Nonce must be 32 bytes. if (covenant.items[2].length !== 32) return false; break; } case types.REDEEM: { // Has to come from a REVEAL. if (i >= tx.inputs.length) return false; // Should contain name hash and height. if (covenant.items.length !== 2) return false; // Name hash must be valid. if (covenant.items[0].length !== 32) return false; // Height must be 4 bytes. if (covenant.items[1].length !== 4) return false; break; } case types.REGISTER: { // Has to come from a REVEAL. if (i >= tx.inputs.length) return false; // Should contain name hash, height, // record data and block hash. if (covenant.items.length !== 4) return false; // Name hash must be valid. if (covenant.items[0].length !== 32) return false; // Height must be 4 bytes. if (covenant.items[1].length !== 4) return false; // Record data is limited to 512 bytes. if (covenant.items[2].length > rules.MAX_RESOURCE_SIZE) return false; // Must be a block hash. if (covenant.items[3].length !== 32) return false; break; } case types.UPDATE: { // Has to come from a REGISTER, UPDATE, RENEW, or FINALIZE. if (i >= tx.inputs.length) return false; // Should contain name hash, height, and record data. if (covenant.items.length !== 3) return false; // Name hash must be valid. if (covenant.items[0].length !== 32) return false; // Height must be 4 bytes. if (covenant.items[1].length !== 4) return false; // Record data is limited to 512 bytes. if (covenant.items[2].length > rules.MAX_RESOURCE_SIZE) return false; break; } case types.RENEW: { // Has to come from a REGISTER, UPDATE, RENEW, or FINALIZE. if (i >= tx.inputs.length) return false; // Should contain name hash, height, // and a block hash. if (covenant.items.length !== 3) return false; // Name hash must be valid. if (covenant.items[0].length !== 32) return false; // Height must be 4 bytes. if (covenant.items[1].length !== 4) return false; // Must be a block hash. if (covenant.items[2].length !== 32) return false; break; } case types.TRANSFER: { // Has to come from a REGISTER, UPDATE, RENEW, or FINALIZE. if (i >= tx.inputs.length) return false; // Should contain the address we // _intend_ to transfer to. if (covenant.items.length !== 4) return false; // Name hash must be valid. if (covenant.items[0].length !== 32) return false; // Height must be 4 bytes. if (covenant.items[1].length !== 4) return false; // Version must be 1 byte. if (covenant.items[2].length !== 1) return false; // Must have an address. const version = covenant.getU8(2); const hash = covenant.items[3]; // Todo: Add policy rule for high versions. if (version > 31) return false; // Must obey address size limits. if (hash.length < 2 || hash.length > 40) return false; break; } case types.FINALIZE: { // Has to come from a TRANSFER. if (i >= tx.inputs.length) return false; // Should contain name hash and state. if (covenant.items.length !== 7) return false; // Name hash must be valid. if (covenant.items[0].length !== 32) return false; // Must be height. if (covenant.items[1].length !== 4) return false; // Name must be valid. if (!rules.verifyName(covenant.items[2])) return false; // Must be flags byte. if (covenant.items[3].length !== 1) return false; // Must be claim height. if (covenant.items[4].length !== 4) return false; // Must be renewal count. if (covenant.items[5].length !== 4) return false; // Must be a block hash. if (covenant.items[6].length !== 32) return false; const key = rules.hashName(covenant.items[2]); if (!key.equals(covenant.items[0])) return false; break; } case types.REVOKE: { // Has to come from a REGISTER, UPDATE, RENEW, or FINALIZE. if (i >= tx.inputs.length) return false; // Should contain name hash and height. if (covenant.items.length !== 2) return false; // Name hash must be valid. if (covenant.items[0].length !== 32) return false; // Must be height. if (covenant.items[1].length !== 4) return false; break; } default: { // Unknown covenant. // Don't enforce anything other than DoS limits. if (covenant.items.length > consensus.MAX_SCRIPT_STACK) return false; if (covenant.getSize() > rules.MAX_COVENANT_SIZE) return false; break; } } } return true; }; /** * Perform contextual verification for covenants. * Called from `tx.checkInputs()`. * @param {TX} tx * @param {CoinView} view * @param {Number} height * @param {Network} network * @returns {Number} */ rules.verifyCovenants = function verifyCovenants(tx, view, height, network) { assert(tx && view); assert(network && network.names); if (tx.isCoinbase()) { let conjured = 0; for (let i = 1; i < tx.inputs.length; i++) { const {witness} = tx.inputs[i]; const output = tx.output(i); const covenant = tx.covenant(i); assert(output && covenant); // Airdrop Proof. if (!covenant.isClaim()) { assert(covenant.isNone()); if (witness.items.length !== 1) return -1; let proof; try { proof = AirdropProof.decode(witness.items[0]); } catch (e) { return -1; } assert(proof.isSane()); const value = proof.getValue(); if (output.value !== value - proof.fee) return -1; if (output.address.version !== proof.version) return -1; if (!output.address.hash.equals(proof.address)) return -1; conjured += value; if (conjured > consensus.MAX_MONEY) return -1; continue; } // DNSSEC Ownership Proof. if (covenant.getU32(1) !== height) return -1; if (witness.items.length !== 1) return -1; /** @type {OwnershipProof} */ let proof; try { proof = OwnershipProof.decode(witness.items[0]); } catch (e) { return -1; } const data = proof.getData(network); if (!data) return -1; if (output.address.version !== data.version) return -1; if (!output.address.hash.equals(data.hash)) return -1; if (!covenant.getHash(4).equals(data.commitHash)) return -1; if (covenant.getU32(5) !== data.commitHeight) return -1; if (output.value !== data.value - data.fee) return -1; if (data.commitHeight === 0) // Fail early. return -1; if (height >= network.deflationHeight) { if (data.commitHeight === 1) { if (data.fee > 1000 * consensus.COIN) return -1; conjured += data.value; } else { conjured += output.value; } } else { conjured += data.value; } if (conjured > consensus.MAX_MONEY) return -1; } return conjured; } for (let i = 0; i < tx.inputs.length; i++) { const {prevout} = tx.inputs[i]; const entry = view.getEntry(prevout); assert(entry); // coin is the UTXO being consumed as an input const coin = entry.output; const uc = coin.covenant; // output is the UTXO being created by the tx const output = tx.output(i); const covenant = tx.covenant(i); switch (uc.type) { case types.NONE: case types.OPEN: case types.REDEEM: { // Can go nowhere, does not need to be linked. if (!output) break; // Can only go to a NONE, OPEN, or BID. switch (covenant.type) { case types.NONE: break; case types.OPEN: break; case types.BID: break; default: return -1; } break; } case types.BID: { // Must be be linked. if (!output) return -1; // Bid has to go to a reveal. if (!covenant.isReveal()) return -1; // Names must match. if (!covenant.getHash(0).equals(uc.getHash(0))) return -1; // Heights must match. if (covenant.getU32(1) !== uc.getU32(1)) return -1; const nonce = covenant.getHash(2); const blind = rules.blind(output.value, nonce); // The value and nonce must match the // hash they presented in their bid. if (!blind.equals(uc.getHash(3))) return -1; // If they lied to us, they can // never redeem their money. if (coin.value < output.value) return -1; break; } case types.CLAIM: case types.REVEAL: { // Must be be linked. if (!output) return -1; // Reveal has to go to a REGISTER, or // a REDEEM (in the case of the loser). switch (covenant.type) { case types.REGISTER: { // Names must match. if (!covenant.getHash(0).equals(uc.getHash(0))) return -1; // Heights must match. if (covenant.getU32(1) !== uc.getU32(1)) return -1; // Addresses must match. if (!output.address.equals(coin.address)) return -1; // Note: We use a vickrey auction. // Output value must match the second // highest bid. This will be checked // elsewhere. break; } case types.REDEEM: { // Names must match. if (!covenant.getHash(0).equals(uc.getHash(0))) return -1; // Heights must match. if (covenant.getU32(1) !== uc.getU32(1)) return -1; if (uc.isClaim()) return -1; break; } default: { return -1; } } break; } case types.REGISTER: case types.UPDATE: case types.RENEW: case types.FINALIZE: { // Must be be linked. if (!output) return -1; // Money is now locked up forever. if (output.value !== coin.value) return -1; // Addresses must match. if (!output.address.equals(coin.address)) return -1; // Can only send to an UPDATE, RENEW, TRANSFER, or REVOKE. switch (covenant.type) { case types.UPDATE: case types.RENEW: case types.TRANSFER: case types.REVOKE: { // Names must match. if (!covenant.getHash(0).equals(uc.getHash(0))) return -1; // Heights must match. if (covenant.getU32(1) !== uc.getU32(1)) return -1; break; } default: { return -1; } } break; } case types.TRANSFER: { // Must be be linked. if (!output) return -1; // Money is now locked up forever. if (output.value !== coin.value) return -1; // Can only send to an UPDATE, RENEW, FINALIZE, or REVOKE. switch (covenant.type) { case types.UPDATE: case types.RENEW: case types.REVOKE: { // Names must match. if (!covenant.getHash(0).equals(uc.getHash(0))) return -1; // Heights must match. if (covenant.getU32(1) !== uc.getU32(1)) return -1; // Addresses must match. if (!output.address.equals(coin.address)) return -1; break; } case types.FINALIZE: { // Names must match. if (!covenant.getHash(0).equals(uc.getHash(0))) return -1; // Heights must match. if (covenant.getU32(1) !== uc.getU32(1)) return -1; // Address must match the one committed // to in the original transfer covenant. if (output.address.version !== uc.getU8(2)) return -1; if (!output.address.hash.equals(uc.get(3))) return -1; break; } default: { return -1; } } break; } case types.REVOKE: { // Revocations are perma-burned. return -1; } default: { // Unknown covenant. // Don't enforce anything. if (covenant.isName()) return -1; break; } } } return 0; }; /* * Helpers */ function randomString(len) { assert((len >>> 0) === len); let s = ''; for (let i = 0; i < len; i++) { const n = Math.random() * (0x7b - 0x61) + 0x61; const c = Math.floor(n); s += String.fromCharCode(c); } return s; } function modBuffer(buf, num) { assert(Buffer.isBuffer(buf)); assert((num & 0xff) === num); assert(num !== 0); const p = 256 % num; let acc = 0; for (let i = 0; i < buf.length; i++) acc = (p * acc + buf[i]) % num; return acc; }