argus/test/covenant-names.test.mjs

394 lines
23 KiB
JavaScript
Raw Normal View History

// Resolvers reading covenant-held names.
// (Decentralized.DNS/PROTOCOL-ADDENDUM-covenant-names.md)
//
// A covenant-held name's owner and expiry are hashed into its address. The
// transaction says what they are; the resolver checks the claim against the
// address, decides whether an entry was paid for, and stops resolving a name
// whose grace period has passed.
//
// Every transaction here is built by the real builders and, where it spends
// the covenant, accepted by the VM first — then turned into the verbose form
// an electrum server returns and fed to all three JavaScript reducers, which
// must agree.
import test from "node:test";
import { writeFileSync } from "node:fs";
import assert from "node:assert/strict";
import * as L from "@bitauth/libauth";
import {
NETWORKS, buildIndex, buildIndexFromSnapshot, covenantLockHex, covenantTierIndex, covenantClaim, covenantPhase,
pkhToAddress, addressToLockHex, historyHeight,
} from "../src/lib/resolver-web.js";
import * as legacy from "../src/lib/bns.js";
import { covenantTable } from "../src/generate-covenant-table.mjs";
import {
registryTerms, registryLock, commitBoxLock, commitHashFor, covenantClaimOf, registryAnnouncement,
buildRegistryEnterTx, buildRegistryRenewTx, buildRegistryTransferTx, buildRegistryTakeoverTx, buildRegistryReleaseTx,
} from "../src/lib/registry-covenant.js";
import { TIERS, tierForName, BLOCKS_PER_YEAR, GRACE_BLOCKS } from "../src/lib/registry-lifecycle.js";
import { lockToAddress } from "../src/lib/covenant.js";
import { encodePayload, opReturnScript } from "../src/lib/register-tx.js";
const { hexToBin, binToHex, hash160, secp256k1, encodeLockingBytecodeP2pkh, generatePrivateKey } = L;
const NET = NETWORKS.chipnet;
const COV = NET.covenant;
const VM = L.createVirtualMachineBch2025();
// ---------- cast ----------
const person = () => {
const priv = generatePrivateKey(() => crypto.getRandomValues(new Uint8Array(32)));
const pub = secp256k1.derivePublicKeyCompressed(priv);
const pkh = hash160(pub);
const lock = encodeLockingBytecodeP2pkh(pkh);
return { priv, pub, pkh: binToHex(pkh), lock, lockHex: binToHex(lock), address: lockToAddress(lock, NET.prefix, false) };
};
const owner = person(), buyer = person(), taker = person(), stranger = person();
const keyring = (...people) => (lockHex) => { const p = people.find((x) => x.lockHex === lockHex); return p ? { privateKey: p.priv, publicKey: p.pub } : null; };
const KEYS = keyring(owner, buyer, taker, stranger);
const fundLock = hexToBin(COV.fundLock);
const beaconLock = hexToBin(addressToLockHex(NET.beacon));
const termsFor = (name) => registryTerms({ tier: tierForName(name), fundLock, stewardPk: hexToBin(COV.steward) });
let coinSeq = 100;
const coin = (who, sats = 50_000_000n) => ({ txid: (coinSeq++).toString(16).padStart(64, "0"), vout: 0, satoshis: sats, lockingBytecode: who.lock });
// ---------- libauth transaction -> what an electrum server returns ----------
const verbose = (built) => ({
txid: built.txid,
vin: built.transaction.inputs.map((i) => ({ txid: binToHex(i.outpointTransactionHash), vout: i.outpointIndex })),
vout: built.transaction.outputs.map((o, n) => {
const hex = binToHex(o.lockingBytecode);
let address;
try { address = lockToAddress(o.lockingBytecode, NET.prefix, false); } catch { address = undefined; }
return {
n, value: Number(o.valueSatoshis) / 1e8,
scriptPubKey: { hex, ...(address ? { address } : {}) },
...(o.token ? { tokenData: { category: binToHex(o.token.category), amount: String(o.token.amount ?? 0n), ...(o.token.nft ? { nft: { capability: o.token.nft.capability, commitment: binToHex(o.token.nft.commitment) } } : {}) } } : {}),
};
}),
});
const vmAccepts = (built, what) => assert.equal(VM.verify({ transaction: built.transaction, sourceOutputs: built.sourceOutputs }), true, `${what}: the VM must accept it`);
const nextCov = (built, token) => ({ txid: built.txid, vout: 0, satoshis: built.transaction.outputs[0].valueSatoshis, lockingBytecode: built.transaction.outputs[0].lockingBytecode, token });
// A plain registration, hand-made (no contract involved).
let catSeq = 1;
function registration(name, who, records = { u: "https://first.example" }) {
const category = (catSeq++).toString(16).padStart(2, "0").repeat(32);
const token = { category, amount: 0n, nft: { capability: "none", commitment: binToHex(new TextEncoder().encode(name)) } };
const txid = "f" + (catSeq).toString(16).padStart(63, "0");
const tx = {
txid, vin: [{ txid: category, vout: 0 }],
vout: [
{ n: 0, value: 0.00001, scriptPubKey: { hex: who.lockHex, address: who.address }, tokenData: { category, amount: "0", nft: { capability: "none", commitment: token.nft.commitment } } },
{ n: 1, value: 0, scriptPubKey: { hex: binToHex(opReturnScript(encodePayload("REG", name, records))) } },
{ n: 2, value: 0.000006, scriptPubKey: { hex: binToHex(beaconLock), address: NET.beacon } },
],
};
return { tx, cert: { txid, vout: 0, satoshis: 1000n, lockingBytecode: who.lock, token } };
}
const announce = (name, records) => ({ name, records, beaconLock });
// 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 })); });
// ---------- the three reducers, which must agree ----------
async function reduce(entries, height) {
const history = entries.map(([tx, h]) => ({ tx_hash: tx.txid, height: h }));
const txs = Object.fromEntries(entries.map(([tx]) => [tx.txid, tx]));
const snap = buildIndexFromSnapshot({ snapshot: { history, txs }, height });
const el = {
call: async (method, [a]) => method === "blockchain.scripthash.get_history" ? history.map((x) => ({ ...x }))
: method === "blockchain.headers.subscribe" ? { height }
: txs[a],
};
const live = await buildIndex({ client: el, cosignGate: false });
const old = await legacy.buildIndex({ getHistory: async () => history.map((x) => ({ ...x })), getRawTransactionObject: async (id) => txs[id] }, { cosignGate: false, height });
const shape = (idx) => [...idx].map(([k, e]) => JSON.stringify([k, e.records, e.covenant ?? null, e.status ?? null, e.expiry ?? null, e.lapsedRecords ?? null])).sort();
assert.deepEqual(shape(live), shape(snap), "buildIndex and buildIndexFromSnapshot disagree");
assert.deepEqual(shape(old), shape(snap), "bns.js and resolver-web.js disagree");
assert.equal(snap.height, height);
dump(history, txs, height, snap);
return snap;
}
// ---------- the table ----------
test("the network table holds the contract's code, byte for byte", () => {
assert.deepEqual(covenantTable({ fundLock, stewardPk: hexToBin(COV.steward) }), COV, "regenerate: node src/generate-covenant-table.mjs");
for (const t of TIERS) {
const terms = registryTerms({ tier: t.tier, fundLock, stewardPk: hexToBin(COV.steward) });
const state = { ownerPkh: owner.pkh, expiry: 123456 + t.tier };
assert.equal(covenantLockHex(COV, t.tier - 1, state.ownerPkh, state.expiry), binToHex(registryLock(terms, state)), `tier ${t.tier}`);
}
for (const name of ["a.x", "ab.x", "abc.x", "abcd.x", "abcde.x", "abcdefg.x", "abcdefgh.x", "a-much-longer-label.test", "bare"]) {
assert.equal(covenantTierIndex(name, COV) + 1, tierForName(name), name);
}
assert.equal(NETWORKS.mainnet.covenant, null, "mainnet is not pinned");
});
test("pkhToAddress is the inverse of addressToLockHex", () => {
for (const p of [owner, buyer, taker]) {
assert.equal(pkhToAddress(p.pkh, NET.prefix), p.address);
assert.equal(addressToLockHex(pkhToAddress(p.pkh, NET.prefix)), p.lockHex);
}
assert.equal(pkhToAddress(owner.pkh, "bitcoincash"), lockToAddress(owner.lock, "bitcoincash", false));
});
// ---------- entering ----------
const NAME = "holdme.x";
const H_REG = 1000, TIP = 1999, H_ENTER = 2000;
function entered({ name = NAME, who = owner, years = 1, height = TIP, records = { u: "https://held.example" } } = {}) {
const reg = registration(name, who);
const terms = termsFor(name);
const enter = buildRegistryEnterTx({ cert: reg.cert, terms, name, records, ownerPkh: who.pkh, height, years, beaconLock, coins: [coin(who)], changeLock: who.lock, keyFor: KEYS });
return { reg, terms, enter, cov: nextCov(enter, reg.cert.token), state: enter.state };
}
test("a paid entry makes the name covenant-held: owner and expiry come from the verified claim", async () => {
const { reg, enter, state } = entered();
const idx = await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER]], H_ENTER);
const e = idx.get(NAME);
assert.deepEqual(e.covenant, { ownerPkh: owner.pkh, expiry: TIP + BLOCKS_PER_YEAR, tier: 3, version: 1 });
assert.equal(e.owner, owner.address, "the owner is the key in the state, not the covenant's address");
assert.deepEqual(e.records, { u: "https://held.example" });
assert.equal(e.status, "active");
assert.equal(e.expiry, state.expiry);
assert.equal(e.idleEnd, state.expiry + GRACE_BLOCKS);
});
test("the lifecycle: resolves while active and idle, not once open — and stays taken", async () => {
const { reg, enter, state } = entered();
const at = async (h) => (await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER]], h)).get(NAME);
assert.equal((await at(state.expiry - 1)).status, "active");
const idle = await at(state.expiry);
assert.equal(idle.status, "idle");
assert.deepEqual(idle.records, { u: "https://held.example" }, "the owner keeps the records through the grace period");
assert.equal((await at(state.expiry + GRACE_BLOCKS - 1)).status, "idle");
const open = await at(state.expiry + GRACE_BLOCKS);
assert.equal(open.status, "open");
assert.deepEqual(open.records, {}, "a lapsed name resolves to nothing");
assert.deepEqual(open.lapsedRecords, { u: "https://held.example" });
// Its certificate still exists: a fresh registration of the same name is refused.
const again = registration(NAME, stranger, { u: "https://squatter.example" });
const idx = await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER], [again.tx, state.expiry + GRACE_BLOCKS + 5]], state.expiry + GRACE_BLOCKS + 10);
assert.equal(idx.get(NAME).category, reg.cert.token.category);
assert.deepEqual(idx.get(NAME).records, {});
assert.deepEqual(covenantPhase(open.covenant, state.expiry + GRACE_BLOCKS), { status: "open", expiry: state.expiry, idleEnd: state.expiry + GRACE_BLOCKS });
assert.equal(covenantPhase(null, 5), null);
});
test("an entry that was not paid for leaves an ordinary name", async () => {
const plain = async (enter, height, why) => {
const idx = await reduce([[enter.reg.tx, H_REG], [enter.tx, height]], Math.max(height, H_ENTER));
const e = idx.get(enter.name ?? NAME);
assert.equal(e.covenant, undefined, why);
assert.equal(e.status, undefined, why);
assert.deepEqual(e.records, { u: "https://held.example" }, `${why}: it is still a valid update of an ordinary name`);
return e;
};
const base = entered();
const fundIndex = verbose(base.enter).vout.findIndex((v) => v.scriptPubKey.hex === COV.fundLock && !v.tokenData);
assert.equal(fundIndex, 1);
// one satoshi short
const short = verbose(base.enter);
short.vout[1].value = (COV.tiers[2].renewalSats - 1) / 1e8;
await plain({ reg: base.reg, tx: short }, H_ENTER, "one satoshi short of a year");
// paid to somebody else
const elsewhere = verbose(base.enter);
elsewhere.vout[1].scriptPubKey = { hex: stranger.lockHex, address: stranger.address };
await plain({ reg: base.reg, tx: elsewhere }, H_ENTER, "paid to another address");
// a token output to the fund is not a payment
const tokenPay = verbose(base.enter);
tokenPay.vout[1].tokenData = { category: "ee".repeat(32), amount: "5" };
await plain({ reg: base.reg, tx: tokenPay }, H_ENTER, "the fund output carries a token");
// one year paid, two claimed
const far = entered({ height: TIP + BLOCKS_PER_YEAR });
await plain({ reg: far.reg, tx: verbose(far.enter) }, H_ENTER, "expiry further out than was paid for");
// not in a block yet
const e = await plain({ reg: base.reg, tx: verbose(base.enter) }, 0, "unconfirmed");
assert.notEqual(e.owner, owner.address, "until it confirms, the holder is just the address the certificate sits at");
// two years paid, two claimed: fine
const two = entered({ years: 2 });
const idx = await reduce([[two.reg.tx, H_REG], [verbose(two.enter), H_ENTER]], H_ENTER);
assert.equal(idx.get(NAME).covenant.expiry, TIP + 2 * BLOCKS_PER_YEAR);
});
test("a claim that does not hash to the address is not a claim", async () => {
const base = entered();
const holder = verbose(base.enter).vout[0];
const good = covenantClaimOf(base.state);
assert.ok(covenantClaim({ c: good }, holder, NAME));
const bad = [
{ ...good, o: buyer.pkh }, { ...good, e: good.e + 1 }, { ...good, e: String(good.e) }, { ...good, o: good.o.toUpperCase() },
{ o: good.o }, { e: good.e }, null, "x", { ...good, e: 500000000 }, { ...good, e: 0 }, { ...good, e: good.e + 0.5 },
];
for (const c of bad) assert.equal(covenantClaim({ c }, holder, NAME), null, JSON.stringify(c));
// the right state under another tier's prices is another address
assert.equal(covenantClaim({ c: good }, holder, "ab.x"), null);
assert.equal(covenantClaim({ c: good }, holder, "a-long-label.x"), null);
// a network with no covenant pinned knows no covenant-held names
assert.equal(covenantClaim({ c: good }, holder, NAME, NETWORKS.mainnet), null);
// In the index: a short name entered at the cheap tier's address stays ordinary.
const reg = registration("ab.x", owner);
const cheap = registryTerms({ tier: 4, fundLock, stewardPk: hexToBin(COV.steward) });
const enter = buildRegistryEnterTx({ cert: reg.cert, terms: cheap, name: "ab.x", records: { u: "https://held.example" }, ownerPkh: owner.pkh, height: TIP, beaconLock, coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS });
const idx = await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER]], H_ENTER);
assert.equal(idx.get("ab.x").covenant, undefined);
});
// ---------- inside ----------
test("inside the covenant: updates, transfers and renewals are read from announced states", async () => {
const { reg, terms, enter, cov, state } = entered();
const chain = [[reg.tx, H_REG], [verbose(enter), H_ENTER]];
// records update: a transfer to the same key
const upd = buildRegistryTransferTx({ cov, terms, state, newOwnerPkh: owner.pkh, announce: announce(NAME, { u: "https://second.example" }), coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS });
vmAccepts(upd, "records update");
chain.push([verbose(upd), 2100]);
let e = (await reduce(chain, 2100)).get(NAME);
assert.deepEqual(e.records, { u: "https://second.example" });
assert.equal(e.owner, owner.address);
// transfer to the buyer
const xfer = buildRegistryTransferTx({ cov: nextCov(upd, cov.token), terms, state: upd.state, newOwnerPkh: buyer.pkh, announce: announce(NAME, { u: "https://buyer.example" }), coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS });
vmAccepts(xfer, "transfer");
chain.push([verbose(xfer), 2200]);
e = (await reduce(chain, 2200)).get(NAME);
assert.equal(e.owner, buyer.address);
assert.equal(e.covenant.expiry, state.expiry, "a transfer does not move the expiry");
// renewal by the buyer
const renew = buildRegistryRenewTx({ cov: nextCov(xfer, cov.token), terms, state: xfer.state, years: 2, announce: announce(NAME, { u: "https://buyer.example" }), coins: [coin(buyer)], changeLock: buyer.lock, keyFor: KEYS });
vmAccepts(renew, "renew");
chain.push([verbose(renew), 2300]);
e = (await reduce(chain, state.expiry + GRACE_BLOCKS + 1)).get(NAME);
assert.equal(e.covenant.expiry, state.expiry + 2 * BLOCKS_PER_YEAR);
assert.equal(e.status, "active", "the renewal was announced, so the name did not lapse");
// an unannounced renewal is valid on chain and invisible here: the name lapses in the index
const quiet = (await reduce(chain.slice(0, -1), state.expiry + GRACE_BLOCKS + 1)).get(NAME);
assert.equal(quiet.status, "open");
assert.deepEqual(quiet.records, {});
});
test("once inside, a transaction without a verified state changes nothing", async () => {
const { reg, terms, enter, cov, state } = entered();
const base = [[reg.tx, H_REG], [verbose(enter), H_ENTER]];
const real = buildRegistryTransferTx({ cov, terms, state, newOwnerPkh: owner.pkh, announce: announce(NAME, { u: "https://hijack.example" }), coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS });
const payloadAt = verbose(real).vout.findIndex((v) => v.scriptPubKey.hex.startsWith("6a"));
const withPayload = (payload) => { const t = verbose(real); t.vout[payloadAt].scriptPubKey.hex = binToHex(opReturnScript(payload)); return t; };
const cases = [
["no claim at all — the way back to a name that never expires", encodePayload("UPD", NAME, { u: "https://hijack.example" })],
["a claim for another owner", encodePayload("UPD", NAME, { u: "https://hijack.example" }, { o: stranger.pkh, e: state.expiry })],
["a claim for a later expiry", encodePayload("UPD", NAME, { u: "https://hijack.example" }, { o: owner.pkh, e: state.expiry + BLOCKS_PER_YEAR })],
];
for (const [what, payload] of cases) {
const e = (await reduce([...base, [withPayload(payload), 2100]], 2100)).get(NAME);
assert.deepEqual(e.records, { u: "https://held.example" }, what);
assert.equal(e.covenant.expiry, state.expiry, what);
assert.equal(e.owner, owner.address, what);
}
// and the honest announcement of the same transaction is accepted
assert.deepEqual((await reduce([...base, [verbose(real), 2100]], 2100)).get(NAME).records, { u: "https://hijack.example" });
});
// ---------- leaving ----------
test("a takeover brings a lapsed name back, for its new owner", async () => {
const { reg, terms, enter, cov, state } = entered();
const openAt = state.expiry + GRACE_BLOCKS + 100;
const salt = crypto.getRandomValues(new Uint8Array(32));
const box = { txid: "b0".repeat(32), vout: 0, satoshis: 1000n, lockingBytecode: commitBoxLock({ ownerPkh: taker.pkh, commitHash: commitHashFor(NAME, salt) }) };
const take = buildRegistryTakeoverTx({ cov, terms, state, name: NAME, height: openAt, newOwnerPkh: taker.pkh, salt, box, announce: announce(NAME, { u: "https://taker.example" }), coins: [coin(taker)], changeLock: taker.lock, keyFor: KEYS });
vmAccepts(take, "takeover");
const before = (await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER]], openAt)).get(NAME);
assert.deepEqual(before.records, {});
const after = (await reduce([[reg.tx, H_REG], [verbose(enter), H_ENTER], [verbose(take), openAt + 1]], openAt + 1)).get(NAME);
assert.equal(after.owner, taker.address);
assert.deepEqual(after.records, { u: "https://taker.example" });
assert.equal(after.status, "active");
assert.equal(after.covenant.expiry, openAt + BLOCKS_PER_YEAR);
assert.equal(after.category, reg.cert.token.category, "the same certificate: a takeover is not a new registration");
});
test("a release from the covenant frees the name like any other", async () => {
const { reg, terms, enter, cov, state } = entered();
const rel = buildRegistryReleaseTx({ cov, terms, state, announce: announce(NAME), coins: [coin(owner)], changeLock: owner.lock, keyFor: KEYS });
vmAccepts(rel, "release");
const chain = [[reg.tx, H_REG], [verbose(enter), H_ENTER], [verbose(rel), 2100]];
const idx = await reduce(chain, 2100);
assert.equal(idx.has(NAME), false);
assert.equal(idx.released.get(NAME), 2100);
const again = registration(NAME, stranger, { u: "https://next.example" });
assert.equal((await reduce([...chain, [again.tx, 2100]], 2101)).has(NAME), false, "not in the release's own block");
assert.deepEqual((await reduce([...chain, [again.tx, 2101]], 2101)).get(NAME).records, { u: "https://next.example" });
});
// ---------- registering straight in ----------
test("a registration can go straight into the covenant, on the same terms as an entry", async () => {
const name = "straight.x";
const terms = termsFor(name);
const state = { ownerPkh: owner.pkh, expiry: TIP + BLOCKS_PER_YEAR };
const make = (paid) => {
const r = registration(name, owner, { u: "https://straight.example" });
r.tx.vout[0].scriptPubKey = { hex: binToHex(registryLock(terms, state)) };
r.tx.vout[1].scriptPubKey.hex = binToHex(registryAnnouncement({ name, records: { u: "https://straight.example" }, beaconLock }, state, "REG")[0].lockingBytecode);
if (paid) r.tx.vout.push({ n: 3, value: COV.tiers[covenantTierIndex(name, COV)].renewalSats / 1e8, scriptPubKey: { hex: COV.fundLock } });
return r.tx;
};
const held = (await reduce([[make(true), H_ENTER]], H_ENTER)).get(name);
assert.equal(held.covenant.expiry, state.expiry);
assert.equal(held.owner, owner.address);
const unpaid = (await reduce([[make(false), H_ENTER]], H_ENTER)).get(name);
assert.equal(unpaid.covenant, undefined, "registered, but an ordinary name");
assert.deepEqual(unpaid.records, { u: "https://straight.example" });
});
// ---------- which height ----------
test("expiry is judged at the tip when known, else at the newest block in the history — never ahead of the chain", async () => {
const { reg, enter, state } = entered();
const history = [{ tx_hash: reg.tx.txid, height: H_REG }, { tx_hash: enter.txid, height: H_ENTER }];
const txs = { [reg.tx.txid]: reg.tx, [enter.txid]: verbose(enter) };
assert.equal(historyHeight(history), H_ENTER);
assert.equal(historyHeight([{ height: 0 }, { height: -1 }]), 0);
const noTip = buildIndexFromSnapshot({ snapshot: { history, txs } });
assert.equal(noTip.height, H_ENTER);
assert.equal(noTip.get(NAME).status, "active");
const stamped = buildIndexFromSnapshot({ snapshot: { history, txs, height: state.expiry + GRACE_BLOCKS } });
assert.equal(stamped.get(NAME).status, "open");
// a server that will not say the tip: fall back to the history
const el = { call: async (m, [a]) => { if (m === "blockchain.scripthash.get_history") return history.map((x) => ({ ...x })); if (m === "blockchain.headers.subscribe") throw new Error("unsupported"); return txs[a]; } };
const live = await buildIndex({ client: el, cosignGate: false });
assert.equal(live.height, H_ENTER);
// an explicit height wins
assert.equal((await buildIndex({ client: el, cosignGate: false, height: state.expiry })).get(NAME).status, "idle");
});
test("ordinary names are untouched: no status, no expiry, same entry as before", async () => {
const reg = registration("plain.x", owner);
const e = (await reduce([[reg.tx, H_REG]], 10_000_000)).get("plain.x");
assert.deepEqual(Object.keys(e).sort(), ["category", "height", "name", "owner", "records", "txid", "updatedTxid"]);
});