mcdallas / cryptotools

MIT License
207 stars 80 forks source link

ScriptValidationError: Unknown script type #31

Open massmux opened 2 years ago

massmux commented 2 years ago

in this code

def spend(wif,bitcoin_address,recipient_address):
    private = PrivateKey.from_wif(wif)
    address = Address(bitcoin_address)
    # balance is in bitcoin
    bal=address.balance()
    net_amount=bal-0.0002
    print(net_amount)
    send_to = {recipient_address: net_amount}
    estimate_fee = estimatefee(speed="average")
    tx = address.send(to=send_to, fee=0.0002, private=private)
    print(tx)
    result=tx.broadcast()
    return result

i run with spend("wifxxx","bc1source","bc1qdest")

i get this error

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/voucherbot/wallet.py", line 152, in spend
    tx = address.send(to=send_to, fee=0.0002, private=private)
  File "/usr/local/lib/python3.8/site-packages/cryptotools/BTC/address.py", line 159, in send
    inp.sign(private)
  File "/usr/local/lib/python3.8/site-packages/cryptotools/BTC/transaction.py", line 139, in sign
    output_type = self.ref().type()
  File "/usr/local/lib/python3.8/site-packages/cryptotools/BTC/transaction.py", line 271, in type
    return get_type(self.script)
  File "/usr/local/lib/python3.8/site-packages/cryptotools/BTC/script.py", line 145, in get_type
    raise ScriptValidationError(f"Unknown script type: {bytes_to_hex(script)}")
cryptotools.BTC.error.ScriptValidationError: Unknown script type: 

but it seems that bech32 (bc1q) is supported by the lib. where am i wrong?

yurisich commented 2 years ago

It may be due to the input type of the wif argument. If it was created from some other software, it could be using a missing compression hint flag that determines if the wif starts with a K/L, or a 5.

This is from bip-178. Although it's not formally accepted yet, it is referenced in the code base of this project.

image

https://github.com/mcdallas/cryptotools/blob/dbdced863bd2622f4cb41595df1ecad399760d78/cryptotools/ECDSA/secp256k1.py#L62-L66

The flag b'\x01' is the only option for using a compressed public key with a wif. If it's missing, then that private key will eventually reference its uncompressed public key when it's creating scripts to sign. You should get your address from the private key yourself. Avoid using an address manually as your second argument, it makes this edge case harder to spot.

def spend(private_key_bytes, recipient):
    private = PrivateKey.from_bytes(private_key_bytes)
    address = private.to_public().to_address("P2WPKH")
    # ... the rest is the same

If you only have wifs from some other system, then pull the private key bytes out of it, and ignore the compression flags entirely.

from cryptotools.HD.bip32 import base53

def spend(wif, recipient):
    private = PrivateKey.from_bytes(base53.decode(wif)[1:33])
    address = private.to_public().to_address("P2WPKH")
    # ...
mcdallas commented 2 years ago

hey @massmux I think this might be related with a recent change to pull utxo data from blockstream.info instead of blockchain.info and the utxo endpoint does not contain output scripts. Could you try setting the environmental variable CRYPTOTOOLS_BACKEND to blockchaininfo to confirm if that's the issue ?

massmux commented 2 years ago

@mcdallas my wif controls a 3xx address in which funds are. now i am using same code to spend to a bc1 address. I set the environment var as you suggested, the situation actually changed, but i got a different error now the error is the following:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/massmux/.local/lib/python3.8/site-packages/cryptotools/BTC/address.py", line 159, in send
    inp.sign(private)
  File "/home/massmux/.local/lib/python3.8/site-packages/cryptotools/BTC/transaction.py", line 169, in sign
    raise SigningError('Cannot sign P2SH or P2WSH outputs.')
cryptotools.BTC.error.SigningError: Cannot sign P2SH or P2WSH outputs.

can i spend only to legacy?

massmux commented 2 years ago

@yurisich my wif is created externally but it seems ok, it begins with K

mcdallas commented 2 years ago

@massmux You can send to segwit but the problem is where you are spending from. P2SH addresses (starting with 3) have no direct correlation to a single private key, for example they can be 2-of-3 multisig addresses that require multiple keys to sign or they can require no signature at all. As such there is no standard method on how to spend from them.

In the special case that the P2SH address is a segwit-wrapped address (P2SH-P2WPKH) then there is a 1-to-1 relation with a key and perhaps your address is such but I have not yet implemented this functionality. I'll look into adding it.

Finally I would caution against using the send/spend mechanisms in this library with mainnet addresses containing any significant amount because as I mentioned in the disclaimer this library is mostly for educational purposes and not as well battle tested as other wallets like for example electrum.

massmux commented 2 years ago

@mcdallas ok i understand. i try then to spend from bc1 to bc1. this should work, correct?

about last point, dont worry i understand. your library so far is very good, clear and well done

massmux commented 2 years ago

@mcdallas

now i am in the condition suggested. from bc1 towards bc1 address. wif starting with L. But i get again an error, different, but another one. What is it? where am i wrong?

Traceback (most recent call last): File "", line 1, in File "/home/massmux/.local/lib/python3.8/site-packages/cryptotools/BTC/address.py", line 186, in send tx = addr.send(to=to, fee=fee, private=private) File "/home/massmux/.local/lib/python3.8/site-packages/cryptotools/BTC/address.py", line 159, in send inp.sign(private) File "/home/massmux/.local/lib/python3.8/site-packages/cryptotools/BTC/transaction.py", line 140, in sign if self.is_signed(): File "/home/massmux/.local/lib/python3.8/site-packages/cryptotools/BTC/transaction.py", line 182, in is_signed return is_signature(self.witness[0][:-1]) # and is_pubkey(self.witness[-1]) TypeError: 'NoneType' object is not subscriptable

yurisich commented 2 years ago

@massmux I have been spending some time attempting to implement a valid utxo spend that includes witness data. In my case, it was a P2WSH-P2SH, like the one found in the readme of the project.

There's an issue where the sighash uses a tx version number from the default value of b'\x00\x00\x00\x01', when it needs to be set to b'\x00\x00\x00\x02'. You also need to set your signature separately when creating the transaction.

private = cryptotools.PrivateKey.from_hex("...")
public = private.to_public()
script = cryptotools.push(public.encode(compressed=True)) + cryptotools.OP.CHECKSIG.byte
p2sh_address = cryptotools.script_to_address(script, "P2WSH-P2SH")

p2sh_txid = "..."
p2sh_tx = cryptotools.Transaction.get(p2sh_txid)
p2sh_output = p2sh_tx.outputs[0]

send_to = Address("...")
send_amount = 50_000
fee = p2sh_output.value - send_amount
input_tx = p2sh_output.spend()
output_tx = send_to._receive(send_amount)

tx = cryptotools.Transaction(
    inputs=[input_tx],
    outputs=[output_tx]
)

p2sh_bytes = cryptotools.BTC.base58.decode(p2sh_address)
payload = p2sh_bytes[1:-4]
redeem_script = cryptotools.push(cryptotools.sha256(script))

# just to demonstrate....
witness_byte = b'\x00'
assert cryptotools.BTC.script.witness_byte(witver=0) == witness_byte
assert payload == cryptotools.hash160(
    witness_byte + redeem_script
)

Ok, that's the setup. Here's where things can get tricky if you're looking to build your own transaction for signing:

assert input_tx.is_nested() == False
assert input_tx.segwit == False

input_tx.script = (
    # push the witness byte b'\x00' and the 0x21 byte redeem_script
    b'\x22' + (b'\x00' + redeem_script)
)

assert input_tx.segwit == False

# the tx is now "nested", though
assert input_tx.is_nested() == cryptotools.TX.P2WSH

The witness data should be populated with an empty signature at first:

empty_sig = b''
input_tx.witness = [empty_sig, script]

assert input_tx.segwit == True

And then, creating a signature will fail to verify:

utxo_input_to_sign = 0
sig = private.sign_hash(tx.sighash(i=utxo_input_to_sign))

# for clarity's sake
sig_verification_strictness = b'\x01'
assert sig_verification_strictness == cryptotools.SIGHASH.ALL.byte
tx.inputs[0].witness[0] = sig.encode() + b'\x01'

# this isn't working...
assert tx.verify() == False

Fixing the verify step:

assert tx.version == 1
assert tx._version == b'\x00\x00\x00\x01'

tx._version = b'\x00\x00\x00\x02'
tx.version = 2
assert tx.version == 2
assert tx._version == b'\x00\x00\x00\x02'
assert tx.verify() == True