Last active
January 3, 2025 04:37
-
-
Save supertestnet/f08f2e128769ada9a3e31be8fa046c60 to your computer and use it in GitHub Desktop.
In this document, I spend about 15 pages, written over the course of a few weeks, thinking up a way to do a non-interactive coinpool without any consensus-changes to bitcoin. It is a glimpse into my thought process.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Recently, while discussing covenant proposals on twitter, I had this conversation (slightly modified): | |
------------------------------------------------------------------------------------------------------------------------------- | |
Me: Several [covenant] proposals make privacy pools like TornadoCash easier to build on bitcoin so...get on board for the "more privacy" reason! (I think you can build a TornadoCash-like privacy pool with any of the following: op_ctv, op_txhash, op_vault, op_cat, or op_paircommit) | |
Other person: Just a dumb question, a proposal like coinpool or joinpool (tornado cash like privacy pools on CTV) AFAIK requires all the users to stay online so it is possible to interact with the pool. Is it possible to make it tornado cash like, meaning I can open the wallet, deposit into the pool, turn off my PC, and after months I can withdraw the funds? | |
Me: Tldr: yes I think its possible. Long answer: I think I could make that kind of pool with three primitives: blind signatures, transferable state, and rebindable signatures. Each user would *acquire* a blind signature when depositing and provide the unblinded version when withdrawing. The utxo script needs to validate the unblinded signature to ensure (1) they did make a deposit (2) they are withdrawing whatever amount they deposited. If that checks out, the UTXO script should update some state so that it knows this sig can't be reused, and ensure the *new* UTXO (containing the change, i.e. the money owned by every other user) is encumbered by the new state. | |
If, between depositing and withdrawing, the pool utxo changed (e.g. because another user withdrew while our depositor was offline), the unblinded signature would need to be "rebindable," which means it still "works" even if the utxo changed as long as the script remains the same. I think CTV+CSFS gives us rebindable signatures (so does op_cat or APO -- and I think they can often be emulated *today* with sighash_single | anyone_can_pay) and I think bitvm and connector outputs have demonstrated that we already have transferable state. | |
The other factor is blind signatures, and while none of these proposals give us those (well, maybe op_cat, since it seems omnipotent), I do think they can be emulated too, right now, with either sighash_single or sighash_none. (Those allow you to sign a transaction without knowing what outputs you're signing -- making you blind as to who the recipient or recipients will be.) All of which means, yes, I do think at least some of these covenant proposals make non-interactive pools possible, and I also think they may be possible *today* using sighash tricks to emulate blind sigs and rebindable sigs. So I have to think about this some more. | |
------------------------------------------------------------------------------------------------------------------------------- | |
Time to think about this some more. | |
This protocol requires a signer who I will call the coordinator, though he is actually an n-of-n consisting of all depositors. During a “deposit ceremony,” an n of n multisig needs to be created where n === the number of depositors (each depositor has a key), and each depositor needs to tell the others what amount they will send into this multisig (the pool). Then they each need to get a sig from the coordinator that sends them their own money and deposits the change back into the pool. This sig needs to be rebindable; that is, as long as the pool contains enough money, the sig should work for the purpose of giving the user their amount and depositing the remainder in the pool; it should not require spending a particular utxo. | |
hmm…I’m not sure that part’s possible. I think even a rebindable signature (i.e. one which doesn’t care about the inputs) still needs to say what amount each output should receive. I can certainly give the user their amount (that part is known when the signature is created), but the change amount depends on how much was in the pool when the user withdrew. Since I don’t know what the change amount will be, I don’t think I can create a sig that always puts the right change amount back into the pool. | |
I wonder if this is salvageable on the following basis: suppose you got multiple sigs from the coordinator, where each sig is only valid if a connector output exists. Initially, a connector output exists that only makes a subset of signatures valid, the ones that allow each user to withdraw the amount currently owed to them. And if any user withdraws using the sig that’s valid for them, their withdrawal transaction *also* creates a connector that makes the *next* series of sigs valid. These sigs allow each party *other than* the previous withdrawer to withdraw their own amount, and create the next connector, and so forth. | |
A problem with this idea is exponential blowup. If there are 5 people in the group, 5 sigs need to be valid to start with, and each of those 5 needs to make another 4 sigs valid for the next step, so 20 plus the original 5 = 25 sigs need to be valid; and each of the 20 sigs need to make 3 sigs valid for the next step, so 60 plus the previous 25 = 85 sigs need to be valid; and each of the 60 sigs needs to make 2 sigs valid for the next step, so 120 plus the previous 85 = 205 sigs need to be valid; and each of the 120 sigs needs to make 1 sig valid for the next step so 120 plus the previous 205 = 325 sigs need to be valid to allow 5 people in the pool. The more members there are, the worse the exponential blowup gets. | |
The exponential blowup happens because if party A withdraws, a different connector is made than if party B withdraws, and so forth, which means, for each party to have a guaranteed withdrawal, they need a sig that spends every connector output that any party in any step might create. But there might be a way to reduce the exponential blowup by ensuring that every party must create the *same* connector output. | |
But maybe I can fix the exponential blowup like this: imagine a UTXO which *must* be spent in a series of ten transactions, where each one always peels off a dust output and puts the remainder in a change output, ready for use in the next transaction of the ten-transaction series. Each dust output can be used as the connector for every withdrawal, and the next user who wants to withdraw just has to make the next dust output. That allows for ten unilateral withdrawals. So as long as there are only ten users, if each user has ten sigs that each give them their money no matter what round is in play, we're good. | |
This still requires rebindable sigs; each withdrawer must have a sig that somehow allows them to withdraw their own amount *and* put the rest back into the pool, regardless of how much is left; this tx will create a new pool utxo, and every user’s ten withdrawal sigs must not “care” which of those UTXOs they are spending. Which is hard when the amount might be any of 10! different variants. | |
This seems a lot easier if the withdrawal amounts are fixed. Every user deposits 50k sats and can only ever withdraw 50k sats. Then all sigs can be designed to put the right amount back into the pool and it no longer matters in what order people withdraw. | |
But what good is a pool with fixed amounts? Well, I think a facilitator Fred helps here: if the 50k, when withdrawn, always goes into a 2 of 2 of user + Fred, a presigned tx can let the user + Fred divide up the funds between them, so that a user who gave 40k sats to some other user (through Fred) withdraws their original 50k, but then out of that they only get 10k; Fred gets the remainder. | |
This seems to allow for feeless transactions within the pool. (Except Fred would probably charge a fee.) This also requires a way to increase the balance of whoever you sent 40k to. Maybe Fred shouldn’t get the 40k, instead it should be divided up among all the people you sent money to in the pool. But that seems to require them to trust Fred, because the 50k goes into a 2 of 2 of user + Fred, and nothing stops Fred from colluding with the user to take all 50k. Maybe Fred could match every 50k deposit so that all users can withdraw 100k, divided up 50/50 between them and Fred. And if user A sends 10k sats to user B within the pool, we just modify 50/50 for a to 40/60, and modify user B to 60/40. | |
------------------------------------------------------------------------------------------------------------------------------- | |
10 users: A, B, C, D, E, F, G, H, I, J | |
Facilitator: Z | |
Deposit ceremony: | |
A—J say “here are our deposit utxos, each worth 50k sats or more.” | |
A—J say “here are our peeler utxos, each worth 330 sats or more.” | |
A—J say “here are our dust utxos, each worth 330 sats or more.” | |
Z says “here is my matching deposit for each of your deposit utxos.” | |
A—J + Z create an n of n multisig where n = 11 and every user has a key, plus the facilitator | |
A—J prepare a coinjoin that consumes the deposit utxos and the peeler utxos and puts a utxo worth 3300 sats (330 * 10) in the multisig and another utxo worth 1 million sats (100k * 10) in the multisig – but they don’t all share the coinjoin sigs yet | |
A—J + Z prepare a set of 10 peeler transactions; the first consumes the 3300 sat peeler utxo, creates a 330 sat “peel” output, and puts the remainder (2970) back in the multisig; the second consumes the 2970 sat peeler utxo and does the same; etc. 10 times, til nothing is left. | |
Once all of the peeler transactions are prepared, A—J still do not share the coinjoin sigs; instead, they move onto the next step | |
A—J + Z prepare some withdrawal transactions; each one spends a peel output + the money in the multisig; the first 10 each put 900k sats back in the pool along with a separate utxo worth 100k sats; they are signed by A—J + Z; A—J + Z also sign 10 transactions spending the utxo worth 100k sats; each one puts it in A + Z or B + Z, etc., but it requires two signatures: A—J + Z is one and the other is A, or B, or C, etc, depending on who is withdrawing | |
The utxo worth 100k sats has 10 script paths, each one looks like A—J + Z + A, A—J + Z + B, etc. A sig by A—J + Z is created for each script path that only works if that user’s dust utxo exists. When any user withdraws, they use their script path AND spend their dust utxo. Doing so effectively invalidates all of their future script paths, so they can’t replay their withdrawal. This allows the withdrawer to ensure the 100k sats go to an address of Z + themselves; where it is divided up 50/50 to start with, but they can modify that to be 40/60 or whatever, allowing internal transfers. | |
Note that, since the script for the 100k sat utxo has only 10 script paths, no one can troll the withdrawer except one of other members of the pool, and they can only do so in the following manner: suppose the withdrawer is Jerry; any user other than Jerry can troll Jerry by “overwriting” Jerry’s attempt to move the 100k sat utxo to Z + J. They overwrite his tx by broadcasting a tx from a *different* script path that moves the money into Z + <other user> instead. But to do that, the other user has to spend their own dust utxo, and overwrite the tx that would have spent Jerry’s dust utxo. Which means Jerry’s other withdrawal transactions are still valid, so he can just withdraw using the “next” peel output. So it’s no big deal. | |
Pretty sure the above solution requires rebindable signatures and although I’m told you can do that with CTV + CSFS, I don’t know how | |
------------------------------------------------------------------------------------------------------------------------------- | |
New idea | |
Have n people prepare and sign a coinjoin that funds an n of n multisig with 100k sats apiece. Every user has one of the keys to this n of n multisig. | |
Before the users share their coinjoin sigs with one another, they should generate n presigned txs and each user should get a copy. Each presigned tx defines a “round” (e.g. round 1, round 2…round n) within which a 100k sat utxo is created using the pooled funds, and the remainder, if any, returns to the multisig, ready for use in the next round, if any. Each 100k sat utxo (one is generated in every round, so there are n of them) has n script paths, each of which lets a different user move that 100k sat utxo into a “withdrawal address” they shared when they entered the pool. But the sig they need in order to do that also requires them to spend a unique connector output too, which is also locked to the pool’s n of n multisig. This connector output prevents that user from withdrawing in a future round; the pregenerated signatures that allow them to withdraw in any round all require them to spend that connector output, so once it is spent (i.e. once they’ve withdrawn), all their signatures for future rounds effectively become invalid, meaning they cannot withdraw again. | |
This scheme allows n people to enter a pool and exit the pool with all their funds, but it does not, by itself, allow them to *use* the pool in the interim. To allow that, I propose that the withdrawal address submitted by each user when they entered the pool be a 2 of 2 multisig between the user and a routing node. Before entering the pool, the user should get signatures from the routing node allowing them to withdraw all their money from the 2 of 2; observing that there are n different ways that utxo might get created, so their channel funding tx might have one of n different txids, so the user should get n different signatures from the routing node, all of which let the user withdraw the same amount from that channel. Whenever the user makes a transaction in this channel, they need to perform n different identical state updates. | |
The exit mechanism described above is the “sad path” that a user can use if their routing node disappears. If their routing node sticks around, there is a faster, cheaper, and more efficient “happy path” way for users to exit the pool, which is to just exit by sending all their funds out via lightning, leaving their pooled utxo entirely in the hands of whatever routing node they were connected to. Routing nodes are expected to maintain constant uptime, so they can do the following protocol to efficiently close out the pool without broadcasting all the “sad path” transactions. | |
Whenever a routing node acquires full control of a pooled utxo (i.e. its user exited via the happy path), the user should give its privkey to the routing node as part of exiting. The routing node can then communicate with the other routing nodes that it is ready to exit when they are. If the routing node needs to exit immediately, it can use the sad path; otherwise, it can wait til all the other users have exited; if some users are unresponsive, their routing nodes can broadcast their “sad path” transactions to eject them, leaving only “responsive” routing nodes left. But even then, they cannot “straight up” take the funds from the n of n because they don’t have keys from the unresponsive users, since they exited via the “sad path.” So I need to think about what to do in that case. | |
After considering it more, I wonder if I can do something with the connector outputs. Those get spent whenever a user exits; so it seems possible to do this: when a connector is spent, create *another* connector output; the existence of *that* connector output proves its corresponding person exited via the sad path. Now, suppose the pool started out with 10 people, where 8 exited via the “happy path” and 2 exited via the “sad path.” I can prove the 2 exited via the sad path, so if I can somehow prove that the other 8 exited happily, I think I can design a script that lets their channel partners move the funds without the help of the 2 sad pathers. The fact that the 8 are signing to move the funds should prove they exited happily; so maybe I need a way to “count” how many exited sadly and get that number onto the stack. If the number signing to move the funds plus the number who exited sadly is n (i.e. the total), then and only then, let the remaining people move the funds without the approval of the sad pathers. But isn't my only proof of sadness a connector output? I don't think I can get the existence of that UTXO onto the stack…unless maybe I get a signature of it that is sighash_none | anyone_can_pay. Such a signature apparently only commits to the existence of one UTXO, so it should be the same regardless of which tx it shows up in. So if I can get the user to pregenerate a sig for that UTXO with those sighashes, then the sig can be used in any tx, and if it is valid, it proves the existence of that UTXO, and can be counted and I can do math on it. Huzzah! | |
Oh, wait. That signature will only be valid for that input. So I can only count to 1. If I have two inputs from Sad Pather Henry and Sad Pather Jane, both sad pathers can have a connector UTXO that gets spent in my tx, but their sigs proving they are sad pathers are only valid in their own inputs, because they use snyone_can_pay. This means Jane’s input can't know about Henry’s and Henry’s can't know about Jane’s. If I fix that by dropping anyone_can_pay then I must get them to pregenerate sigs for every possible combination of inputs that might prove some combination of users were sad pathers, which sounds like a combinatorial explosion. | |
I had something in mind but I forgot what it was. I think it had to do with the fact that ejection transactions are signed by all parties and the full set of sigs needed to eject someone are held by at least the user and their channel counterparty. Maybe there's a way to penalize all routing nodes if they collude to say someone was ejected when they weren't. But it's even better if I ensure they cannot falsely say someone was ejected. A transaction that moves funds from the n of n and isn't an ejection tx should be impossible unless it contains proof that the ejected parties alone (if any) are not participating in the movement of funds. | |
There's also the fact that a connector not only gets *created* when an ejection occurs; a connector also gets *destroyed.* Is there a way to get proof, on the stack, that a particular utxo *no longer* exists? Or is there maybe a way to make this movement transaction impossible *without* doing something on the stack? They normally need 10 sigs to move the funds; I've been trying to reduce that to 8; what if I keep it at 10 but have 2 of the sad pather sigs only validate if their “sad path worked” UTXO exists? If the sad sigs all use sighash_single then I can ensure only 240 sat dust outputs are created for them; they don't care where the rest of the money goes so the happy sigs can send it wherever they agree to. But this also means the sad path sigs have to sign for every input consumed in this tx – which is variable depending on who becomes unresponsive, so it sounds like a combinatorial explosion. | |
I think there's an easy solution: since this protocol assumes every user has a hedgehog channel, every user has to come online once in a while. So just use optimism. The channel partners can move the n of n funds at any time to a pre-approved location but only if they put up n bonds worth 100k. If any user was not ejected, their connector still exists, so they can use it to collect their bond. Ejected people can't but don't need to because they already exited. But um…this means the whole pool is pointless because it inevitably results in as many on chain channels as there are users, and the point of the pool was to avoid that. | |
What if, when you get ejected, you have to reveal a secret within 2 weeks, otherwise your channel partner can take your money? Then the secret proves which parties were ejected and I can count those. But if a party does not reveal their secret and loses their funds, I need a backup way of proving they were still ejected. | |
What if I use yet another connector to keep track of what round I'm in? If connector A exists we are in round 1 and need all 10 sigs to move the funds; if someone is ejected, connector A creates connector B, and if connector B exists we only need 9 signs to move the funds. But what stops Alice from exiting with 100k sats and then signing with Carol onwards to move everyone else’s money, including Bob’s, and split up Bob’s amongst one another? Can I make it so Alice can only sign if her connector still exists? | |
Maybe I can do this: everybody creates a sighash_single sig at the beginning that is only valid if a 100k sat utxo is created at index 1 and puts that user’s money in the one where they can leave with it. This means their sig will be one byte longer than the default sig. So we can allow single ejections by counting the lengths of each sig and making sure only one is bigger than 64 bytes. That bigsig still requires spending the user’s connector. Not sure what to do next. | |
Can’t I have a 10 of 10 multisig that also has a script for 9 of 9, 8 of 8, etc., but where each of those scripts is “guarded” by a requirement to show a valid sig proving which one of the ten got ejected? I think so, and I also think that valid sig can be generated during the deposit ceremony, and I think I can also “know” which user was kicked out by making the “kickout” transaction produce one of ten different connectors, such that whichever sig is valid lets the script know which user left. So if my users are A, B, C, D, E, F, G, H, I, and J, I can have the funds go from a 10 of 10 with 10 connectors (A—J) to a 9 of 9 with only B—J or only A—I, or any other person can be left out. | |
Another thought: I wondered for a sec if I could keep it as a 10 of 10 multisig and have each user produce 10 sigs of kind sighash_none. That would mean that once they leave, the other users can pass in their “next” sighash_none sig to allow the chain to continue without the ejected user needing to sign anything new. And since sighash_none sigs are sized differently, I can even order the pubkeys and tell, in each round, which users were ejected (because their sigs are bigger) and which remain. But this requires the sighash_none sigs to spend connectors that are only created after the user leaves the group, otherwise they can be stolen from by having everyone pass in their sighash_none signature *before* they've left. And it's less efficient than my previous idea anyway so I just mention it here because it's got some interesting characteristics that I might find useful later. | |
Actually I might find it useful right now: I forgot that I need a way for the routing nodes to disperse the remaining funds efficiently after ejecting all the sad pathers. I need a script that lets them prove who left so that the remaining keys can spend the money without them. If I order the pubkeys A—J, and pass in sighash_none sigs for the sad pathers (which prove that user left, because it isn't valid otherwise), I can drop keys whose sigs were bigger than 64 bytes; the remaining keys belong to the happy pathers, so I can count them and require that amount of sigs to sign the tx that disperses the funds to the routing nodes. | |
But how to make a sig that is only valid if you got ejected, if it *also* has to allow *other ejected folks* to pass in their sigs to the same transaction? I don't think these sigs can commit to connectors because no one knows which connectors will be unspent when they make the sigs. Maybe I could somehow use several 5_byte_CSFSs instead of a “bitcoin” signature – though I don’t think those can commit to any transaction data except timelock data | |
Wait, I might have just gotten it. When a routing node picks someone to eject, they must initiate the ejection by spending from the multisig to create a dust output in address A and put the rest of the multisig funds back in the multisig. (That is tx1.) The routing node may then use a pregenerated sig to spend the dust output they just created, but only if, with it, they create one of 10 connectors, each of which is unique to whoever they wish to eject. (That is tx2.) Then they may use a pregenerated sig to spend the money in the multisig *with* that connector to eject one person (in tx3) – and that person cannot be ejected twice because now their connector is gone, and it was required no matter what round they “left” in. | |
Ok but still, how can I ensure the happy-pathers may spend the money in the multisig *without* the sad-pathers once all the sad-pathers are gone? Somehow I still need to *detect* that a sad pather is gone. | |
What if the 10-of-10 multisig path says, for each sig, you *either* need to reveal that sig *or* one of 10 secret preimages where each routing node knows one of them? If a preimage is revealed before ejecting someone, that person can use the preimage to reimburse themselves using a bond created by the routing node? That would work except creating the bond is creating a utxo, which means you need 10 utxos (I think? Maybe the bonds, too, can be created as needed in a kind of chain?), and a pool of 10 people in 1 utxo is pointless if it only works by creating 10 *different* utxos for each of those people. | |
But I think the bonds *can* be created as needed, and then spending-post-ejection becomes a decent “challenge-response” protocol. The routing nodes can “sign for a user” at any time by revealing their preimages, but it’s unsafe to do so unless they’ve ejected that person from the pool, because if that person *hasn’t* been ejected, he or she can create their “ejection connector” (which proves they were still in the pool at the time) and use it to pull an equivalent amount of money from whichever routing node spent their money. Huzzah, I think that does it! | |
Well, I still think it does it, but I realized that there is another solvable snag: every time a sig is required, any routing node can reveal a preimage instead; so if there are 10 people, a single routing node can take all the money by revealing 10 preimages, thus not requiring any signatures. Now, that’s okay if we ensure the routing node has enough capital to reimburse all 10 users, if necessary, via 10 challenges, but I think I can do better: I think I can ensure that only 1 person can be ejected per round. I can do this by making it so that if you use a signature, a number gets incremented by 1, and if you use a preimage, it does not. In the first round, then, I can require the incremented number to be 9 or greater, thus allowing only 1 person to be ejected in that round; and in the second round, I can require the incremented number to be 8 or greater, thus allowing 1 new person to be ejected in that round; and so forth. | |
But wait…doesn’t this *still* require each routing node to lock up enough capital to cover everyone? Each routing node can kick any participant out in any round. Which means, if there are 10 people and each one made a 10k deposit, the routing node can “claim” – once per round – that 10k has exited the coinpool with a given user. And if the routing node makes that claim, that is supposed to allow that user to initiate a challenge that essentially says “*my* 10k did *not* exit the coinpool!” and simultaneously reimburse themselves with the routing node’s money. But *that* requires the routing node’s money to *be available.* And it can’t be available unless it is *locked up* ahead of time. So the routing node must lock up 10k sats ahead of time for *each user.* | |
Maybe I can use sighash_anyone_can_pay to let the routing node *fund* the “challenge address” with any utxo they have lying around when they go to kick someone out. The script for the “challenge address” can say “the routing node can sweep me if 2 weeks go by, or the user can sweep me if they learn a preimage.” But then the user will be able to sweep it as soon as they get ejected, because that reveals the preimage. Somehow there must be a secret the user only learns if he *wasn’t* ejected, which sounds paradoxical – at every moment prior to being ejected, the user “wasn’t ejected [yet],” which implies he should have learned the secret during all of those moments – and once he knows it, I don’t think I can expect him to forget it. | |
Of course, if he is *not* ejected yet, I can let him create a connector, so if he *does* create that connector, it serves as proof that he wasn’t ejected yet; but I don’t think I can use connectors with an address funded via a transaction involving sighash_anyone_can_pay because I won’t know the txid of the funding transaction til it happens, and for a connector to work, I need to know the txid of the funding transaction when I prepare the connector, which is before that. (I think.) | |
Maybe I can do something with bitvm tricks. Each routing node can make a commitment, in each round, to which people are not ejected yet (i.e. which ones are still in the pool). If they then “claim” they kicked someone out (i.e. by revealing the preimage for that person), can I catch them in a lie of “claiming” to kick someone out while that person’s “in round” connector is still creatable? If so, perhaps I can identify the victim by their connector and reimburse them with that routing node’s money by having the victim prove the routing node made a false commitment. But even if I can do that, would that solve the problem of making the routing node put up enough money in advance to cover the cost of a failed challenge? | |
Wait…earlier I said “But then the user will be able to sweep it as soon as they get ejected, because that reveals the preimage.” But that’s not true, is it? Ejecting someone is done using a multisig – it does not reveal a preimage. A routing node only reveals the preimage when they want to sign “for” that user in a “happy path” dispersal, since users who exited via the “sad path” can’t be expected to sign “happy path” transactions. So what I want is for users to have a “challenge opportunity” when a routing node signs *for* them – such that if they *were* ejected (i.e. their connector is consumed), they cannot do anything bad to the routing nodes, so the routing nodes get their bond back. | |
I wonder if it’s possible to do this: if the routing nodes sign for one or more users in a “happy path” tx, that “happy path” tx is only valid if it simultaneously creates the same number of utxos as the number of people signed-for, where each utxo is a “bond utxo” worth the deposit amount; if I can do that, I further want to ensure that each bond utxo can be swept by a user if their connector still exists, but if 2 weeks go by, the routing nodes get that money back. | |
The latter of the two requirements seems simple enough *if* I know each bond utxo’s funding tx info during the setup ceremony, namely, the txid and vout that created that bond utxo. | |
------------------------------------------------------------------------------------------------------------------------------- | |
Ok I think I got it | |
For n users, there are n + 2 scripts in the multisig | |
The first script is a simple n of n – this is for two things: ejecting sad-pathers and dispersing funds via the happy path (where everyone exited via lightning, only the routing nodes are left, and they are all cooperative) | |
The second script is only used in dispersing funds in a less-happy path, and is effectively used for counting how many sad-pathers were ejected. It has two sections: the “n-of-n section” requires n of n pregenerated signatures (so that it can enforce where funds go unless everyone is cooperative, in which case they would probably use the first script anyway), and the other section, the “counting section,” requires one of two things from each pubkey: either a *second* signature (this one is *not* pregenerated), or a routing node can substitute that with a preimage. This section *counts* how many preimages were substituted for signatures, thus learning how many sad-pathers there are. For example, if the multisig has 10 users, and 8 of them are happy-pathers, the happy pathers can create 8 signatures but will need to substitute 2 preimages for the two sad-pathers, and the script counts them and knows there are 2 sad pathers. Importantly, the counting session comes *before* the n-of-n section, and, by using k OP_CODESEPARATORs where k equals the number of sad-pathers, we can enforce a rule, for this script, that the signatures in the “n-of-n” section must *commit to* the value of k. So, if there are 10 users, they will each generate 10 signatures for the n-of-n section – one if there is just 1 sad-pather, a different one if there are 2 sad-pathers, etc. all the way up to 10. This forces the happy-pathers to use this section if there are *any* sad-pathers, and to use the pregenerated signatures corresponding to *how many sad-pathers there actually are.* And *this* means we can generate the right number of bonds *and* know their txids and vouts during the setup ceremony. So: if there are k sad-pathers, the n-of-n section ensures that the only valid way to spend the money using this path is by putting the money into k bonds, each worth 100k - 330 sats; any user who was *not* ejected has a sig that lets them consume any of the k bonds *plus their connector* to exit. (Their routing node can also do it for them.) And, thanks to the bonds being an n of n, they can only sweep their money into an address they provided during the setup ceremony. If a user *was* ejected at some point, they cannot sweep their money because their connector no longer exists; and I have the bond money go back to the routing nodes after 2 weeks. (The transaction doing so is *also* created during the setup ceremony.) | |
The remaining n scripts deal with the money “left over” after the sad-pathers have definitely exited via the above mechanism. (Recall that only k bonds were created, with k equal to the number of sad-pathers; the rest of the money was deposited back into the multisig, and will be spent using this path.) Each of these scripts is “guarded” by an n of n that can only be satisfied by consuming a connector created by whatever transaction spent the money using the second script; therefore, these scripts all only take effect after the sad-pathers have all exited and only happy-pathers remain. (This also allows me to know, in advance, how many happy-pathers are left; just have the second script create a unique connector for each value of k, and then use whatever one of the n remaining scripts is “unlocked” by that connector – I could even have it put the value of k on the stack right away, if that was useful.) After that n-of-n part, each of these scripts basically has the script from the “counting section” – the happy-pathers must spend the money cooperatively, substituting a preimage for any missing sad-pather, but without substituting more than k preimages. This ensures the happy-pathers cooperate in this part and that no happy-pather can steal money from the other happy-pathers by substituting preimages for everyone except himself (or herself). Presumably, the happy-pathers would, when preparing to disperse the funds, first create signatures for this part, then use the second path to unlock the right one of the remaining n scripts, then finish off this part and disperse the remaining funds amongst one another now that all the sad-pathers are gone. | |
------------------------------------------------------------------------------------------------------------------------------- | |
A few thoughts about the second script. In it, I count how many preimages are substituted for signatures in order to learn how many sad-pathers there are; but I think I can know how many sad-pathers there are without that, just based on which “round” it is. If a bunch of money is deposited into the multisig in a “funding tx,” and then whenever someone is ejected the rest goes back into the multisig, the utxo info should be “new” every time someone gets ejected, and match 1:1 with however many people were ejected so far. So I can say utxo A (created by the funding output) is used when there are 0 people ejected; utxo B (created as the “change” of the first ejection transaction) is used when exactly 1 person has been ejected; utxo C (created as the “change” of the second ejection transaction) is used when exactly 2 people have been ejected; and so forth. So if I can’t use the *first* script (because at least one person had to be ejected), then the *second* script knows how many bonds must be created right from the get-go, and I don’t need to do any counting. So the only way to spend utxo C via the second script (if there are 10 users who each deposited 100k sats) is to disperse 300k sats into 3 bonds of 100k sats apiece (minus a connector’s value), and then 3 people can exit using those bonds…but they already exited, why make a new exit for them? Oh, right, it had to do with the preimage stuff – if a user exited then I am supposed to be able to use a preimage as a substitute for their signature when dispersing the funds, but I can only do that if I prove they exited, so I need to give them a chance to exit by sweeping their bond with their connector and somehow the preimages factor into that too somewhere, but I am too tired to remember how, maybe I’ll look at this some more tomorrow. | |
After thinking about it, I think the whole idea is flawed because it allows any party to withdraw using the second script and then sign in one of the remaining n scripts in place of another party, thus *not* dispersing funds to that party, but to themselves, and effectively exiting twice, but the second time with someone else’s money. | |
------------------------------------------------------------------------------------------------------------------------------- | |
New idea | |
Have n people prepare and sign a coinjoin that funds an n of n multisig with Q sats apiece. It is important that Q be the same for every user and that every user has one of the keys to the multisig. Before the users share their coinjoin sigs with one another, they generate n presigned txs, to which each user gets a copy. Each presigned tx defines a “round” (e.g. round 1, round 2...round n) within which a utxo worth Q sats is created using the pooled funds, and the remainder, if any, returns to the multisig, ready for use in the next round, if any. In each round, the Q-sat utxo created in that round is locked to a “midstate address” with n script paths, each of which lets a different user spend that Q-sat utxo, but only with n-of-n signatures (i.e. a signature from every other party; these signatures are also generated before anyone deposits money into the multisig). | |
To withdraw from the midstate address, a user has to supply the n-of-n signatures for their script path and reveal a “withdrawal secret” – a different one in each round for each user. The n-of-n sigs are signed with sighash_all | anyone_can_pay and they force the user to do two things: (1) the user must move the money to an address they chose in advance, before anyone deposited money into the multisig, and (2) the user must fund a “fidelity bond” that is worth 2 times the value of Q. The fidelity bond’s script allows anyone to burn the withdrawer’s bond, but only if they know *two* of the withdrawer’s withdrawal secrets. That way, if the withdrawer tries to steal from the coinpool by withdrawing twice (i.e. in two different rounds), they will almost certainly lose Q sats as long as anyone is paying attention to the theft attempt. This mechanism disincentivizes theft attempts game-theoretically: the thief stands to *lose* Q instead of *gaining* Q, so they will probably not do it. Assuming the user has *not* tried to withdraw twice, no one can burn his or her funds, so after a period of 3 days, he or she can collect his or her bond and all is well. | |
A problem with this idea is that I don’t think I can use it as a channel factory [edit: I have more thoughts on this; see below]. In order to make the user fund the bond and withdraw in the same transaction, I use anyone_can_pay for the withdrawal transaction. As a result, no one can know in advance what the txid of the user’s withdrawal transaction will be; therefore, the user cannot withdraw into an address with presigned transactions representing their initial lightning state (or hedgehog state). | |
I could modify this by having the user (1) create a dust utxo when they enter the pool (2) get it back when they spend from the midstate utxo and (3) not allow them to use spend from the midstate utxo unless their dust utxo exists, which means they *can’t* withdraw in a subsequent round. But that seems to require creating n utxos during the setup ceremony, which defeats the purpose of a channel factory – if you’re okay with creating n utxos on the base layer, just create n channels on the base layer, which would be more efficient. | |
------------------------------------------------------------------------------------------------------------------------------- | |
Wait, I think I *can* do a channel factory with this – but I don’t think it’s a good idea. In the paragraph that starts “To withdraw from the midstate address, a user has to supply the n-of-n signatures...” I can change it thusly: the user has to supply the pregenerated n-of-n signatures *and also* a signature from his or her channel counterparty. (This one is *not* generated during the general setup ceremony, but only by the user and their channel counterparty, separate from everyone else). The user and the channel counterparty only produce that signature once they’ve decided which utxo to use to fund the fidelity bond, and the channel counterparty can rest assured that the user can’t use a different one unilaterally, because that would invalidate the channel counterparty’s signature, which is required for this to work. So now the user or the channel counterparty can get the transaction to this step and then know the precise txid that the channel will end up having. Boom, crisis averted! | |
Except for this: the utxo that funds the fidelity bond, in a channel factory setting, has to be created by someone. That someone is either a trusted third party, or, if not, the user and the channel counterparty have to cooperate to do one of two things: create that utxo in advance or ensure that either of them *can* create it on demand. So it’s either trusted, created in advance, or created on demand. But if it’s trusted, why use a channel factory? Just let your counterparty hold your funds. And if it’s created in advance, why use a channel factory? You’re creating a utxo on the base layer during the setup ceremony anyway, so you’re not saving any money. And if they make it creatable on demand, why use a channel factory? You already have a creatable 2 of 2 of more than enough value; use presigned txs to give it an initial state, and update that state off-chain, and you’ll save money by doing that instead of *also* participating in this channel factory scheme. | |
Maybe the user and their channel counterparty are both willing to trust a 1 of n scheme, e.g. “we have ten people who promised that they will create a utxo with info X for us on demand, and we only have to trust that one of them will actually do it. And we probably won’t even need to because this is the “unhappy” path anyway – as long as the coinpool works as intended, we’ll never need to *put up* a bond.” | |
Oh yeah, another problem with using this for a channel factory: Alice’s counterparty is going to want a guarantee that he can leave, which means Alice needs to give him at least one of her withdrawal secrets. But since he might need to leave in any round, Alice actually needs to give him *several* withdrawal secrets. And that means Alice’s channel counterparty can always burn her funds. | |
Wait, no it doesn’t – you can’t burn someone’s funds in this scheme if they’ve already withdrawn, you can only burn your bond. And if Alice hasn’t withdrawn yet then the channel counterparty has to do the withdrawal into a 2 of 2 multisig where he only has one key. He can burn his own funds *after* that but that’s no big deal to Alice – she’s got her money in the multisig, safe and sound. | |
------------------------------------------------------------------------------------------------------------------------------- | |
I had a series of ideas today that I think make this a kind of channel factory again. The first insight I had was this: Alice can transfer an entire utxo to someone else if she makes the withdrawal address an htlc to which she knows the preimage. If she *doesn’t* disclose the preimage, she gets her money back after 2 weeks. If she *does* disclose the preimage, her intended recipient can take the money. | |
After that, it occurred to me that right now I have everyone presign *one* transaction for Alice and that transaction has two outputs: one puts all of her money in an address of her choice, the other is a bond that will burn double that amount if she is spending fraudulently. But I could, instead, have everyone presign a transaction giving Alice 90% and her channel counterparty 10%. | |
And if I have them presign *ten* transactions, or *twenty,* or even *several hundred,* each one can give Alice and her counterparty a different amount. To pay her counterparty, Alice can give him the data he needs – probably just the preimage to a tapleaf guard – to any one of those transactions. As long as each “larger” amount has a smaller timelock on the signature, the channel counterparty can broadcast whichever one gives him the most money. Which means I have a way for Alice to pay a variety of amounts to her counterparty off-chain. | |
And this can even work over lightning, because to the counterparty, each “bigger” preimage is worth the amount it pays him (minus however much he’s already been paid in this channel), so if he learns of a lightning invoice locked to the hash of that preimage, and worth the same amount it’s worth to him, he shouldn’t mind paying it at all. (Though this does require some way for the recipient to make an invoice locked to that specific hash – but that’s fine, Blixt and Zeus both let you do this, and other wallets can add support too.) | |
After I thought of all this, I was like, “Wow, that’s cool, so this is like half of a channel factory – it can give you a sort-of channel, not a lightning channel, but something that *works* with lightning, and you can *send* money, but it’s unfortunate that you can’t *receive* any.” But I kept thinking about it and it occurred to me that if *one party* is *sending,* the *other party* is *receiving.* Which means this *does* support receiving – it’s just one-way, one party can send money to the other. And *then* I realized, why not give each party in the multisig *two* channels? In one, they are the sender, and in the other, they are the recipient. And that makes it work both ways. | |
So that’s where I’m at – I’ve got something that can work pretty similarly to a channel factory, but with some cool properties: | |
- it doesn’t need a soft fork | |
- you can put your money in it without worrying that your utxos will expire (though when you *receive* a payment you *do* have a time limit to sweep it or risk your counterparty broadcasting a transaction that pays you less money) | |
- it scales linearly with the number of users – O(n) instead of O(n**2) | |
- users can exit in any order, at any time, without coordinating with other users (but the last users to exit could theoretically be stolen from if the thief was willing to burn more money than they stole) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment