283 lines
18 KiB
JavaScript
283 lines
18 KiB
JavaScript
|
|
// Commit–reveal for first registrations.
|
|||
|
|
// (Decentralized.DNS/PROTOCOL-ADDENDUM-commit-reveal.md)
|
|||
|
|
//
|
|||
|
|
// A REG shows its name to the mempool; a commit made a block earlier is what
|
|||
|
|
// lets its author keep it. These tests build real CMT and REG transactions
|
|||
|
|
// with the library and run them through every JavaScript reducer, which must
|
|||
|
|
// agree.
|
|||
|
|
|
|||
|
|
import test from "node:test";
|
|||
|
|
import { writeFileSync } from "node:fs";
|
|||
|
|
import assert from "node:assert/strict";
|
|||
|
|
import * as L from "@bitauth/libauth";
|
|||
|
|
import { NETWORKS, buildIndex, buildIndexFromSnapshot, commitmentHex, onlyPayload } from "../src/lib/resolver-web.js";
|
|||
|
|
import * as legacy from "../src/lib/bns.js";
|
|||
|
|
import {
|
|||
|
|
buildCommitTx, buildRegistrationTx, buildUpdateTx, buildReleaseTx, commitmentHash, newRegistrationSalt, encodeCommitPayload, encodePayload, opReturnScript, REG_SALT_BYTES,
|
|||
|
|
} from "../src/lib/register-tx.js";
|
|||
|
|
import { addressToLockingBytecode } from "../src/lib/chain.js";
|
|||
|
|
import { lockToAddress } from "../src/lib/covenant.js";
|
|||
|
|
import { registryTerms, registryLock, registryCommitment, buildRegistryRegisterTx } from "../src/lib/registry-covenant.js";
|
|||
|
|
import { tierForName, BLOCKS_PER_YEAR } from "../src/lib/registry-lifecycle.js";
|
|||
|
|
|
|||
|
|
const { hexToBin, binToHex, hash160, hash256, secp256k1, encodeLockingBytecodeP2pkh, generatePrivateKey, encodeTransaction } = L;
|
|||
|
|
const NET = NETWORKS.chipnet;
|
|||
|
|
const RULES = NET.commitReveal;
|
|||
|
|
const STRICT = { ...NET, commitReveal: { ...RULES, required: true } };
|
|||
|
|
|
|||
|
|
const person = () => {
|
|||
|
|
const priv = generatePrivateKey(() => crypto.getRandomValues(new Uint8Array(32)));
|
|||
|
|
const pub = secp256k1.derivePublicKeyCompressed(priv);
|
|||
|
|
const lock = encodeLockingBytecodeP2pkh(hash160(pub));
|
|||
|
|
return { priv, pub, pkh: binToHex(hash160(pub)), lock, lockHex: binToHex(lock), address: lockToAddress(lock, NET.prefix, false) };
|
|||
|
|
};
|
|||
|
|
const alice = person(), bob = person(), mallory = person();
|
|||
|
|
let coinSeq = 1;
|
|||
|
|
const coin = (who, sats = 10_000_000n) => ({ txid: (coinSeq++).toString(16).padStart(64, "0"), vout: 0, satoshis: sats, lockingBytecode: who.lock });
|
|||
|
|
|
|||
|
|
// ---------- libauth transaction -> what an electrum server returns ----------
|
|||
|
|
const verbose = (transaction) => {
|
|||
|
|
const txid = binToHex(hash256(encodeTransaction(transaction)).slice().reverse());
|
|||
|
|
return {
|
|||
|
|
txid,
|
|||
|
|
vin: transaction.inputs.map((i) => ({ txid: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })),
|
|||
|
|
vout: transaction.outputs.map((o, n) => {
|
|||
|
|
let address;
|
|||
|
|
try { address = lockToAddress(o.lockingBytecode, NET.prefix, false); } catch { address = undefined; }
|
|||
|
|
return {
|
|||
|
|
n, value: Number(o.valueSatoshis) / 1e8,
|
|||
|
|
scriptPubKey: { hex: binToHex(o.lockingBytecode), ...(address ? { address } : {}) },
|
|||
|
|
...(o.token ? { tokenData: { category: binToHex(o.token.category), amount: String(o.token.amount ?? 0n), ...(o.token.nft ? { nft: { capability: o.token.nft.capability, commitment: binToHex(o.token.nft.commitment) } } : {}) } } : {}),
|
|||
|
|
};
|
|||
|
|
}),
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ---------- the two transactions ----------
|
|||
|
|
const commitFor = (who, name, salt = newRegistrationSalt(), lock = who.lock) => ({
|
|||
|
|
salt,
|
|||
|
|
tx: verbose(buildCommitTx({ commitment: commitmentHash(salt, name, lock), utxos: [coin(who)], changeAddress: who.address, addressToLockingBytecode }).transaction),
|
|||
|
|
});
|
|||
|
|
const reg = (who, name, { salt = null, records = { u: `https://${who === alice ? "alice" : who === bob ? "bob" : "mallory"}.example` } } = {}) =>
|
|||
|
|
verbose(buildRegistrationTx({ name, ownerAddress: who.address, records, utxos: [coin(who)], salt, addressToLockingBytecode }).transaction);
|
|||
|
|
const withPayload = (tx, payload) => {
|
|||
|
|
const t = structuredClone(tx);
|
|||
|
|
const at = t.vout.findIndex((v) => v.scriptPubKey.hex.startsWith("6a"));
|
|||
|
|
t.vout[at].scriptPubKey.hex = binToHex(opReturnScript(new TextEncoder().encode("BNS1" + JSON.stringify(payload))));
|
|||
|
|
return t;
|
|||
|
|
};
|
|||
|
|
const payloadOf = (tx) => onlyPayload(tx.vout);
|
|||
|
|
|
|||
|
|
// BNS_DUMP=<file>: write every reduction as a vector, so the Android reducer
|
|||
|
|
// can be run against the same histories on the desktop (see the memory note).
|
|||
|
|
const DUMPED = [];
|
|||
|
|
const dump = (history, txs, height, idx) => {
|
|||
|
|
if (!process.env.BNS_DUMP) return;
|
|||
|
|
DUMPED.push({ history, txs, height, expect: [...idx].map(([k, e]) => ({ name: k, category: e.category, txid: e.txid, records: e.records, ownerPkh: e.covenant?.ownerPkh ?? null, expiry: e.covenant?.expiry ?? 0, status: e.status ?? null })) });
|
|||
|
|
};
|
|||
|
|
process.on("exit", () => { if (process.env.BNS_DUMP && DUMPED.length) writeFileSync(process.env.BNS_DUMP, JSON.stringify({ vectors: DUMPED })); });
|
|||
|
|
|
|||
|
|
// ---------- reducers ----------
|
|||
|
|
async function reduce(entries, { network = NET } = {}) {
|
|||
|
|
const history = entries.map(([tx, h]) => ({ tx_hash: tx.txid, height: h }));
|
|||
|
|
const txs = Object.fromEntries(entries.map(([tx]) => [tx.txid, tx]));
|
|||
|
|
const snap = buildIndexFromSnapshot({ snapshot: { history, txs }, network });
|
|||
|
|
const el = { call: async (method, [a]) => (method === "blockchain.scripthash.get_history" ? history.map((x) => ({ ...x })) : method === "blockchain.headers.subscribe" ? { height: 0 } : txs[a]) };
|
|||
|
|
const live = await buildIndex({ client: el, cosignGate: false, network });
|
|||
|
|
const shape = (idx) => [...idx].map(([k, e]) => JSON.stringify([k, e.category, e.txid, e.records])).sort();
|
|||
|
|
assert.deepEqual(shape(live), shape(snap), "buildIndex and buildIndexFromSnapshot disagree");
|
|||
|
|
if (network === NET) {
|
|||
|
|
const old = await legacy.buildIndex({ getHistory: async () => history.map((x) => ({ ...x })), getRawTransactionObject: async (id) => txs[id] }, { cosignGate: false });
|
|||
|
|
assert.deepEqual(shape(old), shape(snap), "bns.js and resolver-web.js disagree");
|
|||
|
|
}
|
|||
|
|
if (network === NET) dump(history, txs, 0, snap);
|
|||
|
|
return snap;
|
|||
|
|
}
|
|||
|
|
const owner = (idx, name) => idx.get(name)?.owner ?? null;
|
|||
|
|
|
|||
|
|
const NAME = "wanted.x";
|
|||
|
|
|
|||
|
|
// ---------- the commitment ----------
|
|||
|
|
|
|||
|
|
test("the commitment is sha256(salt ‖ name ‖ certificate lock), the same in the builder and the resolver", () => {
|
|||
|
|
const salt = newRegistrationSalt();
|
|||
|
|
assert.equal(salt.length, REG_SALT_BYTES);
|
|||
|
|
assert.equal(binToHex(commitmentHash(salt, NAME, alice.lock)), commitmentHex(binToHex(salt), NAME, alice.lockHex));
|
|||
|
|
// bare labels are hashed as they appear on chain
|
|||
|
|
assert.equal(binToHex(commitmentHash(salt, "plain.bch", alice.lock)), commitmentHex(binToHex(salt), "plain", alice.lockHex));
|
|||
|
|
// each ingredient matters
|
|||
|
|
const base = commitmentHex(binToHex(salt), NAME, alice.lockHex);
|
|||
|
|
assert.notEqual(commitmentHex(binToHex(newRegistrationSalt()), NAME, alice.lockHex), base);
|
|||
|
|
assert.notEqual(commitmentHex(binToHex(salt), "wanted.y", alice.lockHex), base);
|
|||
|
|
assert.notEqual(commitmentHex(binToHex(salt), NAME, bob.lockHex), base);
|
|||
|
|
assert.throws(() => commitmentHash(new Uint8Array(15), NAME, alice.lock), /16 bytes/);
|
|||
|
|
// a commit says nothing but a hash, and is a message like any other
|
|||
|
|
assert.deepEqual(JSON.parse(new TextDecoder().decode(encodeCommitPayload(base)).slice(4)), { o: "CMT", h: base });
|
|||
|
|
const c = commitFor(alice, NAME);
|
|||
|
|
assert.equal(payloadOf(c.tx).o, "CMT");
|
|||
|
|
assert.equal(c.tx.vout.some((v) => v.tokenData), false, "a commit mints nothing");
|
|||
|
|
assert.ok(c.tx.vout.some((v) => v.scriptPubKey.address === NET.beacon), "and pays the beacon, to be found");
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---------- a reveal ----------
|
|||
|
|
|
|||
|
|
test("a reveal registers the name when its commit is a block deep", async () => {
|
|||
|
|
const c = commitFor(alice, NAME);
|
|||
|
|
const r = reg(alice, NAME, { salt: c.salt });
|
|||
|
|
assert.equal(payloadOf(r).s, binToHex(c.salt));
|
|||
|
|
const idx = await reduce([[c.tx, 100], [r, 101]]);
|
|||
|
|
assert.equal(owner(idx, NAME), alice.address);
|
|||
|
|
assert.deepEqual(idx.get(NAME).records, { u: "https://alice.example" });
|
|||
|
|
// the edges of the window
|
|||
|
|
assert.ok((await reduce([[c.tx, 100], [r, 100 + RULES.maxAge]])).has(NAME), "the oldest a commit may be");
|
|||
|
|
// not in a block yet: accepted for now, judged again when it confirms
|
|||
|
|
assert.ok((await reduce([[c.tx, 100], [r, 0]])).has(NAME));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("a reveal without a usable commit registers nothing", async () => {
|
|||
|
|
const c = commitFor(alice, NAME);
|
|||
|
|
const r = reg(alice, NAME, { salt: c.salt });
|
|||
|
|
const free = async (entries, why) => assert.equal((await reduce(entries)).has(NAME), false, why);
|
|||
|
|
|
|||
|
|
await free([[r, 101]], "no commit at all");
|
|||
|
|
await free([[c.tx, 101], [r, 101]], "commit in the same block");
|
|||
|
|
await free([[c.tx, 102], [r, 101]], "commit after the reveal");
|
|||
|
|
await free([[c.tx, 100], [r, 100 + RULES.maxAge + 1]], "commit too old");
|
|||
|
|
await free([[c.tx, 0], [r, 0]], "commit not in a block yet");
|
|||
|
|
await free([[c.tx, 100], [reg(alice, NAME, { salt: newRegistrationSalt() }), 101]], "another salt");
|
|||
|
|
await free([[c.tx, 100], [reg(mallory, NAME, { salt: c.salt }), 101]], "revealed to another owner: the commit binds the address");
|
|||
|
|
await free([[c.tx, 100], [reg(alice, "wanted.y", { salt: c.salt }), 101]], "another name");
|
|||
|
|
const p = payloadOf(r);
|
|||
|
|
for (const s of [p.s.slice(2), p.s.toUpperCase(), p.s + "00", null, 5, ""]) {
|
|||
|
|
await free([[c.tx, 100], [withPayload(r, { ...p, s }), 101]], `malformed salt ${JSON.stringify(s)}`);
|
|||
|
|
}
|
|||
|
|
// a commit and a registration in one transaction is two messages, so none
|
|||
|
|
const both = structuredClone(r);
|
|||
|
|
both.vout.push({ n: both.vout.length, value: 0, scriptPubKey: { hex: binToHex(opReturnScript(encodeCommitPayload("ab".repeat(32)))) } });
|
|||
|
|
await free([[c.tx, 100], [both, 101]], "CMT and REG in one transaction");
|
|||
|
|
// the same commit published again later does not make it younger
|
|||
|
|
const again = verbose(buildCommitTx({ commitment: commitmentHash(c.salt, NAME, alice.lock), utxos: [coin(mallory)], changeAddress: mallory.address, addressToLockingBytecode }).transaction);
|
|||
|
|
await free([[c.tx, 100], [again, 400], [r, 401]], "the earliest commit of a hash is the one that counts");
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---------- who wins ----------
|
|||
|
|
|
|||
|
|
test("the point of it: the earlier commit wins even when its reveal is mined later", async () => {
|
|||
|
|
const a = commitFor(alice, NAME), b = commitFor(bob, NAME);
|
|||
|
|
const ra = reg(alice, NAME, { salt: a.salt }), rb = reg(bob, NAME, { salt: b.salt });
|
|||
|
|
// Alice committed first. Bob's reveal is mined first.
|
|||
|
|
let idx = await reduce([[a.tx, 100], [b.tx, 101], [rb, 105], [ra, 105 + RULES.contestBlocks]]);
|
|||
|
|
assert.equal(owner(idx, NAME), alice.address, "inside the contest window the older commit takes the name");
|
|||
|
|
assert.equal(idx.get(NAME).txid, ra.txid);
|
|||
|
|
// …and in the same block, whatever the txids say
|
|||
|
|
idx = await reduce([[a.tx, 100], [b.tx, 101], [rb, 105], [ra, 105]]);
|
|||
|
|
assert.equal(owner(idx, NAME), alice.address);
|
|||
|
|
// Until Alice's reveal exists, the name is Bob's.
|
|||
|
|
assert.equal(owner(await reduce([[a.tx, 100], [b.tx, 101], [rb, 105]]), NAME), bob.address);
|
|||
|
|
// Commits in the same block: the lower commit txid.
|
|||
|
|
const first = a.tx.txid < b.tx.txid ? alice : bob;
|
|||
|
|
assert.equal(owner(await reduce([[a.tx, 100], [b.tx, 100], [rb, 105], [ra, 106]]), NAME), first.address);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("after the contest window a registration is final", async () => {
|
|||
|
|
const a = commitFor(alice, NAME), b = commitFor(bob, NAME);
|
|||
|
|
const ra = reg(alice, NAME, { salt: a.salt }), rb = reg(bob, NAME, { salt: b.salt });
|
|||
|
|
const idx = await reduce([[a.tx, 100], [b.tx, 101], [rb, 105], [ra, 105 + RULES.contestBlocks + 1]]);
|
|||
|
|
assert.equal(owner(idx, NAME), bob.address, "an older commit revealed too late does not take a name back");
|
|||
|
|
// A standing commit, kept for the day someone wants the name, is worth nothing once they have it.
|
|||
|
|
const hoard = commitFor(mallory, NAME);
|
|||
|
|
const late = reg(mallory, NAME, { salt: hoard.salt });
|
|||
|
|
assert.equal(owner(await reduce([[hoard.tx, 50], [b.tx, 101], [rb, 105], [late, 105 + RULES.contestBlocks + 1]]), NAME), bob.address);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("a front-runner who copies the name out of the mempool loses", async () => {
|
|||
|
|
const a = commitFor(alice, NAME);
|
|||
|
|
const ra = reg(alice, NAME, { salt: a.salt });
|
|||
|
|
const copy = reg(mallory, NAME); // no commit: Mallory learned the name from Alice's reveal
|
|||
|
|
// same block as the reveal, a block before it (a miner who held Alice's back), or after
|
|||
|
|
for (const [hCopy, hReveal] of [[105, 105], [105, 106], [105, 105 + RULES.contestBlocks], [106, 105]]) {
|
|||
|
|
const idx = await reduce([[a.tx, 100], [copy, hCopy], [ra, hReveal]]);
|
|||
|
|
assert.equal(owner(idx, NAME), alice.address, `copy at ${hCopy}, reveal at ${hReveal}`);
|
|||
|
|
}
|
|||
|
|
// Mallory commits the moment she sees the reveal: her commit is younger, and too young.
|
|||
|
|
const m = commitFor(mallory, NAME);
|
|||
|
|
const rm = reg(mallory, NAME, { salt: m.salt });
|
|||
|
|
assert.equal(owner(await reduce([[a.tx, 100], [m.tx, 105], [ra, 105], [rm, 106]]), NAME), alice.address);
|
|||
|
|
assert.equal(owner(await reduce([[a.tx, 100], [m.tx, 105], [rm, 106], [ra, 107]]), NAME), alice.address, "even with Alice's reveal held back two blocks");
|
|||
|
|
// What the window does not cover: a reveal censored for longer than the window.
|
|||
|
|
assert.equal(owner(await reduce([[a.tx, 100], [m.tx, 105], [rm, 106], [ra, 106 + RULES.contestBlocks + 1]]), NAME), mallory.address);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("the loser of a contest holds nothing: its updates are ignored", async () => {
|
|||
|
|
const a = commitFor(alice, NAME);
|
|||
|
|
const ra = reg(alice, NAME, { salt: a.salt });
|
|||
|
|
const copy = reg(mallory, NAME);
|
|||
|
|
const cat = copy.vout.find((v) => v.tokenData).tokenData.category;
|
|||
|
|
const cert = { txid: copy.txid, vout: copy.vout.findIndex((v) => v.tokenData), satoshis: 1000n, lockingBytecode: mallory.lock, token: { category: cat, amount: 0n, nft: { capability: "none", commitment: binToHex(new TextEncoder().encode(NAME)) } } };
|
|||
|
|
const upd = verbose(buildUpdateTx({ name: NAME, records: { u: "https://phishing.example" }, certificateUtxo: cert, utxos: [coin(mallory)], ownerAddress: mallory.address, addressToLockingBytecode }).transaction);
|
|||
|
|
const idx = await reduce([[a.tx, 100], [copy, 105], [upd, 106], [ra, 107]]);
|
|||
|
|
assert.equal(owner(idx, NAME), alice.address);
|
|||
|
|
assert.deepEqual(idx.get(NAME).records, { u: "https://alice.example" });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("registrations without a commit behave exactly as before", async () => {
|
|||
|
|
const first = reg(alice, NAME), second = reg(bob, NAME);
|
|||
|
|
assert.equal(payloadOf(first).s, undefined);
|
|||
|
|
assert.equal(owner(await reduce([[first, 100], [second, 101]]), NAME), alice.address);
|
|||
|
|
assert.equal(owner(await reduce([[second, 101], [first, 100]]), NAME), alice.address);
|
|||
|
|
const low = first.txid < second.txid ? alice : bob;
|
|||
|
|
assert.equal(owner(await reduce([[first, 100], [second, 100]]), NAME), low.address, "same block: the lower txid, as always");
|
|||
|
|
assert.equal(owner(await reduce([[first, 0], [second, 100]]), NAME), bob.address, "confirmed beats unconfirmed");
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("one commit backs one registration", async () => {
|
|||
|
|
const a = commitFor(alice, NAME);
|
|||
|
|
const ra = reg(alice, NAME, { salt: a.salt });
|
|||
|
|
const cat = ra.vout.find((v) => v.tokenData).tokenData.category;
|
|||
|
|
const cert = { txid: ra.txid, vout: ra.vout.findIndex((v) => v.tokenData), satoshis: 1000n, lockingBytecode: alice.lock, token: { category: cat, amount: 0n, nft: { capability: "none", commitment: binToHex(new TextEncoder().encode(NAME)) } } };
|
|||
|
|
const rel = verbose(buildReleaseTx({ name: NAME, certificateUtxo: cert, utxos: [coin(alice)], ownerAddress: alice.address, addressToLockingBytecode }).transaction);
|
|||
|
|
const again = reg(alice, NAME, { salt: a.salt });
|
|||
|
|
const idx = await reduce([[a.tx, 100], [ra, 101], [rel, 102], [again, 104]]);
|
|||
|
|
assert.equal(idx.has(NAME), false, "the commit was used by the first registration");
|
|||
|
|
// a fresh commit registers it again
|
|||
|
|
const b = commitFor(alice, NAME);
|
|||
|
|
assert.ok((await reduce([[a.tx, 100], [ra, 101], [rel, 102], [b.tx, 103], [reg(alice, NAME, { salt: b.salt }), 104]])).has(NAME));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---------- where it is mandatory ----------
|
|||
|
|
|
|||
|
|
test("where commit–reveal is required, a registration without a salt is void", async () => {
|
|||
|
|
assert.equal(NETWORKS.mainnet.commitReveal.required, true, "mainnet starts with commit–reveal from block one");
|
|||
|
|
assert.equal(RULES.required, false, "chipnet keeps accepting plain registrations");
|
|||
|
|
const plain = reg(mallory, NAME);
|
|||
|
|
assert.equal((await reduce([[plain, 100]], { network: STRICT })).has(NAME), false);
|
|||
|
|
const a = commitFor(alice, NAME);
|
|||
|
|
const ra = reg(alice, NAME, { salt: a.salt });
|
|||
|
|
const idx = await reduce([[plain, 99], [a.tx, 100], [ra, 101]], { network: STRICT });
|
|||
|
|
assert.equal(owner(idx, NAME), alice.address);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---------- straight into the covenant ----------
|
|||
|
|
|
|||
|
|
test("a covenant registration commits to the covenant address — owner and expiry are fixed at commit time", async () => {
|
|||
|
|
const name = "vaulted.x";
|
|||
|
|
const COV = NET.covenant;
|
|||
|
|
const terms = registryTerms({ tier: tierForName(name), fundLock: hexToBin(COV.fundLock), stewardPk: hexToBin(COV.steward) });
|
|||
|
|
const keyFor = (lockHex) => (lockHex === alice.lockHex ? { privateKey: alice.priv, publicKey: alice.pub } : null);
|
|||
|
|
const beaconLock = addressToLockingBytecode(NET.beacon);
|
|||
|
|
const state = { ownerPkh: alice.pkh, expiry: 100 + BLOCKS_PER_YEAR }; // counted from the tip when committing
|
|||
|
|
const salt = newRegistrationSalt();
|
|||
|
|
const commitment = registryCommitment({ name, terms, state, salt });
|
|||
|
|
assert.equal(binToHex(commitment), commitmentHex(binToHex(salt), name, binToHex(registryLock(terms, state))));
|
|||
|
|
const cmt = verbose(buildCommitTx({ commitment, utxos: [coin(alice)], changeAddress: alice.address, addressToLockingBytecode }).transaction);
|
|||
|
|
const make = (st) => verbose(buildRegistryRegisterTx({ genesis: coin(alice), terms, name, records: { u: "https://vault.example" }, state: st, salt, beaconLock, coins: [coin(alice)], changeLock: alice.lock, keyFor }).transaction);
|
|||
|
|
|
|||
|
|
const e = (await reduce([[cmt, 100], [make(state), 103]], { network: STRICT })).get(name);
|
|||
|
|
assert.equal(e.owner, alice.address);
|
|||
|
|
assert.deepEqual(e.covenant, { ownerPkh: alice.pkh, expiry: state.expiry, tier: tierForName(name), version: 1 });
|
|||
|
|
assert.equal(e.status, "active");
|
|||
|
|
// the same salt cannot be revealed into a different state: it is another address
|
|||
|
|
assert.equal((await reduce([[cmt, 100], [make({ ...state, expiry: state.expiry + 1 }), 103]], { network: STRICT })).has(name), false);
|
|||
|
|
assert.equal((await reduce([[cmt, 100], [make({ ...state, ownerPkh: mallory.pkh }), 103]], { network: STRICT })).has(name), false);
|
|||
|
|
});
|