test: close PR-10 review debt - selection/detail/redeemer pins, signer signs a script tx
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
MIN_AP3X_DEPOSIT, getRegistryAddress, resolveAgentUtxo, parseAgentDatum,
|
||||
buildRegisterAgent, buildUpdateAgent, buildTransferAgent, buildDeregisterAgent, buildMessageAgent,
|
||||
} from './registry-build.js';
|
||||
import type { RegistryAgentProfile } from './registry-build.js';
|
||||
|
||||
function newProvider() {
|
||||
return new OgmiosProvider({ ogmiosUrl: VECTOR_OGMIOS_URL, submitUrl: VECTOR_SUBMIT_URL, koiosUrl: VECTOR_KOIOS_URL });
|
||||
@@ -102,7 +103,7 @@ ${capList}
|
||||
try {
|
||||
const provider = newProvider();
|
||||
const utxos = await provider.getUtxos(getRegistryAddress());
|
||||
const profiles = [];
|
||||
const profiles: RegistryAgentProfile[] = [];
|
||||
for (const utxo of utxos) {
|
||||
if (!utxo.datum) continue;
|
||||
const profile = parseAgentDatum(utxo.datum, `${utxo.txHash}#${utxo.outputIndex}`, utxo.assets);
|
||||
|
||||
@@ -10,7 +10,9 @@ import type { RawServerHandle } from '../setup.ts';
|
||||
// unit-tests RateLimiter/limiterFor in isolation; test:smoke only ever calls
|
||||
// listTools(). Neither exercises a single tool handler, so neither would
|
||||
// notice checkRateLimit/rateLimiter falling out of scope in agent-network.ts
|
||||
// or self-improvement.ts (both @ts-nocheck - tsc can't catch it either). See
|
||||
// or self-improvement.ts. agent-network.ts is typed now, so tsc catches a
|
||||
// scoping regression there; self-improvement.ts (typed in the
|
||||
// self-improvement-keyless PR) is the remaining compiler-blind case. See
|
||||
// .superpowers/sdd/pr5-integration-tests-report.md for the mutation-testing
|
||||
// evidence.
|
||||
//
|
||||
|
||||
@@ -346,6 +346,25 @@ describe('registry owner ops (real validator, offline)', () => {
|
||||
}
|
||||
assert.ok(tx.body().mint(), 'no mint (burn) field on the deregister tx');
|
||||
});
|
||||
|
||||
test('deregister picks the LARGEST pure-AP3X wallet UTxO as the extra input', async () => {
|
||||
const small = { txHash: 'a1'.repeat(32), outputIndex: 0, address: OWN_ADDRESS, assets: { lovelace: 3_000_000n } };
|
||||
const large = { txHash: 'a2'.repeat(32), outputIndex: 0, address: OWN_ADDRESS, assets: { lovelace: 900_000_000n } };
|
||||
// small listed FIRST so first-match selection would pick it
|
||||
const provider = new FixtureProvider([small, large, REGISTRY_UTXO]);
|
||||
const lucid = await lucidForAddress(provider, OWN_ADDRESS);
|
||||
const r = await buildDeregisterAgent(lucid, provider, { changeAddress: OWN_ADDRESS, agentId: AGENT_DID });
|
||||
const tx = CML.Transaction.from_cbor_hex(r.txCbor);
|
||||
const ins = tx.body().inputs();
|
||||
let sawLarge = false, sawSmall = false;
|
||||
for (let i = 0; i < ins.len(); i++) {
|
||||
const h = ins.get(i).transaction_id().to_hex();
|
||||
if (h === 'a2'.repeat(32)) sawLarge = true;
|
||||
if (h === 'a1'.repeat(32)) sawSmall = true;
|
||||
}
|
||||
assert.ok(sawLarge, 'largest pure-AP3X UTxO must be the chosen extra input');
|
||||
assert.ok(!sawSmall, 'the smaller pure-AP3X UTxO must not be chosen over the larger');
|
||||
});
|
||||
});
|
||||
|
||||
// Correction round (see task-3-report.md): differential testing against the
|
||||
@@ -423,6 +442,15 @@ describe('registry owner ops - value preservation & deregister floor (correction
|
||||
.complete();
|
||||
const tx = decodeTx(completed.toCBOR());
|
||||
assert.equal(tx.body().inputs().len(), 1, 'sole input - Lucid should not need to pull in any extra wallet UTxO here');
|
||||
|
||||
// Pin: buildDeregisterAgent's returned `detail` is the ACTUAL spent
|
||||
// registry UTxO lovelace (25 AP3X here), not the MIN_AP3X_DEPOSIT
|
||||
// constant. Goes through buildDeregisterAgent itself (unlike the direct
|
||||
// build above), which is safe here because the provider also carries
|
||||
// FIXTURE_UTXOS, so the extra fee UTxO that path unconditionally pulls in
|
||||
// is available.
|
||||
const r = await buildDeregisterAgent(lucid, provider, { changeAddress: OWN_ADDRESS, agentId: AGENT_DID });
|
||||
assert.equal(r.detail, '25000000', 'detail must be the spent registry UTxO lovelace, not the constant');
|
||||
});
|
||||
|
||||
test('deregister throws an actionable error (not a validator crash) when the wallet has no plain AP3X-only UTxO', async () => {
|
||||
|
||||
@@ -81,6 +81,7 @@ describe('signWithMnemonic preserves an existing script witness set (hermetic pi
|
||||
test('signing a script-bearing tx keeps the vkey witness AND the original scripts/redeemers/datums, and leaves the body hash unchanged', () => {
|
||||
const unsignedCborHex = buildSyntheticScriptBearingTx();
|
||||
const unsignedBodyHashHex = CML.hash_transaction(CML.Transaction.from_cbor_hex(unsignedCborHex).body()).to_hex();
|
||||
const redeemersBefore = CML.Transaction.from_cbor_hex(unsignedCborHex).witness_set().redeemers()!.to_cbor_hex();
|
||||
|
||||
const { signedCborHex, txHash } = signWithMnemonic(unsignedCborHex, TEST_ONLY_PUBLIC_MNEMONIC);
|
||||
|
||||
@@ -106,6 +107,10 @@ describe('signWithMnemonic preserves an existing script witness set (hermetic pi
|
||||
redeemers!.as_map_redeemer_key_to_redeemer_val()?.len(), 1,
|
||||
'expected exactly 1 redeemer to survive signing',
|
||||
);
|
||||
assert.equal(
|
||||
redeemers!.to_cbor_hex(), redeemersBefore,
|
||||
'redeemer CBOR must survive signing byte-identically',
|
||||
);
|
||||
|
||||
const datums = ws.plutus_datums();
|
||||
assert.ok(datums && datums.len() === 1, `expected the original plutus datum to survive signing, got ${datums?.len() ?? 0}`);
|
||||
|
||||
122
packages/signer/test/sign-script-tx.test.ts
Normal file
122
packages/signer/test/sign-script-tx.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// packages/signer/test/sign-script-tx.test.ts
|
||||
//
|
||||
// Hermetic pin: proves the SIGNER's own signTransaction (src/sign.ts) signs a
|
||||
// script-bearing transaction without dropping its plutus_v3_scripts,
|
||||
// redeemers, or plutus_datums. Standing follow-up closed here: sign.test.ts's
|
||||
// FIXTURE_CBOR carries no script witnesses at all, so the signer suite never
|
||||
// exercised this path before this file - "the signer suite never signs a
|
||||
// script tx".
|
||||
//
|
||||
// Mirrors packages/builder/test/sign-helper.test.ts's synthetic
|
||||
// script-bearing tx construction (same shape: one input, one output, one
|
||||
// PlutusV3 script, one redeemer, one datum in the witness set) so the two
|
||||
// "does signing preserve a script witness set" pins - builder's
|
||||
// signWithMnemonic path and the signer's own signTransaction(cbor, bech32Key)
|
||||
// path - stay directly comparable, even though the two packages sign through
|
||||
// different call surfaces (a mnemonic vs. a raw bech32 private key).
|
||||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as CML from '@anastasia-labs/cardano-multiplatform-lib-nodejs';
|
||||
import { signTransaction } from '../src/sign.ts';
|
||||
import { MnemonicKeySource } from '../src/keysource.ts';
|
||||
|
||||
// Public BIP39 test vector: 23x "abandon" + the checksum word "art" (the
|
||||
// 24-word encoding of all-zero, 256-bit entropy - verified directly against
|
||||
// the `bip39` package's own entropyToMnemonic(), not transcribed from
|
||||
// memory: `bip39.entropyToMnemonic('00'.repeat(32))`). Same constant
|
||||
// sign-helper.test.ts uses on the builder side. This is the canonical
|
||||
// example mnemonic used throughout the crypto tooling ecosystem specifically
|
||||
// because it is universally known, was never funded, and holds nothing on
|
||||
// any network. TEST-ONLY - used here purely to exercise signing logic
|
||||
// offline; never use it, or any mnemonic like it, for anything that touches
|
||||
// real funds.
|
||||
const TEST_ONLY_PUBLIC_MNEMONIC =
|
||||
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon ' +
|
||||
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art';
|
||||
|
||||
/**
|
||||
* Builds a synthetic, offline, script-bearing unsigned transaction: one
|
||||
* input, one output, and a witness set carrying one PlutusV3 script, one
|
||||
* redeemer, and one datum - enough to exercise every witness-set category
|
||||
* signTransaction must preserve, without a live network, a real validator,
|
||||
* or Lucid's TransactionBuilder. Identical shape to
|
||||
* sign-helper.test.ts's buildSyntheticScriptBearingTx, parameterised on the
|
||||
* output address so the caller can supply one derived via the signer's own
|
||||
* MnemonicKeySource rather than a second, separate walletFromSeed call.
|
||||
*/
|
||||
function buildSyntheticScriptBearingTx(address: string): string {
|
||||
const addr = CML.Address.from_bech32(address);
|
||||
|
||||
const inputs = CML.TransactionInputList.new();
|
||||
inputs.add(CML.TransactionInput.new(CML.TransactionHash.from_hex('00'.repeat(32)), 0n));
|
||||
|
||||
const outputs = CML.TransactionOutputList.new();
|
||||
outputs.add(CML.TransactionOutput.new(addr, CML.Value.from_coin(2_000_000n)));
|
||||
|
||||
const body = CML.TransactionBody.new(inputs, outputs, 200_000n);
|
||||
|
||||
const witnessSet = CML.TransactionWitnessSet.new();
|
||||
|
||||
const scripts = CML.PlutusV3ScriptList.new();
|
||||
scripts.add(CML.PlutusV3Script.from_hex('4e4d01000033222220051200120011'));
|
||||
witnessSet.set_plutus_v3_scripts(scripts);
|
||||
|
||||
const redeemerMap = CML.MapRedeemerKeyToRedeemerVal.new();
|
||||
redeemerMap.insert(
|
||||
CML.RedeemerKey.new(CML.RedeemerTag.Mint, 0n),
|
||||
CML.RedeemerVal.new(CML.PlutusData.new_bytes(new Uint8Array([1, 2, 3])), CML.ExUnits.new(1_000n, 1_000n)),
|
||||
);
|
||||
witnessSet.set_redeemers(CML.Redeemers.new_map_redeemer_key_to_redeemer_val(redeemerMap));
|
||||
|
||||
const datums = CML.PlutusDataList.new();
|
||||
datums.add(CML.PlutusData.new_bytes(new Uint8Array([9, 9, 9])));
|
||||
witnessSet.set_plutus_datums(datums);
|
||||
|
||||
return CML.Transaction.new(body, witnessSet, true).to_cbor_hex();
|
||||
}
|
||||
|
||||
describe('signTransaction preserves an existing script witness set (hermetic pin, signer side)', () => {
|
||||
test('signing a script-bearing tx keeps the vkey witness AND the original scripts/redeemers/datums, and leaves the body hash unchanged', () => {
|
||||
// Same canonical derivation MnemonicKeySource.load() uses elsewhere in
|
||||
// this package (walletFromSeed under Mainnet, account index 0) - reused
|
||||
// here rather than a second, separate call, so this fixture's output
|
||||
// address and its signing key come from one source of truth.
|
||||
const { privateKeyBech32, address } = new MnemonicKeySource(TEST_ONLY_PUBLIC_MNEMONIC).load();
|
||||
|
||||
const unsignedCborHex = buildSyntheticScriptBearingTx(address);
|
||||
const unsignedBodyHashHex = CML.hash_transaction(CML.Transaction.from_cbor_hex(unsignedCborHex).body()).to_hex();
|
||||
const redeemersBefore = CML.Transaction.from_cbor_hex(unsignedCborHex).witness_set().redeemers()!.to_cbor_hex();
|
||||
|
||||
const { signedCborHex, txHashHex } = signTransaction(unsignedCborHex, privateKeyBech32);
|
||||
|
||||
assert.equal(txHashHex, unsignedBodyHashHex, 'signing must not change the transaction body hash');
|
||||
|
||||
const signed = CML.Transaction.from_cbor_hex(signedCborHex);
|
||||
assert.equal(
|
||||
CML.hash_transaction(signed.body()).to_hex(), unsignedBodyHashHex,
|
||||
'the signed tx body hash must still match the unsigned one',
|
||||
);
|
||||
|
||||
const ws = signed.witness_set();
|
||||
|
||||
const vkeys = ws.vkeywitnesses();
|
||||
assert.ok(vkeys && vkeys.len() === 1, `expected exactly 1 vkey witness after signing, got ${vkeys?.len() ?? 0}`);
|
||||
|
||||
const scripts = ws.plutus_v3_scripts();
|
||||
assert.ok(scripts && scripts.len() === 1, `expected the original plutus_v3 script to survive signing, got ${scripts?.len() ?? 0}`);
|
||||
|
||||
const redeemers = ws.redeemers();
|
||||
assert.ok(redeemers, 'expected the original redeemers to survive signing (got none)');
|
||||
assert.equal(
|
||||
redeemers!.as_map_redeemer_key_to_redeemer_val()?.len(), 1,
|
||||
'expected exactly 1 redeemer to survive signing',
|
||||
);
|
||||
assert.equal(
|
||||
redeemers!.to_cbor_hex(), redeemersBefore,
|
||||
'redeemer CBOR must survive signing byte-identically',
|
||||
);
|
||||
|
||||
const datums = ws.plutus_datums();
|
||||
assert.ok(datums && datums.len() === 1, `expected the original plutus datum to survive signing, got ${datums?.len() ?? 0}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user