Source Code

;; multisig-verify.clar
;; A contract for verifying both P2SH and P2WSH Bitcoin multisig addresses
;; by calling the existing sbtc-bootstrap-signers contract

;; Error codes
(define-constant ERR-INVALID-PUBKEY u1)
(define-constant ERR-INVALID-SIG-THRESHOLD u2)
(define-constant ERR-TOO-MANY-PUBKEYS u3)
(define-constant ERR-TOO-FEW-PUBKEYS u4)
(define-constant ERR-INVALID-SCRIPT u5)

;; Bitcoin script constants
(define-constant OP-0 0x00)
(define-constant OP-HASH160 0xa9)
(define-constant OP-EQUAL 0x87)
(define-constant PUSH-20 0x14)  ;; Push 20 bytes onto the stack
(define-constant PUSH-32 0x20)  ;; Push 32 bytes onto the stack

;; Create P2SH scriptPubKey from a script hash
;; Format: OP_HASH160 <20-byte-hash> OP_EQUAL
(define-read-only (script-hash-to-p2sh (script-hash (buff 20)))
  (concat 
    (concat 
      (concat 
        OP-HASH160 
        PUSH-20
      )
      script-hash
    )
    OP-EQUAL
  )
)

;; Create P2WSH scriptPubKey from a witness program
;; Format: 0 <32-byte-hash>
(define-read-only (witness-program-to-p2wsh (witness-program (buff 32)))
  (concat 
    OP-0
    (concat 
      PUSH-32
      witness-program
    )
  )
)

;; Verify a Legacy P2SH multisig address (starting with '3')
(define-read-only (verify-p2sh-multisig
    (pubkeys (list 128 (buff 33)))
    (m uint)
    (script-pub-key (buff 23))
  )
  (let (
      ;; Get the script hash using the sBTC bootstrap signers contract
      (script-hash (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-bootstrap-signers pubkeys-to-hash pubkeys m))
      ;; Create the expected P2SH script
      (expected-script-pub-key (script-hash-to-p2sh script-hash))
    )
    ;; Compare with the provided script
    (ok (is-eq script-pub-key expected-script-pub-key))
  )
)

;; Verify a SegWit P2WSH multisig address (starting with 'bc1q')
(define-read-only (verify-p2wsh-multisig
    (pubkeys (list 128 (buff 33)))
    (m uint)
    (script-pub-key (buff 34))  ;; P2WSH is 34 bytes: 0x0020 + 32-byte SHA256 hash
  )
  (let (
      ;; First get the redeem script
      (redeem-script (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-bootstrap-signers pubkeys-to-spend-script pubkeys m))
      ;; For P2WSH, hash the redeem script with SHA256 (not HASH160)
      (witness-program (sha256 redeem-script))
      ;; Create the expected P2WSH script
      (expected-script-pub-key (witness-program-to-p2wsh witness-program))
    )
    ;; Compare with the provided script
    (ok (is-eq script-pub-key expected-script-pub-key))
  )
)

;; Main verification function that handles both P2SH and P2WSH addresses
(define-public (verify-multisig-address
    (pubkeys (list 128 (buff 33)))
    (m uint)
    (is-segwit bool)  
    (script-pub-key (optional (buff 34)))  ;; Optional buffer for flexibility
  )
  (if is-segwit
      ;; For SegWit P2WSH addresses (34 bytes)
      (verify-p2wsh-multisig pubkeys m 
        (default-to 
          (witness-program-to-p2wsh (sha256 (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-bootstrap-signers pubkeys-to-spend-script pubkeys m)))
          script-pub-key
        )
      )
      ;; For Legacy P2SH addresses (23 bytes)
      (verify-p2sh-multisig pubkeys m 
        (unwrap-panic (as-max-len? 
          (default-to 
            (script-hash-to-p2sh (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-bootstrap-signers pubkeys-to-hash pubkeys m))
            script-pub-key
          )
          u23
        ))
      )
  )
)

;; Helper function to detect address type from scriptPubKey length
(define-read-only (is-segwit-script (script-pub-key (buff 34)))
  ;; P2WSH is 34 bytes, P2SH is 23 bytes
  (is-eq (len script-pub-key) u34)
)

;; Auto-detect the address type and verify if it matches the expected pubkeys
(define-public (generate-and-verify-p2sh
    (pubkeys (list 128 (buff 33)))
    (m uint)
  )
  (ok (script-hash-to-p2sh 
    (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-bootstrap-signers pubkeys-to-hash pubkeys m)
  ))
)

;; Generate and return a P2WSH scriptPubKey for given pubkeys
(define-public (generate-and-verify-p2wsh
    (pubkeys (list 128 (buff 33)))
    (m uint)
  )
  (ok (witness-program-to-p2wsh 
    (sha256 (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-bootstrap-signers pubkeys-to-spend-script pubkeys m))
  ))
)


;; (define-read-only (btc-to-stx (input (string-ascii 60)))
;;   (let (
;;     ;; Decode base58 string to numbers
;;     (b58-numbers (map unwrap-uint (filter is-some-uint (map b58-to-uint input))))
;;     ;; Validate all characters are valid base58
;;     (t1 (asserts! (>= (len b58-numbers) (len input)) ERR_INVALID_CHAR))
;;     ;; Count leading '1's (zeros in base58)
;;     (leading-ones-count (default-to (len input) (index-of? (map is-zero b58-numbers) false)))
;;     ;; Convert to bytes
;;     (decoded (concat (fold decode-outer to-decode LST) leading-zeros))
;;     (decoded-hex (fold to-hex-rev decoded 0x))
;;     ;; Verify checksum
;;     (actual-checksum (unwrap-panic (slice? (sha256 (sha256 (unwrap-panic (slice? decoded-hex u0 (- decoded-hex-len u4))))) u0 u4)))
;;     (expected-checksum (unwrap-panic (slice? decoded-hex (- decoded-hex-len u4) decoded-hex-len)))
;;     (t3 (asserts! (is-eq actual-checksum expected-checksum) ERR_BAD_CHECKSUM))
;;     ;; Extract version and construct principal
;;     (version (unwrap-panic (element-at? STX_VER (unwrap! (index-of? BTC_VER (unwrap-panic (element-at? decoded-hex u0))) ERR_INVALID_VERSION))))
;;     )
;;     (principal-construct? version (unwrap-panic (as-max-len? (unwrap-panic (slice? decoded-hex u1 (- decoded-hex-len u4))) u20)))
;;   )
;; )


(define-public (verify-user-in-multisig
    (multisig-script-pub-key (buff 34))
    (pubkeys (list 128 (buff 33)))
    (m uint)
  )
  (let (
      ;; Get the pubkey hash directly from tx-sender (user's Stacks address)
      ;; (user-pubkey-hash (get hash-bytes (unwrap-panic (principal-destruct? tx-sender))))
      ;; perhaps use a helper var instead of defining it in checkpubkey
      
      ;; Check if any pubkey in the multisig list hashes to the user's pubkey hash
      (user-is-signer (fold check-pubkey pubkeys false))
      
      ;; Verify the multisig itself is valid
      (multisig-valid (unwrap-panic (verify-multisig-address pubkeys m true (some multisig-script-pub-key))))
    )
    ;; Only return true if both conditions are met
    (ok (and user-is-signer multisig-valid))
  )
)

;; Closure function that checks if the user's pubkey hash matches any pubkey in the list
(define-private (check-pubkey 
    (pubkey (buff 33)) 
    (found bool)
  )
  (let
    (
      ;; Get the pubkey hash directly from tx-sender (user's Stacks address)
      (user-pubkey-hash (get hash-bytes (unwrap-panic (principal-destruct? tx-sender))))
    )
    (if found
        true  ;; Already found a match, short-circuit
        (is-eq (hash160 pubkey) user-pubkey-hash)  ;; Check current pubkey
    )
  )
)

Functions (11)

FunctionAccessArgs
script-hash-to-p2shread-onlyscript-hash: (buff 20
witness-program-to-p2wshread-onlywitness-program: (buff 32
verify-p2sh-multisigread-onlypubkeys: (list 128 (buff 33
verify-p2wsh-multisigread-onlypubkeys: (list 128 (buff 33
verify-multisig-addresspublicpubkeys: (list 128 (buff 33
is-segwit-scriptread-onlyscript-pub-key: (buff 34
generate-and-verify-p2shpublicpubkeys: (list 128 (buff 33
generate-and-verify-p2wshpublicpubkeys: (list 128 (buff 33
btc-to-stxread-onlyinput: (string-ascii 60
verify-user-in-multisigpublicmultisig-script-pub-key: (buff 34
check-pubkeyprivatepubkey: (buff 33