LIP: 0050
Title: Introduce Legacy module
Author: Andreas Kendziorra <[email protected]>
Maxime Gagnebin <[email protected]>
Rishi Mittal <[email protected]>
Discussions-To: https://research.lisk.com/t/introduce-legacy-module/319
Status: Active (Lisk Core only)
Type: Standards Track
Created: 2021-08-18
Updated: 2024-01-04
Requires: 0018
The Legacy module maintains all accounts on the Lisk mainchain that received balance transfers to their address in the old 8-byte format and for which no public key is associated. The Legacy module also implements a command allowing validators without a BLS key to register one.
In this LIP, we specify the properties of the Legacy module, along with their serialization and default values. Furthermore, we specify the commands and the functions that can be called from off-chain services.
This module is only needed for the Lisk mainchain.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
Once LIP 0018 is active on the Lisk mainchain, all nodes for the Lisk mainchain must maintain the accounts that received some funds before the implementation of LIP 0018, but do not have an associated public key. The balance of these accounts is maintained in the legacy accounts substore and can be recovered with a reclaim transaction.
Furthermore, validators registered before the implementation of the Validators module do not have a registered BLS key. This module implements a command to allow those validators to register a BLS key and hence participate in the certificate generation process. With the same command, validators can update their generator key, note that this update can only be done once when setting the BLS key.
Implementing the Legacy module avoids the need for other modules (specifically the Token module and the Validators module) to handle legacy behaviors present only on the Lisk mainchain. This module is only part of the Lisk mainchain, and should not be implemented in any sidechain.
The Legacy module has the name MODULE_NAME_LEGACY
(defined in the table below).
We define the following constants:
Name | Type | Value | Description |
---|---|---|---|
MODULE_NAME_LEGACY |
string | "legacy" | The name of the Legacy module. |
COMMAND_RECLAIM |
string | "reclaimLSK" | The name of the reclaim command. |
COMMAND_REGISTER_KEYS |
string | "registerKeys" | The name of the register keys command. |
EVENT_NAME_ACCOUNT_RECLAIMED |
string | "accountReclaimed" | The name of the account reclaimed event. |
EVENT_NAME_KEYS_REGISTERED |
string | "keysRegistered" | The name of the keys registered event. |
SUBSTORE_PREFIX_LEGACY_ACCOUNTS |
bytes | 0x0000 | Substore prefix of the legacy accounts substore. This contains the addresses and balances of legacy accounts. |
LENGTH_ADDRESS |
uint32 | 20 | The length of an address in bytes. |
LENGTH_LEGACY_ADDRESS |
uint32 | 8 | The length of a legacy address in bytes |
LENGTH_BLS_KEY |
uint32 | 48 | The length of a BLS key in bytes |
LENGTH_PROOF_OF_POSSESSION |
uint32 | 96 | The length of a proof of possession in bytes |
LENGTH_GENERATOR_KEY |
uint32 | 32 | The length of a generator key in bytes |
INVALID_BLS_KEY |
bytes | LENGTH_BLS_KEY bytes, all set to 0 |
The BLS public key set during the migration for validators without a BLS key. |
INVALID_ED25519_KEY |
bytes | LENGTH_GENERATOR_KEY bytes, all set to 255 |
The Ed25519 public key set during the migration for validators without a generator key. |
ADDRESS_LEGACY_RESERVE |
bytes | SHA-256(b'legacyReserve')[:LENGTH_ADDRESS] |
The address used to store all tokens of legacy accounts. |
Name | Type | Validation | Description |
---|---|---|---|
PublicKeyEd25519 |
bytes | Must be of length 32. | Used for Ed25519 public keys. |
Calling a function fct
from another module (named ModuleName
) is represented by ModuleName.fct(required inputs)
.
This substore contains an array with the addresses and the balances of all legacy accounts for which no reclaim transaction was included.
- The substore prefix is set to
SUBSTORE_PREFIX_LEGACY_ACCOUNTS
. - The store key is a byte array of length
LENGTH_LEGACY_ADDRESS
representing the legacy address. - The store value is set to the serialization using
legacyAccountsSchema
of the balance of the legacy account. - Notation: For the rest of this proposal, let
legacyAccounts(legacyAddress)
be the legacy accounts substore entry with store keylegacyAddress
, deserialized using thelegacyAccountsSchema
schema.
legacyAccountsSchema = {
"type": "object",
"required": ["balance"],
"properties": {
"balance": {
"dataType": "uint64",
"fieldNumber": 1
}
}
}
This substore contains an entry for each legacy address for which no reclaim transaction was included. See also the βAccounts without Public Keyβ section in LIP 0018.
Obtaining the legacy address of length LENGTH_LEGACY_ADDRESS
from a public key.
The legacy address corresponding to the given input.
def getLegacyAddress(publicKey: PublicKeyEd25519) -> bytes:
hashedKey = SHA-256(publicKey)
firstEightBytes = first LENGTH_LEGACY_ADDRESS bytes of hashedKey
reversedEightBytes = firstEightBytes reversed
return reversedEightBytes
This event has name = EVENT_NAME_ACCOUNT_RECLAIMED
.
This event is emitted when a legacy account is reclaimed.
legacyAddress
: the legacy address of the reclaimed account.address
: the address of the reclaimed account.
accountReclaimedEventDataSchema = {
"type": "object",
"required" = ["legacyAddress", "address", "amount"],
"properties": {
"legacyAddress": {
"dataType": "bytes",
"length": LENGTH_LEGACY_ADDRESS,
"fieldNumber": 1
},
"address": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 2
},
"amount": {
"dataType": "uint64",
"fieldNumber": 3
}
}
}
This event has name = EVENT_NAME_KEYS_REGISTERED
.
This event is emitted when validator keys are registered.
address
: the address sending the command.generatorKey
: the registered generator key.blsKey
: the registered BLS key.
keysRegisteredEventDataSchema = {
"type": "object",
"required" = ["address", "generatorKey", "blsKey"],
"properties": {
"address": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 1
},
"generatorKey": {
"dataType": "bytes",
"length": LENGTH_GENERATOR_KEY,
"fieldNumber": 2
},
"blsKey": {
"dataType": "bytes",
"length": LENGTH_BLS_KEY,
"fieldNumber": 3
}
}
}
This command allows users to reclaim tokens from a legacy account as defined in LIP 0018. Here, we clarify the verification and execution logic with respect to the module store.
Transactions executing this command have:
module = MODULE_NAME_LEGACY
,command = COMMAND_RECLAIM
.
The params
property of a reclaim transaction must obey the following schema:
reclaimParamsSchema = {
"type": "object",
"required": ["amount"],
"properties": {
"amount": {
"dataType": "uint64",
"fieldNumber": 1
}
}
}
def verify(trs: Transaction) -> None:
trsParams = decode(reclaimParamsSchema, trs.params)
legacyAddress = getLegacyAddress(trs.senderPublicKey)
if legacyAccounts(legacyAddress) is empty:
raise Exception('Public key does not correspond to a reclaimable account.')
if legacyAccounts(legacyAddress).balance != trsParams.amount:
raise Exception('Input amount does not equal the balance of the legacy account.')
def execute(trs: Transaction) -> None:
trsParams = decode(reclaimParamsSchema, trs.params)
legacyAddress = getLegacyAddress(trs.senderPublicKey)
delete legacyAccounts(legacyAddress) from the legacy accounts substore
newAddress = SHA-256(trs.senderPublicKey)[:LENGTH_ADDRESS]
# Unlock the tokens in the legacy reserve account and transfer them to the
# account reclaiming the tokens.
Token.unlock(ADDRESS_LEGACY_RESERVE, MODULE_NAME_LEGACY, Token.getTokenIDLSK(), trsParams.amount)
Token.transfer(ADDRESS_LEGACY_RESERVE, newAddress, Token.getTokenIDLSK(), trsParams.amount)
emitEvent(
module=MODULE_NAME_LEGACY,
name=EVENT_NAME_ACCOUNT_RECLAIMED,
data={
"legacyAddress": legacyAddress,
"address": newAddress,
"amount": trsParams.amount
},
topics=[
legacyAddress,
newAddress
]
)
This command allows migrated legacy validators register the required keys. In particular, this command is used by all migrated validators to register a BLS key. This command cannot be used to modify an existing BLS key. Transactions executing this command have:
module = MODULE_NAME_LEGACY
,command = COMMAND_REGISTER_KEYS
.
The params
property of a register BLS key transaction must obey the following schema:
registerKeysParamsSchema = {
"type": "object",
"required": ["blsKey", "proofOfPossession", "generatorKey"],
"properties": {
"blsKey": {
"dataType": "bytes",
"length": LENGTH_BLS_KEY,
"fieldNumber": 1
},
"proofOfPossession": {
"dataType": "bytes",
"length": LENGTH_PROOF_OF_POSSESSION,
"fieldNumber": 2
},
"generatorKey": {
"dataType": "bytes",
"length": LENGTH_GENERATOR_KEY,
"fieldNumber": 3
}
}
}
def verify(trs: Transaction) -> None:
validatorAddress = SHA-256(trs.senderPublicKey)[:LENGTH_ADDRESS]
# An exception would also be raised if no validator account for this address.
if Validators.getValidatorKeys(validatorAddress).blsKey != INVALID_BLS_KEY:
raise Exception('Validator already has a registered BLS key.')
def execute(trs: Transaction) -> None:
trsParams = decode(registerKeysParamsSchema, trs.params)
validatorAddress = SHA-256(trs.senderPublicKey)[:LENGTH_ADDRESS]
Validators.setValidatorGeneratorKey(validatorAddress, trsParams.generatorKey)
# The calls below will raise an exception if the proof of possession is invalid
# with respect to the given BLS key.
Validators.setValidatorBLSKey(validatorAddress, trsParams.proofOfPossession, trsParams.blsKey)
emitEvent(
module=MODULE_NAME_LEGACY,
name=EVENT_NAME_KEYS_REGISTERED,
data={
"address": validatorAddress,
"generatorKey": trsParams.generatorKey,
"blsKey": trsParams.blsKey
},
topics=[
validatorAddress,
trsParams.generatorKey,
trsParams.blsKey
]
)
pos.unbanValidator(validatorAddress)
This module does not expose any functions.
This section specifies the non-trivial or recommended endpoints of the Legacy module and does not include all endpoints.
This function provides the legacy address and balance of the corresponding legacy accounts.
An object with the properties legacyAddress
and balance
, where legacyAddress
is the legacy address for publicKey
and balance
is the balance of the corresponding legacy account, or with 0
balance if the account is not available for reclaim.
def getLegacyAccount(publicKey: PublicKeyEd25519) -> dict:
legacyAddress = getLegacyAddress(publicKey)
if legacyAccounts(legacyAddress) is empty:
return {"legacyAddress": legacyAddress,
"balance": 0}
else:
balance = legacyAccounts(legacyAddress).balance
return {"legacyAddress": legacyAddress,
"balance": balance}
Let genesisBlockAssetBytes
be the data
bytes included in the block assets for the legacy module and let genesisBlockAssetObject
be the deserialization of genesisBlockAssetBytes
according to the genesisLegacyStoreSchema
schema given below. If the deserialization fails, reject the block.
genesisLegacyStoreSchema = {
"type": "object",
"required": ["accounts"],
"properties": {
"accounts": {
"type": "array",
"fieldNumber": 1,
"items": {
"type": "object",
"required": ["address", "balance"],
"properties": {
"address": {
"dataType": "bytes",
"length": LENGTH_LEGACY_ADDRESS,
"fieldNumber": 1
},
"balance": {
"dataType": "uint64",
"fieldNumber": 2
}
}
}
}
}
}
Then, do the following:
- Check if the
address
properties of the entries ingenesisBlockAssetObject.accounts
are pairwise distinct. If not, reject the block. - Check if the sum of the
balance
properties of the entries ingenesisBlockAssetObject.accounts
is less than 264. If not, reject the block. - Check if the sum of the
balance
properties of the entries ingenesisBlockAssetObject.accounts
equalsToken.getLockedAmount(ADDRESS_LEGACY_RESERVE, MODULE_NAME_LEGACY, Token.getTokenIDLSK())
. - For every
account
ingenesisBlockAssetObject.accounts
:- Create an entry in the legacy accounts substore with
storeKey = account.address
andstoreValue
being the serialized value ofaccount.balance
according tolegacyAccountsSchema
.
- Create an entry in the legacy accounts substore with
This LIP defines a new module and specifies its store, which in turn will become part of the state tree and will be authenticated by the state root. As such, it will induce a hardfork.
https://github.com/LiskHQ/lisk-core/tree/v4.0.0/src/application/modules/legacy