Skip to content

Commit 836d917

Browse files
authored
Speculatively implement P2S, OP_EVAL, and OP_POW (#150)
1 parent 681f962 commit 836d917

File tree

93 files changed

+1388
-89
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+1388
-89
lines changed

.changeset/tall-actors-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@bitauth/libauth': minor
3+
---
4+
5+
Speculatively implement P2S, OP_EVAL, and OP_POW

src/lib/engine/types/template-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,8 @@ export type WalletTemplateScriptLocking = WalletTemplateScript & {
841841
* The presence of the `lockingType` property indicates that this script is a
842842
* locking script. It must be present on any script referenced by the
843843
* `unlocks` property of another script.
844+
*
845+
* TODO: migrate `standard` -> `p2s`
844846
*/
845847
lockingType: 'p2sh20' | 'p2sh32' | 'standard';
846848
};

src/lib/language/language-utils.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,8 @@ export const extractEvaluationSamples = <
651651
* outer evaluation appear before their parent sample (which uses their result).
652652
*/
653653
export const extractEvaluationSamplesRecursive = <
654-
ProgramState extends AuthenticationProgramStateMinimum,
654+
ProgramState extends AuthenticationProgramStateControlStack &
655+
AuthenticationProgramStateMinimum,
655656
>({
656657
/**
657658
* The range of the script node that was evaluated to produce the `trace`
@@ -670,6 +671,8 @@ export const extractEvaluationSamplesRecursive = <
670671
nodes: ScriptReductionTraceScriptNode<ProgramState>['script'];
671672
trace: ProgramState[];
672673
}): SampleExtractionResult<ProgramState> => {
674+
const statesNotProducedByOpEval = (state: ProgramState) =>
675+
!state.controlStack.some((item) => typeof item === 'object');
673676
const extractEvaluations = (
674677
node: ScriptReductionTraceChildNode<ProgramState>,
675678
depth = 1,
@@ -690,7 +693,9 @@ export const extractEvaluationSamplesRecursive = <
690693
],
691694
[],
692695
);
693-
const traceWithoutUnlockingPhase = node.trace.slice(1);
696+
const traceWithoutUnlockingPhase = node.trace
697+
.slice(1)
698+
.filter(statesNotProducedByOpEval);
694699
const evaluationBeginToken = '$(';
695700
const evaluationEndToken = ')';
696701
const extracted = extractEvaluationSamples<ProgramState>({
@@ -711,7 +716,7 @@ export const extractEvaluationSamplesRecursive = <
711716
const { samples, unmatchedStates } = extractEvaluationSamples<ProgramState>({
712717
evaluationRange,
713718
nodes,
714-
trace,
719+
trace: trace.filter(statesNotProducedByOpEval),
715720
});
716721

717722
const childSamples = nodes.reduce<EvaluationSample<ProgramState>[]>(
@@ -825,6 +830,7 @@ export const extractUnexecutedRanges = <
825830
return containedRangesExcluded;
826831
};
827832

833+
const oneBelowHash160 = 19;
828834
/**
829835
* Given a stack, return a summary of the stack's contents, encoding valid VM
830836
* numbers as numbers, and all other stack items as hex literals.
@@ -833,7 +839,9 @@ export const extractUnexecutedRanges = <
833839
*/
834840
export const summarizeStack = (stack: Uint8Array[]) =>
835841
stack.map((item) => {
836-
const asNumber = vmNumberToBigInt(item);
842+
const asNumber = vmNumberToBigInt(item, {
843+
maximumVmNumberByteLength: oneBelowHash160,
844+
});
837845
return `0x${binToHex(item)}${
838846
typeof asNumber === 'string' ? '' : `(${asNumber.toString()})`
839847
}`;
@@ -879,8 +887,7 @@ export const summarizeDebugTrace = <
879887
...(nextState.error === undefined
880888
? {}
881889
: { error: nextState.error }),
882-
execute:
883-
state.controlStack[state.controlStack.length - 1] !== false,
890+
execute: state.controlStack.every((item) => item !== false),
884891
instruction:
885892
'instruction' in state
886893
? state.instruction

src/lib/vm/instruction-sets/bch/2023/bch-2023-consensus.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export const ConsensusBch2023 = {
1717
* A.K.A. `MAX_SCRIPT_SIZE`
1818
*/
1919
maximumBytecodeLength: 10000,
20-
maximumCommitmentLength: 40,
2120
/**
2221
* A.K.A. `MAX_CONSENSUS_VERSION`
2322
*/
@@ -40,6 +39,10 @@ export const ConsensusBch2023 = {
4039
* A.K.A. `MAX_SCRIPT_ELEMENT_SIZE`
4140
*/
4241
maximumStackItemLength: 520,
42+
/**
43+
* When set to `-1`, only BCH_2023_05 standard patterns are accepted.
44+
*/
45+
maximumStandardLockingBytecodeLength: -1,
4346
/**
4447
* A.K.A. `MAX_STANDARD_TX_SIZE`
4548
*/
@@ -48,6 +51,7 @@ export const ConsensusBch2023 = {
4851
* A.K.A. `MAX_TX_IN_SCRIPT_SIG_SIZE`
4952
*/
5053
maximumStandardUnlockingBytecodeLength: 1650,
54+
maximumTokenCommitmentLength: 40,
5155
/**
5256
* A.K.A. `MAX_TX_SIZE`
5357
*/

src/lib/vm/instruction-sets/bch/2023/bch-2023-instruction-set.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -911,10 +911,20 @@ export const createInstructionSetBch2023 = <
911911

912912
// eslint-disable-next-line functional/no-loop-statements
913913
for (const [index, output] of sourceOutputs.entries()) {
914-
if (!isStandardUtxoBytecode(output.lockingBytecode)) {
914+
if (consensus.maximumStandardLockingBytecodeLength === -1) {
915+
if (!isStandardUtxoBytecode(output.lockingBytecode)) {
916+
return formatError(
917+
AuthenticationErrorCommon.verifyStandardFailedNonstandardSourceOutput,
918+
`Source output ${index} is non-standard: locking bytecode does not match a standard pattern: P2PKH, P2PK, P2SH, P2MS, or arbitrary data (OP_RETURN).`,
919+
);
920+
}
921+
} else if (
922+
output.lockingBytecode.length >
923+
consensus.maximumStandardLockingBytecodeLength
924+
) {
915925
return formatError(
916926
AuthenticationErrorCommon.verifyStandardFailedNonstandardSourceOutput,
917-
`Source output ${index} is non-standard: locking bytecode does not match a standard pattern: P2PKH, P2PK, P2SH, P2MS, or arbitrary data (OP_RETURN).`,
927+
`Source output ${index} is non-standard: locking bytecode length of ${output.lockingBytecode.length} exceeds the maximum standard locking bytecode length of ${consensus.maximumStandardLockingBytecodeLength}.`,
918928
);
919929
}
920930
}
@@ -923,21 +933,32 @@ export const createInstructionSetBch2023 = <
923933
let totalArbitraryDataBytes = 0;
924934
// eslint-disable-next-line functional/no-loop-statements
925935
for (const [index, output] of transaction.outputs.entries()) {
926-
if (!isStandardOutputBytecode(output.lockingBytecode)) {
927-
return formatError(
928-
AuthenticationErrorCommon.verifyStandardFailedNonstandardOutput,
929-
`Transaction output ${index} is non-standard: locking bytecode does not match a standard pattern: P2PKH, P2PK, P2SH, P2MS, or arbitrary data (OP_RETURN).`,
930-
);
931-
}
932-
// eslint-disable-next-line functional/no-conditional-statements
933-
if (isArbitraryDataOutput(output.lockingBytecode)) {
934-
// eslint-disable-next-line functional/no-expression-statements
935-
totalArbitraryDataBytes += output.lockingBytecode.length + 1;
936+
if (consensus.maximumStandardLockingBytecodeLength === -1) {
937+
if (!isStandardOutputBytecode(output.lockingBytecode)) {
938+
return formatError(
939+
AuthenticationErrorCommon.verifyStandardFailedNonstandardOutput,
940+
`Transaction output ${index} is non-standard: locking bytecode does not match a standard pattern: P2PKH, P2PK, P2SH, P2MS, or arbitrary data (OP_RETURN).`,
941+
);
942+
}
943+
} else if (
944+
output.lockingBytecode.length >
945+
consensus.maximumStandardLockingBytecodeLength
946+
) {
947+
// eslint-disable-next-line functional/no-conditional-statements
948+
if (isArbitraryDataOutput(output.lockingBytecode)) {
949+
// eslint-disable-next-line functional/no-expression-statements
950+
totalArbitraryDataBytes += output.lockingBytecode.length + 1;
951+
} else {
952+
return formatError(
953+
AuthenticationErrorCommon.verifyStandardFailedNonstandardOutput,
954+
`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).`,
955+
);
956+
}
936957
}
937958
if (isDustOutput(output)) {
938959
return formatError(
939960
AuthenticationErrorCommon.verifyStandardFailedDustOutput,
940-
` Transaction output ${index} must have a value of at least ${getDustThreshold(
961+
`Transaction output ${index} must have a value of at least ${getDustThreshold(
941962
output,
942963
)} satoshis. Current value: ${output.valueSatoshis}`,
943964
);
@@ -973,6 +994,9 @@ export const createInstructionSetBch2023 = <
973994
const tokenValidationResult = verifyTransactionTokens(
974995
transaction,
975996
sourceOutputs,
997+
{
998+
maximumTokenCommitmentLength: consensus.maximumTokenCommitmentLength,
999+
},
9761000
);
9771001
if (tokenValidationResult !== true) {
9781002
return tokenValidationResult;

src/lib/vm/instruction-sets/bch/2023/bch-2023-tokens.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,17 @@ export const extractTransactionOutputTokenData = (
177177
export const verifyTransactionTokens = (
178178
transaction: Transaction,
179179
sourceOutputs: Output[],
180+
{ maximumTokenCommitmentLength }: { maximumTokenCommitmentLength: number },
180181
) => {
181182
const excessiveCommitment = [...sourceOutputs, ...transaction.outputs].find(
182183
(output) =>
183184
output.token?.nft?.commitment !== undefined &&
184-
output.token.nft.commitment.length >
185-
ConsensusBch2023.maximumCommitmentLength,
185+
output.token.nft.commitment.length > maximumTokenCommitmentLength,
186186
);
187187
if (excessiveCommitment !== undefined) {
188188
return formatError(
189189
AuthenticationErrorCommon.tokenValidationExcessiveCommitmentLength,
190-
`A token commitment exceeds the consensus limit of ${
191-
ConsensusBch2023.maximumCommitmentLength
192-
} bytes. Excessive token commitment length: ${
190+
`A token commitment exceeds the consensus limit of ${maximumTokenCommitmentLength} bytes. Excessive token commitment length: ${
193191
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
194192
excessiveCommitment.token!.nft!.commitment.length
195193
}`,

src/lib/vm/instruction-sets/bch/2026/bch-2026-consensus.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { ConsensusBch2025 } from '../2025/bch-2025-consensus.js';
44
* Consensus setting overrides for the `BCH_SPEC` instruction set.
55
*/
66
// eslint-disable-next-line @typescript-eslint/naming-convention
7-
export const ConsensusBch2026Overrides = {};
7+
export const ConsensusBch2026Overrides = {
8+
maximumStandardLockingBytecodeLength: 201,
9+
maximumTokenCommitmentLength: 80,
10+
};
811

912
/**
1013
* Consensus settings for the `BCH_SPEC` instruction set.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { OpcodeDescriptionsBch2023 } from '../2023/bch-2023-descriptions.js';
2+
3+
/**
4+
* Descriptions for the opcodes added to the `BCH_2026_05` instruction set
5+
* beyond those present in `BCH_2025_05`.
6+
*/
7+
export enum OpcodeDescriptionsBch2026Additions {
8+
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.',
9+
OP_BEGIN = 'Push the current instruction pointer index to the control stack as an integer (to be read by OP_UNTIL).',
10+
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).',
11+
}
12+
13+
/**
14+
* Descriptions for the `BCH_SPEC` instruction set.
15+
*/
16+
// eslint-disable-next-line @typescript-eslint/naming-convention
17+
export const OpcodeDescriptionsBch2026 = {
18+
...OpcodeDescriptionsBch2023,
19+
...OpcodeDescriptionsBch2026Additions,
20+
};

src/lib/vm/instruction-sets/bch/2026/bch-2026-errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export enum AuthenticationErrorBch2026Additions {
44
unexpectedUntil = 'Encountered an OP_UNTIL that is not following a matching OP_BEGIN.',
55
unexpectedUntilMissingEndIf = 'Encountered an OP_UNTIL before the previous OP_IF was closed by an OP_ENDIF.',
66
excessiveLooping = 'Program attempted an OP_UNTIL operation that would exceed the limit of repeated bytes.',
7+
malformedEval = 'Program attempted to OP_EVAL malformed bytecode.',
78
}
89

910
/**
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type {
2+
AuthenticationInstructionMalformed,
3+
AuthenticationProgramStateBch2026,
4+
} from '../../../../lib.js';
5+
import {
6+
applyError,
7+
authenticationInstructionsAreMalformed,
8+
decodeAuthenticationInstructions,
9+
disassembleAuthenticationInstructionMalformed,
10+
executionIsActive,
11+
pushToControlStack,
12+
useOneStackItem,
13+
} from '../../common/common.js';
14+
15+
import { AuthenticationErrorBch2026 } from './bch-2026-errors.js';
16+
import { OpcodesBch2026 } from './bch-2026-opcodes.js';
17+
18+
export const opEval = <State extends AuthenticationProgramStateBch2026>(
19+
state: State,
20+
) => {
21+
if (executionIsActive(state)) {
22+
return useOneStackItem(state, (nextState, [item]) => {
23+
const newInstructions = decodeAuthenticationInstructions(item);
24+
25+
if (authenticationInstructionsAreMalformed(newInstructions)) {
26+
return applyError(
27+
nextState,
28+
AuthenticationErrorBch2026.malformedEval,
29+
`Malformed instruction: ${disassembleAuthenticationInstructionMalformed(
30+
OpcodesBch2026,
31+
newInstructions[
32+
newInstructions.length - 1
33+
] as AuthenticationInstructionMalformed,
34+
)}.`,
35+
);
36+
}
37+
38+
const manuallyAdvance = 1;
39+
const finalState = pushToControlStack(nextState, {
40+
instructions: nextState.instructions,
41+
ip: nextState.ip + manuallyAdvance,
42+
});
43+
finalState.ip = 0 - manuallyAdvance; // eslint-disable-line functional/no-expression-statements, functional/immutable-data
44+
finalState.instructions = newInstructions; // eslint-disable-line functional/no-expression-statements, functional/immutable-data
45+
return finalState;
46+
});
47+
}
48+
return state;
49+
};

0 commit comments

Comments
 (0)