Implement BIP 340-342 validation - Implement Taproot signature hashing (consensus)

https://github.com/bitcoin/bitcoin/pulls/17977

Host: jnewbery  -  PR author: sipa

The PR branch HEAD was ad51604 at the time of this review club meeting.

This is the third Review Club meeting on the (work in progress) implementation of BIP 340-342. We’ve previously looked at:

Those preliminary changes have been merged into the master branch in separate PRs.

This week, we’ll look at one commit from PR 17977 - Implement Taproot signature hashing.

Participants are encouraged to review the notes from the session on PR18401 in particular, since we’ll be covering a lot of the same topics this week.

Notes

  • The transaction digest that is used in signature creation and verification is calculated from different parts of the transaction. There are extensive notes covering this topic from our session on PR18401.

  • BIP 341 contains the specification of the new signature validation rules.

  • The Taproot BIP Review series covered this topic in week 4. The meeting log from that review session has more details.

  • This week, we’ll review the implementation of the Taproot transaction hashing algorithm. We won’t look at full signature validation. It may be useful to check out just up to the commit Implement Taproot signature hashing (BIP 341). Specifically, we’ll be looking at the SignatureHashSchnorr() function in src/script/interpreter.cpp.

  • It’s probably most instructive to compare the new SignatureHashSchnorr() function to the SignatureHash() function immediately below, which calculates the signature hash for segwit v0 and legacy transactions.

Questions

  1. SignatureHashSchnorr() is a templated function. Why? What types T is the template function instantiated with? Hint: look at how the existing SignatureHash() function is called.

  2. SignatureHashSchnorr() is passed a PrecomputedTransactionData* argument. What data is stored in this structure? Why?

  3. SignatureHashSchnorr() is passed a hash_type argument. How many valid hash types are there? The hash_type parameter is split into output_type and input_type here:

      // Hash type
      const uint8_t output_type = ...
      const uint8_t input_type = ...
    

    How many valid values are there for output_type and input_type?

  4. Just like SignatureHash(), the new function creates a local CHashWriter object. CHashWriter is a stream-like object. Other objects can be serialized into it (using the << operator), and at the end, CHashWriter::GetHash() is called to retrieve the digest.

    One difference from SignatureHash() is that the CHashWriter is copy-constructed from HasherTapSighash. How is that object constructed, and what’s the difference from a regular CHashWriter object?

  5. If the hash_type does not have the ANYONECANPAY flag, certain parts of the transaction are added to the CHashWriter. What are those elements, and how is that different from SignatureHash()?

  6. A spend_type byte is added the CHashWriter. What are the component parts of spend_type, and what do they indicate? Hint: refer back to BIP 341 and BIP 342.

  7. If the hash_type indicates SIGHASH_SINGLE, there’s a check here:

    if (in_pos >= tx_to.vout.size()) return false;

    What is that testing, and why?

Meeting Log

  119:00 <jnewbery> #startmeeting
  219:00 <jnewbery> hi folks!
  319:00 <willcl_ark> hi
  419:00 <pinheadmz> 👋
  519:00 <emzy> hi
  619:00 <fjahr> hi
  719:00 <jnewbery> welcome to bitcoin core PR review club :)
  819:00 <gzhao408> hola
  919:00 <michaelfolkson> hi
 1019:01 <nehan> hi
 1119:01 <jnewbery> today we're going to be looking at a commit from the taproot implementation PR
 1219:01 <jnewbery> notes/questions in the normal place: https://bitcoincore.reviews/17977.html
 1319:01 <troygiorshev> hi
 1419:01 <kanzure> hi
 1519:01 <jnewbery> normal reminder: there are no stupid questions. We're all here to learn, so please speak up if something isn't clear to you. You'll be helping others too!
 1619:01 <raking> hi
 1719:02 <felixweis> hi
 1819:02 <webby> hi
 1919:02 <jonatack> hi (afk -> sleep, will read tomorrow am)
 2019:02 <jnewbery> other reminder: you can ask questions at any time. You don't have to ask to ask. I'll guide the discussion with some prepared questions, but feel free to jump in at any point with your question
 2119:02 <jnewbery> ok, let's get started
 2219:02 <jnewbery> who had a chance to review the commit?
 2319:02 <fjahr> y
 2419:03 <jnewbery> (not necessarily the full PR, just the changes to signature hashing)
 2519:03 <pinheadmz> y
 2619:03 <willcl_ark> y
 2719:03 <michaelfolkson> y
 2819:03 <emzy> n
 2919:03 <troygiorshev> y/n
 3019:03 <nehan> n
 3119:03 <webby> n
 3219:03 <jnewbery> great!
 3319:03 <raking> n
 3419:03 <felixweis> y
 3519:03 <jnewbery> First question: SignatureHashSchnorr() is a templated function. Why? What types T is the template function instantiated with? Hint: look at how the existing SignatureHash() function is called.
 3619:04 <troygiorshev> CTransaction and CMutableTransaction, I think that's it
 3719:04 <pinheadmz> is it because the function can accept both mutabelTX and TX ?
 3819:04 <willcl_ark> so you don't have to copy MutableTransaction into a Transaction each time?
 3919:04 <fjahr> Different transaction types for the spending transaction. Could be CTransaction or CMutableTransaction afaict
 4019:04 <jnewbery> troygiorshev pinheadmz willcl_ark fjahr: correct! We can call it with either a CTransaction or CMutableTransaction, just like SignatureHash()
 4119:05 <jnewbery> any questions about that? Is everyone happy with templates?
 4219:06 <jnewbery> I'll move on, but feel free to come back and ask about a previous question if it doesn't make sense to you
 4319:06 <jnewbery> next
 4419:06 <gzhao408> in what situations are we working with a CMutableTransaction? our own txns?
 4519:06 <jnewbery> SignatureHashSchnorr() is passed a PrecomputedTransactionData* argument. What data is stored in this structure? Why?
 4619:06 <pinheadmz> gzhao408 yeah, when the wallet (for ex) is constructing a tx
 4719:06 <jnewbery> great question gzhao408! Anyone know?
 4819:06 <willcl_ark> Depends on which BIP143 (Segwit v0) or BIP341 (taproot) features the tx uses.
 4919:06 <pinheadmz> you can add inputs and outputs and signatures to an MTX
 5019:06 <pinheadmz> but a TX is what comes out of a block for verification
 5119:07 <jnewbery> pinheadmz: how about transactions that aren't in a block?
 5219:07 <pinheadmz> mempool tx are also not mutable
 5319:07 <sipa> (for some historic context: the distinction between tx and mtx arose from wanting to cache the txid and later wtxid inside a tx)
 5419:07 <jnewbery> pinheadmz: yep
 5519:08 <jnewbery> thanks sipa!
 5619:08 <sipa> as a tx is immutable, it can be cached without ever needing to worry about inconsistency or locking or whatever
 5719:08 <sipa> it may also enable having different container structures for tx that are more efficient (e.g. no separate allocation for all the inputs, outputs, scripts, ...)
 5819:08 <michaelfolkson> The state switches from mutable to immutable on announcement? Should check code...
 5919:09 <sipa> michaelfolkson: there is no state; it's an entirely different class
 6019:09 <pinheadmz> michaelfolkson i thought the walletconverts to tx before broadcast
 6119:09 <jnewbery> willcl_ark: yes, what we cache depends on the transaction type. Can you expand?
 6219:09 <sipa> and yes, it's converted from mtx to tx after signing
 6319:09 <willcl_ark> BIP143 tx: hashPrevouts, hashSequence, hashOutputs
 6419:10 <willcl_ark> BIP341 tx: as BIP143 + spentAmounts + spentScripts
 6519:10 <theStack> hi
 6619:10 <gzhao408> thanks this is really helpful :)
 6719:11 <pinheadmz> willcl_ark and some are double-sha256 and some are single-sha256!
 6819:11 <gzhao408> why do we double-sha256 things?
 6919:11 <sipa> gzhao408: because that's what BIP143 prescribes :)
 7019:12 <jnewbery> willcl_ark: I think those are actually all cached for all witness transactions. It's just that m_spent_amounts_hash and m_spent_scripts_hash are only used if the tx is taproot
 7119:12 <sipa> if your question is why did BIP143's authors chose that option: it was aiming to change as little as possible compared to pre-segwit sighashing, iirc
 7219:12 <jnewbery> gzhao408: it's what satoshi would have wanted
 7319:12 <willcl_ark> jnewbery: ah ok :) that makes sense tbh
 7419:12 <sipa> jnewbery: the PR was just updated, it now computes only the necessaey things
 7519:13 <jnewbery> sipa: ah! I haven't looked at this week's changes
 7619:13 <troygiorshev> gzhao408: apparently maybe protection against length extention attacks (but don't take my word on that)
 7719:13 <willcl_ark> Ah i checked out the PR from bitcoin/bitcoin
 7819:13 <sipa> troygiorshev: length extension attacks are not relevant in this context
 7919:14 <troygiorshev> sipa: ok thx
 8019:14 <sipa> (they apply when you're using a hash as a MAC, which implies there is secret data)
 8119:14 <jnewbery> does anyone have questions about PrecomputedTransactionData or should we move on?
 8219:14 <sipa> however, that may be the (ill advised) reason why satoshi chose for double hashing
 8319:15 <jnewbery> oh wow. PrecomputedTransactionData::Init() has changed quite a lot
 8419:16 <jnewbery> this is what it looked like previously: ttps://github.com/bitcoin-core-review-club/bitcoin/commit/41d08f5d77f52bec0e31bb081d85fff2d67d0467#diff-be2905e2f5218ecdbe4e55637dac75f3R1312-R1331
 8519:16 <jnewbery> https://github.com/bitcoin-core-review-club/bitcoin/commit/41d08f5d77f52bec0e31bb081d85fff2d67d0467#diff-be2905e2f5218ecdbe4e55637dac75f3R1312-R1331
 8619:16 <jnewbery> Next question. SignatureHashSchnorr() is passed a hash_type argument. How many valid hash types are there?
 8719:17 <michaelfolkson> 4
 8819:17 <jnewbery> any advances on 4?
 8919:17 <willcl_ark> 5
 9019:17 <willcl_ark> and two masks
 9119:17 <pinheadmz> 7 total i think?
 9219:17 <willcl_ark> or is it 6 and one mask
 9319:17 <pinheadmz> well 6 + default 0x00
 9419:17 <willcl_ark> hmmm
 9519:17 <sipa> how many total combinations are there?
 9619:17 <willcl_ark> final answer: 7
 9719:17 <sipa> bingo
 9819:17 <michaelfolkson> Are we including default and masks in that?
 9919:18 <jnewbery> that's right!
10019:18 <jnewbery> How many valid values are there for output_type and input_type?
10119:18 <jnewbery> https://github.com/bitcoin-core-review-club/bitcoin/commit/41d08f5d77f52bec0e31bb081d85fff2d67d0467#diff-be2905e2f5218ecdbe4e55637dac75f3R1371-R1372
10219:18 <willcl_ark> 2 output and 1 input?
10319:19 <michaelfolkson> I don't know what the masks are doing. Can someone explain? :)
10419:19 <raking> same
10519:19 <willcl_ark> they're for a bitwise AND
10619:19 <gzhao408> it's leftmost bit and 2 rightmost bits
10719:19 <sipa> they extract information
10819:19 <jnewbery> michaelfolkson: all will become clear shortly
10919:19 <michaelfolkson> Ok I'll be patient
11019:19 <jnewbery> willcl_ark: it can't be 1 for input, otherwise we wouldn't need a variable to store it :)
11119:20 <jnewbery> any advances on 2 and 1?
11219:20 <willcl_ark> jnewbery: oh, I see what I've done
11319:21 <theStack> next try: 3 values for output, 2 values for input
11419:21 <jnewbery> ok, the sighash type dictates what parts of the transaction we hash to go into the signature
11519:21 <jnewbery> theStack: yes!
11619:21 <willcl_ark> so input has 8? and output 7?
11719:21 <willcl_ark> oh, no ok
11819:22 <jnewbery> part of the sighash type indicates which parts of the transaction's outputs go into the signature hash
11919:22 <jnewbery> that can take the values SIGHASH_ALL, SIGHASH_SINGLE and SIGHASH_NONE (3 different options)
12019:22 <gzhao408> i read it as output = SIGHASH_ALL, SIGHHASH_SINGLE, or SIGHASH_NONE and input = SIGHASH_ANYONECANPAY or 0?
12119:23 <jnewbery> the other part of the sighash indicates which parts of the transaction's inputs go into the signature hash
12219:23 <sipa> gzhao408: correct
12319:23 <jnewbery> that can take the values SIGHASH_ANYONECANPAY or not (2 different options)
12419:24 <troygiorshev> isn't the check on line 1501 unneccesary?
12519:24 <jnewbery> those can be combined in any way so we get 3*2 = 6 different possibilities
12619:24 <jnewbery> so where does the 7th come from?
12719:24 <pinheadmz> huh this is a much simper way to think about sighashing
12819:24 <pinheadmz> i been trying to kinda memorize each type individually
12919:25 <pinheadmz> jnewbery that 7th is the default - sighash all
13019:25 <pinheadmz> if there is no sighash byte at the end of the sig
13119:25 <willcl_ark> ok I see now
13219:25 <sipa> pinheadmz: but 1 is sighash_all already
13319:25 <sipa> how does 0 differ?
13419:25 <pinheadmz> you can not actually add 0x00 literally, it woudl be invalid
13519:25 <sipa> indeed
13619:25 <pinheadmz> its the default value assigned internaly if not sighash byte is present
13719:26 <pinheadmz> and in fact 0x00 is invalid if it is present
13819:26 <sipa> 0 is the implicit sighash type when the byte is missing
13919:26 <sipa> so why is it an implicit 0 and not an implicit 1?
14019:26 <pinheadmz> "because thats what is says in bip341" :-)
14119:26 <willcl_ark> heh
14219:26 <fjahr> jnewbery: you mean have_annex?
14319:27 <sipa> pinheadmz: technically correct is the only kind of correct
14419:27 <sipa> ;)
14519:27 <troygiorshev> sipa: because the mask is on the last two bits?
14619:27 <jnewbery> fjahr: I don't understand what you mean
14719:27 <troygiorshev> (no that's not it)
14819:27 <pinheadmz> sipa i actually don tknow - thought sighash all was only, no byte added -> implict 0x00
14919:27 <sipa> troygiorshev: both 0 and 1 result in the same value after masking (ALL for outputs, 0 for inputs)
15019:28 <sipa> fjahr: that's something unrelated
15119:28 <sipa> it's hashed as well, but elsewhere
15219:28 <jnewbery> fjahr: oh, you mean the annex is the 7th option? No, that's unrelated here. We'll get to it in a bit
15319:28 <fjahr> ok
15419:29 <sipa> so 0 is different from 1, because we don't want people to be able to malleate a 64-byte sig into a 65-byte one
15519:29 <sipa> if it was just an implicit 1, you'd be able to add or remove it without affecting the validity of the signatire
15619:30 <theStack> ah, that makes sense
15719:30 <jnewbery> great point sipa. Thanks!
15819:30 <jnewbery> ok, next question
15919:30 <jnewbery> Just like SignatureHash(), the new function creates a local CHashWriter object. CHashWriter is a stream-like object. Other objects can be serialized into it (using the << operator), and at the end, CHashWriter::GetHash() is called to retrieve the digest.
16019:30 <jnewbery> One difference from SignatureHash() is that the CHashWriter is copy-constructed from HasherTapSighash. How is that object constructed, and what’s the difference from a regular CHashWriter object?
16119:30 <pinheadmz> but adding 0x01 at the end of the sig is not valid right? if you want sighash all you just leave it as a 64 byte sig with no explciit type?
16219:31 <jnewbery> Oh, I see that it's now called HASHER_TAPSIGHASH in the latest push
16319:32 <willcl_ark> sipa: Can't you malleate it in the opposite direction then? from 65 bytes to 64 bytes
16419:32 <sipa> pinheadmz: you can do either; have a 64-byte one with implicit sighash 0, or explicitly make a 65-byte sig with hashtype 1; their semantics are the same... but the signature will still differ because the hash commits to the actual hashtype value
16519:32 <sipa> willcl_ark: no
16619:32 <troygiorshev> jnewbery: it starts with the double tag
16719:33 <jnewbery> troygiorshev: exactly
16819:33 ℹ me is now known as Guest86102
16919:33 <jnewbery> ok, follow-up question: what are tagged hashes and why do we use them in taproot
17019:34 <pinheadmz> jnewbery i think its to prevent hash collisions with hashing the same data for other purposes
17119:34 <troygiorshev> which, if I'm reading 340 right, helps prevent a collision across different uses of the same hash function
17219:34 <jnewbery> pinheadmz troygiorshev: precisely
17319:35 <jnewbery> See the section on tagged hashes in https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#design for full motivation
17419:35 <troygiorshev> gzhao408: a new meaning of "double SHA256" :D
17519:35 <sipa> troygiorshev: so why HASHER_TAPSIGHASH and not just start by writing the double tag into the stream?
17619:35 <pinheadmz> sipa to cache the prefix ?
17719:36 <fjahr> It's 64 bytes which is the size of a block in sha256
17819:36 <felixweis> midstate caching
17919:36 <sipa> yup, exactly
18019:36 <sipa> it's copying the midstate after hashing the tag, so it avoids redoing that for every sighash
18119:37 <troygiorshev> and it's static const so we get some sort of speedup copy constructing it?
18219:37 <jnewbery> pinheadmz fjahr felixweiss: great stuff. If anyone is confused by the words 'midstate' or 'block' in this context, look up the SHA256 or merkle-damgard hash construction wikipedia pages
18319:38 <jnewbery> troygiorshev: I'm not sure if the static or const make a difference there. Can you explain?
18419:39 <jnewbery> next question: If the hash_type does not have the ANYONECANPAY flag, certain parts of the transaction are added to the CHashWriter. What are those elements, and how is that different from SignatureHash()?
18519:40 <willcl_ark> It adds prevouts_hash, spend_amounts_hash, spent_scripts_hash and sequences_hash to the cache if its not ANYONECANPAY
18619:40 <pinheadmz> a hash of all the inputs' prevouts
18719:41 <willcl_ark> but SignatureHash does `SHA256Uint256(GetPrevoutHash(txTo)`
18819:42 <jnewbery> willcl_ark: yes! https://github.com/bitcoin-core-review-club/bitcoin/commit/41d08f5d77f52bec0e31bb081d85fff2d67d0467#diff-be2905e2f5218ecdbe4e55637dac75f3R1380-R1385
18919:43 <pinheadmz> so weitness v0 sighashing only commits to the value of the input being signed?
19019:43 <troygiorshev> jnewbery: hmm I may be wrong, need to explore it a bit further
19119:43 <pinheadmz> but v1 sighash we commit to value of ALL inputs? (unless ANYONECANPAY)
19219:43 <pinheadmz> this is related to the recent hardware wallet fee-attack that was published i think
19319:44 <felixweis> thats what cache->m_spent_amounts_hash is about
19419:44 <jnewbery> pinheadmz: exactly. Can you explain a bit more about the fee-attack?
19519:45 <pinheadmz> ok sure, so currently you can trick a hardware wallet into signing two separate transactions each with two inputs (say a big value and a small value input) then, create a new TX with just the two high value inputs, making the input to the tx unexpectedly large. the output value doesnt change, so the wallet loses a big fee
19619:46 <sipa> troygiorshev: static just makes the variable inaccessible outside the compilation unit, and const prevents code from accidentally modifying the cached value; neither changes performance, they just prevent things we don't want to happen
19719:46 <michaelfolkson> Luke did a good explanation of it https://diyhpl.us/wiki/transcripts/la-bitdevs/2020-06-18-luke-dashjr-segwit-psbt-vulnerability/
19819:46 <pinheadmz> although now that im trying to explain, it seems like sighash single must be used for the attack so the bip341 updated couldnt prevent that ?
19919:46 <sipa> pinheadmz: it doesn't need sighash_single
20019:47 <pinheadmz> ok (grokking)
20119:47 <sipa> and yes, signing all inputs prevents it (in sighash_all)
20219:47 <sipa> *signing all input amounts
20319:47 <jnewbery> there's a good write-up of the segwit v0 sighash fee issue in the optech newsletter: https://bitcoinops.org/en/newsletters/2020/06/10/#fee-overpayment-attack-on-multi-input-segwit-transactions
20419:48 <pinheadmz> oh the trick is lying about the value of an input being spent
20519:48 <pinheadmz> which would normally make a tx invalid on the network
20619:48 <jnewbery> it's been known about for some time, and adding a new segwit version allows us to resolve that class of issues by introducing a new sighash algorithm
20719:48 <pinheadmz> but the attecker throws that input away for the final tx
20819:48 <jnewbery> Next question: A spend_type byte is added the CHashWriter. What are the component parts of spend_type, and what do they indicate? Hint: refer back to BIP 341 and BIP 342.
20919:49 <jnewbery> fjahr: you might be able to answer this one :)
21019:49 <willcl_ark> Whether it's taproot or tapscript and with annex or not
21119:49 <michaelfolkson> ext_flag and annex_present
21219:49 <pinheadmz> script path vs key path
21319:49 <pinheadmz> oh and a flag for has_annex
21419:49 <jnewbery> willcl_ark michaelfolkson pinheadmz: exactly correct
21519:50 <jnewbery> it's specified in https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#common-signature-message
21619:50 <jnewbery> "spend_type (1): equal to (ext_flag * 2) + annex_present, where annex_present is 0 if no annex is present, or 1 otherwise (the original witness stack has two or more witness elements, and the first byte of the last element is 0x50)"
21719:50 <jnewbery> Last question:
21819:50 <jnewbery> If the hash_type indicates SIGHASH_SINGLE, there’s a check here:
21919:50 <jnewbery> if (in_pos >= tx_to.vout.size()) return false;
22019:51 <jnewbery> What is that testing, and why?
22119:51 <pinheadmz> heh, the sighash single bug :-)
22219:51 <jnewbery> pinheadmz: go on...
22319:51 <felixweis> SIGHASH_SINGLE is a mode where for every input there is 1 output
22419:51 <pinheadmz> if there is an input signed with sighash single but no corresponding output...
22519:52 <pinheadmz> satoshis code returns an error code of 1
22619:52 <pinheadmz> which gets interpreted as 32 byte hash value!
22719:52 <pinheadmz> which means you can compute a signature and store in the output of the tx being spent
22819:53 <pinheadmz> and this happened on mainnet and ive heard it refered to as "the coolese transaction ever"
22919:53 <pinheadmz> and AFAIK, the only legit use for OP_CODESEPARATOR ?
23019:53 <willcl_ark> good knowledge !
23119:54 <pinheadmz> willcl_ark https://github.com/bcoin-org/bcoin/blob/master/test/tx-test.js#L260-L264
23219:54 <jnewbery> hmmm I'm not sure about the part about OP_CODESEPARATOR. I know that people have tried to use that in protocols like tumblebit to allow different signing paths
23319:54 <pinheadmz> ah ok, i was hoping to get correted on that
23419:54 <sipa> pinheadmz: i vaguely recall that, but i don't remember the details
23519:55 <jnewbery> There are some mailing list posts by roconnor about OP_CODESEPARATOR in the last few months if you're interested
23619:55 <jeremyrubin> wait the tx's signature can be comitted to inside the output being spent?
23719:55 <pinheadmz> sipa was it not possile to fix sighashsingle in witnessv0?
23819:56 <pinheadmz> jeremyrubin yes in this case bc of sighash single bug. bc the data signed is 0x0000.....01
23919:56 <jnewbery> There's a writeup of the sighash_single bug here: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2014-November/006878.html
24019:56 <pinheadmz> (not an actual digest of the spending tx)
24119:56 <sipa> pinheadmz: i think we just chose not to
24219:56 <sipa> (i don't think it's really a bug a such, just a major footgun)
24319:56 <pinheadmz> jnewbery is excellent at finding things quickly! i knew it was a peter todd post but couldnt find it
24419:57 <sipa> pinheadmz: if you run into it, you're already doing something wrong
24519:57 <jnewbery> the idea behind SIGHASH_SINGLE is that only the output with the corresponding index to the input being spent is included in the hash. Obviously for that to work, there needs to be an output with the same index as the input being signed
24619:57 <jnewbery> that's what the check is for
24719:58 <jnewbery> ok, 3 minutes left. Any final questions?
24819:58 <instagibbs> https://github.com/UnderhandedCrypto/entries/blob/master/2016/JonasNick/README-JUDGES.txt nickler did a contest submission based on the weirdness
24919:58 <jnewbery> s/3/2/
25019:59 <michaelfolkson> Wen moar Taproot
25119:59 <willcl_ark> instagibbs: and a good writeup of the bug too
25219:59 <willcl_ark> that's cleared it right up
25320:00 <jnewbery> michaelfolkson: we'll do more taproot soon. Next week is multiprocess though, hosted by ryanofsky. You won't want to miss it!
25420:00 <jnewbery> ok, times up. Thanks all. Great session
25520:00 <jnewbery> #endmeeting
25620:00 <troygiorshev> thanks jnewbery!
25720:00 <willcl_ark> thanks jnewbery
25820:00 <pinheadmz> good jam today! thanks everyone for being so smart and so polite! 🚀
25920:00 <fjahr> thanks jnewbery
26020:00 <Platapus> That was a good meeting
26120:00 <nehan> thanks!
26220:00 <luke-jr> (doh, off by an hour)
26320:00 <willcl_ark> that was a fun one
26420:00 <Platapus> Thank you
26520:00 <jnewbery> off by one errors are the best errors
26620:00 <felixweis> thanks everyone! jnebery for hosting
26720:01 <emzy> thanks!
26820:01 <michaelfolkson> Thanks jnewbery