From 3b94084581d615ea25ce5fd3cbb338ba6ae6857e Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月14日 17:22:41 +0100 Subject: [PATCH 01/14] Add optimisations - Use NUMEQUALVERIFY for the final function in a contract - Only drop final OP_VERIFY if remaining stack < 5 - Calculate byte size for OP_RETURN output HexLiterals so this does not need to be done by the script - Update fixtures --- misc/v0.3.0/mecenas_030.json | 2 +- .../src/generation/GenerateTargetTraversal.ts | 52 +++++-- packages/cashc/test/generation/fixtures.ts | 143 +++++++++++++++--- .../cashscript/test/e2e/HodlVault.test.ts | 1 + packages/cashscript/test/e2e/P2PKH.test.ts | 1 + .../test/e2e/TransferWithTimeout.test.ts | 2 + packages/cashscript/test/e2e/misc.test.ts | 2 + .../cashscript/test/fixture/announcement.json | 6 +- .../test/fixture/bounded_bytes.json | 4 +- .../cashscript/test/fixture/hodl_vault.json | 4 +- packages/cashscript/test/fixture/mecenas.json | 6 +- .../test/fixture/simple_covenant.json | 4 +- .../test/fixture/transfer_with_timeout.json | 6 +- 13 files changed, 179 insertions(+), 54 deletions(-) diff --git a/misc/v0.3.0/mecenas_030.json b/misc/v0.3.0/mecenas_030.json index 095a454f..02a73725 100644 --- a/misc/v0.3.0/mecenas_030.json +++ b/misc/v0.3.0/mecenas_030.json @@ -52,4 +52,4 @@ "version": "0.3.0" }, "updatedAt": "2020-01-09T13:02:28.771Z" -} \ No newline at end of file +} diff --git a/packages/cashc/src/generation/GenerateTargetTraversal.ts b/packages/cashc/src/generation/GenerateTargetTraversal.ts index 7e768e08..702548df 100644 --- a/packages/cashc/src/generation/GenerateTargetTraversal.ts +++ b/packages/cashc/src/generation/GenerateTargetTraversal.ts @@ -108,21 +108,28 @@ export default class GenerateTargetTraversal extends AstTraversal { this.emit(Op.OP_PICK); } + // All functions are if-else statements, except the final one which is + // enforced with NUMEQUALVERIFY this.emit(Data.encodeInt(i)); this.emit(Op.OP_NUMEQUAL); - this.emit(Op.OP_IF); + if (i < node.functions.length - 1) { + this.emit(Op.OP_IF); + } else { + this.emit(Op.OP_VERIFY); + } + f = this.visit(f) as FunctionDefinitionNode; - this.emit(Op.OP_ELSE); if (i < node.functions.length - 1) { - this.stack = [...stackCopy]; + this.emit(Op.OP_ELSE); } + this.stack = [...stackCopy]; return f; }); - - this.emit(Op.OP_FALSE); - node.functions.forEach(() => this.emit(Op.OP_ENDIF)); + for (let i = 0; i < node.functions.length - 1; i += 1) { + this.emit(Op.OP_ENDIF); + } } return node; @@ -142,13 +149,14 @@ export default class GenerateTargetTraversal extends AstTraversal { // Remove final OP_VERIFY // If the final opcodes are OP_CHECK{LOCKTIME|SEQUENCE}VERIFY OP_DROP // Or if the final opcode is OP_ENDIF + // Or if the remaining stack size>=5 (2DROP 2DROP 1 < NIP NIP NIP NIP) // then push it back to the script, and push OP_TRUE to the stack const finalOp = this.output.pop(); - if (finalOp === Op.OP_DROP || finalOp === Op.OP_ENDIF) { + this.pushToStack('(value)'); + if (finalOp === Op.OP_DROP || finalOp === Op.OP_ENDIF || (finalOp && this.stack.length>= 5)) { this.emit(finalOp); this.emit(Op.OP_TRUE); } - this.pushToStack('(value)'); this.cleanStack(); return node; } @@ -460,15 +468,27 @@ export default class GenerateTargetTraversal extends AstTraversal { // elements.forEach((el) => { this.visit(el); + // Push the element's size (and calculate VarInt) this.emit(Op.OP_SIZE); - this.emit(Op.OP_DUP); - this.emit(Data.encodeInt(75)); - this.emit(Op.OP_GREATERTHAN); - this.emit(Op.OP_IF); - this.emit(Buffer.from('4c', 'hex')); - this.emit(Op.OP_SWAP); - this.emit(Op.OP_CAT); - this.emit(Op.OP_ENDIF); + if (el instanceof HexLiteralNode) { + // If the argument is a literal, we know its size + if (el.value.byteLength> 75) { + this.emit(Buffer.from('4c', 'hex')); + this.emit(Op.OP_SWAP); + this.emit(Op.OP_CAT); + } + } else { + // If the argument is not a literal, the script needs to check size + this.emit(Op.OP_DUP); + this.emit(Data.encodeInt(75)); + this.emit(Op.OP_GREATERTHAN); + this.emit(Op.OP_IF); + this.emit(Buffer.from('4c', 'hex')); + this.emit(Op.OP_SWAP); + this.emit(Op.OP_CAT); + this.emit(Op.OP_ENDIF); + } + // Concat size and arguments this.emit(Op.OP_SWAP); this.emit(Op.OP_CAT); this.emit(Op.OP_CAT); diff --git a/packages/cashc/test/generation/fixtures.ts b/packages/cashc/test/generation/fixtures.ts index 5def70ed..24f4fa80 100644 --- a/packages/cashc/test/generation/fixtures.ts +++ b/packages/cashc/test/generation/fixtures.ts @@ -16,7 +16,9 @@ export const fixtures: Fixture[] = [ constructorInputs: [{ name: 'pkh', type: 'bytes20' }], abi: [{ name: 'spend', covenant: false, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }] }], bytecode: + // require(hash160(pk) == pkh) 'OP_OVER OP_HASH160 OP_EQUALVERIFY ' + // require(checkSig(s, pk)) + 'OP_CHECKSIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'p2pkh.cash'), { encoding: 'utf-8' }), networks: {}, @@ -34,12 +36,19 @@ export const fixtures: Fixture[] = [ constructorInputs: [{ name: 'x', type: 'int' }, { name: 'y', type: 'string' }], abi: [{ name: 'hello', covenant: false, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }] }], bytecode: + // int myVariable = 10 - 4 'OP_10 OP_4 OP_SUB ' + // int myOtherVariable = 20 + myVariable % 2 + '14 OP_SWAP OP_2 OP_MOD OP_ADD ' + // require(myOtherVariable> x) + 'OP_LESSTHAN OP_VERIFY ' + // string hw = "Hello World" + '48656c6c6f20576f726c64 ' + // hw = hw + y + 'OP_DUP OP_ROT OP_CAT ' + // require(ripemd160(pk) == ripemd160(hw)) + 'OP_2 OP_PICK OP_RIPEMD160 OP_SWAP OP_RIPEMD160 OP_EQUALVERIFY ' + // require(checkSig(s, pk)) + 'OP_ROT OP_ROT OP_CHECKSIG ' + 'OP_NIP', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'reassignment.cash'), { encoding: 'utf-8' }), @@ -58,15 +67,25 @@ export const fixtures: Fixture[] = [ constructorInputs: [{ name: 'x', type: 'int' }, { name: 'y', type: 'int' }], abi: [{ name: 'hello', covenant: false, inputs: [{ name: 'a', type: 'int' }, { name: 'b', type: 'int' }] }], bytecode: + // int d = a + b 'OP_2OVER OP_ADD ' + // d = d - a + 'OP_DUP OP_4 OP_PICK OP_SUB ' + // if (d == x - 2) { + 'OP_DUP OP_3 OP_ROLL OP_2 OP_SUB OP_NUMEQUAL OP_IF ' + // int c = d + b + 'OP_DUP OP_5 OP_PICK OP_ADD ' + // d = a + c + 'OP_4 OP_PICK OP_OVER OP_ADD OP_ROT OP_DROP OP_SWAP ' + // require(c> d) + 'OP_2DUP OP_LESSTHAN OP_VERIFY ' + // } else { + 'OP_DROP OP_ELSE ' + // require(d == a) } + 'OP_DUP OP_4 OP_PICK OP_NUMEQUALVERIFY OP_ENDIF ' + // d = d + a + 'OP_DUP OP_4 OP_ROLL OP_ADD ' + // require(d == y) + 'OP_3 OP_ROLL OP_NUMEQUAL ' + 'OP_NIP OP_NIP OP_NIP', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'if_statement.cash'), { encoding: 'utf-8' }), @@ -88,13 +107,18 @@ export const fixtures: Fixture[] = [ { name: 'timeout', covenant: false, inputs: [{ name: 'senderSig', type: 'sig' }] }, ], bytecode: + // function transfer 'OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF ' + // require(checkSig(recipientSig, recipient)) + 'OP_4 OP_ROLL OP_ROT OP_CHECKSIG ' + 'OP_NIP OP_NIP OP_NIP OP_ELSE ' - + 'OP_3 OP_ROLL OP_1 OP_NUMEQUAL OP_IF ' + // function timeout + + 'OP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY ' + // require(checkSig(senderSig, sender)) + 'OP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY ' + // require(tx.time>= timeout) + 'OP_SWAP OP_CHECKLOCKTIMEVERIFY OP_2DROP OP_1 ' - + 'OP_ELSE OP_0 OP_ENDIF OP_ENDIF', + + 'OP_ENDIF', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'multifunction.cash'), { encoding: 'utf-8' }), networks: {}, compiler: { @@ -114,28 +138,49 @@ export const fixtures: Fixture[] = [ { name: 'timeout', covenant: false, inputs: [{ name: 'b', type: 'int' }] }, ], bytecode: + // function transfer 'OP_2 OP_PICK OP_0 OP_NUMEQUAL OP_IF ' + // int d = a + b + 'OP_3 OP_PICK OP_5 OP_PICK OP_ADD ' + // d = d - a + 'OP_DUP OP_5 OP_PICK OP_SUB ' + // if (d == x) { + 'OP_DUP OP_3 OP_ROLL OP_NUMEQUAL OP_IF ' + // int c = d + b + 'OP_DUP OP_6 OP_PICK OP_ADD ' + // d = a + c + 'OP_5 OP_PICK OP_OVER OP_ADD OP_ROT OP_DROP OP_SWAP ' + // require(c> d) + 'OP_2DUP OP_LESSTHAN OP_VERIFY ' + // } else { + 'OP_DROP OP_ELSE ' + // d = a } + 'OP_4 OP_PICK OP_NIP OP_ENDIF ' + // d = d + a + 'OP_DUP OP_5 OP_ROLL OP_ADD ' - + 'OP_3 OP_ROLL OP_NUMEQUAL ' - + 'OP_NIP OP_NIP OP_NIP OP_NIP OP_ELSE ' - + 'OP_ROT OP_1 OP_NUMEQUAL OP_IF ' - + 'OP_2 OP_PICK OP_DUP OP_2 OP_ADD ' + // require(d == y) + + 'OP_3 OP_ROLL OP_NUMEQUALVERIFY ' + + 'OP_2DROP OP_2DROP OP_1 OP_ELSE ' + // function timeout + + 'OP_ROT OP_1 OP_NUMEQUALVERIFY ' + // int d = b + + 'OP_2 OP_PICK ' + // d = d + 2 + + 'OP_DUP OP_2 OP_ADD ' + // if (d == x) { + 'OP_DUP OP_3 OP_ROLL OP_NUMEQUAL OP_IF ' + // int c = d + b + 'OP_DUP OP_4 OP_PICK OP_ADD ' + // d = c + d + 'OP_2DUP OP_ADD OP_ROT OP_DROP OP_SWAP ' + // require(c> d) } + 'OP_2DUP OP_LESSTHAN OP_VERIFY ' + 'OP_DROP OP_ENDIF ' + // d = b + '' + // require(d == y) + 'OP_2SWAP OP_NUMEQUAL ' - + 'OP_NIP OP_NIP OP_ELSE OP_0 OP_ENDIF OP_ENDIF', + + 'OP_NIP OP_NIP OP_ENDIF', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'multifunction_if_statements.cash'), { encoding: 'utf-8' }), networks: {}, compiler: { @@ -152,8 +197,8 @@ export const fixtures: Fixture[] = [ constructorInputs: [{ name: 'pk1', type: 'pubkey' }, { name: 'pk2', type: 'pubkey' }, { name: 'pk3', type: 'pubkey' }], abi: [{ name: 'spend', covenant: false, inputs: [{ name: 's1', type: 'sig' }, { name: 's2', type: 'sig' }] }], bytecode: - 'OP_0 OP_3 OP_ROLL OP_4 OP_ROLL OP_2 ' - + 'OP_3 OP_ROLL OP_2ROT OP_SWAP OP_3 OP_CHECKMULTISIG', + // require(checkMultiSig([s1, s2], [pk1, pk2, pk3])) + 'OP_0 OP_3 OP_ROLL OP_4 OP_ROLL OP_2 OP_3 OP_ROLL OP_2ROT OP_SWAP OP_3 OP_CHECKMULTISIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', '2_of_3_multisig.cash'), { encoding: 'utf-8' }), networks: {}, compiler: { @@ -170,8 +215,11 @@ export const fixtures: Fixture[] = [ constructorInputs: [{ name: 'b', type: 'bytes' }], abi: [{ name: 'spend', covenant: false, inputs: [] }], bytecode: + // bytes x = b.split(b.length / 2)[1] 'OP_DUP OP_DUP OP_SIZE OP_NIP OP_2 OP_DIV OP_SPLIT OP_NIP ' + // require(x != b) + 'OP_2DUP OP_EQUAL OP_NOT OP_VERIFY ' + // bytes x = b.split(b.length / 2)[1] + 'OP_SWAP OP_4 OP_SPLIT OP_DROP OP_EQUAL OP_NOT', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'split_size.cash'), { encoding: 'utf-8' }), networks: {}, @@ -189,7 +237,9 @@ export const fixtures: Fixture[] = [ constructorInputs: [], abi: [{ name: 'hello', covenant: false, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }] }], bytecode: + // require((ripemd160(bytes(pk)) == hash160(0x0) == !true)); 'OP_DUP OP_RIPEMD160 OP_0 OP_HASH160 OP_EQUAL OP_1 OP_NOT OP_EQUALVERIFY ' + // require(checkSig(s, pk)); + 'OP_CHECKSIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'cast_hash_checksig.cash'), { encoding: 'utf-8' }), networks: {}, @@ -207,9 +257,10 @@ export const fixtures: Fixture[] = [ constructorInputs: [], abi: [{ name: 'hello', covenant: false, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }, { name: 'data', type: 'bytes' }] }], bytecode: + // require(checkSig(s, pk)) 'OP_2DUP OP_CHECKSIGVERIFY ' - + 'OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP ' - + 'OP_ROT OP_ROT OP_CHECKDATASIG', + // require(checkDataSig(datasig(s), data, pk)) + + 'OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP OP_ROT OP_ROT OP_CHECKDATASIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'checkdatasig.cash'), { encoding: 'utf-8' }), networks: {}, compiler: { @@ -241,12 +292,19 @@ export const fixtures: Fixture[] = [ }, ], bytecode: + // int blockHeight = int(oracleMessage.split(4)[0]) 'OP_6 OP_PICK OP_4 OP_SPLIT OP_DROP OP_BIN2NUM ' + // int price = int(oracleMessage.split(4)[1]) + 'OP_7 OP_PICK OP_4 OP_SPLIT OP_NIP OP_BIN2NUM ' + // require(blockHeight>= minBlock); + 'OP_OVER OP_5 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY ' + // require(tx.time>= blockHeight); + 'OP_SWAP OP_CHECKLOCKTIMEVERIFY OP_DROP ' + // require(price>= priceTarget); + 'OP_3 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY ' + // require(checkDataSig(oracleSig, oracleMessage, oraclePk)); + 'OP_3 OP_ROLL OP_4 OP_ROLL OP_3 OP_ROLL OP_CHECKDATASIGVERIFY ' + // require(checkSig(ownerSig, ownerPk)); + 'OP_CHECKSIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'hodl_vault.cash'), { encoding: 'utf-8' }), networks: {}, @@ -264,11 +322,15 @@ export const fixtures: Fixture[] = [ constructorInputs: [], abi: [{ name: 'hello', covenant: false, inputs: [] }], bytecode: + // int a = 1; int b = 2; int c = 3; int d = 4; int e = 5; int f = 6; 'OP_1 OP_2 OP_3 OP_4 OP_5 OP_6 ' + // if (a < 3) { + 'OP_5 OP_PICK OP_3 OP_LESSTHAN OP_IF ' + // a = 3 } + 'OP_3 OP_6 OP_ROLL OP_DROP OP_SWAP OP_TOALTSTACK OP_SWAP OP_TOALTSTACK OP_SWAP ' + 'OP_TOALTSTACK OP_SWAP OP_TOALTSTACK OP_SWAP OP_FROMALTSTACK OP_FROMALTSTACK ' + 'OP_FROMALTSTACK OP_FROMALTSTACK OP_ENDIF ' + // require(a> b + c + d + e + f); + 'OP_2ROT OP_5 OP_ROLL OP_ADD OP_4 OP_ROLL OP_ADD ' + 'OP_3 OP_ROLL OP_ADD OP_ROT OP_ADD OP_GREATERTHAN', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'deep_replace.cash'), { encoding: 'utf-8' }), @@ -286,7 +348,7 @@ export const fixtures: Fixture[] = [ contractName: 'BoundedBytes', constructorInputs: [], abi: [{ name: 'spend', covenant: false, inputs: [{ name: 'b', type: 'bytes4' }, { name: 'i', type: 'int' }] }], - bytecode: 'OP_SWAP OP_4 OP_NUM2BIN OP_EQUAL', + bytecode: 'OP_SWAP OP_4 OP_NUM2BIN OP_EQUAL', // require(b == bytes4(i)) source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'bounded_bytes.cash'), { encoding: 'utf-8' }), networks: {}, compiler: { @@ -308,8 +370,13 @@ export const fixtures: Fixture[] = [ ], abi: [{ name: 'spend', covenant: true, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }] }], bytecode: + // preimage parsing 'OP_OVER OP_4 OP_SPLIT 65 OP_SPLIT OP_NIP OP_SIZE 34 OP_SUB OP_SPLIT OP_DROP ' - + 'OP_SWAP OP_ROT OP_EQUALVERIFY 00 OP_EQUALVERIFY ' + // require(tx.version == requiredVersion) + + 'OP_SWAP OP_ROT OP_EQUALVERIFY ' + // require(tx.bytecode == 0x00) + + '00 OP_EQUALVERIFY ' + // require(checkSig(s, pk)) + preimage verification + 'OP_ROT OP_ROT OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP ' + 'OP_4 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'covenant.cash'), { encoding: 'utf-8' }), @@ -333,11 +400,18 @@ export const fixtures: Fixture[] = [ ], abi: [{ name: 'spend', covenant: true, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }] }], bytecode: + // preimage parsing 'OP_OVER 24 OP_SPLIT OP_NIP 20 OP_SPLIT 25 OP_SPLIT OP_NIP OP_SIZE 34 OP_SUB OP_SPLIT OP_DROP ' + // require(!checkSig(s, pk)) + 'OP_5 OP_PICK OP_5 OP_PICK OP_CHECKSIG OP_NOT OP_VERIFY ' - + 'OP_SWAP OP_ROT OP_EQUALVERIFY 00 OP_EQUALVERIFY ' + // require(tx.hashSequence == requiredHS) + + 'OP_SWAP OP_ROT OP_EQUALVERIFY ' + // require(tx.bytecode == 0x00) + + '00 OP_EQUALVERIFY ' + // require(checkSig(s, pk)) + preimage verification + 'OP_2 OP_PICK OP_2 OP_PICK OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP ' + 'OP_4 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIGVERIFY ' + // require(checkSig(s, pk)) + 'OP_CHECKSIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'covenant_multiple_checksig.cash'), { encoding: 'utf-8' }), networks: {}, @@ -355,7 +429,11 @@ export const fixtures: Fixture[] = [ constructorInputs: [], abi: [{ name: 'spend', covenant: true, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }] }], bytecode: - 'OP_DUP OP_SIZE OP_4 OP_SUB OP_SPLIT OP_NIP OP_1 OP_EQUALVERIFY ' + // preimage parsing + 'OP_DUP OP_SIZE OP_4 OP_SUB OP_SPLIT OP_NIP ' + // require(tx.hashtype == bytes(0x01)) + + 'OP_1 OP_EQUALVERIFY ' + // require(checkSig(s, pk)) + preimage verification + 'OP_ROT OP_ROT OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP ' + 'OP_4 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'covenant_only_hashtype.cash'), { encoding: 'utf-8' }), @@ -374,7 +452,11 @@ export const fixtures: Fixture[] = [ constructorInputs: [], abi: [{ name: 'spend', covenant: true, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }] }], bytecode: - 'OP_DUP OP_4 OP_SPLIT OP_DROP OP_1 OP_EQUALVERIFY ' + // preimage parsing + 'OP_DUP OP_4 OP_SPLIT OP_DROP ' + // require(tx.version == bytes(0x01)) + + 'OP_1 OP_EQUALVERIFY ' + // require(checkSig(s, pk)) + preimage verification + 'OP_ROT OP_ROT OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP ' + 'OP_4 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIG', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'covenant_only_version.cash'), { encoding: 'utf-8' }), @@ -451,29 +533,47 @@ export const fixtures: Fixture[] = [ { name: 'reclaim', covenant: false, inputs: [{ name: 'pk', type: 'pubkey' }, { name: 's', type: 'sig' }] }, ], bytecode: + // function receive 'OP_4 OP_PICK OP_0 OP_NUMEQUAL OP_IF ' + // preimage parsing + 'OP_5 OP_PICK 69 OP_SPLIT OP_NIP OP_SIZE 34 OP_SUB OP_SPLIT ' + 'OP_8 OP_SPLIT OP_4 OP_SPLIT OP_NIP 20 OP_SPLIT OP_DROP ' + // require(checkSig(s, pk)) + preimage verification + 'OP_10 OP_ROLL OP_10 OP_ROLL OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP ' + 'OP_12 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIGVERIFY ' + // require(tx.age>= period) + 'OP_6 OP_ROLL OP_CHECKSEQUENCEVERIFY OP_DROP ' + // int fee = 1000 + 'e803 ' + // int intValue = int(bytes(tx.value)) + 'OP_ROT OP_BIN2NUM ' + // if (intValue <= pledge + fee) { + 'OP_DUP OP_7 OP_PICK OP_3 OP_PICK OP_ADD OP_LESSTHANOREQUAL OP_IF ' + // bytes8 amount1 = bytes8(intValue - fee) + 'OP_2DUP OP_SWAP OP_SUB OP_8 OP_NUM2BIN ' + // bytes34 out1 = new OutputP2PKH(amount1, recipient) + 'OP_DUP 1976a914 OP_CAT OP_6 OP_PICK OP_CAT 88ac OP_CAT ' + // require(hash256(out1) == tx.hashOutputs) + 'OP_DUP OP_HASH256 OP_5 OP_PICK OP_EQUALVERIFY OP_2DROP ' + // } else { + 'OP_ELSE ' + // bytes8 amount1 = bytes8(pledge) + 'OP_6 OP_PICK OP_8 OP_NUM2BIN ' + // bytes8 amount2 = bytes8(intValue - pledge - fee) + 'OP_OVER OP_8 OP_PICK OP_SUB OP_3 OP_PICK OP_SUB OP_8 OP_NUM2BIN ' + // bytes34 out1 = new OutputP2PKH(amount1, recipient) + 'OP_OVER 1976a914 OP_CAT OP_7 OP_PICK OP_CAT 88ac OP_CAT ' + // bytes32 out2 = new OutputP2SH(amount2, hash160(tx.bytecode)) + 'OP_OVER 17a914 OP_CAT OP_7 OP_PICK OP_HASH160 OP_CAT 87 OP_CAT ' + // require(hash256(out1 + out2) == tx.hashOutputs) } + 'OP_2DUP OP_CAT OP_HASH256 OP_7 OP_PICK OP_EQUALVERIFY OP_2DROP OP_2DROP OP_ENDIF ' - + 'OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 ' - + 'OP_ELSE OP_4 OP_ROLL OP_1 OP_NUMEQUAL OP_IF ' + + 'OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE ' + // function reclaim + + 'OP_4 OP_ROLL OP_1 OP_NUMEQUALVERIFY ' + // require(hash160(pk) == funder) + 'OP_4 OP_PICK OP_HASH160 OP_ROT OP_EQUALVERIFY ' - + 'OP_4 OP_ROLL OP_4 OP_ROLL OP_CHECKSIG OP_NIP OP_NIP OP_NIP ' - + 'OP_ELSE OP_0 OP_ENDIF OP_ENDIF', + // require(checkSig(s, pk)) + + 'OP_4 OP_ROLL OP_4 OP_ROLL OP_CHECKSIG OP_NIP OP_NIP OP_NIP OP_ENDIF', source: fs.readFileSync(path.join(__dirname, '..', 'fixture', 'mecenas.cash'), { encoding: 'utf-8' }), networks: {}, compiler: { @@ -499,8 +599,7 @@ export const fixtures: Fixture[] = [ + 'OP_2ROT OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP ' + 'OP_7 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIGVERIFY ' // bytes announcement = new OutputNullData(...) - + '0000000000000000 6a 6d02 OP_SIZE OP_DUP 4b OP_GREATERTHAN OP_IF ' - + '4c OP_SWAP OP_CAT OP_ENDIF OP_SWAP OP_CAT OP_CAT ' + + '0000000000000000 6a 6d02 OP_SIZE OP_SWAP OP_CAT OP_CAT ' + '4120636f6e7472616374206d6179206e6f7420696e6a75726520612068756d616e20626' + '5696e67206f722c207468726f75676820696e616374696f6e2c20616c6c6f77206120687' + '56d616e206265696e6720746f20636f6d6520746f206861726d2e ' diff --git a/packages/cashscript/test/e2e/HodlVault.test.ts b/packages/cashscript/test/e2e/HodlVault.test.ts index bfef879d..e8ef9bfb 100644 --- a/packages/cashscript/test/e2e/HodlVault.test.ts +++ b/packages/cashscript/test/e2e/HodlVault.test.ts @@ -14,6 +14,7 @@ describe('HodlVault', () => { beforeAll(() => { const HodlVault = Contract.import(path.join(__dirname, '..', 'fixture', 'hodl_vault.json'), 'testnet'); hodlVault = HodlVault.new(alicePk, oraclePk, 597000, 30000); + console.log(hodlVault.address); }); describe('send (to one)', () => { diff --git a/packages/cashscript/test/e2e/P2PKH.test.ts b/packages/cashscript/test/e2e/P2PKH.test.ts index d20186c5..253698f9 100644 --- a/packages/cashscript/test/e2e/P2PKH.test.ts +++ b/packages/cashscript/test/e2e/P2PKH.test.ts @@ -16,6 +16,7 @@ describe('P2PKH', () => { beforeAll(() => { const P2PKH = Contract.import(path.join(__dirname, '..', 'fixture', 'p2pkh.json'), 'testnet'); p2pkhInstance = P2PKH.new(alicePkh); + console.log(p2pkhInstance.address); }); describe('send (to one)', () => { diff --git a/packages/cashscript/test/e2e/TransferWithTimeout.test.ts b/packages/cashscript/test/e2e/TransferWithTimeout.test.ts index 71f14fd7..df5c31d6 100644 --- a/packages/cashscript/test/e2e/TransferWithTimeout.test.ts +++ b/packages/cashscript/test/e2e/TransferWithTimeout.test.ts @@ -16,6 +16,8 @@ describe('TransferWithTimeout', () => { const TWT = Contract.import(path.join(__dirname, '..', 'fixture', 'transfer_with_timeout.json'), 'testnet'); twtInstancePast = TWT.new(alicePk, bobPk, 1000000); twtInstanceFuture = TWT.new(alicePk, bobPk, 2000000); + console.log(twtInstancePast.address); + console.log(twtInstanceFuture.address); }); describe('send (to one)', () => { diff --git a/packages/cashscript/test/e2e/misc.test.ts b/packages/cashscript/test/e2e/misc.test.ts index 61429bfa..e3a158ec 100644 --- a/packages/cashscript/test/e2e/misc.test.ts +++ b/packages/cashscript/test/e2e/misc.test.ts @@ -12,6 +12,7 @@ describe('BoundedBytes', () => { beforeAll(() => { const BoundedBytes = Contract.import(path.join(__dirname, '..', 'fixture', 'bounded_bytes.json'), 'testnet'); bbInstance = BoundedBytes.new(); + console.log(bbInstance.address); }); describe('send (to one)', () => { @@ -65,6 +66,7 @@ describe('Simple Covenant', () => { beforeAll(() => { const Covenant = Contract.import(path.join(__dirname, '..', 'fixture', 'simple_covenant.json'), 'testnet'); covenant = Covenant.new(); + console.log(covenant.address); }); describe('send', () => { diff --git a/packages/cashscript/test/fixture/announcement.json b/packages/cashscript/test/fixture/announcement.json index d52829a4..4e2cf77e 100644 --- a/packages/cashscript/test/fixture/announcement.json +++ b/packages/cashscript/test/fixture/announcement.json @@ -17,12 +17,12 @@ ] } ], - "bytecode": "OP_DUP 69 OP_SPLIT OP_NIP OP_SIZE 34 OP_SUB OP_SPLIT OP_8 OP_SPLIT OP_4 OP_SPLIT OP_NIP 20 OP_SPLIT OP_DROP OP_2ROT OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP OP_7 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIGVERIFY 0000000000000000 6a 6d02 OP_SIZE OP_DUP 4b OP_GREATERTHAN OP_IF 4c OP_SWAP OP_CAT OP_ENDIF OP_SWAP OP_CAT OP_CAT 4120636f6e7472616374206d6179206e6f7420696e6a75726520612068756d616e206265696e67206f722c207468726f75676820696e616374696f6e2c20616c6c6f7720612068756d616e206265696e6720746f20636f6d6520746f206861726d2e OP_SIZE OP_DUP 4b OP_GREATERTHAN OP_IF 4c OP_SWAP OP_CAT OP_ENDIF OP_SWAP OP_CAT OP_CAT OP_SIZE OP_SWAP OP_CAT OP_CAT e803 OP_3 OP_ROLL OP_BIN2NUM OP_OVER OP_SUB OP_DUP OP_ROT OP_GREATERTHANOREQUAL OP_IF OP_DUP OP_8 OP_NUM2BIN 17a914 OP_CAT OP_4 OP_PICK OP_HASH160 OP_CAT 87 OP_CAT OP_2OVER OP_2 OP_PICK OP_CAT OP_HASH256 OP_EQUALVERIFY OP_DROP OP_ELSE OP_2 OP_PICK OP_2 OP_PICK OP_HASH256 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_1", + "bytecode": "OP_DUP 69 OP_SPLIT OP_NIP OP_SIZE 34 OP_SUB OP_SPLIT OP_8 OP_SPLIT OP_4 OP_SPLIT OP_NIP 20 OP_SPLIT OP_DROP OP_2ROT OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP OP_7 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIGVERIFY 0000000000000000 6a 6d02 OP_SIZE OP_SWAP OP_CAT OP_CAT 4120636f6e7472616374206d6179206e6f7420696e6a75726520612068756d616e206265696e67206f722c207468726f75676820696e616374696f6e2c20616c6c6f7720612068756d616e206265696e6720746f20636f6d6520746f206861726d2e OP_SIZE OP_DUP 4b OP_GREATERTHAN OP_IF 4c OP_SWAP OP_CAT OP_ENDIF OP_SWAP OP_CAT OP_CAT OP_SIZE OP_SWAP OP_CAT OP_CAT e803 OP_3 OP_ROLL OP_BIN2NUM OP_OVER OP_SUB OP_DUP OP_ROT OP_GREATERTHANOREQUAL OP_IF OP_DUP OP_8 OP_NUM2BIN 17a914 OP_CAT OP_4 OP_PICK OP_HASH160 OP_CAT 87 OP_CAT OP_2OVER OP_2 OP_PICK OP_CAT OP_HASH256 OP_EQUALVERIFY OP_DROP OP_ELSE OP_2 OP_PICK OP_2 OP_PICK OP_HASH256 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_1", "source": "pragma cashscript ^0.3.3;\n\n/* This is a contract showcasing covenants outside of regular transactional use.\n * It enforces the contract to make an \"announcement\" on Memo.cash, and send the\n * remainder of contract funds back to the contract.\n */\ncontract Announcement() {\n function announce(pubkey pk, sig s) {\n require(checkSig(s, pk));\n\n // Create the memo.cash announcement output\n bytes announcement = new OutputNullData([\n 0x6d02,\n bytes('A contract may not injure a human being or, through inaction, allow a human being to come to harm.')\n ]);\n\n // Calculate leftover money after fee (1000 sats)\n // Add change output if there's enough leftover for another announcement\n // otherwise donate the remainder to the miner\n int minerFee = 1000;\n int changeAmount = int(bytes(tx.value)) - minerFee;\n if (changeAmount>= minerFee) {\n bytes32 change = new OutputP2SH(bytes8(changeAmount), hash160(tx.bytecode));\n require(tx.hashOutputs == hash256(announcement + change));\n } else {\n require(tx.hashOutputs == hash256(announcement));\n }\n }\n}\n", "networks": {}, "compiler": { "name": "cashc", "version": "0.3.4" }, - "updatedAt": "2020-03-11T10:22:38.512Z" -} \ No newline at end of file + "updatedAt": "2020-02-14T14:49:27.425Z" +} diff --git a/packages/cashscript/test/fixture/bounded_bytes.json b/packages/cashscript/test/fixture/bounded_bytes.json index 3745a69d..6d853342 100644 --- a/packages/cashscript/test/fixture/bounded_bytes.json +++ b/packages/cashscript/test/fixture/bounded_bytes.json @@ -24,5 +24,5 @@ "name": "cashc", "version": "0.3.4" }, - "updatedAt": "2020-03-11T10:22:39.294Z" -} \ No newline at end of file + "updatedAt": "2020-02-14T14:49:28.022Z" +} diff --git a/packages/cashscript/test/fixture/hodl_vault.json b/packages/cashscript/test/fixture/hodl_vault.json index 05be0b43..bd78db02 100644 --- a/packages/cashscript/test/fixture/hodl_vault.json +++ b/packages/cashscript/test/fixture/hodl_vault.json @@ -45,5 +45,5 @@ "name": "cashc", "version": "0.3.4" }, - "updatedAt": "2020-03-11T10:22:38.916Z" -} \ No newline at end of file + "updatedAt": "2020-02-14T14:49:27.730Z" +} diff --git a/packages/cashscript/test/fixture/mecenas.json b/packages/cashscript/test/fixture/mecenas.json index ba233206..52c5b1f4 100644 --- a/packages/cashscript/test/fixture/mecenas.json +++ b/packages/cashscript/test/fixture/mecenas.json @@ -44,12 +44,12 @@ ] } ], - "bytecode": "OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_4 OP_PICK 69 OP_SPLIT OP_NIP OP_SIZE 34 OP_SUB OP_SPLIT OP_8 OP_SPLIT OP_4 OP_SPLIT OP_NIP 20 OP_SPLIT OP_DROP OP_9 OP_ROLL OP_9 OP_ROLL OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP OP_11 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIGVERIFY e803 OP_ROT OP_BIN2NUM OP_DUP OP_7 OP_PICK OP_3 OP_PICK OP_ADD OP_LESSTHANOREQUAL OP_IF OP_2DUP OP_SWAP OP_SUB OP_8 OP_NUM2BIN OP_DUP 1976a914 OP_CAT OP_6 OP_PICK OP_CAT 88ac OP_CAT OP_DUP OP_HASH256 OP_5 OP_PICK OP_EQUALVERIFY OP_2DROP OP_ELSE OP_6 OP_PICK OP_8 OP_NUM2BIN OP_OVER OP_8 OP_PICK OP_SUB OP_3 OP_PICK OP_SUB OP_8 OP_NUM2BIN OP_OVER 1976a914 OP_CAT OP_7 OP_PICK OP_CAT 88ac OP_CAT OP_OVER 17a914 OP_CAT OP_7 OP_PICK OP_HASH160 OP_CAT 87 OP_CAT OP_2DUP OP_CAT OP_HASH256 OP_7 OP_PICK OP_EQUALVERIFY OP_2DROP OP_2DROP OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_3 OP_ROLL OP_1 OP_NUMEQUAL OP_IF OP_3 OP_PICK OP_HASH160 OP_ROT OP_EQUALVERIFY OP_2SWAP OP_CHECKSIG OP_NIP OP_NIP OP_ELSE OP_0 OP_ENDIF OP_ENDIF", + "bytecode": "OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_4 OP_PICK 69 OP_SPLIT OP_NIP OP_SIZE 34 OP_SUB OP_SPLIT OP_8 OP_SPLIT OP_4 OP_SPLIT OP_NIP 20 OP_SPLIT OP_DROP OP_9 OP_ROLL OP_9 OP_ROLL OP_2DUP OP_SWAP OP_SIZE OP_1SUB OP_SPLIT OP_DROP OP_11 OP_ROLL OP_SHA256 OP_ROT OP_CHECKDATASIGVERIFY OP_CHECKSIGVERIFY e803 OP_ROT OP_BIN2NUM OP_DUP OP_7 OP_PICK OP_3 OP_PICK OP_ADD OP_LESSTHANOREQUAL OP_IF OP_2DUP OP_SWAP OP_SUB OP_8 OP_NUM2BIN OP_DUP 1976a914 OP_CAT OP_6 OP_PICK OP_CAT 88ac OP_CAT OP_DUP OP_HASH256 OP_5 OP_PICK OP_EQUALVERIFY OP_2DROP OP_ELSE OP_6 OP_PICK OP_8 OP_NUM2BIN OP_OVER OP_8 OP_PICK OP_SUB OP_3 OP_PICK OP_SUB OP_8 OP_NUM2BIN OP_OVER 1976a914 OP_CAT OP_7 OP_PICK OP_CAT 88ac OP_CAT OP_OVER 17a914 OP_CAT OP_7 OP_PICK OP_HASH160 OP_CAT 87 OP_CAT OP_2DUP OP_CAT OP_HASH256 OP_7 OP_PICK OP_EQUALVERIFY OP_2DROP OP_2DROP OP_ENDIF OP_2DROP OP_2DROP OP_2DROP OP_2DROP OP_1 OP_ELSE OP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_3 OP_PICK OP_HASH160 OP_ROT OP_EQUALVERIFY OP_2SWAP OP_CHECKSIG OP_NIP OP_NIP OP_ENDIF", "source": "pragma cashscript ^0.3.0;\n\n/* This is an unofficial CashScript port of Licho's Mecenas contract. It is\n * not compatible with Licho's EC plugin, but rather meant as a demonstration\n * of covenants in CashScript.\n * The time checking has been removed so it can be tested without time requirements.\n */\ncontract Mecenas(bytes20 recipient, bytes20 funder, int pledge/*, int period*/) {\n function receive(pubkey pk, sig s) {\n require(checkSig(s, pk));\n\n // require(tx.age>= period);\n\n int fee = 1000;\n int intValue = int(bytes(tx.value));\n\n if (intValue <= pledge + fee) {\n bytes8 amount1 = bytes8(intValue - fee);\n bytes34 out1 = new OutputP2PKH(amount1, recipient);\n require(hash256(out1) == tx.hashOutputs);\n } else {\n bytes8 amount1 = bytes8(pledge);\n bytes8 amount2 = bytes8(intValue - pledge - fee);\n bytes34 out1 = new OutputP2PKH(amount1, recipient);\n bytes32 out2 = new OutputP2SH(amount2, hash160(tx.bytecode));\n require(hash256(out1 + out2) == tx.hashOutputs);\n }\n }\n\n function reclaim(pubkey pk, sig s) {\n require(hash160(pk) == funder);\n require(checkSig(s, pk));\n }\n}\n", "networks": {}, "compiler": { "name": "cashc", "version": "0.3.4" }, - "updatedAt": "2020-03-11T10:22:40.860Z" -} \ No newline at end of file + "updatedAt": "2020-02-14T14:49:29.239Z" +} diff --git a/packages/cashscript/test/fixture/simple_covenant.json b/packages/cashscript/test/fixture/simple_covenant.json index 39e8abb6..e1cabac5 100644 --- a/packages/cashscript/test/fixture/simple_covenant.json +++ b/packages/cashscript/test/fixture/simple_covenant.json @@ -24,5 +24,5 @@ "name": "cashc", "version": "0.3.4" }, - "updatedAt": "2020-03-11T10:22:39.686Z" -} \ No newline at end of file + "updatedAt": "2020-02-14T14:49:28.326Z" +} diff --git a/packages/cashscript/test/fixture/transfer_with_timeout.json b/packages/cashscript/test/fixture/transfer_with_timeout.json index 6e6d4723..53a0c061 100644 --- a/packages/cashscript/test/fixture/transfer_with_timeout.json +++ b/packages/cashscript/test/fixture/transfer_with_timeout.json @@ -36,12 +36,12 @@ ] } ], - "bytecode": "OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_4 OP_ROLL OP_ROT OP_CHECKSIG OP_NIP OP_NIP OP_NIP OP_ELSE OP_3 OP_ROLL OP_1 OP_NUMEQUAL OP_IF OP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY OP_SWAP OP_CHECKLOCKTIMEVERIFY OP_2DROP OP_1 OP_ELSE OP_0 OP_ENDIF OP_ENDIF", + "bytecode": "OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_4 OP_ROLL OP_ROT OP_CHECKSIG OP_NIP OP_NIP OP_NIP OP_ELSE OP_3 OP_ROLL OP_1 OP_NUMEQUALVERIFY OP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY OP_SWAP OP_CHECKLOCKTIMEVERIFY OP_2DROP OP_1 OP_ENDIF", "source": "contract TransferWithTimeout(\n pubkey sender,\n pubkey recipient,\n int timeout\n) {\n // Require recipient's signature to match\n function transfer(sig recipientSig) {\n require(checkSig(recipientSig, recipient));\n }\n\n // Require timeout time to be reached and sender's signature to match\n function timeout(sig senderSig) {\n require(checkSig(senderSig, sender));\n require(tx.time>= timeout);\n }\n}\n", "networks": {}, "compiler": { "name": "cashc", "version": "0.3.4" }, - "updatedAt": "2020-03-11T10:22:40.066Z" -} \ No newline at end of file + "updatedAt": "2020-02-14T14:49:28.621Z" +} From d4c00894e39af233531232ab0b54f87c51d914f7 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月19日 20:36:49 +0100 Subject: [PATCH 02/14] Add start of OperationSimulation - UnaryOp and BinaryOp can now be simulated if all operands are literals - bitcoin-ts is used for this simulation so it's executed as it would be in practice --- packages/cashc/package.json | 2 + packages/cashc/src/Errors.ts | 13 ++ packages/cashc/src/generation/Script.ts | 34 ++- .../src/optimisations/OperationSimulations.ts | 139 +++++++++++++ packages/cashc/src/util.ts | 10 +- packages/cashc/test/optimisations/fixtures.ts | 194 ++++++++++++++++++ .../test/optimisations/simulation.test.ts | 177 ++++++++++++++++ 7 files changed, 556 insertions(+), 13 deletions(-) create mode 100644 packages/cashc/src/optimisations/OperationSimulations.ts create mode 100644 packages/cashc/test/optimisations/fixtures.ts create mode 100644 packages/cashc/test/optimisations/simulation.test.ts diff --git a/packages/cashc/package.json b/packages/cashc/package.json index b453352b..b5a55b79 100644 --- a/packages/cashc/package.json +++ b/packages/cashc/package.json @@ -17,10 +17,12 @@ "@types/yargs": "^13.0.0", "antlr4ts": "^0.5.0-alpha.3", "bitbox-sdk": "^8.8.0", + "bitcoin-ts": "^1.12.0", "semver": "^6.3.0", "yargs": "^14.0.0" }, "devDependencies": { + "delay": "^4.3.0", "eslint": "^6.6.0", "jest": "^24.9.0", "ts-jest": "^24.3.0", diff --git a/packages/cashc/src/Errors.ts b/packages/cashc/src/Errors.ts index 25ff4f4f..85105117 100644 --- a/packages/cashc/src/Errors.ts +++ b/packages/cashc/src/Errors.ts @@ -16,6 +16,7 @@ import { ArrayNode, TupleIndexOpNode, RequireNode, + ExpressionNode, } from './ast/AST'; import { Type, PrimitiveType } from './ast/Type'; import { Symbol, SymbolType } from './ast/SymbolTable'; @@ -205,3 +206,15 @@ export class UnverifiedCovenantError extends CashScriptError { super(node, `Covenant variable ${node.name} was used without signature check`); } } + +export class SimulationError extends CashScriptError {} + +export class WillCauseFailureError extends SimulationError { + constructor( + node: ExpressionNode, + ) { + super(node, 'Statement will cause a transaction error'); + } +} + +export class ExecutionError extends Error {} diff --git a/packages/cashc/src/generation/Script.ts b/packages/cashc/src/generation/Script.ts index ab74a33a..9d108eea 100644 --- a/packages/cashc/src/generation/Script.ts +++ b/packages/cashc/src/generation/Script.ts @@ -52,7 +52,7 @@ export class toOps { } static fromBinaryOp(op: BinaryOperator, numeric: boolean = false): Script { - const mapping = { + const mapping: { [key in BinaryOperator]: Script } = { [BinaryOperator.DIV]: [Op.OP_DIV], [BinaryOperator.MOD]: [Op.OP_MOD], [BinaryOperator.PLUS]: [Op.OP_CAT], @@ -68,17 +68,11 @@ export class toOps { }; if (numeric) { - switch (op) { - case BinaryOperator.PLUS: - return [Op.OP_ADD]; - case BinaryOperator.EQ: - return [Op.OP_NUMEQUAL]; - case BinaryOperator.NE: - return [Op.OP_NUMNOTEQUAL]; - default: - return mapping[op]; - } + mapping[BinaryOperator.PLUS] = [Op.OP_ADD]; + mapping[BinaryOperator.EQ] = [Op.OP_NUMEQUAL]; + mapping[BinaryOperator.NE] = [Op.OP_NUMNOTEQUAL]; } + return mapping[op]; } @@ -91,3 +85,21 @@ export class toOps { return mapping[op]; } } + +export function returnsBool(op: GlobalFunction | BinaryOperator | UnaryOperator): boolean { + return [ + GlobalFunction.CHECKDATASIG, + GlobalFunction.CHECKMULTISIG, + GlobalFunction.CHECKSIG, + GlobalFunction.WITHIN, + BinaryOperator.LT, + BinaryOperator.LE, + BinaryOperator.GT, + BinaryOperator.GE, + BinaryOperator.EQ, + BinaryOperator.NE, + BinaryOperator.AND, + BinaryOperator.OR, + UnaryOperator.NOT, + ].includes(op); +} diff --git a/packages/cashc/src/optimisations/OperationSimulations.ts b/packages/cashc/src/optimisations/OperationSimulations.ts new file mode 100644 index 00000000..a04d6248 --- /dev/null +++ b/packages/cashc/src/optimisations/OperationSimulations.ts @@ -0,0 +1,139 @@ +import { Script as BScript } from 'bitbox-sdk'; +import { + AuthenticationVirtualMachine, + AuthenticationProgramStateBCH, + AuthenticationProgramBCH, + instantiateVirtualMachineBCH, + createAuthenticationProgramStateCommonEmpty, + parseBytecode, +} from 'bitcoin-ts'; +import { UnaryOperator, BinaryOperator } from '../ast/Operator'; +import { + LiteralNode, + BoolLiteralNode, + IntLiteralNode, + HexLiteralNode, + StringLiteralNode, +} from '../ast/AST'; +import { Data } from '../util'; +import { Script, toOps, returnsBool } from '../generation/Script'; +import { ExecutionError } from '../Errors'; + +type BCH_VM = AuthenticationVirtualMachine; +let vm: BCH_VM; +instantiateVirtualMachineBCH().then((res: BCH_VM) => { + vm = res; +}); + +export function executeScriptOnVM(script: Script): Uint8Array[] { + const state = createAuthenticationProgramStateCommonEmpty( + parseBytecode(Uint8Array.from(new BScript().encode(script))), + ) as AuthenticationProgramStateBCH; + const res = vm.stateEvaluate(state); + if (res.error) { + throw new ExecutionError(res.error); + } + // console.log(res.stack); + return res.stack; +} + +export function applyUnaryOperator( + op: UnaryOperator, + expr: LiteralNode, +): LiteralNode { + if (expr instanceof BoolLiteralNode) { + return applyUnaryOperatorToBool(op, expr); + } else if (expr instanceof IntLiteralNode) { + return applyUnaryOperatorToInt(op, expr); + } else { + throw new Error(); // Already checked in typecheck + } +} + +function applyUnaryOperatorToBool( + op: UnaryOperator, + expr: BoolLiteralNode, +): BoolLiteralNode { + const script: Script = ([Data.encodeBool(expr.value)] as Script) + .concat(toOps.fromUnaryOp(op)); + const res = executeScriptOnVM(script)[0]; + return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); +} + +function applyUnaryOperatorToInt( + op: UnaryOperator, + expr: IntLiteralNode, +): IntLiteralNode { + const script: Script = ([Data.encodeInt(expr.value)] as Script) + .concat(toOps.fromUnaryOp(op)); + const res = executeScriptOnVM(script)[0]; + return new IntLiteralNode(Data.decodeInt(Buffer.from(res))); +} + +export function applyBinaryOperator( + left: LiteralNode, + op: BinaryOperator, + right: LiteralNode, +): LiteralNode { + if (left instanceof BoolLiteralNode && right instanceof BoolLiteralNode) { + return applyBinaryOperatorToBool(left, op, right); + } else if (left instanceof IntLiteralNode && right instanceof IntLiteralNode) { + return applyBinaryOperatorToInt(left, op, right); + } else if (left instanceof StringLiteralNode && right instanceof StringLiteralNode) { + return applyBinaryOperatorToString(left, op, right); + } else if (left instanceof HexLiteralNode && right instanceof HexLiteralNode) { + return applyBinaryOperatorToHex(left, op, right); + } else { + throw new Error(); // Already checked in typecheck + } +} + +function applyBinaryOperatorToBool( + left: BoolLiteralNode, + op: BinaryOperator, + right: BoolLiteralNode, +): BoolLiteralNode { + const script: Script = ([Data.encodeBool(left.value), Data.encodeBool(right.value)] as Script) + .concat(toOps.fromBinaryOp(op, true)); + const res = executeScriptOnVM(script)[0]; + return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); +} + +function applyBinaryOperatorToInt( + left: IntLiteralNode, + op: BinaryOperator, + right: IntLiteralNode, +): LiteralNode { + const script: Script = ([Data.encodeInt(left.value), Data.encodeInt(right.value)] as Script) + .concat(toOps.fromBinaryOp(op, true)); + const res = executeScriptOnVM(script)[0]; + return returnsBool(op) + ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) + : new IntLiteralNode(Data.decodeInt(Buffer.from(res), 5)); +} + +function applyBinaryOperatorToString( + left: StringLiteralNode, + op: BinaryOperator, + right: StringLiteralNode, +): LiteralNode { + const script: Script = ([Data.encodeString(left.value), Data.encodeString(right.value)] as Script) + .concat(toOps.fromBinaryOp(op)); + const res = executeScriptOnVM(script)[0]; + return returnsBool(op) + ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) + : new StringLiteralNode(Data.decodeString(Buffer.from(res)), left.quote); +} + +function applyBinaryOperatorToHex( + left: HexLiteralNode, + op: BinaryOperator, + right: HexLiteralNode, +): LiteralNode { + const script: Script = ([left.value, right.value] as Script) + .concat(toOps.fromBinaryOp(op)); + const res = executeScriptOnVM(script)[0]; + return returnsBool(op) + ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) + : new HexLiteralNode(Buffer.from(res)); +} diff --git a/packages/cashc/src/util.ts b/packages/cashc/src/util.ts index 6fb0f706..f9503063 100644 --- a/packages/cashc/src/util.ts +++ b/packages/cashc/src/util.ts @@ -18,15 +18,21 @@ export const Data = { encodeBool(b: boolean): Buffer { return b ? this.encodeInt(1) : this.encodeInt(0); }, + decodeBool(b: Buffer): boolean { + return this.decodeInt(b) !== 0; + }, encodeInt(i: number): Buffer { return new BScript().encodeNumber(i); }, - decodeInt(i: Buffer): number { - return new BScript().decodeNumber(i); + decodeInt(i: Buffer, maxLength?: number): number { + return new BScript().decodeNumber(i, maxLength); }, encodeString(s: string): Buffer { return Buffer.from(s, 'ascii'); }, + decodeString(s: Buffer): string { + return s.toString('ascii'); + }, scriptToAsm(s: Script): string { return new BScript().toASM(new BScript().encode(s)); }, diff --git a/packages/cashc/test/optimisations/fixtures.ts b/packages/cashc/test/optimisations/fixtures.ts new file mode 100644 index 00000000..ebc681c3 --- /dev/null +++ b/packages/cashc/test/optimisations/fixtures.ts @@ -0,0 +1,194 @@ +import { BinaryOperator } from '../../src/ast/Operator'; + +const MAXINT = 2147483647; + +enum Error { + DIVIDE_BY_ZERO = 'Program attempted to divide a number by zero.', + INVALID_SCRIPT_NUMBER = 'Invalid input: this operation requires a valid Script Number.', + ATTEMPTED_BIG_PUSH = 'Program attempted to push a stack item which exceeded the maximum stack item length (520 bytes).', +} + +export const fixtures = { + applyUnaryOperator: { + bool: { + success: [ + ['should apply !true', true, false], + ['should apply !false', false, true], + ], + }, + int: { + success: [ + ['should apply -10', 10, -10], + ['should apply -(-10)', -10, 10], + ['should apply -MAXINT', MAXINT, -MAXINT], + ['should apply -(-MAXINT)', -MAXINT, MAXINT], + ['should apply -0', 0, 0], + ['should apply -(-0)', -0, 0], + ], + fail: [ + ['should fail on -(MAXINT + 1)', MAXINT + 1, Error.INVALID_SCRIPT_NUMBER], + ['should fail on -(-MAXINT - 1)', -MAXINT - 1, Error.INVALID_SCRIPT_NUMBER], + ], + }, + }, + applyBinaryOperator: { + bool: { + success: [ + ['should apply true && false', true, BinaryOperator.AND, false, false], + ['should apply true && true', true, BinaryOperator.AND, true, true], + ['should apply false && false', false, BinaryOperator.AND, false, false], + ['should apply true || false', true, BinaryOperator.OR, false, true], + ['should apply true || true', true, BinaryOperator.OR, true, true], + ['should apply false || false', false, BinaryOperator.OR, false, false], + ], + }, + int: { + success: [ + // DIV + ['should apply 27 / 7', 27, BinaryOperator.DIV, 7, 3], + ['should apply 27 / (-7)', 27, BinaryOperator.DIV, -7, -3], + ['should apply (-27) / 7', -27, BinaryOperator.DIV, 7, -3], + ['should apply (-27) / (-7)', -27, BinaryOperator.DIV, -7, 3], + ['should apply MAXINT / MAXINT', MAXINT, BinaryOperator.DIV, MAXINT, 1], + ['should apply 1 / MAXINT', 1, BinaryOperator.DIV, MAXINT, 0], + ['should apply (-MAXINT) / MAXINT', -MAXINT, BinaryOperator.DIV, MAXINT, -1], + ['should apply (-1) / MAXINT', -1, BinaryOperator.DIV, MAXINT, 0], + // MOD + ['should apply 27 % 7', 27, BinaryOperator.MOD, 7, 6], + ['should apply 27 % (-7)', 27, BinaryOperator.MOD, -7, 6], + ['should apply (-27) % 7', -27, BinaryOperator.MOD, 7, -6], + ['should apply (-27) % (-7)', -27, BinaryOperator.MOD, -7, -6], + ['should apply MAXINT % MAXINT', MAXINT, BinaryOperator.MOD, MAXINT, 0], + ['should apply 1 % MAXINT', 1, BinaryOperator.MOD, MAXINT, 1], + ['should apply (-MAXINT) % MAXINT', -MAXINT, BinaryOperator.MOD, MAXINT, 0], + ['should apply (-1) % MAXINT', -1, BinaryOperator.MOD, MAXINT, -1], + // PLUS + ['should apply 27 + 7', 27, BinaryOperator.PLUS, 7, 34], + ['should apply 27 + (-7)', 27, BinaryOperator.PLUS, -7, 20], + ['should apply (-27) + 7', -27, BinaryOperator.PLUS, 7, -20], + ['should apply (-27) + (-7)', -27, BinaryOperator.PLUS, -7, -34], + ['should apply 1 + MAXINT', 1, BinaryOperator.PLUS, MAXINT, MAXINT + 1], + ['should apply (-MAXINT) + MAXINT', -MAXINT, BinaryOperator.PLUS, MAXINT, 0], + // MINUS + ['should apply 27 - 7', 27, BinaryOperator.MINUS, 7, 20], + ['should apply 27 - (-7)', 27, BinaryOperator.MINUS, -7, 34], + ['should apply (-27) - 7', -27, BinaryOperator.MINUS, 7, -34], + ['should apply (-27) - (-7)', -27, BinaryOperator.MINUS, -7, -20], + ['should apply (-1) - MAXINT', -1, BinaryOperator.MINUS, MAXINT, -MAXINT - 1], + ['should apply MAXINT - MAXINT', MAXINT, BinaryOperator.MINUS, MAXINT, 0], + // LT + ['should apply 5 < 4', 5, BinaryOperator.LT, 4, false], + ['should apply 4 < 4', 4, BinaryOperator.LT, 4, false], + ['should apply 3 < 4', 3, BinaryOperator.LT, 4, true], + // LE + ['should apply 5 <= 4', 5, BinaryOperator.LE, 4, false], + ['should apply 4 <= 4', 4, BinaryOperator.LE, 4, true], + ['should apply 3 <= 4', 3, BinaryOperator.LE, 4, true], + // GT + ['should apply 5> 4', 5, BinaryOperator.GT, 4, true], + ['should apply 4> 4', 4, BinaryOperator.GT, 4, false], + ['should apply 3> 4', 3, BinaryOperator.GT, 4, false], + // GE + ['should apply 5>= 4', 5, BinaryOperator.GE, 4, true], + ['should apply 4>= 4', 4, BinaryOperator.GE, 4, true], + ['should apply 3>= 4', 3, BinaryOperator.GE, 4, false], + // EQ + ['should apply 5 == 4', 5, BinaryOperator.EQ, 4, false], + ['should apply 4 == 4', 4, BinaryOperator.EQ, 4, true], + ['should apply 3 == 4', 3, BinaryOperator.EQ, 4, false], + // NE + ['should apply 5 != 4', 5, BinaryOperator.NE, 4, true], + ['should apply 4 != 4', 4, BinaryOperator.NE, 4, false], + ['should apply 3 != 4', 3, BinaryOperator.NE, 4, true], + ], + fail: [ + // DIV + ['should fail on 27 / 0', 27, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], + ['should fail on MAXINT / 0', MAXINT, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], + ['should fail on (-MAXINT) / 0', -MAXINT, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], + ['should fail on 0 / 0', 0, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], + ['should fail on (MAXINT + 1) / 2', MAXINT + 1, BinaryOperator.DIV, 2, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) / 2', -MAXINT - 1, BinaryOperator.DIV, 2, Error.INVALID_SCRIPT_NUMBER], + // MOD + ['should fail on 27 % 0', 27, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], + ['should fail on MAXINT % 0', MAXINT, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], + ['should fail on (-MAXINT) % 0', -MAXINT, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], + ['should fail on 0 % 0', 0, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], + ['should fail on (MAXINT + 1) % 2', MAXINT + 1, BinaryOperator.MOD, 2, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) % 2', -MAXINT - 1, BinaryOperator.MOD, 2, Error.INVALID_SCRIPT_NUMBER], + // PLUS + ['should fail on (MAXINT + 1) + 1', MAXINT + 1, BinaryOperator.PLUS, 1, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) + 1', -MAXINT - 1, BinaryOperator.PLUS, 1, Error.INVALID_SCRIPT_NUMBER], + // MINUS + ['should fail on (MAXINT + 1) - 1', MAXINT + 1, BinaryOperator.MINUS, 1, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) - 1', -MAXINT - 1, BinaryOperator.MINUS, 1, Error.INVALID_SCRIPT_NUMBER], + // LT + ['should fail on MAXINT + 1 < 4', MAXINT + 1, BinaryOperator.LT, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) < 4', -MAXINT - 1, BinaryOperator.LT, 4, Error.INVALID_SCRIPT_NUMBER], + // LE + ['should fail on MAXINT + 1 <= 4', MAXINT + 1, BinaryOperator.LE, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) <= 4', -MAXINT - 1, BinaryOperator.LE, 4, Error.INVALID_SCRIPT_NUMBER], + // GT + ['should fail on MAXINT + 1> 4', MAXINT + 1, BinaryOperator.GT, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1)> 4', -MAXINT - 1, BinaryOperator.GT, 4, Error.INVALID_SCRIPT_NUMBER], + // GE + ['should fail on MAXINT + 1>= 4', MAXINT + 1, BinaryOperator.GE, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1)>= 4', -MAXINT - 1, BinaryOperator.GE, 4, Error.INVALID_SCRIPT_NUMBER], + // EQ + ['should fail on MAXINT + 1 == 4', MAXINT + 1, BinaryOperator.EQ, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) == 4', -MAXINT - 1, BinaryOperator.EQ, 4, Error.INVALID_SCRIPT_NUMBER], + // NE + ['should fail on MAXINT + 1 != 4', MAXINT + 1, BinaryOperator.NE, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) != 4', -MAXINT - 1, BinaryOperator.NE, 4, Error.INVALID_SCRIPT_NUMBER], + ], + }, + string: { + success: [ + // PLUS + ['should apply "Chancellor on brink of " + "second bailout for banks"', + 'Chancellor on brink of ', BinaryOperator.PLUS, 'second bailout for banks', + 'Chancellor on brink of second bailout for banks'], + ['should apply "" + ""', '', BinaryOperator.PLUS, '', ''], + ['should apply almost_maxlen_x + "X"', 'X'.repeat(519), BinaryOperator.PLUS, 'X', 'X'.repeat(520)], + ['should apply maxlen_x + ""', 'X'.repeat(520), BinaryOperator.PLUS, '', 'X'.repeat(520)], + ['should apply "" + maxlen_x', '', BinaryOperator.PLUS, 'X'.repeat(520), 'X'.repeat(520)], + // EQ + ['should apply "BCH" == "BCH"', 'BCH', BinaryOperator.EQ, 'BCH', true], + ['should apply "BCH" == "BTC"', 'BCH', BinaryOperator.EQ, 'BTC', false], + ['should apply "BCH" == "BSV"', 'BCH', BinaryOperator.EQ, 'BSV', false], + // NE + ['should apply "BCH" != "BCH"', 'BCH', BinaryOperator.NE, 'BCH', false], + ['should apply "BCH" != "BTC"', 'BCH', BinaryOperator.NE, 'BTC', true], + ['should apply "BCH" != "BSV"', 'BCH', BinaryOperator.NE, 'BSV', true], + ], + fail: [ + // PLUS + ['should fail on maxlen_x + "X"', 'X'.repeat(520), BinaryOperator.PLUS, 'X', Error.ATTEMPTED_BIG_PUSH], + ['should fail on large_x + large_y', 'X'.repeat(260), BinaryOperator.PLUS, 'Y'.repeat(261), Error.ATTEMPTED_BIG_PUSH], + ], + }, + hex: { + success: [ + // PLUS + ['should apply 0xdead + 0xbeef', 'dead', BinaryOperator.PLUS, 'beef', 'deadbeef'], + ['should apply 0x + 0x', '', BinaryOperator.PLUS, '', ''], + ['should apply almost_maxlen_x + 0x58', '58'.repeat(519), BinaryOperator.PLUS, '58', '58'.repeat(520)], + ['should apply maxlen_x + 0x', '58'.repeat(520), BinaryOperator.PLUS, '', '58'.repeat(520)], + ['should apply 0x + maxlen_x', '', BinaryOperator.PLUS, '58'.repeat(520), '58'.repeat(520)], + // EQ + ['should apply 0x424348 == 0x424348', '424348', BinaryOperator.EQ, '424348', true], + ['should apply 0x424348 == 0x425443', '424348', BinaryOperator.EQ, '425443', false], + ['should apply 0x424348 == 0x425356', '424348', BinaryOperator.EQ, '425356', false], + // NE + ['should apply 0x424348 != 0x424348', '424348', BinaryOperator.NE, '424348', false], + ['should apply 0x424348 != 0x425443', '424348', BinaryOperator.NE, '425443', true], + ['should apply 0x424348 != 0x425356', '424348', BinaryOperator.NE, '425356', true], + ], + fail: [ + // PLUS + ['should fail on maxlen_x + 0x58', '58'.repeat(520), BinaryOperator.PLUS, '58', Error.ATTEMPTED_BIG_PUSH], + ['should fail on large_x + large_y', '58'.repeat(260), BinaryOperator.PLUS, '59'.repeat(261), Error.ATTEMPTED_BIG_PUSH], + ], + }, + }, +}; diff --git a/packages/cashc/test/optimisations/simulation.test.ts b/packages/cashc/test/optimisations/simulation.test.ts new file mode 100644 index 00000000..3cab16d1 --- /dev/null +++ b/packages/cashc/test/optimisations/simulation.test.ts @@ -0,0 +1,177 @@ +/* simulation.test.ts + * + * - This file is used to test the simulation of operations + */ + +import delay from 'delay'; +import { applyUnaryOperator, applyBinaryOperator } from '../../src/optimisations/OperationSimulations'; +import { UnaryOperator, BinaryOperator } from '../../src/ast/Operator'; +import { + IntLiteralNode, + BoolLiteralNode, + StringLiteralNode, + HexLiteralNode, +} from '../../src/ast/AST'; +import { fixtures } from './fixtures'; +import { ExecutionError } from '../../src/Errors'; + +describe('Operation simulation', () => { + beforeAll(async () => { + await delay(100); + }); + + describe('applyUnaryOperator', () => { + fixtures.applyUnaryOperator.bool.success.forEach(([should, input, expected]: any) => { + it(should as string, () => { + // when + const res = applyUnaryOperator(UnaryOperator.NOT, new BoolLiteralNode(input as boolean)); + + // then + expect(res).toBeInstanceOf(BoolLiteralNode); + expect((res as BoolLiteralNode).value).toBe(expected); + }); + }); + + fixtures.applyUnaryOperator.int.success.forEach(([should, input, expected]: any) => { + it(should as string, () => { + // when + const res = applyUnaryOperator(UnaryOperator.NEGATE, new IntLiteralNode(input as number)); + + // then + expect(res).toBeInstanceOf(IntLiteralNode); + expect((res as IntLiteralNode).value).toBe(expected); + }); + }); + + fixtures.applyUnaryOperator.int.fail.forEach(([should, input, expected]: any) => { + it(should as string, () => { + expect(() => { + // when + applyUnaryOperator(UnaryOperator.NEGATE, new IntLiteralNode(input as number)); + + // then + }).toThrow(new ExecutionError(expected as string)); + }); + }); + }); + + describe('applyBinaryOperator', () => { + fixtures.applyBinaryOperator.bool.success + .forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + // when + const res = applyBinaryOperator( + new BoolLiteralNode(left as boolean), + op as BinaryOperator, + new BoolLiteralNode(right as boolean), + ); + + // then + expect(res).toBeInstanceOf(BoolLiteralNode); + expect((res as BoolLiteralNode).value).toBe(expected); + }); + }); + + fixtures.applyBinaryOperator.int.success + .forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + // when + const res = applyBinaryOperator( + new IntLiteralNode(left as number), + op as BinaryOperator, + new IntLiteralNode(right as number), + ); + + // then + const expectedNode = typeof expected === 'boolean' + ? new BoolLiteralNode(expected) + : new IntLiteralNode(expected); + expect(res).toEqual(expectedNode); + }); + }); + + fixtures.applyBinaryOperator.int.fail + .forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + expect(() => { + // when + applyBinaryOperator( + new IntLiteralNode(left as number), + op as BinaryOperator, + new IntLiteralNode(right as number), + ); + + // then + }).toThrow(new ExecutionError(expected as string)); + }); + }); + + fixtures.applyBinaryOperator.string.success + .forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + // when + const res = applyBinaryOperator( + new StringLiteralNode(left as string, '"'), + op as BinaryOperator, + new StringLiteralNode(right as string, '"'), + ); + + // then + const expectedNode = typeof expected === 'boolean' + ? new BoolLiteralNode(expected) + : new StringLiteralNode(expected, '"'); + expect(res).toEqual(expectedNode); + }); + }); + + fixtures.applyBinaryOperator.string.fail + .forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + expect(() => { + // when + applyBinaryOperator( + new StringLiteralNode(left as string, '"'), + op as BinaryOperator, + new StringLiteralNode(right as string, '"'), + ); + + // then + }).toThrow(new ExecutionError(expected as string)); + }); + }); + + fixtures.applyBinaryOperator.hex.success + .forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + // when + const res = applyBinaryOperator( + new HexLiteralNode(Buffer.from(left, 'hex')), + op as BinaryOperator, + new HexLiteralNode(Buffer.from(right, 'hex')), + ); + + // then + const expectedNode = typeof expected === 'boolean' + ? new BoolLiteralNode(expected) + : new HexLiteralNode(Buffer.from(expected, 'hex')); + expect(res).toEqual(expectedNode); + }); + }); + + fixtures.applyBinaryOperator.hex.fail + .forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + expect(() => { + // when + applyBinaryOperator( + new HexLiteralNode(Buffer.from(left, 'hex')), + op as BinaryOperator, + new HexLiteralNode(Buffer.from(right, 'hex')), + ); + + // then + }).toThrow(new ExecutionError(expected as string)); + }); + }); + }); +}); From b5a3938e3900f5aa8da0c9fe987359d903689a6d Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月24日 14:48:28 +0100 Subject: [PATCH 03/14] Add simulation of global function calls --- packages/cashc/src/generation/Script.ts | 47 +++++--- .../src/optimisations/OperationSimulations.ts | 50 ++++++++- packages/cashc/test/optimisations/fixtures.ts | 100 ++++++++++++++++++ .../test/optimisations/simulation.test.ts | 43 +++++++- packages/cashc/test/test-util.ts | 19 ++++ 5 files changed, 238 insertions(+), 21 deletions(-) diff --git a/packages/cashc/src/generation/Script.ts b/packages/cashc/src/generation/Script.ts index 9d108eea..f4cc47ad 100644 --- a/packages/cashc/src/generation/Script.ts +++ b/packages/cashc/src/generation/Script.ts @@ -86,20 +86,35 @@ export class toOps { } } -export function returnsBool(op: GlobalFunction | BinaryOperator | UnaryOperator): boolean { - return [ - GlobalFunction.CHECKDATASIG, - GlobalFunction.CHECKMULTISIG, - GlobalFunction.CHECKSIG, - GlobalFunction.WITHIN, - BinaryOperator.LT, - BinaryOperator.LE, - BinaryOperator.GT, - BinaryOperator.GE, - BinaryOperator.EQ, - BinaryOperator.NE, - BinaryOperator.AND, - BinaryOperator.OR, - UnaryOperator.NOT, - ].includes(op); +export function returnType(op: GlobalFunction | BinaryOperator | UnaryOperator): Type { + const mapping = { + [GlobalFunction.ABS]: PrimitiveType.INT, + [GlobalFunction.CHECKDATASIG]: PrimitiveType.BOOL, + [GlobalFunction.CHECKMULTISIG]: PrimitiveType.BOOL, + [GlobalFunction.CHECKSIG]: PrimitiveType.BOOL, + [GlobalFunction.HASH160]: new BytesType(20), + [GlobalFunction.HASH256]: new BytesType(32), + [GlobalFunction.MAX]: PrimitiveType.INT, + [GlobalFunction.MIN]: PrimitiveType.INT, + [GlobalFunction.REQUIRE]: PrimitiveType.ANY, // TODO: void + [GlobalFunction.RIPEMD160]: new BytesType(20), + [GlobalFunction.SHA1]: new BytesType(32), + [GlobalFunction.SHA256]: new BytesType(32), + [GlobalFunction.WITHIN]: PrimitiveType.BOOL, + [BinaryOperator.DIV]: PrimitiveType.INT, + [BinaryOperator.MINUS]: PrimitiveType.INT, + [BinaryOperator.MOD]: PrimitiveType.INT, + [BinaryOperator.PLUS]: PrimitiveType.ANY, // TODO: int/string/bytes + [BinaryOperator.LT]: PrimitiveType.BOOL, + [BinaryOperator.LE]: PrimitiveType.BOOL, + [BinaryOperator.GT]: PrimitiveType.BOOL, + [BinaryOperator.GE]: PrimitiveType.BOOL, + [BinaryOperator.EQ]: PrimitiveType.BOOL, + [BinaryOperator.NE]: PrimitiveType.BOOL, + [BinaryOperator.AND]: PrimitiveType.BOOL, + [BinaryOperator.OR]: PrimitiveType.BOOL, + [UnaryOperator.NOT]: PrimitiveType.BOOL, + [UnaryOperator.NEGATE]: PrimitiveType.INT, + }; + return mapping[op]; } diff --git a/packages/cashc/src/optimisations/OperationSimulations.ts b/packages/cashc/src/optimisations/OperationSimulations.ts index a04d6248..18ae2451 100644 --- a/packages/cashc/src/optimisations/OperationSimulations.ts +++ b/packages/cashc/src/optimisations/OperationSimulations.ts @@ -14,10 +14,14 @@ import { IntLiteralNode, HexLiteralNode, StringLiteralNode, + FunctionCallNode, + Node, } from '../ast/AST'; +import { GlobalFunction } from '../ast/Globals'; import { Data } from '../util'; -import { Script, toOps, returnsBool } from '../generation/Script'; +import { Script, toOps, returnType } from '../generation/Script'; import { ExecutionError } from '../Errors'; +import { PrimitiveType, BytesType } from '../ast/Type'; type BCH_VM = AuthenticationVirtualMachine; let vm: BCH_VM; @@ -107,7 +111,7 @@ function applyBinaryOperatorToInt( const script: Script = ([Data.encodeInt(left.value), Data.encodeInt(right.value)] as Script) .concat(toOps.fromBinaryOp(op, true)); const res = executeScriptOnVM(script)[0]; - return returnsBool(op) + return returnType(op) === PrimitiveType.BOOL ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) : new IntLiteralNode(Data.decodeInt(Buffer.from(res), 5)); } @@ -120,7 +124,7 @@ function applyBinaryOperatorToString( const script: Script = ([Data.encodeString(left.value), Data.encodeString(right.value)] as Script) .concat(toOps.fromBinaryOp(op)); const res = executeScriptOnVM(script)[0]; - return returnsBool(op) + return returnType(op) === PrimitiveType.BOOL ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) : new StringLiteralNode(Data.decodeString(Buffer.from(res)), left.quote); } @@ -133,7 +137,45 @@ function applyBinaryOperatorToHex( const script: Script = ([left.value, right.value] as Script) .concat(toOps.fromBinaryOp(op)); const res = executeScriptOnVM(script)[0]; - return returnsBool(op) + return returnType(op) === PrimitiveType.BOOL ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) : new HexLiteralNode(Buffer.from(res)); } + +export function applyGlobalFunction(node: FunctionCallNode): Node { + const { parameters } = node; + const fn = node.identifier.name as GlobalFunction; + + // Don't apply checkMultiSig or require for now + if (fn === GlobalFunction.CHECKMULTISIG || fn === GlobalFunction.REQUIRE) { + return node; + } + + let script: Script = parameters.map((p) => { + if (p instanceof BoolLiteralNode) { + return Data.encodeBool(p.value); + } else if (p instanceof IntLiteralNode) { + return Data.encodeInt(p.value); + } else if (p instanceof StringLiteralNode) { + return Data.encodeString(p.value); + } else if (p instanceof HexLiteralNode) { + return p.value; + } else { + throw new Error(); // Already checked in typecheck + } + }); + script = script.concat(toOps.fromFunction(node.identifier.name as GlobalFunction)); + + const res = executeScriptOnVM(script)[0]; + if (returnType(fn) === PrimitiveType.BOOL) { + return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); + } else if (returnType(fn) === PrimitiveType.INT) { + return new IntLiteralNode(Data.decodeInt(Buffer.from(res))); + } else if (returnType(fn) === PrimitiveType.STRING) { + return new StringLiteralNode(Data.decodeString(Buffer.from(res)), '"'); + } else if (returnType(fn) instanceof BytesType) { + return new HexLiteralNode(Buffer.from(res)); + } else { + throw new Error(); // Already checked in typecheck + } +} diff --git a/packages/cashc/test/optimisations/fixtures.ts b/packages/cashc/test/optimisations/fixtures.ts index ebc681c3..cae3a8c1 100644 --- a/packages/cashc/test/optimisations/fixtures.ts +++ b/packages/cashc/test/optimisations/fixtures.ts @@ -1,4 +1,5 @@ import { BinaryOperator } from '../../src/ast/Operator'; +import { GlobalFunction } from '../../src/ast/Globals'; const MAXINT = 2147483647; @@ -6,8 +7,19 @@ enum Error { DIVIDE_BY_ZERO = 'Program attempted to divide a number by zero.', INVALID_SCRIPT_NUMBER = 'Invalid input: this operation requires a valid Script Number.', ATTEMPTED_BIG_PUSH = 'Program attempted to push a stack item which exceeded the maximum stack item length (520 bytes).', + FAILED_VERIFY = 'Program failed an OP_VERIFY operation.', + IMPROPERLY_ENCODED_SIG = 'Encountered an improperly encoded signature.', + NULLFAIL = 'Program failed a signature verification with a non-null signature (violating the "NULLFAIL" rule).', } +// (https://kjur.github.io/jsrsasign/sample/sample-ecdsa.html) +const SigCheck = { + privateKey: '0xce0505a758bcdc077280ec5ba3784ebb379222bc174a43085f1175e07567ca48', + publicKey: '0x02037f06dfa5fce0aad70228758c6943d65dfda55fee3be6378803faca367f93a7', + message: '0x4372616967205772696768742069732061206c69617220616e642061206672617564', + signature: '0x304502210084aa14c5b64d398139123681eab5f7ce7c31ea2ef2617ac48a8e5d4c90b7802002200783fcf94417bf7c519126f70dea77caa097ed1e5d5a988f7c61799a72601e83', +}; + export const fixtures = { applyUnaryOperator: { bool: { @@ -191,4 +203,92 @@ export const fixtures = { ], }, }, + applyGlobalFunction: { + success: [ + // TODO REQUIRE (require statements are handled differently) + // ['should apply require(true)', GlobalFunction.REQUIRE, [true], undefined], + // ABS + ['should apply abs(1)', GlobalFunction.ABS, [1], 1], + ['should apply abs(-1)', GlobalFunction.ABS, [-1], 1], + ['should apply abs(0)', GlobalFunction.ABS, [0], 0], + ['should apply abs(-0)', GlobalFunction.ABS, [-0], 0], + ['should apply abs(MAXINT)', GlobalFunction.ABS, [MAXINT], MAXINT], + ['should apply abs(-MAXINT)', GlobalFunction.ABS, [-MAXINT], MAXINT], + // MIN + ['should apply min(42, 43)', GlobalFunction.MIN, [42, 43], 42], + ['should apply min(-41, -42)', GlobalFunction.MIN, [-41, -42], -42], + ['should apply min(42, 42)', GlobalFunction.MIN, [42, 42], 42], + ['should apply min(MAXINT, MAXINT - 1)', GlobalFunction.MIN, [MAXINT, MAXINT - 1], MAXINT - 1], + ['should apply min(-MAXINT, -MAXINT + 1)', GlobalFunction.MIN, [-MAXINT, -MAXINT + 1], -MAXINT], + // MAX + ['should apply max(41, 42)', GlobalFunction.MAX, [41, 42], 42], + ['should apply max(-42, -43)', GlobalFunction.MAX, [-42, -43], -42], + ['should apply max(42, 42)', GlobalFunction.MAX, [42, 42], 42], + ['should apply max(MAXINT, MAXINT - 1)', GlobalFunction.MAX, [MAXINT, MAXINT - 1], MAXINT], + ['should apply max(-MAXINT, -MAXINT + 1)', GlobalFunction.MAX, [-MAXINT, -MAXINT + 1], -MAXINT + 1], + // WITHIN + ['should apply within(0, 0, 1)', GlobalFunction.WITHIN, [0, 0, 1], true], + ['should apply within(-1, 0, 1)', GlobalFunction.WITHIN, [-1, 0, 1], false], + ['should apply within(1, 0, 1)', GlobalFunction.WITHIN, [1, 0, 1], false], + ['should apply within(0, -MAXINT, MAXINT)', GlobalFunction.WITHIN, [0, -MAXINT, MAXINT], true], + // WITHIN + ['should apply within(0, 0, 1)', GlobalFunction.WITHIN, [0, 0, 1], true], + ['should apply within(-1, 0, 1)', GlobalFunction.WITHIN, [-1, 0, 1], false], + ['should apply within(1, 0, 1)', GlobalFunction.WITHIN, [1, 0, 1], false], + ['should apply within(0, -MAXINT, MAXINT)', GlobalFunction.WITHIN, [0, -MAXINT, MAXINT], true], + // RIPEMD160 + ['should apply ripemd160(MAXINT)', GlobalFunction.RIPEMD160, [MAXINT], '0x6f16403bdabf8cf81c538fad9ede667a79d110c2'], + ['should apply ripemd160(MAXINT + 1)', GlobalFunction.RIPEMD160, [MAXINT + 1], '0xa4fb59ff07445e291b2aadcfe5ff07e66bcd59c7'], + ['should apply ripemd160("Bitcoin Cash")', GlobalFunction.RIPEMD160, ['Bitcoin Cash'], '0x780e2f7d61a3656b798469deb77217c2f74fbe07'], + ['should apply ripemd160(0xbeef)', GlobalFunction.RIPEMD160, ['0xbeef'], '0x80bfa1e5e3df329b05042b3b00f6ceb823869644'], + // SHA1 + ['should apply sha1(MAXINT)', GlobalFunction.SHA1, [MAXINT], '0xf8cc915cc37c33ac4821bef67546385089e61a28'], + ['should apply sha1(MAXINT + 1)', GlobalFunction.SHA1, [MAXINT + 1], '0x8a15949805d1f859ee2eb8621285c8751cd4b840'], + ['should apply sha1("Bitcoin Cash")', GlobalFunction.SHA1, ['Bitcoin Cash'], '0xfec4fda3473ceb5ebf9e4c2144d00a50f4e3a326'], + ['should apply sha1(0xbeef)', GlobalFunction.SHA1, ['0xbeef'], '0x536391604f507559af68c402bff2ca9f8f5a0b66'], + // SHA256 + ['should apply sha256(MAXINT)', GlobalFunction.SHA256, [MAXINT], '0xa2c70538651a7e9296b097e8c3dfc1b195a945802ffe45aa471868fba6f1042e'], + ['should apply sha256(MAXINT + 1)', GlobalFunction.SHA256, [MAXINT + 1], '0xe78d3dd142c149e0d63d8456546e562ad0fcf348b1dbd2721fd599e82bea503a'], + ['should apply sha256("Bitcoin Cash")', GlobalFunction.SHA256, ['Bitcoin Cash'], '0x8a9851255d671c4e0ac3ad525ad0ff595cb31a1ad85327a77df3d15129b0a245'], + ['should apply sha256(0xbeef)', GlobalFunction.SHA256, ['0xbeef'], '0x17e117288642879110850b62f83cb13d07e7961e321c1c762ff5e5ab83029c7c'], + // HASH160 + ['should apply hash160(MAXINT)', GlobalFunction.HASH160, [MAXINT], '0xdcf9155897114017018437a6068c7de036fe16a2'], + ['should apply hash160(MAXINT + 1)', GlobalFunction.HASH160, [MAXINT + 1], '0x82b956f8d10ef7e818ea574b5f60b1e16d653273'], + ['should apply hash160("Bitcoin Cash")', GlobalFunction.HASH160, ['Bitcoin Cash'], '0x2d74b8d22f4c09d36f7d22e6cf1c796dcaee272a'], + ['should apply hash160(0xbeef)', GlobalFunction.HASH160, ['0xbeef'], '0x0514fea7ae9f4fa3c4d2db22b196ca26e1bffc94'], + // HASH256 + ['should apply hash256(MAXINT)', GlobalFunction.HASH256, [MAXINT], '0x13f876b544a4baf1867dcb3f52f2c096cea373e0659ab046f55596b045ac4ffb'], + ['should apply hash256(MAXINT + 1)', GlobalFunction.HASH256, [MAXINT + 1], '0x280846b322340fa8c0e4464d6cd8631b10685cf3a9a15325583cc934ac42bf84'], + ['should apply hash256("Bitcoin Cash")', GlobalFunction.HASH256, ['Bitcoin Cash'], '0x00d44bd6d8ba4fd7c154ac2c073394aa3a755334992fa1b254f3f4a984f6d0fb'], + ['should apply hash256(0xbeef)', GlobalFunction.HASH256, ['0xbeef'], '0x44ee00d701711b07ee3b4ddf7f165f6b09cd2e7c512d6d2a9a25aab487f2f740'], + // CHECKSIG (only check empty sig) + [`should apply checkSig(0x, ${SigCheck.publicKey})`, GlobalFunction.CHECKSIG, ['0x', SigCheck.publicKey], false], + // TODO CHECKMULTISIG (need to refactor code generation) + // CHECKDATASIG + [`should apply checkDataSig(${SigCheck.signature}, ${SigCheck.message}, ${SigCheck.publicKey})`, GlobalFunction.CHECKDATASIG, [SigCheck.signature, SigCheck.message, SigCheck.publicKey], true], + [`should apply checkDataSig(0x, ${SigCheck.message}, ${SigCheck.publicKey})`, GlobalFunction.CHECKDATASIG, ['0x', SigCheck.message, SigCheck.publicKey], false], + ], + fail: [ + // REQUIRE + // ['should fail on require(false)', GlobalFunction.REQUIRE, [false], 'TODOERROR'], + // ABS + ['should fail on abs(MAXINT + 1)', GlobalFunction.ABS, [MAXINT + 1], Error.INVALID_SCRIPT_NUMBER], + ['should fail on abs(-MAXINT - 1)', GlobalFunction.ABS, [-MAXINT - 1], Error.INVALID_SCRIPT_NUMBER], + // MIN + ['should fail on min(-MAXINT - 1, 0)', GlobalFunction.MIN, [-MAXINT - 1, 0], Error.INVALID_SCRIPT_NUMBER], + ['should fail on min(MAXINT + 1, 0)', GlobalFunction.MIN, [MAXINT + 1, 0], Error.INVALID_SCRIPT_NUMBER], + // MAX + ['should fail on max(-MAXINT - 1, 0)', GlobalFunction.MAX, [-MAXINT - 1, 0], Error.INVALID_SCRIPT_NUMBER], + ['should fail on max(MAXINT + 1, 0)', GlobalFunction.MAX, [MAXINT + 1, 0], Error.INVALID_SCRIPT_NUMBER], + // WITHIN + ['should fail on within(-MAXINT - 1, 0, 1)', GlobalFunction.WITHIN, [-MAXINT - 1, 0, 1], Error.INVALID_SCRIPT_NUMBER], + ['should fail on within(MAXINT + 1, 0, 1)', GlobalFunction.WITHIN, [MAXINT + 1, 0, 1], Error.INVALID_SCRIPT_NUMBER], + // CHECKSIG + [`should fail on checkSig(0x01, ${SigCheck.publicKey})`, GlobalFunction.CHECKSIG, ['0x01', SigCheck.publicKey], Error.IMPROPERLY_ENCODED_SIG], + [`should fail on checkSig(${SigCheck.signature}41, ${SigCheck.publicKey})`, GlobalFunction.CHECKSIG, [`${SigCheck.signature}41`, SigCheck.publicKey], Error.NULLFAIL], + // CHECKDATASIG + [`should fail on checkDataSig(0x01, ${SigCheck.message}, ${SigCheck.publicKey})`, GlobalFunction.CHECKDATASIG, ['0x01', SigCheck.message, SigCheck.publicKey], Error.IMPROPERLY_ENCODED_SIG], + [`should fail on checkDataSig(${SigCheck.signature.slice(0, -2)}00, ${SigCheck.message}, ${SigCheck.publicKey})`, GlobalFunction.CHECKDATASIG, [`${SigCheck.signature.slice(0, -2)}00`, SigCheck.message, SigCheck.publicKey], Error.NULLFAIL], + ], + }, }; diff --git a/packages/cashc/test/optimisations/simulation.test.ts b/packages/cashc/test/optimisations/simulation.test.ts index 3cab16d1..329b7ab4 100644 --- a/packages/cashc/test/optimisations/simulation.test.ts +++ b/packages/cashc/test/optimisations/simulation.test.ts @@ -4,16 +4,19 @@ */ import delay from 'delay'; -import { applyUnaryOperator, applyBinaryOperator } from '../../src/optimisations/OperationSimulations'; +import { applyUnaryOperator, applyBinaryOperator, applyGlobalFunction } from '../../src/optimisations/OperationSimulations'; import { UnaryOperator, BinaryOperator } from '../../src/ast/Operator'; import { IntLiteralNode, BoolLiteralNode, StringLiteralNode, HexLiteralNode, + FunctionCallNode, + IdentifierNode, } from '../../src/ast/AST'; import { fixtures } from './fixtures'; import { ExecutionError } from '../../src/Errors'; +import { literalToNode } from '../test-util'; describe('Operation simulation', () => { beforeAll(async () => { @@ -174,4 +177,42 @@ describe('Operation simulation', () => { }); }); }); + + describe('applyGlobalFunction', () => { + fixtures.applyGlobalFunction.success + .forEach(([should, fn, parameters, expected]: any) => { + it(should as string, () => { + // given + const parameterNodes = (parameters as (boolean | number | string)[]).map(literalToNode); + const expectedNode = typeof expected === 'undefined' ? undefined : literalToNode(expected); + + // when + const res = applyGlobalFunction(new FunctionCallNode( + new IdentifierNode(fn as string), + parameterNodes, + )); + + // then + expect(res).toEqual(expectedNode); + }); + }); + + fixtures.applyGlobalFunction.fail + .forEach(([should, fn, parameters, expected]: any) => { + it(should as string, () => { + expect(() => { + // given + const parameterNodes = (parameters as (boolean | number | string)[]).map(literalToNode); + + // when + applyGlobalFunction(new FunctionCallNode( + new IdentifierNode(fn as string), + parameterNodes, + )); + + // then + }).toThrow(new ExecutionError(expected as string)); + }); + }); + }); }); diff --git a/packages/cashc/test/test-util.ts b/packages/cashc/test/test-util.ts index a25551d9..b3fdd920 100644 --- a/packages/cashc/test/test-util.ts +++ b/packages/cashc/test/test-util.ts @@ -5,6 +5,25 @@ import fs from 'fs'; import path from 'path'; import { CashScriptParser } from '../src/grammar/CashScriptParser'; import { CashScriptLexer } from '../src/grammar/CashScriptLexer'; +import { + LiteralNode, + BoolLiteralNode, + IntLiteralNode, + HexLiteralNode, + StringLiteralNode, +} from '../src/ast/AST'; + +export function literalToNode(literal: boolean | number | string): LiteralNode { + if (typeof literal === 'boolean') { + return new BoolLiteralNode(literal); + } else if (typeof literal === 'number') { + return new IntLiteralNode(literal); + } else if (literal.startsWith('0x')) { + return new HexLiteralNode(Buffer.from(literal.slice(2), 'hex')); + } else { + return new StringLiteralNode(literal, '"'); + } +} export function getSubdirectories(directory: string): string[] { return fs.readdirSync(directory) From 014dded6a292337bac92703d9bd6e04bef576555 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月24日 14:55:17 +0100 Subject: [PATCH 04/14] Increase test delay for Operation Simulation --- packages/cashc/test/optimisations/simulation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cashc/test/optimisations/simulation.test.ts b/packages/cashc/test/optimisations/simulation.test.ts index 329b7ab4..a3e5de33 100644 --- a/packages/cashc/test/optimisations/simulation.test.ts +++ b/packages/cashc/test/optimisations/simulation.test.ts @@ -20,7 +20,7 @@ import { literalToNode } from '../test-util'; describe('Operation simulation', () => { beforeAll(async () => { - await delay(100); + await delay(1000); }); describe('applyUnaryOperator', () => { From 6facd03f4251e25191a239c5f454c699b1a2b60b Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月25日 17:33:46 +0100 Subject: [PATCH 05/14] Small changes - Re-order OperationSimulations.ts - Remove GlobalFunction.REQUIRE as it's unused --- packages/cashc/src/ast/Globals.ts | 1 - packages/cashc/src/generation/Script.ts | 2 - .../src/optimisations/OperationSimulations.ts | 127 ++++++++++-------- packages/cashc/test/optimisations/fixtures.ts | 4 - 4 files changed, 68 insertions(+), 66 deletions(-) diff --git a/packages/cashc/src/ast/Globals.ts b/packages/cashc/src/ast/Globals.ts index 8b6b2106..a8514c13 100644 --- a/packages/cashc/src/ast/Globals.ts +++ b/packages/cashc/src/ast/Globals.ts @@ -16,7 +16,6 @@ export const NumberUnit: { [index:string] : number } = { export enum GlobalFunction { - REQUIRE = 'require', ABS = 'abs', MIN = 'min', MAX = 'max', diff --git a/packages/cashc/src/generation/Script.ts b/packages/cashc/src/generation/Script.ts index f4cc47ad..042974d4 100644 --- a/packages/cashc/src/generation/Script.ts +++ b/packages/cashc/src/generation/Script.ts @@ -39,7 +39,6 @@ export class toOps { [GlobalFunction.CHECKSIG]: [Op.OP_CHECKSIG], [GlobalFunction.MAX]: [Op.OP_MAX], [GlobalFunction.MIN]: [Op.OP_MIN], - [GlobalFunction.REQUIRE]: [Op.OP_VERIFY], [GlobalFunction.RIPEMD160]: [Op.OP_RIPEMD160], [GlobalFunction.SHA1]: [Op.OP_SHA1], [GlobalFunction.SHA256]: [Op.OP_SHA256], @@ -96,7 +95,6 @@ export function returnType(op: GlobalFunction | BinaryOperator | UnaryOperator): [GlobalFunction.HASH256]: new BytesType(32), [GlobalFunction.MAX]: PrimitiveType.INT, [GlobalFunction.MIN]: PrimitiveType.INT, - [GlobalFunction.REQUIRE]: PrimitiveType.ANY, // TODO: void [GlobalFunction.RIPEMD160]: new BytesType(20), [GlobalFunction.SHA1]: new BytesType(32), [GlobalFunction.SHA256]: new BytesType(32), diff --git a/packages/cashc/src/optimisations/OperationSimulations.ts b/packages/cashc/src/optimisations/OperationSimulations.ts index 18ae2451..482dd47b 100644 --- a/packages/cashc/src/optimisations/OperationSimulations.ts +++ b/packages/cashc/src/optimisations/OperationSimulations.ts @@ -41,39 +41,53 @@ export function executeScriptOnVM(script: Script): Uint8Array[] { return res.stack; } -export function applyUnaryOperator( - op: UnaryOperator, - expr: LiteralNode, -): LiteralNode { - if (expr instanceof BoolLiteralNode) { - return applyUnaryOperatorToBool(op, expr); - } else if (expr instanceof IntLiteralNode) { - return applyUnaryOperatorToInt(op, expr); - } else { - throw new Error(); // Already checked in typecheck +// TODO: RequireNode +// TODO: BranchNode +// TODO: CastNode + +export function applyGlobalFunction(node: FunctionCallNode): Node { + const { parameters } = node; + const fn = node.identifier.name as GlobalFunction; + + // TODO: Apply checkMultiSig (requires code generation refactor) + // This is not very important, since checkMultiSig likely requires external args + if (fn === GlobalFunction.CHECKMULTISIG) { + return node; } -} -function applyUnaryOperatorToBool( - op: UnaryOperator, - expr: BoolLiteralNode, -): BoolLiteralNode { - const script: Script = ([Data.encodeBool(expr.value)] as Script) - .concat(toOps.fromUnaryOp(op)); - const res = executeScriptOnVM(script)[0]; - return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); -} + let script: Script = parameters.map((p) => { + if (p instanceof BoolLiteralNode) { + return Data.encodeBool(p.value); + } else if (p instanceof IntLiteralNode) { + return Data.encodeInt(p.value); + } else if (p instanceof StringLiteralNode) { + return Data.encodeString(p.value); + } else if (p instanceof HexLiteralNode) { + return p.value; + } else { + throw new Error(); // Already checked in typecheck + } + }); + script = script.concat(toOps.fromFunction(node.identifier.name as GlobalFunction)); -function applyUnaryOperatorToInt( - op: UnaryOperator, - expr: IntLiteralNode, -): IntLiteralNode { - const script: Script = ([Data.encodeInt(expr.value)] as Script) - .concat(toOps.fromUnaryOp(op)); const res = executeScriptOnVM(script)[0]; - return new IntLiteralNode(Data.decodeInt(Buffer.from(res))); + if (returnType(fn) === PrimitiveType.BOOL) { + return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); + } else if (returnType(fn) === PrimitiveType.INT) { + return new IntLiteralNode(Data.decodeInt(Buffer.from(res))); + } else if (returnType(fn) === PrimitiveType.STRING) { + return new StringLiteralNode(Data.decodeString(Buffer.from(res)), '"'); + } else if (returnType(fn) instanceof BytesType) { + return new HexLiteralNode(Buffer.from(res)); + } else { + throw new Error(); // Already checked in typecheck + } } +// TODO: InstantiationNode +// TODO: TupleIndexOpNode + SplitOpNode +// TODO: SizeOpNode + export function applyBinaryOperator( left: LiteralNode, op: BinaryOperator, @@ -142,40 +156,35 @@ function applyBinaryOperatorToHex( : new HexLiteralNode(Buffer.from(res)); } -export function applyGlobalFunction(node: FunctionCallNode): Node { - const { parameters } = node; - const fn = node.identifier.name as GlobalFunction; - - // Don't apply checkMultiSig or require for now - if (fn === GlobalFunction.CHECKMULTISIG || fn === GlobalFunction.REQUIRE) { - return node; +export function applyUnaryOperator( + op: UnaryOperator, + expr: LiteralNode, +): LiteralNode { + if (expr instanceof BoolLiteralNode) { + return applyUnaryOperatorToBool(op, expr); + } else if (expr instanceof IntLiteralNode) { + return applyUnaryOperatorToInt(op, expr); + } else { + throw new Error(); // Already checked in typecheck } +} - let script: Script = parameters.map((p) => { - if (p instanceof BoolLiteralNode) { - return Data.encodeBool(p.value); - } else if (p instanceof IntLiteralNode) { - return Data.encodeInt(p.value); - } else if (p instanceof StringLiteralNode) { - return Data.encodeString(p.value); - } else if (p instanceof HexLiteralNode) { - return p.value; - } else { - throw new Error(); // Already checked in typecheck - } - }); - script = script.concat(toOps.fromFunction(node.identifier.name as GlobalFunction)); +function applyUnaryOperatorToBool( + op: UnaryOperator, + expr: BoolLiteralNode, +): BoolLiteralNode { + const script: Script = ([Data.encodeBool(expr.value)] as Script) + .concat(toOps.fromUnaryOp(op)); + const res = executeScriptOnVM(script)[0]; + return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); +} +function applyUnaryOperatorToInt( + op: UnaryOperator, + expr: IntLiteralNode, +): IntLiteralNode { + const script: Script = ([Data.encodeInt(expr.value)] as Script) + .concat(toOps.fromUnaryOp(op)); const res = executeScriptOnVM(script)[0]; - if (returnType(fn) === PrimitiveType.BOOL) { - return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); - } else if (returnType(fn) === PrimitiveType.INT) { - return new IntLiteralNode(Data.decodeInt(Buffer.from(res))); - } else if (returnType(fn) === PrimitiveType.STRING) { - return new StringLiteralNode(Data.decodeString(Buffer.from(res)), '"'); - } else if (returnType(fn) instanceof BytesType) { - return new HexLiteralNode(Buffer.from(res)); - } else { - throw new Error(); // Already checked in typecheck - } + return new IntLiteralNode(Data.decodeInt(Buffer.from(res))); } diff --git a/packages/cashc/test/optimisations/fixtures.ts b/packages/cashc/test/optimisations/fixtures.ts index cae3a8c1..9df1949a 100644 --- a/packages/cashc/test/optimisations/fixtures.ts +++ b/packages/cashc/test/optimisations/fixtures.ts @@ -205,8 +205,6 @@ export const fixtures = { }, applyGlobalFunction: { success: [ - // TODO REQUIRE (require statements are handled differently) - // ['should apply require(true)', GlobalFunction.REQUIRE, [true], undefined], // ABS ['should apply abs(1)', GlobalFunction.ABS, [1], 1], ['should apply abs(-1)', GlobalFunction.ABS, [-1], 1], @@ -269,8 +267,6 @@ export const fixtures = { [`should apply checkDataSig(0x, ${SigCheck.message}, ${SigCheck.publicKey})`, GlobalFunction.CHECKDATASIG, ['0x', SigCheck.message, SigCheck.publicKey], false], ], fail: [ - // REQUIRE - // ['should fail on require(false)', GlobalFunction.REQUIRE, [false], 'TODOERROR'], // ABS ['should fail on abs(MAXINT + 1)', GlobalFunction.ABS, [MAXINT + 1], Error.INVALID_SCRIPT_NUMBER], ['should fail on abs(-MAXINT - 1)', GlobalFunction.ABS, [-MAXINT - 1], Error.INVALID_SCRIPT_NUMBER], From 8c13f32114ffe473df5f1230cd171495c555da54 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月25日 17:36:30 +0100 Subject: [PATCH 06/14] Fix sha1 return type, was bytes32, should be bytes20 --- packages/cashc/src/ast/Globals.ts | 2 +- packages/cashc/src/generation/Script.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cashc/src/ast/Globals.ts b/packages/cashc/src/ast/Globals.ts index a8514c13..fe09df94 100644 --- a/packages/cashc/src/ast/Globals.ts +++ b/packages/cashc/src/ast/Globals.ts @@ -97,7 +97,7 @@ GLOBAL_SYMBOL_TABLE.set( Symbol.function(GlobalFunction.RIPEMD160, new BytesType(20), [PrimitiveType.ANY]), ); GLOBAL_SYMBOL_TABLE.set( - Symbol.function(GlobalFunction.SHA1, new BytesType(32), [PrimitiveType.ANY]), + Symbol.function(GlobalFunction.SHA1, new BytesType(20), [PrimitiveType.ANY]), ); GLOBAL_SYMBOL_TABLE.set( Symbol.function(GlobalFunction.SHA256, new BytesType(32), [PrimitiveType.ANY]), diff --git a/packages/cashc/src/generation/Script.ts b/packages/cashc/src/generation/Script.ts index 042974d4..ae169b70 100644 --- a/packages/cashc/src/generation/Script.ts +++ b/packages/cashc/src/generation/Script.ts @@ -96,7 +96,7 @@ export function returnType(op: GlobalFunction | BinaryOperator | UnaryOperator): [GlobalFunction.MAX]: PrimitiveType.INT, [GlobalFunction.MIN]: PrimitiveType.INT, [GlobalFunction.RIPEMD160]: new BytesType(20), - [GlobalFunction.SHA1]: new BytesType(32), + [GlobalFunction.SHA1]: new BytesType(20), [GlobalFunction.SHA256]: new BytesType(32), [GlobalFunction.WITHIN]: PrimitiveType.BOOL, [BinaryOperator.DIV]: PrimitiveType.INT, From d41f874ca05717758299f4289f90278f6e126df8 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月26日 18:44:13 +0100 Subject: [PATCH 07/14] BREAKING: casting int to bytes only calls num2bin if a size is passed --- packages/cashc/src/generation/Script.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cashc/src/generation/Script.ts b/packages/cashc/src/generation/Script.ts index ae169b70..950daec9 100644 --- a/packages/cashc/src/generation/Script.ts +++ b/packages/cashc/src/generation/Script.ts @@ -20,8 +20,8 @@ export class toOps { } static fromCast(from: Type, to: Type): Script { - if (from === PrimitiveType.INT && to instanceof BytesType) { - return [Data.encodeInt(to.bound || 8), Op.OP_NUM2BIN]; + if (from === PrimitiveType.INT && to instanceof BytesType && to.bound !== undefined) { + return [Data.encodeInt(to.bound), Op.OP_NUM2BIN]; } else if (from !== PrimitiveType.INT && to === PrimitiveType.INT) { return [Op.OP_BIN2NUM]; } else if (from === PrimitiveType.SIG && to === PrimitiveType.DATASIG) { From 982d1c74d5af01504e01d069e91dd8aabb6013ad Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月26日 18:59:35 +0100 Subject: [PATCH 08/14] Update boolean decoding - Used to only treat numeric 0 as false - Now treats any 0 value as false --- packages/cashc/src/util.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/cashc/src/util.ts b/packages/cashc/src/util.ts index f9503063..4178da05 100644 --- a/packages/cashc/src/util.ts +++ b/packages/cashc/src/util.ts @@ -19,7 +19,15 @@ export const Data = { return b ? this.encodeInt(1) : this.encodeInt(0); }, decodeBool(b: Buffer): boolean { - return this.decodeInt(b) !== 0; + // Any encoding of 0 is false, else true + for (let i = 0; i < b.byteLength; i += 1) { + if (b[i] !== 0) { + // Can be negative zero + if (i === b.byteLength - 1 && b[i] === 0x80) return false; + return true; + } + } + return false; }, encodeInt(i: number): Buffer { return new BScript().encodeNumber(i); From 1a0baf4560dee3247ecff8996d8cf140ef61b032 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月26日 11:30:49 +0100 Subject: [PATCH 09/14] Refactor OptimisationSimulation - Generalised across types --- packages/cashc/src/generation/Script.ts | 14 +- .../src/optimisations/OperationSimulations.ts | 157 +++----- packages/cashc/test/optimisations/fixtures.ts | 339 ++++++++---------- .../test/optimisations/simulation.test.ts | 230 ++++-------- 4 files changed, 287 insertions(+), 453 deletions(-) diff --git a/packages/cashc/src/generation/Script.ts b/packages/cashc/src/generation/Script.ts index 950daec9..75b95ad7 100644 --- a/packages/cashc/src/generation/Script.ts +++ b/packages/cashc/src/generation/Script.ts @@ -85,7 +85,10 @@ export class toOps { } } -export function returnType(op: GlobalFunction | BinaryOperator | UnaryOperator): Type { +export function returnType( + op: GlobalFunction | BinaryOperator | UnaryOperator, + operandType?: Type, +): Type { const mapping = { [GlobalFunction.ABS]: PrimitiveType.INT, [GlobalFunction.CHECKDATASIG]: PrimitiveType.BOOL, @@ -102,7 +105,7 @@ export function returnType(op: GlobalFunction | BinaryOperator | UnaryOperator): [BinaryOperator.DIV]: PrimitiveType.INT, [BinaryOperator.MINUS]: PrimitiveType.INT, [BinaryOperator.MOD]: PrimitiveType.INT, - [BinaryOperator.PLUS]: PrimitiveType.ANY, // TODO: int/string/bytes + [BinaryOperator.PLUS]: new BytesType(), [BinaryOperator.LT]: PrimitiveType.BOOL, [BinaryOperator.LE]: PrimitiveType.BOOL, [BinaryOperator.GT]: PrimitiveType.BOOL, @@ -114,5 +117,12 @@ export function returnType(op: GlobalFunction | BinaryOperator | UnaryOperator): [UnaryOperator.NOT]: PrimitiveType.BOOL, [UnaryOperator.NEGATE]: PrimitiveType.INT, }; + + if (operandType === PrimitiveType.INT) { + mapping[BinaryOperator.PLUS] = PrimitiveType.INT; + } else if (operandType === PrimitiveType.STRING) { + mapping[BinaryOperator.PLUS] = PrimitiveType.STRING; + } + return mapping[op]; } diff --git a/packages/cashc/src/optimisations/OperationSimulations.ts b/packages/cashc/src/optimisations/OperationSimulations.ts index 482dd47b..ff0aa27b 100644 --- a/packages/cashc/src/optimisations/OperationSimulations.ts +++ b/packages/cashc/src/optimisations/OperationSimulations.ts @@ -19,9 +19,14 @@ import { } from '../ast/AST'; import { GlobalFunction } from '../ast/Globals'; import { Data } from '../util'; -import { Script, toOps, returnType } from '../generation/Script'; +import { + Script, + toOps, + returnType, + Op, +} from '../generation/Script'; import { ExecutionError } from '../Errors'; -import { PrimitiveType, BytesType } from '../ast/Type'; +import { PrimitiveType, Type } from '../ast/Type'; type BCH_VM = AuthenticationVirtualMachine; let vm: BCH_VM; @@ -41,6 +46,32 @@ export function executeScriptOnVM(script: Script): Uint8Array[] { return res.stack; } +export function encodeLiteralNode(node: LiteralNode): Buffer { + if (node instanceof BoolLiteralNode) { + return Data.encodeBool(node.value); + } else if (node instanceof IntLiteralNode) { + return Data.encodeInt(node.value); + } else if (node instanceof StringLiteralNode) { + return Data.encodeString(node.value); + } else if (node instanceof HexLiteralNode) { + return node.value; + } else { + throw new Error(); // Can't happen + } +} + +export function decodeLiteralNode(value: Buffer, type: Type): LiteralNode { + if (type === PrimitiveType.BOOL) { + return new BoolLiteralNode(Data.decodeBool(value)); + } else if (type === PrimitiveType.INT) { + return new IntLiteralNode(Data.decodeInt(value, 5)); + } else if (type === PrimitiveType.STRING) { + return new StringLiteralNode(Data.decodeString(value), '"'); + } else { + return new HexLiteralNode(value); + } +} + // TODO: RequireNode // TODO: BranchNode // TODO: CastNode @@ -55,136 +86,38 @@ export function applyGlobalFunction(node: FunctionCallNode): Node { return node; } - let script: Script = parameters.map((p) => { - if (p instanceof BoolLiteralNode) { - return Data.encodeBool(p.value); - } else if (p instanceof IntLiteralNode) { - return Data.encodeInt(p.value); - } else if (p instanceof StringLiteralNode) { - return Data.encodeString(p.value); - } else if (p instanceof HexLiteralNode) { - return p.value; - } else { - throw new Error(); // Already checked in typecheck - } - }); - script = script.concat(toOps.fromFunction(node.identifier.name as GlobalFunction)); - + const script: Script = (parameters.map(encodeLiteralNode) as Script) + .concat(toOps.fromFunction(node.identifier.name as GlobalFunction)); const res = executeScriptOnVM(script)[0]; - if (returnType(fn) === PrimitiveType.BOOL) { - return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); - } else if (returnType(fn) === PrimitiveType.INT) { - return new IntLiteralNode(Data.decodeInt(Buffer.from(res))); - } else if (returnType(fn) === PrimitiveType.STRING) { - return new StringLiteralNode(Data.decodeString(Buffer.from(res)), '"'); - } else if (returnType(fn) instanceof BytesType) { - return new HexLiteralNode(Buffer.from(res)); - } else { - throw new Error(); // Already checked in typecheck - } + return decodeLiteralNode(Buffer.from(res), returnType(fn)); } // TODO: InstantiationNode // TODO: TupleIndexOpNode + SplitOpNode // TODO: SizeOpNode + export function applyBinaryOperator( left: LiteralNode, op: BinaryOperator, right: LiteralNode, ): LiteralNode { - if (left instanceof BoolLiteralNode && right instanceof BoolLiteralNode) { - return applyBinaryOperatorToBool(left, op, right); - } else if (left instanceof IntLiteralNode && right instanceof IntLiteralNode) { - return applyBinaryOperatorToInt(left, op, right); - } else if (left instanceof StringLiteralNode && right instanceof StringLiteralNode) { - return applyBinaryOperatorToString(left, op, right); - } else if (left instanceof HexLiteralNode && right instanceof HexLiteralNode) { - return applyBinaryOperatorToHex(left, op, right); - } else { - throw new Error(); // Already checked in typecheck - } -} - -function applyBinaryOperatorToBool( - left: BoolLiteralNode, - op: BinaryOperator, - right: BoolLiteralNode, -): BoolLiteralNode { - const script: Script = ([Data.encodeBool(left.value), Data.encodeBool(right.value)] as Script) - .concat(toOps.fromBinaryOp(op, true)); - const res = executeScriptOnVM(script)[0]; - return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); -} - -function applyBinaryOperatorToInt( - left: IntLiteralNode, - op: BinaryOperator, - right: IntLiteralNode, -): LiteralNode { - const script: Script = ([Data.encodeInt(left.value), Data.encodeInt(right.value)] as Script) - .concat(toOps.fromBinaryOp(op, true)); - const res = executeScriptOnVM(script)[0]; - return returnType(op) === PrimitiveType.BOOL - ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) - : new IntLiteralNode(Data.decodeInt(Buffer.from(res), 5)); -} - -function applyBinaryOperatorToString( - left: StringLiteralNode, - op: BinaryOperator, - right: StringLiteralNode, -): LiteralNode { - const script: Script = ([Data.encodeString(left.value), Data.encodeString(right.value)] as Script) - .concat(toOps.fromBinaryOp(op)); - const res = executeScriptOnVM(script)[0]; - return returnType(op) === PrimitiveType.BOOL - ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) - : new StringLiteralNode(Data.decodeString(Buffer.from(res)), left.quote); -} + let operandType; + if (left instanceof IntLiteralNode) operandType = PrimitiveType.INT; + if (left instanceof StringLiteralNode) operandType = PrimitiveType.STRING; -function applyBinaryOperatorToHex( - left: HexLiteralNode, - op: BinaryOperator, - right: HexLiteralNode, -): LiteralNode { - const script: Script = ([left.value, right.value] as Script) - .concat(toOps.fromBinaryOp(op)); + const script: Script = ([left, right].map(encodeLiteralNode) as Script) + .concat(toOps.fromBinaryOp(op, operandType === PrimitiveType.INT)); const res = executeScriptOnVM(script)[0]; - return returnType(op) === PrimitiveType.BOOL - ? new BoolLiteralNode(Data.decodeBool(Buffer.from(res))) - : new HexLiteralNode(Buffer.from(res)); + return decodeLiteralNode(Buffer.from(res), returnType(op, operandType)); } export function applyUnaryOperator( op: UnaryOperator, expr: LiteralNode, ): LiteralNode { - if (expr instanceof BoolLiteralNode) { - return applyUnaryOperatorToBool(op, expr); - } else if (expr instanceof IntLiteralNode) { - return applyUnaryOperatorToInt(op, expr); - } else { - throw new Error(); // Already checked in typecheck - } -} - -function applyUnaryOperatorToBool( - op: UnaryOperator, - expr: BoolLiteralNode, -): BoolLiteralNode { - const script: Script = ([Data.encodeBool(expr.value)] as Script) - .concat(toOps.fromUnaryOp(op)); + const script: Script = ([encodeLiteralNode(expr)] as Script).concat(toOps.fromUnaryOp(op)); const res = executeScriptOnVM(script)[0]; - return new BoolLiteralNode(Data.decodeBool(Buffer.from(res))); + return decodeLiteralNode(Buffer.from(res), returnType(op)); } -function applyUnaryOperatorToInt( - op: UnaryOperator, - expr: IntLiteralNode, -): IntLiteralNode { - const script: Script = ([Data.encodeInt(expr.value)] as Script) - .concat(toOps.fromUnaryOp(op)); - const res = executeScriptOnVM(script)[0]; - return new IntLiteralNode(Data.decodeInt(Buffer.from(res))); -} diff --git a/packages/cashc/test/optimisations/fixtures.ts b/packages/cashc/test/optimisations/fixtures.ts index 9df1949a..9d535a05 100644 --- a/packages/cashc/test/optimisations/fixtures.ts +++ b/packages/cashc/test/optimisations/fixtures.ts @@ -1,4 +1,4 @@ -import { BinaryOperator } from '../../src/ast/Operator'; +import { BinaryOperator, UnaryOperator } from '../../src/ast/Operator'; import { GlobalFunction } from '../../src/ast/Globals'; const MAXINT = 2147483647; @@ -21,188 +21,6 @@ const SigCheck = { }; export const fixtures = { - applyUnaryOperator: { - bool: { - success: [ - ['should apply !true', true, false], - ['should apply !false', false, true], - ], - }, - int: { - success: [ - ['should apply -10', 10, -10], - ['should apply -(-10)', -10, 10], - ['should apply -MAXINT', MAXINT, -MAXINT], - ['should apply -(-MAXINT)', -MAXINT, MAXINT], - ['should apply -0', 0, 0], - ['should apply -(-0)', -0, 0], - ], - fail: [ - ['should fail on -(MAXINT + 1)', MAXINT + 1, Error.INVALID_SCRIPT_NUMBER], - ['should fail on -(-MAXINT - 1)', -MAXINT - 1, Error.INVALID_SCRIPT_NUMBER], - ], - }, - }, - applyBinaryOperator: { - bool: { - success: [ - ['should apply true && false', true, BinaryOperator.AND, false, false], - ['should apply true && true', true, BinaryOperator.AND, true, true], - ['should apply false && false', false, BinaryOperator.AND, false, false], - ['should apply true || false', true, BinaryOperator.OR, false, true], - ['should apply true || true', true, BinaryOperator.OR, true, true], - ['should apply false || false', false, BinaryOperator.OR, false, false], - ], - }, - int: { - success: [ - // DIV - ['should apply 27 / 7', 27, BinaryOperator.DIV, 7, 3], - ['should apply 27 / (-7)', 27, BinaryOperator.DIV, -7, -3], - ['should apply (-27) / 7', -27, BinaryOperator.DIV, 7, -3], - ['should apply (-27) / (-7)', -27, BinaryOperator.DIV, -7, 3], - ['should apply MAXINT / MAXINT', MAXINT, BinaryOperator.DIV, MAXINT, 1], - ['should apply 1 / MAXINT', 1, BinaryOperator.DIV, MAXINT, 0], - ['should apply (-MAXINT) / MAXINT', -MAXINT, BinaryOperator.DIV, MAXINT, -1], - ['should apply (-1) / MAXINT', -1, BinaryOperator.DIV, MAXINT, 0], - // MOD - ['should apply 27 % 7', 27, BinaryOperator.MOD, 7, 6], - ['should apply 27 % (-7)', 27, BinaryOperator.MOD, -7, 6], - ['should apply (-27) % 7', -27, BinaryOperator.MOD, 7, -6], - ['should apply (-27) % (-7)', -27, BinaryOperator.MOD, -7, -6], - ['should apply MAXINT % MAXINT', MAXINT, BinaryOperator.MOD, MAXINT, 0], - ['should apply 1 % MAXINT', 1, BinaryOperator.MOD, MAXINT, 1], - ['should apply (-MAXINT) % MAXINT', -MAXINT, BinaryOperator.MOD, MAXINT, 0], - ['should apply (-1) % MAXINT', -1, BinaryOperator.MOD, MAXINT, -1], - // PLUS - ['should apply 27 + 7', 27, BinaryOperator.PLUS, 7, 34], - ['should apply 27 + (-7)', 27, BinaryOperator.PLUS, -7, 20], - ['should apply (-27) + 7', -27, BinaryOperator.PLUS, 7, -20], - ['should apply (-27) + (-7)', -27, BinaryOperator.PLUS, -7, -34], - ['should apply 1 + MAXINT', 1, BinaryOperator.PLUS, MAXINT, MAXINT + 1], - ['should apply (-MAXINT) + MAXINT', -MAXINT, BinaryOperator.PLUS, MAXINT, 0], - // MINUS - ['should apply 27 - 7', 27, BinaryOperator.MINUS, 7, 20], - ['should apply 27 - (-7)', 27, BinaryOperator.MINUS, -7, 34], - ['should apply (-27) - 7', -27, BinaryOperator.MINUS, 7, -34], - ['should apply (-27) - (-7)', -27, BinaryOperator.MINUS, -7, -20], - ['should apply (-1) - MAXINT', -1, BinaryOperator.MINUS, MAXINT, -MAXINT - 1], - ['should apply MAXINT - MAXINT', MAXINT, BinaryOperator.MINUS, MAXINT, 0], - // LT - ['should apply 5 < 4', 5, BinaryOperator.LT, 4, false], - ['should apply 4 < 4', 4, BinaryOperator.LT, 4, false], - ['should apply 3 < 4', 3, BinaryOperator.LT, 4, true], - // LE - ['should apply 5 <= 4', 5, BinaryOperator.LE, 4, false], - ['should apply 4 <= 4', 4, BinaryOperator.LE, 4, true], - ['should apply 3 <= 4', 3, BinaryOperator.LE, 4, true], - // GT - ['should apply 5> 4', 5, BinaryOperator.GT, 4, true], - ['should apply 4> 4', 4, BinaryOperator.GT, 4, false], - ['should apply 3> 4', 3, BinaryOperator.GT, 4, false], - // GE - ['should apply 5>= 4', 5, BinaryOperator.GE, 4, true], - ['should apply 4>= 4', 4, BinaryOperator.GE, 4, true], - ['should apply 3>= 4', 3, BinaryOperator.GE, 4, false], - // EQ - ['should apply 5 == 4', 5, BinaryOperator.EQ, 4, false], - ['should apply 4 == 4', 4, BinaryOperator.EQ, 4, true], - ['should apply 3 == 4', 3, BinaryOperator.EQ, 4, false], - // NE - ['should apply 5 != 4', 5, BinaryOperator.NE, 4, true], - ['should apply 4 != 4', 4, BinaryOperator.NE, 4, false], - ['should apply 3 != 4', 3, BinaryOperator.NE, 4, true], - ], - fail: [ - // DIV - ['should fail on 27 / 0', 27, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], - ['should fail on MAXINT / 0', MAXINT, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], - ['should fail on (-MAXINT) / 0', -MAXINT, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], - ['should fail on 0 / 0', 0, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], - ['should fail on (MAXINT + 1) / 2', MAXINT + 1, BinaryOperator.DIV, 2, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1) / 2', -MAXINT - 1, BinaryOperator.DIV, 2, Error.INVALID_SCRIPT_NUMBER], - // MOD - ['should fail on 27 % 0', 27, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], - ['should fail on MAXINT % 0', MAXINT, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], - ['should fail on (-MAXINT) % 0', -MAXINT, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], - ['should fail on 0 % 0', 0, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], - ['should fail on (MAXINT + 1) % 2', MAXINT + 1, BinaryOperator.MOD, 2, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1) % 2', -MAXINT - 1, BinaryOperator.MOD, 2, Error.INVALID_SCRIPT_NUMBER], - // PLUS - ['should fail on (MAXINT + 1) + 1', MAXINT + 1, BinaryOperator.PLUS, 1, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1) + 1', -MAXINT - 1, BinaryOperator.PLUS, 1, Error.INVALID_SCRIPT_NUMBER], - // MINUS - ['should fail on (MAXINT + 1) - 1', MAXINT + 1, BinaryOperator.MINUS, 1, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1) - 1', -MAXINT - 1, BinaryOperator.MINUS, 1, Error.INVALID_SCRIPT_NUMBER], - // LT - ['should fail on MAXINT + 1 < 4', MAXINT + 1, BinaryOperator.LT, 4, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1) < 4', -MAXINT - 1, BinaryOperator.LT, 4, Error.INVALID_SCRIPT_NUMBER], - // LE - ['should fail on MAXINT + 1 <= 4', MAXINT + 1, BinaryOperator.LE, 4, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1) <= 4', -MAXINT - 1, BinaryOperator.LE, 4, Error.INVALID_SCRIPT_NUMBER], - // GT - ['should fail on MAXINT + 1> 4', MAXINT + 1, BinaryOperator.GT, 4, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1)> 4', -MAXINT - 1, BinaryOperator.GT, 4, Error.INVALID_SCRIPT_NUMBER], - // GE - ['should fail on MAXINT + 1>= 4', MAXINT + 1, BinaryOperator.GE, 4, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1)>= 4', -MAXINT - 1, BinaryOperator.GE, 4, Error.INVALID_SCRIPT_NUMBER], - // EQ - ['should fail on MAXINT + 1 == 4', MAXINT + 1, BinaryOperator.EQ, 4, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1) == 4', -MAXINT - 1, BinaryOperator.EQ, 4, Error.INVALID_SCRIPT_NUMBER], - // NE - ['should fail on MAXINT + 1 != 4', MAXINT + 1, BinaryOperator.NE, 4, Error.INVALID_SCRIPT_NUMBER], - ['should fail on (-MAXINT - 1) != 4', -MAXINT - 1, BinaryOperator.NE, 4, Error.INVALID_SCRIPT_NUMBER], - ], - }, - string: { - success: [ - // PLUS - ['should apply "Chancellor on brink of " + "second bailout for banks"', - 'Chancellor on brink of ', BinaryOperator.PLUS, 'second bailout for banks', - 'Chancellor on brink of second bailout for banks'], - ['should apply "" + ""', '', BinaryOperator.PLUS, '', ''], - ['should apply almost_maxlen_x + "X"', 'X'.repeat(519), BinaryOperator.PLUS, 'X', 'X'.repeat(520)], - ['should apply maxlen_x + ""', 'X'.repeat(520), BinaryOperator.PLUS, '', 'X'.repeat(520)], - ['should apply "" + maxlen_x', '', BinaryOperator.PLUS, 'X'.repeat(520), 'X'.repeat(520)], - // EQ - ['should apply "BCH" == "BCH"', 'BCH', BinaryOperator.EQ, 'BCH', true], - ['should apply "BCH" == "BTC"', 'BCH', BinaryOperator.EQ, 'BTC', false], - ['should apply "BCH" == "BSV"', 'BCH', BinaryOperator.EQ, 'BSV', false], - // NE - ['should apply "BCH" != "BCH"', 'BCH', BinaryOperator.NE, 'BCH', false], - ['should apply "BCH" != "BTC"', 'BCH', BinaryOperator.NE, 'BTC', true], - ['should apply "BCH" != "BSV"', 'BCH', BinaryOperator.NE, 'BSV', true], - ], - fail: [ - // PLUS - ['should fail on maxlen_x + "X"', 'X'.repeat(520), BinaryOperator.PLUS, 'X', Error.ATTEMPTED_BIG_PUSH], - ['should fail on large_x + large_y', 'X'.repeat(260), BinaryOperator.PLUS, 'Y'.repeat(261), Error.ATTEMPTED_BIG_PUSH], - ], - }, - hex: { - success: [ - // PLUS - ['should apply 0xdead + 0xbeef', 'dead', BinaryOperator.PLUS, 'beef', 'deadbeef'], - ['should apply 0x + 0x', '', BinaryOperator.PLUS, '', ''], - ['should apply almost_maxlen_x + 0x58', '58'.repeat(519), BinaryOperator.PLUS, '58', '58'.repeat(520)], - ['should apply maxlen_x + 0x', '58'.repeat(520), BinaryOperator.PLUS, '', '58'.repeat(520)], - ['should apply 0x + maxlen_x', '', BinaryOperator.PLUS, '58'.repeat(520), '58'.repeat(520)], - // EQ - ['should apply 0x424348 == 0x424348', '424348', BinaryOperator.EQ, '424348', true], - ['should apply 0x424348 == 0x425443', '424348', BinaryOperator.EQ, '425443', false], - ['should apply 0x424348 == 0x425356', '424348', BinaryOperator.EQ, '425356', false], - // NE - ['should apply 0x424348 != 0x424348', '424348', BinaryOperator.NE, '424348', false], - ['should apply 0x424348 != 0x425443', '424348', BinaryOperator.NE, '425443', true], - ['should apply 0x424348 != 0x425356', '424348', BinaryOperator.NE, '425356', true], - ], - fail: [ - // PLUS - ['should fail on maxlen_x + 0x58', '58'.repeat(520), BinaryOperator.PLUS, '58', Error.ATTEMPTED_BIG_PUSH], - ['should fail on large_x + large_y', '58'.repeat(260), BinaryOperator.PLUS, '59'.repeat(261), Error.ATTEMPTED_BIG_PUSH], - ], - }, - }, applyGlobalFunction: { success: [ // ABS @@ -287,4 +105,159 @@ export const fixtures = { [`should fail on checkDataSig(${SigCheck.signature.slice(0, -2)}00, ${SigCheck.message}, ${SigCheck.publicKey})`, GlobalFunction.CHECKDATASIG, [`${SigCheck.signature.slice(0, -2)}00`, SigCheck.message, SigCheck.publicKey], Error.NULLFAIL], ], }, + applyBinaryOperator: { + success: [ + // AND + ['should apply true && false', true, BinaryOperator.AND, false, false], + ['should apply true && true', true, BinaryOperator.AND, true, true], + ['should apply false && false', false, BinaryOperator.AND, false, false], + // OR + ['should apply true || false', true, BinaryOperator.OR, false, true], + ['should apply true || true', true, BinaryOperator.OR, true, true], + ['should apply false || false', false, BinaryOperator.OR, false, false], + // DIV + ['should apply 27 / 7', 27, BinaryOperator.DIV, 7, 3], + ['should apply 27 / (-7)', 27, BinaryOperator.DIV, -7, -3], + ['should apply (-27) / 7', -27, BinaryOperator.DIV, 7, -3], + ['should apply (-27) / (-7)', -27, BinaryOperator.DIV, -7, 3], + ['should apply MAXINT / MAXINT', MAXINT, BinaryOperator.DIV, MAXINT, 1], + ['should apply 1 / MAXINT', 1, BinaryOperator.DIV, MAXINT, 0], + ['should apply (-MAXINT) / MAXINT', -MAXINT, BinaryOperator.DIV, MAXINT, -1], + ['should apply (-1) / MAXINT', -1, BinaryOperator.DIV, MAXINT, 0], + // MOD + ['should apply 27 % 7', 27, BinaryOperator.MOD, 7, 6], + ['should apply 27 % (-7)', 27, BinaryOperator.MOD, -7, 6], + ['should apply (-27) % 7', -27, BinaryOperator.MOD, 7, -6], + ['should apply (-27) % (-7)', -27, BinaryOperator.MOD, -7, -6], + ['should apply MAXINT % MAXINT', MAXINT, BinaryOperator.MOD, MAXINT, 0], + ['should apply 1 % MAXINT', 1, BinaryOperator.MOD, MAXINT, 1], + ['should apply (-MAXINT) % MAXINT', -MAXINT, BinaryOperator.MOD, MAXINT, 0], + ['should apply (-1) % MAXINT', -1, BinaryOperator.MOD, MAXINT, -1], + // PLUS + // int + ['should apply 27 + 7', 27, BinaryOperator.PLUS, 7, 34], + ['should apply 27 + (-7)', 27, BinaryOperator.PLUS, -7, 20], + ['should apply (-27) + 7', -27, BinaryOperator.PLUS, 7, -20], + ['should apply (-27) + (-7)', -27, BinaryOperator.PLUS, -7, -34], + ['should apply 1 + MAXINT', 1, BinaryOperator.PLUS, MAXINT, MAXINT + 1], + ['should apply (-MAXINT) + MAXINT', -MAXINT, BinaryOperator.PLUS, MAXINT, 0], + // string + ['should apply "Chancellor on brink of " + "second bailout for banks"', + 'Chancellor on brink of ', BinaryOperator.PLUS, 'second bailout for banks', + 'Chancellor on brink of second bailout for banks'], + ['should apply "" + ""', '', BinaryOperator.PLUS, '', ''], + ['should apply almost_maxlen_x + "X"', 'X'.repeat(519), BinaryOperator.PLUS, 'X', 'X'.repeat(520)], + ['should apply maxlen_x + ""', 'X'.repeat(520), BinaryOperator.PLUS, '', 'X'.repeat(520)], + ['should apply "" + maxlen_x', '', BinaryOperator.PLUS, 'X'.repeat(520), 'X'.repeat(520)], + // hex + ['should apply 0xdead + 0xbeef', '0xdead', BinaryOperator.PLUS, '0xbeef', '0xdeadbeef'], + ['should apply 0x + 0x', '0x', BinaryOperator.PLUS, '0x', '0x'], + ['should apply almost_maxlen_x + 0x58', `0x${'58'.repeat(519)}`, BinaryOperator.PLUS, '0x58', `0x${'58'.repeat(520)}`], + ['should apply maxlen_x + 0x', `0x${'58'.repeat(520)}`, BinaryOperator.PLUS, '0x', `0x${'58'.repeat(520)}`], + ['should apply 0x + maxlen_x', '0x', BinaryOperator.PLUS, `0x${'58'.repeat(520)}`, `0x${'58'.repeat(520)}`], + // MINUS + ['should apply 27 - 7', 27, BinaryOperator.MINUS, 7, 20], + ['should apply 27 - (-7)', 27, BinaryOperator.MINUS, -7, 34], + ['should apply (-27) - 7', -27, BinaryOperator.MINUS, 7, -34], + ['should apply (-27) - (-7)', -27, BinaryOperator.MINUS, -7, -20], + ['should apply (-1) - MAXINT', -1, BinaryOperator.MINUS, MAXINT, -MAXINT - 1], + ['should apply MAXINT - MAXINT', MAXINT, BinaryOperator.MINUS, MAXINT, 0], + // LT + ['should apply 5 < 4', 5, BinaryOperator.LT, 4, false], + ['should apply 4 < 4', 4, BinaryOperator.LT, 4, false], + ['should apply 3 < 4', 3, BinaryOperator.LT, 4, true], + // LE + ['should apply 5 <= 4', 5, BinaryOperator.LE, 4, false], + ['should apply 4 <= 4', 4, BinaryOperator.LE, 4, true], + ['should apply 3 <= 4', 3, BinaryOperator.LE, 4, true], + // GT + ['should apply 5> 4', 5, BinaryOperator.GT, 4, true], + ['should apply 4> 4', 4, BinaryOperator.GT, 4, false], + ['should apply 3> 4', 3, BinaryOperator.GT, 4, false], + // GE + ['should apply 5>= 4', 5, BinaryOperator.GE, 4, true], + ['should apply 4>= 4', 4, BinaryOperator.GE, 4, true], + ['should apply 3>= 4', 3, BinaryOperator.GE, 4, false], + // EQ + ['should apply 5 == 4', 5, BinaryOperator.EQ, 4, false], + ['should apply 4 == 4', 4, BinaryOperator.EQ, 4, true], + ['should apply 3 == 4', 3, BinaryOperator.EQ, 4, false], + ['should apply "BCH" == "BCH"', 'BCH', BinaryOperator.EQ, 'BCH', true], + ['should apply "BCH" == "BTC"', 'BCH', BinaryOperator.EQ, 'BTC', false], + ['should apply "BCH" == "BSV"', 'BCH', BinaryOperator.EQ, 'BSV', false], + ['should apply 0x424348 == 0x424348', '0x424348', BinaryOperator.EQ, '0x424348', true], + ['should apply 0x424348 == 0x425443', '0x424348', BinaryOperator.EQ, '0x425443', false], + ['should apply 0x424348 == 0x425356', '0x424348', BinaryOperator.EQ, '0x425356', false], + // NE + ['should apply 5 != 4', 5, BinaryOperator.NE, 4, true], + ['should apply 4 != 4', 4, BinaryOperator.NE, 4, false], + ['should apply 3 != 4', 3, BinaryOperator.NE, 4, true], + ['should apply "BCH" != "BCH"', 'BCH', BinaryOperator.NE, 'BCH', false], + ['should apply "BCH" != "BTC"', 'BCH', BinaryOperator.NE, 'BTC', true], + ['should apply "BCH" != "BSV"', 'BCH', BinaryOperator.NE, 'BSV', true], + ['should apply 0x424348 != 0x424348', '0x424348', BinaryOperator.NE, '0x424348', false], + ['should apply 0x424348 != 0x425443', '0x424348', BinaryOperator.NE, '0x425443', true], + ['should apply 0x424348 != 0x425356', '0x424348', BinaryOperator.NE, '0x425356', true], + ], + fail: [ + // DIV + ['should fail on 27 / 0', 27, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], + ['should fail on MAXINT / 0', MAXINT, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], + ['should fail on (-MAXINT) / 0', -MAXINT, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], + ['should fail on 0 / 0', 0, BinaryOperator.DIV, 0, Error.DIVIDE_BY_ZERO], + ['should fail on (MAXINT + 1) / 2', MAXINT + 1, BinaryOperator.DIV, 2, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) / 2', -MAXINT - 1, BinaryOperator.DIV, 2, Error.INVALID_SCRIPT_NUMBER], + // MOD + ['should fail on 27 % 0', 27, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], + ['should fail on MAXINT % 0', MAXINT, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], + ['should fail on (-MAXINT) % 0', -MAXINT, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], + ['should fail on 0 % 0', 0, BinaryOperator.MOD, 0, Error.DIVIDE_BY_ZERO], + ['should fail on (MAXINT + 1) % 2', MAXINT + 1, BinaryOperator.MOD, 2, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) % 2', -MAXINT - 1, BinaryOperator.MOD, 2, Error.INVALID_SCRIPT_NUMBER], + // PLUS + ['should fail on (MAXINT + 1) + 1', MAXINT + 1, BinaryOperator.PLUS, 1, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) + 1', -MAXINT - 1, BinaryOperator.PLUS, 1, Error.INVALID_SCRIPT_NUMBER], + ['should fail on maxlen_x + "X"', 'X'.repeat(520), BinaryOperator.PLUS, 'X', Error.ATTEMPTED_BIG_PUSH], + ['should fail on large_x + large_y', 'X'.repeat(260), BinaryOperator.PLUS, 'Y'.repeat(261), Error.ATTEMPTED_BIG_PUSH], + ['should fail on maxlen_x + 0x58', `0x${'58'.repeat(520)}`, BinaryOperator.PLUS, '0x58', Error.ATTEMPTED_BIG_PUSH], + ['should fail on large_x + large_y', `0x${'58'.repeat(260)}`, BinaryOperator.PLUS, `0x${'59'.repeat(261)}`, Error.ATTEMPTED_BIG_PUSH], + // MINUS + ['should fail on (MAXINT + 1) - 1', MAXINT + 1, BinaryOperator.MINUS, 1, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) - 1', -MAXINT - 1, BinaryOperator.MINUS, 1, Error.INVALID_SCRIPT_NUMBER], + // LT + ['should fail on MAXINT + 1 < 4', MAXINT + 1, BinaryOperator.LT, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) < 4', -MAXINT - 1, BinaryOperator.LT, 4, Error.INVALID_SCRIPT_NUMBER], + // LE + ['should fail on MAXINT + 1 <= 4', MAXINT + 1, BinaryOperator.LE, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) <= 4', -MAXINT - 1, BinaryOperator.LE, 4, Error.INVALID_SCRIPT_NUMBER], + // GT + ['should fail on MAXINT + 1> 4', MAXINT + 1, BinaryOperator.GT, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1)> 4', -MAXINT - 1, BinaryOperator.GT, 4, Error.INVALID_SCRIPT_NUMBER], + // GE + ['should fail on MAXINT + 1>= 4', MAXINT + 1, BinaryOperator.GE, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1)>= 4', -MAXINT - 1, BinaryOperator.GE, 4, Error.INVALID_SCRIPT_NUMBER], + // EQ + ['should fail on MAXINT + 1 == 4', MAXINT + 1, BinaryOperator.EQ, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) == 4', -MAXINT - 1, BinaryOperator.EQ, 4, Error.INVALID_SCRIPT_NUMBER], + // NE + ['should fail on MAXINT + 1 != 4', MAXINT + 1, BinaryOperator.NE, 4, Error.INVALID_SCRIPT_NUMBER], + ['should fail on (-MAXINT - 1) != 4', -MAXINT - 1, BinaryOperator.NE, 4, Error.INVALID_SCRIPT_NUMBER], + ], + }, + applyUnaryOperator: { + success: [ + ['should apply !true', UnaryOperator.NOT, true, false], + ['should apply !false', UnaryOperator.NOT, false, true], + ['should apply -10', UnaryOperator.NEGATE, 10, -10], + ['should apply -(-10)', UnaryOperator.NEGATE, -10, 10], + ['should apply -MAXINT', UnaryOperator.NEGATE, MAXINT, -MAXINT], + ['should apply -(-MAXINT)', UnaryOperator.NEGATE, -MAXINT, MAXINT], + ['should apply -0', UnaryOperator.NEGATE, 0, 0], + ['should apply -(-0)', UnaryOperator.NEGATE, -0, 0], + ], + fail: [ + ['should fail on -(MAXINT + 1)', UnaryOperator.NEGATE, MAXINT + 1, Error.INVALID_SCRIPT_NUMBER], + ['should fail on -(-MAXINT - 1)', UnaryOperator.NEGATE, -MAXINT - 1, Error.INVALID_SCRIPT_NUMBER], + ], + }, }; diff --git a/packages/cashc/test/optimisations/simulation.test.ts b/packages/cashc/test/optimisations/simulation.test.ts index a3e5de33..824c668e 100644 --- a/packages/cashc/test/optimisations/simulation.test.ts +++ b/packages/cashc/test/optimisations/simulation.test.ts @@ -4,7 +4,12 @@ */ import delay from 'delay'; -import { applyUnaryOperator, applyBinaryOperator, applyGlobalFunction } from '../../src/optimisations/OperationSimulations'; +import { + applyUnaryOperator, + applyBinaryOperator, + applyGlobalFunction, + applySizeOp, +} from '../../src/optimisations/OperationSimulations'; import { UnaryOperator, BinaryOperator } from '../../src/ast/Operator'; import { IntLiteralNode, @@ -23,161 +28,6 @@ describe('Operation simulation', () => { await delay(1000); }); - describe('applyUnaryOperator', () => { - fixtures.applyUnaryOperator.bool.success.forEach(([should, input, expected]: any) => { - it(should as string, () => { - // when - const res = applyUnaryOperator(UnaryOperator.NOT, new BoolLiteralNode(input as boolean)); - - // then - expect(res).toBeInstanceOf(BoolLiteralNode); - expect((res as BoolLiteralNode).value).toBe(expected); - }); - }); - - fixtures.applyUnaryOperator.int.success.forEach(([should, input, expected]: any) => { - it(should as string, () => { - // when - const res = applyUnaryOperator(UnaryOperator.NEGATE, new IntLiteralNode(input as number)); - - // then - expect(res).toBeInstanceOf(IntLiteralNode); - expect((res as IntLiteralNode).value).toBe(expected); - }); - }); - - fixtures.applyUnaryOperator.int.fail.forEach(([should, input, expected]: any) => { - it(should as string, () => { - expect(() => { - // when - applyUnaryOperator(UnaryOperator.NEGATE, new IntLiteralNode(input as number)); - - // then - }).toThrow(new ExecutionError(expected as string)); - }); - }); - }); - - describe('applyBinaryOperator', () => { - fixtures.applyBinaryOperator.bool.success - .forEach(([should, left, op, right, expected]: any) => { - it(should as string, () => { - // when - const res = applyBinaryOperator( - new BoolLiteralNode(left as boolean), - op as BinaryOperator, - new BoolLiteralNode(right as boolean), - ); - - // then - expect(res).toBeInstanceOf(BoolLiteralNode); - expect((res as BoolLiteralNode).value).toBe(expected); - }); - }); - - fixtures.applyBinaryOperator.int.success - .forEach(([should, left, op, right, expected]: any) => { - it(should as string, () => { - // when - const res = applyBinaryOperator( - new IntLiteralNode(left as number), - op as BinaryOperator, - new IntLiteralNode(right as number), - ); - - // then - const expectedNode = typeof expected === 'boolean' - ? new BoolLiteralNode(expected) - : new IntLiteralNode(expected); - expect(res).toEqual(expectedNode); - }); - }); - - fixtures.applyBinaryOperator.int.fail - .forEach(([should, left, op, right, expected]: any) => { - it(should as string, () => { - expect(() => { - // when - applyBinaryOperator( - new IntLiteralNode(left as number), - op as BinaryOperator, - new IntLiteralNode(right as number), - ); - - // then - }).toThrow(new ExecutionError(expected as string)); - }); - }); - - fixtures.applyBinaryOperator.string.success - .forEach(([should, left, op, right, expected]: any) => { - it(should as string, () => { - // when - const res = applyBinaryOperator( - new StringLiteralNode(left as string, '"'), - op as BinaryOperator, - new StringLiteralNode(right as string, '"'), - ); - - // then - const expectedNode = typeof expected === 'boolean' - ? new BoolLiteralNode(expected) - : new StringLiteralNode(expected, '"'); - expect(res).toEqual(expectedNode); - }); - }); - - fixtures.applyBinaryOperator.string.fail - .forEach(([should, left, op, right, expected]: any) => { - it(should as string, () => { - expect(() => { - // when - applyBinaryOperator( - new StringLiteralNode(left as string, '"'), - op as BinaryOperator, - new StringLiteralNode(right as string, '"'), - ); - - // then - }).toThrow(new ExecutionError(expected as string)); - }); - }); - - fixtures.applyBinaryOperator.hex.success - .forEach(([should, left, op, right, expected]: any) => { - it(should as string, () => { - // when - const res = applyBinaryOperator( - new HexLiteralNode(Buffer.from(left, 'hex')), - op as BinaryOperator, - new HexLiteralNode(Buffer.from(right, 'hex')), - ); - - // then - const expectedNode = typeof expected === 'boolean' - ? new BoolLiteralNode(expected) - : new HexLiteralNode(Buffer.from(expected, 'hex')); - expect(res).toEqual(expectedNode); - }); - }); - - fixtures.applyBinaryOperator.hex.fail - .forEach(([should, left, op, right, expected]: any) => { - it(should as string, () => { - expect(() => { - // when - applyBinaryOperator( - new HexLiteralNode(Buffer.from(left, 'hex')), - op as BinaryOperator, - new HexLiteralNode(Buffer.from(right, 'hex')), - ); - - // then - }).toThrow(new ExecutionError(expected as string)); - }); - }); - }); - describe('applyGlobalFunction', () => { fixtures.applyGlobalFunction.success .forEach(([should, fn, parameters, expected]: any) => { @@ -215,4 +65,72 @@ describe('Operation simulation', () => { }); }); }); + + describe('applyBinaryOperator', () => { + fixtures.applyBinaryOperator.success.forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + // given + const leftNode = literalToNode(left); + const rightNode = literalToNode(right); + const expectedNode = literalToNode(expected); + + // when + const res = applyBinaryOperator(leftNode, op as BinaryOperator, rightNode); + + // then + expect(res).toEqual(expectedNode); + }); + }); + + fixtures.applyBinaryOperator.fail.forEach(([should, left, op, right, expected]: any) => { + it(should as string, () => { + // given + const leftNode = literalToNode(left); + const rightNode = literalToNode(right); + + expect(() => { + // when + applyBinaryOperator(leftNode, op as BinaryOperator, rightNode); + + // then + }).toThrow(new ExecutionError(expected as string)); + }); + }); + }); + + describe('applyUnaryOperator', () => { + fixtures.applyUnaryOperator.success.forEach(([should, op, input, expected]: any) => { + it(should as string, () => { + // given + const inputNode = literalToNode(input as number | boolean); + const expectedNode = literalToNode(expected); + + // when + const res = applyUnaryOperator( + op as UnaryOperator, + inputNode as IntLiteralNode | BoolLiteralNode, + ); + + // then + expect(res).toEqual(expectedNode); + }); + }); + + fixtures.applyUnaryOperator.fail.forEach(([should, op, input, expected]: any) => { + it(should as string, () => { + // given + const inputNode = literalToNode(input as number | boolean); + + expect(() => { + // when + applyUnaryOperator( + op as UnaryOperator, + inputNode as IntLiteralNode | BoolLiteralNode, + ); + + // then + }).toThrow(new ExecutionError(expected as string)); + }); + }); + }); }); From 6afe21959246aaa23c055db9e38edb1f76113e02 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月26日 12:30:32 +0100 Subject: [PATCH 10/14] Add simulation of SizeOp --- .../src/optimisations/OperationSimulations.ts | 6 +++++- packages/cashc/test/optimisations/fixtures.ts | 8 ++++++++ .../cashc/test/optimisations/simulation.test.ts | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/cashc/src/optimisations/OperationSimulations.ts b/packages/cashc/src/optimisations/OperationSimulations.ts index ff0aa27b..e7cb47c9 100644 --- a/packages/cashc/src/optimisations/OperationSimulations.ts +++ b/packages/cashc/src/optimisations/OperationSimulations.ts @@ -94,8 +94,12 @@ export function applyGlobalFunction(node: FunctionCallNode): Node { // TODO: InstantiationNode // TODO: TupleIndexOpNode + SplitOpNode -// TODO: SizeOpNode +export function applySizeOp(node: StringLiteralNode | HexLiteralNode): LiteralNode { + const script: Script = [encodeLiteralNode(node), Op.OP_SIZE, Op.OP_NIP]; + const res = executeScriptOnVM(script)[0]; + return decodeLiteralNode(Buffer.from(res), PrimitiveType.INT); +} export function applyBinaryOperator( left: LiteralNode, diff --git a/packages/cashc/test/optimisations/fixtures.ts b/packages/cashc/test/optimisations/fixtures.ts index 9d535a05..d2853397 100644 --- a/packages/cashc/test/optimisations/fixtures.ts +++ b/packages/cashc/test/optimisations/fixtures.ts @@ -105,6 +105,14 @@ export const fixtures = { [`should fail on checkDataSig(${SigCheck.signature.slice(0, -2)}00, ${SigCheck.message}, ${SigCheck.publicKey})`, GlobalFunction.CHECKDATASIG, [`${SigCheck.signature.slice(0, -2)}00`, SigCheck.message, SigCheck.publicKey], Error.NULLFAIL], ], }, + applySizeOp: { + success: [ + ['should apply "Bitcoin Cash".length', 'Bitcoin Cash', 12], + ['should apply 0xbeef.length', '0xbeef', 2], + ['should apply maxlen_x.length', `0x${'58'.repeat(520)}`, 520], + ['should apply maxlen_x.length', 'X'.repeat(520), 520], + ], + }, applyBinaryOperator: { success: [ // AND diff --git a/packages/cashc/test/optimisations/simulation.test.ts b/packages/cashc/test/optimisations/simulation.test.ts index 824c668e..7a668ec6 100644 --- a/packages/cashc/test/optimisations/simulation.test.ts +++ b/packages/cashc/test/optimisations/simulation.test.ts @@ -66,6 +66,22 @@ describe('Operation simulation', () => { }); }); + describe('applySizeOp', () => { + fixtures.applySizeOp.success.forEach(([should, input, expected]: any) => { + it(should as string, () => { + // given + const inputNode = literalToNode(input as string); + const expectedNode = literalToNode(expected); + + // when + const res = applySizeOp(inputNode as StringLiteralNode | HexLiteralNode); + + // then + expect(res).toEqual(expectedNode); + }); + }); + }); + describe('applyBinaryOperator', () => { fixtures.applyBinaryOperator.success.forEach(([should, left, op, right, expected]: any) => { it(should as string, () => { From 8a9ef04ea0f6b26c90cfbcc31d00185ada2ba80d Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年2月26日 18:59:54 +0100 Subject: [PATCH 11/14] Add simulation of casting --- .../src/optimisations/OperationSimulations.ts | 16 ++++++-- packages/cashc/test/optimisations/fixtures.ts | 40 +++++++++++++++++++ .../test/optimisations/simulation.test.ts | 31 ++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/packages/cashc/src/optimisations/OperationSimulations.ts b/packages/cashc/src/optimisations/OperationSimulations.ts index e7cb47c9..393b3482 100644 --- a/packages/cashc/src/optimisations/OperationSimulations.ts +++ b/packages/cashc/src/optimisations/OperationSimulations.ts @@ -26,7 +26,7 @@ import { Op, } from '../generation/Script'; import { ExecutionError } from '../Errors'; -import { PrimitiveType, Type } from '../ast/Type'; +import { PrimitiveType, Type, BytesType } from '../ast/Type'; type BCH_VM = AuthenticationVirtualMachine; let vm: BCH_VM; @@ -74,7 +74,18 @@ export function decodeLiteralNode(value: Buffer, type: Type): LiteralNode { // TODO: RequireNode // TODO: BranchNode -// TODO: CastNode + +export function applyCast(node: LiteralNode, castType: Type): LiteralNode { + let sourceType = new BytesType(); + if (node instanceof BoolLiteralNode) sourceType = PrimitiveType.BOOL; + if (node instanceof IntLiteralNode) sourceType = PrimitiveType.INT; + if (node instanceof StringLiteralNode) sourceType = PrimitiveType.STRING; + + const script: Script = ([encodeLiteralNode(node)] as Script) + .concat(toOps.fromCast(sourceType, castType)); + const res = executeScriptOnVM(script)[0]; + return decodeLiteralNode(Buffer.from(res), castType); +} export function applyGlobalFunction(node: FunctionCallNode): Node { const { parameters } = node; @@ -124,4 +135,3 @@ export function applyUnaryOperator( const res = executeScriptOnVM(script)[0]; return decodeLiteralNode(Buffer.from(res), returnType(op)); } - diff --git a/packages/cashc/test/optimisations/fixtures.ts b/packages/cashc/test/optimisations/fixtures.ts index d2853397..6e517b32 100644 --- a/packages/cashc/test/optimisations/fixtures.ts +++ b/packages/cashc/test/optimisations/fixtures.ts @@ -1,5 +1,6 @@ import { BinaryOperator, UnaryOperator } from '../../src/ast/Operator'; import { GlobalFunction } from '../../src/ast/Globals'; +import { PrimitiveType, BytesType } from '../../src'; const MAXINT = 2147483647; @@ -10,6 +11,8 @@ enum Error { FAILED_VERIFY = 'Program failed an OP_VERIFY operation.', IMPROPERLY_ENCODED_SIG = 'Encountered an improperly encoded signature.', NULLFAIL = 'Program failed a signature verification with a non-null signature (violating the "NULLFAIL" rule).', + NUM2BIN = 'Program called an OP_NUM2BIN operation with an insufficient byte length to re-encode the provided number.', + BIN2NUM = 'Program attempted an OP_BIN2NUM operation on a byte sequence which cannot be encoded within the maximum Script Number length (4 bytes).', } // (https://kjur.github.io/jsrsasign/sample/sample-ecdsa.html) @@ -21,6 +24,43 @@ const SigCheck = { }; export const fixtures = { + applyCast: { + success: [ + ['should apply bool(-12)', PrimitiveType.BOOL, -12, true], + ['should apply bool(MAXINT + 1)', PrimitiveType.BOOL, MAXINT + 1, true], + ['should apply bool(0)', PrimitiveType.BOOL, 0, false], + ['should apply bool(maxlen_x)', PrimitiveType.BOOL, `0x${'58'.repeat(520)}`, true], + ['should apply bool(0x)', PrimitiveType.BOOL, '0x', false], + ['should apply bool(0x00)', PrimitiveType.BOOL, '0x00', false], + ['should apply bool(0x0080)', PrimitiveType.BOOL, '0x0080', false], + ['should apply bool(0x01)', PrimitiveType.BOOL, '0x01', true], + ['should apply int(true)', PrimitiveType.INT, true, 1], + ['should apply int(false)', PrimitiveType.INT, false, 0], + ['should apply int(0x)', PrimitiveType.INT, '0x', 0], + ['should apply int(0x0000)', PrimitiveType.INT, '0x0000', 0], + ['should apply int(0x0080)', PrimitiveType.INT, '0x0080', 0], + ['should apply int(0xe803)', PrimitiveType.INT, '0xe803', 1000], + ['should apply int(0xe8030000)', PrimitiveType.INT, '0xe8030000', 1000], + ['should apply int(0xe80300000080)', PrimitiveType.INT, '0xe80300000080', -1000], + ['should apply int(0xffffff7f)', PrimitiveType.INT, '0xffffff7f', MAXINT], + ['should apply int(0xffffffff)', PrimitiveType.INT, '0xffffffff', -MAXINT], + ['should apply bytes(true)', new BytesType(), true, '0x01'], + ['should apply bytes(false)', new BytesType(), false, '0x'], + ['should apply bytes(true)', new BytesType(), true, '0x01'], + ['should apply bytes(-12)', new BytesType(), -12, '0x8c'], + ['should apply bytes(MAXINT + 1)', new BytesType(), MAXINT + 1, '0x0000008000'], + ['should apply bytes(0)', new BytesType(), 0, '0x'], + ['should apply bytes4(MAXINT)', new BytesType(4), MAXINT, '0xffffff7f'], + ['should apply bytes4(1000)', new BytesType(4), 1000, '0xe8030000'], + ['should apply bytes2(1000)', new BytesType(2), 1000, '0xe803'], + ['should apply bytes("BCH")', new BytesType(), 'BCH', '0x424348'], + ], + fail: [ + ['should fail on int(0xffffffffff)', PrimitiveType.INT, '0xffffffffff', Error.BIN2NUM], + ['should fail on bytes4(MAXINT + 1)', new BytesType(4), MAXINT + 1, Error.NUM2BIN], + ['should fail on bytes4(MAXINT)', new BytesType(521), MAXINT, Error.ATTEMPTED_BIG_PUSH], + ], + }, applyGlobalFunction: { success: [ // ABS diff --git a/packages/cashc/test/optimisations/simulation.test.ts b/packages/cashc/test/optimisations/simulation.test.ts index 7a668ec6..16acff78 100644 --- a/packages/cashc/test/optimisations/simulation.test.ts +++ b/packages/cashc/test/optimisations/simulation.test.ts @@ -9,6 +9,7 @@ import { applyBinaryOperator, applyGlobalFunction, applySizeOp, + applyCast, } from '../../src/optimisations/OperationSimulations'; import { UnaryOperator, BinaryOperator } from '../../src/ast/Operator'; import { @@ -28,6 +29,36 @@ describe('Operation simulation', () => { await delay(1000); }); + describe('applyCast', () => { + fixtures.applyCast.success.forEach(([should, castType, input, expected]: any) => { + it(should as string, () => { + // given + const inputNode = literalToNode(input); + const expectedNode = literalToNode(expected); + + // when + const res = applyCast(inputNode, castType); + + // then + expect(res).toEqual(expectedNode); + }); + }); + + fixtures.applyCast.fail.forEach(([should, castType, input, expected]: any) => { + it(should as string, () => { + // given + const inputNode = literalToNode(input); + + expect(() => { + // when + applyCast(inputNode, castType); + + // then + }).toThrow(new ExecutionError(expected as string)); + }); + }); + }); + describe('applyGlobalFunction', () => { fixtures.applyGlobalFunction.success .forEach(([should, fn, parameters, expected]: any) => { From e7b9960eb5ab8d528506b2bdc867c1389f31e4e5 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年3月11日 16:49:41 +0100 Subject: [PATCH 12/14] Allow Blocks to be used as Statements - This is a prerequisite for simplification of (among others) if-statements - Make list of statements required (it was already de facto so) - [Code generation] Move scope depth tracking and scope cleanup to BlockNode rather than BranchNode --- packages/cashc/src/ast/AST.ts | 4 +-- packages/cashc/src/ast/AstBuilder.ts | 4 +-- packages/cashc/src/ast/AstTraversal.ts | 6 ++-- .../src/generation/GenerateTargetTraversal.ts | 29 +++++++++++-------- .../src/print/OutputSourceCodeTraversal.ts | 2 +- .../src/semantic/SymbolTableTraversal.ts | 4 +-- .../cashc/src/semantic/TypeCheckTraversal.ts | 5 ++-- 7 files changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/cashc/src/ast/AST.ts b/packages/cashc/src/ast/AST.ts index 7bf27189..d1a15ad4 100644 --- a/packages/cashc/src/ast/AST.ts +++ b/packages/cashc/src/ast/AST.ts @@ -147,11 +147,11 @@ export class BranchNode extends StatementNode { } } -export class BlockNode extends Node { +export class BlockNode extends StatementNode { symbolTable?: SymbolTable; constructor( - public statements?: StatementNode[], + public statements: StatementNode[], ) { super(); } diff --git a/packages/cashc/src/ast/AstBuilder.ts b/packages/cashc/src/ast/AstBuilder.ts index e1f5dcba..023012ea 100644 --- a/packages/cashc/src/ast/AstBuilder.ts +++ b/packages/cashc/src/ast/AstBuilder.ts @@ -185,8 +185,8 @@ export default class AstBuilder visitIfStatement(ctx: IfStatementContext): BranchNode { const condition = this.visit(ctx.expression()); - const ifBlock = this.visit(ctx._ifBlock) as StatementNode; - const elseBlock = ctx._elseBlock && this.visit(ctx._elseBlock) as StatementNode; + const ifBlock = this.visit(ctx._ifBlock) as BlockNode; + const elseBlock = ctx._elseBlock && this.visit(ctx._elseBlock) as BlockNode; const branch = new BranchNode(condition, ifBlock, elseBlock); branch.location = Location.fromCtx(ctx); return branch; diff --git a/packages/cashc/src/ast/AstTraversal.ts b/packages/cashc/src/ast/AstTraversal.ts index 3532403e..43b517e7 100644 --- a/packages/cashc/src/ast/AstTraversal.ts +++ b/packages/cashc/src/ast/AstTraversal.ts @@ -73,13 +73,13 @@ export default class AstTraversal extends AstVisitor { visitBranch(node: BranchNode): Node { node.condition = this.visit(node.condition); - node.ifBlock = this.visit(node.ifBlock) as StatementNode; - node.elseBlock = this.visitOptional(node.elseBlock) as StatementNode; + node.ifBlock = this.visit(node.ifBlock) as BlockNode; + node.elseBlock = this.visitOptional(node.elseBlock) as BlockNode; return node; } visitBlock(node: BlockNode): Node { - node.statements = this.visitOptionalList(node.statements) as StatementNode[]; + node.statements = this.visitList(node.statements) as StatementNode[]; return node; } diff --git a/packages/cashc/src/generation/GenerateTargetTraversal.ts b/packages/cashc/src/generation/GenerateTargetTraversal.ts index 702548df..558acaa1 100644 --- a/packages/cashc/src/generation/GenerateTargetTraversal.ts +++ b/packages/cashc/src/generation/GenerateTargetTraversal.ts @@ -144,7 +144,10 @@ export default class GenerateTargetTraversal extends AstTraversal { } node.parameters = this.visitList(node.parameters) as ParameterNode[]; - node.body = this.visit(node.body) as BlockNode; + + // Don't visit block node, just its children, to skip the scope cleanup + // of the block node. End-of-function cleanup is done separately below. + node.body = new BlockNode(this.visitList(node.body.statements)); // Remove final OP_VERIFY // If the final opcodes are OP_CHECK{LOCKTIME|SEQUENCE}VERIFY OP_DROP @@ -333,26 +336,28 @@ export default class GenerateTargetTraversal extends AstTraversal { node.condition = this.visit(node.condition); this.popFromStack(); - this.scopeDepth += 1; this.emit(Op.OP_IF); - - let stackDepth = this.stack.length; - node.ifBlock = this.visit(node.ifBlock); - this.removeScopedVariables(stackDepth); + node.ifBlock = this.visit(node.ifBlock) as BlockNode; if (node.elseBlock) { this.emit(Op.OP_ELSE); - stackDepth = this.stack.length; - node.elseBlock = this.visit(node.elseBlock); - this.removeScopedVariables(stackDepth); + node.elseBlock = this.visit(node.elseBlock) as BlockNode; } this.emit(Op.OP_ENDIF); - this.scopeDepth -= 1; return node; } + visitBlock(node: BlockNode): Node { + this.scopeDepth += 1; + const stackDepth = this.stack.length; + node.statements = this.visitList(node.statements); + this.removeScopedVariables(stackDepth); + this.scopeDepth -= 1; + return node; + } + removeScopedVariables(depthBeforeScope: number): void { const dropCount = this.stack.length - depthBeforeScope; for (let i = 0; i < dropCount; i += 1) { @@ -572,8 +577,6 @@ export default class GenerateTargetTraversal extends AstTraversal { const stackIndex = this.getStackIndex(node.name); this.emit(Data.encodeInt(stackIndex)); - // If the final use is inside an if-statement, we still OP_PICK it - // We do this so that there's no difference in stack depths between execution paths if (this.isOpRoll(node)) { this.emit(Op.OP_ROLL); this.removeFromStack(stackIndex); @@ -586,6 +589,8 @@ export default class GenerateTargetTraversal extends AstTraversal { } isOpRoll(node: IdentifierNode): boolean { + // If the final use of a variable is inside a block, we still OP_PICK it + // We do this so that there's no difference in stack depths between execution paths return this.currentFunction.opRolls.get(node.name) === node && this.scopeDepth === 0; } diff --git a/packages/cashc/src/print/OutputSourceCodeTraversal.ts b/packages/cashc/src/print/OutputSourceCodeTraversal.ts index bb5da14f..d3be4251 100644 --- a/packages/cashc/src/print/OutputSourceCodeTraversal.ts +++ b/packages/cashc/src/print/OutputSourceCodeTraversal.ts @@ -150,7 +150,7 @@ export default class OutputSourceCodeTraversal extends AstTraversal { this.addOutput('\n'); this.indent(); - node.statements = this.visitOptionalList(node.statements) as StatementNode[]; + node.statements = this.visitList(node.statements) as StatementNode[]; this.unindent(); this.addOutput('}', true); diff --git a/packages/cashc/src/semantic/SymbolTableTraversal.ts b/packages/cashc/src/semantic/SymbolTableTraversal.ts index 2229cf24..421741ed 100644 --- a/packages/cashc/src/semantic/SymbolTableTraversal.ts +++ b/packages/cashc/src/semantic/SymbolTableTraversal.ts @@ -66,7 +66,7 @@ export default class SymbolTableTraversal extends AstTraversal { this.symbolTables.unshift(node.symbolTable); node.parameters = this.visitList(node.parameters) as ParameterNode[]; - node.body = this.visit(node.body); + node.body = this.visit(node.body) as BlockNode; const unusedSymbols = node.symbolTable.unusedSymbols(); if (unusedSymbols.length !== 0) { @@ -81,7 +81,7 @@ export default class SymbolTableTraversal extends AstTraversal { node.symbolTable = new SymbolTable(this.symbolTables[0]); this.symbolTables.unshift(node.symbolTable); - node.statements = this.visitOptionalList(node.statements) as StatementNode[]; + node.statements = this.visitList(node.statements) as StatementNode[]; const unusedSymbols = node.symbolTable.unusedSymbols(); if (unusedSymbols.length !== 0) { diff --git a/packages/cashc/src/semantic/TypeCheckTraversal.ts b/packages/cashc/src/semantic/TypeCheckTraversal.ts index 2cd18dfe..e92a9a29 100644 --- a/packages/cashc/src/semantic/TypeCheckTraversal.ts +++ b/packages/cashc/src/semantic/TypeCheckTraversal.ts @@ -15,6 +15,7 @@ import { RequireNode, Node, InstantiationNode, + BlockNode, } from '../ast/AST'; import AstTraversal from '../ast/AstTraversal'; import { @@ -87,8 +88,8 @@ export default class TypeCheckTraversal extends AstTraversal { visitBranch(node: BranchNode): Node { node.condition = this.visit(node.condition); - node.ifBlock = this.visit(node.ifBlock); - node.elseBlock = this.visitOptional(node.elseBlock); + node.ifBlock = this.visit(node.ifBlock) as BlockNode; + node.elseBlock = this.visitOptional(node.elseBlock) as BlockNode; if (!implicitlyCastable(node.condition.type, PrimitiveType.BOOL)) { throw new TypeError(node, node.condition.type, PrimitiveType.BOOL); From 598fec42def48a1df6e28a7267f941c8787ff97c Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年3月11日 17:41:53 +0100 Subject: [PATCH 13/14] Add simulation of if-statements --- .../src/optimisations/OperationSimulations.ts | 12 ++++- packages/cashc/test/optimisations/fixtures.ts | 44 +++++++++++++++++++ .../test/optimisations/simulation.test.ts | 13 ++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/cashc/src/optimisations/OperationSimulations.ts b/packages/cashc/src/optimisations/OperationSimulations.ts index 393b3482..15eeef5d 100644 --- a/packages/cashc/src/optimisations/OperationSimulations.ts +++ b/packages/cashc/src/optimisations/OperationSimulations.ts @@ -16,6 +16,8 @@ import { StringLiteralNode, FunctionCallNode, Node, + BranchNode, + BlockNode, } from '../ast/AST'; import { GlobalFunction } from '../ast/Globals'; import { Data } from '../util'; @@ -73,7 +75,15 @@ export function decodeLiteralNode(value: Buffer, type: Type): LiteralNode { } // TODO: RequireNode -// TODO: BranchNode + +export function applyBranch(node: BranchNode): BlockNode { + if (!(node.condition instanceof BoolLiteralNode)) throw new Error(); + if (node.condition.value) { + return node.ifBlock; + } else { + return node.elseBlock || new BlockNode([]); + } +} export function applyCast(node: LiteralNode, castType: Type): LiteralNode { let sourceType = new BytesType(); diff --git a/packages/cashc/test/optimisations/fixtures.ts b/packages/cashc/test/optimisations/fixtures.ts index 6e517b32..0addb29f 100644 --- a/packages/cashc/test/optimisations/fixtures.ts +++ b/packages/cashc/test/optimisations/fixtures.ts @@ -1,6 +1,12 @@ import { BinaryOperator, UnaryOperator } from '../../src/ast/Operator'; import { GlobalFunction } from '../../src/ast/Globals'; import { PrimitiveType, BytesType } from '../../src'; +import { + BranchNode, + BoolLiteralNode, + RequireNode, + BlockNode, +} from '../../src/ast/AST'; const MAXINT = 2147483647; @@ -24,6 +30,44 @@ const SigCheck = { }; export const fixtures = { + applyBranch: { + success: [ + [ + 'should apply if (true) require(true)', + new BranchNode( + new BoolLiteralNode(true), + new BlockNode([new RequireNode(new BoolLiteralNode(true))]), + ), + new BlockNode([new RequireNode(new BoolLiteralNode(true))]), + ], + [ + 'should apply if (false) require(true)', + new BranchNode( + new BoolLiteralNode(false), + new BlockNode([new RequireNode(new BoolLiteralNode(true))]), + ), + new BlockNode([]), + ], + [ + 'should apply if (true) require(true) else require(false)', + new BranchNode( + new BoolLiteralNode(true), + new BlockNode([new RequireNode(new BoolLiteralNode(true))]), + new BlockNode([new RequireNode(new BoolLiteralNode(false))]), + ), + new BlockNode([new RequireNode(new BoolLiteralNode(true))]), + ], + [ + 'should apply if (false) require(true) else require(false)', + new BranchNode( + new BoolLiteralNode(false), + new BlockNode([new RequireNode(new BoolLiteralNode(true))]), + new BlockNode([new RequireNode(new BoolLiteralNode(false))]), + ), + new BlockNode([new RequireNode(new BoolLiteralNode(false))]), + ], + ], + }, applyCast: { success: [ ['should apply bool(-12)', PrimitiveType.BOOL, -12, true], diff --git a/packages/cashc/test/optimisations/simulation.test.ts b/packages/cashc/test/optimisations/simulation.test.ts index 16acff78..5974258b 100644 --- a/packages/cashc/test/optimisations/simulation.test.ts +++ b/packages/cashc/test/optimisations/simulation.test.ts @@ -10,6 +10,7 @@ import { applyGlobalFunction, applySizeOp, applyCast, + applyBranch, } from '../../src/optimisations/OperationSimulations'; import { UnaryOperator, BinaryOperator } from '../../src/ast/Operator'; import { @@ -29,6 +30,18 @@ describe('Operation simulation', () => { await delay(1000); }); + describe('applyBranch', () => { + fixtures.applyBranch.success.forEach(([should, branchNode, expectedNodes]: any) => { + it(should as string, () => { + // when + const res = applyBranch(branchNode); + + // then + expect(res).toEqual(expectedNodes); + }); + }); + }); + describe('applyCast', () => { fixtures.applyCast.success.forEach(([should, castType, input, expected]: any) => { it(should as string, () => { From 62523c1c6dc25d0d36e755ac2e2dcf842b618c6e Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: 2020年3月12日 19:41:44 +0100 Subject: [PATCH 14/14] Finalise OperationSimulations - Add applySplitAndIndex - Add applyRequire - Instantiation simplification will be done in the code generation due to complexity and partial simplification --- .../src/optimisations/OperationSimulations.ts | 29 ++++++++-- packages/cashc/test/optimisations/fixtures.ts | 34 ++++++++++++ .../test/optimisations/simulation.test.ts | 54 ++++++++++++++++++- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/packages/cashc/src/optimisations/OperationSimulations.ts b/packages/cashc/src/optimisations/OperationSimulations.ts index 15eeef5d..8b0d1d58 100644 --- a/packages/cashc/src/optimisations/OperationSimulations.ts +++ b/packages/cashc/src/optimisations/OperationSimulations.ts @@ -18,6 +18,9 @@ import { Node, BranchNode, BlockNode, + RequireNode, + TupleIndexOpNode, + SplitOpNode, } from '../ast/AST'; import { GlobalFunction } from '../ast/Globals'; import { Data } from '../util'; @@ -74,7 +77,11 @@ export function decodeLiteralNode(value: Buffer, type: Type): LiteralNode { } } -// TODO: RequireNode +export function applyRequire(node: RequireNode): Node { + if (!(node.expression instanceof BoolLiteralNode)) throw new Error(); + // TODO: throw an error on "always failing require" (outside of branches) + return node.expression.value ? new BlockNode([]) : node; +} export function applyBranch(node: BranchNode): BlockNode { if (!(node.condition instanceof BoolLiteralNode)) throw new Error(); @@ -113,8 +120,24 @@ export function applyGlobalFunction(node: FunctionCallNode): Node { return decodeLiteralNode(Buffer.from(res), returnType(fn)); } -// TODO: InstantiationNode -// TODO: TupleIndexOpNode + SplitOpNode +// Optimisation of InstantiationNode Literals is done in Target Code Generation +// due to the more complex nature of these operations and possibility for +// partial simplification. + +export function applySplitAndIndex(node: TupleIndexOpNode): LiteralNode { + if (!(node.tuple instanceof SplitOpNode)) throw new Error(); + if (!(node.tuple.object instanceof LiteralNode)) throw new Error(); + if (!(node.tuple.index instanceof LiteralNode)) throw new Error(); + const indexOp = node.index === 0 ? Op.OP_DROP : Op.OP_NIP; + const type = node.tuple.object.type || new BytesType(); + const script: Script = [ + encodeLiteralNode(node.tuple.object), + encodeLiteralNode(node.tuple.index), + Op.OP_SPLIT, indexOp, + ]; + const res = executeScriptOnVM(script)[0]; + return decodeLiteralNode(Buffer.from(res), type); +} export function applySizeOp(node: StringLiteralNode | HexLiteralNode): LiteralNode { const script: Script = [encodeLiteralNode(node), Op.OP_SIZE, Op.OP_NIP]; diff --git a/packages/cashc/test/optimisations/fixtures.ts b/packages/cashc/test/optimisations/fixtures.ts index 0addb29f..5dbfa12e 100644 --- a/packages/cashc/test/optimisations/fixtures.ts +++ b/packages/cashc/test/optimisations/fixtures.ts @@ -19,6 +19,7 @@ enum Error { NULLFAIL = 'Program failed a signature verification with a non-null signature (violating the "NULLFAIL" rule).', NUM2BIN = 'Program called an OP_NUM2BIN operation with an insufficient byte length to re-encode the provided number.', BIN2NUM = 'Program attempted an OP_BIN2NUM operation on a byte sequence which cannot be encoded within the maximum Script Number length (4 bytes).', + SPLIT_INDEX = 'Program called an OP_SPLIT operation with an invalid index.', } // (https://kjur.github.io/jsrsasign/sample/sample-ecdsa.html) @@ -30,6 +31,12 @@ const SigCheck = { }; export const fixtures = { + applyRequire: { + success: [ + ['should apply require(true)', new RequireNode(new BoolLiteralNode(true)), new BlockNode([])], + ['should not apply require(false)', new RequireNode(new BoolLiteralNode(false)), new RequireNode(new BoolLiteralNode(false))], + ], + }, applyBranch: { success: [ [ @@ -197,6 +204,33 @@ export const fixtures = { ['should apply maxlen_x.length', 'X'.repeat(520), 520], ], }, + applySplitAndIndex: { + success: [ + ['should apply "Bitcoin Cash".split(7)[0]', 'Bitcoin Cash', 7, 0, 'Bitcoin'], + ['should apply "Bitcoin Cash".split(8)[1]', 'Bitcoin Cash', 8, 1, 'Cash'], + ['should apply "Bitcoin Cash".split(0)[0]', 'Bitcoin Cash', 0, 0, ''], + ['should apply "Bitcoin Cash".split(0)[1]', 'Bitcoin Cash', 0, 1, 'Bitcoin Cash'], + ['should apply "Bitcoin Cash".split(12)[0]', 'Bitcoin Cash', 12, 0, 'Bitcoin Cash'], + ['should apply "Bitcoin Cash".split(12)[1]', 'Bitcoin Cash', 12, 1, ''], + ['should apply "".split(0)[0]', '', 0, 0, ''], + ['should apply "".split(0)[1]', '', 0, 1, ''], + ['should apply 0xbeef.split(1)[0]', '0xbeef', 1, 0, '0xbe'], + ['should apply 0xbeef.split(1)[1]', '0xbeef', 1, 1, '0xef'], + ['should apply 0xbeef.split(0)[0]', '0xbeef', 0, 0, '0x'], + ['should apply 0xbeef.split(0)[1]', '0xbeef', 0, 1, '0xbeef'], + ['should apply 0xbeef.split(2)[0]', '0xbeef', 2, 0, '0xbeef'], + ['should apply 0xbeef.split(2)[1]', '0xbeef', 2, 1, '0x'], + ['should apply 0x.split(0)[0]', '0x', 0, 0, '0x'], + ['should apply 0x.split(0)[1]', '0x', 0, 1, '0x'], + ['should apply maxlen_x.split(200)[0]', `0x${'58'.repeat(520)}`, 200, 0, `0x${'58'.repeat(200)}`], + ['should apply maxlen_x.split(200)[0]', 'X'.repeat(520), 200, 0, 'X'.repeat(200)], + ], + fail: [ + ['should fail on "Bitcoin Cash".split(-1)[0]', 'Bitcoin Cash', -1, 0, Error.SPLIT_INDEX], + ['should fail on "Bitcoin Cash".split(13)[0]', 'Bitcoin Cash', 13, 0, Error.SPLIT_INDEX], + ['should fail on "Bitcoin Cash".split(MAXINT + 1)[0]', 'Bitcoin Cash', MAXINT + 1, 0, Error.INVALID_SCRIPT_NUMBER], + ], + }, applyBinaryOperator: { success: [ // AND diff --git a/packages/cashc/test/optimisations/simulation.test.ts b/packages/cashc/test/optimisations/simulation.test.ts index 5974258b..6fd14766 100644 --- a/packages/cashc/test/optimisations/simulation.test.ts +++ b/packages/cashc/test/optimisations/simulation.test.ts @@ -11,6 +11,8 @@ import { applySizeOp, applyCast, applyBranch, + applyRequire, + applySplitAndIndex, } from '../../src/optimisations/OperationSimulations'; import { UnaryOperator, BinaryOperator } from '../../src/ast/Operator'; import { @@ -20,6 +22,8 @@ import { HexLiteralNode, FunctionCallNode, IdentifierNode, + TupleIndexOpNode, + SplitOpNode, } from '../../src/ast/AST'; import { fixtures } from './fixtures'; import { ExecutionError } from '../../src/Errors'; @@ -30,14 +34,26 @@ describe('Operation simulation', () => { await delay(1000); }); + describe('applyRequire', () => { + fixtures.applyRequire.success.forEach(([should, requireNode, expectedNode]: any) => { + it(should as string, () => { + // when + const res = applyRequire(requireNode); + + // then + expect(res).toEqual(expectedNode); + }); + }); + }); + describe('applyBranch', () => { - fixtures.applyBranch.success.forEach(([should, branchNode, expectedNodes]: any) => { + fixtures.applyBranch.success.forEach(([should, branchNode, expectedNode]: any) => { it(should as string, () => { // when const res = applyBranch(branchNode); // then - expect(res).toEqual(expectedNodes); + expect(res).toEqual(expectedNode); }); }); }); @@ -110,6 +126,40 @@ describe('Operation simulation', () => { }); }); + describe('applySplitAndIndex', () => { + fixtures.applySplitAndIndex.success.forEach(([should, input, split, index, expected]: any) => { + it(should as string, () => { + // given + const inputNode = literalToNode(input); + const splitNode = literalToNode(split); + const indexNode = new TupleIndexOpNode(new SplitOpNode(inputNode, splitNode), index); + const expectedNode = literalToNode(expected); + + // when + const res = applySplitAndIndex(indexNode); + + // then + expect(res).toEqual(expectedNode); + }); + }); + + fixtures.applySplitAndIndex.fail.forEach(([should, input, split, index, expected]: any) => { + it(should as string, () => { + // given + const inputNode = literalToNode(input); + const splitNode = literalToNode(split); + const indexNode = new TupleIndexOpNode(new SplitOpNode(inputNode, splitNode), index); + + expect(() => { + // when + applySplitAndIndex(indexNode); + + // then + }).toThrow(new ExecutionError(expected)); + }); + }); + }); + describe('applySizeOp', () => { fixtures.applySizeOp.success.forEach(([should, input, expected]: any) => { it(should as string, () => {

AltStyle によって変換されたページ (->オリジナル) /