In part 3 of this series, we explain how we derive the user account address from the root private key on the secure node and from the root public key on the public node.
Now let's see how we can design a secure server setup.
But before that, let's understand the normal process of locally signing transactions as currently implemented by the Ardor wallet itself.
Local signing is composed of the following steps
- Submit the transaction data to a remote full node we do not necessarily trust together with the account public key.
- Receive in response a representation of the transaction bytes.
- Verify that the transaction bytes indeed represent the submitted transaction data, otherwise reject the response.
- Sign the transaction bytes locally.
- Submit the signed transaction bytes to the remote node using the broadcastTransaction API.
While this process has proven itself as secure, recall that in the case of an Ardor wallet, the wallet itself stores (or requests from the user each time) the private key of the account that signs transactions. However, in the online game setup, the public game server only stores the master public key, from which all user account public keys are derived. In contrast to the wallet setup, an additional secure node stores the seed from which all account private keys are derived. Therefore, this requires a different solution.
To support the two nodes solution of (1) light client public server + (2) full node secure server, we need one more piece of secret information that only the public server knows namely a server private key. The server private key is a private key of an empty blockchain account. This limits the damage of a server hack as the server account is empty and no user private keys are stored by the server.
In this setup, the public game server does not need to store the full blockchain. It stores the private key for an empty server account and the master public key for all user accounts.
The secure node is a contract runner node configured to run the payment contract. It stores the master private key (seed) from which all user private keys are derived. It also functions as a bundler node configured to only bundle message transactions submitted by one of the online server accounts using a custom bundler.
To understand this setup, let's see how a tournament participant (the sender) can send the tournament registration fee of 100 IGNIS to the tournament organizer (the recipient).
The process starts with the public node submitting a message transaction to the blockchain. This zero-fee transaction is signed by the server private key, and it is a trigger transaction for the payment contract. The encrypted invocation parameters specify the BIP32 path for the sender and recipient accounts, the amount to send and perhaps additional info like the tournament id.
The secure node is a contract runner node running the payment contract and a bundler node. First, it bundles the server transaction, then once the server transaction is confirmed, it triggers the payment contract which parses the invocation parameters, retrieves the sender private key and recipient public key, calculates the fee and submits the transaction.
The public node can connect to any public node to monitor the transactions and display the up-to-date balances to the players.
The advantage of this solution is that the online public server does not need to store the user's private keys, while the secure server that stores the user's private keys can remain hidden from attackers since there is no direct communication between the public server and the secure node, as all communication relies on blockchain transactions.
This improves privacy since an attacker looking at the blockchain can only see encrypted message transactions and the resulting payments but has no direct way to associate a specific game player with their blockchain account.
To improve scaling, the online server can submit a single message transaction containing the details of many payment transactions. For example, when tournament rewards are being distributed, in the future, we may develop a special payment transaction type with multiple recipients.
The main remaining risk in case the public server is hacked is that the hacker can steal funds by signing transactions with the server's private key that will trick the secure node to submit real malicious payment transactions for draining the user accounts.
To restrict the damage caused by the unlikely event of a hacked server private key, the payment contract should enforce some limits per game server to mitigate losses in case a server is hacked.
The following limits KPIs are enforced
- dailyAmountAccountLimit - total daily amount sent to a given account
- dailyAmountGlobalLimit - total daily amount sent overall
- dailyTransactionsAccountLimit - total number of transactions sent to a given account
- dailyTransactionsGlobalLimit - total number of transactions, overall
If one of these limits is reached, the payment contract does not submit the transaction and a notification is sent to the server operator.
To summarize, we present here a two-server solution that should improve security and privacy compared to a more traditional setup of an online server storing all user private keys.