// Sia (v2 era) primitives: the Sia binary encoder, standard unlock-hash // addresses, walletd's per-index key derivation, the v2 input signature hash // and transaction weight. Mirrors go.sia.tech/core/types; every encoding // here was checked against a real mainnet v2 transaction. module.exports = function makeSia({ ed25519, blake2b }) { const b256 = (data) => blake2b(data, { dkLen: 32 }); const enc = new TextEncoder(); const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); const fromHex = (h) => Uint8Array.from(String(h).replace(/^0x/, "").match(/../g) || [], (x) => parseInt(x, 16)); // ---- encoder --------------------------------------------------------------- class Encoder { constructor() { this.parts = []; this.length = 0; } write(b) { this.parts.push(b); this.length += b.length; return this; } u8(n) { return this.write(Uint8Array.from([n & 0xff])); } bool(v) { return this.u8(v ? 1 : 0); } u64(n) { let v = BigInt(n); const out = new Uint8Array(8); for (let i = 0; i < 8; i++) { out[i] = Number(v & 0xffn); v >>= 8n; } return this.write(out); } bytes(b) { return this.u64(b.length).write(b); } str(s) { return this.bytes(enc.encode(s)); } // Currency is a 128-bit little-endian pair (lo, hi). currency(hastings) { const v = BigInt(hastings); if (v < 0n || v >= (1n << 128n)) throw new Error("currency out of range"); return this.u64(v & ((1n << 64n) - 1n)).u64(v >> 64n); } bytesOut() { const out = new Uint8Array(this.length); let o = 0; for (const p of this.parts) { out.set(p, o); o += p.length; } return out; } } const SPECIFIER_ED25519 = (() => { const s = new Uint8Array(16); s.set(enc.encode("ed25519")); return s; })(); // ---- addresses --------------------------------------------------------------- const LEAF = 0, NODE = 1; const sumPair = (a, b) => b256(Uint8Array.from([NODE, ...a, ...b])); const leaf = (bytes) => b256(Uint8Array.from([LEAF, ...bytes])); // Merkle root of the standard UnlockConditions {timelock 0, [pk], sigs 1}. function standardUnlockHash(pk) { const timelockHash = leaf(new Encoder().u64(0).bytesOut()); const keyHash = leaf(new Encoder().write(SPECIFIER_ED25519).bytes(pk).bytesOut()); const sigsHash = leaf(new Encoder().u64(1).bytesOut()); return sumPair(sumPair(timelockHash, keyHash), sigsHash); } const addressString = (addr32) => toHex(addr32) + toHex(b256(addr32).slice(0, 6)); function parseAddress(s) { const t = String(s || "").trim().toLowerCase().replace(/^addr:/, ""); if (!/^[0-9a-f]{76}$/.test(t)) throw new Error("address must be 76 hex characters"); const raw = fromHex(t); const body = raw.slice(0, 32); if (toHex(b256(body).slice(0, 6)) !== toHex(raw.slice(32))) throw new Error("address checksum is wrong"); return { bytes: body, address: t }; } // ---- keys -------------------------------------------------------------------- // walletd: key_i = ed25519 seed blake2b(seed32 || index u64le). function keyFromSeed(seed32, index) { const priv = b256(new Encoder().write(seed32).u64(index).bytesOut()); const pub = ed25519.getPublicKey(priv); return { priv, pub, address32: standardUnlockHash(pub), address: addressString(standardUnlockHash(pub)) }; } const sign = (priv, msg) => ed25519.sign(msg, priv); const verify = (pub, msg, sig) => ed25519.verify(sig, msg, pub); // ---- v2 transactions --------------------------------------------------------- // tx: { inputs: [{ parentId(hex) }], outputs: [{ value(BigInt), address32 }], minerFee(BigInt) } // Sig hash = blake2b("sia/sig/input|" || 0x02 || V2TransactionSemantics). function inputSigHash(tx) { const e = new Encoder().write(enc.encode("sia/sig/input|")).u8(2); e.u64(tx.inputs.length); for (const i of tx.inputs) e.write(fromHex(i.parentId)); e.u64(tx.outputs.length); for (const o of tx.outputs) e.currency(o.value).write(o.address32); e.u64(0).u64(0); // siafund inputs / outputs e.u64(0).u64(0).u64(0); // contracts, revisions, resolutions e.u64(0); // attestations e.bytes(new Uint8Array(0)); // arbitrary data e.bool(false); // new foundation address e.currency(tx.minerFee); return b256(e.bytesOut()); } // Weight = length of the full V2Transaction encoding (fees are per byte). // Signed inputs carry the parent element with its Merkle proof, the policy // and one 64-byte signature. function weight(tx) { const e = new Encoder().u8(2); let fields = 0; if (tx.inputs.length) fields |= 1; if (tx.outputs.length) fields |= 2; if (tx.minerFee > 0n) fields |= 1 << 10; e.u64(fields); if (tx.inputs.length) { e.u64(tx.inputs.length); for (const i of tx.inputs) { e.u64(i.leafIndex || 0).u64((i.merkleProof || []).length); for (const p of i.merkleProof || []) e.write(fromHex(p)); e.write(fromHex(i.parentId)).currency(i.value).write(i.address32).u64(i.maturityHeight || 0); // SatisfiedPolicy: version 1, op 7 (unlock conditions), uc, 1 sig, 0 preimages e.u8(1).u8(7).u64(0).u64(1).write(SPECIFIER_ED25519).bytes(i.pub).u64(1); e.u64(1).write(new Uint8Array(64)).u64(0); } } if (tx.outputs.length) { e.u64(tx.outputs.length); for (const o of tx.outputs) e.currency(o.value).write(o.address32); } if (tx.minerFee > 0n) e.currency(tx.minerFee); return e.length; } // walletd JSON for /api/txpool/broadcast. function toJson(tx, sigs) { return { siacoinInputs: tx.inputs.map((i, k) => ({ parent: i.element, satisfiedPolicy: { policy: { type: "uc", policy: { timelock: 0, publicKeys: ["ed25519:" + toHex(i.pub)], signaturesRequired: 1 } }, signatures: [toHex(sigs[k])], }, })), siacoinOutputs: tx.outputs.map((o) => ({ value: o.value.toString(), address: addressString(o.address32) })), minerFee: tx.minerFee.toString(), }; } // ---- units ------------------------------------------------------------------- const HASTINGS_PER_SC = 10n ** 24n; function formatSC(hastings, decimals = 6) { const v = BigInt(hastings); const neg = v < 0n; const a = neg ? -v : v; const whole = a / HASTINGS_PER_SC; let frac = (a % HASTINGS_PER_SC).toString().padStart(24, "0").slice(0, decimals).replace(/0+$/, ""); if (frac.length < 2) frac = frac.padEnd(2, "0"); return (neg ? "-" : "") + whole.toString() + "." + frac; } function parseSC(text) { const s = String(text || "").trim().replace(/,/g, ""); if (!/^\d*(\.\d*)?$/.test(s) || s === "" || s === ".") throw new Error("amount must be a number"); const [w = "0", f = ""] = s.split("."); if (f.length > 24) throw new Error("too many decimals"); return BigInt(w || "0") * HASTINGS_PER_SC + BigInt((f + "0".repeat(24)).slice(0, 24)); } return { Encoder, standardUnlockHash, addressString, parseAddress, keyFromSeed, sign, verify, inputSigHash, weight, toJson, formatSC, parseSC, HASTINGS_PER_SC, toHex, fromHex, b256 }; };