Skip to content

Commit

Permalink
Speculatively implement P2S, OP_EVAL, and OP_POW
Browse files Browse the repository at this point in the history
  • Loading branch information
bitjson committed Dec 12, 2024
1 parent 681f962 commit 9628c6b
Show file tree
Hide file tree
Showing 93 changed files with 1,322 additions and 89 deletions.
5 changes: 5 additions & 0 deletions .changeset/tall-actors-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bitauth/libauth': minor
---

Speculatively implement P2S, OP_EVAL, and OP_POW
2 changes: 2 additions & 0 deletions src/lib/engine/types/template-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,8 @@ export type WalletTemplateScriptLocking = WalletTemplateScript & {
* The presence of the `lockingType` property indicates that this script is a
* locking script. It must be present on any script referenced by the
* `unlocks` property of another script.
*
* TODO: migrate `standard` -> `p2s`
*/
lockingType: 'p2sh20' | 'p2sh32' | 'standard';
};
Expand Down
19 changes: 13 additions & 6 deletions src/lib/language/language-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,8 @@ export const extractEvaluationSamples = <
* outer evaluation appear before their parent sample (which uses their result).
*/
export const extractEvaluationSamplesRecursive = <
ProgramState extends AuthenticationProgramStateMinimum,
ProgramState extends AuthenticationProgramStateControlStack &
AuthenticationProgramStateMinimum,
>({
/**
* The range of the script node that was evaluated to produce the `trace`
Expand All @@ -670,6 +671,8 @@ export const extractEvaluationSamplesRecursive = <
nodes: ScriptReductionTraceScriptNode<ProgramState>['script'];
trace: ProgramState[];
}): SampleExtractionResult<ProgramState> => {
const statesNotProducedByOpEval = (state: ProgramState) =>
!state.controlStack.some((item) => typeof item === 'object');
const extractEvaluations = (
node: ScriptReductionTraceChildNode<ProgramState>,
depth = 1,
Expand All @@ -690,7 +693,9 @@ export const extractEvaluationSamplesRecursive = <
],
[],
);
const traceWithoutUnlockingPhase = node.trace.slice(1);
const traceWithoutUnlockingPhase = node.trace
.slice(1)
.filter(statesNotProducedByOpEval);
const evaluationBeginToken = '$(';
const evaluationEndToken = ')';
const extracted = extractEvaluationSamples<ProgramState>({
Expand All @@ -711,7 +716,7 @@ export const extractEvaluationSamplesRecursive = <
const { samples, unmatchedStates } = extractEvaluationSamples<ProgramState>({
evaluationRange,
nodes,
trace,
trace: trace.filter(statesNotProducedByOpEval),
});

const childSamples = nodes.reduce<EvaluationSample<ProgramState>[]>(
Expand Down Expand Up @@ -825,6 +830,7 @@ export const extractUnexecutedRanges = <
return containedRangesExcluded;
};

const oneBelowHash160 = 19;
/**
* Given a stack, return a summary of the stack's contents, encoding valid VM
* numbers as numbers, and all other stack items as hex literals.
Expand All @@ -833,7 +839,9 @@ export const extractUnexecutedRanges = <
*/
export const summarizeStack = (stack: Uint8Array[]) =>
stack.map((item) => {
const asNumber = vmNumberToBigInt(item);
const asNumber = vmNumberToBigInt(item, {
maximumVmNumberByteLength: oneBelowHash160,
});
return `0x${binToHex(item)}${
typeof asNumber === 'string' ? '' : `(${asNumber.toString()})`
}`;
Expand Down Expand Up @@ -879,8 +887,7 @@ export const summarizeDebugTrace = <
...(nextState.error === undefined
? {}
: { error: nextState.error }),
execute:
state.controlStack[state.controlStack.length - 1] !== false,
execute: state.controlStack.every((item) => item !== false),
instruction:
'instruction' in state
? state.instruction
Expand Down
6 changes: 5 additions & 1 deletion src/lib/vm/instruction-sets/bch/2023/bch-2023-consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const ConsensusBch2023 = {
* A.K.A. `MAX_SCRIPT_SIZE`
*/
maximumBytecodeLength: 10000,
maximumCommitmentLength: 40,
/**
* A.K.A. `MAX_CONSENSUS_VERSION`
*/
Expand All @@ -40,6 +39,10 @@ export const ConsensusBch2023 = {
* A.K.A. `MAX_SCRIPT_ELEMENT_SIZE`
*/
maximumStackItemLength: 520,
/**
* When set to `-1`, only BCH_2023_05 standard patterns are accepted.
*/
maximumStandardLockingBytecodeLength: -1,
/**
* A.K.A. `MAX_STANDARD_TX_SIZE`
*/
Expand All @@ -48,6 +51,7 @@ export const ConsensusBch2023 = {
* A.K.A. `MAX_TX_IN_SCRIPT_SIG_SIZE`
*/
maximumStandardUnlockingBytecodeLength: 1650,
maximumTokenCommitmentLength: 40,
/**
* A.K.A. `MAX_TX_SIZE`
*/
Expand Down
50 changes: 37 additions & 13 deletions src/lib/vm/instruction-sets/bch/2023/bch-2023-instruction-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,10 +911,20 @@ export const createInstructionSetBch2023 = <

// eslint-disable-next-line functional/no-loop-statements
for (const [index, output] of sourceOutputs.entries()) {
if (!isStandardUtxoBytecode(output.lockingBytecode)) {
if (consensus.maximumStandardLockingBytecodeLength === -1) {
if (!isStandardUtxoBytecode(output.lockingBytecode)) {
return formatError(
AuthenticationErrorCommon.verifyStandardFailedNonstandardSourceOutput,
`Source output ${index} is non-standard: locking bytecode does not match a standard pattern: P2PKH, P2PK, P2SH, P2MS, or arbitrary data (OP_RETURN).`,
);
}
} else if (
output.lockingBytecode.length >
consensus.maximumStandardLockingBytecodeLength
) {
return formatError(
AuthenticationErrorCommon.verifyStandardFailedNonstandardSourceOutput,
`Source output ${index} is non-standard: locking bytecode does not match a standard pattern: P2PKH, P2PK, P2SH, P2MS, or arbitrary data (OP_RETURN).`,
`Source output ${index} is non-standard: locking bytecode length of ${output.lockingBytecode.length} exceeds the maximum standard locking bytecode length of ${consensus.maximumStandardLockingBytecodeLength}.`,
);
}
}
Expand All @@ -923,21 +933,32 @@ export const createInstructionSetBch2023 = <
let totalArbitraryDataBytes = 0;
// eslint-disable-next-line functional/no-loop-statements
for (const [index, output] of transaction.outputs.entries()) {
if (!isStandardOutputBytecode(output.lockingBytecode)) {
return formatError(
AuthenticationErrorCommon.verifyStandardFailedNonstandardOutput,
`Transaction output ${index} is non-standard: locking bytecode does not match a standard pattern: P2PKH, P2PK, P2SH, P2MS, or arbitrary data (OP_RETURN).`,
);
}
// eslint-disable-next-line functional/no-conditional-statements
if (isArbitraryDataOutput(output.lockingBytecode)) {
// eslint-disable-next-line functional/no-expression-statements
totalArbitraryDataBytes += output.lockingBytecode.length + 1;
if (consensus.maximumStandardLockingBytecodeLength === -1) {
if (!isStandardOutputBytecode(output.lockingBytecode)) {
return formatError(
AuthenticationErrorCommon.verifyStandardFailedNonstandardOutput,
`Transaction output ${index} is non-standard: locking bytecode does not match a standard pattern: P2PKH, P2PK, P2SH, P2MS, or arbitrary data (OP_RETURN).`,
);
}
} else if (
output.lockingBytecode.length >
consensus.maximumStandardLockingBytecodeLength
) {
// eslint-disable-next-line functional/no-conditional-statements
if (isArbitraryDataOutput(output.lockingBytecode)) {
// eslint-disable-next-line functional/no-expression-statements
totalArbitraryDataBytes += output.lockingBytecode.length + 1;
} else {
return formatError(
AuthenticationErrorCommon.verifyStandardFailedNonstandardOutput,
`Transaction output ${index} is non-standard: locking bytecode length of ${output.lockingBytecode.length} exceeds the maximum standard locking bytecode length of ${consensus.maximumStandardLockingBytecodeLength} and does not match the standard arbitrary data pattern (OP_RETURN).`,
);
}
}
if (isDustOutput(output)) {
return formatError(
AuthenticationErrorCommon.verifyStandardFailedDustOutput,
` Transaction output ${index} must have a value of at least ${getDustThreshold(
`Transaction output ${index} must have a value of at least ${getDustThreshold(
output,
)} satoshis. Current value: ${output.valueSatoshis}`,
);
Expand Down Expand Up @@ -973,6 +994,9 @@ export const createInstructionSetBch2023 = <
const tokenValidationResult = verifyTransactionTokens(
transaction,
sourceOutputs,
{
maximumTokenCommitmentLength: consensus.maximumTokenCommitmentLength,
},
);
if (tokenValidationResult !== true) {
return tokenValidationResult;
Expand Down
8 changes: 3 additions & 5 deletions src/lib/vm/instruction-sets/bch/2023/bch-2023-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,17 @@ export const extractTransactionOutputTokenData = (
export const verifyTransactionTokens = (
transaction: Transaction,
sourceOutputs: Output[],
{ maximumTokenCommitmentLength }: { maximumTokenCommitmentLength: number },
) => {
const excessiveCommitment = [...sourceOutputs, ...transaction.outputs].find(
(output) =>
output.token?.nft?.commitment !== undefined &&
output.token.nft.commitment.length >
ConsensusBch2023.maximumCommitmentLength,
output.token.nft.commitment.length > maximumTokenCommitmentLength,
);
if (excessiveCommitment !== undefined) {
return formatError(
AuthenticationErrorCommon.tokenValidationExcessiveCommitmentLength,
`A token commitment exceeds the consensus limit of ${
ConsensusBch2023.maximumCommitmentLength
} bytes. Excessive token commitment length: ${
`A token commitment exceeds the consensus limit of ${maximumTokenCommitmentLength} bytes. Excessive token commitment length: ${
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
excessiveCommitment.token!.nft!.commitment.length
}`,
Expand Down
5 changes: 4 additions & 1 deletion src/lib/vm/instruction-sets/bch/2026/bch-2026-consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { ConsensusBch2025 } from '../2025/bch-2025-consensus.js';
* Consensus setting overrides for the `BCH_SPEC` instruction set.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const ConsensusBch2026Overrides = {};
export const ConsensusBch2026Overrides = {
maximumStandardLockingBytecodeLength: 201,
maximumTokenCommitmentLength: 80,
};

/**
* Consensus settings for the `BCH_SPEC` instruction set.
Expand Down
20 changes: 20 additions & 0 deletions src/lib/vm/instruction-sets/bch/2026/bch-2026-descriptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { OpcodeDescriptionsBch2023 } from '../2023/bch-2023-descriptions.js';

/**
* Descriptions for the opcodes added to the `BCH_2026_05` instruction set
* beyond those present in `BCH_2025_05`.
*/
export enum OpcodeDescriptionsBch2026Additions {
OP_EVAL = 'Pop the top item from the stack as bytecode. Preserve the active bytecode at the top of the control stack, then evaluate the bytecode as if it were the active bytecode (without modifying the stack, alternate stack, or other evaluation context). When the evaluation is complete, restore the original bytecode and continue evaluation after the OP_EVAL instruction. If the bytecode is malformed, error.',
OP_BEGIN = 'Push the current instruction pointer index to the control stack as an integer (to be read by OP_UNTIL).',
OP_UNTIL = 'Pop the top item from the control stack (if the control value is not an integer, error). Add the difference between the control value and the current instruction pointer index to the repeated bytes counter, if the sum of the repeated bytes counter and the active bytecode length is greater than the maximum bytecode length, error. Pop the top item from the stack, if the value is not truthy, move the instruction pointer to the control value (and re-evaluate the OP_BEGIN).',
}

/**
* Descriptions for the `BCH_SPEC` instruction set.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const OpcodeDescriptionsBch2026 = {
...OpcodeDescriptionsBch2023,
...OpcodeDescriptionsBch2026Additions,
};
1 change: 1 addition & 0 deletions src/lib/vm/instruction-sets/bch/2026/bch-2026-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum AuthenticationErrorBch2026Additions {
unexpectedUntil = 'Encountered an OP_UNTIL that is not following a matching OP_BEGIN.',
unexpectedUntilMissingEndIf = 'Encountered an OP_UNTIL before the previous OP_IF was closed by an OP_ENDIF.',
excessiveLooping = 'Program attempted an OP_UNTIL operation that would exceed the limit of repeated bytes.',
malformedEval = 'Program attempted to OP_EVAL malformed bytecode.',
}

/**
Expand Down
49 changes: 49 additions & 0 deletions src/lib/vm/instruction-sets/bch/2026/bch-2026-eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type {
AuthenticationInstructionMalformed,
AuthenticationProgramStateBch2026,
} from '../../../../lib.js';
import {
applyError,
authenticationInstructionsAreMalformed,
decodeAuthenticationInstructions,
disassembleAuthenticationInstructionMalformed,
executionIsActive,
pushToControlStack,
useOneStackItem,
} from '../../common/common.js';

import { AuthenticationErrorBch2026 } from './bch-2026-errors.js';
import { OpcodesBch2026 } from './bch-2026-opcodes.js';

export const opEval = <State extends AuthenticationProgramStateBch2026>(
state: State,
) => {
if (executionIsActive(state)) {
return useOneStackItem(state, (nextState, [item]) => {
const newInstructions = decodeAuthenticationInstructions(item);

if (authenticationInstructionsAreMalformed(newInstructions)) {
return applyError(
nextState,
AuthenticationErrorBch2026.malformedEval,
`Malformed instruction: ${disassembleAuthenticationInstructionMalformed(
OpcodesBch2026,
newInstructions[
newInstructions.length - 1
] as AuthenticationInstructionMalformed,
)}.`,
);
}

const manuallyAdvance = 1;
const finalState = pushToControlStack(nextState, {
instructions: nextState.instructions,
ip: nextState.ip + manuallyAdvance,
});
finalState.ip = 0 - manuallyAdvance; // eslint-disable-line functional/no-expression-statements, functional/immutable-data
finalState.instructions = newInstructions; // eslint-disable-line functional/no-expression-statements, functional/immutable-data
return finalState;
});
}
return state;
};
19 changes: 19 additions & 0 deletions src/lib/vm/instruction-sets/bch/2026/bch-2026-instruction-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '../../../../crypto/crypto.js';
import type {
AuthenticationProgramBch,
AuthenticationProgramStackFrame,
InstructionSet,
ResolvedTransactionBch,
Ripemd160,
Expand All @@ -17,6 +18,7 @@ import { createInstructionSetBch2025 } from '../2025/bch-2025-instruction-set.js
import { opBegin, opUntil } from '../2026/bch-2026-loops.js';

import { ConsensusBch2026 } from './bch-2026-consensus.js';
import { opEval } from './bch-2026-eval.js';
import { OpcodesBch2026 } from './bch-2026-opcodes.js';
import type { AuthenticationProgramStateBch2026 } from './bch-2026-types.js';
/**
Expand Down Expand Up @@ -80,13 +82,30 @@ export const createInstructionSetBch2026 = <
});
return {
...instructionSet,
/* eslint-disable functional/no-loop-statements, functional/immutable-data, functional/no-expression-statements */
continue: (state) => {
if (state.error !== undefined) return false;
while (
state.ip >= state.instructions.length &&
state.controlStack.length > 0 &&
typeof state.controlStack[state.controlStack.length - 1] === 'object'
) {
const { instructions, ip } =
state.controlStack.pop() as AuthenticationProgramStackFrame;
state.ip = ip;
state.instructions = instructions;
}
return state.ip < state.instructions.length;
},
/* eslint-enable functional/no-loop-statements, functional/immutable-data, functional/no-expression-statements */
initialize: (program) =>
({
...instructionSet.initialize?.(program),
repeatedBytes: 0,
}) as Partial<AuthenticationProgramStateBch2026> as Partial<AuthenticationProgramState>,
operations: {
...instructionSet.operations,
[OpcodesBch2026.OP_EVAL]: opEval,
[OpcodesBch2026.OP_BEGIN]: opBegin,
[OpcodesBch2026.OP_UNTIL]: opUntil,
},
Expand Down
9 changes: 5 additions & 4 deletions src/lib/vm/instruction-sets/bch/2026/bch-2026-loops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
stackItemIsTruthy,
useOneStackItem,
} from '../../common/common.js';
import { AuthenticationErrorBchSpec } from '../spec/bch-spec-errors.js';

import { AuthenticationErrorBch2026 } from './bch-2026-errors.js';

const enum Constants {
markInactiveOpBegin = -1,
Expand All @@ -27,14 +28,14 @@ export const opUntil = <State extends AuthenticationProgramStateBch2026>(
// eslint-disable-next-line functional/immutable-data
const controlValue = state.controlStack.pop();
if (typeof controlValue !== 'number') {
return applyError(state, AuthenticationErrorBchSpec.unexpectedUntil);
return applyError(state, AuthenticationErrorBch2026.unexpectedUntil);
}
if (!executionIsActive(state)) {
return controlValue === Constants.markInactiveOpBegin
? state
: applyError(
state,
AuthenticationErrorBchSpec.unexpectedUntilMissingEndIf,
AuthenticationErrorBch2026.unexpectedUntilMissingEndIf,
);
}

Expand All @@ -51,7 +52,7 @@ export const opUntil = <State extends AuthenticationProgramStateBch2026>(
) {
return applyError(
state,
AuthenticationErrorBchSpec.excessiveLooping,
AuthenticationErrorBch2026.excessiveLooping,
`Repeated bytes: ${state.repeatedBytes}; active bytecode length: ${activeBytecodeLength}`,
);
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/vm/instruction-sets/bch/2026/bch-2026-opcodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { OpcodesBch2023 } from '../2023/bch-2023-opcodes.js';
* `BCH_2023_05`.
*/
export enum OpcodesBch2026Additions {
OP_EVAL = 0x62,
OP_BEGIN = 0x65,
OP_UNTIL = 0x66,
}
Expand Down
Loading

0 comments on commit 9628c6b

Please sign in to comment.