Stealth Development Blog 007: 01/25/2019
Finishing up Registry Housekeeping and Starting Registry User Operations
This week I focused on wrapping up the core registry code and began working on the qPoS opcodes that specify state changes in the registry. These opcodes are new instructions in the language called “Script”, inherited from Bitcoin. The Script language is primarily intended to specify spending conditions for unspent transaction outputs (UTXOs), but it can be used for other purposes, like data storage. With qPoS, I use it to mark transactions that operate on the registry. These operations include purchasing stakers, claiming staking rewards, changing the owner and delegate keys, and toggling stakers between enabled and disabled states.
Finalizing Staking Rounds: Purging Stakers and the Ledger
I began the week by writing code to purge pubkeys (public keys) and their associated balances from the registry ledger, which are key events in the finalization of each round of staking. I’ll briefly describe one challenge related to purging that I encountered, which is the purging of public keys from the registry ledger.
The staker registry, which I refer to simply as the registry, holds information about all extant stakers, the queue that specifies when stakers sign blocks, and a ledger of earnings from block production. Because stakers can have delegates that may change from time to time, and may also get transferred to new owners, earnings are not awarded to the stakers, but to the pubkeys that specify ownership and delegation. Because of this fact, the registry has a separate ledger that holds unclaimed earnings.
Use of the staker ledger is much more efficient than the alternative, which is to make one or more UTXOs every time a staker produces a block. The addition of new UTXOs creates storage and computational burdens for network nodes. With the efficiency of the registry ledger, however, comes an accounting burden in that each pubkey must have its own balance.
A significant complication of this design is that a single pubkey can be affiliated with more than one staker. This possibility for multiple affiliations comes into play when account balances must be docked for staking inactivity. To reduce resource usage, account balances that are not affiliated with an active staker are docked a small amount of XST at the end of every round of qPoS. Eventually an abandoned account will be docked to a zero balance, and the registry will purge the zero balance, as well as its pubkey owner, from the registry ledger. The tracking of which pubkeys are affiliated with extant stakers requires a dedicated data structure that uses running tallies.
Opcodes for General Purpose User Interaction
After writing the code to finalize each round of staking, I moved to writing the templates, parsers, and validation for the opcodes. One result of this coding is that I changed how opcodes work from what is described in the Whitepaper. The Whitepaper says that opcodes will be executed upon spending them as inputs, in much the same way that a Script signature is executed to prove spending authority. However, as cool as this system sounds, it is overly complicated and unnecessary because the input signature suffices as the authorizing signature for registry operations. This description might be confusing, so I provide a figure to help explain what I mean.
In this figure, the authorizing signature signs the input transaction of 1 XST. Even though this input had a 20 byte hashed pubkey for a destination, the signature must provide the 33 byte compressed pubkey, which can’t be inferred from the hashed pubkey. Since the compressed pubkey must be provided, it specifies the balance in the ledger registry that is claimed because these accounts are identified by compressed pubkeys. Although it is not clear from the above figure, the claim transaction is sent to a standard bitcoin-style address (e.g. S8knUucS6iFwAt1S8BmRpiTykqQfRLcape).
Other registry operations are also specified with opcodes within canonical Script programs, even though these operations are never executed by the Script interpreter. Instead, they are interpreted by the general C++ execution environment. The full set of novel opcodes used by qPoS are
- Purchase staker, setting all keys the same: OP_PURCHASE1
- Purchase staker, setting three different keys: OP_PURCHASE3
- Change owner key: OP_SETOWNER
- Change delegate key and payout: OP_SETDELEGATE
- Change controller key: OP_SETCONTROLLER
- Enable: OP_ENABLE
- Disable: OP_DISABLE
- Claim earnings: OP_CLAIM
Each of these opcodes corresponds to a similarly named transaction type. For example, a properly formed OP_PURCHASE1 transaction is called TX_PURCHASE1.
To give an idea of what I mean by a well formed canonical Script program, consider the TX_SETDELEGATE Script program, which is specified by a template (templates are precise specifications for the types of Script programs that nodes will accept):
CScript() << 0x27 << OP_SETDELEGATE
What is this template saying? First, 0x27, the hexadecimal representation of 39, is an opcode without a name. It says that the next 39 bytes of the script contains data that will be “consumed” by the next opcode, in this case OP_SETDELEGATE. What is the nature of these 39 bytes of data? In this case it is 4 bytes for the staker ID + 33 bytes for the compressed pubkey + 2 bytes that specify what fraction of the block rewards the delegate may claim.
One question is: if the script is interpreted by the general C++ execution environment rather than the Script interpreter, why make a well formed script? The main reason is that by having a well formed script, it is possible to utilize the existing Script infrastructure. For example, this infrastructure has a very sophisticated function that can match a script with its template and identify the transaction type. Writing this kind of pattern matching code from scratch is laborious and error prone.
There are many ways to specify these registry operations using standard transactions, but I decided that the most sensible way is to make canonical Scripts that describe both the operations and data, such that they would have correct syntax within the Script programming language itself. The main practical advantage is that I can use the existing Script infrastructure to evaluate these operations.
–––––
Staker Aliases
In addition to some other improvements to the registry, I added staker aliases. Within the code, stakers are identified as numbers, starting with the number 1. These numbers are called staker IDs, and they are all unique in perpetuity. I realized that for user interfaces, like staker monitors, numbered stakers could be confusing. Staker aliases will make such interfaces more user friendly. Aliases are between 3 and 16 characters, composed of only letters and numbers, and must start with a letter. Their representations are case sensitive, but their uniqueness isn’t case sensitive. In other words, I could register a staker called “StealthRules”, and the blockchain would always remember it as “StealthRules”, but no one could register “stealthrules”. Additionally, aliases live forever, so even if my staker were terminated, no one could register “StealthRules” (or any case variant of it) again. Finally, aliases are set upon registration and they cannot be changed. So think carefully about what you want to call your staker!
So what’s left to do?
I believe I’m close enough to see the light of testnet here, so I’m going to give a to-do list that I think will get me there. As always, we are careful not to make promises or give public “road maps”, so this is just a subjective appraisal of what needs to be done before testnet. Disclaimer: any dates expressed, implied, inferred, or imagined should be interpreted by the reader as “probably never”.
- Add code to transaction processing that interprets the qPoS-specific Script programs (enumerated above as TX_PURCHASE1, etc). In most cases, registry state changes occur when blocks are finalized and added to the chain. Some operations must be delayed until significantly after the corresponding block has been added to the main chain. Both validation and finalization code needs to be written. I have already started to work on the validation code for the most difficult transaction type, TX_CLAIM. I may write more about TX_CLAIM next week to illustrate why it is more difficult than the others.
- Add trust score code. PoS chains of all varieties have some notion of a most trusted chain. It is analogous to PoW where the most trusted chain is the chain with the most work. For qPoS, chain trust is a function of staker weight, which increases for each staker with the net number of blocks it has produced.
- Write the qPoS block signing and signature validation code. In traditional PoW and PoS systems, the coinbase and coinstake transactions specify the public key corresponding to the block signature. With qPoS, the public key is stored in the registry and determined by lookup using the stakerID, which is a field (member) of the block data structure.
- Code the appropriate delays between when a staker is purchased from the blockchain and when it is added to the registry. A sufficient delay will prevent an exploit where an individual can buy a staker, orphan their purchase using PoS, and effectively double spend the block chain, netting one or more free stakers. This type of attack is not a general PoS attack, but arises from the specific design of qPoS.
- I need to code similar delays for TX_CLAIM to prevent lost earnings, where an individual’s claim transaction gets orphaned by a reorganization.
- For reorganizations, I need to write code to roll back the registry state using snapshots and replay. The ability to roll back the registry will serve as protection against reorganizations and attacks that depend on reorganization like I describe above.
- I need to write code to replay the registry from genesis in the rare case that the oldest registry snapshot is newer than the newest block.
- I need to write minimal RPC calls for the qPoS-specific operations (TX_PURCHASE1, TX_PURCHASE3, etc).
–––––
Hondo