I’ve recently released an alpha version of a ticket splitting matcher service on testnet. This post is meant to be a summary of the challenges of building a split ticket service and to serve as reference for their future development.
Ticket Transaction (SStx) Layout Requirements
- At most 64 inputs and 128+1 outputs
- The number of outputs must be twice the number of inputs, plus one
- The first output must be a P2PKH or P2PSH script (usually referred as the “ticket address"2
) prepended with an
- The even-indexed outputs (apart from the one with index 0) must be
OP_RETURNdata, formatted in a specific way (“change address"3 )
- The odd outputs must be
OP_RETURNdata formatted in a specific way (“commitment address"4 )
Its inputs must also follow an additional set of requirements which are checked when a full decred node receives a new candidate block for inclusion in the blockchain (as implemented in the CheckTransactionInputs function):
- Inputs to a ticket purchase must be P2PKH or P2SH
- Each individual input amount must be equal to the sum of its respective commitment and change amounts
These requirements mean that a lot of coordination is needed in order to build a valid ticket transaction. In particular, the requirement of needing input amounts equal to the commitments and that the number of outputs must be twice that of inputs (plus the ticket address output) means that the participants must prepare their inputs ahead of time in order to have the inputs sum up to the ticket amount.
It is possible to use inputs that have an amount of coins larger than the desired participation amount and then use the corresponding change output of the stake transaction to return the rest of the coins to the original wallet. However on the current implementation of the split ticket service I’m assuming that will be uncommon, and that a split transaction will be needed to consolidate inputs anyway, so for the moment the matching service always creates it.
A ticket purchase transaction locks the amount of coins specified on its ticket address output (the first output of the transaction) until the ticket is voted, missed or expired. These transactions must necessarily spend the coins from the ticket utxo.
In other words, the wallet in control of the ticket address is the only one that can release its funds.
On solo voting wallets, the user is in control of both the voting and ticket funding wallet, therefore they can be sure the funds will be accessible.
On stakepool voting tickets, the ticket address is a 1-of-2 multisig address, so the wallet is always able to create a transaction to redeem the funds.
However, on a split ticket it’s not possible for all participants to have access to the private keys of the voting address (due to influence amplification concerns - see below). Only a single participant (and optionally a voting pool) can have access to it.
Therefore, it is possible to reach a situation in which non-voting participants may be locked out of their funds: if the pool and the voter participant both fail to create (or cooperate against creating) the SSgen or SSrtx.
Unfortunately, as noted below on the section about voting rights and influence amplification, the voting case cannot be solved. Only a single voter (weighted by their contribution percentage of the ticket purchase) can be allowed to have access to the voting private key. But in case of a successful vote, the original participants receive their contributions back (along with the reward) therefore this is not a concern from the perspective of getting locked out of funds.
The revocation case is more serious. If the voter participant or the pool fail to create the SSgen (or if the ticket expires) the original non-voting participants must be able to retrieve their contributions back.
This can be accomplished in one of two ways:
- Pre-create the revocation transaction (during the split ticket purchase workflow) and distribute it to all participants;
- Use a more complex voting script than a standard 1-of-2 multisig script.
Option 1 is more simpler to implement and verify. However, it adds the requirement that non-voting participants of a split ticket must store the corresponding revocation transaction until the ticket is voted or expires, if they want to be certain of not losing access to the original funds.
This is not a big requirement in terms of computational resources (at most a few KB per ticket purchase, which can be released after voting/revocation) but introduces another piece of data that a conscientious user must keep track of in the event of (for example) formatting their hard drive.
Option 2 introduces a more complex voting script that makes use of the newly introduced CSV opcode (
OP_CHECKSEQUENCEVERIFY) to allow anyone to create the revocation transaction.
While the script also would have to be stored, it could be reused between transactions (lowering storage requirements) and in fact could even be a single one per stakepool, publicly announced by it and therefore always known.
A few more details of this second option are available at the design doc of the split ticket service, but suffice to say it would involve a much more substantial change to the underlying software (specially because it would involve changing how a voting wallet signs the vote transaction).
Note on Vote and Revocation Outputs
It’s important to note that consensus rules enforce the layout of SSgen and SSrtx transactions to ensure that the output addresses and values always correspond to the ones stored in the commitment outputs of the SStx.
The number, addresses and values of the outputs must conform to what these functions consider correct, therefore the voter (or a stakepool) cannot decide to pay the rewards (or return the funds) to different addresses.
Voting Rights & Influence Amplification
Currently, block validation and on-chain agenda voting are individual bits stored on the second output of SSgens. Therefore, on a split ticket either only a single participant can decide the value of those bits or some strategy must be be used to select its value.
One of the challenges of the split ticket voting is ensuring that no major holder (“whale”) can gain undue influence on the network by exploiting the strategy used to select the vote bits.
I’m using the term influence amplification as that exploitation. An example of a strategy vulnerable to influence amplification is the majority rule: choosing the vote bits of the agenda that at least 51% of the participants of the split ticket (as measured by their percentage of contribution) have chosen.
That strategy is vulnerable because a large decred holder could buy exactly 51% of many tickets, therefore massively increasing its voting influence. As a practical (though simplified) example, assuming a decred holder (or group) in control of 1,000,000 (1e6) DCR and a (long-term stable) ticket price of 100 DCR. Instead of 10,000 individual tickets, they could buy 51% of 19,607 tickets, almost doubling their influence on the network.
Therefore any voting rights decision strategy must be weighed by the contribution percentage to the ticket, such that a 51% contribution to the ticket must be equal to a 51% chance of having the voting rights.
Also note that allowing a participant to stop the matching session and start a new one if they are not selected to vote also leads to influence amplification, since someone could keep creating new sessions until they are selected (discussed a bit more on the spam protection section below).
If a single (or few) matching service providers are offered on the network, they may become bottlenecks (vulnerable to DOS attacks) and single points of failure (vulnerable to compromise).
This is an important concern specially because having access to all participants inputs and outputs individually, the matcher service may be able to track both past and future transactions for all participants.
Since the matcher chooses the voting address, it may also (either unknowingly or maliciously) cause influence amplification, by always choosing one specific voter or even outright steal voting power (by always directing the ticket address to itself).
And finally, a malicious matcher may cause fee drain of the participants by publishing the split transaction but not the ticket and immediately spending one of the outputs destined to the ticket (thus making the ticket transaction invalid).
These problems are somewhat similar (though bigger) to those faced by users that choose to use a stakepool to vote on their behalf. While the stakepool cannot outright steal the user’s funds or direct the voting address to itself, it can vote in a manner not endorsed by the user.
There are a few alternatives for dealing with this centralization concerns:
- Use one matcher service per stakepool;
- Develop a completely decentralized matching service.
Option 1 is the simplest and most readily available. If users trust their stakepool to vote on their behalf, they should be able to trust it to run their matching service to other people on the same stakepool.
There is obviously more room for exploitation by the combination of a matcher service provider and a stakepool on a split ticket then on a single ticket (in particular due to voting rights selection). However, the stakepools could possibly be monitored and the presence of competing stakepools mean users have at lease some choice on who to trust.
Option 2 (a completely decentralized matcher service) following the steps of DiceMix/CoinShufle++ we could possibly design and implement a fully P2P matcher service. That will, however, be a much larger and longer undertaking.
Another concern for people wanting to participate on a split ticket purchase is to be able to timely finish the session successfully.
From the point of view of matching services, there is the need to prevent a single user from starting and purposefully failing to finish sessions, either as a targeted attack to the stakepool or an influence amplification attempt.
If the matching service is provided by stakepools, they can trivially detect problematic users by requiring that all participants authenticate first, using their respective api keys.
Participants that fail to finish too many sessions might be banned by some amount of time by the stakepool.
There may still be a need to coordinate among stakepools, or to develop and deploy some auditing service in order to prevent cross-stakepool spam attempts.
The influence amplification possibilities of spamming the split ticket services are specially concerning. One possible solution is to configure the matcher not to use the participant provided voting address, but to let the stakepool use the address of its own voting wallet and disclose the chosen voter only after the ticket transaction is published. This has the disadvantage of having the voter import and store an additional private key in order to vote (or to forgo the option to vote instead of the stakepool).
The last challenge for deploying the split ticket service is adoption. If the service is provided by stakepools, this will require modifying the stakepool software and requiring stakepool operators to update it (or detect which stakepools are available for ticket splitting).
In the mean time, a single centralized service could be offered for those wishing to participate on split ticket purchases, though that is vulnerable to the previously listed problems.
The next milestone for the split ticket matcher service is a beta service available on mainnet for private matching.
↩ At the time of this writing, version 1.2.0 (still unreleased) slightly changes this to `CheckIsSStx`.
↩ This is the only actual spendable output of the sstx. Essentially, this is the address that is allowed to spend the SStx either in the form of an SSgen (vote) or SSrtx (revocation).
↩ The change address is meant to return excess input amount back into the funding wallet, but due to the use of split transactions it is usually the zero address (a P2PKH address filled with zeros), with a zero amount.
↩ These commitments are the interesting outputs of an SStx. Each of them follows this outline (more easily understood by looking at how they are constructed on the [txscript](https://github.com/decred/dcrd/blob/v1.1.2/txscript/standard.go#L914) package: ```OP_RETURN [commitment address] [commitment amount] [fee limits]``` The *fee limits* bytes indicate how the corresponding output of an SSgen or SSrtx should be. In other words, it establishes that, in order for the vote or revocation to be valid, the output (of the SSgen/SSrtx) with the same index as the output of the ticket purchase must not be above or below the original value, plus the vote reward (or minus the revocation fee). The function that calculates the expected maximum/minimum amounts of return, given a set of limits is [VerifyStakingPkhsAndAmounts](https://github.com/decred/dcrd/blob/v1.1.2/blockchain/stake/staketx.go#L659)