Add test for subtract fee from recipient behavior (tests, wallet)

Host: glozow  -  PR author: ryanofsky

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


  • The CreateTransaction() function constructs a transaction based on a list of CRecipient objects. We can think of the resulting transaction as a redistribution of the input coins to new owners in three different categories (not all are mandatory):

    • Recipients: Outputs are created for each of the recipients specified when creating the transaction.

    • Miner: The miner can claim the difference between the transaction’s inputs and outputs as part of their mining reward. While it’s possible to create a transaction with no fee, miners are less likely to mine it and Bitcoin Core nodes won’t accept it into their mempools.

    • Self: The wallet might create a change output back to itself if the inputs exceed the amount needed for the payment(s) and fees. This output isn’t necessarily present in every transaction.

  • Before selecting inputs, the wallet calculates a target amount based on the total payment amount(s) and fees. If a CRecipient has fSubtractFeeAmount=true, the fee is deducted from the payment, and thus included in the target amount instead of added to it.

  • If a change output would be dust (i.e. it’s not economical to create and spend the output because the fee is higher than the amount), it is “dropped” and absorbed into one of the other payments. The expected behavior is to put it back into the recipient output(s) rather than giving it to the miner.


  1. Did you review the PR? Concept ACK, approach ACK, tested ACK, or NACK?

  2. The commit message for the first commit notes “no change in behavior.” How might your review strategy differ based on whether a commit is supposed to change behavior?

  3. What does the CreateSyncedWallet() function do? Are there any other places where it could be reused?

  4. What does it mean to “subtract fee from recipient” when creating a transaction?

  5. What behavior that “might have recently changed in #17331” is being tested in spend_tests.cpp?

  6. What does TestChain100Setup do for us? Why is it needed in spend_tests.cpp?

  7. Why is there an extra :: in front of cs_main here? (Hint: (::) is called a scope resolution operator). Why are these lines enclosed in their own scope?

  8. What is the value of fee set in this line?

  9. What exactly does check_tx do?

  10. The lambda check_tx captures the local variable, std::unique_ptr<CWallet> wallet, by reference, so that it can be used in the lambda function. Why is this capture by reference instead of by value? Hint: to capture the variable var by value, the capture clause (also known as lambda introducer) must be [var] instead of [&var].

  11. Can you think of any other test cases that should be added?

Meeting Log

  117:00 <glozow> #startmeeting
  217:00 <jnewbery> hiiii
  317:00 <glozow> welcome to PR Review Club! :D
  417:00 <dopedsilicon> Hiiiiii
  517:00 <glozow> We'
  617:00 <jomsox> hello everybody!
  717:00 <absently> hi
  817:00 <theStack> hi
  917:00 <sipa> ohai
 1017:00 <stickies-v> hi everyone!
 1117:00 <glozow> we're* looking at PR #22155 Wallet test: Add test for subtract fee from recipient behavior today
 1217:00 <larryruane> hi
 1317:00 <murch> Hello
 1417:01 <glozow> Notes are here:
 1517:01 <S3RK> hi
 1617:01 <glozow> PR is here:
 1717:01 <glozow> Did anybody get a chance to review the PR? y/n
 1817:01 <S3RK> y
 1917:01 <raj_> 0.3y
 2017:01 <glozow> o, is it anybody's first time?
 2117:02 <stickies-v> n (only partially - will mostly be listening/learning)
 2217:02 <merkle_noob[m]> Hello everyone.
 2317:02 <schmidty> hi
 2417:02 <Azorcode> Hello Guys
 2517:02 <jarolrod> fixed the link:
 2617:02 <absently> hello shadowy super coders
 2717:02 <jarolrod> 🥃
 2817:03 <glozow> jarolrod: thank you
 2917:03 <merkle_noob[m]> It's my first time joining in early😅
 3017:03 <glozow> i think the review club website is displaying it with s, that's not the first time i've pasted a bad link :O
 3117:03 <glozow> merkle_noob[m]: welcome!
 3217:04 <glozow> let's start reviewing this PR together :) The commit message for the first commit notes "no change in behavior." How might your review strategy differ based on whether a commit is supposed to change behavior?
 3317:04 <larryruane> review: n (only very little)
 3417:04 <absently> I don't know why my pr comparing script hasn't worked for this PR ?:| git diff HEAD $(git merge-base HEAD master)
 3517:04 <merkle_noob[m]> glozow: Thanks! I hope to learn a lot today🙏
 3617:05 <jnewbery> absently: it got merged this morning, so all the commits are also in master
 3717:05 <glozow> absently: maybe your local master is behind?
 3817:05 <absently> jnewbery ah that would do it!
 3917:05 <glozow> oh right, there wouldn't be a diff if it's in master
 4017:05 <josibake> hi, (sorry a lil late)
 4117:05 <jnewbery> (but that's no reason not to review the PR!)
 4217:06 <absently> it's a handy little script (when it works ;] )
 4317:06 <raj_> absently, you should get the diffs if you compare by commit hashes.
 4417:06 <stickies-v> If a commit claims to not change behaviour, I would focus more on ensuring it actually doesn't. For behaviour changing commits, I think it's important to focus more on potential new vulnerabilities because of the change
 4517:06 <murch> glozow: I would focus more looking on how it improves the existing behavior instead of considering for each line how it might break something in the first pass
 4617:06 <glozow> stickies-v: great answer!
 4717:06 <theStack> for refactoring or "no change in behavior" commits, it's often helpful to pass extra arguments to view the diff, to verify it's move-only... like e.g. --move-colored or --ignore-space-change
 4817:06 <svav> Hi
 4917:07 <b10c> hi
 5017:07 <larryruane> theStack: +1
 5117:07 <glozow> murch: theStack: yeah definitely
 5217:07 <biteskola> hi! nice to be here!
 5317:07 <glozow> i like --color-moved=dimmed_zebra
 5417:07 <glozow> (if it's a moveonly)
 5517:08 <larryruane> is there a reason not to always use those diff options?
 5617:08 <raj_> also can we expect no-behaviour change shouldn't fail any functional test?
 5717:08 <josibake> are move only and "no change in behavior" the same thing?
 5817:08 <sipa> josibake: no
 5917:08 <glozow> larryruane: i guess sometimes whitespace affects the code, e.g. in python
 6017:08 <sipa> there are refactors possible that don't change behavior but possibly substantially change the code
 6117:08 <theStack> larryruane: hm i could image e.g. within strings spacing could be important
 6217:09 <larryruane> josibake: you could replace a linear search with a tree search, and that wouldn't be move-only
 6317:09 <murch> josibake: No!
 6417:09 <sipa> josibake: move-only commits are just a subset of no-behavior-change ones (and a subset that's particularly easy to review)
 6517:09 <sipa> even just comments/documentation changes are not move-only
 6617:09 <larryruane> well I guess, is performance change a no-behavior change??
 6717:09 <jnewbery> raj_: functional tests should *always* pass on all commits
 6817:10 <sipa> larryruane: debatable; i'd call it no observable behavior change :)
 6917:10 <murch> raj_: I think that the expectation is that every commit should pass all tests
 7017:10 <raj_> jnewbery, murch ah silly me..
 7117:10 <glozow> i agree^
 7217:10 <sipa> easiest approach: first delete all the tests *hides*
 7317:11 <jnewbery> step two: delete all the code
 7417:11 <jnewbery> no bugs
 7517:11 <sipa> :D
 7617:11 <b10c> no review club either :(
 7717:11 <absently> :C
 7817:11 <glozow> we can review remove-only PRs
 7917:11 <dopedsilicon> :(
 8017:11 <jnewbery> b10c: ah, good point. Let's not do that then
 8117:12 <glozow> ok next question: What does the `CreateSyncedWallet()` function do? Are there any other places where it could be reused?
 8217:12 <merkle_noob[m]> So if I understand correctly, an analogy could be like breaking a large class into a set of small classes/interfaces, etc while ensuring that the code behaves the same way functionally... Please correct me ifI'm wrong.
 8317:13 <glozow> merkle_noob[m]: yeah, that's probably an example of a no-behavior-change change
 8417:14 <S3RK> It creates a new CWallet with mock db and syncs it to the test chain tip
 8517:16 <glozow> merkle_noob[m]: maybe somewhat relevant. in bitcoin core, i've seen a lot of PRs that first do a bunch of refactors, then 1-2 commits changing behavior and it makes stuff much easier to review
 8617:16 <glozow> S3RK: yep!
 8717:16 <stickies-v> S3RK: arguably it's not really the CreateSyncedWallet() function that does the mocking and syncing though, if my understanding is correct?
 8817:17 <S3RK> yes, it calls other funcs to achieve that :)
 8917:18 <glozow> next question: What does it mean to "subtract fee from recipient" when creating a transaction?
 9017:18 <S3RK> not sure about the second part of the question though. Maybe it could be reused in other test modules?
 9117:18 <merkle_noob[m]> glozow: I see... Thanks for the info...
 9217:19 <larryruane> `CreateSyncedWallet()` returns a `std::unique_ptr<CWallet>` -- is my understanding correct that this is similar to a `new` (allocates memory) but is somehow preferrable?
 9317:19 <svav> It means the recipient pays the fee, so it's deducted from the transaction amount that they were going to receive.
 9417:19 <raj_> it means the recipient pays for the fee.. noob question: is this always true?
 9517:19 <larryruane> (oh sorry, we had moved on)
 9617:19 <glozow> larryruane: no worries, everyone should feel free to ask any question at any time
 9717:20 <S3RK> raj_ it's not always the case
 9817:20 <josibake> S3RK that was my understanding, that it's a utility to use any time you want a .. synced wallet, to avoid repeating the calls to the other functions
 9917:20 <murch> glozow: The recipient amount that's specified in the transaction amount is reduced by the amount of fees the transaction pays. If there are multiple outputs with this instruction the fee is distributed equally among them (iirc).
10017:20 <raj_> Oh ya.. CRecipient it has a bool flag to decide that..
10117:20 <theStack> when sending amount n and a txfee fee, the recipient receives (n - fee). normally the recipient would receive n and the fee is deducted from the sender
10217:21 <murch> raj_: It should be false by default
10317:21 <sipa> larryruane: you know what a unique_ptr does?
10417:21 <stickies-v> S3RK: sorry had a closer look, you're absolutely right about the mocking and syncing
10517:21 <glozow> make_unique will allocate it in dynamic memory (like `new` but not exactly the same thing) and return a `std::unique_ptr` which "owns" that piece of memory and will handle releasing it when it goes out of scope
10617:22 <glozow> murch: theStack: raj_: good answers
10717:22 <glozow> followup question: what happens if there are multiple recipients in the tx?
10817:22 <larryruane> glozow: I see, that's definitely better (in general) prevents memory leaks
10917:22 <glozow> larryruane: and we'll discuss it more in a later question :D
11017:22 <raj_> thanks murch , yes we are turning it on in the test..
11117:23 <sipa> glozow: make_unique pretty just calls new under the hood and feeds it to the unique_ptr constructor
11217:23 <raj_> glozow, it should distribute the extra equally?
11317:23 <larryruane> sipa: I think the `unique_ptr` class prevents copying the pointer, so there's no need to keep a reference count (IIUC)
11417:23 <sipa> larryruane: that's the difference with shared_ptr
11517:24 <jnewbery> larryruane: if you like learning from books, I'd stronly recommend Effective Modern C++ by Meyers. There are a few chapters in there about smart pointers (unique_ptr and shared_ptr)
11617:24 <sipa> raw pointers don't have any management; you're responsible for cleaning them up yourself
11717:24 <larryruane> jnewbery: sipa: thanks, will do
11817:24 <glozow> raj_: yeah. wonder where that code is
11917:24 <jnewbery> (although std::make_unique<T>() wasn't introduced until C++14, so it's not covered in that book)
12017:25 <glozow> aha:
12117:25 <sipa> larryruane: a unique_ptr is really just a wrapper around a raw pointer in practice, but it (a) prevents copying as you say and (b) automatically destroys the object when the unique_ptr goes out of scope, so you don't need to worry about calling free yourself - it's said that the unique_ptr "owns" the pointer
12217:25 <glozow> jnewbery: no, i'm pretty sure it's covered
12317:25 <glozow> that's the book with the peacock on the cover right? there's a chapter on `new` vs `make_shared` i think
12417:26 <sipa> glozow: make_shared is in c++11; make_unique is not
12517:26 <absently> sipa what did you mean by "calling free yourself"?
12617:26 <sipa> absently: i'm wrong; i meant calling "delete" yourself
12717:26 <jnewbery> glozow: ah ok, I'm sure there are some subsequent changes to smart pointers that weren't available when that book was published. Can't remember exactly what
12817:26 <larryruane> glozow: you're right, beginning on page 118
12917:26 <glozow> larryruane: hohoho
13017:27 <sipa> (make_shared is also a lot more interesting than make_unique; you can't implement make_shared with the same efficiency yourself; make_unique is literally just new + unique_ptr constructor)
13117:27 <jnewbery> I've been shown up in my knowledge of the Effective C++ books 😳
13217:28 <murch> jnewbery: Next someone will beat you at Carcasonne
13317:28 <merkle_noob[m]> glozow: So based on the code, it does fee subtraction equally for all recipients.
13417:28 <glozow> merkle_noob[m]: yep
13517:28 <glozow> next question: What behavior that "might have recently changed in #17331" is being tested in spend_tests.cpp?
13617:29 <josibake> glozow: if im reading the code correctly, cant the first recipient end up paying slightly more?
13717:29 <glozow> or, what exactly is spent_tests testing?
13817:29 <merkle_noob[m]> glozow: I was instead thinking that it calculated the fee based on the amount sent to each recipient, and then carried out fee subtraction.
13917:30 <jnewbery> sipa: I'm not sure I'd say that the unique_ptr "owns" the pointer, rather that the unique_ptr "owns" the object that the pointer points to (ie is responsible for its lifetime and releasing resources when it's no longer needed)
14017:30 <glozow> josibake: right, any remainder is paid by the first recipient
14117:30 <raj_> glozow, the test is ensuring that dust changes are added to the recipient, not in fee.. Although I am not sure if thats something that was changed in #17331
14217:30 <glozow> btw, we also did a review club on #17331 if y'all are interested:
14317:30 <glozow> was hosted by murch
14417:30 <sipa> jnewbery: fair point
14517:32 <glozow> raj_: right, what is "dust change" ? :)
14617:33 <raj_> glozow, a change that is uneconomical to spend.
14717:33 <murch> When the excess of the input selection beyond the sum of recipient outputs and fees is smaller than the cost of creating a change output
14817:33 <absently> glozow change that is below a threshold
14917:33 <murch> WEll, actually smaller than creating and spending the change
15017:33 <glozow> raj_: right, so we wanted to make a change output, but then we realized it was such a tiny amount that it would cost more to spend it
15117:34 <glozow> so we decide we're not going to make the change output afterall
15217:34 <glozow> what happens if we just drop the output? who gets that money?
15317:35 <raj_> follow up question, the dust amount in test is 123, is it just random?
15417:35 <stickies-v> glozow: the miner does
15517:35 <glozow> stickies-v: correct
15617:35 <glozow> is there a better way to allocate those funds?
15717:36 <larryruane> could conceivably burn it, then it would go back to everyone
15817:36 <glozow> (in a tx where we're subtracting fees from recipients)
15917:36 <larryruane> (in effect... smaller total supply)
16017:36 <S3RK> depends on how we define "better" but there are other ways
16117:36 <theStack> larryruane: interesting idea :)
16217:37 <glozow> raj_: i believe the 123 is arbitrary
16317:37 <glozow> well, it's definitely small enough to be dust
16417:37 <stickies-v> probably we'd prefer the recipient to pay slightly less fees given that we're transacting with them?
16517:37 <glozow> but i imagine 120 would have been fine too
16617:37 <glozow> stickies-v: exactly. subtract less from the recipients
16717:37 <murch> raj_: Usually the dust limit is calculated from `(input vsize + output vsize)*3`, so it seems to be arbitrary
16817:37 <glozow> that's the behavior being tested here
16917:38 <glozow> any questions about this?
17017:38 <murch> larryruane: burning it would require creating an ouptut, tho
17117:38 <raj_> glozow, in the last test then, we are testing with to_reduce = fee + 123, If 123 is random, I wonder how far we can increase it before the test fails, ie. it creates a change output.
17217:39 <raj_> it failed at 1000, what should be the bound here?
17317:39 <raj_> fee is 1340..
17417:39 <absently> larryruane destroying money/wealth reduces our capacity to express our needs, using the money to induce block production is long-term incentive compatible with bitcoin operation
17517:40 <glozow> raj_: nice testing!
17617:40 <glozow> and yeah, 1340 is the answer to q8
17717:41 <murch> raj_: The dust limit for p2wpkh should be 298 sat/vB, iirc.
17817:41 <raj_> glozow, Ah sorry for spoiler.. :D
17917:41 <josibake> similar to how the first recipient gets the most subtracted, couldn't you just give back to the first recipient if there is a dust change?
18017:41 <larryruane> absently: yes, I'm not saying burning is a good idea, just theoretically possible (but as murch says, that would require an output anyway) ... but many people mistakenly think that destroying money is actual waste, but it is not, like even with fiat, if you burn a $100 bill, you're making everyone else slightly better off
18117:41 <josibake> seems like it would balance out for the first recipient
18217:41 <glozow> raj_: not a problem at all :P good testing
18317:42 <raj_> glozow, murch, does it make sense to test this bound in the test also?
18417:42 <absently> larryruane seems we have different opinions - that's fine :)
18517:42 <murch> josibake: I thought the first recipient only pays the remainder additionally if it doesn't cleanly divides by the `n` recipients
18617:42 <glozow> josibake: i think the first recipient paying remainder is inevitable and a pretty insignificant amount, but when we're refunding we also would want that to be somewhat equal
18717:43 <murch> glozow: Yeah, tthat's what I was trying to say
18817:43 <murch> But you put that much more clearly
18917:44 <glozow> murch: tanks tanks
19017:44 <josibake> glozow: that makes sense, i wasn't thinking of the relative size of the two. dust could actually be worth quite a bit more which is why redistributing is better?
19117:44 <murch> raj_: It should be tested where the dust limit is enforced. I don't think it would be good practice to test behavior explicitly here that isn't in the purview of the tested function
19217:45 <theStack> absently: i don't think a decrease in money supply is a problem at all (i think austrian economists pretty much agree that the total money supply doesn't matter); if it is, we would have a serious problem, lots of private keys will get lost forever
19317:45 <theStack> (sorry for off-topic :x)
19417:46 <murch> josibake: If you have three recipients that you divide the fee among, the first will pay up to 2 sats more. But dust will be up to 297 sats even for the most blockweight efficient output type currently used on the network
19517:46 <larryruane> even satoshi (although i agree not infallable) wrote something about unspendable outputs being a gift to everyone (i don't have a reference handy)
19617:46 <murch> (Yes, ...)
19717:46 <glozow> josibake: yeah, the dust here is 123 satoshis. whereas i'm pretty sure if you have 3 recipients, at most the first recipient is paying 2 satoshis extra 🤷
19817:46 <glozow> murch: oops i said what you said better this time
19917:46 <murch> :D
20017:47 <glozow> okay i wanna make sure we get to the c++ questions. Why is there an extra :: in front of cs_main?
20117:47 <glozow> here:
20217:47 <josibake> glozow, murch: thanks, real numbers help haha
20317:47 <raj_> murch, yes that makes sense, but in the last test we are checking that even its ok to over pay the recipient, so it might make sense to check we are overpaying upto the max bound, instead of a random extra.
20417:48 <larryruane> glozow: does that emphasize it's a global variable, and also keeps it from being confused with an object member?
20517:48 <larryruane> I don't think there are any object members called `cs_main` so only the first reason applies? I've always wondered this
20617:48 <raj_> glozow, I always wondered but never dared to ask..
20717:49 <larryruane> raj_: I DOUBLE DOG dare you! (haha0
20817:49 <absently> glozow in order to define a function outside a class
20917:49 <murch> raj_: By using an arbitrary limit rather than the actual number the test remains as good as it is even when the actual number changes
21017:49 <raj_> random guess, is it because they are defined in the current namespace?
21117:49 <S3RK> raj_ I think this can make the test more fragile as it makes it dependent on unrelated implementation details
21217:49 <glozow> `::` is the scope resolution operator
21317:50 <glozow> we're not defining a function here
21417:50 <larryruane> murch: Yes, I think we don't want to make tests to fragile, right?
21517:50 <absently> oh >_<
21617:50 <murch> raj_: It's also nice to test things with various numbers so you don't end up having some hidden behavior where it only ever works for a specific value
21717:50 <raj_> murch, larryruane yes that makes sense.. thanks..
21817:50 <glozow> defining funciton outside class would be something like this:
21917:51 <glozow> here, we're inside a local scope and want to clarify that we're using a variable defined outside the scope
22017:52 <S3RK> is it required tho? are there multiple options to resolve it?
22117:52 <absently> ah that helps me ty
22217:52 <glozow> murch: i agree. i assume that's also why it sets `fOverrideFeeRate=true`
22317:52 <glozow> S3RK: I thiiiink it would still work if you removed it
22417:53 <larryruane> But why do we see `cs_main` in so many places without the `::`?
22517:53 <glozow> this particular test i mean
22617:53 <raj_> glozow, that outside scope is which one? The one immediately out or any parent scope of the current scope?
22717:53 <glozow> tbh i am not sure
22817:54 <larryruane> is it the case that all _new_ instances of `cs_main` should be `::cs_main`?
22917:55 <glozow> larryruane: no, i think it depends on the scope of the code
23017:56 <larryruane> if I may ask one other question as we're close on time (feel free to ignore), why is `check_tx` a lambda, instead of a normal function declared just before the function that calls it? just to reduce its scope to where it's needed, to keep it close to where it's used? I like the idea, just curious about the reason(s)
23117:57 <glozow> ah good point, lemme ask my favorite question before we run out of time: The lambda check_tx captures the local variable, std::unique_ptr<CWallet> wallet, by reference, so that it can be used in the lambda function. Why is this capture by reference instead of by value?
23217:57 <raj_> glozow, because it would drop the wallet at return if we passed by value?
23317:58 <larryruane> glozow: is it because `check_tx` modifies the wallet object?
23417:58 <glozow> raj_: _can_ we pass the wallet by value?
23517:58 <larryruane> (also it's more efficient, but that's not so important in test code)
23617:58 <sipa> because you don't want to copy the entire gargantuan wallet object?
23717:59 <sipa> also it'd lose the transactiont that was created
23817:59 <S3RK> larryruane my guess is that it's lambda to confine it to the scope of this particular test and not the whole file which could contain more different tests
23917:59 <larryruane> (it may not modify the wallet, now that I look at it again)
24017:59 <larryruane> S3RK: +1
24118:00 <raj_> glozow, we cant? yes there is an error, but i don't understand what it says..
24218:00 <glozow> hint: `wallet` is a `std::unique_ptr<CWallet>`
24318:00 <sipa> oh.
24418:00 <raj_> ohhh.. right..
24518:00 <sipa> then it's obviously not possible; i should have checked the code first
24618:00 <glozow> teehee
24718:01 <jnewbery> passing by value makes a copy of the thing being passed
24818:01 <glozow> yep, you can't pass a copy of the unique pointer
24918:01 <raj_> CreateSyncdWallet returns unique pointer.
25018:01 <glozow> i imagine that's also why it's a lambda instead of a helper function
25118:01 <glozow> oh oops we're out of time!
25218:01 <glozow> #endmeeting
25318:01 <jnewbery> Right, unique_ptr doesn't have a copy ctor (because if it did it wouldn't be unique!)
25418:02 <glozow> exactly
25518:02 <larryruane> on line 19 the lambda is declared as an `auto`, maybe we can discuss next time, I'm never sure if it's better to use `auto` or write out the type
25618:02 <jnewbery> thanks glozow!!
25718:02 <larryruane> jnewbery: glozow: great answers, thanks
25818:02 <raj_> thanks glozow for hosting, really great one to dig into, learned a ton..
25918:02 <glozow> larryruane: i think that's chapter 1 of effective modern c++!
26018:02 <absently> thanks glozow et al
26118:02 <theStack> thanks for hosting glozow
26218:03 <larryruane> ah ok.. thanks glozow this was great! thanks to everyone!
26318:03 <glozow> also, that lambda can be a `const auto`
26418:03 <josibake> thanks everyone, still a c++ n00b so this was super helpful
26518:03 <stickies-v> a lot of new stuff for me today, thanks for hosting this very useful session glozow and everyone else for contributing!
26618:03 <svav> Thanks glozow and all
26718:03 <josibake> jnewbery: gonnat grab a copy of effective c++ :)
26818:03 <S3RK> thank you for hosting!
26918:03 <jnewbery> josibake: it's a great read :)
27018:03 <sipa> josibake: make sure it's not a unique_ptr<effective c++>
27118:03 <glozow> thanks everyone :D glad that people were willing to dig into some c++
27218:03 <biteskola> thanks! :)
27318:03 <merkle_noob[m]> Thanks glozow, and to every other person who participated. Learnt a ton...
27418:04 <larryruane> sipa: 🤣
27518:04 <josibake> sipa: lol
27618:04 <murch> Thanks for hosting!
27718:04 <glozow> sipa: 😂