shared: opt-in CIP-33 reference-scripts for script-spending txs

When ESCROW_REF_UTXO / ADVERT_REF_UTXO env vars are set (format
<txhash>#<idx>), Claim/Submit/Accept/Reclaim/RetireAdvert call
.readFrom([refUtxo]) instead of inlining the validator CBOR via
.attach.SpendingValidator(). Lucid-evolution 0.4.30 auto-extracts
utxo.scriptRef into the script-witness map (dist/index.js:1903–1913),
so no separate attach call is needed. Default (env unset) is unchanged.

Adds buildLiveTxForPublishReferenceScripts (a one-shot tx that
publishes both validators as scriptRef-bearing UTxOs at a caller-
supplied address — typically the all-zero-pkh enterprise burn address)
and the supplier tx:publish-reference-scripts CLI that wraps it.

Targeted at testnet trial; mainnet rollout is a separate flip after
verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
david
2026-05-14 15:25:59 +02:00
parent 15693c8532
commit 5cae0c4f6c
7 changed files with 351 additions and 18 deletions

View File

@@ -96,3 +96,19 @@ ADVERT_DETAIL_URI=
# If empty, defaults to sha256("") =
# e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
ADVERT_DETAIL_HASH=
# ─── Optional: CIP-33 reference scripts ───────────────────────────────────
# Saves witness-set fee on script-spending txs (Claim, Submit, Accept,
# Reclaim, RetireAdvert) by referencing on-chain script UTxOs instead of
# inlining the full validator CBOR. Leave unset to use inlined scripts
# (current behavior).
#
# Publish the two reference UTxOs (one-shot; uses SUPPLIER_PRIV_KEY_HEX +
# OGMIOS_URL + NETWORK_ID from this env file):
# docker compose -f deploy/testnet/docker-compose.supplier.yml run --rm \
# --entrypoint sh supplier \
# -c "node_modules/.bin/tsx supplier/src/cli/publish-reference-scripts.ts"
# Paste the two printed refs here AND into buyer/.env, then
# `docker compose down && up -d` to apply.
# ESCROW_REF_UTXO=<txhash>#0
# ADVERT_REF_UTXO=<txhash>#1

View File

@@ -6,7 +6,8 @@
"scripts": {
"typecheck": "tsc --noEmit",
"tx:post-advert": "tsx src/cli/post-advert.ts",
"tx:retire-advert": "tsx src/cli/retire-advert.ts"
"tx:retire-advert": "tsx src/cli/retire-advert.ts",
"tx:publish-reference-scripts": "tsx src/cli/publish-reference-scripts.ts"
},
"dependencies": {
"@marketplace/shared": "workspace:*",

View File

@@ -0,0 +1,135 @@
/**
* publish-reference-scripts.ts — CLI entry point for
* `pnpm --filter @marketplace/supplier tx:publish-reference-scripts`.
*
* Publishes the escrow + advert validator scripts as CIP-33 reference UTxOs
* at a known burn address (header 0x60 testnet / 0x61 mainnet, all-zero pkh).
* After this tx confirms, set the printed refs in supplier/.env + buyer/.env:
* ESCROW_REF_UTXO=<txhash>#0
* ADVERT_REF_UTXO=<txhash>#1
*
* Reads env (no flags):
* SUPPLIER_PRIV_KEY_HEX 64-hex Ed25519 private key
* OGMIOS_URL https:// or wss:// endpoint (HTTPS preferred)
* NETWORK_ID "0" (testnet) or "1" (mainnet)
* VECTOR_ZERO_TIME_MS optional, mainnet override; defaults to testnet genesis
*
* stdout (success):
* ESCROW_REF_UTXO=<txhash>#0
* ADVERT_REF_UTXO=<txhash>#1
* stderr: progress lines.
*/
import { createHash } from "crypto";
import * as ed from "@noble/ed25519";
import { blake2b } from "@noble/hashes/blake2b";
import { bech32 } from "bech32";
import { LiveOgmiosProvider } from "@marketplace/shared/chain";
import {
buildLiveTxForPublishReferenceScripts,
loadEscrowScript,
loadAdvertScript,
pkhToEnterpriseAddress,
} from "@marketplace/shared/tx";
import type { WalletKey } from "@marketplace/shared/tx";
ed.etc.sha512Sync = (...messages: Uint8Array[]): Uint8Array => {
const h = createHash("sha512");
for (const m of messages) h.update(m);
return new Uint8Array(h.digest());
};
const HEX64_RE = /^[0-9a-f]{64}$/;
const ZERO_PKH_HEX = "0".repeat(56);
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
function bytesToHex(b: Uint8Array): string {
let out = "";
for (let i = 0; i < b.length; i++) out += b[i].toString(16).padStart(2, "0");
return out;
}
function deriveWalletKey(privHex: string, networkId: 0 | 1): WalletKey {
const priv = hexToBytes(privHex);
const pub = ed.getPublicKey(priv);
const pubHex = bytesToHex(pub);
const pkh = blake2b(pub, { dkLen: 28 });
const pkhHex = bytesToHex(pkh);
const header = networkId === 0 ? 0x60 : 0x61;
const payload = new Uint8Array(29);
payload[0] = header;
payload.set(pkh, 1);
const hrp = networkId === 0 ? "addr_test" : "addr";
const addr = bech32.encode(hrp, bech32.toWords(payload), 1023);
return {
pubKeyHash: pkhHex,
pubKeyHex: pubHex,
privateKeyHex: privHex,
address: addr,
};
}
export async function main(env: Record<string, string | undefined>): Promise<number> {
const privHex = (env.SUPPLIER_PRIV_KEY_HEX ?? "").toLowerCase();
const ogmiosUrl = env.OGMIOS_URL ?? "";
const networkIdStr = env.NETWORK_ID ?? "";
if (!HEX64_RE.test(privHex)) {
process.stderr.write("error: SUPPLIER_PRIV_KEY_HEX must be 64 lowercase hex chars\n");
return 1;
}
if (!ogmiosUrl) {
process.stderr.write("error: OGMIOS_URL is required\n");
return 1;
}
if (networkIdStr !== "0" && networkIdStr !== "1") {
process.stderr.write(`error: NETWORK_ID must be "0" or "1", got: ${networkIdStr}\n`);
return 1;
}
const networkId: 0 | 1 = networkIdStr === "1" ? 1 : 0;
const walletKey = deriveWalletKey(privHex, networkId);
const burnAddr = pkhToEnterpriseAddress(ZERO_PKH_HEX, networkId);
const { script: escrowScript } = loadEscrowScript();
const { script: advertScript } = loadAdvertScript();
process.stderr.write(`wallet: ${walletKey.address}\n`);
process.stderr.write(`burn addr: ${burnAddr}\n`);
process.stderr.write(`ogmios: ${ogmiosUrl}\n`);
process.stderr.write(`network: ${networkId === 0 ? "testnet" : "mainnet"}\n`);
const chain = new LiveOgmiosProvider({ ogmiosUrl });
try {
process.stderr.write(`building publish tx...\n`);
const result = await buildLiveTxForPublishReferenceScripts({
chain,
walletKey,
burnAddr,
escrowScript,
advertScript,
});
process.stderr.write(`submitted: ${result.expectedTxHash}\n`);
process.stdout.write(`ESCROW_REF_UTXO=${result.formattedEscrowRef}\n`);
process.stdout.write(`ADVERT_REF_UTXO=${result.formattedAdvertRef}\n`);
return 0;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(`error: ${msg}\n`);
return 1;
}
}
if (
process.argv[1]?.endsWith("publish-reference-scripts.ts") ||
process.argv[1]?.endsWith("publish-reference-scripts.js")
) {
main(process.env).then((code) => process.exit(code));
}