Merge pull request #9 from Apex-Fusion/fix/escrow-minada-floor
fix(escrow): min-ada floor so long-capability Submits settle
This commit is contained in:
@@ -42,6 +42,7 @@ import { loadBlueprint } from "../blueprint.js";
|
||||
import { encodeTxBody, sha256Hex } from "../internal/testTxBody.js";
|
||||
import { mockSlotToWallclockMs, NETWORK_BUFFER_MS } from "../internal/constants.js";
|
||||
import { detectCborBackend } from "../internal/cborBackend.js";
|
||||
import { escrowLockFloor } from "../internal/minAdaFloor.js";
|
||||
import type { LiveOgmiosProvider } from "../../chain/LiveOgmiosProvider.js";
|
||||
|
||||
export interface PostChatEscrowParams {
|
||||
@@ -132,7 +133,7 @@ export async function buildPostChatEscrowTx(
|
||||
const requestSpecHash = sha256Utf8Hex(requestSpecCanonical);
|
||||
const promptHash = chatSessionPromptHash({ session_nonce });
|
||||
|
||||
const totalLocked =
|
||||
const economicTotal =
|
||||
advertDatum.price_lovelace +
|
||||
advertDatum.buyer_bond_lovelace +
|
||||
advertDatum.supplier_bond_lovelace;
|
||||
@@ -154,6 +155,11 @@ export async function buildPostChatEscrowTx(
|
||||
state: "Open",
|
||||
};
|
||||
|
||||
// Lock enough that the escrow output satisfies min-ada in its LARGEST
|
||||
// future state (Submitted) - value_equal on Claim/Submit forbids any
|
||||
// later bump (min-ada floor).
|
||||
const totalLocked = escrowLockFloor(escrowDatum, economicTotal);
|
||||
|
||||
// Live path: reuse the capability-agnostic chat builder (messages = []).
|
||||
// The on-chain commitment is the escrowDatum (carrying our session-init
|
||||
// prompt_hash); messages are only used for the mock cbor body, which we omit.
|
||||
|
||||
@@ -38,6 +38,7 @@ import { loadBlueprint } from "../blueprint.js";
|
||||
import { encodeTxBody, sha256Hex } from "../internal/testTxBody.js";
|
||||
import { mockSlotToWallclockMs, NETWORK_BUFFER_MS } from "../internal/constants.js";
|
||||
import { detectCborBackend } from "../internal/cborBackend.js";
|
||||
import { escrowLockFloor } from "../internal/minAdaFloor.js";
|
||||
import type { LiveOgmiosProvider } from "../../chain/LiveOgmiosProvider.js";
|
||||
|
||||
export interface PostEscrowParams {
|
||||
@@ -129,7 +130,7 @@ export async function buildPostEscrowTx(
|
||||
// supplier-side validation in M1-C can verify role/content/system integrity.
|
||||
const promptHash = sha256Utf8Hex(canonicalize(messages));
|
||||
|
||||
const totalLocked =
|
||||
const economicTotal =
|
||||
advertDatum.price_lovelace +
|
||||
advertDatum.buyer_bond_lovelace +
|
||||
advertDatum.supplier_bond_lovelace;
|
||||
@@ -151,6 +152,11 @@ export async function buildPostEscrowTx(
|
||||
state: "Open",
|
||||
};
|
||||
|
||||
// Lock enough that the escrow output satisfies min-ada in its LARGEST
|
||||
// future state (Submitted) - value_equal on Claim/Submit forbids any
|
||||
// later bump (min-ada floor).
|
||||
const totalLocked = escrowLockFloor(escrowDatum, economicTotal);
|
||||
|
||||
// Live path: produce real Cardano CBOR via lucid-evolution.
|
||||
if (detectCborBackend(chain) === "live") {
|
||||
const liveCborPath = "../internal/liveCbor.js";
|
||||
|
||||
@@ -31,6 +31,7 @@ import { loadBlueprint } from "../blueprint.js";
|
||||
import { encodeTxBody, sha256Hex } from "../internal/testTxBody.js";
|
||||
import { mockSlotToWallclockMs, NETWORK_BUFFER_MS } from "../internal/constants.js";
|
||||
import { detectCborBackend } from "../internal/cborBackend.js";
|
||||
import { escrowLockFloor } from "../internal/minAdaFloor.js";
|
||||
import type { LiveOgmiosProvider } from "../../chain/LiveOgmiosProvider.js";
|
||||
|
||||
/** TTS request envelope. The buyer-side hash and the supplier-side hash
|
||||
@@ -149,7 +150,7 @@ export async function buildPostTtsEscrowTx(
|
||||
const requestSpecHash = sha256Utf8Hex(requestSpecCanonical);
|
||||
const promptHash = ttsPromptHash(request);
|
||||
|
||||
const totalLocked =
|
||||
const economicTotal =
|
||||
advertDatum.price_lovelace +
|
||||
advertDatum.buyer_bond_lovelace +
|
||||
advertDatum.supplier_bond_lovelace;
|
||||
@@ -171,6 +172,11 @@ export async function buildPostTtsEscrowTx(
|
||||
state: "Open",
|
||||
};
|
||||
|
||||
// Lock enough that the escrow output satisfies min-ada in its LARGEST
|
||||
// future state (Submitted) - value_equal on Claim/Submit forbids any
|
||||
// later bump (min-ada floor).
|
||||
const totalLocked = escrowLockFloor(escrowDatum, economicTotal);
|
||||
|
||||
// Live path: reuse the chat builder. The live cbor path is already
|
||||
// capability-agnostic — `messages` is `void`-marked there. We pass an
|
||||
// empty array so the type checker is satisfied; the actual on-chain
|
||||
|
||||
67
packages/shared/src/tx/internal/minAdaFloor.ts
Normal file
67
packages/shared/src/tx/internal/minAdaFloor.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* minAdaFloor.ts — min-ada floor for the escrow's LARGEST future datum.
|
||||
*
|
||||
* The escrow validator requires value_equal(continuing, in) on Claim and
|
||||
* Submit, so the lovelace locked at POST time must already satisfy min-ada
|
||||
* for every datum the output will ever carry. The largest is the Submitted
|
||||
* state: submitted_at None -> Some(POSIX ms) and result_receipt_hash
|
||||
* None -> Some(32 bytes) add ~43 bytes over Open/Claimed. If the posted
|
||||
* lovelace sits below that state's min-ada, lucid bumps the Submit
|
||||
* continuing output to min-ada, value_equal fails, and the validator
|
||||
* errors with no traces (the 2026-08-07 mainnet incident: the 33-byte
|
||||
* "ocr.page.extract.chandra-ocr-2.v1" capability_id pushed the Submitted
|
||||
* output 11,030 lovelace over a 2,200,000 lock; the 20-byte chat
|
||||
* capability stayed 49,310 under, which is why only OCR Submits failed).
|
||||
*
|
||||
* escrowLockFloor() returns the lovelace to lock: the economic total
|
||||
* (payment + both bonds), raised to the Submitted-state min-ada plus a
|
||||
* safety margin when the total alone is too small.
|
||||
*/
|
||||
|
||||
import type { EscrowDatum } from "../../cbor/types.js";
|
||||
import { encodeEscrowDatum } from "../../cbor/EscrowDatum.js";
|
||||
|
||||
/** Conway/Babbage utxoCostPerByte on Vector mainnet (protocol params). */
|
||||
const UTXO_COST_PER_BYTE = 4310n;
|
||||
|
||||
/** Ledger constant: min-ada = (160 + serialized output bytes) * costPerByte. */
|
||||
const OUTPUT_OVERHEAD_BYTES = 160n;
|
||||
|
||||
/** Covers CBOR framing drift between this estimate and the ledger's exact
|
||||
* serialization (address, coin width, datum wrapper). Generous on purpose:
|
||||
* 50k lovelace = 0.05 AP3X, dwarfed by per-leg tx fees. */
|
||||
const SAFETY_MARGIN_LOVELACE = 50_000n;
|
||||
|
||||
/** Serialized-output byte estimate for an enterprise script address output
|
||||
* carrying `lovelace` and an inline datum of `datumBytes`. Mirrors the
|
||||
* arithmetic validated against mainnet on 2026-08-07 (minada-calc). */
|
||||
function estimateOutputBytes(datumBytes: number, lovelace: bigint): bigint {
|
||||
const addr = 1n + 29n;
|
||||
const coin = 1n + (lovelace > 0xffffffffn ? 8n : 4n);
|
||||
const datumField = 1n + 2n + 3n + BigInt(datumBytes);
|
||||
const mapOverhead = 3n;
|
||||
return addr + coin + datumField + mapOverhead;
|
||||
}
|
||||
|
||||
/** Min-ada for the escrow output when it carries `datum`. */
|
||||
export function minAdaForEscrowDatum(datum: EscrowDatum, lovelace: bigint): bigint {
|
||||
const datumBytes = encodeEscrowDatum(datum).length / 2;
|
||||
const size = estimateOutputBytes(datumBytes, lovelace);
|
||||
return (OUTPUT_OVERHEAD_BYTES + size) * UTXO_COST_PER_BYTE;
|
||||
}
|
||||
|
||||
/** The lovelace a POST must lock so the escrow output satisfies min-ada in
|
||||
* every reachable state, including Submitted. `economicTotal` is
|
||||
* payment + buyer_bond + supplier_bond. */
|
||||
export function escrowLockFloor(openDatum: EscrowDatum, economicTotal: bigint): bigint {
|
||||
const submittedShape: EscrowDatum = {
|
||||
...openDatum,
|
||||
// Representative worst-case Submit fields: a 13-digit POSIX ms stamp
|
||||
// and a 32-byte receipt hash — sizes, not values, matter here.
|
||||
submitted_at: openDatum.deliver_by,
|
||||
result_receipt_hash: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
state: "Submitted",
|
||||
};
|
||||
const floor = minAdaForEscrowDatum(submittedShape, economicTotal) + SAFETY_MARGIN_LOVELACE;
|
||||
return floor > economicTotal ? floor : economicTotal;
|
||||
}
|
||||
Reference in New Issue
Block a user