Skip to content

Latest commit

Β 

History

History
623 lines (516 loc) Β· 27.7 KB

lip-0041.md

File metadata and controls

623 lines (516 loc) Β· 27.7 KB
LIP: 0041
Title: Introduce Auth module
Author: Alessandro Ricottone <[email protected]>
        Ishan Tiwari <[email protected]>
Discussions-To: https://research.lisk.com/t/introduce-auth-module
Status: Draft
Type: Standards Track
Created: 2021-07-27
Updated: 2023-02-22
Requires: 0040

Abstract

The Auth module is responsible for handling and verifying nonces and for transaction signature validation, including transactions from multisignature accounts. In this LIP, we specify the properties of the Auth module, along with their serialization and default values. Furthermore, we specify the state transitions logic defined within this module, i.e. the commands, the protocol logic injected during the block lifecycle, and the functions that can be called from other modules or off-chain services.

Copyright

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

Motivation

In the Lisk protocol, some verification steps are common to all transactions. In particular, to validate a transaction it is necessary to validate the signatures and the nonce. These validation steps are handled by the authentication (Auth) module. The Auth module supersedes the Keys and Sequence modules.

In this LIP we specify the properties, serialization, and default values of the Auth module, as well as the protocol logic processed during a block lifecycle, the commands, and the functions exposed to other modules and to off-chain services.

Rationale

Validating a transaction requires reading the state to get the nonce and possibly the multisignature keys from the user account. Since both these checks are necessarily done together, it makes sense to store these values together, to reduce the number of database accesses, rather than in two separate modules.

Specification

In this section, we specify the substores that are part of the Auth module store, the commands, and the protocol logic called during the block lifecycle. The Auth module has module name MODULE_NAME_AUTH (see the table below).

Constants

We define the following constants:

Name Type Value Description
MODULE_NAME_AUTH string "auth" Name of the Auth module.
SUBSTORE_PREFIX_AUTH bytes 0x0000 Substore prefix of the auth data substore.
COMMAND_REGISTER_MULTISIGNATURE string "registerMultisignature" Name of the register multisignature command.
EVENT_NAME_MULTISIGNATURE_REGISTERED string "multisignatureRegistration" Name of the signature registered event.
EVENT_NAME_INVALID_SIGNATURE string "invalidSignature" Name of the invalid signature event.
MESSAGE_TAG_MULTISIG_REG bytes "LSK_RMSG_" as ASCII-encoded literal Message tag for the signatures contained in the register multisignature command (see LIP 0037).
MESSAGE_TAG_TRANSACTION bytes "LSK_TX_" as ASCII-encoded literal Message tag for transaction signatures (see LIP 0037).
MAX_NUMBER_OF_SIGNATURES uint32 64 Max number of signatures for a multisignature.
ADDRESS_LENGTH uint32 20 Length in bytes of type Address.
ED25519_PUBLIC_KEY_LENGTH uint32 32 Length in bytes of type PublicKeyEd25519.
ED25519_SIGNATURE_LENGTH uint32 64 Length in bytes of type SignatureEd25519.

Type Definition

Name Type Validation Description
Address bytes Must be of length ADDRESS_LENGTH. Address of an account.
PublicKeyEd25519 bytes Must be of length ED25519_PUBLIC_KEY_LENGTH. Used for Ed25519 public keys.
SignatureEd25519 bytes Must be of length ED25519_SIGNATURE_LENGTH. Used for Ed25519 signatures.
AuthAccount object Must follow the authAccountSchema schema. An object representing a auth account.

Auth Module Store

The key-value pairs in the module store are organized in the following substore.

Auth Data Substore

Substore Prefix, Store Key, and Store Value
  • The substore prefix is set to SUBSTORE_PREFIX_AUTH.
  • Store keys are set to ADDRESS_LENGTH bytes addresses, representing a user address.
  • Store values are set to auth account data structures, holding the properties indicated below, serialized using the JSON schema authAccountSchema, presented below.
  • Notation: For the rest of this proposal let authAccount(address) be an entry in the auth data substore identified by the store key address, deserialized using authAccountSchema schema.
JSON Schema
authAccountSchema = {
    "type": "object",
    "required": ["nonce", "numberOfSignatures", "mandatoryKeys", "optionalKeys"],
    "properties": {
        "nonce": {
            "dataType": "uint64",
            "fieldNumber": 1
        },
        "numberOfSignatures": {
            "dataType": "uint32",
            "fieldNumber": 2
        },
        "mandatoryKeys": {
            "type": "array",
            "fieldNumber": 3,
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            }
        },
        "optionalKeys": {
            "type": "array",
            "fieldNumber": 4,
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            }
        }
    }
}
Properties and Default values

In this section, we describe the properties of an auth account and specify their default values.

  • nonce: The nonce represents the total number of transactions sent from the account. Each time a new transaction is sent, the nonce is incremented by one. The default value of this property is 0.
  • numberOfSignatures: The number of private keys that must sign a transaction. This value is greater than 0 if and only if a register multisignature transaction for this account was included. The default value of this property is 0.
  • mandatoryKeys: An array of public keys in lexicographical order. The corresponding private keys have to necessarily sign the transaction. A valid public key is ED25519_PUBLIC_KEY_LENGTH bytes long. The default value of this property is an empty array.
  • optionalKeys: An array of public keys in lexicographical order. The corresponding private keys can optionally sign the transaction. The number of corresponding private keys that have to sign the transaction equals numberOfSignatures minus the length of mandatoryKeys. A valid public key is ED25519_PUBLIC_KEY_LENGTH bytes long. The default value of this property is an empty array.

Events

MultisignatureRegistered

This event has name = EVENT_NAME_MULTISIGNATURE_REGISTERED. This event is emitted when a multisignature is registered.

Topics
  • address: The address for which the multisignature has been registered.
Data
multisigRegDataSchema = {
    "type": "object",
    "required": ["numberOfSignatures", "mandatoryKeys", "optionalKeys"],
    "properties": {
        "numberOfSignatures": {
            "dataType": "uint32",
            "fieldNumber": 1
        },
        "mandatoryKeys": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            },
            "fieldNumber": 2
        },
        "optionalKeys": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            },
            "fieldNumber": 3
        }
    }
}

InvalidSignature

This event has name = EVENT_NAME_INVALID_SIGNATURE. This event is emitted when the signature validation of a multisignature fails.

Topics
  • address: The address for which the multisignature has been registered.
Data
invalidSigDataSchema = {
    "type": "object",
    "required": ["numberOfSignatures", "mandatoryKeys", "optionalKeys", "failingPublicKey", "failingSignature"],
    "properties": {
        "numberOfSignatures": {
            "dataType": "uint32",
            "fieldNumber": 1
        },
        "mandatoryKeys": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            },
            "fieldNumber": 2
        },
        "optionalKeys": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            },
            "fieldNumber": 3
        },
        "failingPublicKey": {
            "dataType": "bytes",
            "length" : ED25519_PUBLIC_KEY_LENGTH,
            "fieldNumber": 4
        },
        "failingSignature": {
            "dataType": "bytes",
            "length" : ED25519_SIGNATURE_LENGTH,
            "fieldNumber": 5
        }
    }
}

Commands

RegisterMultisignature

This command allows users to register a multisignature for the sender account. These specifications supersede those given in LIP 0017. The function verifyEd25519 is defined in LIP 0062 and chainID is the chain identifier of the chain.

Transactions executing this command have:

  • module = MODULE_NAME_AUTH,
  • command = COMMAND_REGISTER_MULTISIGNATURE.
Parameters

The params property of a multisignature registration transaction must obey the following schema.

multisigRegParamsSchema = {
    "type": "object",
    "required": ["numberOfSignatures",  "mandatoryKeys", "optionalKeys", "signatures"],
    "properties": {
        "numberOfSignatures": {
            "dataType": "uint32",
            "fieldNumber": 1
        },
        "mandatoryKeys": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            },
            "fieldNumber": 2
        },
        "optionalKeys": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            },
            "fieldNumber": 3
        },
        "signatures": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_SIGNATURE_LENGTH
            },
            "fieldNumber": 4
        }
    }
 }

The schema multisigRegParamsSchema contains 4 properties. This schema supersedes the one defined in LIP 0028.

  1. numberOfSignatures: The number of private keys that must sign a transaction.
  2. mandatoryKeys: An array of public keys. A valid public key is ED25519_PUBLIC_KEY_LENGTH bytes long. Once the account is registered as a multisignature account, every outgoing transaction requires a signature for every public key in mandatoryKeys.
  3. optionalKeys: An array of public keys. A valid public key is ED25519_PUBLIC_KEY_LENGTH bytes long. Once the account is registered as a multisignature account, every outgoing transaction requires some signatures for some public keys in optionalKeys (the number of needed signatures depends on the numberOfSignatures property and may also be zero).
  4. signatures: An array of signatures, corresponding to the public keys contained in mandatoryKeys and optionalKeys. All public keys must have a corresponding signature. The signatures corresponding to mandatoryKeys are appended first, followed by those corresponding to optionalKeys, where the order of signatures is the same as the order of public keys in the respective array. The message that is signed contains the parameters of the transaction, serialized using the multisigRegMsgSchema schema given below.
multisigRegMsgSchema = {
    "type": "object",
    "required": ["address", "nonce", "numberOfSignatures", "mandatoryKeys",  "optionalKeys"],
    "properties": {
        "address": {
            "dataType": "bytes",
            "length" : ADDRESS_LENGTH
            "fieldNumber": 1
        },
        "nonce": {
            "dataType": "uint64",
            "fieldNumber": 2
        },
        "numberOfSignatures": {
            "dataType": "uint32",
            "fieldNumber": 3
        },
        "mandatoryKeys": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            },
            "fieldNumber": 4
        },
        "optionalKeys": {
            "type": "array",
            "items": {
                "dataType": "bytes",
                "length" : ED25519_PUBLIC_KEY_LENGTH
            },
            "fieldNumber": 5
        }
    }
}
Verification
def verify(trs: Transaction) -> None:
    trsParams = decode(multisigRegParamsSchema, trs.params)
    validateObjectSchema(multisigRegParamsSchema, trsParams)

    # Mandatory keys are pairwise distinct and ordered lexicographically. The array can be empty.
    if trsParams.mandatoryKeys != sorted(set(trsParams.mandatoryKeys)):
        raise Exception('Invalid mandatoryKeys property.')
    # Optional keys are pairwise distinct and ordered lexicographically. The array can be empty.
    if trsParams.optionalKeys != sorted(set(trsParams.optionalKeys)):
        raise Exception('Invalid optionalKeys property.')
    # Mandatory and optional keys must be disjoint.
    allKeys = trsParams.mandatoryKeys + trsParams.optionalKeys
    if len(allKeys) != len(set(allKeys)):
        raise Exception('mandatoryKeys and optionalKeys properties are not disjoint arrays.')
    # Union of mandatory and optional keys must contain between 1 and MAX_NUMBER_OF_SIGNATURES keys (included).
    if len(allKeys) < 1 or len(allKeys) > MAX_NUMBER_OF_SIGNATURES:
        raise Exception('Invalid length of mandatoryKeys and optionalKeys properties.')
    # Number of signatures must be an integer between 1 and the total number of public keys.
    if trsParams.numberOfSignatures < 1 or trsParams.numberOfSignatures > len(allKeys):
        raise Exception('Invalid numberOfSignatures property.')
    # Number of signatures must not be smaller than the number of mandatory keys.
    if trsParams.numberOfSignatures < len(trsParams.mandatoryKeys):
        raise Exception('Invalid numberOfSignatures property.')
    # Each key must have a corresponding signature.
    if len(trsParams.signatures) != len(allKeys):
        raise Exception('Invalid length of signatures property.')
Execution
def execute(trs: Transaction) -> None:
    trsParams = decode(multisigRegParamsSchema, trs.params)
    # Verify the signatures.
    message = encode(
        schema = multisigRegMsgSchema,
        object = {
            "address": senderAddress,
            "nonce": trs.nonce,
            "numberOfSignatures": trsParams.numberOfSignatures,
            "mandatoryKeys": trsParams.mandatoryKeys,
            "optionalKeys": trsParams.optionalKeys
        }
    )
    allKeys = trsParams.mandatoryKeys + trsParams.optionalKeys
    for signature, key in zip(trsParams.signatures, allKeys):
        if not verifyEd25519(key, MESSAGE_TAG_MULTISIG_REG, chainID, message, signature):
            emitPersistentEvent(
                module = MODULE_NAME_AUTH,
                name = EVENT_NAME_INVALID_SIGNATURE,
                data = {
                    "numberOfSignatures": trsParams.numberOfSignatures,
                    "mandatoryKeys": trsParams.mandatoryKeys,
                    "optionalKeys": trsParams.optionalKeys,
                    "failingPublicKey": key,
                    "failingSignature": signature
                },
                topics=[senderAddress]
            )
            raise Exception(f'Invalid signature for public key {key.hex()}.')

    senderAddress = Address derived from trs.senderPublicKey
    authAccount(senderAddress).numberOfSignatures = trsParams.numberOfSignatures
    authAccount(senderAddress).mandatoryKeys = trsParams.mandatoryKeys
    authAccount(senderAddress).optionalKeys = trsParams.optionalKeys

    emitEvent(
        module = MODULE_NAME_AUTH,
        name = EVENT_NAME_MULTISIGNATURE_REGISTERED,
        data = {
            "numberOfSignatures": trsParams.numberOfSignatures,
            "mandatoryKeys": trsParams.mandatoryKeys,
            "optionalKeys": trsParams.optionalKeys
        },
        topics=[senderAddress]
    )

Internal Functions

The Auth module has the following internal function.

isMultisignatureAccount

This function checks whether the account corresponding to the input address has registered a multisignature.

def isMultisignatureAccount(address: Address) -> bool:
    if there is no entry in the auth data substore with storeKey == address:
        return False

    if authAccount(senderAddress).numberOfSignatures == 0:
        return False

    return True

Protocol Logic for Other Modules

getAuthAccount

This function is used to retrieve information about the auth account corresponding to the input address. It returns authAccount(address). If there is no entry corresponding to address, it throws an error.

def getAuthAccount(address: Address) -> AuthAccount:
    if no entry in the auth data substore exist with storeKey == address:
        raise Exception('No auth account found for the input address.')
    return authAccount(address)

Endpoints for Off-Chain Services

getAuthAccount

This function works exactly as the function getAuthAccount defined above.

isValidSignature

This function verifies the signatures of a given transaction trs, including transactions from multisignature accounts. It returns true if the transaction object contains a valid signature, false otherwise. This is done using the verifySignatures(trs) function defined below.

def isValidSignature(trs: Transaction) -> bool:
    try:
        verifySignatures(trs)
        return True
    except:
        return False

isValidNonce

This function verifies the nonce of a given transaction trs and returns a boolean value. It returns true if the transaction object contains a valid nonce, false otherwise. This is done using the verifyNonce(trs) function defined below.

def isValidNonce(trs: Transaction) -> bool:
    try:
        verifyNonce(trs)
        return True
    except:
        return False

Genesis Block Processing

Genesis State Initialization

During the genesis state initialization stage, the following steps are executed. If any step fails, the block is discarded and has no further effect.

Let genesisBlockAssetBytes be the data bytes included in the block assets for the Auth module and let genesisBlockAssetObject be the deserialization of genesisBlockAssetBytes according to the genesisAuthStoreSchema schema, given below.

  • For each entry entry in genesisBlockAssetObject.authDataSubstore, let address = entry.address and authAccount = entry.authAccount and check the following:
    • Each entry has a different address and all address have length ADDRESS_LENGTH.
    • If authAccount.numberOfSignatures is not 0 (a multisignature account), check that the rules listed in LIP 0017 are respected. In particular:
      • The keys in the authAccount.mandatoryKeys array have length ED25519_PUBLIC_KEY_LENGTH, are pairwise distinct and in lexicographical order. The array may be empty.
      • The keys in the authAccount.optionalKeys array have length ED25519_PUBLIC_KEY_LENGTH, are pairwise distinct and in lexicographical order. The array may be empty.
      • The arrays authAccount.mandatoryKeys and authAccount.optionalKeys are disjoint; their union has at least one element and at most MAX_NUMBER_OF_SIGNATURES elements.
      • authAccount.numberOfSignatures is an integer between 1 and the total number of public keys of the account (sum of the length of authAccount.mandatoryKeys and authAccount.optionalKeys).
      • authAccount.numberOfSignatures is not smaller than the length of authAccount.mandatoryKeys.
  • Finally, for each entry entry in genesisBlockAssetObject.authDataSubstore, create a corresponding entry in the auth data substore with storeKey = entry.address and storeValue = entry.authAccount.
genesisAuthStoreSchema = {
    "type": "object",
    "required": ["authDataSubstore"],
    "properties": {
        "authDataSubstore": {
            "type": "array",
            "fieldNumber": 1,
            "items": {
                "type": "object",
                "required": ["address", "authAccount"],
                "properties": {
                    "address": {
                        "dataType": "bytes",
                        "length": ADDRESS_LENGTH,
                        "fieldNumber": 1
                    },
                    "authAccount": {
                        "type": "object",
                        "fieldNumber": 2,
                        "required": [
                            "nonce",
                            "numberOfSignatures",
                            "mandatoryKeys",
                            "optionalKeys"
                        ],
                        "properties": {
                            "nonce": {
                                "dataType": "uint64",
                                "fieldNumber": 1
                            },
                            "numberOfSignatures": {
                                "dataType": "uint32",
                                "fieldNumber": 2
                            },
                            "mandatoryKeys": {
                                "type": "array",
                                "fieldNumber": 3,
                                "items": {
                                    "dataType": "bytes",
                                    "length": ED25519_PUBLIC_KEY_LENGTH
                                }
                            },
                            "optionalKeys": {
                                "type": "array",
                                "fieldNumber": 4,
                                "items": {
                                    "dataType": "bytes",
                                    "length": ED25519_PUBLIC_KEY_LENGTH,
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Genesis State Finalization

The Auth module does not execute any logic during the genesis state finalization.

Block Processing

Transaction Verification

As part of the verification of a transaction trs, the nonce and signatures properties are verified as follows.

def verifyTransaction(trs: Transaction) -> None:
    verifyNonce(trs)
    verifySignatures(trs)

We define the following auxiliary function to verify the nonce of a transaction. Notice that a transaction in the transaction pool with a nonce greater than the account nonce is considered "pending" rather than invalid, since it could become valid in the future.

def verifyNonce(trs: Transaction) -> None:
    senderAddress = Address derived from trs.senderPublicKey
    if there is no entry in the auth data substore with storeKey == senderAddress:
        expectedNonce = 0
    else:
        expectedNonce = authAccount(senderAddress).nonce

    if trs.nonce != expectedNonce:
        raise Exception(f'Invalid nonce property. Expected {expectedNonce}, got {trs.nonce}.')

We define the following auxiliary function to verify the validity of a signature. These specifications supersede those given in LIP 0017. The function verifyEd25519 is defined in LIP 0062 and chainID is the chain identifier of the chain.

def verifySignatures(trs: Transaction) -> None:
    # Remove signatures from transaction before serialization.
    unsignedTrs = trs
    unsignedTrs.signatures = []
    unsignedTrsBytes = encode(
        schema = transactionSchema,
        object = unsignedTrs
    )

    # Check if the account is a multisignature account.
    senderAddress = Address derived from trs.senderPublicKey

    # Multisignature account case.
    if isMultisignatureAccount(senderAddress):
        allKeys = authAccount(senderAddress).mandatoryKeys + authAccount(senderAddress).optionalKeys
        if len(trs.signatures) != len(allKeys):
            raise Exception(f'Transactions from this multisignature account should have exactly {len(allKeys)} signatures. Found {len(trs.signatures)} signatures.')

        nonEmptySignatures = [sig for sig in trs.signatures if len(sig) > 0]
        if len(nonEmptySignatures) != authAccount(senderAddress).numberOfSignatures:
            raise Exception(f'Transaction signatures does not match required number of signatures: {authAccount(senderAddress).numberOfSignatures}')

        numberOfMandatoryKeys = len(authAccount(senderAddress).mandatoryKeys)
        for idx in range(numberOfMandatoryKeys):
            key = authAccount(senderAddress).mandatoryKeys[idx]
            if not verifyEd25519(key, MESSAGE_TAG_TRANSACTION, chainID, unsignedTrsBytes, trs.signatures[idx]):
                raise Exception(f'Invalid signature for public key {key.hex()}.')

        for idx in range(numberOfMandatoryKeys, len(allKeys)):
            # Skip empty signature.
            if len(trs.signatures[idx]) == 0:
                continue

            key = authAccount(senderAddress).optionalKeys[idx - numberOfMandatoryKeys]
            if not verifyEd25519(key, MESSAGE_TAG_TRANSACTION, chainID, unsignedTrsBytes, trs.signatures[idx]):
                raise Exception(f'Invalid signature for public key {key.hex()}.')

    # Single signature account case.
    else:
        if len(trs.signatures) != 1:
            raise Exception(f'Transactions from a single signature account should have exactly one signature. Found {len(trs.signatures)} signatures.')

        if not verifyEd25519(trs.senderPublicKey, MESSAGE_TAG_TRANSACTION, chainID, unsignedTrsBytes, trs.signatures[0]):
            raise Exception(f'Invalid signature for public key {key.hex()}.')

Before Command Execution

Before the command referenced in a transaction trs is executed, an auth account is created if no account exists for the sender address.

def beforeCommandExecute(trs: Transaction) -> None:
    senderAddress = Address derived from trs.senderPublicKey
    if there is no entry in the auth data substore with storeKey == senderAddress:
        create an entry in the auth data substore with storeKey = senderAddress, initialized to default values
    authAccount(senderAddress).nonce += 1

Backwards Compatibility

This LIP defines a new storage interface for the Auth module, which in turn will become part of the state tree and will be authenticated by the state root. An existing chain including the Auth module will need to perform a hardfork.