diff --git a/packages/cashc/src/Errors.ts b/packages/cashc/src/Errors.ts index fd3551216..a4141392d 100644 --- a/packages/cashc/src/Errors.ts +++ b/packages/cashc/src/Errors.ts @@ -21,6 +21,7 @@ import { SliceNode, IntLiteralNode, TupleAssignmentNode, + ExpressionNode, } from './ast/AST.js'; import { SymbolType } from './ast/SymbolTable.js'; import { Location } from './ast/Location.js'; @@ -328,6 +329,15 @@ export class IndexOutOfBoundsError extends CashScriptError { } } +export class NullDataChunkTooLargeError extends CashScriptError { + constructor( + node: ExpressionNode, + byteLength: number, + ) { + super(node, `LockingBytecodeNullData chunks can be at most 255 bytes, but found ${byteLength} bytes`); + } +} + export class BitshiftBitcountNegativeError extends CashScriptError { constructor( node: BinaryOpNode, diff --git a/packages/cashc/src/ast/AstBuilder.ts b/packages/cashc/src/ast/AstBuilder.ts index 4362b8ab5..74a7a2566 100644 --- a/packages/cashc/src/ast/AstBuilder.ts +++ b/packages/cashc/src/ast/AstBuilder.ts @@ -566,8 +566,13 @@ export default class AstBuilder } createHexLiteral(ctx: LiteralContext): HexLiteralNode { - const hexString = ctx.HexLiteral().getText(); - const hexValue = hexToBin(hexString.substring(2)); + const hexString = ctx.HexLiteral().getText().substring(2); + + if (hexString.length % 2 !== 0) { + throw new ParseError(`Hex literal "0x${hexString}" should have an even number of digits`, Location.fromCtx(ctx)); + } + + const hexValue = hexToBin(hexString); const hexLiteral = new HexLiteralNode(hexValue); hexLiteral.location = Location.fromCtx(ctx); return hexLiteral; diff --git a/packages/cashc/src/generation/GenerateTargetTraversal.ts b/packages/cashc/src/generation/GenerateTargetTraversal.ts index ddc25171a..96912a644 100644 --- a/packages/cashc/src/generation/GenerateTargetTraversal.ts +++ b/packages/cashc/src/generation/GenerateTargetTraversal.ts @@ -3,6 +3,8 @@ import { asmToScript, encodeBool, encodeInt, + encodeNullDataPushOpcode, + encodeNullDataScript, encodeString, Op, OpOrData, @@ -61,6 +63,7 @@ import { DoWhileNode, WhileNode, ForNode, + ExpressionNode, } from '../ast/AST.js'; import AstTraversal from '../ast/AstTraversal.js'; import { GlobalFunction, Class } from '../ast/Globals.js'; @@ -72,7 +75,7 @@ import { compileTimeOp, compileUnaryOp, } from './utils.js'; -import { isNumericType } from '../utils.js'; +import { getCompileTimeBytes, isNumericType } from '../utils.js'; import { collectFunctionCalls, isRecursive, shouldInline } from './inlining.js'; import type { InternalCompilerOptions } from '../compiler.js'; @@ -898,44 +901,12 @@ export default class GenerateTargetTraversal extends AstTraversal { this.emit(Op.OP_CAT, { location: node.location, positionHint: PositionHint.END }); this.popFromStack(2); } else if (node.identifier.name === Class.LOCKING_BYTECODE_NULLDATA) { - // Total script = OP_RETURN ( )+ + // Total script = OP_RETURN ( )+ // OP_RETURN this.emit(hexToBin('6a'), { location: node.location, positionHint: PositionHint.START }); this.pushToStack('(value)'); const { elements } = node.parameters[0] as ArrayNode; - // - elements.forEach((element) => { - this.visit(element); - - // The element comes first, then all other opcodes have PositionHint.END because they come after the element - const elementLocationData = { location: element.location, positionHint: PositionHint.END }; - - // Push the element's size (and calculate VarInt) - this.emit(Op.OP_SIZE, elementLocationData); - if (element instanceof HexLiteralNode) { - // If the argument is a literal, we know its size - if (element.value.byteLength > 75) { - this.emit(hexToBin('4c'), elementLocationData); - this.emit(Op.OP_SWAP, elementLocationData); - this.emit(Op.OP_CAT, elementLocationData); - } - } else { - // If the argument is not a literal, the script needs to check size - this.emit(Op.OP_DUP, elementLocationData); - this.emit(encodeInt(75n), elementLocationData); - this.emit(Op.OP_GREATERTHAN, elementLocationData); - this.emit(Op.OP_IF, elementLocationData); - this.emit(hexToBin('4c'), elementLocationData); - this.emit(Op.OP_SWAP, elementLocationData); - this.emit(Op.OP_CAT, elementLocationData); - this.emit(Op.OP_ENDIF, elementLocationData); - } - // Concat size and arguments - this.emit(Op.OP_SWAP, elementLocationData); - this.emit(Op.OP_CAT, elementLocationData); - this.emit(Op.OP_CAT, elementLocationData); - this.popFromStack(); - }); + elements.forEach((element) => this.emitNullDataChunk(element)); this.popFromStack(); } else { throw new Error(); // Should not happen @@ -946,6 +917,60 @@ export default class GenerateTargetTraversal extends AstTraversal { return node; } + // Appends to the OP_RETURN script on top of the stack. The push opcode has to match + // encodeNullDataScript(): OP_PUSHBYTES_N for 1-75 bytes, and OP_PUSHDATA1 N for 0 or 76-255 bytes + private emitNullDataChunk(element: ExpressionNode): void { + const startLocationData = { location: element.location, positionHint: PositionHint.START }; + const endLocationData = { location: element.location, positionHint: PositionHint.END }; + + // Chunks with a value known at compile time are pushed together with their push opcode + // (encodeNullDataScript() without an OP_RETURN encodes just the chunk, exactly like the SDK does) + const compileTimeValue = getCompileTimeBytes(element); + if (compileTimeValue !== undefined) { + this.emit(encodeNullDataScript([compileTimeValue]), startLocationData); + this.emit(Op.OP_CAT, endLocationData); + return; + } + + // Chunks with a length known at compile time (e.g. bytes20) are preceded by a constant push opcode + if (element.type instanceof BytesType && element.type.bound !== undefined) { + this.emit(encodeNullDataPushOpcode(element.type.bound), startLocationData); + this.pushToStack('(value)'); + this.visit(element); + this.emit(Op.OP_CAT, endLocationData); + this.emit(Op.OP_CAT, endLocationData); + this.popFromStack(2); + return; + } + + // Otherwise the push opcode is computed from the chunk's size at runtime + this.visit(element); + + // OP_SIZE is a signed VM number, so it is empty for empty elements, and has an extra 0x00 byte for 128-255 bytes. + // In those cases (and for 76-127 bytes) we convert it to a single byte, and prepend OP_PUSHDATA1. + this.emit(Op.OP_SIZE, endLocationData); + this.emit(Op.OP_DUP, endLocationData); + this.emit(encodeInt(1n), endLocationData); + this.emit(encodeInt(76n), endLocationData); + this.emit(Op.OP_WITHIN, endLocationData); + this.emit(Op.OP_NOTIF, endLocationData); + this.emit(encodeInt(2n), endLocationData); + this.emit(Op.OP_NUM2BIN, endLocationData); + this.emit(encodeInt(1n), endLocationData); + this.emit(Op.OP_SPLIT, endLocationData); + this.emit(Op.OP_DROP, endLocationData); + this.emit(hexToBin('4c'), endLocationData); + this.emit(Op.OP_SWAP, endLocationData); + this.emit(Op.OP_CAT, endLocationData); + this.emit(Op.OP_ENDIF, endLocationData); + + // Concat push opcode and chunk to the OP_RETURN script + this.emit(Op.OP_SWAP, endLocationData); + this.emit(Op.OP_CAT, endLocationData); + this.emit(Op.OP_CAT, endLocationData); + this.popFromStack(); + } + visitTupleIndexOp(node: TupleIndexOpNode): Node { node.tuple = this.visit(node.tuple); diff --git a/packages/cashc/src/semantic/EnsureFinalRequireTraversal.ts b/packages/cashc/src/semantic/EnsureFinalRequireTraversal.ts index bc474c3dc..88d63f3ff 100644 --- a/packages/cashc/src/semantic/EnsureFinalRequireTraversal.ts +++ b/packages/cashc/src/semantic/EnsureFinalRequireTraversal.ts @@ -53,9 +53,6 @@ export default class EnsureFinalRequireTraversal extends AstTraversal { } } -// TODO: This code is a bit convoluted, but we're likely to make changes to allow early returns before a mainline release, -// so we're leaving this code as-is for now. - function ensureSingleTailReturn(body: BlockNode): void { const statements = body.statements ?? []; const finalStatement = statements[statements.length - 1]; diff --git a/packages/cashc/src/semantic/TypeCheckTraversal.ts b/packages/cashc/src/semantic/TypeCheckTraversal.ts index b76a75db4..8249cf111 100644 --- a/packages/cashc/src/semantic/TypeCheckTraversal.ts +++ b/packages/cashc/src/semantic/TypeCheckTraversal.ts @@ -52,13 +52,14 @@ import { ArrayElementError, IndexOutOfBoundsError, BitshiftBitcountNegativeError, + NullDataChunkTooLargeError, UnusedFunctionReturnError, ReturnTypeError, } from '../Errors.js'; import { BinaryOperator, NullaryOperator, UnaryOperator } from '../ast/Operator.js'; -import { GlobalFunction } from '../ast/Globals.js'; +import { Class, GlobalFunction } from '../ast/Globals.js'; import { Symbol } from '../ast/SymbolTable.js'; -import { functionReturnType, resultingTypeForBinaryOp } from '../utils.js'; +import { functionReturnType, getCompileTimeByteLength, resultingTypeForBinaryOp } from '../utils.js'; export default class TypeCheckTraversal extends AstTraversal { private currentFunctionReturnTypes: Type[] = []; @@ -79,7 +80,10 @@ export default class TypeCheckTraversal extends AstTraversal { visitTupleAssignment(node: TupleAssignmentNode): Node { node.tuple = this.visit(node.tuple); - const targetsType = new TupleType(node.targets.map((target) => target.type!)); + // Reassigned variables use their current type, which may have been narrowed by a preceding x.length == N check + const targetsType = new TupleType(node.targets.map((target) => ( + target.isReassignment ? target.identifier.symbol!.type : target.type! + ))); if (!implicitlyCastable(node.tuple.type, targetsType)) { const targetNames = node.targets.map((target) => target.identifier.name).join(', '); const syntheticAssignment = new VariableDefinitionNode(targetsType, [], targetNames, node.tuple); @@ -263,6 +267,10 @@ export default class TypeCheckTraversal extends AstTraversal { const parameterTypes = node.parameters.map((p) => p.type!); expectParameters(node, parameterTypes, symbol.parameters); + if (node.identifier.name === Class.LOCKING_BYTECODE_NULLDATA) { + expectNullDataChunkSizes(node.parameters[0] as ArrayNode); + } + node.type = type; return node; } @@ -511,6 +519,13 @@ function expectTuple(node: ExpectedNode, actual?: Type): void { } } +function expectNullDataChunkSizes(chunks: ArrayNode): void { + chunks.elements.forEach((chunk) => { + const byteLength = getCompileTimeByteLength(chunk); + if (byteLength !== undefined && byteLength > 255) throw new NullDataChunkTooLargeError(chunk, byteLength); + }); +} + type AssigningNode = AssignNode | VariableDefinitionNode | ConstantDefinitionNode; function expectAssignable(node: AssigningNode, actual?: Type, expected?: Type): void { if (!implicitlyCastable(actual, expected)) { diff --git a/packages/cashc/src/utils.ts b/packages/cashc/src/utils.ts index 6c46e1c85..e740bb46c 100644 --- a/packages/cashc/src/utils.ts +++ b/packages/cashc/src/utils.ts @@ -1,4 +1,5 @@ -import { BytesType, implicitlyCastable, PrimitiveType, TupleType, Type } from '@cashscript/utils'; +import { BytesType, encodeString, implicitlyCastable, PrimitiveType, TupleType, Type } from '@cashscript/utils'; +import { CastNode, ExpressionNode, HexLiteralNode, StringLiteralNode } from './ast/AST.js'; import { BinaryOperator } from './ast/Operator.js'; export function functionReturnType(returnTypes?: Type[]): Type { @@ -26,3 +27,16 @@ export function resultingTypeForBinaryOp( export function isNumericType(type?: Type): boolean { return type === PrimitiveType.INT || type === PrimitiveType.BOOL; } + +// The value of a bytes expression if it is known at compile time: (casts of) hex and string literals +export function getCompileTimeBytes(node: ExpressionNode): Uint8Array | undefined { + if (node instanceof HexLiteralNode) return node.value; + if (node instanceof StringLiteralNode) return encodeString(node.value); + if (node instanceof CastNode && node.type instanceof BytesType) return getCompileTimeBytes(node.expression); + return undefined; +} + +// The length of a bytes expression if it is known at compile time, from its value or its type (e.g. bytes20) +export function getCompileTimeByteLength(node: ExpressionNode): number | undefined { + return getCompileTimeBytes(node)?.byteLength ?? (node.type instanceof BytesType ? node.type.bound : undefined); +} diff --git a/packages/cashc/test/compiler/AssignTypeError/narrowing_tuple_reassignment.cash b/packages/cashc/test/compiler/AssignTypeError/narrowing_tuple_reassignment.cash new file mode 100644 index 000000000..a5a09616b --- /dev/null +++ b/packages/cashc/test/compiler/AssignTypeError/narrowing_tuple_reassignment.cash @@ -0,0 +1,8 @@ +contract Test() { + function hello(bytes data, bytes other) { + require(data.length == 20); + data, bytes rest = other.split(5); + bytes20 narrowed = data; + require(narrowed == data && rest.length > 0); + } +} diff --git a/packages/cashc/test/compiler/InvalidParameterTypeError/ripemd160_too_many_args.cash b/packages/cashc/test/compiler/InvalidParameterTypeError/ripemd160_too_many_args.cash index cd25c8a99..d8a7d6553 100644 --- a/packages/cashc/test/compiler/InvalidParameterTypeError/ripemd160_too_many_args.cash +++ b/packages/cashc/test/compiler/InvalidParameterTypeError/ripemd160_too_many_args.cash @@ -1,5 +1,5 @@ contract Test() { function hello(bool b) { - require(ripemd160(true, b) == 0x0); + require(ripemd160(true, b) == 0x00); } } diff --git a/packages/cashc/test/compiler/NullDataChunkTooLargeError/bounded_chunk_too_large.cash b/packages/cashc/test/compiler/NullDataChunkTooLargeError/bounded_chunk_too_large.cash new file mode 100644 index 000000000..3f0d1f0c1 --- /dev/null +++ b/packages/cashc/test/compiler/NullDataChunkTooLargeError/bounded_chunk_too_large.cash @@ -0,0 +1,5 @@ +contract Test() { + function spend(bytes256 data) { + require(tx.outputs[0].lockingBytecode == new LockingBytecodeNullData([0x6d02, data])); + } +} diff --git a/packages/cashc/test/compiler/NullDataChunkTooLargeError/literal_chunk_too_large.cash b/packages/cashc/test/compiler/NullDataChunkTooLargeError/literal_chunk_too_large.cash new file mode 100644 index 000000000..522ffc135 --- /dev/null +++ b/packages/cashc/test/compiler/NullDataChunkTooLargeError/literal_chunk_too_large.cash @@ -0,0 +1,5 @@ +contract Test() { + function spend() { + require(tx.outputs[0].lockingBytecode == new LockingBytecodeNullData([0x6d02, 0xabababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab])); + } +} diff --git a/packages/cashc/test/compiler/NullDataChunkTooLargeError/string_chunk_too_large.cash b/packages/cashc/test/compiler/NullDataChunkTooLargeError/string_chunk_too_large.cash new file mode 100644 index 000000000..ee45c6570 --- /dev/null +++ b/packages/cashc/test/compiler/NullDataChunkTooLargeError/string_chunk_too_large.cash @@ -0,0 +1,5 @@ +contract Test() { + function spend() { + require(tx.outputs[0].lockingBytecode == new LockingBytecodeNullData([0x6d02, bytes('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')])); + } +} diff --git a/packages/cashc/test/compiler/ParseError/incorrect_nested_comment.cash b/packages/cashc/test/compiler/ParseError/incorrect_nested_comment.cash index efc42704e..60dc35c7d 100644 --- a/packages/cashc/test/compiler/ParseError/incorrect_nested_comment.cash +++ b/packages/cashc/test/compiler/ParseError/incorrect_nested_comment.cash @@ -6,7 +6,7 @@ contract Test(int x, string y) { // Line comments are a thing function hello(sig s, pubkey pk) { int i = 400 + x; - bytes b = 0x07364897987fe87 + bytes(x); + bytes b = 0x07364897987fe807 + bytes(x); int myVariable = 10 - 4; // they can go at the end of the line int myOtherVariable = i + myVariable % 2; diff --git a/packages/cashc/test/compiler/ParseError/odd_length_hex_literal.cash b/packages/cashc/test/compiler/ParseError/odd_length_hex_literal.cash new file mode 100644 index 000000000..c72fc6d60 --- /dev/null +++ b/packages/cashc/test/compiler/ParseError/odd_length_hex_literal.cash @@ -0,0 +1,6 @@ +contract Test() { + function test() { + bytes b = 0x123; + require(b == 0x1203); + } +} diff --git a/packages/cashc/test/compiler/ParseError/unterminated_comment.cash b/packages/cashc/test/compiler/ParseError/unterminated_comment.cash index 99c31b467..44c550823 100644 --- a/packages/cashc/test/compiler/ParseError/unterminated_comment.cash +++ b/packages/cashc/test/compiler/ParseError/unterminated_comment.cash @@ -6,7 +6,7 @@ contract Test(int x, string y) { // Line comments are a thing function hello(sig s, pubkey pk) { int i = 400 + x; - bytes b = 0x07364897987fe87 + bytes(x); + bytes b = 0x07364897987fe807 + bytes(x); int myVariable = 10 - 4; // they can go at the end of the line int myOtherVariable = i + myVariable % 2; diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/announcement.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/announcement.ts index 07ff49c81..676e7d324 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/announcement.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/announcement.ts @@ -8,11 +8,10 @@ export const fixtures: Fixture[] = [ abi: [{ name: 'announce', inputs: [] }], bytecode: // bytes announcement = new LockingBytecodeNullData(...) - '6a 6d02 OP_SIZE OP_SWAP OP_CAT OP_CAT ' - + '4120636f6e7472616374206d6179206e6f7420696e6a75726520612068756d616e20626' + '6a 026d02 OP_CAT ' + + '4c624120636f6e7472616374206d6179206e6f7420696e6a75726520612068756d616e20626' + '5696e67206f722c207468726f75676820696e616374696f6e2c20616c6c6f77206120687' - + '56d616e206265696e6720746f20636f6d6520746f206861726d2e ' - + 'OP_SIZE OP_DUP 4b OP_GREATERTHAN OP_IF 4c OP_SWAP OP_CAT OP_ENDIF OP_SWAP OP_CAT OP_CAT ' + + '56d616e206265696e6720746f20636f6d6520746f206861726d2e OP_CAT ' // require(tx.outputs[0].value == 0) + 'OP_0 OP_OUTPUTVALUE OP_0 OP_NUMEQUALVERIFY ' // require(tx.outputs[0].lockingBytecode == announcement) @@ -32,17 +31,17 @@ export const fixtures: Fixture[] = [ // Stack clean-up + 'OP_DROP OP_1', debug: { - bytecode: '016a026d02827c7e7e4c624120636f6e7472616374206d6179206e6f7420696e6a75726520612068756d616e206265696e67206f722c207468726f75676820696e616374696f6e2c20616c6c6f7720612068756d616e206265696e6720746f20636f6d6520746f206861726d2e8276014ba063014c7c7e687c7e7e00cc009d00cd8802e803c0c67894767ba26351cdc0c78851cc789d687551', + bytecode: '016a03026d027e4c644c624120636f6e7472616374206d6179206e6f7420696e6a75726520612068756d616e206265696e67206f722c207468726f75676820696e616374696f6e2c20616c6c6f7720612068756d616e206265696e6720746f20636f6d6520746f206861726d2e7e00cc009d00cd8802e803c0c67894767ba26351cdc0c78851cc789d687551', logs: [], requires: [ - { ip: 22, line: 16 }, - { ip: 25, line: 17 }, - { ip: 39, line: 24 }, - { ip: 43, line: 25 }, + { ip: 8, line: 16 }, + { ip: 11, line: 17 }, + { ip: 25, line: 24 }, + { ip: 29, line: 25 }, ], - sourceMap: '10:29:13:10;11:12:11:18;::::1;;;;12:18:12:118:0;:12::119:1;;;;;;;;;;;;16:27:16:28:0;:16::35:1;:39::40:0;:8::42:1;17:27:17:28:0;:16::45:1;:8::63;21:23:21:27:0;22:37:22:58;:27::65:1;:68::76:0;:27:::1;23:12:23:24:0;:28::36;:12:::1;:38:26:9:0;24:31:24:32;:20::49:1;:63::84:0;:53::101:1;:12::103;25:31:25:32:0;:20::39:1;:43::55:0;:12::57:1;23:38:26:9;8:24:27:5;', + sourceMap: '10:29:13:10;11:12:11:18;::::1;12::12:119:0;::::1;16:27:16:28:0;:16::35:1;:39::40:0;:8::42:1;17:27:17:28:0;:16::45:1;:8::63;21:23:21:27:0;22:37:22:58;:27::65:1;:68::76:0;:27:::1;23:12:23:24:0;:28::36;:12:::1;:38:26:9:0;24:31:24:32;:20::49:1;:63::84:0;:53::101:1;:12::103;25:31:25:32:0;:20::39:1;:43::55:0;:12::57:1;23:38:26:9;8:24:27:5;', }, - fingerprint: '542596767034cea0f3a5933a1efa49bb9aa569ec70c5f0261afd36ea845fa746', + fingerprint: '9e756de2e118935481a7a2fec9d5a375a05ffb0a4031c6567d4d603f74650a87', }, }, ]; diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/comments.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/comments.ts index 135d2a764..597cfdf23 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/comments.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/comments.ts @@ -9,7 +9,7 @@ export const fixtures: Fixture[] = [ bytecode: // int i = 400 + x; '9001 OP_OVER OP_ADD ' - // bytes b = 0x07364897987fe87 + bytes(x); + // bytes b = 0x07364897987fe807 + bytes(x); + '07364897987fe807 OP_2 OP_PICK OP_CAT ' // int myVariable = 10 - 4; // they can go at the end of the line + 'OP_10 OP_4 OP_SUB ' @@ -30,7 +30,7 @@ export const fixtures: Fixture[] = [ fingerprint: '63ee25992293cf9dd8752923c17b8a78840ecea53d75e814643cbb2ac914c751', debug: { bytecode: '02900178930807364897987fe80752797e5a549452797c5297935279a0697b5aa0637801149f69707cad67765188686d6d51', - sourceMap: '9:16:9:19;:22::23;:16:::1;10:18:10:35:0;:44::45;;:18::46:1;12:25:12:27:0;:30::31;:25:::1;13:30:13::0;;:34::44;:47::48;:34:::1;:30;14:82:14:83:0;;:16:::1;:8::85;16:12:16:13:0;:16::18;:12:::1;:20:19:9:0;17::17:21;:24::26;:20:::1;:12::28;18:29:18:34:0;;:12::37:1;20::20:31:0;:20::21;:25::29;:12::31:1;;8:37:21:5;;', + sourceMap: '9:16:9:19;:22::23;:16:::1;10:18:10:36:0;:45::46;;:18::47:1;12:25:12:27:0;:30::31;:25:::1;13:30:13::0;;:34::44;:47::48;:34:::1;:30;14:82:14:83:0;;:16:::1;:8::85;16:12:16:13:0;:16::18;:12:::1;:20:19:9:0;17::17:21;:24::26;:20:::1;:12::28;18:29:18:34:0;;:12::37:1;20::20:31:0;:20::21;:25::29;:12::31:1;;8:37:21:5;;', logs: [], requires: [{ ip: 20, line: 14 }, { ip: 28, line: 17 }, { ip: 31, line: 18 }, { ip: 35, line: 20 }], }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/covenant_all_fields.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/covenant_all_fields.ts index 6b920f1da..a673a8205 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/covenant_all_fields.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/covenant_all_fields.ts @@ -37,13 +37,13 @@ export const fixtures: Fixture[] = [ + 'OP_0 OP_OUTPUTVALUE 1027 OP_NUMEQUALVERIFY ' // require(tx.outputs[0].lockingBytecode.length == 100) + 'OP_0 OP_OUTPUTBYTECODE OP_SIZE OP_NIP 64 OP_NUMEQUALVERIFY ' - // require(tx.inputs[0].tokenCategory == 0x000000000000000000000000000000000000000000000000000000000000000) + // require(tx.inputs[0].tokenCategory == 0x0000000000000000000000000000000000000000000000000000000000000000) + 'OP_0 OP_UTXOTOKENCATEGORY 0000000000000000000000000000000000000000000000000000000000000000 OP_EQUALVERIFY ' // require(tx.inputs[0].nftCommitment == 0x00); + 'OP_0 OP_UTXOTOKENCOMMITMENT 00 OP_EQUALVERIFY ' // require(tx.inputs[0].tokenAmount == 100); + 'OP_0 OP_UTXOTOKENAMOUNT 64 OP_NUMEQUALVERIFY ' - // require(tx.outputs[0].tokenCategory == 0x000000000000000000000000000000000000000000000000000000000000000) + // require(tx.outputs[0].tokenCategory == 0x0000000000000000000000000000000000000000000000000000000000000000) + 'OP_0 OP_OUTPUTTOKENCATEGORY 0000000000000000000000000000000000000000000000000000000000000000 OP_EQUALVERIFY ' // require(tx.outputs[0].nftCommitment == 0x00); + 'OP_0 OP_OUTPUTTOKENCOMMITMENT 00 OP_EQUALVERIFY ' @@ -79,7 +79,7 @@ export const fixtures: Fixture[] = [ { ip: 80, line: 21 }, { ip: 85, line: 22 }, ], - sourceMap: '2:21:2:21;::::1;;3:16:3:26:0;:30::31;:8::33:1;4:16:4:27:0;:31::32;:8::34:1;5:16:5:32:0;:36::37;:8::39:1;6:16:6:33:0;:37::38;:8::40:1;7:16:7:37:0;:41::42;:8::44:1;8:16:8:35:0;:::42:1;;:46::49:0;:8::51:1;9:26:9:27:0;:16::34:1;:38::43:0;:8::45:1;10:26:10:27:0;:16::44:1;:::51;;:55::60:0;:8::62:1;11:26:11:27:0;:16::52:1;:56::121:0;:8::123:1;12:26:12:27:0;:16::42:1;:46::47:0;:8::49:1;13:26:13:27:0;:16::46:1;:::53;;:57::60:0;:8::62:1;14:26:14:27:0;:16::43:1;:47::48:0;:8::50:1;15:27:15:28:0;:16::35:1;:39::44:0;:8::46:1;16:27:16:28:0;:16::45:1;:::52;;:56::59:0;:8::61:1;17:26:17:27:0;:16::42:1;:46::111:0;:8::113:1;18:26:18:27:0;:16::42:1;:46::50:0;:8::52:1;19:26:19:27:0;:16::40:1;:44::47:0;:8::49:1;20:27:20:28:0;:16::43:1;:47::112:0;:8::114:1;21:27:21:28:0;:16::43:1;:47::51:0;:8::53:1;22:27:22:28:0;:16::41:1;:45::48:0;:8::50:1', + sourceMap: '2:21:2:21;::::1;;3:16:3:26:0;:30::31;:8::33:1;4:16:4:27:0;:31::32;:8::34:1;5:16:5:32:0;:36::37;:8::39:1;6:16:6:33:0;:37::38;:8::40:1;7:16:7:37:0;:41::42;:8::44:1;8:16:8:35:0;:::42:1;;:46::49:0;:8::51:1;9:26:9:27:0;:16::34:1;:38::43:0;:8::45:1;10:26:10:27:0;:16::44:1;:::51;;:55::60:0;:8::62:1;11:26:11:27:0;:16::52:1;:56::122:0;:8::124:1;12:26:12:27:0;:16::42:1;:46::47:0;:8::49:1;13:26:13:27:0;:16::46:1;:::53;;:57::60:0;:8::62:1;14:26:14:27:0;:16::43:1;:47::48:0;:8::50:1;15:27:15:28:0;:16::35:1;:39::44:0;:8::46:1;16:27:16:28:0;:16::45:1;:::52;;:56::59:0;:8::61:1;17:26:17:27:0;:16::42:1;:46::112:0;:8::114:1;18:26:18:27:0;:16::42:1;:46::50:0;:8::52:1;19:26:19:27:0;:16::40:1;:44::47:0;:8::49:1;20:27:20:28:0;:16::43:1;:47::113:0;:8::115:1;21:27:21:28:0;:16::43:1;:47::51:0;:8::53:1;22:27:22:28:0;:16::41:1;:45::48:0;:8::50:1', sourceTags: '0:2:lg', }, fingerprint: '371d30dbd28672395a164baee67b27ad86454fa53daccdb7a770a7916902f607', diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/everything.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/everything.ts index 8fd84b618..30c7083c9 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/everything.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/everything.ts @@ -9,7 +9,7 @@ export const fixtures: Fixture[] = [ bytecode: // int i = 400 + x; '9001 OP_OVER OP_ADD ' - // bytes b = 0x07364897987fe87 + bytes(y); + // bytes b = 0x07364897987fe807 + bytes(y); + '07364897987fe807 OP_3 OP_PICK OP_CAT ' // int myVariable = 10 - int(false); // they can go at the end of the line + 'OP_10 OP_0 OP_SUB ' @@ -50,7 +50,7 @@ export const fixtures: Fixture[] = [ fingerprint: 'db741271dde641b395ed7d3f282179c607d56f29b2ba7cddff3b7670a24072e2', debug: { bytecode: '02900178930807364897987fe80753797e5a0094527978935297765479a06953795a547aa678a68802f401b275567a8277018a9f695479029001a063029001557a757c6b7c6b7b7b6c6c6855795aa063547901149f6956795879ad675579559f6300696776519d68686d6d6d6d51', - sourceMap: '9:16:9:19;:22::23;:16:::1;10:18:10:35:0;:44::45;;:18::46:1;12:25:12:27:0;:34::39;:25::40:1;13:31:13:32:0;;:35::45;:31:::1;:49::50:0;:30:::1;14:16:14:31:0;:82::83;;:16:::1;:8::85;16:26:16:27:0;;17:21:17:23;19:26:19:27;;:16::28:1;:48::58:0;:32::60:1;:8::62;20:28:20:31:0;:8::33:1;;21:16:21:17:0;;:::24:1;;:27::30:0;:16:::1;:8::32;23:12:23:13:0;;:16::19;:12:::1;:21:25:9:0;24:16:24:19;:12::20:1;;;;;;;;;;;23:21:25:9;27:12:27:13:0;;:16::18;:12:::1;:20:30:9:0;28::28:21;;:24::26;:20:::1;:12::28;29:29:29:30:0;;:32::34;;:12::37:1;30:15:33::0;:19:30:20;;:23::24;:19:::1;:26:32:9:0;31:20:31:25;:12::27:1;33::33:37:0;:20::30;:34::35;:12::37:1;;30:15;8:37:34:5;;;;', + sourceMap: '9:16:9:19;:22::23;:16:::1;10:18:10:36:0;:45::46;;:18::47:1;12:25:12:27:0;:34::39;:25::40:1;13:31:13:32:0;;:35::45;:31:::1;:49::50:0;:30:::1;14:16:14:31:0;:82::83;;:16:::1;:8::85;16:26:16:27:0;;17:21:17:23;19:26:19:27;;:16::28:1;:48::58:0;:32::60:1;:8::62;20:28:20:31:0;:8::33:1;;21:16:21:17:0;;:::24:1;;:27::30:0;:16:::1;:8::32;23:12:23:13:0;;:16::19;:12:::1;:21:25:9:0;24:16:24:19;:12::20:1;;;;;;;;;;;23:21:25:9;27:12:27:13:0;;:16::18;:12:::1;:20:30:9:0;28::28:21;;:24::26;:20:::1;:12::28;29:29:29:30:0;;:32::34;;:12::37:1;30:15:33::0;:19:30:20;;:23::24;:19:::1;:26:32:9:0;31:20:31:25;:12::27:1;33::33:37:0;:20::30;:34::35;:12::37:1;;30:15;8:37:34:5;;;;', logs: [], requires: [ { ip: 22, line: 14 }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/locking_bytecode_nulldata_chunks.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/locking_bytecode_nulldata_chunks.ts new file mode 100644 index 000000000..0e947b97a --- /dev/null +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/locking_bytecode_nulldata_chunks.ts @@ -0,0 +1,39 @@ +import { Fixture } from '../../fixture-utils.js'; + +export const fixtures: Fixture[] = [ + { + artifact: { + contractName: 'LockingBytecodeNullDataChunks', + constructorInputs: [], + abi: [ + { name: 'spend', inputs: [{ name: 'pkh', type: 'bytes20' }, { name: 'data', type: 'bytes' }] }, + ], + bytecode: + // (parameter validation for bytes20 pkh) + 'OP_SIZE 14 OP_EQUALVERIFY ' + // require(tx.outputs[0].lockingBytecode == new LockingBytecodeNullData([ + + 'OP_0 OP_OUTPUTBYTECODE 6a ' + // 0x6d02 (known value) + + '026d02 OP_CAT ' + // bytes('memo') (known value) + + '046d656d6f OP_CAT ' + // pkh (known length) + + '14 OP_3 OP_ROLL OP_CAT OP_CAT ' + // data (unknown length) + + 'OP_ROT OP_SIZE OP_DUP OP_1 4c OP_WITHIN OP_NOTIF OP_2 OP_NUM2BIN OP_1 OP_SPLIT OP_DROP 4c OP_SWAP OP_CAT OP_ENDIF ' + + 'OP_SWAP OP_CAT OP_CAT ' + // ])); + + 'OP_EQUAL', + debug: { + bytecode: '8201148800cd016a03026d027e05046d656d6f7e0114537a7e7e7b827651014ca5645280517f75014c7c7e687c7e7e87', + sourceMap: '2:19:2:30;;;4:27:4:28;:16::45:1;:49::112:0;:78::84;::::1;:86::99:0;::::1;:101::104:0;;;::::1;;:106::110:0;::::1;;;;;;;;;;;;;;;;;;:8::114', + logs: [], + requires: [ + { ip: 35, line: 4 }, + ], + sourceTags: '0:2:pv', + }, + fingerprint: '334d3b680b5a80af6ff0a2158f339b6edfd530828669f34b06e7c9aa41556126', + }, + }, +]; diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment_narrowing.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment_narrowing.ts new file mode 100644 index 000000000..20d1cb7b8 --- /dev/null +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment_narrowing.ts @@ -0,0 +1,35 @@ +import { Fixture } from '../../fixture-utils.js'; + +export const fixtures: Fixture[] = [ + { + artifact: { + contractName: 'TupleReassignmentNarrowing', + constructorInputs: [], + abi: [ + { name: 'spend', inputs: [{ name: 'data', type: 'bytes' }, { name: 'other', type: 'bytes' }] }, + ], + bytecode: + // require(data.length == 20); + 'OP_DUP OP_SIZE OP_NIP 14 OP_NUMEQUALVERIFY ' + // data, bytes rest = other.split(20); + + 'OP_SWAP 14 OP_SPLIT ' + // bytes20 narrowed = data; + + 'OP_OVER ' + // require(narrowed == data && rest.length > 0); + + 'OP_ROT OP_EQUAL OP_SWAP OP_SIZE OP_NIP OP_0 OP_GREATERTHAN OP_BOOLAND ' + // Cleanup + + 'OP_NIP', + debug: { + bytecode: '76827701149d7c01147f787b877c827700a09a77', + sourceMap: '3:16:3:20;:::27:1;;:31::33:0;:8::35:1;5:27:5:32:0;:39::41;:27::42:1;6::6:31:0;7:28:7:32;:16:::1;:36::40:0;:::47:1;;:50::51:0;:36:::1;:8::53;2:44:8:5', + logs: [], + requires: [ + { ip: 4, line: 3 }, + { ip: 17, line: 7 }, + ], + sourceTags: '17:17:sc', + }, + fingerprint: '1a51d44cecfd534a1332d599fee5890353c16bc9bcb4514ab7f46a86747640d8', + }, + }, +]; diff --git a/packages/cashc/test/valid-contract-files/comments.cash b/packages/cashc/test/valid-contract-files/comments.cash index 8ea8705af..6d6bf477a 100644 --- a/packages/cashc/test/valid-contract-files/comments.cash +++ b/packages/cashc/test/valid-contract-files/comments.cash @@ -7,7 +7,7 @@ contract Test(int x) { // Line comments are a thing function hello(sig s, pubkey pk) { int i = 400 + x; - bytes b = 0x07364897987fe87 + bytes(x); + bytes b = 0x07364897987fe807 + bytes(x); int myVariable = 10 - 4; // they can go at the end of the line int myOtherVariable = i + myVariable % 2; diff --git a/packages/cashc/test/valid-contract-files/covenant_all_fields.cash b/packages/cashc/test/valid-contract-files/covenant_all_fields.cash index 442c1ff34..fce70514a 100644 --- a/packages/cashc/test/valid-contract-files/covenant_all_fields.cash +++ b/packages/cashc/test/valid-contract-files/covenant_all_fields.cash @@ -8,16 +8,16 @@ contract Covenant() { require(this.activeBytecode.length == 300); require(tx.inputs[0].value == 10000); require(tx.inputs[0].lockingBytecode.length == 10000); - require(tx.inputs[0].outpointTransactionHash == 0x000000000000000000000000000000000000000000000000000000000000000); + require(tx.inputs[0].outpointTransactionHash == 0x0000000000000000000000000000000000000000000000000000000000000000); require(tx.inputs[0].outpointIndex == 0); require(tx.inputs[0].unlockingBytecode.length == 100); require(tx.inputs[0].sequenceNumber == 0); require(tx.outputs[0].value == 10000); require(tx.outputs[0].lockingBytecode.length == 100); - require(tx.inputs[0].tokenCategory == 0x000000000000000000000000000000000000000000000000000000000000000); + require(tx.inputs[0].tokenCategory == 0x0000000000000000000000000000000000000000000000000000000000000000); require(tx.inputs[0].nftCommitment == 0x00); require(tx.inputs[0].tokenAmount == 100); - require(tx.outputs[0].tokenCategory == 0x000000000000000000000000000000000000000000000000000000000000000); + require(tx.outputs[0].tokenCategory == 0x0000000000000000000000000000000000000000000000000000000000000000); require(tx.outputs[0].nftCommitment == 0x00); require(tx.outputs[0].tokenAmount == 100); } diff --git a/packages/cashc/test/valid-contract-files/everything.cash b/packages/cashc/test/valid-contract-files/everything.cash index 63dab05c2..256ec8693 100644 --- a/packages/cashc/test/valid-contract-files/everything.cash +++ b/packages/cashc/test/valid-contract-files/everything.cash @@ -7,7 +7,7 @@ contract Test(int x, string y) { // Line comments are a thing function hello(sig s, pubkey pk) { int i = 400 + x; - bytes b = 0x07364897987fe87 + bytes(y); + bytes b = 0x07364897987fe807 + bytes(y); int myVariable = 10 - int(false); // they can go at the end of the line int myOtherVariable = (i + myVariable) % 2; diff --git a/packages/cashc/test/valid-contract-files/locking_bytecode_nulldata_chunks.cash b/packages/cashc/test/valid-contract-files/locking_bytecode_nulldata_chunks.cash new file mode 100644 index 000000000..64e7ed7c4 --- /dev/null +++ b/packages/cashc/test/valid-contract-files/locking_bytecode_nulldata_chunks.cash @@ -0,0 +1,6 @@ +contract LockingBytecodeNullDataChunks() { + function spend(bytes20 pkh, bytes data) { + // A hex literal, a string literal, a chunk with a known length and a chunk with an unknown length + require(tx.outputs[0].lockingBytecode == new LockingBytecodeNullData([0x6d02, bytes('memo'), pkh, data])); + } +} diff --git a/packages/cashc/test/valid-contract-files/tuple_reassignment_narrowing.cash b/packages/cashc/test/valid-contract-files/tuple_reassignment_narrowing.cash new file mode 100644 index 000000000..168ad1ca6 --- /dev/null +++ b/packages/cashc/test/valid-contract-files/tuple_reassignment_narrowing.cash @@ -0,0 +1,9 @@ +contract TupleReassignmentNarrowing() { + function spend(bytes data, bytes other) { + require(data.length == 20); + // Reassigning a narrowed variable keeps its narrowed type, so the new value must have the same bound + data, bytes rest = other.split(20); + bytes20 narrowed = data; + require(narrowed == data && rest.length > 0); + } +} diff --git a/packages/utils/src/script.ts b/packages/utils/src/script.ts index 9cb080e78..7a62fd74d 100644 --- a/packages/utils/src/script.ts +++ b/packages/utils/src/script.ts @@ -134,15 +134,13 @@ export function encodeNullDataScript(chunks: OpOrData[]): Uint8Array { return new Uint8Array([chunk]); } - const pushdataOpcode = getPushDataOpcode(chunk); + const pushdataOpcode = encodeNullDataPushOpcode(chunk.byteLength); return new Uint8Array([...pushdataOpcode, ...chunk]); }), ); } -function getPushDataOpcode(data: Uint8Array): Uint8Array { - const { byteLength } = data; - +export function encodeNullDataPushOpcode(byteLength: number): Uint8Array { if (byteLength === 0) return Uint8Array.from([0x4c, 0x00]); if (byteLength < 76) return Uint8Array.from([byteLength]); if (byteLength < 256) return Uint8Array.from([0x4c, byteLength]); diff --git a/website/docs/language/globals.md b/website/docs/language/globals.md index cdaadc87e..6449e6ef5 100644 --- a/website/docs/language/globals.md +++ b/website/docs/language/globals.md @@ -247,7 +247,7 @@ Creates new P2SH32 locking bytecode for the script hash, where `scriptHash` is t new LockingBytecodeNullData(bytes[] chunks): bytes ``` -Creates new OP_RETURN locking bytecode with `chunks` as its OP_RETURN data. +Creates new OP_RETURN locking bytecode with `chunks` as its OP_RETURN data. Each chunk is pushed the same way as in the SDK's `addOpReturnOutput()`: with `OP_PUSHDATA1` for empty chunks and chunks of 76-255 bytes, and with a direct push for chunks of 1-75 bytes. Chunks larger than 255 bytes are not supported, and cause a compile error when their size is known at compile time. :::note `LockingBytecodeNullData` outputs are provably unspendable, so any BCH sent there would be burned. For these outputs no dust-minimum is enforced so often `LockingBytecodeNullData` outputs have 0 BCH on them. diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index 8ab42ab3b..e32172a1c 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -16,6 +16,8 @@ This release contains several breaking changes, please refer to the [migration n - :hammer_and_wrench: Unused variables that are not marked `unused` now produce a compiler warning instead of a compilation error. Warnings are printed with `console.warn`, or passed to the new `warningListener` compiler option. - :hammer_and_wrench: Add a compiler warning for values assigned to a variable that are never read afterwards. - :bug: Fix bug where date literal parsing was different per locale, it now uses UTC. +- :bug: Fix bug where `LockingBytecodeNullData` used an incorrect push opcode for empty chunks and chunks of 128-255 bytes, so it did not match the SDK's `addOpReturnOutput()`. Chunks with a value or length that is known at compile time now use a precomputed push opcode, which makes them smaller. This changes the bytecode of all contracts that use `LockingBytecodeNullData`. +- :bug: Fix bug where hex literals with an odd number of digits (e.g. `0x123`) compiled to a different value (`0x1203`), they now cause a compile error. - :racehorse: Add new `OP_SWAP OP_MUL`, `OP_NOT OP_NOT` and `TO_ALTSTACK OP_FROMALTSTACK` optimisations. - :racehorse: Add new optimisations for loop counter updates, reassignments inside loops and order-independent operations (`OP_BOOLAND`, `OP_BOOLOR`, `OP_MIN`, `OP_MAX`). - :racehorse: Greatly improve compiler speed for very large contracts.