Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions packages/cashscript/src/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,8 @@ export class TransactionBuilder {
console.warn('For the best debugging experience, please recompile your contract with cashc version 0.11.0 or newer.');
}

return debugLibauthTemplate(this.getLibauthTemplate(), this);
const template = getLibauthTemplate(this, this.buildLibauthTransaction(), { useTransactionBytecode: true });
return debugLibauthTemplate(template, this, () => this.getBitauthUriWithFallback());
}

/**
Expand Down Expand Up @@ -518,22 +519,22 @@ export class TransactionBuilder {
txid = await this.provider.sendRawTransaction(tx);
} catch (e: any) {
const reason = e.error ?? e.message;

const getBitauthUriWithFallback = (): string => {
try {
return getBitauthUri(this.getLibauthTemplate());
} catch {
return 'Bitauth URI generation failed';
}
};

throw new FailedTransactionError(reason, getBitauthUriWithFallback());
throw new FailedTransactionError(reason, this.getBitauthUriWithFallback());
}

// The transaction was broadcast successfully, so failing to retrieve it afterwards is not a failed transaction
return raw ? this.getTxDetails(txid, raw) : this.getTxDetails(txid);
}

// Building the link must not replace the error it is attached to
private getBitauthUriWithFallback(): string {
try {
return getBitauthUri(this.getLibauthTemplate());
} catch {
return 'Bitauth URI generation failed';
}
}

private async getTxDetails(txid: string): Promise<TransactionDetails>;
private async getTxDetails(txid: string, raw: true): Promise<string>;
private async getTxDetails(txid: string, raw?: true): Promise<TransactionDetails | string> {
Expand Down
61 changes: 40 additions & 21 deletions packages/cashscript/src/debugging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export type DebugResult = AuthenticationProgramStateCommon[];
export type DebugResults = Record<string, DebugResult>;

// debugs the template, optionally logging the execution data
export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]): DebugResults => {
export const debugTemplate = (
template: WalletTemplate,
artifacts: Artifact[],
getErrorBitauthUri: () => string = () => getBitauthUri(template),
): DebugResults => {
// If a contract has the same name, but a different bytecode, then it is considered a name collision
const hasArtifactNameCollision = artifacts.some(
(artifact) => (
Expand All @@ -26,25 +30,33 @@ export const debugTemplate = (template: WalletTemplate, artifacts: Artifact[]):
const results: DebugResults = {};
const unlockingScriptIds = Object.keys(template.scripts).filter((key) => 'unlocks' in template.scripts[key]);

const context = createDebugContext(template, getErrorBitauthUri);

for (const unlockingScriptId of unlockingScriptIds) {
const scenarioIds = (template.scripts[unlockingScriptId] as WalletTemplateScriptUnlocking).passes ?? [];

const matchingArtifact = artifacts.find((artifact) => unlockingScriptId.startsWith(artifact.contractName));

for (const scenarioId of scenarioIds) {
results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario(template, matchingArtifact, unlockingScriptId, scenarioId);
results[`${unlockingScriptId}.${scenarioId}`] = debugSingleScenario(
template, context, matchingArtifact, unlockingScriptId, scenarioId,
);
}
}

verifyFullTransaction(template);
verifyFullTransaction(template, context);

return results;
};

const debugSingleScenario = (
template: WalletTemplate, artifact: Artifact | undefined, unlockingScriptId: string, scenarioId: string,
template: WalletTemplate,
context: DebugContext,
artifact: Artifact | undefined,
unlockingScriptId: string,
scenarioId: string,
): DebugResult => {
const { vm, program } = createProgram(template, unlockingScriptId, scenarioId);
const { vm, program } = createProgram(template, context, unlockingScriptId, scenarioId);

const fullDebugSteps = vm.debug(program);

Expand Down Expand Up @@ -121,7 +133,7 @@ const debugSingleScenario = (
// public key does not match pkh in EQUALVERIFY
// Note: due to P2PKHUnlocker implementation, the CHECKSIG cannot fail in practice, only the EQUALVERIFY can fail
if (!artifact) {
throw new FailedTransactionError(error, getBitauthUri(template));
throw new FailedTransactionError(error, context.getErrorBitauthUri());
}

const frame = resolveFrame(artifact, lastExecutedDebugStep);
Expand All @@ -136,13 +148,13 @@ const debugSingleScenario = (

// Note that we use failingIp here rather than requireStatementIp, see comment above
throw new FailedRequireError(
artifact, failingIp, requireStatement, inputIndex, getBitauthUri(template), error, frame, callStack,
artifact, failingIp, requireStatement, inputIndex, context.getErrorBitauthUri(), error, frame, callStack,
);
}

// Note that we use failingIp here rather than requireStatementIp, see comment above
throw new FailedTransactionEvaluationError(
artifact, failingIp, inputIndex, getBitauthUri(template), error, frame,
artifact, failingIp, inputIndex, context.getErrorBitauthUri(), error, frame,
);
}

Expand All @@ -167,7 +179,7 @@ const debugSingleScenario = (
// If there is no artifact, this is a P2PKH debug error, final verify can only occur when final CHECKSIG failed
// Note: due to P2PKHUnlocker implementation, this cannot happen in practice
if (!artifact) {
throw new FailedTransactionError(evaluationResult, getBitauthUri(template));
throw new FailedTransactionError(evaluationResult, context.getErrorBitauthUri());
}

const frame = resolveFrame(artifact, lastExecutedDebugStep);
Expand All @@ -183,15 +195,15 @@ const debugSingleScenario = (
sourcemapInstructionPointer,
requireStatement,
inputIndex,
getBitauthUri(template),
context.getErrorBitauthUri(),
undefined,
frame,
callStack,
);
}

throw new FailedTransactionEvaluationError(
artifact, sourcemapInstructionPointer, inputIndex, getBitauthUri(template), evaluationResult, frame,
artifact, sourcemapInstructionPointer, inputIndex, context.getErrorBitauthUri(), evaluationResult, frame,
);
}

Expand All @@ -207,12 +219,19 @@ const extractInputIndexFromScenario = (scenarioId: string): number => {

type Program = AuthenticationProgramCommon;
type CreateProgramResult = { vm: VM, program: Program };
type DebugContext = { compiler: ReturnType<typeof createCompiler>, vm: VM, getErrorBitauthUri: () => string };

const createDebugContext = (template: WalletTemplate, getErrorBitauthUri: () => string): DebugContext => ({
compiler: createCompiler(walletTemplateToCompilerConfiguration(template)),
vm: createVirtualMachine(template.supported[0] as VmTarget),
getErrorBitauthUri,
});

// internal util. instantiates the virtual machine and compiles the template into a program
const createProgram = (template: WalletTemplate, unlockingScriptId: string, scenarioId: string): CreateProgramResult => {
const configuration = walletTemplateToCompilerConfiguration(template);
const vm = createVirtualMachine(template.supported[0] as VmTarget);
const compiler = createCompiler(configuration);
// internal util. compiles a scenario of the template into a program
const createProgram = (
template: WalletTemplate, context: DebugContext, unlockingScriptId: string, scenarioId: string,
): CreateProgramResult => {
const { compiler, vm } = context;

if (!template.scripts[unlockingScriptId]) {
throw new Error(`No unlock script found in template for ID ${unlockingScriptId}`);
Expand All @@ -229,11 +248,11 @@ const createProgram = (template: WalletTemplate, unlockingScriptId: string, scen
});

if (typeof scenarioGeneration === 'string') {
throw new FailedTransactionError(scenarioGeneration, getBitauthUri(template));
throw new FailedTransactionError(scenarioGeneration, context.getErrorBitauthUri());
}

if (typeof scenarioGeneration.scenario === 'string') {
throw new FailedTransactionError(scenarioGeneration.scenario, getBitauthUri(template));
throw new FailedTransactionError(scenarioGeneration.scenario, context.getErrorBitauthUri());
}

return { vm, program: scenarioGeneration.scenario.program };
Expand Down Expand Up @@ -379,23 +398,23 @@ const getFinalExecutedVerifyIp = (executedDebugSteps: AuthenticationProgramState

// After debugging, we want to verify the full transaction to ensure it is valid (this catches any errors that are not
// necessarily script errors)
const verifyFullTransaction = (template: WalletTemplate): void => {
const verifyFullTransaction = (template: WalletTemplate, context: DebugContext): void => {
const placeholderScriptId = Object.keys(template.scripts).find((key) => 'unlocks' in template.scripts[key]);
const placeholderScenarioId = (template.scripts[placeholderScriptId ?? ''] as WalletTemplateScriptUnlocking)?.passes?.[0];

if (!placeholderScenarioId || !placeholderScriptId) {
throw new Error('No placeholder scenario ID or script ID found');
}

const { vm, program } = createProgram(template, placeholderScriptId, placeholderScenarioId);
const { vm, program } = createProgram(template, context, placeholderScriptId, placeholderScenarioId);

const verificationResult = vm.verify({
sourceOutputs: program.sourceOutputs,
transaction: program.transaction,
});

if (typeof verificationResult === 'string') {
throw new FailedTransactionError(verificationResult, getBitauthUri(template));
throw new FailedTransactionError(verificationResult, context.getErrorBitauthUri());
}
};

Expand Down
Loading
Loading