From 4bb320a7bd2c6aeb222227889c612c7c8fc2c9fa Mon Sep 17 00:00:00 2001 From: friedger Date: Wed, 22 Nov 2023 21:54:01 +0100 Subject: [PATCH 1/4] feat: remove non-compact version --- Clarinet.toml | 5 -- contracts/clarity-bitcoin.clar | 11 --- contracts/examples/send-to-first-input.clar | 31 ------- contracts/helper.clar | 3 - .../tests/clarity-bitcoin-helper_test.clar | 13 +++ contracts/tests/clarity-bitcoin_test.clar | 90 +++++-------------- tests/send-to-first-input.test.ts | 56 ------------ 7 files changed, 37 insertions(+), 172 deletions(-) delete mode 100644 contracts/examples/send-to-first-input.clar delete mode 100644 tests/send-to-first-input.test.ts diff --git a/Clarinet.toml b/Clarinet.toml index 3f17206..c6fa731 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -45,11 +45,6 @@ path = 'contracts/tests/clarity-bitcoin-helper_test.clar' clarity_version = 2 epoch = 2.1 -[contracts.send-to-first-input] -path = 'contracts/examples/send-to-first-input.clar' -clarity_version = 2 -epoch = 2.1 - [contracts.send-to-first-input-compact] path = 'contracts/examples/send-to-first-input-compact.clar' clarity_version = 2 diff --git a/contracts/clarity-bitcoin.clar b/contracts/clarity-bitcoin.clar index b79ae27..85ecfee 100644 --- a/contracts/clarity-bitcoin.clar +++ b/contracts/clarity-bitcoin.clar @@ -490,17 +490,6 @@ (let ((block (unwrap! (parse-block-header header) (err ERR-BAD-HEADER)))) (was-tx-mined-internal height tx header (get merkle-root block) proof))) -;; Determine whether or not a Bitcoin transaction was mined in a prior Bitcoin block. -;; with the given header object and merkle proof. -;; Returns (ok txid) if tx was mined else -;; returns (err u1) if the header is invalid or -;; returns (err u2) if the proof is invalid. -(define-read-only (was-tx-mined (height uint) (tx (buff 4096)) - (header { version: (buff 4), parent: (buff 32), merkle-root: (buff 32), timestamp: (buff 4), nbits: (buff 4), nonce: (buff 4) }) - (proof { tx-index: uint, hashes: (list 14 (buff 32)), tree-depth: uint})) - (was-tx-mined-internal height tx (contract-call? .clarity-bitcoin-helper concat-header header) (reverse-buff32 (get merkle-root header)) proof)) - - ;; Private function to verify block header and merkle proof. ;; This function must only be called with the merkle root of the provided header. ;; Use was-tx-mined-compact with header as a buffer or diff --git a/contracts/examples/send-to-first-input.clar b/contracts/examples/send-to-first-input.clar deleted file mode 100644 index aed398b..0000000 --- a/contracts/examples/send-to-first-input.clar +++ /dev/null @@ -1,31 +0,0 @@ -(define-constant SATS-PER-STX u1000) -(define-constant err-not-found (err u404)) -(define-constant err-unsupported-tx (err u500)) -(define-constant err-out-not-found (err u501)) -(define-constant err-in-not-found (err u502)) - - -;; TODO get price from miners -(define-read-only (sats-to-stx (sats uint)) - (/ sats SATS-PER-STX)) - -;; for compressed public keys -(define-read-only (p2pkh-to-principal (scriptSig (buff 256))) - (let ((pk (unwrap! (as-max-len? (unwrap! (slice? scriptSig (- (len scriptSig) u33) (len scriptSig)) none) u33) none))) - (some (unwrap! (principal-of? pk) none)))) - -(define-public (send-to-first-input (height uint) (tx (buff 1024)) - (header { version: (buff 4), parent: (buff 32), merkle-root: (buff 32), timestamp: (buff 4), nbits: (buff 4), nonce: (buff 4) }) - (proof { tx-index: uint, hashes: (list 14 (buff 32)), tree-depth: uint})) - (let ( - ;; extract parts of Bitcoin transaction - (tx-obj (try! (contract-call? .clarity-bitcoin parse-tx tx))) - (id-of-mined-tx (try! (contract-call? .clarity-bitcoin was-tx-mined height tx header proof))) - (first-output (unwrap! (element-at (get outs tx-obj) u0) err-out-not-found)) - (first-input (unwrap! (element-at (get ins tx-obj) u0) err-in-not-found))) - ;; TODO check whether the tx-sender is the same as the first output - - ;; transfer stx to first-input - (stx-transfer? (sats-to-stx (get value first-output)) - tx-sender - (unwrap! (p2pkh-to-principal (get scriptSig first-input)) err-unsupported-tx)))) diff --git a/contracts/helper.clar b/contracts/helper.clar index 2b9046d..50426a5 100644 --- a/contracts/helper.clar +++ b/contracts/helper.clar @@ -10,9 +10,6 @@ (define-public (verify-mp (reverse-tx-id (buff 32)) (merkle-root (buff 32)) (proof { tx-index: uint, hashes: (list 14 (buff 32)), tree-depth: uint})) (contract-call? .clarity-bitcoin verify-merkle-proof reverse-tx-id merkle-root proof)) -(define-public (was-tx-mined (height uint) (tx (buff 1024)) (header { version: (buff 4), parent: (buff 32), merkle-root: (buff 32), timestamp: (buff 4), nbits: (buff 4), nonce: (buff 4) }) (proof { tx-index: uint, hashes: (list 14 (buff 32)), tree-depth: uint})) - (contract-call? .clarity-bitcoin was-tx-mined height tx header proof)) - (define-public (was-tx-mined-compact (height uint) (tx (buff 1024)) (header (buff 80)) (proof { tx-index: uint, hashes: (list 14 (buff 32)), tree-depth: uint})) (contract-call? .clarity-bitcoin was-tx-mined-compact height tx header proof)) diff --git a/contracts/tests/clarity-bitcoin-helper_test.clar b/contracts/tests/clarity-bitcoin-helper_test.clar index db4d83d..dda6ede 100644 --- a/contracts/tests/clarity-bitcoin-helper_test.clar +++ b/contracts/tests/clarity-bitcoin-helper_test.clar @@ -15,3 +15,16 @@ (asserts! (is-eq result 0x1122334401112233445566778899aabbccddeeff00112233445566778899aabbccddeeff000011223380112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff001122334401001122334455667780112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff0044332211) (err result)) (ok true))) +;; @name concat header +;; block id: 000000000000000606f86a5bc8fb6e38b16050fb4676dea26cba5222583c4d86 +(define-public (test-concat-header) + (let ((result (contract-call? .clarity-bitcoin-helper concat-header + {merkle-root: 0x9160ba7ae5f29f9632dc0cd89f466ee64e2dddfde737a40808ddc147cd82406f, + version: 0x0000a020, + nbits: 0x88a12719, + nonce: 0x9842cec7, + timestamp: 0x18b84864, + parent: 0x65bc9201b5b5a1d695a18e4d5efe5d52d8ccc4129a2499141d00000000000000 + }))) + (asserts! (is-eq result 0x0000a02065bc9201b5b5a1d695a18e4d5efe5d52d8ccc4129a2499141d000000000000009160ba7ae5f29f9632dc0cd89f466ee64e2dddfde737a40808ddc147cd82406f18b8486488a127199842cec7) (err result)) + (ok true))) diff --git a/contracts/tests/clarity-bitcoin_test.clar b/contracts/tests/clarity-bitcoin_test.clar index 06b59b3..91a3c56 100644 --- a/contracts/tests/clarity-bitcoin_test.clar +++ b/contracts/tests/clarity-bitcoin_test.clar @@ -210,6 +210,30 @@ ) ) +;; @name verify transaction with wrong block height +;; arbitrary segwit transaction +(define-public (test-was-tx-mined-internal-8) + (let ( + (burnchain-block-height u1) + (txid 0x3b3a7a31c949048fabf759e670a55ffd5b9472a12e748b684db5d264b6852084) + (raw-tx 0x020000000218f905443202116524547142bd55b69335dfc4e4c66ff3afaaaab6267b557c4b030000000000000000e0dbdf1039321ab7a2626ca5458e766c6107690b1a1923e075c4f691cc4928ac0000000000000000000220a10700000000002200208730dbfaa29c49f00312812aa12a62335113909711deb8da5ecedd14688188363c5f26010000000022512036f4ff452cb82e505436e73d0a8b630041b71e037e5997290ba1fe0ae7f4d8d56d182500) + ;; block id: 000000000000000606f86a5bc8fb6e38b16050fb4676dea26cba5222583c4d86 + (raw-block-header 0x0000a02065bc9201b5b5a1d695a18e4d5efe5d52d8ccc4129a2499141d000000000000009160ba7ae5f29f9632dc0cd89f466ee64e2dddfde737a40808ddc147cd82406f18b8486488a127199842cec7) + (parsed-block-header (contract-call? .clarity-bitcoin parse-block-header raw-block-header)) + (parsed-tx (contract-call? .clarity-bitcoin parse-tx raw-tx)) + ) + (let ((result (contract-call? .clarity-bitcoin was-tx-mined-compact + burnchain-block-height + raw-tx + raw-block-header + {tx-index: u3, + tree-depth: u2, + hashes: (list 0x3313f803502a6f9a89ac09ff9e8f9d8032aa7c35cc6d1679487622e944c8ccb8 0xc4e620f495d8a30d8d919fc148fe55c8873b4aefe43116bc6ef895aa51572215)} + ))) + (asserts! (is-eq result (err ERR-HEADER-HEIGHT-MISMATCH)) (err "expected ERR-HEADER-HEIGHT-MISMATCH")) + (ok true)) + ) +) ;; @name verify segwit transaction with left over data (define-public (test-parse-tx) @@ -238,72 +262,6 @@ ) ) -;; @name verify transaction with header object -(define-public (test-was-tx-mined-with-header-object-1) - (let ( - (burnchain-block-height u2431087) - (txid 0x3b3a7a31c949048fabf759e670a55ffd5b9472a12e748b684db5d264b6852084) - (raw-tx 0x020000000218f905443202116524547142bd55b69335dfc4e4c66ff3afaaaab6267b557c4b030000000000000000e0dbdf1039321ab7a2626ca5458e766c6107690b1a1923e075c4f691cc4928ac0000000000000000000220a10700000000002200208730dbfaa29c49f00312812aa12a62335113909711deb8da5ecedd14688188363c5f26010000000022512036f4ff452cb82e505436e73d0a8b630041b71e037e5997290ba1fe0ae7f4d8d56d182500) - ;; block id: 000000000000000606f86a5bc8fb6e38b16050fb4676dea26cba5222583c4d86 - (raw-block-header 0x0000a02065bc9201b5b5a1d695a18e4d5efe5d52d8ccc4129a2499141d000000000000009160ba7ae5f29f9632dc0cd89f466ee64e2dddfde737a40808ddc147cd82406f18b8486488a127199842cec7) - (parsed-block-header (contract-call? .clarity-bitcoin parse-block-header raw-block-header)) - (parsed-tx (contract-call? .clarity-bitcoin parse-tx raw-tx)) - ) - ;; prepare - - (let ((result (contract-call? .clarity-bitcoin was-tx-mined - burnchain-block-height - raw-tx - {merkle-root: 0x9160ba7ae5f29f9632dc0cd89f466ee64e2dddfde737a40808ddc147cd82406f, - version: 0x0000a020, - nbits: 0x88a12719, - nonce: 0x9842cec7, - timestamp: 0x18b84864, - parent: 0x65bc9201b5b5a1d695a18e4d5efe5d52d8ccc4129a2499141d00000000000000, - } - {tx-index: u3, - tree-depth: u2, - hashes: (list 0x3313f803502a6f9a89ac09ff9e8f9d8032aa7c35cc6d1679487622e944c8ccb8 0xc4e620f495d8a30d8d919fc148fe55c8873b4aefe43116bc6ef895aa51572215)} - ))) - (asserts! (is-eq result (ok txid)) (err "expected txid")) - (ok true)) - ) -) - - -;; @name verify transaction with header object but wrong version -(define-public (test-was-tx-mined-with-header-object-2) - (let ( - (burnchain-block-height u2431087) - (txid 0x3b3a7a31c949048fabf759e670a55ffd5b9472a12e748b684db5d264b6852084) - (raw-tx 0x020000000218f905443202116524547142bd55b69335dfc4e4c66ff3afaaaab6267b557c4b030000000000000000e0dbdf1039321ab7a2626ca5458e766c6107690b1a1923e075c4f691cc4928ac0000000000000000000220a10700000000002200208730dbfaa29c49f00312812aa12a62335113909711deb8da5ecedd14688188363c5f26010000000022512036f4ff452cb82e505436e73d0a8b630041b71e037e5997290ba1fe0ae7f4d8d56d182500) - ;; block id: 000000000000000606f86a5bc8fb6e38b16050fb4676dea26cba5222583c4d86 - (raw-block-header 0x0000a02065bc9201b5b5a1d695a18e4d5efe5d52d8ccc4129a2499141d000000000000009160ba7ae5f29f9632dc0cd89f466ee64e2dddfde737a40808ddc147cd82406f18b8486488a127199842cec7) - (parsed-block-header (contract-call? .clarity-bitcoin parse-block-header raw-block-header)) - (parsed-tx (contract-call? .clarity-bitcoin parse-tx raw-tx)) - ) - ;; prepare - - (let ((result (contract-call? .clarity-bitcoin was-tx-mined - burnchain-block-height - raw-tx - {merkle-root: 0x9160ba7ae5f29f9632dc0cd89f466ee64e2dddfde737a40808ddc147cd82406f, - version: 0x00000000, - nbits: 0x88a12719, - nonce: 0x9842cec7, - timestamp: 0x18b84864, - parent: 0x65bc9201b5b5a1d695a18e4d5efe5d52d8ccc4129a2499141d00000000000000, - } - {tx-index: u3, - tree-depth: u2, - hashes: (list 0x3313f803502a6f9a89ac09ff9e8f9d8032aa7c35cc6d1679487622e944c8ccb8 0xc4e620f495d8a30d8d919fc148fe55c8873b4aefe43116bc6ef895aa51572215)} - ))) - (asserts! (is-eq result (err ERR-HEADER-HEIGHT-MISMATCH)) (err "expected ERR-HEADER-HEIGHT-MISMATCH")) - (ok true)) - ) -) - - (define-constant ERR-OUT-OF-BOUNDS u1) (define-constant ERR-TOO-MANY-TXINS u2) (define-constant ERR-TOO-MANY-TXOUTS u3) diff --git a/tests/send-to-first-input.test.ts b/tests/send-to-first-input.test.ts deleted file mode 100644 index 0eac109..0000000 --- a/tests/send-to-first-input.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { hexToBytes } from "./utils.ts"; -import { Cl } from "@stacks/transactions"; -import { tx as Tx } from "@hirosystems/clarinet-sdk"; -const accounts = simnet.getAccounts(); -const chain = simnet; - -describe("Send to first input", () => { - it("Ensure that scriptSig is converted to principal", () => { - const deployer = accounts.get("deployer")!; - const response = chain.callReadOnlyFn( - "send-to-first-input", - "p2pkh-to-principal", - [ - Cl.bufferFromHex( - "473044022017e2af6e1308d431365deeb5739d41a909cf0d61a9c0e48f3ae5b0bd6544bfc5022066e73dd26d71d824552b034b322603cce8b936912b99f4f3df512e502bd7c11e012103d7b3bc2d0b4b72a845c469c9fee3c8cf475a2f237e379d7f75a4f463f7bd6ebd" - ), - ], - deployer - ); - - expect(response.result).toBeSome( - Cl.standardPrincipal("ST2X7X1A0649S3BJR0DEB58NQ73E24FVWPPVK11WA") - ); - }); - - it("Ensure that users can't sent incomplete proofs", () => { - const deployer = accounts.get("deployer")!; - const block = chain.mineBlock([ - Tx.callPublicFn( - "send-to-first-input", - "send-to-first-input", - [ - Cl.uint(1), - Cl.buffer(hexToBytes("00")), - Cl.tuple({ - version: Cl.buffer(hexToBytes("00")), - parent: Cl.buffer(hexToBytes("00")), - "merkle-root": Cl.buffer(hexToBytes("00")), - timestamp: Cl.buffer(hexToBytes("00")), - nbits: Cl.buffer(hexToBytes("00")), - nonce: Cl.buffer(hexToBytes("00")), - }), - Cl.tuple({ - "tx-index": Cl.uint(1), - hashes: Cl.list([]), - "tree-depth": Cl.uint(1), - }), - ], - deployer - ), - ]); - - expect(block[0].result).toBeErr(Cl.uint(1)); - }); -}); From 1c49308a08b7286603ed7dcd5192fe247bf6a73f Mon Sep 17 00:00:00 2001 From: friedger Date: Wed, 22 Nov 2023 22:16:16 +0100 Subject: [PATCH 2/4] chore: add tests for invalid wproof --- .../tests/clarity-bitcoin_segwit_test.clar | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/contracts/tests/clarity-bitcoin_segwit_test.clar b/contracts/tests/clarity-bitcoin_segwit_test.clar index df3eec2..5872a19 100644 --- a/contracts/tests/clarity-bitcoin_segwit_test.clar +++ b/contracts/tests/clarity-bitcoin_segwit_test.clar @@ -178,9 +178,45 @@ ) ) -;; @name OP_RETURN is too large. Fails to parse segwit transaction + +;; @name verify segwit transaction where the incorrect wproof with zeros only ;; arbitrary segwit transaction (define-public (test-was-wtx-mined-internal-5) + (let ( + (burnchain-block-height u2431567) + ;; txid: 2fd0308a0bca2f4ea40fe93a19be976a40b2d5e0df08df1dd991b4df31a563fc + (raw-tx 0x020000000001017d406bb6466e0da97778f55ece77ab6becc415c930dbe618e81e8dae53ba914400000000171600143e8d4581104393d916566886ae01e26be8f8975afeffffff0276410300000000001600143d5f37543d2916547fe9b1242322b22f6e46d6929c9291d8000000001600145b3298860caeb3302f09e58b7cc3cf58d0a6740402463043021f53943d5951726d73a952473b49aa44dde64446f106749806d6478b0bdc053102200863a4f25914f3032afa8549f329962e8ecc7c575a1aefc51102a566ff5ece70012103742fe8a7244ab8384b3d533eb19138e2758c2b7a2e351c9ec2b8912cf4e046c24e1a2500) + ;; block id: 00000000096ae97d41a543592c3680477444acdc86c877aeb4832744691cb94b + (raw-block-header 0x000000208af1a70ab2ed062c7ac53eb56b053498db50f0d9c41f0dc8a5efcb1b000000007b64b9e16eb97b1fb32977aa00e2cb7418856b1e794e232be4f3b4b0512cb31256845064ffff001dc3cdbab0) + (witness-merkle-root 0xb2ea7fb39beae6b5a225c85cb7087561909e1d17bba74de3592a3c1da3944983) + (witness-reserved-data 0x0000000000000000000000000000000000000000000000000000000000000000) + ;; txid: 84f8a86015b0a95763bb16128ff301a60f668356548405178953ab1e4cbff36e + (raw-coinbase-tx 0x01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff32034f1a2500045684506404c862b40c0c11c14a6400000000000000000a636b706f6f6c0e2f6d696e65642062792072736b2fffffffff0317ea2b00000000001976a914ec2f9ffaba0d68ea6bd7c25cedfe2ae710938e6088ac0000000000000000266a24aa21a9ede2ee16ef0a8c0fd6ccb8c78f297199f0f143629121801c46b1e1487c0123cb4b00000000000000002a6a52534b424c4f434b3acb9b4841f625100fb87055003991835f3183bff7668865e93e1f1118003a492d00000000) + (parsed-block-header (contract-call? .clarity-bitcoin parse-block-header raw-block-header)) + (parsed-tx (contract-call? .clarity-bitcoin parse-wtx raw-tx false)) + ) + + (let ((result (contract-call? .clarity-bitcoin was-segwit-tx-mined-compact + burnchain-block-height + raw-tx + raw-block-header + u1 + u7 + (list 0x0000000000000000000000000000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000000000000000000000000000) + witness-merkle-root + witness-reserved-data + raw-coinbase-tx + (list 0xfc63a531dfb491d91ddf08dfe0d5b2406a97be193ae90fa44e2fca0b8a30d02f 0x33274cc92f8b980272688e01114cc2944fb661d1aa3a658c7d29675a46a4d5ad 0x1172bf0943aad7bc580aaab5f5d356b1c172f11c297ccf0077515309906352f2 0x2af4fd00ac79b65c0c508fbf44c22d1cf5acb084770079a94bd72ac816cfceb8 0xed4ca325a1800f0dfb2ab9d63761ecb358c014424e684c820d0d87ace45474a1 0xd3292e0e550420e500f29663dfc8ef632dbcb119c8a1ddf49aa3d32ecad83084 0x6369b65eea600edbd69b56386be9269f9662ca3f384a0ca21922ac03d2936102) + ))) + (asserts! (is-eq result (err ERR-WITNESS-TX-NOT-IN-COMMITMENT)) (err result)) + (ok true) + ) + ) +) + +;; @name OP_RETURN is too large. Fails to parse segwit transaction +;; arbitrary segwit transaction +(define-public (test-was-wtx-mined-internal-6) (let ( (burnchain-block-height u2430921) ;; txid: c770364da721e34eeb1a67f09c986fa5e4f13f9819df727e604691f42f5340a1 @@ -215,7 +251,7 @@ ;; @name verify segwit transaction where OP_RETURN is in output[1] ;; arbitrary segwit transaction -(define-public (test-was-wtx-mined-internal-6) +(define-public (test-was-wtx-mined-internal-7) (let ( (burnchain-block-height u2431087) ;; txid: 3b3a7a31c949048fabf759e670a55ffd5b9472a12e748b684db5d264b6852084 From f6854333d6c924362f466db12599b54ac25d9b00 Mon Sep 17 00:00:00 2001 From: friedger Date: Tue, 28 Nov 2023 12:45:11 +0100 Subject: [PATCH 3/4] feat: add function annotations for unit tests --- tests/clar.test.ts | 151 ++++++++++++++++----- tests/utils/clarity-parser.ts | 240 ++++++++++++++++++++++++++++++++++ 2 files changed, 360 insertions(+), 31 deletions(-) create mode 100644 tests/utils/clarity-parser.ts diff --git a/tests/clar.test.ts b/tests/clar.test.ts index 52ca561..45d8f20 100644 --- a/tests/clar.test.ts +++ b/tests/clar.test.ts @@ -1,49 +1,138 @@ import { ParsedTransactionResult, tx } from "@hirosystems/clarinet-sdk"; import { Cl, ClarityType, cvToString } from "@stacks/transactions"; import { describe, expect, it } from "vitest"; +import { + FunctionAnnotations, + extractTestAnnotations, +} from "./utils/clarity-parser"; + +function isTestContract(contractName: string) { + return ( + contractName.substring(contractName.length - 5) === "_test" && + contractName.substring(contractName.length - 10) !== "_flow_test" + ); +} const accounts = simnet.getAccounts(); -simnet.getContractsInterfaces().forEach((contract, name) => { - if (!name.endsWith("_test")) { +simnet.getContractsInterfaces().forEach((contract, contractFQN) => { + if (!isTestContract(contractFQN)) { return; } - describe(name, () => { - const prepare = + + describe(contractFQN, () => { + const hasDefaultPrepareFunction = contract.functions.findIndex((f) => f.name === "prepare") >= 0; - let block: ParsedTransactionResult[]; - contract.functions.forEach((fn) => { - if (!fn.name.startsWith("test-")) { + contract.functions.forEach((functionCall) => { + const functionName = functionCall.name; + if (!functionName.startsWith("test-")) { return; } + const source = simnet.getContractSource(contractFQN)!; + const annotations: any = extractTestAnnotations(source); + const functionAnnotations: FunctionAnnotations = + annotations[functionName] || {}; + + const mineBlocksBefore = + parseInt(annotations["mine-blocks-before"] as string) || 0; - it(fn.name, () => { - if (prepare) { - block = simnet.mineBlock([ - tx.callPublicFn(name, "prepare", [], accounts.get("deployer")!), - tx.callPublicFn(name, fn.name, [], accounts.get("deployer")!), - ]); - expect(block[0].result).toBeOk(Cl.bool(true)); - - if (block[1].result.type === ClarityType.ResponseErr) { - console.log(cvToString(block[1].result)); - } expect( - block[1].result, - `${name}, ${fn.name}, ${cvToString(block[0].result)}` - ).toBeOk(Cl.bool(true)); + it(`${functionCall.name}${ + functionAnnotations.name ? `: ${functionAnnotations.name}` : "" + }`, () => { + if (hasDefaultPrepareFunction && !functionAnnotations.prepare) + functionAnnotations.prepare = "prepare"; + if (functionAnnotations["no-prepare"]) + delete functionAnnotations.prepare; + + const callerAddress = functionAnnotations.caller + ? annotations.caller[0] === "'" + ? `${(annotations.caller as string).substring(1)}` + : accounts.get(annotations.caller)! + : accounts.get("deployer")!; + + if (functionAnnotations.prepare) { + mineBlockWithPrepareAndTestFunctionCall( + contractFQN, + functionAnnotations.prepare as string, + mineBlocksBefore, + functionName, + callerAddress + ); } else { - block = simnet.mineBlock([ - tx.callPublicFn(name, fn.name, [], accounts.get("deployer")!), - ]); - if (block[0].result.type === ClarityType.ResponseErr) { - console.log(cvToString(block[0].result)); - } - expect( - block[0].result, - `${name}, ${fn.name}, ${cvToString(block[0].result)}` - ).toBeOk(Cl.bool(true)); + mineBlockWithTestFunctionCall( + contractFQN, + mineBlocksBefore, + functionName, + callerAddress + ); } }); }); }); }); +function mineBlockWithPrepareAndTestFunctionCall( + contractFQN: string, + prepareFunctionName: string, + mineBlocksBefore: number, + functionName: string, + callerAddress: string +) { + if (mineBlocksBefore > 0) { + let block = simnet.mineBlock([ + tx.callPublicFn( + contractFQN, + prepareFunctionName, + [], + accounts.get("deployer")! + ), + ]); + expectOkTrue(block, contractFQN, prepareFunctionName, 0); + simnet.mineEmptyBlocks(mineBlocksBefore - 1); + + block = simnet.mineBlock([ + tx.callPublicFn(contractFQN, functionName, [], callerAddress), + ]); + + expectOkTrue(block, contractFQN, functionName, 0); + } else { + let block = simnet.mineBlock([ + tx.callPublicFn( + contractFQN, + prepareFunctionName, + [], + accounts.get("deployer")! + ), + tx.callPublicFn(contractFQN, functionName, [], callerAddress), + ]); + expectOkTrue(block, contractFQN, prepareFunctionName, 0); + expectOkTrue(block, contractFQN, functionName, 1); + } +} + +function mineBlockWithTestFunctionCall( + contractFQN: string, + mineBlocksBefore: number, + functionName: string, + callerAddress: string +) { + simnet.mineEmptyBlocks(mineBlocksBefore); + const block = simnet.mineBlock([ + tx.callPublicFn(contractFQN, functionName, [], callerAddress), + ]); + expectOkTrue(block, contractFQN, functionName, 0); +} + +function expectOkTrue( + block: ParsedTransactionResult[], + contractFQN: string, + functionName: string, + index: number = 0 +) { + if (block[index].result.type === ClarityType.ResponseErr) { + console.log(cvToString(block[index].result)); + } + expect( + block[index].result, + `${contractFQN}, ${functionName}, ${cvToString(block[index].result)}` + ).toBeOk(Cl.bool(true)); +} diff --git a/tests/utils/clarity-parser.ts b/tests/utils/clarity-parser.ts new file mode 100644 index 0000000..5e3ef95 --- /dev/null +++ b/tests/utils/clarity-parser.ts @@ -0,0 +1,240 @@ +export type FunctionAnnotations = { [key: string]: string | boolean }; +export type FunctionBody = { + callAnnotations: FunctionAnnotations[]; + callInfo: CallInfo; +}[]; + +export type ContractCall = { + callAnnotations: FunctionAnnotations; + callInfo: CallInfo; +}; + +export type CallInfo = { + contractName: string; + functionName: string; + args: { type: string; value: string }[]; +}; + +const functionRegex = + /^([ \t]{0,};;[ \t]{0,}@[^()]+?)\n[ \t]{0,}\(define-public[\s]+\((.+?)[ \t|)]/gm; +const annotationsRegex = /^;;[ \t]{1,}@([a-z-]+)(?:$|[ \t]+?(.+?))$/; + +/** + * Parser function for normal unit tests. + * + * Takes the whole contract source and returns an object containing + * the function annotations for each function + * @param contractSource + * @returns + */ +export function extractTestAnnotations(contractSource: string) { + const functionAnnotations: any = {}; + const matches = contractSource.replace(/\r/g, "").matchAll(functionRegex); + for (const [, comments, functionName] of matches) { + functionAnnotations[functionName] = {}; + const lines = comments.split("\n"); + for (const line of lines) { + const [, prop, value] = line.match(annotationsRegex) || []; + if (prop) functionAnnotations[functionName][prop] = value ?? true; + } + } + return functionAnnotations; +} + +/** + * Parser function for flow unit tests. + * + * Flow unit tests can be used for tx calls are required where + * the tx-sender should be equal to the contract-caller. + * + * Takes the whole contract source and returns an object containing + * the function annotations and function bodies for each function. + * @param contractSource + * @returns + */ +export function extractTestAnnotationsAndCalls(contractSource: string) { + const functionAnnotations: any = {}; + const functionBodies: any = {}; + contractSource = contractSource.replace(/\r/g, ""); + const matches1 = contractSource.matchAll(functionRegex); + + let indexStart: number = -1; + let headerLength: number = 0; + let indexEnd: number = -1; + let lastFunctionName: string = ""; + let contractCalls: { + callAnnotations: FunctionAnnotations; + callInfo: CallInfo; + }[]; + for (const [functionHeader, comments, functionName] of matches1) { + if (functionName.substring(0, 5) !== "test-") continue; + functionAnnotations[functionName] = {}; + const lines = comments.split("\n"); + for (const line of lines) { + const [, prop, value] = line.match(annotationsRegex) || []; + if (prop) functionAnnotations[functionName][prop] = value ?? true; + } + if (indexStart < 0) { + indexStart = contractSource.indexOf(functionHeader); + headerLength = functionHeader.length; + lastFunctionName = functionName; + } else { + indexEnd = contractSource.indexOf(functionHeader); + const lastFunctionBody = contractSource.substring( + indexStart + headerLength, + indexEnd + ); + + // add contracts calls in functions body for last function + contractCalls = extractContractCalls(lastFunctionBody); + + functionBodies[lastFunctionName] = contractCalls; + indexStart = indexEnd; + headerLength = functionHeader.length; + lastFunctionName = functionName; + } + } + const lastFunctionBody = contractSource.substring(indexStart + headerLength); + contractCalls = extractContractCalls(lastFunctionBody); + functionBodies[lastFunctionName] = contractCalls; + + return [functionAnnotations, functionBodies]; +} + +const callRegex = + /\n*^([ \t]{0,};;[ \t]{0,}@[\s\S]+?)\n[ \t]{0,}(\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\))/gm; + +/** + * Takes a string and returns an array of objects containing + * the call annotations and call info within the function body. + * + * The function body should look like this + * (begin + * ... lines of code.. + * (ok true)) + * + * Only two lines of code are accepted: + * 1. (unwrap! (contract-call? .contract-name function-name args)) + * 2. (try! (function-name)) + * @param lastFunctionBody + * @returns + */ +export function extractContractCalls(lastFunctionBody: string) { + const calls = lastFunctionBody.matchAll(callRegex); + const contractCalls: ContractCall[] = []; + for (const [, comments, call] of calls) { + const callAnnotations: FunctionAnnotations = {}; + const lines = comments.split("\n"); + for (const line of lines) { + const [, prop, value] = line.trim().match(annotationsRegex) || []; + if (prop) callAnnotations[prop] = value ?? true; + } + // try to extract call info from (unwrap! (contract-call? ...)) + let callInfo = extractUnwrapInfo(call); + if (!callInfo) { + // try to extract call info from (try! (my-function)) + callInfo = extractCallInfo(call); + } + if (callInfo) { + contractCalls.push({ callAnnotations, callInfo }); + } else { + throw new Error(`Could not extract call info from ${call}`); + } + } + return contractCalls; +} + +// take a string containing function arguments and +// split them correctly into an array of argument strings +function splitArgs(argString: string): string[] { + const splitArgs: string[] = []; + let argStart = 0; + let brackets = 0; // curly brackets + let rbrackets = 0; // round brackets + + for (let i = 0; i < argString.length; i++) { + const char = argString[i]; + + if (char === "{") brackets++; + if (char === "}") brackets--; + if (char === "(") rbrackets++; + if (char === ")") rbrackets--; + + const atLastChar = i === argString.length - 1; + if ((char === " " && brackets === 0 && rbrackets === 0) || atLastChar) { + const newArg = argString.slice(argStart, i + (atLastChar ? 1 : 0)); + if (newArg.trim()) { + splitArgs.push(newArg.trim()); + } + argStart = i + 1; + } + } + + return splitArgs; +} + +function parseTuple(tupleString: string): string { + const tupleItems = tupleString + .slice(1, -1) + .split(",") + .map((item) => { + const [key, value] = item.split(":").map((s) => s.trim()); + const uintMatch = value.match(/u(\d+)/); + if (uintMatch) { + return `"${key}": types.uint(${uintMatch[1]})`; + } else { + return `${key}: "${value}"`; + } + }) + .join(", "); + + return `types.tuple({${tupleItems}})`; +} + +function extractUnwrapInfo(statement: string): CallInfo | null { + const match = statement.match( + /\(unwrap! \(contract-call\? \.(.+?) (.+?)(( .+?)*)\)/ + ); + if (!match) return null; + + const contractName = match[1]; + const functionName = match[2]; + const argStrings = splitArgs(match[3]); + + const args = argStrings.map((arg) => parseArg(arg)); + + return { + contractName, + functionName, + args, + }; +} + +function parseArg(arg: string): { type: string; value: string } { + if (arg.startsWith("'")) { + return { type: "principal", value: `types.principal("${arg.slice(1)}")` }; + } else if (arg.startsWith("u")) { + return { type: "uint", value: `types.uint(${arg.slice(1)})` }; + } else if (arg.startsWith("{")) { + return { type: "tuple", value: parseTuple(arg) }; + } else if (arg.startsWith("(some ")) { + return { + type: "some", + value: `types.some(${parseArg(arg.substring(6, arg.length)).value})`, + }; + } else if (arg === "none") { + return { type: "none", value: "types.none()" }; + } else { + return { type: "raw", value: `"${arg}"` }; + } +} + +function extractCallInfo(statement: string) { + const match = statement.match(/\(try! \((.+?)\)\)/); + if (!match) return null; + return { contractName: "", functionName: match[1], args: [] }; +} + +export function getContractName(contractId: string) { + return contractId.split(".")[1]; +} From d1b6c95c5a2c77f4b735d0f077f0bb6970b7626c Mon Sep 17 00:00:00 2001 From: friedger Date: Wed, 29 Nov 2023 14:54:15 +0100 Subject: [PATCH 4/4] chore: add comment about feature for v5 --- contracts/clarity-bitcoin.clar | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/clarity-bitcoin.clar b/contracts/clarity-bitcoin.clar index 85ecfee..ebf2b14 100644 --- a/contracts/clarity-bitcoin.clar +++ b/contracts/clarity-bitcoin.clar @@ -1,6 +1,8 @@ ;; @contract stateless contract to verify bitcoin transaction ;; @version 5 +;; version 5 adds support for txid generation and improves security + ;; Error codes (define-constant ERR-OUT-OF-BOUNDS u1) (define-constant ERR-TOO-MANY-TXINS u2)