Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Latest commit

Β 

History

History
433 lines (331 loc) Β· 15.6 KB

lip-0050.md

File metadata and controls

433 lines (331 loc) Β· 15.6 KB
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

Abstract

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.

Copyright

This LIP is licensed under the Creative Commons Zero 1.0 Universal.

Motivation

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.

Specification

Module name

The Legacy module has the name MODULE_NAME_LEGACY (defined in the table below).

Notation and Constants

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.

Type Definition

Name Type Validation Description
PublicKeyEd25519 bytes Must be of length 32. Used for Ed25519 public keys.

Functions from Other Modules

Calling a function fct from another module (named ModuleName) is represented by ModuleName.fct(required inputs).

Legacy Module Store

Legacy Accounts Substore

This substore contains an array with the addresses and the balances of all legacy accounts for which no reclaim transaction was included.

Substore Prefix, Store Key, and Store Value
  • 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 key legacyAddress, deserialized using the legacyAccountsSchema schema.
JSON Schema
legacyAccountsSchema = {
    "type": "object",
    "required": ["balance"],
    "properties": {
        "balance": {
            "dataType": "uint64",
            "fieldNumber": 1
        }
    }
}
Properties

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.

Internal Functions

Legacy Addresses

Obtaining the legacy address of length LENGTH_LEGACY_ADDRESS from a public key.

Returns

The legacy address corresponding to the given input.

Execution
def getLegacyAddress(publicKey: PublicKeyEd25519) -> bytes:
    hashedKey = SHA-256(publicKey)
    firstEightBytes = first LENGTH_LEGACY_ADDRESS bytes of hashedKey
    reversedEightBytes = firstEightBytes reversed
    return reversedEightBytes

Events

AccountReclaimed

This event has name = EVENT_NAME_ACCOUNT_RECLAIMED. This event is emitted when a legacy account is reclaimed.

Topics
  • legacyAddress: the legacy address of the reclaimed account.
  • address: the address of the reclaimed account.
Data
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
        }
    }
}

KeysRegistered

This event has name = EVENT_NAME_KEYS_REGISTERED. This event is emitted when validator keys are registered.

Topics
  • address: the address sending the command.
  • generatorKey: the registered generator key.
  • blsKey: the registered BLS key.
Data
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
        }
    }
}

Commands

Reclaim

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.
Parameters

The params property of a reclaim transaction must obey the following schema:

reclaimParamsSchema = {
    "type": "object",
    "required": ["amount"],
    "properties": {
        "amount": {
            "dataType": "uint64",
            "fieldNumber": 1
        }
    }
}
Verification
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.')
Execution
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
        ]
    )

Register Keys

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.
Parameters

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
        }
    }
}
Verification
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.')
Execution
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)

Protocol Logic for Other Modules

This module does not expose any functions.

Endpoints for Off-Chain Services

This section specifies the non-trivial or recommended endpoints of the Legacy module and does not include all endpoints.

getLegacyAccount

This function provides the legacy address and balance of the corresponding legacy accounts.

Returns

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.

Execution
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}

Genesis Block Processing

Genesis State Initialization

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 in genesisBlockAssetObject.accounts are pairwise distinct. If not, reject the block.
  • Check if the sum of the balance properties of the entries in genesisBlockAssetObject.accounts is less than 264. If not, reject the block.
  • Check if the sum of the balance properties of the entries in genesisBlockAssetObject.accounts equals Token.getLockedAmount(ADDRESS_LEGACY_RESERVE, MODULE_NAME_LEGACY, Token.getTokenIDLSK()).
  • For every account in genesisBlockAssetObject.accounts:
    • Create an entry in the legacy accounts substore with storeKey = account.address and storeValue being the serialized value of account.balance according to legacyAccountsSchema.

Backwards Compatibility

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.

Reference Implementation

https://github.com/LiskHQ/lisk-core/tree/v4.0.0/src/application/modules/legacy