Accounts and templates address generation algorithm

In terms of addressing, Spacemesh took quite a different path than Ethereum. I am going to analyze why is that so. Moreover, there are some remaining questions we need to answer before Genesis. In conclusion we are (probably) going to change the way we generate addresses.

How this is solved in Ethereum ?

In Ethereum addresses are:

  • for an externally owned account (EOA) the address is last 20 bytes of Keccak-256 hash of the public key
  • for a smart contract account - last 20 bytes of Keccak-256 hash of creator EOA and its nonce

Why not just copy-paste the solution Ethereum has ?

Short answer is: because we have accounts abstraction (= ability to customize signatures, nonces and other “meta” things), account unification (EOA and SC are merged into a single concept) and templates mechanism. These mechanisms together make our situation somewhat more complicated than Ethereum has.

So, first of all, a template is just a piece of on-chain code, there is no mutable state in a template. We encourage reusing code via templates and in principle there is no need to store the same code twice. The on-chain code duplication, which is a nightmare in Ethereum, can be just completely avoided in Spacemesh. So, it feels completely natural to follow the IPFS style i.e. use the the addressing-as-contents-hash pattern for templates. So the address of a template should be the hash of the binary representation of this template.

In the contrary, our accounts do have mutable state. So … should we then just copy-paste the EOA approach from Ethereum at least in this point ? No, we cannot - because the whole idea of “signatures”, i.e. the public-private key pair dedicated for signing transactions for a given account - this idea is not part of our platform. Because we wanted signatures to be completely customizable, so it is the template defining signatures validation (or any form of validation).

The only information given at account creation is:

  • the template that newly created account must be based on
  • immutable state aka “constructor parameters”

(I tend to use object-oriented analogy: accounts and templates are like objects and classes. From this point of view, account creation is like new object creation, and so immutable state is analogous to constructor parameters).

Hence, the natural idea is to calculate hash over (template address, immutable state) pair and use this hash as the account address. This implies that two different accounts sharing the same template and the having the same immutable state is an impossible situation. This impossibility must be taken into account by template developer. Also, it may be used for some interesting tricks (for example one can create a “singleton” template, i.e. a template with hard guarantee to have only one account using it).

Picking the optimal length

We want to use 256-bit length hashing algorithm (like Ethereum does). Which leads to 32-bytes of an address. This is generally perceived as an overkill in terms of implied storage. Ethereum optimizes this by taking only last 20 bytes of the hash. We decided to take 24 bytes. There is separate reasoning behind this decision which I am skipping now.

The “naive” approach

Let me put together the “naive” approach to addressing I just sketched above:

  • for templates: hash the template definition (i.e. its binary form) and take last 24 bytes of this hash
  • for accounts: hash the (template, immutable state) pair and take last 24 bytes of this hash

Interestingly, this algorithm can be seen as “almost” a generalization of Ethereum’s approach. In Spacemesh:

  • the closest thing to Ethereum’s EOA is a muggle account using SingleSigWallet template; in this case the resulting address will be the hash of (combined):
    • SingleSigWallet template address
    • immutable state (which happens to be the public key)
  • the closest thing to Ethereum’s SC is an elf account: in this case the developer of the corresponding template will (usually) include some sort of nonce-like field so to allow several accounts of this template to be possible; this nonce will then be hashed together with the template address to form the account address

Overall, the “naive” approach would work but … it turns out to be problematic. Let me explain why.

Naive approach problem #1: replay attacks

In real life Spacemesh will have many instances. I mean - many independent blockchains using the same software. At the very least this situation will be caused by several test networks, which are obviously isolated from Mainnet.

Let us look closer at one example: I am using a SingleSigWallet account. The address of my account is implied by the selection of my tempate (SingleSigWallet) and my public key (which was passed to the account at creation, via immutable state). If I am reusing the same key pair on testnet and mainnet, then my account address in both networks will be identical. This means that someone can take my transaction from the Testnet and send the same transaction, as it is, to Mainnet. It will look legal, it will have valid signature and it has a good chance to be executed. This trick opens up a whole world of possible attacks - we will call them “cross-chain replay attacks”.

Are these attacks also possible in Ethereum ? Definitely yes.

So, why this is supposed to be a problem for us, while it apparently is not much of a problem for Ethereum? Because Ethereum has built-in strict nonces (nonce is numeric sequence, with no holes allowed). This largely limits the attacker maneuvering capabilities: cross-chain replay attacks will usually fall short because of nonce mismatch.

In our case, the situation is much more problematic. First of all, nonces mechanics is not part of the platform - rather it is part of the flexibility provided by templates. So, the template developer may implement various nonce-like algorithms. Second of all, in the (largely simplified) Genesis version of Spacemesh, the nonce mechanics built into hardcoded templates is SIGNIFICANTLY DIFFERENT than the one in Ethereum: namely, we allow holes in the nonce sequence. This implies that the cross-chain replay attacks are really easily available. And the attacker can do quite a lot of mess with applying them. What is worst:

  • the cost of such an attack is zero
  • there is no way to even prove that some transaction was a result of an attack (because it cannot be distinguished from a perfectly honest transaction)

The obvious solution would be: just do not use the same key pair for Mainnet and Testnet. But then, this rule can be just a recommendation for users. So not everybody will follow such a recommendation. Also, such rule can possibly complicate some business-level test scenarios.

What is even worse, thanks to the enormous flexibility of templates, this “obvious solution” is not that obvious at all: some templates may implement authentication schemes not based cryptographic signatures. In such case, we are coming back to the problem of replay attacks.

Naive approach problem #2: hidden-immutable-state trick is broken

This one is even more subtle problem.

Similarly to Ethereum, a user can calculate the address of an account before actually creating it. Such an address can be then given to another person - and can be used - for example as a target of a coin transfer transaction. For this case we have the “stubs” solution - so a stub is an account that has non-zero balance recorded despite the template of this account and its immutable state is NOT YET KNOWN.

In Ethereum there are no templates, so there is no stubs, so there is nothing really hidden. But in Spacemesh, the ability of having this hidden information sealed into the stub is a relatively powerful and new idea, which we barely started to explore. To illustrate how powerful this feature is, let me provide at least one non-trivial example of using it (see - Appendix A at the end of this post).

Now, assuming we appreciate the added value of this “hidden immutable state” feature, we won’t be happy to know that something is decreasing its usefulness. But this is exactly what happens when specific hidden immutable state is about to be used simultaneously on Testnet and Mainnet - the self-spawn transaction for the same account address on Testnet would reveal the information we wanted to keep hidden on Mainnet.

Naive approach problem #3: hashing the whole immutable state is just too much !

With our naive approach we enforce all the information sealed into account to be available at the account address calculation. Which is just too limiting. Why this is limiting is explained also in Appendix A below.

This problem also shows up in the case of singleton templates (a template that enforces only one account using such template to be ever created we call “singleton template”). In the naive approach singleton templates cannot have immutable state !

The approach I recommend

The solution to avoid above problems is:

  • retain address generation for templates as in naive solution
  • use keyed hash for account address generation - where the key is some information available at the chain launch day which cannot be changed and is specific to the chain (and we will be using this “chain-id”)
  • let the template explicitly split immutable state into two parts: hashable and non-hashable. Only the hashable part goes into address-creation hashing

The perfect candidate for chain-id in Spacemesh is the golden ATX id.

Moreover, I recommend to use SHA-3 hashing algorithm for address generation.

But wait, why I insist to mix different hash algorithms in Spacemesh (SHA-2 and SHA-3) ?

One could say that using the same hashing algorithm in the whole Spacemesh design is most rational decision - in the name of simplicity.

However, we do have reasons to use more than one:

  • For PoET/PoST/POPS-VRF we mostly need a well-known hashing acceleration, with long years invested worldwide into optimizing its implementations, and with hardware acceleration support in mainstream processors. This is because we want to minimize chances that a major breakthrough in its calculation will break our PoET/PoST calibration; this is because in this contexts we mostly use hashing for proof-of-work pieces we need here and there
  • On the other hand, for addresses and signatures, we should rather use strongest hashing possible, hopefully quantum-hard, because blockchain security directly depends on this strength. An adversary may want to invest significant amount of computing power and time to attempt reversing just one hash (i.e. finding just one collision, for a specific hash value).

Therefore, the optimal answers are:

  • SHA-2 (256-bit) for PoET/PoST/POPS-VRF
  • SHA-3 (256-bit) for template addresses, account addresses and tx signatures

Appendix A: Real-life example of using hidden-immutable-state trick

Let’s imagine the following unusual crowdfunding model: there is a value (think: a long bitstring) which I want to be guessed. I am the founder of this crowdfunding campaign. I do not care who will guess the value, I am only interested in the fact that the guessing was successful. Everyone can add money to the fund. Whoever will guess the value, takes all the funds. Of course, as a founder, I know the answer upfront but I do not want to reveal it.

Why such a weird crowdfunding model may be useful at all ? Well, it could for example stimulate certain research areas. Let’s say I am the last person alive that worked in the Voyager-1/2 mission. Let’s say I am 95 years old and I am about to die. And I am rich. During the last days of my life I announce that I actually placed a small piece of paper with a very long number inside of Voyager-1 probe, and this was a secret for all these years. And I want somehow create yet another incentive to push the humankind towards space exploration. So I create this crowdfunding on the public blockchain Spacemesh - where I place 100 million dollars and I want to make sure that this money will be available only to human/organization that will find a way to reach Voyager-1 and will find my hidden message. Presumably, an entity capable to complete such a stunt would have to research and engineer spaceship propulsion technology to catch Voyager on its way to the deep space, which as of 2023 seems pretty unfeasible with currently available technology.

So, I create a suitable smart contract on Spacemesh, with this puzzle suitably encoded, I transfer all my money to this account, then I destroy the last copy of my secret number I kept at home. And then of course I die.

Let’s say I am a well known hero known for big ideas, so most people believe that this is not a joke but rather true story - and that this piece of paper said to be hidden in Voyager is really there. So for the next decades several more people will lock their money into this smart contract, and after another 100 years the value locked there is at the order of 10^10 USD. At some point, this incentive may highly influence the rocket propulsion technology innovation. So, as we can see, that sort of puzzle is not a nonsense.

Now, let us think for a while how exactly such a smart contract may be materialized within Spacemesh architecture.

One option seems to be obvious: we will create a template which takes as immutable state only one value X, which is a 32-byte array. The verify() method of this template always answers true. The only public method drain(answer, targetAccount) does two things:

  1. checks if hash(answer) = X
  2. if this check returns true then it sends all the funds accumulated so far to targetAccount

So what is do in my last days before my death: I hash the voyager hidden message to calculate value X, then I deploy the template as described and I spawn new account, providing X in the immutable state. Of course sending coins to the crowdfunding account is a built-in feature, so we do not need to implement this part explicitly - it is just given for free.

Another way of implementing the same crowdfunding idea is slightly different: we do our check inside of the verify() method, while drain() sends the money unconditionally.

But interestingly enough there is yet another way of achieving the same goal - and this one is pretty tricky. In this solution the check is not implemented explicitly at all !

The crowdfunding account will be created by our hero engineer only as a stub: he will send the transfer will all the initial money to the specified account address. The trick is that is is necessary to know the number hidden in Voyager-1 to execute the self-spawn!!!

So, the immutable state would be structured as follows: (answer, targetAccount). answer goes into the hashable immutable state, while targetAccount is non-hashable. Only when providing the right answer, the self-spawn will succeed. Then, method drain() will be sending money only to targetAccount, which of course got locked at the moment of account creation.

Of course all 3 solutions are not secure enough. Nodes in the P2P network could notice the transaction with precious answer coming to the mempool and replace it with a different transaction, reusing the same answer. Most probably a secure implementation would need to use zero-knowledge-proofs. But of course I primarily wanted to inspire active thinking of the audience :slight_smile:

I believe there are unlimited number of situations where the creator of an account would like to hide immutable state, at least for some time.

Another example that comes to my mind is a lottery. Lottery participants can provide only account numbers, and reveal something in the immutable state (like the person name, for example) only after the lottery has ended. The idea that some information can be bound to the account address, but hidden up to some future point in time, seems at least interesting. Of course we may always materialize such a cryptographic commitment explicitly (by creating an account and storing some hash in the immutable state as the actual commitment).

So, to be honest I partially consider this post as a teaser. I would like to hear what others think.

1 Like

Thanks for the investigation and the ideas. I disagree with your assertion that Spacemesh is meaningfully different from Ethereum in this design space, and I think we should not deviate from standard design without good reason. I don’t see good reason.

This used to be true, but it hasn’t been since the introduction of CREATE2 a few years ago. CREATE2 allows a smart contract to be deployed at a predictable address that won’t change depending on state or future events. It works very much like our smart contract accounts (spawned templates), with the difference that it receives bytecode rather than a template address:

new_address = hash(0xFF, sender, salt, bytecode)

Of course, it’s less flexible than our design since the only “hashable” “immutable” state is the salt. But this is sufficient for Ethereum and the same design should be sufficient for Spacemesh, too. Template authors should have the option, and indeed should be encouraged, to include a salt in immutable state. I don’t think we need to do anything more complex than this. In particular this solves the issue you described that:

Moving on to replay attacks:

Cross-chain replay attack protection was added to Ethereum all the way back in 2016 in the form of EIP-155. It’s solved by our GENESISID design which is already implemented, and the spec for which covers prior art including Ethereum’s solution.

We don’t need to account for cross-chain replay protection in templates as it’s handled at the tx level. It has nothing to do with addresses or nonces. (Replay attacks against a single contract on the same chain is another topic, but again, all standard template designs will include a nonce scheme that fixes this.)

This is also not true. One of the most interesting use cases for CREATE2 in Ethereum is counterfactual instantiation of smart contracts (which you’re referring to as “hidden-immutable-state”). There’s a large body of work on this idea in Ethereum; see for instance counterfactual state channels. It’s also very similar to Bitcoin’s recent taproot upgrade.

I don’t agree that the idea that such “hidden information” can accidentally be exposed via a transaction on another chain is a realistic problem or that it needs to be factored into our design. This is a specific case of a much more general problem (that actually has much more realistic applications in cross-domain MEV) that we’re not going to solve on our own. There are many sophisticated ways to address this, such as ZK proofs.

To recap:

Agree

Disagree. GENESISID is included in transactions, and templates can (and should) support a salt in hashable, immutable state. This is sufficient.

Interesting idea. I look forward to discussing.

Thanks!