trustwallet / wallet-core

Cross-platform, cross-blockchain wallet library.
https://developer.trustwallet.com/wallet-core
Apache License 2.0
2.77k stars 1.56k forks source link

Transaction fails to broadcast (Invalid Schnorr signature) when spending P2TR and P2WPKH. #3946

Closed paulo-bc closed 1 month ago

paulo-bc commented 1 month ago

Describe the bug

Building a transaction with two inputs (one P2TR utxo and one P2WPKH utxo), transaction successfully builds but error non-mandatory-script-verify-flag (Invalid Schnorr signature) happens when trying to broadcast.

To Reproduce Using 4.1.0 release, test_sign_with_p2tr_and_p2wpkh test passes, but the built transaction fails to broadcast. To prove that signing a tx with a single P2TR input works as expected, we wrote test_sign_with_p2tr_only. That test passes, and the resulting transaction was broadcasted successfully.

import WalletCore
import XCTest

final class Tests: XCTestCase {
    func test_sign_with_p2tr_and_p2wpkh() throws {
        // TestNet3 Data

        let p2trReceiveAddress0 = "tb1pcm7lhn5g6c4ueefe9kfvxgvv8xgaqrw9a80tvewn2gesdwjaeqfswarlgw"
        let p2trTxId = "9e85eb060fd9ac650fe0ae97a0f7940a8fdefe094d9f51335891727d70d380e7"
        let p2trValue: Int64 = 889
        let p2trIndex: UInt32 = 0
        let p2tr_pk_str = "ba04e058c795e864fc07199ba36178708b4374b8449db298b43e4d39ba78e86f"

        let p2wpkhReceiveAddress0 = "tb1qal5xl77a7d9vtaafda960lcxexn3uw9fqpdds5"
        let p2wpkhTxId = "902f0828ffcd8f35fe3f4903bd0a871c429cb5685b7a74cf262287f590ef40d2"
        let p2wpkhValue: Int64 = 1_100
        let p2wpkhIndex: UInt32 = 2
        let p2wpkh_pk_str = "1ec2a0f68e84edf0a6811dc2d35cf366bb495cf2475dbdf7a0545aab54864b22"

        // P2TR UTXO

        let p2trPrivateKeyData = Data(hexString: p2tr_pk_str)!
        let p2trPrivateKey = PrivateKey(data: p2trPrivateKeyData)!
        let p2trPublicKey = p2trPrivateKey.getPublicKeySecp256k1(compressed: false)
        let p2trUtxo = BitcoinV2Input.with {
            $0.outPoint = BitcoinV2OutPoint.with {
                $0.hash = Data.reverse(hexString: p2trTxId)
                $0.vout = p2trIndex
            }
            $0.value = p2trValue
            $0.sighashType = BitcoinSigHashType.all.rawValue
            $0.scriptBuilder = .with { $0.p2TrKeyPath = p2trPublicKey.data }
        }

        // P2WPKH UTXO

        let p2wpkhPrivateKeyData = Data(hexString: p2wpkh_pk_str)!
        let p2wpkhPrivateKey = PrivateKey(data: p2wpkhPrivateKeyData)!
        let p2wpkhPublicKey = p2wpkhPrivateKey.getPublicKeySecp256k1(compressed: false)
        let p2wpkhUtxo = BitcoinV2Input.with {
            $0.outPoint = BitcoinV2OutPoint.with {
                $0.hash = Data.reverse(hexString: p2wpkhTxId)
                $0.vout = p2wpkhIndex
            }
            $0.value = p2wpkhValue
            $0.sighashType = BitcoinSigHashType.all.rawValue
            $0.scriptBuilder = .with { $0.p2Wpkh = .with { $0.pubkey = p2wpkhPublicKey.data } }
        }

        let out0 = BitcoinV2Output.with {
            $0.value = p2trValue
            $0.toAddress = p2trReceiveAddress0
        }

        let changeOutput = BitcoinV2Output.with {
            $0.value = 1_100 - 210
            $0.toAddress = p2wpkhReceiveAddress0
        }

        let signingInput = BitcoinV2SigningInput.with {
            $0.version = .v2
            $0.privateKeys = [p2trPrivateKeyData, p2wpkhPrivateKeyData]
            $0.inputs = [p2trUtxo, p2wpkhUtxo]
            $0.outputs = [out0, changeOutput]
            $0.inputSelector = .useAll
            $0.chainInfo = BitcoinV2ChainInfo.with {
                $0.p2PkhPrefix = 0
                $0.p2ShPrefix = 5
            }
            $0.dangerousUseFixedSchnorrRng = false
            $0.fixedDustThreshold = 546
        }

        let legacySigningInput = BitcoinSigningInput.with { $0.signingV2 = signingInput }
        let output: BitcoinSigningOutput = AnySigner.sign(input: legacySigningInput, coin: .bitcoin)
        XCTAssertEqual(output.error, .ok)

        let outputV2 = output.signingResultV2
        print(outputV2.encoded.hexString)
        XCTAssertEqual(outputV2.error, .ok)
        /*
         encoded:
         02000000000101c4320fa80886ec307e36be4f866e2728c6a85d6e42f87503afea37108ebe91bd0000000000ffffffff011603000000000000225120c6fdfbce88d62bcce5392d92c3218c3991d00dc5e9deb665d3523306ba5dc813014081b692f9224e8d2e934bc497360957efb7c3b6bf13514a0f1acc666a4d101ba16f3f35cedc8b43a5ab45fa362d459b53b5d182695784e10090203bd5d34af55800000000
         */
    }

    func test_sign_with_p2tr_only() throws {
        // TestNet3 Data

        let p2trReceiveAddress0 = "tb1pcm7lhn5g6c4ueefe9kfvxgvv8xgaqrw9a80tvewn2gesdwjaeqfswarlgw"
        let p2trTxId = "bd91be8e1037eaaf0375f8426e5da8c628276e864fbe367e30ec8608a80f32c4"
        let p2trValue: Int64 = 1_000
        let p2trIndex: UInt32 = 0
        let p2tr_pk_str = "ba04e058c795e864fc07199ba36178708b4374b8449db298b43e4d39ba78e86f"

        // P2TR UTXO

        let p2trPrivateKeyData = Data(hexString: p2tr_pk_str)!
        let p2trPrivateKey = PrivateKey(data: p2trPrivateKeyData)!
        let p2trPublicKey = p2trPrivateKey.getPublicKeySecp256k1(compressed: false)
        let p2trUtxo = BitcoinV2Input.with {
            $0.outPoint = BitcoinV2OutPoint.with {
                $0.hash = Data.reverse(hexString: p2trTxId)
                $0.vout = p2trIndex
            }
            $0.value = p2trValue
            $0.sighashType = BitcoinSigHashType.all.rawValue
            $0.scriptBuilder = .with { $0.p2TrKeyPath = p2trPublicKey.data }
        }

        let out0 = BitcoinV2Output.with {
            $0.value = 1_000 - 210
            $0.toAddress = p2trReceiveAddress0
        }

        let signingInput = BitcoinV2SigningInput.with {
            $0.version = .v2
            $0.privateKeys = [p2trPrivateKeyData]
            $0.inputs = [p2trUtxo]
            $0.outputs = [out0]
            $0.inputSelector = .useAll
            $0.chainInfo = BitcoinV2ChainInfo.with {
                $0.p2PkhPrefix = 0
                $0.p2ShPrefix = 5
            }
            $0.dangerousUseFixedSchnorrRng = false
            $0.fixedDustThreshold = 546
        }

        let legacySigningInput = BitcoinSigningInput.with { $0.signingV2 = signingInput }
        let output: BitcoinSigningOutput = AnySigner.sign(input: legacySigningInput, coin: .bitcoin)
        XCTAssertEqual(output.error, .ok)

        let outputV2 = output.signingResultV2
        print(outputV2.encoded.hexString)
        XCTAssertEqual(outputV2.error, .ok)
        /*
         encoded:
         02000000000101c4320fa80886ec307e36be4f866e2728c6a85d6e42f87503afea37108ebe91bd0000000000ffffffff017903000000000000225120c6fdfbce88d62bcce5392d92c3218c3991d00dc5e9deb665d3523306ba5dc8130140685522251f628c5ea05e83380d5d64611649e294bbeb692587cf5cd8b573ab78a49f492fa7a3e1bf74817bafaa8ba39bb1d41eca08bae4314b9e28a2ac300cbe00000000
         hash:
         9e85eb060fd9ac650fe0ae97a0f7940a8fdefe094d9f51335891727d70d380e7
         */
    }
}

Expected behavior Transaction is broadcasted successfully.

Additional context This unit test runs on TestNet3, but the same issue happens on mainnet. Broadcaster: https://mempool.space/testnet/tx/push Note that if two P2TR inputs are used, transaction can be broadcasted successfully.

satoshiotomakan commented 1 month ago

Hi @paulo-bc, thank you for opening the issue. Could you please advise do you tweak the p2tr_pk_str private key? In WalletCore, we take original ECDSA/Schnorr private keys, and tweak them ourselves.

paulo-bc commented 1 month ago

Hi @satoshiotomakan, I have not tweaked the p2tr_pk_str private key, it is the standard one for the m/86'/1'/0'/0/0 derivation. Do you mean I have to tweak it myself and then use the result in BitcoinV2SigningInput.with, or that BitcoinV2SigningInput.with will already tweak it?

Edit: Seems like it is the latter, as this seems to do the tweaking. Right?

Edit 2: I have updated the original post with another test case that successfully signs a tx with a single P2TR Input/Output.

satoshiotomakan commented 1 month ago

Hi @paulo-bc, thank you for giving the context. No, the private key has to be not tweaked, so your code looks good. I'll try to reproduce the same problem today and will be right back

satoshiotomakan commented 1 month ago

Hi @paulo-bc, I fixed the issue at the PR https://github.com/trustwallet/wallet-core/pull/3979