535 lines
13 KiB
JavaScript
535 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const bio = require('bufio');
|
|
const blake2b = require('bcrypto/lib/blake2b');
|
|
|
|
const types = {
|
|
NONE: 0,
|
|
BID: 1,
|
|
REVEAL: 2,
|
|
REGISTER: 3,
|
|
REDEEM: 4,
|
|
TRANSFER: 5,
|
|
REVOKE: 6
|
|
};
|
|
|
|
exports.types = types;
|
|
|
|
exports.MAX_NAME_SIZE = 64;
|
|
exports.MAX_RECORD_SIZE = 256;
|
|
exports.MAX_COVENANT_SIZE = 1 + exports.MAX_RECORD_SIZE;
|
|
exports.MAX_COVENANT_TYPE = types.REVOKE;
|
|
exports.ROLLOUT_INTERVAL = (7 * 24 * 60) / 2.5 | 0;
|
|
exports.RENEWAL_PERIOD = (182 * 24 * 60) / 2.5 | 0;
|
|
exports.RENEWAL_WINDOW = (365 * 24 * 60) / 2.5 | 0;
|
|
exports.RENEWAL_MATURITY = (30 * 24 * 60) / 2.5 | 0;
|
|
exports.REVOCATION_WINDOW = (2 * 24 * 60) / 2.5 | 0;
|
|
|
|
exports.BIDDING_PERIOD = 1;
|
|
exports.REVEAL_PERIOD = 1;
|
|
exports.TOTAL_PERIOD = exports.BIDDING_PERIOD + exports.REVEAL_PERIOD;
|
|
|
|
exports.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, 3, 0, 0,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 3,
|
|
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, 0
|
|
]);
|
|
|
|
exports.verifyName = function verifyName(name) {
|
|
if (Buffer.isBuffer(name))
|
|
return exports.verifyBinary(name);
|
|
return exports.verifyString(name);
|
|
};
|
|
|
|
exports.verifyString = function verifyString(str) {
|
|
assert(typeof str === 'string');
|
|
|
|
if (str.length === 0)
|
|
return false;
|
|
|
|
if (str.length > exports.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 = exports.CHARSET[ch];
|
|
|
|
switch (type) {
|
|
case 0: // non-printable
|
|
return false;
|
|
case 1: // 0-9
|
|
case 2: // a-z
|
|
break;
|
|
case 3: // - and _
|
|
// Do not allow at end or beginning.
|
|
if (i === 0 || i === str.length - 1)
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
exports.verifyBinary = function verifyBinary(buf) {
|
|
assert(Buffer.isBuffer(buf));
|
|
|
|
if (buf.length === 0)
|
|
return false;
|
|
|
|
if (buf.length > exports.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 = exports.CHARSET[ch];
|
|
|
|
switch (type) {
|
|
case 0: // non-printable
|
|
return false;
|
|
case 1: // 0-9
|
|
case 2: // a-z
|
|
break;
|
|
case 3: // - and _
|
|
// Do not allow at end or beginning.
|
|
if (i === 0 || i === buf.length - 1)
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
exports.hasSaneCovenants = function hasSaneCovenants(tx) {
|
|
// Coinbases cannot use covenants.
|
|
if (tx.isCoinbase()) {
|
|
for (const {covenant} of tx.outputs) {
|
|
if (covenant.type !== types.NONE)
|
|
return false;
|
|
|
|
if (covenant.items.length !== 0)
|
|
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 payment or a reveal (loser).
|
|
if (covenant.items.length !== 0)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.BID: {
|
|
// Should contain a name and hash.
|
|
if (covenant.items.length !== 2)
|
|
return false;
|
|
|
|
// Name must be valid.
|
|
if (!exports.verifyName(covenant.items[0]))
|
|
return false;
|
|
|
|
// Hash must be 32 bytes.
|
|
if (covenant.items[1].length !== 32)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.REVEAL: {
|
|
// Has to come from a BID.
|
|
if (i >= tx.inputs.length)
|
|
return false;
|
|
|
|
// Should contain a nonce.
|
|
if (covenant.items.length !== 2)
|
|
return false;
|
|
|
|
// Name must be valid.
|
|
if (!exports.verifyName(covenant.items[0]))
|
|
return false;
|
|
|
|
// Nonce must be 32 bytes.
|
|
if (covenant.items[1].length !== 32)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.REGISTER: {
|
|
// Has to come from an REGISTER or REVEAL.
|
|
if (i >= tx.inputs.length)
|
|
return false;
|
|
|
|
// Should contain record data and possibly a block hash.
|
|
if (covenant.items.length < 2 || covenant.items.length > 3)
|
|
return false;
|
|
|
|
// Name must be valid.
|
|
if (!exports.verifyName(covenant.items[0]))
|
|
return false;
|
|
|
|
// Record data is limited to 1kb.
|
|
if (covenant.items[1].length > exports.MAX_RECORD_SIZE)
|
|
return false;
|
|
|
|
if (covenant.items.length === 3) {
|
|
// Must be a block hash.
|
|
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 data.
|
|
if (covenant.items.length !== 1)
|
|
return false;
|
|
|
|
// Name must be valid.
|
|
if (!exports.verifyName(covenant.items[0]))
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.TRANSFER: {
|
|
// Has to come from an REGISTER.
|
|
if (i >= tx.inputs.length)
|
|
return false;
|
|
|
|
// Should contain record data and address.
|
|
if (covenant.items.length !== 4)
|
|
return false;
|
|
|
|
// Name must be valid.
|
|
if (!exports.verifyName(covenant.items[0]))
|
|
return false;
|
|
|
|
// Record data is limited to 1kb.
|
|
if (covenant.items[1].length > exports.MAX_RECORD_SIZE)
|
|
return false;
|
|
|
|
// Must obey address size limits.
|
|
if (covenant.items[2].length < 4 || covenant.items[2].length > 42)
|
|
return false;
|
|
|
|
// No point in transferring if addrs are the same.
|
|
if (output.address.toRaw().equals(covenant.items[2]))
|
|
return false;
|
|
|
|
// Must commit to the value the recipient is putting in.
|
|
let value;
|
|
try {
|
|
value = encoding.readU64(uc.items[3], 0);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
|
|
if (value > consensus.MAX_MONEY)
|
|
return false;
|
|
|
|
if (output.value < value)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.REVOKE: {
|
|
// Has to come from an REGISTER.
|
|
if (i >= tx.inputs.length)
|
|
return false;
|
|
|
|
// Should contain name data.
|
|
if (covenant.items.length !== 1)
|
|
return false;
|
|
|
|
// Name must be valid.
|
|
if (!exports.verifyName(covenant.items[0]))
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
default: {
|
|
// Unknown covenant.
|
|
// Don't enforce anything.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
exports.isLinked = function isLinked(covenant) {
|
|
return covenant.type > types.BID;
|
|
};
|
|
|
|
exports.isUnspendable = function isUnspendable(coin, height) {
|
|
switch (coin.covenant.type) {
|
|
case types.NONE:
|
|
case types.REDEEM:
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
};
|
|
|
|
exports.verifyCovenants = function verifyCovenants(tx, view, height) {
|
|
if (tx.isCoinbase())
|
|
return true;
|
|
|
|
for (let i = 0; i < tx.inputs.length; i++) {
|
|
const {prevout} = tx.inputs[i];
|
|
const entry = view.getEntry(prevout);
|
|
|
|
if (!entry)
|
|
continue;
|
|
|
|
const coin = entry.output;
|
|
const uc = coin.covenant;
|
|
|
|
let output = null;
|
|
let covenant = null;
|
|
|
|
if (i < tx.outputs.length) {
|
|
output = tx.outputs[i];
|
|
covenant = output.covenant;
|
|
}
|
|
|
|
switch (uc.type) {
|
|
case types.NONE:
|
|
case types.REDEEM: {
|
|
// Can go nowhere.
|
|
if (!output)
|
|
break;
|
|
|
|
// Can only go to a payment or bid.
|
|
if (covenant.type !== types.NONE
|
|
&& covenant.type !== types.BID
|
|
&& covenant.type <= exports.MAX_COVENANT_TYPE) {
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case types.BID: {
|
|
// Must be be linked.
|
|
if (!output)
|
|
return false;
|
|
|
|
// Bid has to go to a reveal.
|
|
if (covenant.type !== types.REVEAL)
|
|
return false;
|
|
|
|
// Names must match.
|
|
if (!covenant.items[0].equals(uc.items[0]))
|
|
return false;
|
|
|
|
const nonce = covenant.items[1];
|
|
const blind = exports.blind(output.value, nonce);
|
|
|
|
// The value and nonce must match the
|
|
// hash they presented in their bid.
|
|
if (!blind.equals(uc.items[1]))
|
|
return false;
|
|
|
|
// If they lied to us, they can
|
|
// never redeem their money.
|
|
if (coin.value < output.value)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.REVEAL: {
|
|
// Must be be linked.
|
|
if (!output)
|
|
return false;
|
|
|
|
// Addresses must match.
|
|
if (!output.address.equals(coin.address))
|
|
return false;
|
|
|
|
// Names must match.
|
|
if (!covenant.items[0].equals(uc.items[0]))
|
|
return false;
|
|
|
|
// Reveal has to go to an REGISTER, or
|
|
// a REDEEM (in the case of the loser).
|
|
switch (covenant.type) {
|
|
case types.REGISTER: {
|
|
// Money is now locked up forever.
|
|
if (output.value !== coin.value)
|
|
return false;
|
|
|
|
// No renewals allowed here.
|
|
if (covenant.items.length !== 2)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.REDEEM: {
|
|
break;
|
|
}
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case types.REGISTER: {
|
|
// Must be be linked.
|
|
if (!output)
|
|
return false;
|
|
|
|
// Addresses must match.
|
|
if (!output.address.equals(coin.address))
|
|
return false;
|
|
|
|
// Names must match.
|
|
if (!covenant.items[0].equals(uc.items[0]))
|
|
return false;
|
|
|
|
// Can only send to another
|
|
// REGISTER or TRANSFER.
|
|
switch (covenant.type) {
|
|
case types.TRANSFER: {
|
|
// Record data must match during a transfer.
|
|
if (!covenant.items[1].equals(uc.items[1]))
|
|
return false;
|
|
|
|
// Value of the output must be the name
|
|
// value plus the recipients value.
|
|
const value = encoding.readU64(covenant.items[3], 0);
|
|
|
|
if (output.value !== coin.value + value)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.REGISTER: {
|
|
// Money is now locked up forever.
|
|
if (output.value !== coin.value)
|
|
return false;
|
|
break;
|
|
}
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case types.TRANSFER: {
|
|
// Must be be linked.
|
|
if (!output)
|
|
return false;
|
|
|
|
// Names must match.
|
|
if (!covenant.items[0].equals(uc.items[0]))
|
|
return false;
|
|
|
|
// Can only send to another
|
|
// REGISTER, TRANSFER, or REVOKE.
|
|
switch (covenant.type) {
|
|
case types.REGISTER: {
|
|
// Transfers must wait 48 hours before updating.
|
|
if (height < entry.height + exports.REVOCATION_WINDOW)
|
|
return false;
|
|
|
|
// Address must match the one committed
|
|
// to in the original transfer covenant.
|
|
if (!output.address.toRaw().equals(uc.items[2]))
|
|
return false;
|
|
|
|
// Money is now locked up forever.
|
|
if (output.value !== coin.value)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case types.REVOKE: {
|
|
// Transfers must wait 48 hours before updating.
|
|
if (height >= entry.height + exports.REVOCATION_WINDOW)
|
|
return false;
|
|
|
|
// Must match the original owner's address.
|
|
if (!output.address.equals(coin.address))
|
|
return false;
|
|
|
|
const value = encoding.readU64(uc.items[3], 0);
|
|
|
|
// Must be equal to the name value.
|
|
if (output.value !== coin.value - value)
|
|
return false;
|
|
|
|
// Must refund the buyer.
|
|
if (i + 1 >= tx.outputs.length)
|
|
return false;
|
|
|
|
const refund = tx.outputs[i + 1];
|
|
|
|
if (refund.covenant.type !== types.NONE)
|
|
return false;
|
|
|
|
if (!refund.address.toRaw().equals(uc.items[2]))
|
|
return false;
|
|
|
|
if (refund.value !== value)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case types.REVOKE: {
|
|
// Revocations are perma-burned.
|
|
return false;
|
|
}
|
|
default: {
|
|
// Unknown covenant.
|
|
// Don't enforce anything.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
exports.blind = function blind(value, nonce) {
|
|
const bw = bio.write(40);
|
|
bw.writeU64(value);
|
|
bw.writeBytes(nonce);
|
|
return blake2b.digest(bw.render());
|
|
};
|