LIP: 0041
Title: Introduce Auth module
Author: Alessandro Ricottone <alessandro.ricottone@lightcurve.io>
Ishan Tiwari <ishan.tiwari@lightcurve.io>
Discussions-To: https://research.lisk.com/t/introduce-auth-module
Status: Active
Type: Standards Track
Created: 2021-07-27
Updated: 2024-01-04
Requires: 0040
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.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
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.
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.
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).
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 . |
NONCE_VERIFY_STATUS_FAIL |
int32 | -1 | Return value of nonce verification when transaction nonce is invalid and will never become valid (smaller than account nonce). |
NONCE_VERIFY_STATUS_PENDING |
int32 | 0 | Return value of nonce verification when transaction nonce is currently invalid but could become valid in the future (larger than account nonce). |
NONCE_VERIFY_STATUS_OK |
int32 | 1 | Return value of nonce verification when transaction nonce is valid (equal to account nonce). |
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. |
The key-value pairs in the module store are organized in the following substore.
- 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 keyaddress
, deserialized usingauthAccountSchema
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
}
}
}
}
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 isED25519_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 equalsnumberOfSignatures
minus the length ofmandatoryKeys
. A valid public key isED25519_PUBLIC_KEY_LENGTH
bytes long. The default value of this property is an empty array.
This event has name = EVENT_NAME_MULTISIGNATURE_REGISTERED
. This event is emitted when a multisignature is registered.
address
: The address for which the multisignature has been registered.
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
}
}
}
This event has name = EVENT_NAME_INVALID_SIGNATURE
. This event is emitted when the signature validation of a multisignature fails.
address
: The address for which the multisignature has been registered.
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
}
}
}
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
.
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.
numberOfSignatures
: The number of private keys that must sign a transaction.mandatoryKeys
: An array of public keys. A valid public key isED25519_PUBLIC_KEY_LENGTH
bytes long. Once the account is registered as a multisignature account, every outgoing transaction requires a signature for every public key inmandatoryKeys
.optionalKeys
: An array of public keys. A valid public key isED25519_PUBLIC_KEY_LENGTH
bytes long. Once the account is registered as a multisignature account, every outgoing transaction requires some signatures for some public keys inoptionalKeys
(the number of needed signatures depends on thenumberOfSignatures
property and may also be zero).signatures
: An array of signatures, corresponding to the public keys contained inmandatoryKeys
andoptionalKeys
. All public keys must have a corresponding signature. The signatures corresponding tomandatoryKeys
are appended first, followed by those corresponding tooptionalKeys
, 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 themultisigRegMsgSchema
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
}
}
}
def verify(trs: Transaction) -> None:
trsParams = decode(multisigRegParamsSchema, trs.params)
# 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.')
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]
)
The Auth module has the following internal function.
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
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 returns an account object with all properties set to their default values.
def getAuthAccount(address: Address) -> AuthAccount:
if no entry in the auth data substore exist with storeKey == address:
defaultAccount = {
"nonce": 0,
"numberOfSignatures": 0,
"mandatoryKeys": [],
"optionalKeys": []
}
return defaultAccount
return authAccount(address)
This section specifies the non-trivial or recommended endpoints of the Auth module and does not include all endpoints.
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
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:
return verifyNonce(trs) == NONCE_VERIFY_STATUS_OK
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
ingenesisBlockAssetObject.authDataSubstore
, letaddress = entry.address
andauthAccount = entry.authAccount
and check the following:- Each entry has a different
address
and alladdress
have lengthADDRESS_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 lengthED25519_PUBLIC_KEY_LENGTH
, are pairwise distinct and in lexicographical order. The array may be empty. - The keys in the
authAccount.optionalKeys
array have lengthED25519_PUBLIC_KEY_LENGTH
, are pairwise distinct and in lexicographical order. The array may be empty. - The arrays
authAccount.mandatoryKeys
andauthAccount.optionalKeys
are disjoint; their union has at least one element and at mostMAX_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 ofauthAccount.mandatoryKeys
andauthAccount.optionalKeys
).authAccount.numberOfSignatures
is not smaller than the length ofauthAccount.mandatoryKeys
.
- The keys in the
- Each entry has a different
- Finally, for each entry
entry
ingenesisBlockAssetObject.authDataSubstore
, create a corresponding entry in the auth data substore withstoreKey = entry.address
andstoreValue = 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,
}
}
}
}
}
}
}
}
}
The Auth module does not execute any logic during the genesis state finalization.
As part of the verification of a transaction trs
, the nonce
and signatures
properties are verified as follows. During block processing, the transaction trs
is only accepted if the return value of verifyTransaction
equals NONCE_VERIFY_STATUS_OK
. If verifyTransaction
is called from the transaction pool, it is accepted if it is equal to NONCE_VERIFY_STATUS_OK
or NONCE_VERIFY_STATUS_PENDING
.
def verifyTransaction(trs: Transaction) -> int32:
nonceStatus = verifyNonce(trs)
if nonceStatus == NONCE_VERIFY_STATUS_FAIL:
raise Exception('Transaction nonce is lower than account nonce.')
verifySignatures(trs)
return nonceStatus
We define the following auxiliary function to verify the nonce of a transaction.
def verifyNonce(trs: Transaction) -> int32:
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:
return NONCE_VERIFY_STATUS_FAIL
elif trs.nonce == expectedNonce:
return NONCE_VERIFY_STATUS_OK
else
return NONCE_VERIFY_STATUS_PENDING
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 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
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.