Skip to content

Conversation

@dgpv
Copy link

@dgpv dgpv commented Nov 14, 2021

This adds a bunch of new APIs and it is really important for me to hear opinions on the convenience/ergonomics of these APIs. Please don't hesitate to comment if you think something might be made more convenient for the developer.

Of course the correctness/safety is the most important, but convenience is close to that in importance, as it can reduce the chance for mistakes

return True

def create_tap_tweak(self: T_XOnlyPubKey, *,
merkle_root: bytes = b'',
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requiring merkle_root to always be specified as kwarg may feel a bit excessive for the method with just one argument, but I think this is OK since a lot of other functions/methods use merkle_root kwarg, when there are other args, and this way it feels more uniform

get_script_with_control_block() method to be available
'leaf_version' can be supplied for all the script in the tree to
have this version
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to not add leaf_version field to CScript, at least for now. It is not clear to me if the single different leaf version for one script among other scripts will be a normal occurence, even when we'll have many leaf versions. It seems more possible that the whole trees will consist of scripts with particular leaf version. And you can always have a 'tree' of just one script to designate a leaf version for just that script.

return ints


class VarBytesSerializer(Serializer[bytes]):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a duplicate of BytesSerializer

class XOnlyPubKey(bytes):
"""An encapsulated X-Only public key"""

__fullyvalid: bool
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realise that this is not specific to the new taproot/BIP340 code (you already had it in CPubKey, but what is the significance of storing this __fullyvalid bool? Is it somehow useful to have a pubkey object that is from an invalid encoding?

Copy link
Author

@dgpv dgpv Nov 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The Core makes that distinction for some reason
  • It is used in tests to supply invalid pubkeys and check that the code fail with invalid pubkeys

This distinction was present in python-bitcoinlib.

My guess is that Core does this for speed so you can create CPubKey instance from the chunk of bytes quickly and check for validity later.

Maybe it makes sense to throw ValueError on invalid encoding by default, and introduce accept_invalid kwarg for the tests ? This would be breakage of the API, but I doubt this breakage will affect much real-world code except the test code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address classes already have accept_invalid kwarg for from_pubkey() method, but I guess one could accidentally use invalid pubkeys in other places (like, constructing scripts) where they could miss checking the is_fullyvalid().

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTOH, if we introduce that change, and then new code that uses bitcointx starts dropping is_fullyvalid() checks, but afterwards somehow is used with the old version of the lib, then it could pass invalid pubkeys where it should not. This is somewhat alleviated by the fact that the code may list the version requirement for the library, of course.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only two reasonable paths: leave as is, or completely remove, anything else would have the kind of problems you describe in that last comment. Even if I think the 'remove' option is preferable, the taproot patch is not really the right time to make such a big change.

Address classes already have accept_invalid kwarg for from_pubkey() method

It's interesting that this option exists (even though of course default False). I suppose it might be related to burn addresses? (your performance hypothesis makes sense too, i guess). Generally I'd lean towards making no public API support for such things, since the risk of using them incorrectly outweighs the very small amount of usage (burn addresses are very rarely used for anything).

But no action item here, imo: this is orthogonal to the taproot patch.

Copy link
Author

@dgpv dgpv Nov 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it might be related to burn addresses?

Its main use (for addreses) is in tests (tests.test_wallet.test_from_invalid_pubkeys)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and that test_from_invalid_pubkeys was present in python-bitcoinlib since long time ago. So maybe this is really just an option to allow to create burn addresses.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But no action item here, imo: this is orthogonal to the taproot patch.

I created a separate issue: #61

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, since XOnlyPubKey is a completely separate class from CPubKey, I think we can make it raise ValueError on invalid data by default, without any breakage. I think we should keep is_fullyvalid() in XOnlyPubKey though, to make it compatible with CPubKey.

elif len(keydata) == 33:
ensure_isinstance(keydata, CPubKey,
'x-only pubkey data of 33 bytes')
keydata = keydata[1:33]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I missed something obvious, but: why would we create an XOnlyPubkey from just a removal of the parity byte of a CPubKey? I get it that this might be fine in some contexts, but isn't there the danger that the caller is dealing with an existing private/pubkey pair that is not actually valid for XOnly, because it has the wrong parity?

Copy link
Author

@dgpv dgpv Nov 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From BIP340:

the standard 33-byte compressed public key format consists of a byte indicating the oddness of the Y coordinate, plus the full X coordinate.

so we just drop the 'oddness' byte, and are left with 'full X coordinate'.

The code from Core:

    /** Construct an x-only pubkey from a normal pubkey. */
    explicit XOnlyPubKey(const CPubKey& pubkey) : XOnlyPubKey(Span<const unsigned char>(pubkey.begin() + 1, pubkey.begin() + 33)) {}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is the scenario: I have a pre-existing key and for whatever reason I want to make "the taproot address for the same key", I think this is actually wrong-headed, but if you do it, you have some original privkey k, whose public key has coordinates x_k, y_k, if y_k is odd then when you change the pubkey from 03deadbeef.. to deadbeef you are actually encoding the pubkey corresponding to -k, not k (true because on the curve P + -P = O), so a person ignorantly doing this will find themselves unable to sign against this pubkey (although they have the trivial solution of flipping the sign of their privkey, admittedly).

Copy link

@AdamISZ AdamISZ Nov 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I guess this isn't really anything to discuss, it's already the case that the signing algorithm is 'take private key and if it's not an even y, then flip its sign', so this is already addressed/understood.

https://github.com/bitcoin-core/secp256k1/blob/7006f1b97fd8dbf4ef75771dd7c15185811c3f50/src/modules/schnorrsig/main_impl.h#L155-L160

Copy link
Author

@dgpv dgpv Jan 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's another potential problem here to discuss: data taken from invalid CPubKey can actually constitute a valid XOnlyPubKey:

  >>> CPubKey(x('0478d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71')).is_fullyvalid()
False
>>> XOnlyPubKey(x('78d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71')).is_fullyvalid()
True

I think the safe route would be to not accept invalid CPubKey here, those who really need to create XOnlyPubKey from invalid CPubKey can simply strip the first byte themselves.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. But I mean coming back to my first comment, I was only concerned that you would get "the wrong" key from parity stripping, but then as per convo I realized that concern doesn't make sense, because of the way signing works.

But what you raise here, i.e. that someone can start with a completely invalid key and after conversion, end with a valid one ... I mean, I honestly don't want to try to figure out the situations where that could cause a problem. The whole thing just feels kind of wrong for cryptographically or financially sensitive code: if you ever know that input is not correct (not fully valid), stop there, don't leave it around, creating a 'maybe valid in future' object.

I do get that this is coming directly from something in Core, but the KISS principle seems to say 'never create pubkey objects from invalid pubkey serializations'.

So in short I agree with your final sentence :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went and made the change to not accept invalid CPubKey as arguments to XOnlyPubKey(): 01f5b3f

@dgpv dgpv force-pushed the taproot branch 4 times, most recently from 1d3a8fc to 5ea6ade Compare November 15, 2021 12:24
# Not that it matters much in python where we don't have control over
# memory and the keydata is probably spread all over the place anyway,
# but still, do this to be close to the original source
ctypes.memset(keypair_buf, 0, 64)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keypair_buf is 96 bytes, should clear them all

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eac92da

@dgpv
Copy link
Author

dgpv commented Nov 19, 2021

After starting to work on taproot support for python-elementstx, I added a bunch of changes here, related to the fact that Elements has different parameters for tagged hashes and has different default leaf version.

I moved hashers and default leaf version to CoreChainParams: 0db3f0a

Moved tap-tweak-related code from methods of XOnlyPubKey to standalone functions: d317448
This is needed because XOnlyPubKey is not a 'dispatched' class, that is, there's no BitcoinXOnlyPubKey or ElementsXOnlyPubKey, but the hashers are chain-dependent. With them as standalone functions, there will be no confusion on the context where they are used, while if they are methods, there might be confusion when XOnlyPubKey is created in Elements context and used in Bitcoin context, but the user would expect the tweak functions to behave as if in Elements context.

I split sign_schnorr into sign_schnorr_no_tweak and sign_schnorr_tweaked, where the later is only available from CCoinKey: 859d82c
This is also because the tweaks are chain-context-dependent, and CKey is not a 'dispatched' class, but CCoinKey is dispatched, so the correct tags will be used inside CCoinKey's methods when doing tagged hashing.

@AdamISZ
Copy link

AdamISZ commented Nov 21, 2021

I was able to pay into a P2TR address (the plain keypath spend type) and spend out of it on regtest. It's a good sanity check to start. Obviously there is a certain amount of just "messing about" in here, but just for insight I've copied it out. More comments below.

(venv) $ python
Python 3.8.10 (default, Sep 28 2021, 16:10:42) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import bitcointx as btc
>>> btc.allow_secp256k1_experimental_modules()
>>> btc.select_chain_params("bitcoin/regtest")
(<BitcoinMainnetParams: 'bitcoin'>, <BitcoinRegtestParams: 'bitcoin/regtest'>)
>>> from bitcointx.core.key import CKey
>>> priv = b"\x01"*32
>>> k = CKey(priv)
>>> k.xonly_pub
b'\x1b\x84\xc5V{\x12d@\x99]>\xd5\xaa\xba\x05e\xd7\x1e\x184`H\x19\xff\x9c\x17\xf5\xe9\xd5\xdd\x07\x8f'
>>> k.pub
CPubKey(x('031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f'))
>>> from bitcointx.wallet import P2TRCoinAddress
>>> P2TRCoinAddress.from_xonly_pubkey(k.xonly_pub)
P2TRBitcoinRegtestAddress('bcrt1p33wm0auhr9kkahzd6l0kqj85af4cswn276hsxg6zpz85xe2r0y8s7hfsm7')
>>> P2TRCoinAddress.from_pubkey(k.pub)
P2TRBitcoinRegtestAddress('bcrt1p33wm0auhr9kkahzd6l0kqj85af4cswn276hsxg6zpz85xe2r0y8s7hfsm7')
>>> # here i used sendtoaddress in bitcoin-cli to fund the address.
>>> hextx1 = "0200000000010173a9d04d20c5d44d1bbbdb1abef2e6d053bb665f2da3cd31bced6b5eb06c07d00100000000feffffff0280969800000000002251208c5db7f797196d6edc4dd7df6048f4ea6b883a6af6af032342088f436543790f700fe40000000000160014780c6de5527e1462669e975d360f80a524a0833c024730440220661da93bd1a5f0095e89bc491285971d9f1c6c135ad7dedb1de4601b26c1c92c022039d8ed77749906ea5cc0f73cc526c358beec37df073dea03e07b9882850a52f2012102411ebe780264174df55a226d1bb41d3c6e79b06734842e1a04b4b7629bbb63c968030000"
>>> from binascii import hexlify, unhexlify
>>> tx1 = unhexlify(hextx1)
>>> from bitcointx.core import CTransaction
>>> CTransaction.deserialize(tx1)
CBitcoinTransaction([CBitcoinTxIn(CBitcoinOutPoint(lx('d0076cb05e6bedbc31cda32d5f66bb53d0e6f2be1adbbb1b4dd4c5204dd0a973'), 1), CBitcoinScript([]), 0xfffffffe)], [CBitcoinTxOut(0.1*COIN, CBitcoinScript([1, x('8c5db7f797196d6edc4dd7df6048f4ea6b883a6af6af032342088f436543790f')])), CBitcoinTxOut(0.1494616*COIN, CBitcoinScript([0, x('780c6de5527e1462669e975d360f80a524a0833c')]))], 872, 2, CBitcoinTxWitness([CBitcoinTxInWitness(CScriptWitness([x('30440220661da93bd1a5f0095e89bc491285971d9f1c6c135ad7dedb1de4601b26c1c92c022039d8ed77749906ea5cc0f73cc526c358beec37df073dea03e07b9882850a52f201'),x('02411ebe780264174df55a226d1bb41d3c6e79b06734842e1a04b4b7629bbb63c9')]))]))
>>> outs = [{"address": "", "value": 9998000}]
>>> priv2 = b"\x02"*32
>>> k2 = CKey(priv2)
>>> outaddr = P2TRCoinAddress.from_xonly_pubkey(k2.xonly_pub)
>>> outaddr
P2TRBitcoinRegtestAddress('bcrt1p5e6v9v2j5wp3y6c79gaqdqltq7jdv45fswnnm7exmmp2020mqepspf6x45')
>>> #this is the format we use in JM, but not needed here:
>>> outs = [{"address": str(outaddr), "value": 9998000}]
>>> outs
[{'address': 'bcrt1p5e6v9v2j5wp3y6c79gaqdqltq7jdv45fswnnm7exmmp2020mqepspf6x45', 'value': 9998000}]
>>> intxid = unhexlify("a8ab6b6740182bcec11e36a65f0b3baf88969ca3f32d24df2fe4f75875ed8fed")
>>> ins = [(intxid, 0)]
>>> from bitcointx.core import CMutableOutPoint
>>> from bitcointx.core import CMutableTxIn
>>> outpoint = CMutableOutPoint(intxid[::-1], 0)
>>> vin = [CMutableTxIn(prevout=outpoint, nSequence=0xffffffff)]
>>> vin
[CBitcoinMutableTxIn(CBitcoinMutableOutPoint(lx('a8ab6b6740182bcec11e36a65f0b3baf88969ca3f32d24df2fe4f75875ed8fed'), 0), CBitcoinScript([]), 0xffffffff)]
>>> from bitcointx.wallet import CCoinAddress
>>> sPK = CCoinAddress(outs[0]["address"]).to_scriptPubKey()
>>> sPK
CBitcoinScript([1, x('a674c2b152a383126b1e2a3a0683eb07a4d6568983a73dfb26dec2a7a9fb0643')])
>>> from bitcointx.core import CMutableTxOut
>>> vout = [CMutableTxOut(outs[0]["value"], sPK)]
>>> vout
[CBitcoinMutableTxOut(0.09998*COIN, CBitcoinScript([1, x('a674c2b152a383126b1e2a3a0683eb07a4d6568983a73dfb26dec2a7a9fb0643')]))]
>>> from bitcointx.core import CMutableTransaction
>>> tx2 = CMutableTransaction(vin, vout, nVersion=2)
>>> tx2
CBitcoinMutableTransaction([CBitcoinMutableTxIn(CBitcoinMutableOutPoint(lx('a8ab6b6740182bcec11e36a65f0b3baf88969ca3f32d24df2fe4f75875ed8fed'), 0), CBitcoinScript([]), 0xffffffff)], [CBitcoinMutableTxOut(0.09998*COIN, CBitcoinScript([1, x('a674c2b152a383126b1e2a3a0683eb07a4d6568983a73dfb26dec2a7a9fb0643')]))], 0, 2, CBitcoinMutableTxWitness([CBitcoinMutableTxInWitness(CScriptWitness([]))]))
>>> from bitcointx.core import CTxOut
>>> cto1 = CTxOut(10000000, P2TRCoinAddress.from_xonly_pubkey(k.xonly_pub).to_scriptPubKey())
>>> cto1
CBitcoinTxOut(0.1*COIN, CBitcoinScript([1, x('8c5db7f797196d6edc4dd7df6048f4ea6b883a6af6af032342088f436543790f')]))
>>> spent_outputs = [cto1]
>>> from bitcointx.core.script import SignatureHashSchnorr
>>> SignatureHashSchnorr(tx2, 0, spent_outputs)
b'\xae\x92\x97)\xd2\x84\xe3\x1a\xbd\xfcyC\xee\xc9y\x07v\xa7\x04s\x8d\xee\x96\xad\xa4\x03\x85!Sp\r\x92'
>>> sighash = SignatureHashSchnorr(tx2, 0, spent_outputs)
>>> sig = k.sign_schnorr_tweaked(sighash)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'CKey' object has no attribute 'sign_schnorr_tweaked'
>>> from bitcointx.wallet import CCoinKey
>>> k = CCoinKey.from_secret_bytes(b"\x01"*32)
>>> sig = k.sign_schnorr_tweaked(sighash)
>>> len(sig)
64
>>> from bitcointx.core import CMutableTxInWitness
>>> from bitcointx.core.script import CScriptWitness
>>> tx2.wit.vtxinwit[0] = CMutableTxInWitness(CScriptWitness([sig]))
>>> tx2
CBitcoinMutableTransaction([CBitcoinMutableTxIn(CBitcoinMutableOutPoint(lx('a8ab6b6740182bcec11e36a65f0b3baf88969ca3f32d24df2fe4f75875ed8fed'), 0), CBitcoinScript([]), 0xffffffff)], [CBitcoinMutableTxOut(0.09998*COIN, CBitcoinScript([1, x('a674c2b152a383126b1e2a3a0683eb07a4d6568983a73dfb26dec2a7a9fb0643')]))], 0, 2, CBitcoinMutableTxWitness([CBitcoinMutableTxInWitness(CScriptWitness([x('196127ee7b6c2d7bfd9863ed3e9b4e912878a0a80935fe72aab91936264486741524e8d04c16ea76e80f6f20541ff954ab129725d3e5024bfadb17afd4e5a71b')]))]))
>>> CTransaction.deserialize(tx1)
CBitcoinTransaction([CBitcoinTxIn(CBitcoinOutPoint(lx('d0076cb05e6bedbc31cda32d5f66bb53d0e6f2be1adbbb1b4dd4c5204dd0a973'), 1), CBitcoinScript([]), 0xfffffffe)], [CBitcoinTxOut(0.1*COIN, CBitcoinScript([1, x('8c5db7f797196d6edc4dd7df6048f4ea6b883a6af6af032342088f436543790f')])), CBitcoinTxOut(0.1494616*COIN, CBitcoinScript([0, x('780c6de5527e1462669e975d360f80a524a0833c')]))], 872, 2, CBitcoinTxWitness([CBitcoinTxInWitness(CScriptWitness([x('30440220661da93bd1a5f0095e89bc491285971d9f1c6c135ad7dedb1de4601b26c1c92c022039d8ed77749906ea5cc0f73cc526c358beec37df073dea03e07b9882850a52f201'),x('02411ebe780264174df55a226d1bb41d3c6e79b06734842e1a04b4b7629bbb63c9')]))]))
>>> hexlify(tx2.serialize())
b'02000000000101ed8fed7558f7e42fdf242df3a39c9688af3b0b5fa6361ec1ce2b1840676baba80000000000ffffffff01b08e980000000000225120a674c2b152a383126b1e2a3a0683eb07a4d6568983a73dfb26dec2a7a9fb06430140196127ee7b6c2d7bfd9863ed3e9b4e912878a0a80935fe72aab91936264486741524e8d04c16ea76e80f6f20541ff954ab129725d3e5024bfadb17afd4e5a71b00000000'

A few comments: Initially, I found myself rather confused about merkle_root argument, at least specifically for this no-script, provably-keypath-only spending case as recommended in the BIP and implemented here.

For example I think the docstring here still isn't quite enough for a user to be absolutely sure what it's doing:

"""Create a P2TR address from x-only "internal" pubkey (in BIP341 terms)

Raises CCoinAddressError if pubkey is invalid
"""

I think it should state that the actual recommendation for provably-no-script path (as justified in cite note 22) is being followed (or maybe just: that the default procedure Q = P + H(P||m)G is being followed with m == b''), because it isn't immediately obvious from code that you are calling to generate this address from a pubkey is using compute_tap_tweak_hash with merkle_root == b'' (I mean, sure it's not that hard to figure it out, but I felt obliged to do so because the doc didn't give me 100% certainty what procedure is being followed).

Meanwhile I do think this docstring for from_xonly_output_pubkey is clear to a reader, and that should help a lot:

"""Create a P2TR address from x-only pubkey that is already tweaked,
        the "output pubkey" in the terms of BIP341

        Raises CCoinAddressError if pubkey is invalid, unless accept_invalid
        is True.
        """

Another aspect of this: that _sign_schnorr_internal uses merkle_root is None as the differentiator for not tweaking, while merkle_root == b'' is different. I do realise that it's unambiguous and correct, but just mentioning that it seems like the kind of thing that can trip people up (like, they might think they should call sign_schnorr_no_tweak if they don't have a script path).

You can also see I originally didn't notice the CCoinKey part that you mentioned above. But that's clear now.

I think it was pretty smooth and intuitive apart from that, inasmuch as, it's very similar to the pre-existing workflow. But then of course I only tried the simplest possible version of the scriptpubkey to spend. At some point I will try using script path stuff, but it's less of a priority for me as I don't have any use case for now. All the same I will read the script part of the patch a bit more and add any further comments from what I see. Btw search scnorr and shnorr, I think you have a typo in one or two places.

@dgpv
Copy link
Author

dgpv commented Nov 21, 2021

af7e2de

I've noticed that XOnlyPubKey did not have __repr__ implemented, I fixed this.
Also I've searched for scnorr and shnorr, found two mistypes, fixed.
Added docstrings that talk about the differences between different tweak 'modes'.

_sign_schnorr_internal uses merkle_root is None as the differentiator for not tweaking, while merkle_root == b'' is different. I do realise that it's unambiguous and correct, but just mentioning that it seems like the kind of thing that can trip people up (like, they might think they should call sign_schnorr_no_tweak if they don't have a script path)

because this is internal function, I felt that it is ok to use None/b''/<32 bytes> distinction instead of using special flag object as I did before. The function is not supposed to be called from outside the library code, so anyone who calls it is supposedly know that they are doing, as internal functions are not part of API. Still, I added docstring that hopefully gives explanations to those who are looking at the code.

@AdamISZ
Copy link

AdamISZ commented Nov 21, 2021

Great. On that last point, of course, well understood that it is not for external callers; the reason it crops up is because I'm trying to read it carefully to understand exactly what happens when I call the API-level functions. And that's exactly what I don't need to do now because the docstrings are a lot more detailed :) (well, it's always interesting to read the code more, of course! but anyway).

@dgpv
Copy link
Author

dgpv commented Nov 21, 2021

(last force-push to 6270e62 was just to fix docstring of sign_schnorr_tweaked() to make it more readable)

@AdamISZ
Copy link

AdamISZ commented Jan 6, 2022

I wonder if it is sensible for TaprootScriptTree(leaves_or_branches) to raise ValueError by default if leaves_or_branches contain duplicate leaves (and allow such duplicates if a special flag allow_duplicate_leaves=True is supplied)

Interesting.

For what it's worth, my opinion: I don't think this library needs to do that. If there are indeed use cases for duplicate leaves at different points in the tree, then it'd be up to the people creating, reading, signing such transactions (i.e. one step up the software stack from this library) to decide how to deal with it. I suppose my argument is: there are an infinite number of ways in which a leaf might be dangerous to a user, and this is only one; clearly if a user doesn't know the full tree, then there might be a spending path that they should not accept (so agreeing with roconnor's "probably good advice regardless").

@dgpv
Copy link
Author

dgpv commented Jan 22, 2022

(rebased on current master)

tapleaf_hash: Optional[bytes] = None,
codeseparator_pos: int = -1,
annex_hash: Optional[bytes] = None
) -> bytes:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tapleaf_hash, annex_hash, and SIGVERSION_TAPSCRIPT are currently not covered by tests

@dgpv
Copy link
Author

dgpv commented Feb 4, 2022

Synced the taproot branch with https://github.com/Simplexum/python-bitcointx/tree/segwitv1_addrs

@dgpv
Copy link
Author

dgpv commented Feb 14, 2022

rebased on current master

@dgpv dgpv merged commit d5ff9b0 into master Feb 14, 2022
@dgpv
Copy link
Author

dgpv commented Feb 14, 2022

merged

@dgpv dgpv deleted the taproot branch February 14, 2022 10:01
@HRezaei
Copy link

HRezaei commented Apr 8, 2022

Thank you @dgpv for all your efforts.

Please don't hesitate to comment if you think something might be made more convenient for the developer.

The most crucial thing needed now, is some examples, I would say. Even very simple, but fully working when sent to network.

I tried one for keypath spending similar to this comment by @AdamISZ. It produces a transaction hex, but when sent to a network, an error is returned like this:
bitcoin.rpc.JSONRPCError: {'code': -344, 'message': 'Error validating transaction: Transaction missing input or output..'}

My code is attached below, any comments to resolve the issue would be appreciated. I think this, once resolved, can be added to examples:

import bitcointx as btc
btc.allow_secp256k1_experimental_modules()

from binascii import unhexlify
from bitcointx.core import CTransaction
from bitcointx.wallet import P2TRBitcoinTestnetAddress
from bitcointx.core import CMutableOutPoint
from bitcointx.core import CMutableTxIn
from bitcointx.wallet import CCoinAddress
from bitcointx.core import CMutableTxOut
from bitcointx.core import CMutableTransaction
from bitcointx.core import CTxOut
from bitcointx.core.script import SignatureHashSchnorr
from bitcointx.wallet import CCoinKey
from bitcointx.core import CMutableTxInWitness
from bitcointx.core.script import CScriptWitness
from bitcoin.rpc import Proxy


def parse_raw_tx(tx_hex):
    parsed_tx = {
        'version': tx_hex[:8],
        'Input Count': tx_hex[8:10],
        'Input 1 Previous Output Hash': tx_hex[10:74],
    }
    return parsed_tx


print(btc.select_chain_params("bitcoin/testnet"))


def test_tx_from_raw_byte_keys():
    priv = b"\x01"*32
    alice_priv_key = CCoinKey.from_secret_bytes(priv)
    print(alice_priv_key.xonly_pub)

    print(alice_priv_key.pub)

    alice_address = P2TRBitcoinTestnetAddress.from_xonly_pubkey(alice_priv_key.xonly_pub)
    print("Alice address: ", alice_address)

    print(P2TRBitcoinTestnetAddress.from_pubkey(alice_priv_key.pub))

    # here i used a faucet to fund the address.
    tx_id = "86e68fc34258372e96626841c3b05ae6b478d20fe85a769bca91bf1b13f8c83c"
    prev_tx_hex = "02000000000101bdf4080aaefe9d274f5f91ca1250d81cd05e62d67641bbf52e71e246ee8557e40000000000feffffff02abbb0f0000000000160014c885ac9ecdee154b1ac3a372ebc1184601b9da20204e0000000000002251208c5db7f797196d6edc4dd7df6048f4ea6b883a6af6af032342088f436543790f0247304402205de0c18eab95151c1a2908b247c4c5197894a4587ed0326cba06976183f7b058022067251f579e85f027368056cd1fda0a0baef9a5dac69ee6736e2d84f95ee3fa410121034bcc56646b1e8f619522c3db1feebd64bac532a5e09389e4805caf55cf38d3da167b2100"
    prev_tx_bytes = unhexlify(prev_tx_hex)
    prev_tx = CTransaction.deserialize(prev_tx_bytes)

    print("Previous tx: ", prev_tx)
    print("Previous tx parsed: ", parse_raw_tx(prev_tx_hex))
    print("Available balance: ", prev_tx.vout[1].nValue)
    output_index_in_prev_tx = 1
    balance = prev_tx.vout[output_index_in_prev_tx].nValue
    fee = 2000
    amount = balance - fee

    priv2 = b"\x02"*32
    bob_priv_key = CCoinKey.from_secret_bytes(priv2)
    bob_addr = P2TRBitcoinTestnetAddress.from_xonly_pubkey(bob_priv_key.xonly_pub)
    print("Bob address: ", bob_addr)

    # this is the format we use in JM, but not needed here:
    outs = [{"address": str(bob_addr), "value": amount}]
    print("outs:", outs)

    intxid = unhexlify(tx_id)
    outpoint = CMutableOutPoint(intxid[::-1], output_index_in_prev_tx)
    vin = [CMutableTxIn(prevout=outpoint, nSequence=0xffffffff)]
    print("vin:", vin)

    sPK = CCoinAddress(outs[0]["address"]).to_scriptPubKey()
    print("sPK:", sPK)

    vout = [CMutableTxOut(outs[0]["value"], sPK)]
    print("vout:", vout)

    tx = CMutableTransaction(vin, vout, nVersion=2)

    cto1 = CTxOut(balance, alice_address.to_scriptPubKey())
    print("cto1:", cto1)

    spent_outputs = [cto1]
    print("SignatureHashSchnorr:", SignatureHashSchnorr(tx, 0, spent_outputs))

    sighash = SignatureHashSchnorr(tx, 0, spent_outputs)

    sig = alice_priv_key.sign_schnorr_tweaked(sighash)
    print(len(sig))

    tx.wit.vtxinwit[0] = CMutableTxInWitness(CScriptWitness([sig]))
    print("tx:", tx)

    connection = Proxy(service_url='http://api.blockcypher.com/v1/btc/test3/txs/push')
    res = connection.sendrawtransaction(tx)
    print(res)


if __name__ == '__main__':
    test_tx_from_raw_byte_keys()

@AdamISZ
Copy link

AdamISZ commented Apr 8, 2022

@HRezaei just in case it helps, I'm guessing you didn't see this example I did around that time, which tested script path and which broadcast OK: https://gist.github.com/AdamISZ/6233d9d9b8d25483dc5d39cc6b9892a7

@HRezaei
Copy link

HRezaei commented Apr 8, 2022

@HRezaei just in case it helps, I'm guessing you didn't see this example I did around that time, which tested script path and which broadcast OK: https://gist.github.com/AdamISZ/6233d9d9b8d25483dc5d39cc6b9892a7

Thanks @AdamISZ,
But I just tried to broadcast that tx in your example as well and it raises the same error. Maybe my way of broadcasting is wrong?! 🤔

from bitcoin.rpc import Proxy
connection = Proxy(service_url='http://api.blockcypher.com/v1/btc/test3/txs/push')
res = connection.sendrawtransaction(tx2)
print(res)

@AdamISZ
Copy link

AdamISZ commented Apr 8, 2022

But I just tried to broadcast that tx in your example as well and it raises the same error.

Presumably you don't mean that literally? That transaction of course wouldn't broadcast, it's already been mined a month or two back :) But in that case I wonder what you do mean?

Re: using a proxy, I'd just use a signet or testnet (highly prefer signet! testnet3 is awful for many reasons) Core instance and use sendrawtransaction. Less chance of something going wrong with the intermediary and clearer error messages.

@HRezaei
Copy link

HRezaei commented Apr 15, 2022

You're right @AdamISZ,
I guess it's better to continue the discussion under your own gist...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants