diff --git a/commander/oclif.manifest.json b/commander/oclif.manifest.json deleted file mode 100644 index dcca4a28e10..00000000000 --- a/commander/oclif.manifest.json +++ /dev/null @@ -1,683 +0,0 @@ -{ - "version": "6.0.0-rc.3", - "commands": { - "console": { - "id": "console", - "description": "Lisk interactive REPL session to run commands.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "console", - "console --api-ws=ws://localhost:8080", - "console --api-ipc=/path/to/server" - ], - "flags": { - "api-ipc": { - "name": "api-ipc", - "type": "option", - "description": "Enable api-client with IPC communication.", - "multiple": false, - "exclusive": ["api-ws"] - }, - "api-ws": { - "name": "api-ws", - "type": "option", - "description": "Enable api-client with Websocket communication.", - "multiple": false, - "exclusive": ["api-ipc"] - } - }, - "args": [] - }, - "hash-onion": { - "id": "hash-onion", - "description": "Create hash onions to be used by the forger.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "hash-onion --count=1000000 --distance=2000 --pretty", - "hash-onion --count=1000000 --distance=2000 --output ~/my_onion.json" - ], - "flags": { - "output": { - "name": "output", - "type": "option", - "char": "o", - "description": "Output file path", - "multiple": false - }, - "count": { - "name": "count", - "type": "option", - "char": "c", - "description": "Total number of hashes to produce", - "multiple": false, - "default": 1000000 - }, - "distance": { - "name": "distance", - "type": "option", - "char": "d", - "description": "Distance between each hash", - "multiple": false, - "default": 1000 - }, - "pretty": { - "name": "pretty", - "type": "boolean", - "description": "Prints JSON in pretty format rather than condensed.", - "allowNo": false - } - }, - "args": [] - }, - "init": { - "id": "init", - "description": "Bootstrap a blockchain application using Lisk SDK.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "init", - "init --template lisk-ts", - "init --template @some-global-npm-package", - "init /project/path", - "init /project/path --template lisk-ts" - ], - "flags": { - "template": { - "name": "template", - "type": "option", - "char": "t", - "description": "Template to bootstrap the application. It will read from `.liskrc.json` or use `lisk-ts` if not found.", - "multiple": false - }, - "registry": { - "name": "registry", - "type": "option", - "description": "URL of a registry to download dependencies from.", - "multiple": false - } - }, - "args": [ - { - "name": "projectPath", - "description": "Path to create the project.", - "default": "/Users/ishan/repos/lisk-sdk/commander" - } - ] - }, - "endpoint:invoke": { - "id": "endpoint:invoke", - "description": "Invokes the provided endpoint.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "endpoint:invoke {endpoint} {parameters}", - "endpoint:invoke --data-path --file", - "endpoint:invoke generator_getAllKeys", - "endpoint:invoke consensus_getBFTParameters '{\"height\": 2}' -d ~/.lisk/pos-mainchain --pretty", - "endpoint:invoke consensus_getBFTParameters -f ./input.json" - ], - "flags": { - "data-path": { - "name": "data-path", - "type": "option", - "char": "d", - "description": "Directory path to specify where node data is stored. Environment variable \"LISK_DATA_PATH\" can also be used.", - "multiple": false - }, - "pretty": { - "name": "pretty", - "type": "boolean", - "description": "Prints JSON in pretty format rather than condensed.", - "allowNo": false - }, - "file": { - "name": "file", - "type": "option", - "char": "f", - "description": "Input file.", - "multiple": false - } - }, - "args": [ - { "name": "endpoint", "description": "Endpoint to invoke", "required": true }, - { "name": "params", "description": "Endpoint parameters (Optional)", "required": false } - ] - }, - "generate:command": { - "id": "generate:command", - "description": "Creates an command skeleton for the given module name, name and id.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "generate:command moduleName commandName commandID", - "generate:command nft transfer 1" - ], - "flags": { - "template": { - "name": "template", - "type": "option", - "char": "t", - "description": "Template to bootstrap the application. It will read from `.liskrc.json` or use `lisk-ts` if not found.", - "multiple": false - } - }, - "args": [ - { "name": "moduleName", "description": "Module name.", "required": true }, - { "name": "commandName", "description": "Asset name.", "required": true } - ] - }, - "generate:module": { - "id": "generate:module", - "description": "Creates a module skeleton for the given name.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": ["generate:module nft"], - "flags": { - "template": { - "name": "template", - "type": "option", - "char": "t", - "description": "Template to bootstrap the application. It will read from `.liskrc.json` or use `lisk-ts` if not found.", - "multiple": false - } - }, - "args": [{ "name": "moduleName", "description": "Module name.", "required": true }] - }, - "generate:plugin": { - "id": "generate:plugin", - "description": "Creates custom plugin.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "generate:plugin myPlugin", - "generate:plugin myPlugin --standalone --output ./my_plugin" - ], - "flags": { - "template": { - "name": "template", - "type": "option", - "char": "t", - "description": "Template to bootstrap the application. It will read from `.liskrc.json` or use `lisk-ts` if not found.", - "multiple": false - }, - "standalone": { - "name": "standalone", - "type": "boolean", - "description": "Create a standalone plugin package.", - "allowNo": false - }, - "output": { - "name": "output", - "type": "option", - "char": "o", - "description": "Path to create the plugin.", - "multiple": false, - "dependsOn": ["standalone"] - }, - "registry": { - "name": "registry", - "type": "option", - "description": "URL of a registry to download dependencies from.", - "multiple": false, - "dependsOn": ["standalone"] - } - }, - "args": [{ "name": "name", "description": "Name of the plugin.", "required": true }] - }, - "keys:create": { - "id": "keys:create", - "description": "Return keys corresponding to the given passphrase.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "keys:create", - "keys:create --passphrase your-passphrase", - "keys:create --passphrase your-passphrase --no-encrypt", - "keys:create --passphrase your-passphrase --password your-password", - "keys:create --passphrase your-passphrase --password your-password --count 2", - "keys:create --passphrase your-passphrase --no-encrypt --count 2 --offset 1", - "keys:create --passphrase your-passphrase --no-encrypt --count 2 --offset 1 --chainid 1", - "keys:create --passphrase your-passphrase --password your-password --count 2 --offset 1 --chainid 1 --output /mypath/keys.json" - ], - "flags": { - "output": { - "name": "output", - "type": "option", - "char": "o", - "description": "The output directory. Default will set to current working directory.", - "multiple": false - }, - "passphrase": { - "name": "passphrase", - "type": "option", - "char": "p", - "description": "Specifies a source for your secret passphrase. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --passphrase='my secret passphrase' (should only be used where security is not important)\n", - "multiple": false - }, - "no-encrypt": { - "name": "no-encrypt", - "type": "boolean", - "char": "n", - "description": "No encrypted message object to be created", - "allowNo": false - }, - "password": { - "name": "password", - "type": "option", - "char": "w", - "description": "Specifies a source for your secret password. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --password=pass:password123 (should only be used where security is not important)\n", - "multiple": false - }, - "count": { - "name": "count", - "type": "option", - "char": "c", - "description": "Number of keys to create", - "multiple": false, - "default": 1 - }, - "offset": { - "name": "offset", - "type": "option", - "char": "f", - "description": "Offset for the key derivation path", - "multiple": false, - "default": 0 - }, - "chainid": { - "name": "chainid", - "type": "option", - "char": "i", - "description": "Chain id", - "multiple": false, - "default": 0 - }, - "add-legacy": { - "name": "add-legacy", - "type": "boolean", - "description": "Add legacy key derivation path to the result", - "allowNo": false - } - }, - "args": [] - }, - "keys:encrypt": { - "id": "keys:encrypt", - "description": "Encrypt keys from a file and overwrite the file", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "keys:encrypt --file-path ./my/path/keys.json", - "keys:encrypt --file-path ./my/path/keys.json --password mypass" - ], - "flags": { - "file-path": { - "name": "file-path", - "type": "option", - "char": "f", - "description": "Path of the file to encrypt from", - "required": true, - "multiple": false - }, - "password": { - "name": "password", - "type": "option", - "char": "w", - "description": "Specifies a source for your secret password. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --password=pass:password123 (should only be used where security is not important)\n", - "multiple": false - } - }, - "args": [] - }, - "keys:export": { - "id": "keys:export", - "description": "Export to .", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "keys:export --output /mypath/keys.json", - "keys:export --output /mypath/keys.json --data-path ./data " - ], - "flags": { - "data-path": { - "name": "data-path", - "type": "option", - "char": "d", - "description": "Directory path to specify where node data is stored. Environment variable \"LISK_DATA_PATH\" can also be used.", - "multiple": false - }, - "pretty": { - "name": "pretty", - "type": "boolean", - "description": "Prints JSON in pretty format rather than condensed.", - "allowNo": false - }, - "output": { - "name": "output", - "type": "option", - "char": "o", - "description": "The output directory. Default will set to current working directory.", - "required": true, - "multiple": false - } - }, - "args": [] - }, - "keys:import": { - "id": "keys:import", - "description": "Import from .", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "keys:import --file-path ./my/path/keys.json", - "keys:import --file-path ./my/path/keys.json --data-path ./data " - ], - "flags": { - "data-path": { - "name": "data-path", - "type": "option", - "char": "d", - "description": "Directory path to specify where node data is stored. Environment variable \"LISK_DATA_PATH\" can also be used.", - "multiple": false - }, - "pretty": { - "name": "pretty", - "type": "boolean", - "description": "Prints JSON in pretty format rather than condensed.", - "allowNo": false - }, - "file-path": { - "name": "file-path", - "type": "option", - "char": "f", - "description": "Path of the file to import from", - "required": true, - "multiple": false - } - }, - "args": [] - }, - "message:decrypt": { - "id": "message:decrypt", - "description": "\n\tDecrypts a previously encrypted message using your the password used to encrypt.\n\t", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": ["message:decrypt "], - "flags": { - "password": { - "name": "password", - "type": "option", - "char": "w", - "description": "Specifies a source for your secret password. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --password=pass:password123 (should only be used where security is not important)\n", - "multiple": false - }, - "message": { - "name": "message", - "type": "option", - "char": "m", - "description": "Specifies a source for providing a message to the command. If a string is provided directly as an argument, this option will be ignored. The message must be provided via an argument or via this option. Sources must be one of `file` or `stdin`. In the case of `file`, a corresponding identifier must also be provided.\n\tNote: if both secret passphrase and message are passed via stdin, the passphrase must be the first line.\n\tExamples:\n\t- --message=file:/path/to/my/message.txt\n\t- --message=\"hello world\"\n", - "multiple": false - } - }, - "args": [{ "name": "message", "description": "Encrypted message." }] - }, - "message:encrypt": { - "id": "message:encrypt", - "description": "\n\tEncrypts a message with a password provided.\n\t", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": ["message:encrypt \"Hello world\""], - "flags": { - "password": { - "name": "password", - "type": "option", - "char": "w", - "description": "Specifies a source for your secret password. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --password=pass:password123 (should only be used where security is not important)\n", - "multiple": false - }, - "message": { - "name": "message", - "type": "option", - "char": "m", - "description": "Specifies a source for providing a message to the command. If a string is provided directly as an argument, this option will be ignored. The message must be provided via an argument or via this option. Sources must be one of `file` or `stdin`. In the case of `file`, a corresponding identifier must also be provided.\n\tNote: if both secret passphrase and message are passed via stdin, the passphrase must be the first line.\n\tExamples:\n\t- --message=file:/path/to/my/message.txt\n\t- --message=\"hello world\"\n", - "multiple": false - }, - "pretty": { - "name": "pretty", - "type": "boolean", - "description": "Prints JSON in pretty format rather than condensed.", - "allowNo": false - }, - "stringify": { - "name": "stringify", - "type": "boolean", - "char": "s", - "description": "Display encrypted message in stringified format", - "allowNo": false - } - }, - "args": [{ "name": "message", "description": "Message to encrypt." }] - }, - "message:sign": { - "id": "message:sign", - "description": "\n\tSigns a message using your secret passphrase.\n\t", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": ["message:sign \"Hello world\""], - "flags": { - "json": { - "name": "json", - "type": "boolean", - "char": "j", - "description": "Prints output in JSON format. You can change the default behavior in your config.json file.", - "allowNo": true - }, - "pretty": { - "name": "pretty", - "type": "boolean", - "description": "Prints JSON in pretty format rather than condensed. Has no effect if the output is set to table. You can change the default behavior in your config.json file.", - "allowNo": true - }, - "passphrase": { - "name": "passphrase", - "type": "option", - "char": "p", - "description": "Specifies a source for your secret passphrase. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --passphrase='my secret passphrase' (should only be used where security is not important)\n", - "multiple": false - }, - "message": { - "name": "message", - "type": "option", - "char": "m", - "description": "Specifies a source for providing a message to the command. If a string is provided directly as an argument, this option will be ignored. The message must be provided via an argument or via this option. Sources must be one of `file` or `stdin`. In the case of `file`, a corresponding identifier must also be provided.\n\tNote: if both secret passphrase and message are passed via stdin, the passphrase must be the first line.\n\tExamples:\n\t- --message=file:/path/to/my/message.txt\n\t- --message=\"hello world\"\n", - "multiple": false - } - }, - "args": [{ "name": "message", "description": "Message to sign." }] - }, - "message:verify": { - "id": "message:verify", - "description": "\n\tVerifies a signature for a message using the signer’s public key.\n\t", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "message:verify 647aac1e2df8a5c870499d7ddc82236b1e10936977537a3844a6b05ea33f9ef6 2a3ca127efcf7b2bf62ac8c3b1f5acf6997cab62ba9fde3567d188edcbacbc5dc8177fb88d03a8691ce03348f569b121bca9e7a3c43bf5c056382f35ff843c09 \"Hello world\"" - ], - "flags": { - "json": { - "name": "json", - "type": "boolean", - "char": "j", - "description": "Prints output in JSON format. You can change the default behavior in your config.json file.", - "allowNo": true - }, - "pretty": { - "name": "pretty", - "type": "boolean", - "description": "Prints JSON in pretty format rather than condensed. Has no effect if the output is set to table. You can change the default behavior in your config.json file.", - "allowNo": true - }, - "message": { - "name": "message", - "type": "option", - "char": "m", - "description": "Specifies a source for providing a message to the command. If a string is provided directly as an argument, this option will be ignored. The message must be provided via an argument or via this option. Sources must be one of `file` or `stdin`. In the case of `file`, a corresponding identifier must also be provided.\n\tNote: if both secret passphrase and message are passed via stdin, the passphrase must be the first line.\n\tExamples:\n\t- --message=file:/path/to/my/message.txt\n\t- --message=\"hello world\"\n", - "multiple": false - } - }, - "args": [ - { - "name": "publicKey", - "description": "Public key of the signer of the message.", - "required": true - }, - { "name": "signature", "description": "Signature to verify.", "required": true }, - { "name": "message", "description": "Message to verify." } - ] - }, - "passphrase:create": { - "id": "passphrase:create", - "description": "Returns a randomly generated 24 words mnemonic passphrase.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": ["passphrase:create", "passphrase:create --output /mypath/passphrase.json"], - "flags": { - "output": { - "name": "output", - "type": "option", - "char": "o", - "description": "The output directory. Default will set to current working directory.", - "multiple": false - } - }, - "args": [] - }, - "passphrase:decrypt": { - "id": "passphrase:decrypt", - "description": "Decrypt secret passphrase using the password provided at the time of encryption.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "passphrase:decrypt --file-path ./my/path/output.json", - "passphrase:decrypt --file-path ./my/path/output.json --password your-password" - ], - "flags": { - "password": { - "name": "password", - "type": "option", - "char": "w", - "description": "Specifies a source for your secret password. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --password=pass:password123 (should only be used where security is not important)\n", - "multiple": false - }, - "file-path": { - "name": "file-path", - "type": "option", - "char": "f", - "description": "Path of the file to import from", - "required": true, - "multiple": false - } - }, - "args": [] - }, - "passphrase:encrypt": { - "id": "passphrase:encrypt", - "description": "Encrypt secret passphrase using password.", - "strict": true, - "pluginName": "lisk-commander", - "pluginAlias": "lisk-commander", - "pluginType": "core", - "aliases": [], - "examples": [ - "passphrase:encrypt", - "passphrase:encrypt --passphrase your-passphrase --output /mypath/keys.json", - "passphrase:encrypt --password your-password", - "passphrase:encrypt --password your-password --passphrase your-passphrase --output /mypath/keys.json", - "passphrase:encrypt --output-public-key --output /mypath/keys.json" - ], - "flags": { - "password": { - "name": "password", - "type": "option", - "char": "w", - "description": "Specifies a source for your secret password. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --password=pass:password123 (should only be used where security is not important)\n", - "multiple": false - }, - "passphrase": { - "name": "passphrase", - "type": "option", - "char": "p", - "description": "Specifies a source for your secret passphrase. Command will prompt you for input if this option is not set.\n\tExamples:\n\t- --passphrase='my secret passphrase' (should only be used where security is not important)\n", - "multiple": false - }, - "output-public-key": { - "name": "output-public-key", - "type": "boolean", - "description": "Includes the public key in the output. This option is provided for the convenience of node operators.", - "allowNo": false - }, - "output": { - "name": "output", - "type": "option", - "char": "o", - "description": "The output directory. Default will set to current working directory.", - "multiple": false - } - }, - "args": [] - } - } -} diff --git a/commander/package.json b/commander/package.json index 5d095484feb..2d6f0fafca0 100644 --- a/commander/package.json +++ b/commander/package.json @@ -1,6 +1,6 @@ { "name": "lisk-commander", - "version": "6.0.0-rc.6", + "version": "6.1.0-rc.0", "description": "A command line interface for Lisk", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -101,16 +101,16 @@ "/docs" ], "dependencies": { - "@liskhq/lisk-api-client": "^6.0.0-rc.4", - "@liskhq/lisk-chain": "^0.5.0-rc.4", - "@liskhq/lisk-client": "^6.0.0-rc.4", - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-db": "0.3.10", - "@liskhq/lisk-passphrase": "^4.0.0-rc.0", - "@liskhq/lisk-transactions": "^6.0.0-rc.2", + "@liskhq/lisk-api-client": "^6.1.0-rc.0", + "@liskhq/lisk-chain": "^0.6.0-rc.0", + "@liskhq/lisk-client": "^6.1.0-rc.0", + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-db": "0.3.7", + "@liskhq/lisk-passphrase": "^4.1.0-rc.0", + "@liskhq/lisk-transactions": "^6.1.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", - "@liskhq/lisk-validator": "^0.8.0-rc.2", + "@liskhq/lisk-validator": "^0.9.0-rc.0", "@oclif/core": "1.20.4", "@oclif/plugin-autocomplete": "1.3.6", "@oclif/plugin-help": "5.1.19", @@ -121,7 +121,7 @@ "cli-table3": "0.6.0", "fs-extra": "11.1.0", "inquirer": "8.2.5", - "lisk-framework": "^0.11.0-rc.5", + "lisk-framework": "^0.12.0-rc.0", "listr": "0.14.3", "progress": "2.0.3", "semver": "7.5.2", diff --git a/commander/src/bootstrapping/templates/lisk-template-ts/templates/init/package-template.json b/commander/src/bootstrapping/templates/lisk-template-ts/templates/init/package-template.json index 12794990e76..a5c059ee9b6 100644 --- a/commander/src/bootstrapping/templates/lisk-template-ts/templates/init/package-template.json +++ b/commander/src/bootstrapping/templates/lisk-template-ts/templates/init/package-template.json @@ -85,12 +85,12 @@ } }, "dependencies": { - "@liskhq/lisk-framework-dashboard-plugin": "0.3.0-rc.5", - "@liskhq/lisk-framework-faucet-plugin": "0.3.0-rc.5", - "@liskhq/lisk-framework-monitor-plugin": "0.4.0-rc.5", - "@liskhq/lisk-framework-forger-plugin": "0.4.0-rc.5", - "@liskhq/lisk-framework-report-misbehavior-plugin": "0.4.0-rc.5", - "@liskhq/lisk-framework-chain-connector-plugin": "0.1.0-rc.5", + "@liskhq/lisk-framework-dashboard-plugin": "0.4.0-rc.0", + "@liskhq/lisk-framework-faucet-plugin": "0.4.0-rc.0", + "@liskhq/lisk-framework-monitor-plugin": "0.5.0-rc.0", + "@liskhq/lisk-framework-forger-plugin": "0.5.0-rc.0", + "@liskhq/lisk-framework-report-misbehavior-plugin": "0.5.0-rc.0", + "@liskhq/lisk-framework-chain-connector-plugin": "0.2.0-rc.0", "@oclif/core": "1.20.4", "@oclif/plugin-autocomplete": "1.3.6", "@oclif/plugin-help": "5.1.19", @@ -98,8 +98,8 @@ "axios": "0.21.2", "fs-extra": "11.1.0", "inquirer": "8.2.5", - "lisk-commander": "6.0.0-rc.6", - "lisk-sdk": "6.0.0-rc.5", + "lisk-commander": "6.1.0-rc.0", + "lisk-sdk": "6.1.0-rc.0", "tar": "6.1.11", "tslib": "2.4.1" }, diff --git a/commander/src/bootstrapping/templates/lisk-template-ts/templates/init/src/app/modules.ts b/commander/src/bootstrapping/templates/lisk-template-ts/templates/init/src/app/modules.ts index acdfa4fb8f5..d69352da8ae 100644 --- a/commander/src/bootstrapping/templates/lisk-template-ts/templates/init/src/app/modules.ts +++ b/commander/src/bootstrapping/templates/lisk-template-ts/templates/init/src/app/modules.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { Application } from 'lisk-sdk'; -// @ts-expect-error app will have typescript error for unsued variable -export const registerModules = (app: Application): void => {}; +export const registerModules = (_app: Application): void => {}; diff --git a/commander/src/bootstrapping/templates/lisk-template-ts/templates/init_plugin/package.json b/commander/src/bootstrapping/templates/lisk-template-ts/templates/init_plugin/package.json index 6b7ba039875..adbc88b021e 100644 --- a/commander/src/bootstrapping/templates/lisk-template-ts/templates/init_plugin/package.json +++ b/commander/src/bootstrapping/templates/lisk-template-ts/templates/init_plugin/package.json @@ -28,7 +28,7 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "lisk-sdk": "^6.0.0-rc.5" + "lisk-sdk": "^6.1.0-rc.0" }, "devDependencies": { "@types/jest": "26.0.21", diff --git a/commander/src/commands/generate/command.ts b/commander/src/commands/generate/command.ts index 817f414178e..eca875ae018 100644 --- a/commander/src/commands/generate/command.ts +++ b/commander/src/commands/generate/command.ts @@ -22,11 +22,8 @@ interface CommandCommandArgs { } export default class CommandCommand extends BaseBootstrapCommand { - static description = 'Creates an command skeleton for the given module name, name and id.'; - static examples = [ - 'generate:command moduleName commandName commandID', - 'generate:command nft transfer 1', - ]; + static description = 'Creates a command skeleton for the given module name and command name.'; + static examples = ['generate:command moduleName commandName', 'generate:command nft transfer']; static args = [ { name: 'moduleName', diff --git a/commander/src/utils/reader.ts b/commander/src/utils/reader.ts index 96d025fb0e9..cfd58b014a8 100644 --- a/commander/src/utils/reader.ts +++ b/commander/src/utils/reader.ts @@ -22,20 +22,6 @@ import * as readline from 'readline'; import { FileSystemError, ValidationError } from './error'; -interface PropertyValue { - readonly dataType: string; - readonly type: string; - readonly items: { type: string; properties: Record }; -} - -interface Question { - readonly [key: string]: unknown; -} - -interface NestedPropertyTemplate { - [key: string]: string[]; -} - interface NestedAsset { [key: string]: Array>; } @@ -193,36 +179,7 @@ export const readStdIn = async (): Promise => { return readFromStd; }; -const getNestedPropertyTemplate = (schema: Schema): NestedPropertyTemplate => { - const keyValEntries = Object.entries(schema.properties); - const template: NestedPropertyTemplate = {}; - - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < keyValEntries.length; i += 1) { - const [schemaPropertyName, schemaPropertyValue] = keyValEntries[i]; - if ((schemaPropertyValue as PropertyValue).type === 'array') { - // nested items properties - if ((schemaPropertyValue as PropertyValue).items.type === 'object') { - template[schemaPropertyName] = Object.keys( - (schemaPropertyValue as PropertyValue).items.properties, - ); - } - } - } - return template; -}; - -const castValue = ( - val: string, - schemaType: string, -): number | bigint | string | string[] | Record => { - if (schemaType === 'object') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return JSON.parse(val); - } - if (schemaType === 'array') { - return val !== '' ? val.split(',') : []; - } +const castValue = (val: string, schemaType: string): string | number | bigint => { if (schemaType === 'uint64' || schemaType === 'sint64') { return BigInt(val); } @@ -232,107 +189,108 @@ const castValue = ( return val; }; -export const transformAsset = ( - schema: Schema, - data: Record, -): Record => { - const propertySchema = Object.values(schema.properties); - const assetData = {} as Record; - return Object.entries(data).reduce((acc, curr, index) => { - const propSchema = propertySchema[index] as { type: string; dataType: string }; - // Property schema type can be scalar(string, bool, etc..) or structural(object, array) - const schemaType = propSchema.type || propSchema.dataType; - acc[curr[0]] = castValue(curr[1], schemaType); - return acc; - }, assetData); -}; +const castArray = (items: string[], schemaType: string): string[] | number[] | bigint[] => { + if (schemaType === 'uint64' || schemaType === 'sint64') { + return items.map(i => BigInt(i)); + } -export const transformNestedAsset = ( - schema: Schema, - data: Array>, -): NestedAsset => { - const template = getNestedPropertyTemplate(schema); - const result = {} as NestedAsset; - const items: Array> = []; - for (const assetData of data) { - const [[key, val]] = Object.entries(assetData); - const templateValues = template[key]; - const initData = {} as Record; - const valObject = val.split(',').reduce((acc, curr, index) => { - acc[templateValues[index]] = Number.isInteger(Number(curr)) ? Number(curr) : curr; - return acc; - }, initData); - items.push(valObject); - result[key] = items; + if (schemaType === 'uint32' || schemaType === 'sint32') { + return items.map(i => Number(i)); } + + return items; +}; + +const getNestedParametersFromPrompt = async (property: { + name: string; + items: { properties: Record }; +}) => { + let addMore = false; + const nestedArray: Array> = []; + const nestedProperties = Object.keys(property.items.properties); + const nestedPropertiesCsv = nestedProperties.join(','); + do { + const nestedPropertiesAnswer: Record = await inquirer.prompt({ + type: 'input', + name: property.name, + message: `Please enter: ${property.name}(${nestedPropertiesCsv}): `, + }); + + const properties = nestedPropertiesAnswer[property.name].split(','); + + const nestedObject: Record = {}; + + for (let i = 0; i < nestedProperties.length; i += 1) { + const propertySchema = property.items.properties[nestedProperties[i]] as { dataType: string }; + nestedObject[nestedProperties[i]] = + properties[i] === undefined ? '' : castValue(properties[i], propertySchema.dataType); + } + + nestedArray.push(nestedObject); + + const confirmResponse = await inquirer.prompt({ + type: 'confirm', + name: 'askAgain', + message: `Want to enter another ${property.name})`, + }); + + addMore = confirmResponse.askAgain as boolean; + } while (addMore); + + const result = {} as Record; + result[property.name] = nestedArray; + return result; }; -export const prepareQuestions = (schema: Schema): Question[] => { - const keyValEntries = Object.entries(schema.properties); - const questions: Question[] = []; +export const getParamsFromPrompt = async ( + assetSchema: Schema | { properties: Record }, +): Promise> => { + const result: Record = {}; + for (const propertyName of Object.keys(assetSchema.properties)) { + const property = assetSchema.properties[propertyName] as { + dataType?: string; + type?: 'array'; + items?: { dataType?: string; type?: 'object'; properties?: Record }; + }; - for (const [schemaPropertyName, schemaPropertyValue] of keyValEntries) { - if ((schemaPropertyValue as PropertyValue).type === 'array') { - let commaSeparatedKeys: string[] = []; - // nested items properties - if ((schemaPropertyValue as PropertyValue).items.type === 'object') { - commaSeparatedKeys = Object.keys((schemaPropertyValue as PropertyValue).items.properties); - } - questions.push({ - type: 'input', - name: schemaPropertyName, - message: `Please enter: ${schemaPropertyName}(${ - commaSeparatedKeys.length ? commaSeparatedKeys.join(', ') : 'comma separated values (a,b)' - }): `, - }); - if ((schemaPropertyValue as PropertyValue).items.type === 'object') { - questions.push({ - type: 'confirm', - name: 'askAgain', - message: `Want to enter another ${schemaPropertyName}(${commaSeparatedKeys.join(', ')})`, + if (property.type === 'array') { + if (property.items?.type === 'object' && property.items.properties !== undefined) { + const nestedResult = await getNestedParametersFromPrompt({ + name: propertyName, + items: { + properties: property.items.properties, + }, + }); + + result[propertyName] = nestedResult[propertyName]; + } else if (property.items?.type === undefined && property.items?.dataType !== undefined) { + const answer: Record = await inquirer.prompt({ + type: 'input', + name: propertyName, + message: `Please enter: ${propertyName}(comma separated values (a,b)): `, }); + + result[propertyName] = castArray( + answer[propertyName] === '' ? [] : answer[propertyName].split(','), + property.items.dataType, + ); } } else { - questions.push({ + const answer: Record = await inquirer.prompt({ type: 'input', - name: schemaPropertyName, - message: `Please enter: ${schemaPropertyName}: `, + name: propertyName, + message: `Please enter: ${propertyName}: `, }); - } - } - return questions; -}; -export const getParamsFromPrompt = async ( - assetSchema: Schema, - output: Array<{ [key: string]: string }> = [], -): Promise> => { - // prepare array of questions based on asset schema - const questions = prepareQuestions(assetSchema); - if (questions.length === 0) { - return {}; - } - let isTypeConfirm = false; - // Prompt user with prepared questions - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result = await inquirer.prompt(questions).then(async answer => { - const inquirerResult = answer as { [key: string]: string }; - isTypeConfirm = typeof inquirerResult.askAgain === 'boolean'; - // if its a multiple questions prompt user again - if (inquirerResult.askAgain) { - output.push(inquirerResult); - return getParamsFromPrompt(assetSchema, output); + result[propertyName] = castValue( + answer[propertyName], + (property as { dataType: string }).dataType, + ); } - output.push(inquirerResult); - return Promise.resolve(answer); - }); - const filteredResult = output.map(({ askAgain, ...assetProps }) => assetProps); + } - // transform asset prompt result according to asset schema - return isTypeConfirm - ? transformNestedAsset(assetSchema, filteredResult) - : transformAsset(assetSchema, result as Record); + return result; }; export const checkFileExtension = (filePath: string): void => { diff --git a/commander/test/bootstrapping/commands/hash-onion.spec.ts b/commander/test/bootstrapping/commands/hash-onion.spec.ts index f5fddd37067..97eaec9e7a8 100644 --- a/commander/test/bootstrapping/commands/hash-onion.spec.ts +++ b/commander/test/bootstrapping/commands/hash-onion.spec.ts @@ -44,7 +44,7 @@ describe('hash-onion command', () => { for (let i = 0; i < result.hashes.length - 1; i += 1) { let nextHash = Buffer.from(result.hashes[i + 1], 'hex'); for (let j = 0; j < result.distance; j += 1) { - nextHash = cryptography.utils.hash(nextHash).slice(0, 16); + nextHash = cryptography.utils.hash(nextHash).subarray(0, 16); } expect(result.hashes[i]).toBe(nextHash.toString('hex')); } diff --git a/commander/test/bootstrapping/commands/transaction/create.spec.ts b/commander/test/bootstrapping/commands/transaction/create.spec.ts index 78edde36f53..c53c8a438b4 100644 --- a/commander/test/bootstrapping/commands/transaction/create.spec.ts +++ b/commander/test/bootstrapping/commands/transaction/create.spec.ts @@ -22,7 +22,12 @@ import { emptySchema } from '@liskhq/lisk-codec'; import { join } from 'path'; import * as appUtils from '../../../../src/utils/application'; import * as readerUtils from '../../../../src/utils/reader'; -import { tokenTransferParamsSchema, posVoteParamsSchema } from '../../../helpers/transactions'; +import { + tokenTransferParamsSchema, + posVoteParamsSchema, + schemaWithArray, + schemaWithArrayOfObjects, +} from '../../../helpers/transactions'; import { CreateCommand } from '../../../../src/bootstrapping/commands/transaction/create'; import { getConfig } from '../../../helpers/config'; import { PromiseResolvedType } from '../../../../src/types'; @@ -56,6 +61,24 @@ describe('transaction:create command', () => { ], }; + const questionsForTokenTransfer = [ + { message: 'Please enter: tokenID: ', name: 'tokenID', type: 'input' }, + { message: 'Please enter: amount: ', name: 'amount', type: 'input' }, + { + message: 'Please enter: recipientAddress: ', + name: 'recipientAddress', + type: 'input', + }, + { message: 'Please enter: data: ', name: 'data', type: 'input' }, + ]; + + const verifyIfInquirerCallsFor = (questions: Array>) => { + expect(inquirer.prompt).toHaveBeenCalledTimes(questions.length); + for (let i = 0; i < 1; i += 1) { + expect(inquirer.prompt).toHaveBeenNthCalledWith(i + 1, questions[i]); + } + }; + let config: Awaited>; let clientMock: PromiseResolvedType>; @@ -99,6 +122,19 @@ describe('transaction:create command', () => { }, ], }, + { + name: 'nft', + commands: [ + { + name: 'arrayOfItems', + params: schemaWithArray, + }, + { + name: 'arrayOfObjects', + params: schemaWithArrayOfObjects, + }, + ], + }, ], node: { getNodeInfo: jest.fn().mockResolvedValue({ @@ -133,6 +169,88 @@ describe('transaction:create command', () => { 'Missing 3 required args:', ); }); + + it('should throw if casting fails', async () => { + jest.spyOn(inquirer, 'prompt').mockResolvedValue({ + attributesArray: 'a,12312321', + }); + + await expect( + CreateCommandExtended.run( + ['nft', 'arrayOfItems', '100000000', `--passphrase=${passphrase}`], + config, + ), + ).rejects.toThrow(); + }); + + describe('prompt for arrays and array of objects', () => { + it('should inquire arrays as CSV', async () => { + jest.spyOn(inquirer, 'prompt').mockResolvedValue({ + attributesArray: '13213213,12312321', + }); + + await CreateCommandExtended.run( + ['nft', 'arrayOfItems', '100000000', `--passphrase=${passphrase}`], + config, + ); + verifyIfInquirerCallsFor([ + { + type: 'input', + name: 'attributesArray', + message: 'Please enter: attributesArray(comma separated values (a,b)): ', + }, + ]); + + expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledTimes(1); + }); + + it('should inquire each item of the array as a CSV and prompt to add more', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ + attributesArray: 'pos, 0000', + }) + .mockResolvedValueOnce({ + askAgain: true, + }) + .mockResolvedValueOnce({ + attributesArray: 'token, 0000', + }) + .mockResolvedValue({ + askAgain: false, + }); + + await CreateCommandExtended.run( + ['nft', 'arrayOfObjects', '100000000', `--passphrase=${passphrase}`], + config, + ); + + verifyIfInquirerCallsFor([ + { + type: 'input', + name: 'attributesArray', + message: 'Please enter: attributesArray(module,attributes): ', + }, + { + type: 'confirm', + name: 'askAgain', + message: 'Want to enter another attributesArray', + }, + { + type: 'input', + name: 'attributesArray', + message: 'Please enter: attributesArray(module,attributes): ', + }, + { + type: 'confirm', + name: 'askAgain', + message: 'Want to enter another attributesArray', + }, + ]); + + expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledTimes(1); + }); + }); }); describe('transaction:create 2', () => { @@ -395,17 +513,7 @@ describe('transaction:create command', () => { ], config, ); - expect(inquirer.prompt).toHaveBeenCalledTimes(1); - expect(inquirer.prompt).toHaveBeenCalledWith([ - { message: 'Please enter: tokenID: ', name: 'tokenID', type: 'input' }, - { message: 'Please enter: amount: ', name: 'amount', type: 'input' }, - { - message: 'Please enter: recipientAddress: ', - name: 'recipientAddress', - type: 'input', - }, - { message: 'Please enter: data: ', name: 'data', type: 'input' }, - ]); + verifyIfInquirerCallsFor(questionsForTokenTransfer); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledTimes(1); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledWith(undefined, { transaction: expect.any(String), @@ -426,17 +534,7 @@ describe('transaction:create command', () => { ], config, ); - expect(inquirer.prompt).toHaveBeenCalledTimes(1); - expect(inquirer.prompt).toHaveBeenCalledWith([ - { message: 'Please enter: tokenID: ', name: 'tokenID', type: 'input' }, - { message: 'Please enter: amount: ', name: 'amount', type: 'input' }, - { - message: 'Please enter: recipientAddress: ', - name: 'recipientAddress', - type: 'input', - }, - { message: 'Please enter: data: ', name: 'data', type: 'input' }, - ]); + verifyIfInquirerCallsFor(questionsForTokenTransfer); expect(readerUtils.getPassphraseFromPrompt).toHaveBeenCalledWith('passphrase'); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledTimes(1); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledWith(undefined, { @@ -690,17 +788,7 @@ describe('transaction:create command', () => { ['token', 'transfer', '100000000', `--passphrase=${passphrase}`], config, ); - expect(inquirer.prompt).toHaveBeenCalledTimes(1); - expect(inquirer.prompt).toHaveBeenCalledWith([ - { message: 'Please enter: tokenID: ', name: 'tokenID', type: 'input' }, - { message: 'Please enter: amount: ', name: 'amount', type: 'input' }, - { - message: 'Please enter: recipientAddress: ', - name: 'recipientAddress', - type: 'input', - }, - { message: 'Please enter: data: ', name: 'data', type: 'input' }, - ]); + verifyIfInquirerCallsFor(questionsForTokenTransfer); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledTimes(1); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledWith(undefined, { transaction: mockEncodedTransaction.toString('hex'), @@ -714,17 +802,7 @@ describe('transaction:create command', () => { ['token', 'transfer', '100000000', '--nonce=999'], config, ); - expect(inquirer.prompt).toHaveBeenCalledTimes(1); - expect(inquirer.prompt).toHaveBeenCalledWith([ - { message: 'Please enter: tokenID: ', name: 'tokenID', type: 'input' }, - { message: 'Please enter: amount: ', name: 'amount', type: 'input' }, - { - message: 'Please enter: recipientAddress: ', - name: 'recipientAddress', - type: 'input', - }, - { message: 'Please enter: data: ', name: 'data', type: 'input' }, - ]); + verifyIfInquirerCallsFor(questionsForTokenTransfer); expect(readerUtils.getPassphraseFromPrompt).toHaveBeenCalledWith('passphrase'); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledTimes(1); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledWith(undefined, { @@ -736,17 +814,7 @@ describe('transaction:create command', () => { describe('transaction:create token transfer 100000000', () => { it('should prompt user for params and passphrase.', async () => { await CreateCommandExtended.run(['token', 'transfer', '100000000'], config); - expect(inquirer.prompt).toHaveBeenCalledTimes(1); - expect(inquirer.prompt).toHaveBeenCalledWith([ - { message: 'Please enter: tokenID: ', name: 'tokenID', type: 'input' }, - { message: 'Please enter: amount: ', name: 'amount', type: 'input' }, - { - message: 'Please enter: recipientAddress: ', - name: 'recipientAddress', - type: 'input', - }, - { message: 'Please enter: data: ', name: 'data', type: 'input' }, - ]); + verifyIfInquirerCallsFor(questionsForTokenTransfer); expect(readerUtils.getPassphraseFromPrompt).toHaveBeenCalledWith('passphrase'); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledTimes(1); expect(CreateCommandExtended.prototype.printJSON).toHaveBeenCalledWith(undefined, { diff --git a/commander/test/bootstrapping/commands/transaction/prompt.spec.ts b/commander/test/bootstrapping/commands/transaction/prompt.spec.ts index e787c120f81..3a285c60387 100644 --- a/commander/test/bootstrapping/commands/transaction/prompt.spec.ts +++ b/commander/test/bootstrapping/commands/transaction/prompt.spec.ts @@ -12,101 +12,66 @@ * Removal or modification of this copyright notice is prohibited. * */ +import * as inquirer from 'inquirer'; +import { getParamsFromPrompt } from '../../../../src/utils/reader'; +import { castValidationSchema } from '../../../helpers/transactions'; -import { - prepareQuestions, - transformAsset, - transformNestedAsset, -} from '../../../../src/utils/reader'; -import { - tokenTransferParamsSchema, - registerMultisignatureParamsSchema, - posVoteParamsSchema, -} from '../../../helpers/transactions'; - -describe('prompt', () => { - describe('prepareQuestions', () => { - it('should return array of questions for given asset schema', () => { - const questions = prepareQuestions(tokenTransferParamsSchema); - expect(questions).toEqual([ - { type: 'input', name: 'tokenID', message: 'Please enter: tokenID: ' }, - { type: 'input', name: 'amount', message: 'Please enter: amount: ' }, - { - type: 'input', - name: 'recipientAddress', - message: 'Please enter: recipientAddress: ', - }, - { type: 'input', name: 'data', message: 'Please enter: data: ' }, - ]); - }); - }); - - describe('transformAsset', () => { - it('should transform result according to asset schema', () => { - const questions = prepareQuestions(registerMultisignatureParamsSchema); - const transformedAsset = transformAsset(registerMultisignatureParamsSchema, { - numberOfSignatures: '4', - mandatoryKeys: 'a,b', - optionalKeys: '', - signatures: 'c,d', +describe('getParamsFromPrompt', () => { + it('should cast uint64, sint64 types to BigInt and uint32, sint32 to Number', async () => { + const uInt64 = '12312321'; + const sInt64 = '-12321312'; + const uInt32 = '10'; + const sInt32 = '-10'; + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ + uInt64, + }) + .mockResolvedValueOnce({ + sInt64, + }) + .mockResolvedValueOnce({ + uInt32, + }) + .mockResolvedValueOnce({ + sInt32, + }) + .mockResolvedValueOnce({ + uInt64Array: `${uInt64},${uInt64}`, + }) + .mockResolvedValueOnce({ + sInt64Array: `${sInt64},${sInt64}`, + }) + .mockResolvedValueOnce({ + uInt32Array: `${uInt32},${uInt32}`, + }) + .mockResolvedValueOnce({ + sInt32Array: `${sInt32},${sInt32}`, + }) + .mockResolvedValueOnce({ + nested: `${uInt64},${sInt64},${uInt32},${sInt32}`, + }) + .mockResolvedValue({ + askAgain: false, }); - expect(questions).toEqual([ - { - type: 'input', - name: 'numberOfSignatures', - message: 'Please enter: numberOfSignatures: ', - }, - { - type: 'input', - name: 'mandatoryKeys', - message: 'Please enter: mandatoryKeys(comma separated values (a,b)): ', - }, - { - type: 'input', - name: 'optionalKeys', - message: 'Please enter: optionalKeys(comma separated values (a,b)): ', - }, - { - type: 'input', - name: 'signatures', - message: 'Please enter: signatures(comma separated values (a,b)): ', - }, - ]); - expect(transformedAsset).toEqual({ - numberOfSignatures: 4, - mandatoryKeys: ['a', 'b'], - optionalKeys: [], - signatures: ['c', 'd'], - }); - }); - }); - - describe('transformNestedAsset', () => { - it('should transform result according to nested asset schema', () => { - const questions = prepareQuestions(posVoteParamsSchema); - const transformedAsset = transformNestedAsset(posVoteParamsSchema, [ - { stakes: 'a,100' }, - { stakes: 'b,300' }, - ]); - expect(questions).toEqual([ + await expect(getParamsFromPrompt(castValidationSchema)).resolves.toEqual({ + uInt64: BigInt(uInt64), + sInt64: BigInt(sInt64), + uInt32: Number(uInt32), + sInt32: Number(sInt32), + uInt64Array: [BigInt(uInt64), BigInt(uInt64)], + sInt64Array: [BigInt(sInt64), BigInt(sInt64)], + uInt32Array: [Number(uInt32), Number(uInt32)], + sInt32Array: [Number(sInt32), Number(sInt32)], + nested: [ { - type: 'input', - name: 'stakes', - message: 'Please enter: stakes(validatorAddress, amount): ', + uInt64: BigInt(uInt64), + sInt64: BigInt(sInt64), + uInt32: Number(uInt32), + sInt32: Number(sInt32), }, - { - type: 'confirm', - name: 'askAgain', - message: 'Want to enter another stakes(validatorAddress, amount)', - }, - ]); - expect(transformedAsset).toEqual({ - stakes: [ - { validatorAddress: 'a', amount: 100 }, - { validatorAddress: 'b', amount: 300 }, - ], - }); + ], }); }); }); diff --git a/commander/test/helpers/transactions.ts b/commander/test/helpers/transactions.ts index 6088b04d042..9b97a65148d 100644 --- a/commander/test/helpers/transactions.ts +++ b/commander/test/helpers/transactions.ts @@ -165,6 +165,136 @@ export const posVoteParamsSchema = { }, }; +export const schemaWithArray = { + $id: '/lisk/schemaWithArray', + type: 'object', + required: ['attributesArray'], + properties: { + attributesArray: { + type: 'array', + fieldNumber: 1, + items: { + dataType: 'uint64', + }, + }, + }, +}; + +export const schemaWithArrayOfObjects = { + $id: '/lisk/schemaWithArrayOfObjects', + type: 'object', + required: ['attributesArray'], + properties: { + attributesArray: { + type: 'array', + fieldNumber: 4, + items: { + type: 'object', + required: ['module', 'attributes'], + properties: { + module: { + dataType: 'string', + minLength: 0, + maxLength: 10, + pattern: '^[a-zA-Z0-9]*$', + fieldNumber: 1, + }, + attributes: { + dataType: 'bytes', + fieldNumber: 2, + }, + }, + }, + }, + }, +}; + +export const castValidationSchema = { + $id: '/lisk/castValidation', + type: 'object', + required: [ + 'uInt64', + 'sIn64', + 'uInt32', + 'sInt32', + 'uInt64Array', + 'sInt64Array', + 'uInt32Array', + 'sInt32Array', + ], + properties: { + uInt64: { + dataType: 'uint64', + fieldNumber: 1, + }, + sInt64: { + dataType: 'sint64', + fieldNumber: 2, + }, + uInt32: { + dataType: 'uint32', + fieldNumber: 3, + }, + sInt32: { + dataType: 'sint32', + fieldNumber: 4, + }, + uInt64Array: { + type: 'array', + fieldNumber: 5, + items: { + dataType: 'uint64', + }, + }, + sInt64Array: { + type: 'array', + fieldNumber: 6, + items: { + dataType: 'sint64', + }, + }, + uInt32Array: { + type: 'array', + fieldNumber: 7, + items: { + dataType: 'uint32', + }, + }, + sInt32Array: { + type: 'array', + fieldNumber: 8, + items: { + dataType: 'sint32', + }, + }, + nested: { + type: 'array', + fieldNumber: 9, + items: { + type: 'object', + properties: { + uInt64: { + dataType: 'uint64', + fieldNumber: 1, + }, + sInt64: { + dataType: 'sint64', + fieldNumber: 2, + }, + uInt32: { + dataType: 'uint32', + fieldNumber: 3, + }, + sInt32: { + dataType: 'sint32', + fieldNumber: 4, + }, + }, + }, + }, + }, +}; + export const genesisBlockID = Buffer.from( 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'hex', diff --git a/elements/lisk-api-client/package.json b/elements/lisk-api-client/package.json index 5cf49fd4caf..1898cfe07ae 100644 --- a/elements/lisk-api-client/package.json +++ b/elements/lisk-api-client/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-api-client", - "version": "6.0.0-rc.4", + "version": "6.1.0-rc.0", "description": "An API client for the Lisk network", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -35,16 +35,16 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-transactions": "^6.0.0-rc.2", - "@liskhq/lisk-validator": "^0.8.0-rc.2", + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-transactions": "^6.1.0-rc.0", + "@liskhq/lisk-validator": "^0.9.0-rc.0", "isomorphic-ws": "4.0.1", "ws": "8.11.0", "zeromq": "6.0.0-beta.6" }, "devDependencies": { - "@liskhq/lisk-chain": "^0.5.0-rc.4", + "@liskhq/lisk-chain": "^0.6.0-rc.0", "@types/jest": "29.2.3", "@types/jest-when": "3.5.2", "@types/node": "18.15.3", diff --git a/elements/lisk-chain/package.json b/elements/lisk-chain/package.json index a4e48b48377..42026602560 100644 --- a/elements/lisk-chain/package.json +++ b/elements/lisk-chain/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-chain", - "version": "0.5.0-rc.4", + "version": "0.6.0-rc.0", "description": "Blocks and state management implementation that are used for block processing according to the Lisk protocol", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -35,16 +35,16 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-db": "0.3.10", - "@liskhq/lisk-tree": "^0.4.0-rc.2", + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-db": "0.3.7", + "@liskhq/lisk-tree": "^0.5.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", - "@liskhq/lisk-validator": "^0.8.0-rc.2", + "@liskhq/lisk-validator": "^0.9.0-rc.0", "debug": "4.3.4" }, "devDependencies": { - "@liskhq/lisk-passphrase": "^4.0.0-rc.0", + "@liskhq/lisk-passphrase": "^4.1.0-rc.0", "@types/debug": "4.1.5", "@types/faker": "4.1.10", "@types/jest": "29.2.3", diff --git a/elements/lisk-chain/src/block_assets.ts b/elements/lisk-chain/src/block_assets.ts index 9d06176dd90..176157f7bbf 100644 --- a/elements/lisk-chain/src/block_assets.ts +++ b/elements/lisk-chain/src/block_assets.ts @@ -100,7 +100,9 @@ export class BlockAssets { ); } if (last.module > asset.module) { - throw new Error('Assets are not sorted in the increasing values of moduleID.'); + throw new Error( + 'Assets are not sorted by the module property value in lexicographical order.', + ); } // Check for duplicates if (i > 0 && asset.module === last.module) { @@ -118,7 +120,9 @@ export class BlockAssets { validator.validate(blockAssetSchema, asset); if (last.module > asset.module) { - throw new Error('Assets are not sorted in the increasing values of moduleID.'); + throw new Error( + 'Assets are not sorted by the module property value in lexicographical order.', + ); } if (i > 0 && asset.module === last.module) { throw new Error(`Module with ID ${this._assets[i].module} has duplicate entries.`); diff --git a/elements/lisk-chain/src/constants.ts b/elements/lisk-chain/src/constants.ts index b8e792b30f7..fbd33b11848 100644 --- a/elements/lisk-chain/src/constants.ts +++ b/elements/lisk-chain/src/constants.ts @@ -32,7 +32,6 @@ export const GENESIS_BLOCK_TRANSACTION_ROOT = EMPTY_HASH; export const TAG_BLOCK_HEADER = utils.createMessageTag('BH'); export const TAG_TRANSACTION = utils.createMessageTag('TX'); -// TODO: Actual size TBD export const MAX_ASSET_DATA_SIZE_BYTES = 18; export const SIGNATURE_LENGTH_BYTES = 64; diff --git a/elements/lisk-chain/src/data_access/storage.ts b/elements/lisk-chain/src/data_access/storage.ts index 5b194e9cc28..33c2a14dab6 100644 --- a/elements/lisk-chain/src/data_access/storage.ts +++ b/elements/lisk-chain/src/data_access/storage.ts @@ -506,7 +506,7 @@ export class Storage { const ids = await this._db.get(concatDBKeys(DB_KEY_TRANSACTIONS_BLOCK_ID, blockID)); const idLength = 32; for (let i = 0; i < ids.length; i += idLength) { - txIDs.push(ids.slice(i, i + idLength)); + txIDs.push(ids.subarray(i, i + idLength)); } } catch (error) { if (!(error instanceof NotFoundError)) { diff --git a/elements/lisk-chain/src/index.ts b/elements/lisk-chain/src/index.ts index 063909b55fb..4a63b79e9bd 100644 --- a/elements/lisk-chain/src/index.ts +++ b/elements/lisk-chain/src/index.ts @@ -34,6 +34,7 @@ export { MAX_MODULE_NAME_LENGTH, MIN_CROSS_CHAIN_COMMAND_NAME_LENGTH, MIN_MODULE_NAME_LENGTH, + EMPTY_BUFFER, } from './constants'; export * from './db_keys'; export type { RawBlock } from './types'; diff --git a/elements/lisk-chain/src/state_store/state_store.ts b/elements/lisk-chain/src/state_store/state_store.ts index bb27aed7e42..49fdab8e377 100644 --- a/elements/lisk-chain/src/state_store/state_store.ts +++ b/elements/lisk-chain/src/state_store/state_store.ts @@ -47,19 +47,10 @@ export class StateStore { this._latestSnapshotId = -1; } - // TODO: Remove accepting number for subStorePrefix - public getStore(storePrefix: Buffer, subStorePrefix: Buffer | number): StateStore { - let storePrefixBuffer: Buffer; - if (typeof subStorePrefix === 'number') { - storePrefixBuffer = Buffer.alloc(2); - storePrefixBuffer.writeUInt16BE(subStorePrefix, 0); - } else { - storePrefixBuffer = subStorePrefix; - } - + public getStore(storePrefix: Buffer, subStorePrefix: Buffer): StateStore { const subStore = new StateStore( this._db, - Buffer.concat([DB_KEY_STATE_STORE, storePrefix, storePrefixBuffer]), + Buffer.concat([DB_KEY_STATE_STORE, storePrefix, subStorePrefix]), this._cache, ); @@ -186,14 +177,14 @@ export class StateStore { for (const data of cachedValues) { existingKey[data.key.toString('binary')] = true; result.push({ - key: data.key.slice(this._prefix.length), + key: data.key.subarray(this._prefix.length), value: data.value, }); } for (const data of storedData) { if (existingKey[data.key.toString('binary')] === undefined) { result.push({ - key: data.key.slice(this._prefix.length), + key: data.key.subarray(this._prefix.length), value: data.value, }); } diff --git a/elements/lisk-chain/src/state_store/utils.ts b/elements/lisk-chain/src/state_store/utils.ts index 85a56c20be0..23467c785e2 100644 --- a/elements/lisk-chain/src/state_store/utils.ts +++ b/elements/lisk-chain/src/state_store/utils.ts @@ -24,6 +24,6 @@ export const copyBuffer = (value: Buffer): Buffer => { export const toSMTKey = (value: Buffer): Buffer => // First byte is the DB prefix Buffer.concat([ - value.slice(1, SMT_PREFIX_SIZE + 1), - utils.hash(value.slice(SMT_PREFIX_SIZE + 1)), + value.subarray(1, SMT_PREFIX_SIZE + 1), + utils.hash(value.subarray(SMT_PREFIX_SIZE + 1)), ]); diff --git a/elements/lisk-chain/test/unit/block_assets.spec.ts b/elements/lisk-chain/test/unit/block_assets.spec.ts index 3af62b3a842..d075c5e6547 100644 --- a/elements/lisk-chain/test/unit/block_assets.spec.ts +++ b/elements/lisk-chain/test/unit/block_assets.spec.ts @@ -146,11 +146,11 @@ describe('block assets', () => { assetList = [ { module: 'auth', - data: utils.getRandomBytes(MAX_ASSET_DATA_SIZE_BYTES), + data: utils.getRandomBytes(MAX_ASSET_DATA_SIZE_BYTES / 2), }, { module: 'random', - data: utils.getRandomBytes(MAX_ASSET_DATA_SIZE_BYTES), + data: utils.getRandomBytes(MAX_ASSET_DATA_SIZE_BYTES / 2), }, ]; assets = new BlockAssets(assetList); @@ -158,8 +158,8 @@ describe('block assets', () => { }); }); - describe('when the assets are not sorted by moduleID', () => { - it('should throw error when assets are not sorted by moduleID', () => { + describe('when the assets are not sorted by module', () => { + it('should throw error when assets are not sorted by module', () => { assetList = [ { module: 'random', @@ -172,7 +172,7 @@ describe('block assets', () => { ]; assets = new BlockAssets(assetList); expect(() => assets.validate()).toThrow( - 'Assets are not sorted in the increasing values of moduleID.', + 'Assets are not sorted by the module property value in lexicographical order.', ); }); @@ -300,7 +300,7 @@ describe('block assets', () => { ]; assets = new BlockAssets(assetList); expect(() => assets.validateGenesis()).toThrow( - 'Assets are not sorted in the increasing values of moduleID.', + 'Assets are not sorted by the module property value in lexicographical order.', ); }); diff --git a/elements/lisk-chain/test/unit/block_header.spec.ts b/elements/lisk-chain/test/unit/block_header.spec.ts index 1580adff18f..03b6ff38911 100644 --- a/elements/lisk-chain/test/unit/block_header.spec.ts +++ b/elements/lisk-chain/test/unit/block_header.spec.ts @@ -57,7 +57,7 @@ const getGenesisBlockAttrs = () => ({ maxHeightGenerated: 0, validatorsHash: utils.hash(Buffer.alloc(0)), aggregateCommit: { - height: 0, + height: 1009988, aggregationBits: Buffer.alloc(0), certificateSignature: EMPTY_BUFFER, }, @@ -81,8 +81,10 @@ const blockHeaderProps = [ 'previousBlockID', 'generatorAddress', 'transactionRoot', + 'eventRoot', 'assetRoot', 'stateRoot', + 'impliesMaxPrevotes', 'maxHeightPrevoted', 'maxHeightGenerated', 'validatorsHash', @@ -143,6 +145,7 @@ describe('block_header', () => { expect(blockHeader.validatorsHash).toEqual(data.validatorsHash); expect(blockHeader.aggregateCommit).toEqual(data.aggregateCommit); expect(blockHeader.maxHeightPrevoted).toEqual(data.maxHeightPrevoted); + expect(blockHeader.impliesMaxPrevotes).toEqual(data.impliesMaxPrevotes); expect(blockHeader.maxHeightGenerated).toEqual(data.maxHeightGenerated); expect(blockHeader.assetRoot).toEqual(data.assetRoot); expect(blockHeader.transactionRoot).toEqual(data.transactionRoot); @@ -212,6 +215,14 @@ describe('block_header', () => { }); describe('validateGenesis', () => { + it('should not throw when genesis block is valid', () => { + const block = getGenesisBlockAttrs(); + const blockHeader = new BlockHeader({ + ...block, + }); + + expect(() => blockHeader.validateGenesis()).not.toThrow(); + }); it('should throw error if previousBlockID is not 32 bytes', () => { const block = getGenesisBlockAttrs(); const blockHeader = new BlockHeader({ @@ -248,6 +259,15 @@ describe('block_header', () => { ); }); + it('should throw error if maxHeightGenerated is not zero', () => { + const block = getGenesisBlockAttrs(); + const blockHeader = new BlockHeader({ ...block, maxHeightGenerated: 10 }); + + expect(() => blockHeader.validateGenesis()).toThrow( + 'Genesis block header maxHeightGenerated must equal 0', + ); + }); + it('should throw error if maxHeightPrevoted is not equal to header.height', () => { const block = getGenesisBlockAttrs(); const blockHeader = new BlockHeader({ ...block, maxHeightPrevoted: 10 }); @@ -296,6 +316,18 @@ describe('block_header', () => { ); }); + it('should throw error if impliesMaxPrevotes is false', () => { + const block = getGenesisBlockAttrs(); + const blockHeader = new BlockHeader({ + ...block, + impliesMaxPrevotes: false, + }); + + expect(() => blockHeader.validateGenesis()).toThrow( + 'Genesis block header impliesMaxPrevotes must be true', + ); + }); + it('should throw error if signature is not empty buffer', () => { const block = getGenesisBlockAttrs(); const blockHeader = new BlockHeader({ ...block, signature: utils.getRandomBytes(32) }); diff --git a/elements/lisk-chain/test/unit/chain.spec.ts b/elements/lisk-chain/test/unit/chain.spec.ts index d86980c6ff5..4eea93c6c60 100644 --- a/elements/lisk-chain/test/unit/chain.spec.ts +++ b/elements/lisk-chain/test/unit/chain.spec.ts @@ -33,6 +33,7 @@ import { DEFAULT_MAX_BLOCK_HEADER_CACHE, DEFAULT_MIN_BLOCK_HEADER_CACHE, } from '../../src/constants'; +import { BlockAssets, BlockHeader, Transaction } from '../../src'; describe('chain', () => { const constants = { @@ -200,7 +201,7 @@ describe('chain', () => { beforeEach(async () => { stateStore = new StateStore(db); jest.spyOn(stateStore, 'finalize'); - const subStore = stateStore.getStore(utils.intToBuffer(2, 4), 0); + const subStore = stateStore.getStore(utils.intToBuffer(2, 4), Buffer.from([0, 0])); await subStore.set(utils.getRandomBytes(20), utils.getRandomBytes(100)); batch = new Batch(); jest.spyOn(batch, 'set'); @@ -296,11 +297,19 @@ describe('chain', () => { it('should not throw error with a valid block', async () => { const txs = new Array(20).fill(0).map(() => getTransaction()); + const totalSize = txs.reduce((prev, curr) => prev + curr.getBytes().length, 0); + (chainInstance as any).constants.maxTransactionsSize = totalSize; block = await createValidDefaultBlock({ transactions: txs, }); + jest.spyOn(BlockHeader.prototype, 'validate'); + jest.spyOn(BlockAssets.prototype, 'validate'); + jest.spyOn(Transaction.prototype, 'validate'); // Act & assert expect(() => chainInstance.validateBlock(block, { version: 2 })).not.toThrow(); + expect(BlockHeader.prototype.validate).toHaveBeenCalledTimes(1); + expect(BlockAssets.prototype.validate).toHaveBeenCalledTimes(1); + expect(Transaction.prototype.validate).toHaveBeenCalledTimes(txs.length); }); it('should throw error if transaction root does not match', async () => { @@ -317,12 +326,13 @@ describe('chain', () => { it('should throw error if transactions exceeds max transactions length', async () => { // Arrange - (chainInstance as any).constants.maxTransactionsSize = 100; const txs = new Array(200).fill(0).map(() => getTransaction()); + const totalSize = txs.reduce((prev, curr) => prev + curr.getBytes().length, 0); + (chainInstance as any).constants.maxTransactionsSize = totalSize - 1; block = await createValidDefaultBlock({ transactions: txs }); // Act & assert expect(() => chainInstance.validateBlock(block, { version: 2 })).toThrow( - 'Transactions length is longer than configured length: 100.', + `Transactions length is longer than configured length: ${totalSize - 1}.`, ); }); @@ -337,5 +347,41 @@ describe('chain', () => { 'Block version must be 2.', ); }); + + it('should throw error if block header validation fails', async () => { + const txs = new Array(20).fill(0).map(() => getTransaction()); + block = await createValidDefaultBlock({ + transactions: txs, + }); + jest.spyOn(BlockHeader.prototype, 'validate').mockImplementation(() => { + throw new Error('invalid header'); + }); + // Act & assert + expect(() => chainInstance.validateBlock(block, { version: 2 })).toThrow('invalid header'); + }); + + it('should throw error if block asset validation fails', async () => { + const txs = new Array(20).fill(0).map(() => getTransaction()); + block = await createValidDefaultBlock({ + transactions: txs, + }); + jest.spyOn(BlockAssets.prototype, 'validate').mockImplementation(() => { + throw new Error('invalid assets'); + }); + // Act & assert + expect(() => chainInstance.validateBlock(block, { version: 2 })).toThrow('invalid assets'); + }); + + it('should throw error if transaction validation fails', async () => { + const txs = new Array(20).fill(0).map(() => getTransaction()); + block = await createValidDefaultBlock({ + transactions: txs, + }); + jest.spyOn(Transaction.prototype, 'validate').mockImplementation(() => { + throw new Error('invalid tx'); + }); + // Act & assert + expect(() => chainInstance.validateBlock(block, { version: 2 })).toThrow('invalid tx'); + }); }); }); diff --git a/elements/lisk-chain/test/unit/event.spec.ts b/elements/lisk-chain/test/unit/event.spec.ts index 4884d893451..193d0297a8e 100644 --- a/elements/lisk-chain/test/unit/event.spec.ts +++ b/elements/lisk-chain/test/unit/event.spec.ts @@ -71,7 +71,7 @@ describe('event', () => { const { key } = pairs[i]; expect(key).toHaveLength(EVENT_TOPIC_HASH_LENGTH_BYTES + EVENT_TOTAL_INDEX_LENGTH_BYTES); - const index = key.slice(EVENT_TOPIC_HASH_LENGTH_BYTES); + const index = key.subarray(EVENT_TOPIC_HASH_LENGTH_BYTES); // Check index const indexNum = index.readUInt32BE(0); diff --git a/elements/lisk-chain/test/unit/state_store/state_store.spec.ts b/elements/lisk-chain/test/unit/state_store/state_store.spec.ts index 27052098dba..9ee742617dc 100644 --- a/elements/lisk-chain/test/unit/state_store/state_store.spec.ts +++ b/elements/lisk-chain/test/unit/state_store/state_store.spec.ts @@ -31,7 +31,15 @@ const sampleSchema = { describe('state store', () => { let moduleID = utils.intToBuffer(2, 4); + const storePrefix = 0; + const storePrefixBuffer = Buffer.alloc(2); + storePrefixBuffer.writeUInt16BE(storePrefix, 0); + + const anotherStorePrefix = 1; + const anotherStorePrefixBuffer = Buffer.alloc(2); + anotherStorePrefixBuffer.writeUInt16BE(anotherStorePrefix, 0); + const existingKey = utils.getRandomBytes(20); const existingKey2 = utils.getRandomBytes(20); const existingValue = utils.getRandomBytes(64); @@ -43,8 +51,6 @@ describe('state store', () => { beforeEach(async () => { db = new InMemoryDatabase(); stateStore = new StateStore(db); - const storePrefixBuffer = Buffer.alloc(2); - storePrefixBuffer.writeUInt16BE(storePrefix, 0); await db.set( Buffer.concat([stateStore['_prefix'], moduleID, storePrefixBuffer, existingKey]), existingValue, @@ -59,17 +65,17 @@ describe('state store', () => { it('should keep the same cache as the original state store', async () => { const address = utils.getRandomBytes(20); const value = utils.getRandomBytes(64); - const subStore = stateStore.getStore(utils.intToBuffer(2, 4), 0); + const subStore = stateStore.getStore(utils.intToBuffer(2, 4), storePrefixBuffer); await subStore.set(address, value); // create different store from the state store - const newSubStore = stateStore.getStore(utils.intToBuffer(2, 4), 0); + const newSubStore = stateStore.getStore(utils.intToBuffer(2, 4), storePrefixBuffer); const valueFromNewStore = await newSubStore.get(address); expect(valueFromNewStore).toEqual(value); }); it('should append the prefix', () => { - const subStore = stateStore.getStore(utils.intToBuffer(2, 4), 0); + const subStore = stateStore.getStore(utils.intToBuffer(2, 4), storePrefixBuffer); // db prefix(1) + moduleID(4) + storePrefix(2) expect(subStore['_prefix']).toHaveLength(1 + 4 + 2); }); @@ -77,7 +83,7 @@ describe('state store', () => { describe('get', () => { it('should get from the cache if the key already exist', async () => { - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); const newKey = utils.getRandomBytes(20); await subStore.set(newKey, utils.getRandomBytes(10)); jest.spyOn(db, 'get'); @@ -89,7 +95,7 @@ describe('state store', () => { it('should get from the database if the key does not exist', async () => { jest.spyOn(db, 'get'); - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); const value = await subStore.get(existingKey); @@ -98,7 +104,7 @@ describe('state store', () => { }); it('should return copied value', async () => { - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); const value = await subStore.get(existingKey); value[0] = 233; @@ -109,7 +115,7 @@ describe('state store', () => { }); it('should throw not found error if deleted in the key', async () => { - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.del(existingKey); await expect(subStore.get(existingKey)).rejects.toThrow(NotFoundError); @@ -120,7 +126,7 @@ describe('state store', () => { it('should return decoded value', async () => { const address = utils.getRandomBytes(20); const encodedValue = codec.encode(sampleSchema, { address }); - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.set(address, encodedValue); const value = await subStore.getWithSchema>(address, sampleSchema); @@ -133,7 +139,7 @@ describe('state store', () => { it('should update the cached value if it exist in the cache', async () => { const address = utils.getRandomBytes(20); const value = utils.getRandomBytes(50); - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.set(address, value); const updatingValue = await subStore.get(address); @@ -150,7 +156,7 @@ describe('state store', () => { jest.spyOn(db, 'get'); const address = utils.getRandomBytes(20); const value = utils.getRandomBytes(50); - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.set(address, value); const updatingValue = await subStore.get(address); @@ -164,7 +170,7 @@ describe('state store', () => { it('should set encoded value', async () => { const address = utils.getRandomBytes(20); const encodedValue = codec.encode(sampleSchema, { address }); - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.setWithSchema(address, { address }, sampleSchema); const value = await subStore.get(address); @@ -178,7 +184,7 @@ describe('state store', () => { const value = utils.getRandomBytes(50); it('should mark as deleted if it exists in the cache', async () => { - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.set(address, value); await subStore.del(address); @@ -188,7 +194,7 @@ describe('state store', () => { it('should cache the original value and mark as deleted if it does not in the cache', async () => { jest.spyOn(db, 'get'); - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.del(existingKey); expect(db.get).toHaveReturnedTimes(1); @@ -198,7 +204,7 @@ describe('state store', () => { describe('iterate', () => { it('should return all the key-values with the prefix', async () => { - const subStore = stateStore.getStore(moduleID, 1); + const subStore = stateStore.getStore(moduleID, anotherStorePrefixBuffer); await subStore.set(Buffer.from([0]), utils.getRandomBytes(40)); await subStore.set(Buffer.from([1]), utils.getRandomBytes(40)); await subStore.set(Buffer.from([2]), utils.getRandomBytes(40)); @@ -214,9 +220,9 @@ describe('state store', () => { }); it('should return all the key-values with the prefix in reverse order', async () => { - const existingStore = stateStore.getStore(moduleID, storePrefix); + const existingStore = stateStore.getStore(moduleID, storePrefixBuffer); await existingStore.set(Buffer.from([0]), utils.getRandomBytes(40)); - const subStore = stateStore.getStore(moduleID, 1); + const subStore = stateStore.getStore(moduleID, anotherStorePrefixBuffer); await subStore.set(Buffer.from([0]), utils.getRandomBytes(40)); await subStore.set(Buffer.from([1]), utils.getRandomBytes(40)); await subStore.set(Buffer.from([2]), utils.getRandomBytes(40)); @@ -234,7 +240,7 @@ describe('state store', () => { }); it('should not return the deleted values', async () => { - const subStore = stateStore.getStore(moduleID, 1); + const subStore = stateStore.getStore(moduleID, anotherStorePrefixBuffer); await subStore.set(Buffer.from([0]), utils.getRandomBytes(40)); await subStore.set(Buffer.from([1]), utils.getRandomBytes(40)); await subStore.set(Buffer.from([2]), utils.getRandomBytes(40)); @@ -252,7 +258,7 @@ describe('state store', () => { it('should return the updated values in the cache', async () => { const expectedValue = Buffer.from('random'); - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.set(Buffer.from([0]), utils.getRandomBytes(40)); await subStore.set(Buffer.from([1]), utils.getRandomBytes(40)); await subStore.set(Buffer.from([2]), utils.getRandomBytes(40)); @@ -273,7 +279,7 @@ describe('state store', () => { it('should return decoded value', async () => { const address = utils.getRandomBytes(20); const encodedValue = codec.encode(sampleSchema, { address }); - const subStore = stateStore.getStore(moduleID, 1); + const subStore = stateStore.getStore(moduleID, anotherStorePrefixBuffer); await subStore.set(Buffer.from([0]), encodedValue); await subStore.set(Buffer.from([1]), encodedValue); await subStore.set(Buffer.from([2]), encodedValue); @@ -294,7 +300,7 @@ describe('state store', () => { describe('snapshot', () => { it('should not change the snapshot data when other operation is triggered', async () => { - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); subStore.createSnapshot(); const expected = utils.getRandomBytes(64); await subStore.set(Buffer.from([0]), expected); @@ -305,7 +311,7 @@ describe('state store', () => { }); it('should restore to snapshot value when the restore is called', async () => { - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); const id = subStore.createSnapshot(); await subStore.set(Buffer.from([0]), utils.getRandomBytes(64)); await subStore.del(existingKey); @@ -318,7 +324,7 @@ describe('state store', () => { }); it('should throw an error when restoring with an invalid snapshot ID', () => { - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); expect(() => subStore.restoreSnapshot(100)).toThrow( 'Invalid snapshot ID. Cannot revert to an older snapshot.', @@ -326,7 +332,7 @@ describe('state store', () => { }); it('should throw an error when restoring without taking a snapshot first', () => { - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); expect(() => subStore.restoreSnapshot(0)).toThrow( 'Invalid snapshot ID. Cannot revert to an older snapshot.', @@ -348,7 +354,6 @@ describe('state store', () => { const getKey = (mID: number, prefix: number) => { moduleID = Buffer.alloc(4); moduleID.writeInt32BE(mID, 0); - const storePrefixBuffer = Buffer.alloc(2); storePrefixBuffer.writeUInt16BE(prefix, 0); return Buffer.concat([moduleID, storePrefixBuffer]); }; @@ -358,10 +363,10 @@ describe('state store', () => { beforeEach(async () => { data = [getRandomData(), getRandomData(), getRandomData()]; - const subStore = stateStore.getStore(moduleID, storePrefix); + const subStore = stateStore.getStore(moduleID, storePrefixBuffer); await subStore.set(existingKey, utils.getRandomBytes(40)); await subStore.del(existingKey2); - const anotherStore = stateStore.getStore(moduleID, 1); + const anotherStore = stateStore.getStore(moduleID, anotherStorePrefixBuffer); for (const sample of data) { await anotherStore.set(sample.key, sample.value); } diff --git a/elements/lisk-chain/test/unit/transactions.spec.ts b/elements/lisk-chain/test/unit/transactions.spec.ts index 5f098a8d85d..be123d0aa0c 100644 --- a/elements/lisk-chain/test/unit/transactions.spec.ts +++ b/elements/lisk-chain/test/unit/transactions.spec.ts @@ -13,21 +13,40 @@ */ import { utils } from '@liskhq/lisk-cryptography'; import { Transaction } from '../../src/transaction'; +import { MAX_PARAMS_SIZE } from '../../src/constants'; describe('blocks/transactions', () => { - describe('transaction', () => { - it.todo('should have id'); - it.todo('should have senderAddress'); - it.todo('should throw when module is invalid'); - it.todo('should throw when command is invalid'); - it.todo('should throw when sender public key is invalid'); - it.todo('should throw when nonce is invalid'); - it.todo('should throw when fee is invalid'); - it.todo('should throw when params is invalid'); - }); describe('#validateTransaction', () => { let transaction: Transaction; + it('should not throw when transaction is valid', () => { + transaction = new Transaction({ + module: 'token', + command: 'transfer', + fee: BigInt(613000), + // 126 is the size of other properties + params: utils.getRandomBytes(MAX_PARAMS_SIZE), + nonce: BigInt(2), + senderPublicKey: utils.getRandomBytes(32), + signatures: [utils.getRandomBytes(64)], + }); + expect(() => transaction.validate()).not.toThrow(); + }); + + it('should not throw an error if params length is less than MAX_PARAMS_SIZE', () => { + transaction = new Transaction({ + module: 'token', + command: 'transfer', + fee: BigInt(613000), + // 126 is the size of other properties + params: utils.getRandomBytes(MAX_PARAMS_SIZE - 1), + nonce: BigInt(2), + senderPublicKey: utils.getRandomBytes(32), + signatures: [utils.getRandomBytes(64)], + }); + expect(() => transaction.validate()).not.toThrow(); + }); + it('should throw when module name is invalid', () => { transaction = new Transaction({ module: 'token_mod', @@ -54,6 +73,20 @@ describe('blocks/transactions', () => { expect(() => transaction.validate()).toThrow('Invalid command name'); }); + it('should throw when transaction is too big', () => { + transaction = new Transaction({ + module: 'token', + command: 'transfer', + fee: BigInt(613000), + // 126 is the size of other properties + params: utils.getRandomBytes(MAX_PARAMS_SIZE + 1), + nonce: BigInt(2), + senderPublicKey: utils.getRandomBytes(32), + signatures: [utils.getRandomBytes(64)], + }); + expect(() => transaction.validate()).toThrow('Params exceeds max size allowed'); + }); + it('should throw when sender public key is not 32 bytes', () => { transaction = new Transaction({ module: 'token', diff --git a/elements/lisk-client/package.json b/elements/lisk-client/package.json index bfc06230538..550ab2b1a5a 100644 --- a/elements/lisk-client/package.json +++ b/elements/lisk-client/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-client", - "version": "6.0.0-rc.4", + "version": "6.1.0-rc.0", "description": "A default set of Elements for use by clients of the Lisk network", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -56,14 +56,14 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-api-client": "^6.0.0-rc.4", - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-passphrase": "^4.0.0-rc.0", - "@liskhq/lisk-transactions": "^6.0.0-rc.2", - "@liskhq/lisk-tree": "^0.4.0-rc.2", + "@liskhq/lisk-api-client": "^6.1.0-rc.0", + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-passphrase": "^4.1.0-rc.0", + "@liskhq/lisk-transactions": "^6.1.0-rc.0", + "@liskhq/lisk-tree": "^0.5.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", - "@liskhq/lisk-validator": "^0.8.0-rc.2", + "@liskhq/lisk-validator": "^0.9.0-rc.0", "buffer": "6.0.3" }, "devDependencies": { diff --git a/elements/lisk-client/test/lisk-cryptography/utils.spec.ts b/elements/lisk-client/test/lisk-cryptography/utils.spec.ts index 1beb57ca23b..676499786c4 100644 --- a/elements/lisk-client/test/lisk-cryptography/utils.spec.ts +++ b/elements/lisk-client/test/lisk-cryptography/utils.spec.ts @@ -214,7 +214,7 @@ describe('buffer', () => { }); it('should be able to calculate the checkpoint from another checkpoint', () => { - const firstDistanceHashes = hashOnion(hashOnionBuffers[1].slice(), 1000, 1); + const firstDistanceHashes = hashOnion(hashOnionBuffers[1].subarray(), 1000, 1); expect(firstDistanceHashes[0]).toEqual(hashOnionBuffers[0]); expect(firstDistanceHashes[1000]).toEqual(hashOnionBuffers[1]); }); diff --git a/elements/lisk-codec/fuzz/round_trip.js b/elements/lisk-codec/fuzz/round_trip.js index 9ae9a018b68..6118c6dd710 100644 --- a/elements/lisk-codec/fuzz/round_trip.js +++ b/elements/lisk-codec/fuzz/round_trip.js @@ -72,7 +72,7 @@ function mutateRandomByte(buffer) { else if (mutationType < 0.66) { const index = Math.floor(Math.random() * (buffer.length + 1)); const mutation = utils.getRandomBytes(1); - buffer = Buffer.concat([buffer.slice(0, index), mutation, buffer.slice(index)]); + buffer = Buffer.concat([buffer.subarray(0, index), mutation, buffer.subarray(index)]); } // Remove a byte else { @@ -80,7 +80,7 @@ function mutateRandomByte(buffer) { return buffer; // Can't remove byte from buffer of length 1 } const index = Math.floor(Math.random() * buffer.length); - buffer = Buffer.concat([buffer.slice(0, index), buffer.slice(index + 1)]); + buffer = Buffer.concat([buffer.subarray(0, index), buffer.subarray(index + 1)]); } return buffer; diff --git a/elements/lisk-codec/package.json b/elements/lisk-codec/package.json index 24c71845042..0a5b2f592eb 100644 --- a/elements/lisk-codec/package.json +++ b/elements/lisk-codec/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-codec", - "version": "0.4.0-rc.2", + "version": "0.5.0-rc.0", "description": "Implementation of decoder and encoder using Lisk JSON schema according to the Lisk protocol", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -35,9 +35,9 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", - "@liskhq/lisk-validator": "^0.8.0-rc.2" + "@liskhq/lisk-validator": "^0.9.0-rc.0" }, "devDependencies": { "@types/jest": "29.2.3", diff --git a/elements/lisk-codec/src/varint.ts b/elements/lisk-codec/src/varint.ts index 92ce7495384..6494fe1714b 100644 --- a/elements/lisk-codec/src/varint.ts +++ b/elements/lisk-codec/src/varint.ts @@ -14,10 +14,16 @@ /* eslint-disable no-bitwise */ /* eslint-disable no-param-reassign */ +import { MAX_SINT32, MAX_SINT64, MAX_UINT32, MAX_UINT64 } from '@liskhq/lisk-validator'; + const msg = 0x80; const rest = 0x7f; export const writeUInt32 = (value: number): Buffer => { + if (value > MAX_UINT32) { + throw new Error('Value out of range of uint32'); + } + const result: number[] = []; let index = 0; while (value > rest) { @@ -32,6 +38,10 @@ export const writeUInt32 = (value: number): Buffer => { }; export const writeSInt32 = (value: number): Buffer => { + if (value > MAX_SINT32) { + throw new Error('Value out of range of sint32'); + } + if (value >= 0) { return writeUInt32(2 * value); } @@ -39,6 +49,10 @@ export const writeSInt32 = (value: number): Buffer => { }; export const writeUInt64 = (value: bigint): Buffer => { + if (value > MAX_UINT64) { + throw new Error('Value out of range of uint64'); + } + const result: number[] = []; let index = 0; while (value > BigInt(rest)) { @@ -53,6 +67,10 @@ export const writeUInt64 = (value: bigint): Buffer => { }; export const writeSInt64 = (value: bigint): Buffer => { + if (value > MAX_SINT64) { + throw new Error('Value out of range of sint64'); + } + if (value >= BigInt(0)) { return writeUInt64(BigInt(2) * value); } diff --git a/elements/lisk-codec/test/varint.spec.ts b/elements/lisk-codec/test/varint.spec.ts index 8dbfa8a9586..956f1a36055 100644 --- a/elements/lisk-codec/test/varint.spec.ts +++ b/elements/lisk-codec/test/varint.spec.ts @@ -11,6 +11,9 @@ * * Removal or modification of this copyright notice is prohibited. */ + +import { MAX_SINT32, MAX_SINT64, MAX_UINT32, MAX_UINT64 } from '@liskhq/lisk-validator'; + import { writeUInt32, writeSInt32, @@ -24,6 +27,22 @@ import { describe('varint', () => { describe('writer', () => { + it('should fail to encode uint32 when input is out of range', () => { + expect(() => writeUInt32(MAX_UINT32 + 1)).toThrow('Value out of range of uint32'); + }); + + it('should fail to encode uint64 when input is out of range', () => { + expect(() => writeUInt64(MAX_UINT64 + BigInt(1))).toThrow('Value out of range of uint64'); + }); + + it('should fail to encode sint32 when input is out of range', () => { + expect(() => writeSInt32(MAX_SINT32 + 1)).toThrow('Value out of range of sint32'); + }); + + it('should fail to encode sint64 when input is out of range', () => { + expect(() => writeSInt64(MAX_SINT64 + BigInt(1))).toThrow('Value out of range of sint64'); + }); + it('should encode uint32', () => { expect(writeUInt32(0)).toEqual(Buffer.from('00', 'hex')); expect(writeUInt32(300)).toEqual(Buffer.from('ac02', 'hex')); diff --git a/elements/lisk-cryptography/package.json b/elements/lisk-cryptography/package.json index f05e75e53a6..2663010baba 100644 --- a/elements/lisk-cryptography/package.json +++ b/elements/lisk-cryptography/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-cryptography", - "version": "4.0.0-rc.2", + "version": "4.1.0-rc.0", "description": "General cryptographic functions for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-passphrase": "^4.0.0-rc.0", + "@liskhq/lisk-passphrase": "^4.1.0-rc.0", "buffer-reverse": "1.0.1", "hash-wasm": "4.9.0", "tweetnacl": "1.0.3" diff --git a/elements/lisk-cryptography/src/address.ts b/elements/lisk-cryptography/src/address.ts index c4cad619eeb..3abaa752138 100644 --- a/elements/lisk-cryptography/src/address.ts +++ b/elements/lisk-cryptography/src/address.ts @@ -17,6 +17,7 @@ import { BINARY_ADDRESS_LENGTH, DEFAULT_LISK32_ADDRESS_PREFIX, LISK32_ADDRESS_LENGTH, + ED25519_PUBLIC_KEY_LENGTH, } from './constants'; import { getPublicKey } from './nacl'; import { hash } from './utils'; @@ -54,7 +55,7 @@ const convertUInt5ToBase32 = (uint5Array: number[]): string => export const getAddressFromPublicKey = (publicKey: Buffer): Buffer => { const buffer = hash(publicKey); - const truncatedBuffer = buffer.slice(0, BINARY_ADDRESS_LENGTH); + const truncatedBuffer = buffer.subarray(0, BINARY_ADDRESS_LENGTH); if (truncatedBuffer.length !== BINARY_ADDRESS_LENGTH) { throw new Error(`Lisk address must contain exactly ${BINARY_ADDRESS_LENGTH} bytes`); @@ -118,7 +119,12 @@ const addressToLisk32 = (address: Buffer): string => { export const getLisk32AddressFromPublicKey = ( publicKey: Buffer, prefix = DEFAULT_LISK32_ADDRESS_PREFIX, -): string => `${prefix}${addressToLisk32(getAddressFromPublicKey(publicKey))}`; +): string => { + if (publicKey.length !== ED25519_PUBLIC_KEY_LENGTH) { + throw new Error(`publicKey length must be ${ED25519_PUBLIC_KEY_LENGTH}.`); + } + return `${prefix}${addressToLisk32(getAddressFromPublicKey(publicKey))}`; +}; export const validateLisk32Address = ( address: string, diff --git a/elements/lisk-cryptography/src/bls.ts b/elements/lisk-cryptography/src/bls.ts index 36248e2e312..359c1d723d9 100644 --- a/elements/lisk-cryptography/src/bls.ts +++ b/elements/lisk-cryptography/src/bls.ts @@ -181,7 +181,7 @@ const hkdfSHA256 = (ikm: Buffer, length: number, salt: Buffer, info: Buffer) => t = hmacSHA256(PRK, Buffer.concat([t, info, Buffer.from([1 + i])]), SHA256); OKM = Buffer.concat([OKM, t]); } - return OKM.slice(0, length); + return OKM.subarray(0, length); }; const toLamportSK = (IKM: Buffer, salt: Buffer) => { @@ -190,7 +190,7 @@ const toLamportSK = (IKM: Buffer, salt: Buffer) => { const lamportSK = []; for (let i = 0; i < 255; i += 1) { - lamportSK.push(OKM.slice(i * 32, (i + 1) * 32)); + lamportSK.push(OKM.subarray(i * 32, (i + 1) * 32)); } return lamportSK; }; diff --git a/elements/lisk-cryptography/src/constants.ts b/elements/lisk-cryptography/src/constants.ts index bc57b3538d7..790b9e391c0 100644 --- a/elements/lisk-cryptography/src/constants.ts +++ b/elements/lisk-cryptography/src/constants.ts @@ -25,3 +25,4 @@ export const SHA256 = 'sha256'; export const LISK32_CHARSET = 'zxvcpmbn3465o978uyrtkqew2adsjhfg'; export const LISK32_ADDRESS_LENGTH = 41; export const MESSAGE_TAG_NON_PROTOCOL_MESSAGE = 'LSK_NPM_'; +export const ED25519_PUBLIC_KEY_LENGTH = 32; diff --git a/elements/lisk-cryptography/src/ed.ts b/elements/lisk-cryptography/src/ed.ts index 33cde3c6965..2abc718e199 100644 --- a/elements/lisk-cryptography/src/ed.ts +++ b/elements/lisk-cryptography/src/ed.ts @@ -37,8 +37,8 @@ export const getPublicKeyFromPrivateKey = (pk: Buffer): Buffer => getPublicKey(p const getMasterKeyFromSeed = (seed: Buffer) => { const hmac = crypto.createHmac('sha512', ED25519_CURVE); const digest = hmac.update(seed).digest(); - const leftBytes = digest.slice(0, 32); - const rightBytes = digest.slice(32); + const leftBytes = digest.subarray(0, 32); + const rightBytes = digest.subarray(32); return { key: leftBytes, chainCode: rightBytes, @@ -50,8 +50,8 @@ const getChildKey = (node: { key: Buffer; chainCode: Buffer }, index: number) => indexBuffer.writeUInt32BE(index, 0); const data = Buffer.concat([Buffer.alloc(1, 0), node.key, indexBuffer]); const digest = crypto.createHmac('sha512', node.chainCode).update(data).digest(); - const leftBytes = digest.slice(0, 32); - const rightBytes = digest.slice(32); + const leftBytes = digest.subarray(0, 32); + const rightBytes = digest.subarray(32); return { key: leftBytes, diff --git a/elements/lisk-cryptography/src/encrypt.ts b/elements/lisk-cryptography/src/encrypt.ts index 7e5e0fd3e59..64cd6eafb35 100644 --- a/elements/lisk-cryptography/src/encrypt.ts +++ b/elements/lisk-cryptography/src/encrypt.ts @@ -129,7 +129,7 @@ export const encryptAES128GCMWithPassword = async ( key = getKeyFromPassword(password, salt, iterations); } - const cipher = crypto.createCipheriv('aes-128-gcm', key.slice(0, 16), iv); + const cipher = crypto.createCipheriv('aes-128-gcm', key.subarray(0, 16), iv); const firstBlock = Buffer.isBuffer(plainText) ? cipher.update(plainText) : cipher.update(plainText, 'utf8'); @@ -138,7 +138,7 @@ export const encryptAES128GCMWithPassword = async ( return { ciphertext: encrypted.toString('hex'), - mac: crypto.createHash('sha256').update(key.slice(16, 32)).update(encrypted).digest('hex'), + mac: crypto.createHash('sha256').update(key.subarray(16, 32)).update(encrypted).digest('hex'), kdf, kdfparams: { parallelism, @@ -229,7 +229,11 @@ export async function decryptAES128GCMWithPassword( } else { key = getKeyFromPassword(password, hexToBuffer(salt, 'Salt'), iterations); } - const decipher = crypto.createDecipheriv('aes-128-gcm', key.slice(0, 16), hexToBuffer(iv, 'IV')); + const decipher = crypto.createDecipheriv( + 'aes-128-gcm', + key.subarray(0, 16), + hexToBuffer(iv, 'IV'), + ); decipher.setAuthTag(tagBuffer); const firstBlock = decipher.update(hexToBuffer(ciphertext, 'Cipher text')); const decrypted = Buffer.concat([firstBlock, decipher.final()]); diff --git a/elements/lisk-cryptography/src/legacy_address.ts b/elements/lisk-cryptography/src/legacy_address.ts index 01b4d8cb5e7..ca13f154a0a 100644 --- a/elements/lisk-cryptography/src/legacy_address.ts +++ b/elements/lisk-cryptography/src/legacy_address.ts @@ -25,10 +25,10 @@ export const getFirstEightBytesReversed = (input: string | Buffer): Buffer => { // Union type arguments on overloaded functions do not work in typescript. // Relevant discussion: https://github.com/Microsoft/TypeScript/issues/23155 if (typeof input === 'string') { - return reverse(Buffer.from(input).slice(0, BUFFER_SIZE)); + return reverse(Buffer.from(input).subarray(0, BUFFER_SIZE)); } - return reverse(Buffer.from(input).slice(0, BUFFER_SIZE)); + return reverse(Buffer.from(input).subarray(0, BUFFER_SIZE)); }; export const getLegacyAddressFromPublicKey = (publicKey: Buffer): string => { diff --git a/elements/lisk-cryptography/src/nacl/slow.ts b/elements/lisk-cryptography/src/nacl/slow.ts index 4b243150840..8d7f0452490 100644 --- a/elements/lisk-cryptography/src/nacl/slow.ts +++ b/elements/lisk-cryptography/src/nacl/slow.ts @@ -83,7 +83,7 @@ const PRIVATE_KEY_LENGTH = 32; export const getPublicKey: NaclInterface['getPublicKey'] = privateKey => { const { publicKey } = tweetnacl.sign.keyPair.fromSeed( - Uint8Array.from(privateKey.slice(0, PRIVATE_KEY_LENGTH)), + Uint8Array.from(privateKey.subarray(0, PRIVATE_KEY_LENGTH)), ); return Buffer.from(publicKey); diff --git a/elements/lisk-cryptography/src/utils.ts b/elements/lisk-cryptography/src/utils.ts index 086357f8cf3..aecb346c366 100644 --- a/elements/lisk-cryptography/src/utils.ts +++ b/elements/lisk-cryptography/src/utils.ts @@ -176,7 +176,7 @@ const defaultCount = 1000000; const defaultDistance = 1000; export const generateHashOnionSeed = (): Buffer => - hash(getRandomBytes(INPUT_SIZE)).slice(0, HASH_SIZE); + hash(getRandomBytes(INPUT_SIZE)).subarray(0, HASH_SIZE); export const hashOnion = ( seed: Buffer, @@ -195,7 +195,7 @@ export const hashOnion = ( const hashes = [seed]; for (let i = 1; i <= count; i += 1) { - const nextHash = hash(previousHash).slice(0, HASH_SIZE); + const nextHash = hash(previousHash).subarray(0, HASH_SIZE); if (i % distance === 0) { hashes.push(nextHash); } diff --git a/elements/lisk-cryptography/test/address.spec.ts b/elements/lisk-cryptography/test/address.spec.ts index 470a11f7f8c..22acad9534b 100644 --- a/elements/lisk-cryptography/test/address.spec.ts +++ b/elements/lisk-cryptography/test/address.spec.ts @@ -24,6 +24,7 @@ import { LISK32_CHARSET, DEFAULT_LISK32_ADDRESS_PREFIX, LISK32_ADDRESS_LENGTH, + ED25519_PUBLIC_KEY_LENGTH, } from '../src/constants'; import * as utils from '../src/utils'; @@ -45,7 +46,7 @@ describe('address', () => { describe('#getAddressFromPrivateKey', () => { it('should create correct address', () => { - expect(getAddressFromPrivateKey(defaultPrivateKey.slice(0, 64))).toEqual(defaultAddress); + expect(getAddressFromPrivateKey(defaultPrivateKey.subarray(0, 64))).toEqual(defaultAddress); }); }); @@ -57,8 +58,20 @@ describe('address', () => { }); describe('#getLisk32AddressFromPublicKey', () => { + it('should reject when publicKey length not equal to ED25519_PUBLIC_KEY_LENGTH', () => { + expect(() => + getLisk32AddressFromPublicKey( + Buffer.alloc(ED25519_PUBLIC_KEY_LENGTH - 1), + DEFAULT_LISK32_ADDRESS_PREFIX, + ), + ).toThrow(`publicKey length must be ${ED25519_PUBLIC_KEY_LENGTH}.`); + }); + it('should generate lisk32 address from publicKey', () => { - const address = getLisk32AddressFromPublicKey(defaultPublicKey, 'lsk'); + const address = getLisk32AddressFromPublicKey( + defaultPublicKey, + DEFAULT_LISK32_ADDRESS_PREFIX, + ); expect(address).toBe(getLisk32AddressFromAddress(defaultAddress)); }); diff --git a/elements/lisk-cryptography/test/ed.spec.ts b/elements/lisk-cryptography/test/ed.spec.ts index 3155064fc88..721625096cf 100644 --- a/elements/lisk-cryptography/test/ed.spec.ts +++ b/elements/lisk-cryptography/test/ed.spec.ts @@ -166,6 +166,20 @@ describe('getPrivateKeyFromPhraseAndPath', () => { ); }); + it('should derive distinct keys from same valid phrase but distinct paths', async () => { + const privateKeyFromPassphrase = await getPrivateKeyFromPhraseAndPath( + passphrase, + `m/44'/134'/0'`, + ); + + const anotherPrivateKeyFromPassphrase = await getPrivateKeyFromPhraseAndPath( + passphrase, + `m/44'/134'/1'`, + ); + + expect(privateKeyFromPassphrase).not.toEqual(anotherPrivateKeyFromPassphrase); + }); + it('should fail for empty string path', async () => { await expect(getPrivateKeyFromPhraseAndPath(passphrase, '')).rejects.toThrow( 'Invalid key derivation path format', diff --git a/elements/lisk-elements/package.json b/elements/lisk-elements/package.json index cb3a2c04796..b56886a3f75 100644 --- a/elements/lisk-elements/package.json +++ b/elements/lisk-elements/package.json @@ -1,6 +1,6 @@ { "name": "lisk-elements", - "version": "6.0.0-rc.4", + "version": "6.1.0-rc.0", "description": "Elements for building blockchain applications in the Lisk network", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -35,18 +35,18 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-api-client": "^6.0.0-rc.4", - "@liskhq/lisk-chain": "^0.5.0-rc.4", - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-db": "0.3.10", - "@liskhq/lisk-p2p": "^0.9.0-rc.2", - "@liskhq/lisk-passphrase": "^4.0.0-rc.0", - "@liskhq/lisk-transaction-pool": "^0.7.0-rc.2", - "@liskhq/lisk-transactions": "^6.0.0-rc.2", - "@liskhq/lisk-tree": "^0.4.0-rc.2", + "@liskhq/lisk-api-client": "^6.1.0-rc.0", + "@liskhq/lisk-chain": "^0.6.0-rc.0", + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-db": "0.3.7", + "@liskhq/lisk-p2p": "^0.10.0-rc.0", + "@liskhq/lisk-passphrase": "^4.1.0-rc.0", + "@liskhq/lisk-transaction-pool": "^0.8.0-rc.0", + "@liskhq/lisk-transactions": "^6.1.0-rc.0", + "@liskhq/lisk-tree": "^0.5.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", - "@liskhq/lisk-validator": "^0.8.0-rc.2" + "@liskhq/lisk-validator": "^0.9.0-rc.0" }, "devDependencies": { "@types/jest": "29.2.3", diff --git a/elements/lisk-p2p/package.json b/elements/lisk-p2p/package.json index 416e15de252..f04b5889734 100644 --- a/elements/lisk-p2p/package.json +++ b/elements/lisk-p2p/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-p2p", - "version": "0.9.0-rc.2", + "version": "0.10.0-rc.0", "description": "Unstructured P2P library for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -41,9 +41,9 @@ "disableLocalIPs": "./scripts/disableTestLocalIPs.sh 2 19" }, "dependencies": { - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-validator": "^0.8.0-rc.2", + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-validator": "^0.9.0-rc.0", "lodash.shuffle": "4.2.0", "semver": "7.5.2", "socketcluster-client": "14.3.1", diff --git a/elements/lisk-passphrase/package.json b/elements/lisk-passphrase/package.json index 0e576976167..4ad128fad5a 100644 --- a/elements/lisk-passphrase/package.json +++ b/elements/lisk-passphrase/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-passphrase", - "version": "4.0.0-rc.0", + "version": "4.1.0-rc.0", "description": "Mnemonic passphrase helpers for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", diff --git a/elements/lisk-transaction-pool/package.json b/elements/lisk-transaction-pool/package.json index 3007999d4f6..2b280f18182 100644 --- a/elements/lisk-transaction-pool/package.json +++ b/elements/lisk-transaction-pool/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-transaction-pool", - "version": "0.7.0-rc.2", + "version": "0.8.0-rc.0", "description": "Transaction pool library for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -36,7 +36,7 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", "debug": "4.3.4" }, diff --git a/elements/lisk-transactions/package.json b/elements/lisk-transactions/package.json index 01f821250e3..f9850351712 100644 --- a/elements/lisk-transactions/package.json +++ b/elements/lisk-transactions/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-transactions", - "version": "6.0.0-rc.2", + "version": "6.1.0-rc.0", "description": "Utility functions related to transactions according to the Lisk protocol", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -35,9 +35,9 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-validator": "^0.8.0-rc.2" + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-validator": "^0.9.0-rc.0" }, "devDependencies": { "@types/jest": "29.2.3", diff --git a/elements/lisk-tree/package.json b/elements/lisk-tree/package.json index 8b804cf9dbe..5fca3ffbb45 100644 --- a/elements/lisk-tree/package.json +++ b/elements/lisk-tree/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-tree", - "version": "0.4.0-rc.2", + "version": "0.5.0-rc.0", "description": "Library containing Merkle tree implementations for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0" }, "devDependencies": { diff --git a/elements/lisk-tree/src/merkle_tree/merkle_tree.ts b/elements/lisk-tree/src/merkle_tree/merkle_tree.ts index 46703a7a455..3cdd1baa42f 100644 --- a/elements/lisk-tree/src/merkle_tree/merkle_tree.ts +++ b/elements/lisk-tree/src/merkle_tree/merkle_tree.ts @@ -452,10 +452,11 @@ export class MerkleTree { type === NodeType.BRANCH ? value.readInt32BE(BRANCH_PREFIX.length + LAYER_INDEX_SIZE) : value.readInt32BE(LEAF_PREFIX.length); - const rightHash = type === NodeType.BRANCH ? value.slice(-1 * NODE_HASH_SIZE) : Buffer.alloc(0); + const rightHash = + type === NodeType.BRANCH ? value.subarray(-1 * NODE_HASH_SIZE) : Buffer.alloc(0); const leftHash = type === NodeType.BRANCH - ? value.slice(-2 * NODE_HASH_SIZE, -1 * NODE_HASH_SIZE) + ? value.subarray(-2 * NODE_HASH_SIZE, -1 * NODE_HASH_SIZE) : Buffer.alloc(0); return { diff --git a/elements/lisk-utils/package.json b/elements/lisk-utils/package.json index 1219e673ab1..cfc48d298d0 100644 --- a/elements/lisk-utils/package.json +++ b/elements/lisk-utils/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-utils", - "version": "0.4.0-rc.0", + "version": "0.4.0", "description": "Library containing generic utility functions for use with Lisk-related software", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", diff --git a/elements/lisk-validator/package.json b/elements/lisk-validator/package.json index edcd426bafd..c4d3244484b 100644 --- a/elements/lisk-validator/package.json +++ b/elements/lisk-validator/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-validator", - "version": "0.8.0-rc.2", + "version": "0.9.0-rc.0", "description": "Validation library according to the Lisk protocol", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -36,7 +36,7 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", "ajv": "8.1.0", "ajv-formats": "2.1.1", "debug": "4.3.4", diff --git a/elements/lisk-validator/src/constants.ts b/elements/lisk-validator/src/constants.ts index a2473c75065..13b8267d5b3 100644 --- a/elements/lisk-validator/src/constants.ts +++ b/elements/lisk-validator/src/constants.ts @@ -13,9 +13,9 @@ * */ -export const MAX_SINT32 = 2147483647; // (2 ** (32 - 1)) + 1 * -1 -export const MIN_SINT32 = MAX_SINT32 * -1; // ((2 ** (32 - 1)) - 1) * -1 +export const MAX_SINT32 = 2147483647; // (2 ** (32 - 1)) - 1 +export const MIN_SINT32 = MAX_SINT32 * -1 - 1; // (2 ** (32 - 1)) * -1 export const MAX_UINT32 = 4294967295; // (2 ** 32) - 1 -export const MAX_UINT64 = BigInt('18446744073709551615'); // BigInt((2 ** 64) - 1) - 1 -export const MAX_SINT64 = BigInt('9223372036854775807'); // BigInt(2 ** (64 - 1) - 1) -1 -export const MIN_SINT64 = MAX_SINT64 * BigInt(-1) - BigInt(1); // (BigInt(2 ** (64 - 1) - 1) -1) * BigInt(-1) +export const MAX_UINT64 = BigInt('18446744073709551615'); // BigInt((2 ** 64) - 1) - BigInt(1) +export const MAX_SINT64 = BigInt('9223372036854775807'); // BigInt(2 ** (64 - 1) - 1) - BigInt(1) +export const MIN_SINT64 = MAX_SINT64 * BigInt(-1) - BigInt(1); // BigInt(2 ** (64 - 1)) * BigInt(-1) diff --git a/elements/lisk-validator/test/validation.spec.ts b/elements/lisk-validator/test/validation.spec.ts index 4e5aa726076..8bd6077ed86 100644 --- a/elements/lisk-validator/test/validation.spec.ts +++ b/elements/lisk-validator/test/validation.spec.ts @@ -327,8 +327,8 @@ describe('validation', () => { return expect(isSInt32(2147483648)).toBeFalse(); }); - it('should return false when a number "-2147483648" which is just below the limit of sint32', () => { - return expect(isSInt32(-2147483648)).toBeFalse(); + it('should return false when a number "-2147483649" which is just below the limit of sint32', () => { + return expect(isSInt32(-2147483649)).toBeFalse(); }); it('should return true when a valid number was provided', () => { diff --git a/examples/interop/README.md b/examples/interop/README.md index 50cfcda127c..61750cf20eb 100644 --- a/examples/interop/README.md +++ b/examples/interop/README.md @@ -52,7 +52,8 @@ Install and build `pos-sidechain-example-two` #### Run apps using pm2 -Install [pm2](https://pm2.keymetrics.io/) if not installed using `npm install pm2 -g` +- Install [pm2](https://pm2.keymetrics.io/) if not installed using `npm install pm2 -g` +- Install []`ts-node`](https://www.npmjs.com/package/ts-node) globally Run 2 instances mainchain node @@ -79,8 +80,8 @@ Interact with applications using `pm2` #### Register chains - Run `ts-node pos-mainchain-fast/config/scripts/sidechain_registration.ts` to register all the sidechains on the mainchain node. -- Run `ts-node pos-sidechain-example-one/config/scripts/mainchain_registration.ts` to register sidechain `sidechain_example_one` on mainchain. -- Run `ts-node pos-sidechain-example-two/config/scripts/mainchain_registration.ts` to register sidechain `sidechain_example_two` on mainchain. +- Run `ts-node pos-sidechain-example-one/config/scripts/mainchain_registration.ts` to register mainchain on sidechain `sidechain_example_one`. +- Run `ts-node pos-sidechain-example-two/config/scripts/mainchain_registration.ts` to register mainchain on sidechain `sidechain_example_two`. #### Check chain status @@ -89,7 +90,12 @@ Interact with applications using `pm2` Now observe logs, initially it will log `No valid CCU can be generated for the height: ${newBlockHeader.height}` until first finalized height is reached. -When the finalized height is reached, check chain status as described above and it should update lastCertificate height > 0 and status to 1 which means the CCU was sent successfully and chain is active now. +When the finalized height is reached, check chain status as described above and it should update lastCertificate `height > 0` and status to `1` which means the CCU was sent successfully and chain is active now. + +### Authorize ChainConnector plugin to sign and send CCU(Cross-Chain Update) transactions + +Run below command inside each application folder. +`./bin/run endpoint:invoke 'chainConnector_authorize' '{"password": "lisk" }'` #### Cross Chain transfers diff --git a/examples/interop/messageRecovery/initializeMessageRecovery.ts b/examples/interop/messageRecovery/initializeMessageRecovery.ts new file mode 100644 index 00000000000..998a5a4c05f --- /dev/null +++ b/examples/interop/messageRecovery/initializeMessageRecovery.ts @@ -0,0 +1,512 @@ +import { + apiClient, + chain, + cryptography, + MODULE_NAME_INTEROPERABILITY, + messageRecoveryInitializationParamsSchema, + ChainStatus, + codec, + Transaction, + ChannelDataJSON, + ChannelData, + ChainAccountJSON, + Inbox, + Outbox, + db, + ProveResponse, + OutboxRootWitness, +} from 'lisk-sdk'; +import { join } from 'path'; +import { ensureDir } from 'fs-extra'; +import * as os from 'os'; + +// LIP 45 +const STORE_PREFIX_INTEROPERABILITY = Buffer.from('83ed0d25', 'hex'); +const SUBSTORE_PREFIX_CHANNEL_DATA = Buffer.from('a000', 'hex'); + +const HASH_LENGTH = 32; +const CHAIN_ID_LENGTH = 4; +const LOCAL_ID_LENGTH = 4; +const TOKEN_ID_LENGTH = CHAIN_ID_LENGTH + LOCAL_ID_LENGTH; + +const getDBInstance = async ( + dataPath: string, + dbName = 'messageRecoveryPlugin.db', +): Promise => { + const dirPath = join(dataPath.replace('~', os.homedir()), 'plugins/data', dbName); + console.log(`dirPath: ${dirPath}`); + + await ensureDir(dirPath); + return new db.Database(dirPath); +}; + +interface Data { + readonly blockHeader: chain.BlockHeaderJSON; +} + +interface MessageRecoveryInitializationParams { + chainID: Buffer; + channel: Buffer; + bitmap: Buffer; + siblingHashes: Buffer[]; +} + +const channelDataJSONToObj = (channelData: ChannelDataJSON): ChannelData => { + const { inbox, messageFeeTokenID, outbox, partnerChainOutboxRoot, minReturnFeePerByte } = + channelData; + + const inboxJSON: Inbox = { + appendPath: inbox.appendPath.map(ap => Buffer.from(ap, 'hex')), + root: Buffer.from(inbox.root, 'hex'), + size: inbox.size, + }; + + const outboxJSON: Outbox = { + appendPath: outbox.appendPath.map(ap => Buffer.from(ap, 'hex')), + root: Buffer.from(outbox.root, 'hex'), + size: outbox.size, + }; + + return { + messageFeeTokenID: Buffer.from(messageFeeTokenID, 'hex'), + outbox: outboxJSON, + inbox: inboxJSON, + partnerChainOutboxRoot: Buffer.from(partnerChainOutboxRoot, 'hex'), + minReturnFeePerByte: BigInt(minReturnFeePerByte), + }; +}; + +const proveResponseJSONToObj = (proveResponseJSON: ProveResponseJSON): ProveResponse => { + const { + proof: { queries, siblingHashes }, + } = proveResponseJSON; + + return { + proof: { + queries: queries.map(query => ({ + bitmap: Buffer.from(query.bitmap, 'hex'), + key: Buffer.from(query.key, 'hex'), + value: Buffer.from(query.value, 'hex'), + })), + siblingHashes: siblingHashes.map(siblingHash => Buffer.from(siblingHash, 'hex')), + }, + }; +}; + +const inclusionProofsWithHeightAndStateRootSchema = { + $id: `scripts/recovery/inclusionProofs`, + type: 'object', + properties: { + inclusionProofs: { + type: 'array', + fieldNumber: 1, + items: { + type: 'object', + properties: { + height: { dataType: 'uint32', fieldNumber: 1 }, + inclusionProof: { + type: 'object', + fieldNumber: 2, + properties: { + siblingHashes: { + type: 'array', + fieldNumber: 1, + items: { + dataType: 'bytes', + }, + }, + bitmap: { + dataType: 'bytes', + fieldNumber: 2, + }, + key: { + dataType: 'bytes', + fieldNumber: 3, + }, + value: { + dataType: 'bytes', + fieldNumber: 4, + }, + }, + }, + stateRoot: { dataType: 'bytes', fieldNumber: 3 }, + }, + }, + }, + }, +}; +type ProveResponseJSON = JSONObject; + +const inboxOutboxProps = { + appendPath: { + type: 'array', + items: { + dataType: 'bytes', + minLength: HASH_LENGTH, + maxLength: HASH_LENGTH, + }, + fieldNumber: 1, + }, + size: { + dataType: 'uint32', + fieldNumber: 2, + }, + root: { + dataType: 'bytes', + minLength: HASH_LENGTH, + maxLength: HASH_LENGTH, + fieldNumber: 3, + }, +}; + +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0045.md#channel-data-substore +const channelSchema = { + $id: '/example/modules/interoperability/channel', + type: 'object', + required: [ + 'inbox', + 'outbox', + 'partnerChainOutboxRoot', + 'messageFeeTokenID', + 'minReturnFeePerByte', + ], + properties: { + inbox: { + type: 'object', + fieldNumber: 1, + required: ['appendPath', 'size', 'root'], + properties: inboxOutboxProps, + }, + outbox: { + type: 'object', + fieldNumber: 2, + required: ['appendPath', 'size', 'root'], + properties: inboxOutboxProps, + }, + partnerChainOutboxRoot: { + dataType: 'bytes', + minLength: HASH_LENGTH, + maxLength: HASH_LENGTH, + fieldNumber: 3, + }, + messageFeeTokenID: { + dataType: 'bytes', + minLength: TOKEN_ID_LENGTH, + maxLength: TOKEN_ID_LENGTH, + fieldNumber: 4, + }, + minReturnFeePerByte: { + dataType: 'uint64', + fieldNumber: 5, + }, + }, +}; + +type Primitive = string | number | bigint | boolean | null | undefined; +type Replaced = T extends TReplace | TKeep + ? T extends TReplace + ? TWith | Exclude + : T + : { + [P in keyof T]: Replaced; + }; + +type JSONObject = Replaced; + +interface InclusionProofWithHeightAndStateRoot { + height: number; + stateRoot: Buffer; + inclusionProof: OutboxRootWitness & { key: Buffer; value: Buffer }; +} + +type KVStore = db.Database; +const DB_KEY_INCLUSION_PROOF = Buffer.from([1]); + +class InclusionProofModel { + private readonly _db: KVStore; + + public constructor(db: KVStore) { + this._db = db; + } + + public async close() { + await this._db.close(); + } + + public async getAll(): Promise { + let proofs: InclusionProofWithHeightAndStateRoot[] = []; + try { + const encodedInfo = await this._db.get(DB_KEY_INCLUSION_PROOF); + proofs = codec.decode<{ inclusionProofs: InclusionProofWithHeightAndStateRoot[] }>( + inclusionProofsWithHeightAndStateRootSchema, + encodedInfo, + ).inclusionProofs; + } catch (error) { + if (!(error instanceof db.NotFoundError)) { + throw error; + } + } + return proofs; + } + + public async getByHeight( + height: number, + ): Promise { + return (await this.getAll()).find(proof => proof.height === height); + } + + /** + * This will save proofs greater than or equal to given height + * @param height Last certified height + */ + public async deleteProofsUntilHeight(height: number) { + const filteredProofs = (await this.getAll()).filter(proofs => proofs.height >= height); + + await this._db.set( + DB_KEY_INCLUSION_PROOF, + codec.encode(inclusionProofsWithHeightAndStateRootSchema, { + inclusionProofs: filteredProofs, + }), + ); + } + + public async save(inclusionProofWithHeightAndStateRoot: InclusionProofWithHeightAndStateRoot) { + const proofs = await this.getAll(); + proofs.push(inclusionProofWithHeightAndStateRoot); + + const encodedInfo = codec.encode(inclusionProofsWithHeightAndStateRootSchema, { + inclusionProofs: proofs, + }); + await this._db.set(DB_KEY_INCLUSION_PROOF, encodedInfo); + } +} + +const relayerKeyInfo = { + address: 'lsk952ztknjoa3h58es4vgu5ovnoscv3amo7zg4zz', + keyPath: "m/44'/134'/3'", + publicKey: '8960f85f7ab3cc473f29c3a00e6ad66c569f2a84125388274a4f382e11306099', + privateKey: + 'a16702175f750eab29ba286a1e30c86eb6057b2aa8547925a1139341d50ee16c8960f85f7ab3cc473f29c3a00e6ad66c569f2a84125388274a4f382e11306099', + plain: { + generatorKeyPath: "m/25519'/134'/0'/3'", + generatorKey: '60df111d5d97bf45c426d889673a8f04499ba312480b1d913fc49c5a77908b83', + generatorPrivateKey: + 'b623e9d77567fee9c7ea6502275c19849cf5ded916aa5c967835144d5f1295d560df111d5d97bf45c426d889673a8f04499ba312480b1d913fc49c5a77908b83', + blsKeyPath: 'm/12381/134/0/3', + blsKey: + '99b210271475977210d5e92c02e325f011706c5c9fc3861ecfdb8163e078ed1214e8710669e1625b30899c624305bd0e', + blsProofOfPossession: + 'a4486989094ac9225362084642072777ff0a028d89cc735908ad267be53827821093e34f960857140882e2b062f1a02e193ce9f2ad765268ed82fe462e4755dd378d8edf220d1395c9687a3c88f1fc48a5990ebb43585516e18d7228f0b8b9fd', + blsPrivateKey: '3d34f3e44a5ce6b2a3c7b79be6ab76ece0fa46749cf66c41e4d000c6ae3353b6', + }, + encrypted: {}, +}; + +/** + * Steps: + * cd examples/interop/ + * + * make sure `exports.LIVENESS_LIMIT = 2592000;` in `lisk-framework/dist-node/modules/interoperability/constants.js` + * ./start_example (script to configure & register chains) + * + * Call `chainConnector_getSentCCUs` to see if any CCU was sent (sidechain status must change to ACTIVE after first CCU) + * + * call `interoperability_getChainAccount` endpoint to verify `status`, if it still shows 0, observe logs + * `pm2 logs 2` (2 is id of pos-sidechain-example-one), + * Initially, it'll log ``No valid CCU can be generated for the height: X` (e.g. till height 20) + * + * run ***this*** script (make sure sidechain status has changed to ACTIVE (on mainchain)) + * ts-node ./messageRecovery/initializeMessageRecovery.ts // it will keep on saving inclusion proofs for sidechain + * // observe logs to see both mainchain & sidechain are receiving blocks & sidechain is saving inclusion proofs + * // Make sure `sidechainAccount.lastCertificate.height` is increasing (if not sidechain might already have been terminated) + * + * // Now stop ALL nodes + * pm2 stop all + + * terminate sidechain + * // Before terminating a sidechain, make sure, `sidechainAccount.lastCertificate.height` (on mainchain) has reached `Successfully stored inclusion proof at height x` (from sidechain) + * + * pwd + * /examples/interop/pos-mainchain-fast + * + * Change constant in `lisk-framework/dist-node/modules/interoperability/constants.js` + * // exports.LIVENESS_LIMIT = 2592000; + * => exports.LIVENESS_LIMIT = 30; // Next wait for 30 seconds + * + * pm2 start all + * // Now `this running` script (from other terminal window) should show logs again + * while `sidechainAccount.lastCertificate.height` logging SAME value (an indication sidechain has already been terminated) + * + * Run `terminateSidechainForLiveness` command in console (note: `--send` is missing here) + * cd pos-mainchain-fast + * ./bin/run transaction:create interoperability terminateSidechainForLiveness 200000000 --json --passphrase="two thunder nurse process feel fence addict size broccoli swing city speed build slide virus ridge jazz mushroom road fish border argue weapon lens" --key-derivation-path="m/44'/134'/1'" --data-path ~/.lisk/mainchain-node-one + * Please enter: chainID: 04000001 (taken from examples/interop/README.md) + * + * 3. Call `txpool_postTransaction` to `http://127.0.0.1:7881/rpc` with generated transaction + * // Here `7881` is port of mainchain-node-one + */ + +(async () => { + console.log('Starting init message recovery script...'); + + let inclusionProofModel: InclusionProofModel; + try { + inclusionProofModel = new InclusionProofModel(await getDBInstance('~/.lisk')); + console.log('DB is initialized.'); + } catch (error) { + console.log('Error occurred while initializing DB', error); + process.exit(); + } + + const mainchainClient = await apiClient.createIPCClient(`~/.lisk/mainchain-node-one`); + const sidechainClient = await apiClient.createIPCClient(`~/.lisk/pos-sidechain-example-one`); + + const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo'); + const sidechainNodeInfo = await sidechainClient.invoke('system_getNodeInfo'); + + const recoveryKey = Buffer.concat([ + STORE_PREFIX_INTEROPERABILITY, + SUBSTORE_PREFIX_CHANNEL_DATA, + cryptography.utils.hash(Buffer.from(mainchainNodeInfo.chainID as string, 'hex')), + ]); + console.log('recoveryKey: ', recoveryKey); + + // Collect inclusion proofs on sidechain and save it in recoveryDB + sidechainClient.subscribe('chain_newBlock', async (data?: Record) => { + const { blockHeader: receivedBlock } = data as unknown as Data; + const newBlockHeader = chain.BlockHeader.fromJSON(receivedBlock).toObject(); + console.log( + `Received new block on sidechain ${sidechainNodeInfo.chainID} with height ${newBlockHeader.height}`, + ); + + // Returns proof for sidechain lastBlock header stateRoot (which is state root of the last block that was forged) + const proof = proveResponseJSONToObj( + await sidechainClient.invoke('state_prove', { + queryKeys: [recoveryKey.toString('hex')], // `queryKey` is `string` + }), + ).proof; + console.log('proof: ', proof); + + // https://github.com/LiskHQ/lips/blob/main/proposals/lip-0039.md#proof-verification + // To check the proof, the Verifier calls ```verify(queryKeys, proof, merkleRoot) function``` + const smt = new db.SparseMerkleTree(); + console.log( + 'smt.verify: ', + await smt.verify(newBlockHeader.stateRoot, [proof.queries[0].key], proof), + ); + + const inclusionProof = { + key: proof.queries[0].key, + value: proof.queries[0].value, + bitmap: proof.queries[0].bitmap, + siblingHashes: proof.siblingHashes, + }; + + const inclusionProofWithHeightAndStateRoot = { + height: newBlockHeader.height, + stateRoot: newBlockHeader.stateRoot, + inclusionProof, + }; + + await inclusionProofModel.save(inclusionProofWithHeightAndStateRoot); + console.log(`Successfully stored inclusion proof at height ${newBlockHeader.height}`); + }); + + mainchainClient.subscribe('chain_newBlock', async (_data?: Record) => { + const sidechainAccount = await mainchainClient.invoke( + 'interoperability_getChainAccount', + { chainID: sidechainNodeInfo.chainID }, + ); + let lastCertifiedHeight = sidechainAccount.lastCertificate.height; + console.log(`sidechainAccount.lastCertificate.height: ${lastCertifiedHeight}`); + + if (sidechainAccount.status === ChainStatus.TERMINATED) { + // Create recovery transaction + const inclusionProofAtLastCertifiedHeight = await inclusionProofModel.getByHeight( + lastCertifiedHeight, + ); + console.log(`inclusionProofAtLastCertifiedHeight: ${inclusionProofAtLastCertifiedHeight}`); + if (!inclusionProofAtLastCertifiedHeight) { + console.log(`No inclusionProof exists at a given height: ${lastCertifiedHeight}`); + } + + if (inclusionProofAtLastCertifiedHeight) { + const smt = new db.SparseMerkleTree(); + + console.log('State Root: ', inclusionProofAtLastCertifiedHeight.stateRoot.toString('hex')); + console.log('recoveryKey: ', recoveryKey.toString('hex')); + console.log( + 'siblingHashes: ', + inclusionProofAtLastCertifiedHeight.inclusionProof.siblingHashes, + ); + console.log('queries: ', { + bitmap: inclusionProofAtLastCertifiedHeight.inclusionProof.bitmap, + key: inclusionProofAtLastCertifiedHeight.inclusionProof.key, + value: inclusionProofAtLastCertifiedHeight.inclusionProof.value, + }); + + console.log( + 'smt.verify: ', + await smt.verify(inclusionProofAtLastCertifiedHeight.stateRoot, [recoveryKey], { + siblingHashes: inclusionProofAtLastCertifiedHeight.inclusionProof.siblingHashes, + queries: [ + { + bitmap: inclusionProofAtLastCertifiedHeight.inclusionProof.bitmap, + key: inclusionProofAtLastCertifiedHeight.inclusionProof.key, + value: inclusionProofAtLastCertifiedHeight.inclusionProof.value, + }, + ], + }), + ); + + const messageRecoveryInitializationParams: MessageRecoveryInitializationParams = { + // chainID: The ID of the sidechain whose terminated outbox account is to be initialized. + chainID: Buffer.from(sidechainNodeInfo.chainID as string, 'hex'), + // channel: The channel of this chain stored on the terminated sidechain. + channel: codec.encode( + channelSchema, + channelDataJSONToObj( + await sidechainClient.invoke('interoperability_getChannel', { + chainID: mainchainNodeInfo.chainID, + }), + ), + ), + // bitmap: The bitmap of the inclusion proof of the channel in the sidechain state tree. + bitmap: inclusionProofAtLastCertifiedHeight.inclusionProof.bitmap, + siblingHashes: inclusionProofAtLastCertifiedHeight.inclusionProof.siblingHashes, + }; + + const tx = new Transaction({ + module: MODULE_NAME_INTEROPERABILITY, + command: 'initializeMessageRecovery', + fee: BigInt(5450000000), + params: codec.encodeJSON( + messageRecoveryInitializationParamsSchema, + messageRecoveryInitializationParams, + ), + nonce: BigInt( + ( + await mainchainClient.invoke<{ nonce: string }>('auth_getAuthAccount', { + address: cryptography.address.getLisk32AddressFromPublicKey( + Buffer.from(relayerKeyInfo.publicKey, 'hex'), + ), + }) + ).nonce, + ), + senderPublicKey: Buffer.from(relayerKeyInfo.publicKey, 'hex'), + signatures: [], + }); + + tx.sign( + Buffer.from(mainchainNodeInfo.chainID as string, 'hex'), + Buffer.from(relayerKeyInfo.privateKey, 'hex'), + ); + + console.log('Final transaction to be sent to tx_pool: ', tx.getBytes().toString('hex')); + } + + await inclusionProofModel.deleteProofsUntilHeight(lastCertifiedHeight); + process.exit(0); + } + }); +})(); diff --git a/examples/interop/messageRecovery/messageRecovery.ts b/examples/interop/messageRecovery/messageRecovery.ts new file mode 100644 index 00000000000..546e28193cd --- /dev/null +++ b/examples/interop/messageRecovery/messageRecovery.ts @@ -0,0 +1,232 @@ +import { + cryptography, + codec, + CCMsg, + ccmSchema, + apiClient, + Transaction, + db, + MODULE_NAME_INTEROPERABILITY, + messageRecoveryParamsSchema, +} from 'lisk-sdk'; + +// to transfer some LSK, we can use this script - examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts + +import { join } from 'path'; +import { ensureDir } from 'fs-extra'; +import { checkDBError } from '@liskhq/lisk-framework-chain-connector-plugin/dist-node/db'; +import { MerkleTree } from '@liskhq/lisk-tree'; +import { utils } from '@liskhq/lisk-cryptography'; +import * as os from 'os'; +import { ccmsInfoSchema } from './schema'; + +export const relayerKeyInfo = { + address: 'lsk952ztknjoa3h58es4vgu5ovnoscv3amo7zg4zz', + keyPath: "m/44'/134'/3'", + publicKey: '8960f85f7ab3cc473f29c3a00e6ad66c569f2a84125388274a4f382e11306099', + privateKey: + 'a16702175f750eab29ba286a1e30c86eb6057b2aa8547925a1139341d50ee16c8960f85f7ab3cc473f29c3a00e6ad66c569f2a84125388274a4f382e11306099', + plain: { + generatorKeyPath: "m/25519'/134'/0'/3'", + generatorKey: '60df111d5d97bf45c426d889673a8f04499ba312480b1d913fc49c5a77908b83', + generatorPrivateKey: + 'b623e9d77567fee9c7ea6502275c19849cf5ded916aa5c967835144d5f1295d560df111d5d97bf45c426d889673a8f04499ba312480b1d913fc49c5a77908b83', + blsKeyPath: 'm/12381/134/0/3', + blsKey: + '99b210271475977210d5e92c02e325f011706c5c9fc3861ecfdb8163e078ed1214e8710669e1625b30899c624305bd0e', + blsProofOfPossession: + 'a4486989094ac9225362084642072777ff0a028d89cc735908ad267be53827821093e34f960857140882e2b062f1a02e193ce9f2ad765268ed82fe462e4755dd378d8edf220d1395c9687a3c88f1fc48a5990ebb43585516e18d7228f0b8b9fd', + blsPrivateKey: '3d34f3e44a5ce6b2a3c7b79be6ab76ece0fa46749cf66c41e4d000c6ae3353b6', + }, + encrypted: {}, +}; + +interface CCMsInfo { + ccms: CCMsg[]; +} + +export interface Proof { + readonly siblingHashes: ReadonlyArray; + readonly idxs: ReadonlyArray; + readonly size: number; +} + +/** + * Sequence of steps. Also, some steps are mentioned in `initializeMessageRecovery.ts` + * + * pm2 stop all + * rm -rf ~/.lisk + * ./start_nodes + * ts-node ./messageRecovery/events/parse_events.ts (start parsing events) + * + * -------------------- + * + * Make sure ```exports.LIVENESS_LIMIT = 2592000;``` in ```lisk-framework/dist-node/modules/interoperability/constants.js``` + * ts-node pos-mainchain-fast/config/scripts/sidechain_registration.ts (Register sidechain (keep chain connector ON)) + * ts-node pos-sidechain-example-one/config/scripts/mainchain_registration.ts + * + * + * Wait till nodes status change to ACTIVE (as initially they are in REGISTERED status)(check `interoperability_getChainAccount` endpoint) + * + * Start saving inclusion proofs + * - ts-node ./messageRecovery/initializeMessageRecovery.ts (in new console tab/window) + * + * Change constant in ```exports.LIVENESS_LIMIT = 30;``` in ```/lisk-sdk/examples/interop/pos-mainchain-fast/node_modules/lisk-framework/dist-node/modules/interoperability/constants.js``` + * Wait for at least 30 sec + * + * ------------------ + * + * - Turn OFF chain connector plugin on mainchain + * - Make crossChainTransfer on mainchain to sidechain + * - Make the sidechain terminate on mainchain (because of Liveness) + * - Submit Liveness termination transaction on mainchain + * + * Now you are ready to recover. + * By this time you should have below CCMs with idx in sidechain(outbox) on mainchain, + * + * 0. registrationCCM + * 1. crossChainTransferCCM + * 2. terminationCCM + * + * You can now try to recover 1. crossChainTransferCCM (where the balance should return to the sender) + */ + +(async () => { + const mainchainClient = await apiClient.createIPCClient(`~/.lisk/mainchain-node-one`); + const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo'); + + const sidechainClient = await apiClient.createIPCClient(`~/.lisk/pos-sidechain-example-one`); + const sidechainNodeInfo = await sidechainClient.invoke('system_getNodeInfo'); + + // https://github.com/LiskHQ/lips/blob/main/proposals/lip-0054.md#message-recovery-from-the-sidechain-channel-outbox + // TODO: The proof of inclusion for the pending CCMs into the outboxRoot property of the terminated outbox account has to be available. + + /** + * // LIP 54 + * ``` + * Notice that the message recovery mechanism requires that the channel outbox is stored in the chain where the commands are processed. + * In the SDK 6, sidechain channels can only be stored on the mainchain. This means that the message recovery mechanism + * would only work on the mainchain. + */ + + // This mechanism allows to recover any CCM pending in the sidechain channel outbox. + // sidechain channel is stored on mainchain (during sidechain registration process - LIP 43) + + // All cross-chain messages must have the correct format, which is checked by the following logic: + // https://github.com/LiskHQ/lips/blob/main/proposals/lip-0049.md#validateformat + + // ``` The pending CCMs to be recovered have to be available to the sender of the recovery command. ``` + // Before preparing this array, it's worth to check Verification section of `Message Recovery Command` + // https://github.com/LiskHQ/lips/blob/main/proposals/lip-0054.md#verification-1 + + type KVStore = db.Database; + const DB_KEY_EVENTS = Buffer.from([1]); + + class EventsModel { + private readonly _db: KVStore; + + public constructor(db: KVStore) { + this._db = db; + } + + public async close() { + await this._db.close(); + } + + public async getCCMs(): Promise { + let ccms: CCMsg[] = []; + try { + const encodedInfo = await this._db.get(DB_KEY_EVENTS); + ccms = codec.decode(ccmsInfoSchema, encodedInfo).ccms; + } catch (error) { + checkDBError(error); + } + return ccms; + } + } + + const getDBInstance = async (dataPath: string, dbName = 'events.db'): Promise => { + const dirPath = join(dataPath.replace('~', os.homedir()), 'plugins/data', dbName); + console.log(`dirPath: ${dirPath}`); + + await ensureDir(dirPath); + return new db.Database(dirPath); + }; + + const toBytes = (ccm: CCMsg) => codec.encode(ccmSchema, ccm); + + const LEAF_PREFIX = Buffer.from('00', 'hex'); + const eventsModel = new EventsModel(await getDBInstance('~/.lisk')); + const merkleTree = new MerkleTree(); + + const ccms = await eventsModel.getCCMs(); + console.log(ccms); + + const transferCrossChainCCM = ccms.filter( + ccm => ccm.crossChainCommand === 'transferCrossChain', + )[0]; + console.log('Pending token transfer CCM to recover: ', transferCrossChainCCM); + + await merkleTree.init(ccms.map(ccm => toBytes(ccm))); + console.log('merkleTree.root: ', merkleTree.root); + + const queryHash = utils.hash( + Buffer.concat( + [LEAF_PREFIX, toBytes(transferCrossChainCCM)], + LEAF_PREFIX.length + toBytes(transferCrossChainCCM).length, + ), + ); + + const queryHashes = [queryHash]; + console.log('queryHashes: ', queryHashes); + + const proof = await merkleTree.generateProof(queryHashes); + console.log('merkleTree: ', merkleTree); + console.log('merkleTree.generateProof: ', proof); + + interface MessageRecoveryParams { + chainID: Buffer; + crossChainMessages: Buffer[]; + idxs: number[]; + siblingHashes: Buffer[]; + } + + const messageRecoveryParams: MessageRecoveryParams = { + chainID: sidechainNodeInfo.chainID as Buffer, + crossChainMessages: [toBytes(transferCrossChainCCM)], + idxs: proof.idxs as number[], + siblingHashes: proof.siblingHashes as Buffer[], + }; + + // PRE-REQUISITE: examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts + // Final transaction to be submitted + + // In case of recovery, it will simply swap sending/receiving chains & run each CCM in input crossChainMessages[] again + // LIP 54: ```def applyRecovery(trs: Transaction, ccm: CCM) -> None:``` + const tx = new Transaction({ + module: MODULE_NAME_INTEROPERABILITY, + // COMMAND_RECOVER_MESSAGE string "recoverMessage" Name of message recovery command. (LIP 45) + command: 'recoverMessage', + fee: BigInt(5450000000), + params: codec.encodeJSON(messageRecoveryParamsSchema, messageRecoveryParams), + nonce: BigInt( + ( + await mainchainClient.invoke<{ nonce: string }>('auth_getAuthAccount', { + address: cryptography.address.getLisk32AddressFromPublicKey( + Buffer.from(relayerKeyInfo.publicKey, 'hex'), + ), + }) + ).nonce, + ), + senderPublicKey: Buffer.from(relayerKeyInfo.publicKey, 'hex'), + signatures: [], + }); + + tx.sign( + Buffer.from(mainchainNodeInfo.chainID as string, 'hex'), + Buffer.from(relayerKeyInfo.privateKey, 'hex'), + ); + + console.log('Final transaction to be posted to tx_pool: ', tx.getBytes().toString('hex')); + process.exit(0); +})(); diff --git a/examples/interop/messageRecovery/parse_events.ts b/examples/interop/messageRecovery/parse_events.ts new file mode 100644 index 00000000000..ae355b35a6d --- /dev/null +++ b/examples/interop/messageRecovery/parse_events.ts @@ -0,0 +1,197 @@ +// The complete Merkle tree with root equal to the last value of the outboxRoot property of the terminated outbox account +// can be computed from the history of the Lisk mainchain + +import { + chain, + CCMsg, + JSONObject, + MODULE_NAME_INTEROPERABILITY, + Schema, + apiClient, + db, + db as liskDB, +} from 'lisk-sdk'; +import { codec } from '@liskhq/lisk-codec'; +import { CcmSendSuccessEventData, CcmProcessedEventData } from 'lisk-framework'; +import { EVENT_NAME_CCM_PROCESSED } from 'lisk-framework/dist-node/modules/interoperability/constants'; +import { join } from 'path'; +import * as os from 'os'; +import { ensureDir } from 'fs-extra'; +import { ccmsInfoSchema } from './schema'; + +export const checkDBError = (error: Error | unknown) => { + if (!(error instanceof liskDB.NotFoundError)) { + throw error; + } +}; + +type ModuleMetadata = { + stores: { key: string; data: Schema }[]; + events: { name: string; data: Schema }[]; + name: string; +}; + +type ModulesMetadata = [ModuleMetadata]; + +interface Data { + readonly blockHeader: chain.BlockHeaderJSON; +} + +const getInteropAndTokenModulesMetadata = async (mainchainClient: apiClient.APIClient) => { + const { modules: modulesMetadata } = await mainchainClient.invoke<{ modules: ModulesMetadata }>( + 'system_getMetadata', + ); + const interoperabilityMetadata = modulesMetadata.find( + metadata => metadata.name === MODULE_NAME_INTEROPERABILITY, + ); + if (!interoperabilityMetadata) { + throw new Error(`No metadata found for ${MODULE_NAME_INTEROPERABILITY} module.`); + } + + const tokenMetadata = modulesMetadata.find(metadata => metadata.name === 'token'); + if (!tokenMetadata) { + throw new Error(`No metadata found for token module.`); + } + + return [interoperabilityMetadata, tokenMetadata]; +}; + +type KVStore = db.Database; +const DB_KEY_EVENTS = Buffer.from([1]); + +const getDBInstance = async (dataPath: string, dbName = 'events.db'): Promise => { + const dirPath = join(dataPath.replace('~', os.homedir()), 'plugins/data', dbName); + console.log(`dirPath: ${dirPath}`); + + await ensureDir(dirPath); + return new db.Database(dirPath); +}; + +interface CCMsInfo { + ccms: CCMsg[]; +} + +class EventsModel { + private readonly _db: KVStore; + + public constructor(db: KVStore) { + this._db = db; + } + + public async close() { + await this._db.close(); + } + + public async getCCMs(): Promise { + let ccms: CCMsg[] = []; + try { + const encodedInfo = await this._db.get(DB_KEY_EVENTS); + ccms = codec.decode(ccmsInfoSchema, encodedInfo).ccms; + } catch (error) { + checkDBError(error); + } + return ccms; + } + + public async setCCMs(ccms: CCMsg[]) { + const encodedInfo = codec.encode(ccmsInfoSchema, { ccms }); + await this._db.set(DB_KEY_EVENTS, encodedInfo); + } +} + +// It should be run after all nodes have started +// Then we need to run `ts-node pos-mainchain-fast/config/scripts/sidechain_registration.ts` (note the change: const SIDECHAIN_ARRAY = ['one']) +// & then ts-node pos-sidechain-example-one/config/scripts/mainchain_registration.ts (```one```) +(async () => { + const mainchainClient = await apiClient.createIPCClient(`~/.lisk/mainchain-node-one`); + const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo'); + + const eventsDb = await getDBInstance('~/.lisk'); + const eventsModel = new EventsModel(eventsDb); + + mainchainClient.subscribe('chain_newBlock', async (data?: Record) => { + const { blockHeader: receivedBlock } = data as unknown as Data; + const newBlockHeader = chain.BlockHeader.fromJSON(receivedBlock).toObject(); + console.log('\n'); + console.log( + `Received new block ${newBlockHeader.height} on mainchain ${mainchainNodeInfo.chainID}`, + ); + + const allCCMs = await eventsModel.getCCMs(); + console.log('allCCMs => ', allCCMs); + + // Check for events if any and store them + const blockEvents = await mainchainClient.invoke>( + 'chain_getEvents', + { height: newBlockHeader.height }, + ); + + const ccmsFromEvents: CCMsg[] = []; + const interopMetadata = (await getInteropAndTokenModulesMetadata(mainchainClient))[0]; + + const getEventsByName = (name: string) => { + return blockEvents.filter( + eventAttr => eventAttr.module === MODULE_NAME_INTEROPERABILITY && eventAttr.name === name, + ); + }; + + const getEventData = (name: string): Schema => { + const eventInfo = interopMetadata.events.filter(event => event.name === name); + if (!eventInfo?.[0]?.data) { + throw new Error(`No schema found for ${name} event data.`); + } + return eventInfo?.[0]?.data; + }; + + const parseCcmSendSuccessEvents = () => { + const eventsByName = getEventsByName('ccmSendSuccess'); + if (eventsByName) { + const data = getEventData('ccmSendSuccess'); + for (const ccmSentSuccessEvent of eventsByName) { + const ccmSendSuccessEventData = codec.decode( + data, + Buffer.from(ccmSentSuccessEvent.data, 'hex'), + ); + console.log('ccmSendSuccessEventData => ', ccmSendSuccessEventData); + + // Do we need to filter based on `ccm.sendingChainID = mainchain ? + const ccm = ccmSendSuccessEventData.ccm; + if (ccm.sendingChainID.equals(Buffer.from('04000000', 'hex'))) { + ccmsFromEvents.push(ccm); + console.log('ccmsFromEvents.length:::::::::::::::: ', ccmsFromEvents.length); + } + } + } + }; + + const parseCcmProcessedEvents = () => { + const eventsByName = getEventsByName(EVENT_NAME_CCM_PROCESSED); + if (eventsByName) { + const data = getEventData(EVENT_NAME_CCM_PROCESSED); + for (const ccmProcessedEvent of eventsByName) { + const ccmProcessedEventData = codec.decode( + data, + Buffer.from(ccmProcessedEvent.data, 'hex'), + ); + console.log('ccmProcessedEventData => ', ccmProcessedEventData); + + // Do we need to filter based on `ccm.sendingChainID = mainchain ? + const ccm = ccmProcessedEventData.ccm; + if (ccm.sendingChainID.equals(Buffer.from('04000000', 'hex'))) { + ccmsFromEvents.push(ccm); + } + } + } + }; + + parseCcmSendSuccessEvents(); + parseCcmProcessedEvents(); + + for (const ccmFromEvent of ccmsFromEvents) { + allCCMs.push(ccmFromEvent); + } + console.log('allCCMs.length(AFTER push): ', allCCMs.length); + + await eventsModel.setCCMs(allCCMs); + }); +})(); diff --git a/examples/interop/messageRecovery/schema.ts b/examples/interop/messageRecovery/schema.ts new file mode 100644 index 00000000000..d28a54f86c9 --- /dev/null +++ b/examples/interop/messageRecovery/schema.ts @@ -0,0 +1,15 @@ +import { ccmSchema } from 'lisk-sdk'; + +export const ccmsInfoSchema = { + $id: 'msgRecoveryPlugin/ccmsFromEvents', + type: 'object', + properties: { + ccms: { + type: 'array', + fieldNumber: 1, + items: { + ...ccmSchema, + }, + }, + }, +}; diff --git a/examples/interop/messageRecovery/start_nodes.sh b/examples/interop/messageRecovery/start_nodes.sh new file mode 100644 index 00000000000..9bca9d122ff --- /dev/null +++ b/examples/interop/messageRecovery/start_nodes.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +if [ $1 == "--reset" ]; then + echo "*** Clearing Everything ..." + pm2 kill + # Storing Lisk Directory + LISK_PATH=$(pwd) + + cd ~/.lisk && rm -rf mainchain-node-* && rm -rf pos-sidechain-example-* + + # Going back to Lisk Directory + cd $LISK_PATH +fi; + +cd ../.. +echo "*** Building lisk-sdk ..." +{ + yarn cache clean + yarn && yarn build +} || { + echo "***** Error building lisk-sdk *****" + exit +} + +echo "*** Building pos-mainchain-fast ..." +cd examples/interop/pos-mainchain-fast +{ + yarn cache clean + yarn --registry https://npm.lisk.com && yarn build +} || { + echo "***** Error building pos-mainchain-fast *****" + exit +} +cd .. + +echo "*** Building pos-sidechain-example-one ..." +cd pos-sidechain-example-one +{ + yarn cache clean + yarn --registry https://npm.lisk.com && yarn build +} || { + echo "***** Error building pos-sidechain-example-one *****" + exit + } +cd .. + +echo "*** Building pos-sidechain-example-two ..." +cd pos-sidechain-example-two +{ + yarn cache clean + yarn --registry https://npm.lisk.com && yarn build +} || { + echo "***** Error building pos-sidechain-example-two *****" + exit +} +cd .. + +cd pos-mainchain-fast +pm2 start config/mainchain_node_one.sh +pm2 start config/mainchain_node_two.sh +cd .. +pm2 start run_sidechains.json + +echo "All nodes started ..." diff --git a/examples/interop/pos-mainchain-fast/.eslintignore b/examples/interop/pos-mainchain-fast/.eslintignore index 06500399046..f65e71dc66b 100644 --- a/examples/interop/pos-mainchain-fast/.eslintignore +++ b/examples/interop/pos-mainchain-fast/.eslintignore @@ -11,3 +11,4 @@ build scripts config test/_setup.js +src/app/app.ts diff --git a/examples/interop/pos-mainchain-fast/config/default/genesis_assets.json b/examples/interop/pos-mainchain-fast/config/default/genesis_assets.json index 54ed68a6a19..cbadf15b996 100644 --- a/examples/interop/pos-mainchain-fast/config/default/genesis_assets.json +++ b/examples/interop/pos-mainchain-fast/config/default/genesis_assets.json @@ -1116,6 +1116,95 @@ } } }, + { + "module": "nft", + "data": { + "nftSubstore": [], + "supportedNFTsSubstore": [ + { + "chainID": "", + "supportedCollectionIDArray": [] + } + ] + }, + "schema": { + "$id": "/nft/module/genesis", + "type": "object", + "required": ["nftSubstore", "supportedNFTsSubstore"], + "properties": { + "nftSubstore": { + "type": "array", + "fieldNumber": 1, + "items": { + "type": "object", + "required": ["nftID", "owner", "attributesArray"], + "properties": { + "nftID": { + "dataType": "bytes", + "fieldNumber": 1, + "minLength": 16, + "maxLength": 16 + }, + "owner": { + "dataType": "bytes", + "fieldNumber": 2 + }, + "attributesArray": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["module", "attributes"], + "properties": { + "module": { + "dataType": "string", + "minLength": 1, + "maxLength": 32, + "fieldNumber": 1 + }, + "attributes": { + "dataType": "bytes", + "fieldNumber": 2 + } + } + } + } + } + } + }, + "supportedNFTsSubstore": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["chainID", "supportedCollectionIDArray"], + "properties": { + "chainID": { + "dataType": "bytes", + "fieldNumber": 1 + }, + "supportedCollectionIDArray": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["collectionID"], + "properties": { + "collectionID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + } + } + } + } + } + } + } + } + } + }, { "module": "pos", "data": { diff --git a/examples/interop/pos-mainchain-fast/config/default/genesis_block.blob b/examples/interop/pos-mainchain-fast/config/default/genesis_block.blob index c4b5dfdc624..4291af9b6a5 100644 Binary files a/examples/interop/pos-mainchain-fast/config/default/genesis_block.blob and b/examples/interop/pos-mainchain-fast/config/default/genesis_block.blob differ diff --git a/examples/interop/pos-mainchain-fast/config/scripts/mint_nft_mainchain_one.ts b/examples/interop/pos-mainchain-fast/config/scripts/mint_nft_mainchain_one.ts new file mode 100644 index 00000000000..f4cb6fe4171 --- /dev/null +++ b/examples/interop/pos-mainchain-fast/config/scripts/mint_nft_mainchain_one.ts @@ -0,0 +1,57 @@ +import { apiClient, codec, cryptography, Transaction } from 'lisk-sdk'; +import { keys } from '../default/dev-validators.json'; +import { LENGTH_COLLECTION_ID } from '../../../../pos-mainchain/src/app/modules/testNft/constants'; +import { mintNftParamsSchema } from '../../../../pos-mainchain/src/app/modules/testNft/schema'; +(async () => { + const { address } = cryptography; + + const nodeAlias = 'one'; + + const mainchainClient = await apiClient.createIPCClient(`~/.lisk/mainchain-node-one`); + + const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo'); + + const relayerkeyInfo = keys[2]; + const mintNftParams = { + address: address.getAddressFromLisk32Address(relayerkeyInfo.address), + collectionID: Buffer.alloc(LENGTH_COLLECTION_ID, 1), + attributesArray: [ + { + module: 'token', + attributes: Buffer.alloc(8, 2), + }, + ], + }; + + const { nonce } = await mainchainClient.invoke<{ nonce: string }>('auth_getAuthAccount', { + address: address.getLisk32AddressFromPublicKey(Buffer.from(relayerkeyInfo.publicKey, 'hex')), + }); + + const tx = new Transaction({ + module: 'testNft', + command: 'mintNft', + fee: BigInt(200000000), + params: codec.encode(mintNftParamsSchema, mintNftParams), + nonce: BigInt(nonce), + senderPublicKey: Buffer.from(relayerkeyInfo.publicKey, 'hex'), + signatures: [], + }); + + tx.sign( + Buffer.from(mainchainNodeInfo.chainID as string, 'hex'), + Buffer.from(relayerkeyInfo.privateKey, 'hex'), + ); + + const result = await mainchainClient.invoke<{ + transactionId: string; + }>('txpool_postTransaction', { + transaction: tx.getBytes().toString('hex'), + }); + + console.log( + `Sent mint nft transaction to address: ${relayerkeyInfo} to node ${nodeAlias}. Result from transaction pool is: `, + result, + ); + + process.exit(0); +})(); diff --git a/examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts b/examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts index 65ba8d01be1..4f0cafa3146 100644 --- a/examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts +++ b/examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts @@ -79,7 +79,11 @@ type ModulesMetadata = [ }); console.log( - `Sent cross chain transfer transaction (amount: ${params.amount}, recipientAddress: ${recipientLSKAddress}) to send of sidechain (sendingChainID: ${params.receivingChainID}) node ${nodeAlias}. Result from transaction pool is: `, + `Sent cross chain transfer transaction (amount: ${ + params.amount + }, recipientAddress: ${recipientLSKAddress}) to sidechain (receivingChainID: ${params.receivingChainID.toString( + 'hex', + )}) node ${nodeAlias}. Result from transaction pool is: `, result, ); diff --git a/examples/interop/pos-mainchain-fast/config/scripts/transfer_nft_sidechain_one.ts b/examples/interop/pos-mainchain-fast/config/scripts/transfer_nft_sidechain_one.ts new file mode 100644 index 00000000000..2097b6bb774 --- /dev/null +++ b/examples/interop/pos-mainchain-fast/config/scripts/transfer_nft_sidechain_one.ts @@ -0,0 +1,81 @@ +import { apiClient, codec, cryptography, Schema, Transaction } from 'lisk-sdk'; +import { keys } from '../default/dev-validators.json'; +type ModulesMetadata = [ + { + stores: { key: string; data: Schema }[]; + events: { name: string; data: Schema }[]; + name: string; + commands: { name: string; params: Schema }[]; + }, +]; +(async () => { + const { address } = cryptography; + + const nodeAlias = 'one'; + const tokenID = Buffer.from('0400000000000000', 'hex'); + const nftID = Buffer.from('04000000010101010000000000000000', 'hex'); + const sidechainID = Buffer.from('04000001', 'hex'); // Update this to send to another sidechain + const recipientAddress = address.getAddressFromLisk32Address( + 'lskxz85sur2yo22dmcxybe39uvh2fg7s2ezxq4ny9', + ); + + const mainchainClient = await apiClient.createIPCClient(`~/.lisk/mainchain-node-one`); + + const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo'); + + const { modules: modulesMetadata } = await mainchainClient.invoke<{ + modules: ModulesMetadata; + }>('system_getMetadata'); + const tokenMetadata = modulesMetadata.find(m => m.name === 'nft'); + + const ccTransferCMDSchema = tokenMetadata?.commands.filter( + cmd => cmd.name == 'transferCrossChain', + )[0].params as Schema; + + const params = { + nftID, + receivingChainID: sidechainID, + recipientAddress, + data: 'cc nft transfer testing', + messageFee: BigInt('10000000'), + messageFeeTokenID: tokenID, + includeAttributes: true, + }; + + const relayerkeyInfo = keys[2]; + const { nonce } = await mainchainClient.invoke<{ nonce: string }>('auth_getAuthAccount', { + address: address.getLisk32AddressFromPublicKey(Buffer.from(relayerkeyInfo.publicKey, 'hex')), + }); + + const tx = new Transaction({ + module: 'nft', + command: 'transferCrossChain', + fee: BigInt(200000000), + params: codec.encode(ccTransferCMDSchema, params), + nonce: BigInt(nonce), + senderPublicKey: Buffer.from(relayerkeyInfo.publicKey, 'hex'), + signatures: [], + }); + + tx.sign( + Buffer.from(mainchainNodeInfo.chainID as string, 'hex'), + Buffer.from(relayerkeyInfo.privateKey, 'hex'), + ); + + const result = await mainchainClient.invoke<{ + transactionId: string; + }>('txpool_postTransaction', { + transaction: tx.getBytes().toString('hex'), + }); + + console.log( + `Sent cross chain nft transfer transaction recipientAddress: ${params.recipientAddress.toString( + 'hex', + )}) to send of sidechain (sendingChainID: ${ + params.receivingChainID + }) node ${nodeAlias}. Result from transaction pool is: `, + result, + ); + + process.exit(0); +})(); diff --git a/examples/interop/pos-mainchain-fast/package.json b/examples/interop/pos-mainchain-fast/package.json index c1201940363..abb717f6fd3 100644 --- a/examples/interop/pos-mainchain-fast/package.json +++ b/examples/interop/pos-mainchain-fast/package.json @@ -108,12 +108,12 @@ } }, "dependencies": { - "@liskhq/lisk-framework-dashboard-plugin": "^0.3.0-rc.0", - "@liskhq/lisk-framework-faucet-plugin": "^0.3.0-rc.0", - "@liskhq/lisk-framework-monitor-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-forger-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-chain-connector-plugin": "^0.1.0-rc.0", + "@liskhq/lisk-framework-chain-connector-plugin": "^0.2.0-rc.0", + "@liskhq/lisk-framework-dashboard-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-faucet-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-forger-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-monitor-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.5.0-rc.0", "@oclif/core": "1.20.4", "@oclif/plugin-autocomplete": "1.3.6", "@oclif/plugin-help": "5.1.19", @@ -121,8 +121,8 @@ "axios": "1.2.0", "fs-extra": "11.1.0", "inquirer": "8.2.5", - "lisk-commander": "^6.0.0-rc.0", - "lisk-sdk": "^6.0.0-rc.0", + "lisk-commander": "^6.1.0-rc.0", + "lisk-sdk": "^6.1.0-rc.0", "tar": "6.1.12", "tslib": "2.4.1" }, diff --git a/examples/interop/pos-mainchain-fast/src/app/app.ts b/examples/interop/pos-mainchain-fast/src/app/app.ts index c506c018be8..3250d66e460 100644 --- a/examples/interop/pos-mainchain-fast/src/app/app.ts +++ b/examples/interop/pos-mainchain-fast/src/app/app.ts @@ -1,9 +1,22 @@ -import { Application, PartialApplicationConfig } from 'lisk-sdk'; +import { Application, PartialApplicationConfig, NFTModule } from 'lisk-sdk'; +import { TestNftModule } from './modules/testNft/module'; import { registerModules } from './modules'; import { registerPlugins } from './plugins'; export const getApplication = (config: PartialApplicationConfig): Application => { - const { app } = Application.defaultApplication(config, true); + const { app, method } = Application.defaultApplication(config, true); + + const nftModule = new NFTModule(); + const testNftModule = new TestNftModule(); + const interoperabilityModule = app['_registeredModules'].find( + mod => mod.name === 'interoperability', + ); + interoperabilityModule.registerInteroperableModule(nftModule); + nftModule.addDependencies(method.interoperability, method.fee, method.token); + testNftModule.addDependencies(nftModule.method); + + app.registerModule(nftModule); + app.registerModule(testNftModule); registerModules(app); registerPlugins(app); diff --git a/examples/interop/pos-mainchain-fast/src/app/modules.ts b/examples/interop/pos-mainchain-fast/src/app/modules.ts index acdfa4fb8f5..d69352da8ae 100644 --- a/examples/interop/pos-mainchain-fast/src/app/modules.ts +++ b/examples/interop/pos-mainchain-fast/src/app/modules.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { Application } from 'lisk-sdk'; -// @ts-expect-error app will have typescript error for unsued variable -export const registerModules = (app: Application): void => {}; +export const registerModules = (_app: Application): void => {}; diff --git a/examples/interop/pos-mainchain-fast/src/app/modules/testNft/commands/destroy_nft.ts b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/commands/destroy_nft.ts new file mode 100644 index 00000000000..822ad1b174f --- /dev/null +++ b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/commands/destroy_nft.ts @@ -0,0 +1,36 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCommand, CommandExecuteContext, NFTMethod } from 'lisk-sdk'; +import { destroyNftParamsSchema } from '../schema'; + +interface Params { + address: Buffer; + nftID: Buffer; +} + +export class DestroyNftCommand extends BaseCommand { + private _nftMethod!: NFTMethod; + public schema = destroyNftParamsSchema; + + public init(args: { nftMethod: NFTMethod }): void { + this._nftMethod = args.nftMethod; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + await this._nftMethod.destroy(context.getMethodContext(), params.address, params.nftID); + } +} diff --git a/examples/interop/pos-mainchain-fast/src/app/modules/testNft/commands/mint_nft.ts b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/commands/mint_nft.ts new file mode 100644 index 00000000000..bc5638846d4 --- /dev/null +++ b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/commands/mint_nft.ts @@ -0,0 +1,43 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCommand, CommandExecuteContext, NFTMethod } from 'lisk-sdk'; +import { NFTAttributes } from '../types'; +import { mintNftParamsSchema } from '../schema'; + +interface Params { + address: Buffer; + collectionID: Buffer; + attributesArray: NFTAttributes[]; +} + +export class MintNftCommand extends BaseCommand { + private _nftMethod!: NFTMethod; + public schema = mintNftParamsSchema; + + public init(args: { nftMethod: NFTMethod }): void { + this._nftMethod = args.nftMethod; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + await this._nftMethod.create( + context.getMethodContext(), + params.address, + params.collectionID, + params.attributesArray, + ); + } +} diff --git a/examples/interop/pos-mainchain-fast/src/app/modules/testNft/constants.ts b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/constants.ts new file mode 100644 index 00000000000..a0150fad36f --- /dev/null +++ b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/constants.ts @@ -0,0 +1,18 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export const LENGTH_COLLECTION_ID = 4; +export const MIN_LENGTH_MODULE_NAME = 1; +export const MAX_LENGTH_MODULE_NAME = 32; +export const LENGTH_NFT_ID = 16; diff --git a/examples/interop/pos-mainchain-fast/src/app/modules/testNft/endpoint.ts b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/endpoint.ts new file mode 100644 index 00000000000..1d091013741 --- /dev/null +++ b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/endpoint.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEndpoint } from 'lisk-sdk'; + +export class TestNftEndpoint extends BaseEndpoint {} diff --git a/examples/interop/pos-mainchain-fast/src/app/modules/testNft/method.ts b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/method.ts new file mode 100644 index 00000000000..5bab789e7f1 --- /dev/null +++ b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/method.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseMethod } from 'lisk-sdk'; + +export class TestNftMethod extends BaseMethod {} diff --git a/examples/interop/pos-mainchain-fast/src/app/modules/testNft/module.ts b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/module.ts new file mode 100644 index 00000000000..a228abff3af --- /dev/null +++ b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/module.ts @@ -0,0 +1,56 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseModule, ModuleInitArgs, ModuleMetadata, NFTMethod } from 'lisk-sdk'; +import { TestNftEndpoint } from './endpoint'; +import { TestNftMethod } from './method'; +import { MintNftCommand } from './commands/mint_nft'; +import { DestroyNftCommand } from './commands/destroy_nft'; + +export class TestNftModule extends BaseModule { + public endpoint = new TestNftEndpoint(this.stores, this.offchainStores); + public method = new TestNftMethod(this.stores, this.events); + public mintNftCommand = new MintNftCommand(this.stores, this.events); + public destroyNftCommand = new DestroyNftCommand(this.stores, this.events); + public commands = [this.mintNftCommand, this.destroyNftCommand]; + + private _nftMethod!: NFTMethod; + + public addDependencies(nftMethod: NFTMethod) { + this._nftMethod = nftMethod; + } + + public metadata(): ModuleMetadata { + return { + ...this.baseMetadata(), + endpoints: [], + commands: this.commands.map(command => ({ + name: command.name, + params: command.schema, + })), + events: [], + assets: [], + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async init(_args: ModuleInitArgs) { + this.mintNftCommand.init({ + nftMethod: this._nftMethod, + }); + this.destroyNftCommand.init({ + nftMethod: this._nftMethod, + }); + } +} diff --git a/examples/interop/pos-mainchain-fast/src/app/modules/testNft/schema.ts b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/schema.ts new file mode 100644 index 00000000000..c183e9fb8ff --- /dev/null +++ b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/schema.ts @@ -0,0 +1,79 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { + LENGTH_COLLECTION_ID, + LENGTH_NFT_ID, + MAX_LENGTH_MODULE_NAME, + MIN_LENGTH_MODULE_NAME, +} from './constants'; + +export const mintNftParamsSchema = { + $id: '/lisk/nftTransferParams', + type: 'object', + required: ['address', 'collectionID', 'attributesArray'], + properties: { + address: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + collectionID: { + dataType: 'bytes', + minLength: LENGTH_COLLECTION_ID, + maxLength: LENGTH_COLLECTION_ID, + fieldNumber: 2, + }, + attributesArray: { + type: 'array', + fieldNumber: 4, + items: { + type: 'object', + required: ['module', 'attributes'], + properties: { + module: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + pattern: '^[a-zA-Z0-9]*$', + fieldNumber: 1, + }, + attributes: { + dataType: 'bytes', + fieldNumber: 2, + }, + }, + }, + }, + }, +}; + +export const destroyNftParamsSchema = { + $id: '/lisk/nftDestroyParams', + type: 'object', + required: ['address', 'nftID'], + properties: { + address: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 2, + }, + }, +}; diff --git a/examples/interop/pos-mainchain-fast/src/app/modules/testNft/types.ts b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/types.ts new file mode 100644 index 00000000000..8d1af2d969a --- /dev/null +++ b/examples/interop/pos-mainchain-fast/src/app/modules/testNft/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export interface NFTAttributes { + module: string; + attributes: Buffer; +} diff --git a/examples/interop/pos-sidechain-example-one/.eslintignore b/examples/interop/pos-sidechain-example-one/.eslintignore index 06500399046..f65e71dc66b 100644 --- a/examples/interop/pos-sidechain-example-one/.eslintignore +++ b/examples/interop/pos-sidechain-example-one/.eslintignore @@ -11,3 +11,4 @@ build scripts config test/_setup.js +src/app/app.ts diff --git a/examples/interop/pos-sidechain-example-one/config/default/genesis_assets.json b/examples/interop/pos-sidechain-example-one/config/default/genesis_assets.json index 32a11c39edf..2319c6390c0 100644 --- a/examples/interop/pos-sidechain-example-one/config/default/genesis_assets.json +++ b/examples/interop/pos-sidechain-example-one/config/default/genesis_assets.json @@ -1048,6 +1048,95 @@ } } }, + { + "module": "nft", + "data": { + "nftSubstore": [], + "supportedNFTsSubstore": [ + { + "chainID": "", + "supportedCollectionIDArray": [] + } + ] + }, + "schema": { + "$id": "/nft/module/genesis", + "type": "object", + "required": ["nftSubstore", "supportedNFTsSubstore"], + "properties": { + "nftSubstore": { + "type": "array", + "fieldNumber": 1, + "items": { + "type": "object", + "required": ["nftID", "owner", "attributesArray"], + "properties": { + "nftID": { + "dataType": "bytes", + "fieldNumber": 1, + "minLength": 16, + "maxLength": 16 + }, + "owner": { + "dataType": "bytes", + "fieldNumber": 2 + }, + "attributesArray": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["module", "attributes"], + "properties": { + "module": { + "dataType": "string", + "minLength": 1, + "maxLength": 32, + "fieldNumber": 1 + }, + "attributes": { + "dataType": "bytes", + "fieldNumber": 2 + } + } + } + } + } + } + }, + "supportedNFTsSubstore": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["chainID", "supportedCollectionIDArray"], + "properties": { + "chainID": { + "dataType": "bytes", + "fieldNumber": 1 + }, + "supportedCollectionIDArray": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["collectionID"], + "properties": { + "collectionID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + } + } + } + } + } + } + } + } + } + }, { "module": "pos", "data": { @@ -1121,76 +1210,6 @@ "commission": 0, "lastCommissionIncreaseHeight": 0, "sharingCoefficients": [] - }, - { - "address": "lskq3t9jjy3jpt2y3yrc67gs267e4vaedxzwpwzt4", - "name": "genesis_5", - "blsKey": "afc027c9fde4f7f78449ffa7c6d6d0a4417083629cb69988d1344348ae7ebf78c73c378f589bb796157680ad864eca84", - "proofOfPossession": "b01cd09f2911791e47549bce676ca99bf8a9fcd2a975d48ef8d5f863b5d46fadea95df33419a0d43618799d303ecbf3616551a43a1803675b2fe3d008ab025992cfbf9d4ed05aa79573e987e510ce3cab95de7837b7c9d78f716b690df30315e", - "generatorKey": "b837393cb9d926741547bd3e5a4db1cc90f4d9ba01fcbebedff8fe940191fe5b", - "lastGeneratedHeight": 0, - "isBanned": false, - "reportMisbehaviorHeights": [], - "consecutiveMissedBlocks": 0, - "commission": 0, - "lastCommissionIncreaseHeight": 0, - "sharingCoefficients": [] - }, - { - "address": "lskoer36v4xqh5ugsnhebpdzyjxm3nga2o9ogvewh", - "name": "genesis_6", - "blsKey": "8a5e2abfa418fd28e639cbcdfecaa170809a494522371dcb7e3eaf162712733065479e4211f4148b328643a1596e3669", - "proofOfPossession": "93c5fe759302a489f1941ac1156e8da5db210cdeb3038ed0e4c316e5d03685bd0f38692da3f2e188476e25a4b4ee116702de4675d12b101b0506a4d12048199e196b9e521386971b9984a1fa9b769562bca15a15094ffba3cd88287bca3b7608", - "generatorKey": "791d4df4544da16eaa33c7ca15a6bc217990614abb1527b38d4feb32b11bffa0", - "lastGeneratedHeight": 0, - "isBanned": false, - "reportMisbehaviorHeights": [], - "consecutiveMissedBlocks": 0, - "commission": 0, - "lastCommissionIncreaseHeight": 0, - "sharingCoefficients": [] - }, - { - "address": "lska5ezp53c5pu64avrhpj4n9w4n8dgpvqqmqedfd", - "name": "genesis_7", - "blsKey": "b4ab52a758a348ad1d5ae7adad9a7929fad91169f42ccd5d39d7bd1c7718ad1b477c87d511e397501f68596bded868af", - "proofOfPossession": "8a9600fe85b06b5b85a2fa3c28f1fbeba73233fd3f93c26f5d1af3a5a4d7eabda35dcd65297119d66081c45d72eb57d805f8aede67dd40b8888dd7419ccfff5ad19403fa02c1d6b9c88b845db0b69f1ac92dda834cc8032efa4ea430724aba40", - "generatorKey": "4868935a85f6f4498d61e6e7e888510ecd706d530c408291af85d8dd4fc20991", - "lastGeneratedHeight": 0, - "isBanned": false, - "reportMisbehaviorHeights": [], - "consecutiveMissedBlocks": 0, - "commission": 0, - "lastCommissionIncreaseHeight": 0, - "sharingCoefficients": [] - }, - { - "address": "lskydvwhgzryehb9zgogdvvqbrupwog7fpahttffv", - "name": "genesis_8", - "blsKey": "a6388495065a246d3f336460b1498264184ac1ab15e495dfbd7957e122568b3e782b3c163590478ef9427af60ee78687", - "proofOfPossession": "af02d60745287e0fdd056ab9f9139784330f00d074073550fb8d4657cb98e9108b6e47c2268d59cc96fbdc02415f37e914cc922d652d39b8fbafef211d94664660743ef574e7f9334fe60ba38f31bb7e3833dfde9620860d4d3ff9227cc8e943", - "generatorKey": "dafae6549b2b05cc69763f9500da92f3151ae0edf8e5a5fd0cb7a4ba09b6e2a2", - "lastGeneratedHeight": 0, - "isBanned": false, - "reportMisbehaviorHeights": [], - "consecutiveMissedBlocks": 0, - "commission": 0, - "lastCommissionIncreaseHeight": 0, - "sharingCoefficients": [] - }, - { - "address": "lskochy8jh28ne4bxz8c9c7c2uxe3aoag2n9z8zys", - "name": "genesis_9", - "blsKey": "84d8249385edd7edf4ce2220e00f183fc9c2edfc9836c10ff86ba3cdd6c8f2b83b6923a2e5cb60e8ab22801f70e30f03", - "proofOfPossession": "8fbe09682cfcf0f43ab37dfb1b5d35bb5fb0b2a5793555b516232df8dfa08bae53fd2e8203f9d770e8e4e0166c8dc51602123a5c7bbd99a494fbb513332d39f482028a9b3867aca046455a1764178be942190915f1fd6f9fda55c3520079877a", - "generatorKey": "b7d28382bf6d6e5e971d58ebc0f6689094f1ae242f1fc2a0a00ad34cfda7eba5", - "lastGeneratedHeight": 0, - "isBanned": false, - "reportMisbehaviorHeights": [], - "consecutiveMissedBlocks": 0, - "commission": 0, - "lastCommissionIncreaseHeight": 0, - "sharingCoefficients": [] } ], "stakers": [], diff --git a/examples/interop/pos-sidechain-example-one/config/default/genesis_block.blob b/examples/interop/pos-sidechain-example-one/config/default/genesis_block.blob index 989a4649de4..422cc88eb14 100644 Binary files a/examples/interop/pos-sidechain-example-one/config/default/genesis_block.blob and b/examples/interop/pos-sidechain-example-one/config/default/genesis_block.blob differ diff --git a/examples/interop/pos-sidechain-example-one/package.json b/examples/interop/pos-sidechain-example-one/package.json index b16c841b411..75d48829c01 100644 --- a/examples/interop/pos-sidechain-example-one/package.json +++ b/examples/interop/pos-sidechain-example-one/package.json @@ -108,12 +108,12 @@ } }, "dependencies": { - "@liskhq/lisk-framework-dashboard-plugin": "^0.3.0-rc.0", - "@liskhq/lisk-framework-faucet-plugin": "^0.3.0-rc.0", - "@liskhq/lisk-framework-monitor-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-forger-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-chain-connector-plugin": "^0.1.0-rc.0", + "@liskhq/lisk-framework-chain-connector-plugin": "^0.2.0-rc.0", + "@liskhq/lisk-framework-dashboard-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-faucet-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-forger-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-monitor-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.5.0-rc.0", "@oclif/core": "1.20.4", "@oclif/plugin-autocomplete": "1.3.6", "@oclif/plugin-help": "5.1.19", @@ -121,8 +121,8 @@ "axios": "1.2.0", "fs-extra": "11.1.0", "inquirer": "8.2.5", - "lisk-commander": "^6.0.0-rc.0", - "lisk-sdk": "^6.0.0-rc.0", + "lisk-commander": "^6.1.0-rc.0", + "lisk-sdk": "^6.1.0-rc.0", "tar": "6.1.12", "tslib": "2.4.1" }, diff --git a/examples/interop/pos-sidechain-example-one/src/app/app.ts b/examples/interop/pos-sidechain-example-one/src/app/app.ts index 8b084b532fe..20cf00b39c8 100644 --- a/examples/interop/pos-sidechain-example-one/src/app/app.ts +++ b/examples/interop/pos-sidechain-example-one/src/app/app.ts @@ -1,10 +1,23 @@ -import { Application, PartialApplicationConfig } from 'lisk-sdk'; +import { Application, PartialApplicationConfig, NFTModule } from 'lisk-sdk'; +import { TestNftModule } from './modules/testNft/module'; import { registerModules } from './modules'; import { registerPlugins } from './plugins'; import { HelloModule } from './modules/hello/module'; export const getApplication = (config: PartialApplicationConfig): Application => { - const { app } = Application.defaultApplication(config); + const { app, method } = Application.defaultApplication(config, false); + + const nftModule = new NFTModule(); + const testNftModule = new TestNftModule(); + const interoperabilityModule = app['_registeredModules'].find( + mod => mod.name === 'interoperability', + ); + interoperabilityModule.registerInteroperableModule(nftModule); + nftModule.addDependencies(method.interoperability, method.fee, method.token); + testNftModule.addDependencies(nftModule.method); + + app.registerModule(nftModule); + app.registerModule(testNftModule); const helloModule = new HelloModule(); app.registerModule(helloModule); diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules.ts b/examples/interop/pos-sidechain-example-one/src/app/modules.ts index acdfa4fb8f5..d69352da8ae 100644 --- a/examples/interop/pos-sidechain-example-one/src/app/modules.ts +++ b/examples/interop/pos-sidechain-example-one/src/app/modules.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { Application } from 'lisk-sdk'; -// @ts-expect-error app will have typescript error for unsued variable -export const registerModules = (app: Application): void => {}; +export const registerModules = (_app: Application): void => {}; diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/commands/destroy_nft.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/commands/destroy_nft.ts new file mode 100644 index 00000000000..822ad1b174f --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/commands/destroy_nft.ts @@ -0,0 +1,36 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCommand, CommandExecuteContext, NFTMethod } from 'lisk-sdk'; +import { destroyNftParamsSchema } from '../schema'; + +interface Params { + address: Buffer; + nftID: Buffer; +} + +export class DestroyNftCommand extends BaseCommand { + private _nftMethod!: NFTMethod; + public schema = destroyNftParamsSchema; + + public init(args: { nftMethod: NFTMethod }): void { + this._nftMethod = args.nftMethod; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + await this._nftMethod.destroy(context.getMethodContext(), params.address, params.nftID); + } +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/commands/mint_nft.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/commands/mint_nft.ts new file mode 100644 index 00000000000..bc5638846d4 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/commands/mint_nft.ts @@ -0,0 +1,43 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCommand, CommandExecuteContext, NFTMethod } from 'lisk-sdk'; +import { NFTAttributes } from '../types'; +import { mintNftParamsSchema } from '../schema'; + +interface Params { + address: Buffer; + collectionID: Buffer; + attributesArray: NFTAttributes[]; +} + +export class MintNftCommand extends BaseCommand { + private _nftMethod!: NFTMethod; + public schema = mintNftParamsSchema; + + public init(args: { nftMethod: NFTMethod }): void { + this._nftMethod = args.nftMethod; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + await this._nftMethod.create( + context.getMethodContext(), + params.address, + params.collectionID, + params.attributesArray, + ); + } +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/constants.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/constants.ts new file mode 100644 index 00000000000..a0150fad36f --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/constants.ts @@ -0,0 +1,18 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export const LENGTH_COLLECTION_ID = 4; +export const MIN_LENGTH_MODULE_NAME = 1; +export const MAX_LENGTH_MODULE_NAME = 32; +export const LENGTH_NFT_ID = 16; diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/endpoint.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/endpoint.ts new file mode 100644 index 00000000000..1d091013741 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/endpoint.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEndpoint } from 'lisk-sdk'; + +export class TestNftEndpoint extends BaseEndpoint {} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/method.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/method.ts new file mode 100644 index 00000000000..5bab789e7f1 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/method.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseMethod } from 'lisk-sdk'; + +export class TestNftMethod extends BaseMethod {} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/module.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/module.ts new file mode 100644 index 00000000000..a228abff3af --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/module.ts @@ -0,0 +1,56 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseModule, ModuleInitArgs, ModuleMetadata, NFTMethod } from 'lisk-sdk'; +import { TestNftEndpoint } from './endpoint'; +import { TestNftMethod } from './method'; +import { MintNftCommand } from './commands/mint_nft'; +import { DestroyNftCommand } from './commands/destroy_nft'; + +export class TestNftModule extends BaseModule { + public endpoint = new TestNftEndpoint(this.stores, this.offchainStores); + public method = new TestNftMethod(this.stores, this.events); + public mintNftCommand = new MintNftCommand(this.stores, this.events); + public destroyNftCommand = new DestroyNftCommand(this.stores, this.events); + public commands = [this.mintNftCommand, this.destroyNftCommand]; + + private _nftMethod!: NFTMethod; + + public addDependencies(nftMethod: NFTMethod) { + this._nftMethod = nftMethod; + } + + public metadata(): ModuleMetadata { + return { + ...this.baseMetadata(), + endpoints: [], + commands: this.commands.map(command => ({ + name: command.name, + params: command.schema, + })), + events: [], + assets: [], + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async init(_args: ModuleInitArgs) { + this.mintNftCommand.init({ + nftMethod: this._nftMethod, + }); + this.destroyNftCommand.init({ + nftMethod: this._nftMethod, + }); + } +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/schema.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/schema.ts new file mode 100644 index 00000000000..c183e9fb8ff --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/schema.ts @@ -0,0 +1,79 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { + LENGTH_COLLECTION_ID, + LENGTH_NFT_ID, + MAX_LENGTH_MODULE_NAME, + MIN_LENGTH_MODULE_NAME, +} from './constants'; + +export const mintNftParamsSchema = { + $id: '/lisk/nftTransferParams', + type: 'object', + required: ['address', 'collectionID', 'attributesArray'], + properties: { + address: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + collectionID: { + dataType: 'bytes', + minLength: LENGTH_COLLECTION_ID, + maxLength: LENGTH_COLLECTION_ID, + fieldNumber: 2, + }, + attributesArray: { + type: 'array', + fieldNumber: 4, + items: { + type: 'object', + required: ['module', 'attributes'], + properties: { + module: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + pattern: '^[a-zA-Z0-9]*$', + fieldNumber: 1, + }, + attributes: { + dataType: 'bytes', + fieldNumber: 2, + }, + }, + }, + }, + }, +}; + +export const destroyNftParamsSchema = { + $id: '/lisk/nftDestroyParams', + type: 'object', + required: ['address', 'nftID'], + properties: { + address: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 2, + }, + }, +}; diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/types.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/types.ts new file mode 100644 index 00000000000..8d1af2d969a --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/testNft/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export interface NFTAttributes { + module: string; + attributes: Buffer; +} diff --git a/examples/interop/pos-sidechain-example-two/config/default/genesis_block.blob b/examples/interop/pos-sidechain-example-two/config/default/genesis_block.blob index bbedeb8269b..cb9a7a2ab90 100644 Binary files a/examples/interop/pos-sidechain-example-two/config/default/genesis_block.blob and b/examples/interop/pos-sidechain-example-two/config/default/genesis_block.blob differ diff --git a/examples/interop/pos-sidechain-example-two/package.json b/examples/interop/pos-sidechain-example-two/package.json index 6e70f660c5b..fc436548ba8 100644 --- a/examples/interop/pos-sidechain-example-two/package.json +++ b/examples/interop/pos-sidechain-example-two/package.json @@ -108,12 +108,12 @@ } }, "dependencies": { - "@liskhq/lisk-framework-dashboard-plugin": "^0.3.0-rc.0", - "@liskhq/lisk-framework-faucet-plugin": "^0.3.0-rc.0", - "@liskhq/lisk-framework-monitor-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-forger-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-chain-connector-plugin": "^0.1.0-rc.0", + "@liskhq/lisk-framework-chain-connector-plugin": "^0.2.0-rc.0", + "@liskhq/lisk-framework-dashboard-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-faucet-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-forger-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-monitor-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.5.0-rc.0", "@oclif/core": "1.20.4", "@oclif/plugin-autocomplete": "1.3.6", "@oclif/plugin-help": "5.1.19", @@ -121,8 +121,8 @@ "axios": "1.2.0", "fs-extra": "11.1.0", "inquirer": "8.2.5", - "lisk-commander": "^6.0.0-rc.0", - "lisk-sdk": "^6.0.0-rc.0", + "lisk-commander": "^6.1.0-rc.0", + "lisk-sdk": "^6.1.0-rc.0", "tar": "6.1.12", "tslib": "2.4.1" }, diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules.ts b/examples/interop/pos-sidechain-example-two/src/app/modules.ts index acdfa4fb8f5..d69352da8ae 100644 --- a/examples/interop/pos-sidechain-example-two/src/app/modules.ts +++ b/examples/interop/pos-sidechain-example-two/src/app/modules.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { Application } from 'lisk-sdk'; -// @ts-expect-error app will have typescript error for unsued variable -export const registerModules = (app: Application): void => {}; +export const registerModules = (_app: Application): void => {}; diff --git a/examples/poa-sidechain/.eslintignore b/examples/poa-sidechain/.eslintignore new file mode 100644 index 00000000000..00a15e70c20 --- /dev/null +++ b/examples/poa-sidechain/.eslintignore @@ -0,0 +1,14 @@ +docs/ +examples/ +**/*.d.ts +jest.config.js +.eslintrc.js +coverage +benchmark +dist +tmp +build +scripts +config +test/_setup.js +ecosystem.config.js diff --git a/examples/poa-sidechain/.eslintrc.js b/examples/poa-sidechain/.eslintrc.js new file mode 100644 index 00000000000..12f565e1e3c --- /dev/null +++ b/examples/poa-sidechain/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + root: true, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + extends: ['lisk-base/ts'], + rules: { + '@typescript-eslint/member-ordering': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + }, +}; diff --git a/examples/poa-sidechain/.gitignore b/examples/poa-sidechain/.gitignore new file mode 100644 index 00000000000..c36928433e2 --- /dev/null +++ b/examples/poa-sidechain/.gitignore @@ -0,0 +1,45 @@ +# General +~ +.DS_Store +.project +__MACOSX/ +*.swp +*.swo +ssl/ +tmp/ + +# Build revision file generated while building a release +REVISION +npm-shrinkwrap.json + +# Dependency directories +tsconfig.tsbuildinfo +node_modules/ + +# Docs +docs/jsdoc/ + +# Logs +logs/* +logs.log +npm-debug.log +tmux-client-*.log +stacktrace* + +# IDE directories +.vscode/ +.idea/ +Session.vim + +# Config files +sftp-config.json +.secrets + +# Coverage directory used by tools like istanbul +.coverage/ + +# Local config file useful for development +config.local.json + +# Build Directory +dist diff --git a/examples/poa-sidechain/.lintstagedrc.json b/examples/poa-sidechain/.lintstagedrc.json new file mode 100644 index 00000000000..50d39da6d11 --- /dev/null +++ b/examples/poa-sidechain/.lintstagedrc.json @@ -0,0 +1,5 @@ +{ + "*.js": ["prettier --write", "eslint"], + "*.ts": ["prettier --write", "eslint"], + "*.{json,md}": ["prettier --write"] +} diff --git a/examples/poa-sidechain/.liskrc.json b/examples/poa-sidechain/.liskrc.json new file mode 100644 index 00000000000..0011a20d0c3 --- /dev/null +++ b/examples/poa-sidechain/.liskrc.json @@ -0,0 +1,6 @@ +{ + "commander": { + "version": "5.1.9" + }, + "template": "lisk-ts" +} diff --git a/examples/poa-sidechain/.prettierignore b/examples/poa-sidechain/.prettierignore new file mode 100644 index 00000000000..05fdf62598a --- /dev/null +++ b/examples/poa-sidechain/.prettierignore @@ -0,0 +1,36 @@ +# Files +Jenkinsfile* +Makefile +Dockerfile +LICENSE +.DS_Store +data/ +.idea +logs/ + +.gitkeep + +# rc files +.*rc + +## ignore files +.*ignore + +# Ignore extensions +*.png +*.sql + +## jest snapshot +*.snap +*.tsbuildinfo + +# project specific paths +dist/ +bin +tmp/ + +*.pid +*.gz +*.blob + +docker/* diff --git a/examples/poa-sidechain/.prettierrc.json b/examples/poa-sidechain/.prettierrc.json new file mode 100644 index 00000000000..7b23ef55098 --- /dev/null +++ b/examples/poa-sidechain/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all", + "useTabs": true, + "arrowParens": "avoid" +} diff --git a/examples/poa-sidechain/README.md b/examples/poa-sidechain/README.md new file mode 100644 index 00000000000..f50797d1d3a --- /dev/null +++ b/examples/poa-sidechain/README.md @@ -0,0 +1,37 @@ +# PoA Example + +This project was bootstrapped with [Lisk SDK](https://github.com/LiskHQ/lisk-sdk) + +### Start a node + +``` +./bin/run start +``` + +### Add a new module + +``` +lisk generate:module ModuleName ModuleID +// Example +lisk generate:module token 1 +``` + +### Add a new asset + +``` +lisk generate:asset ModuleName AssetName AssetID +// Example +lisk generate:asset token transfer 1 +``` + +### Add a new plugin + +``` +lisk generate:plugin PluginName +// Example +lisk generate:plugin httpAPI +``` + +## Learn More + +You can learn more in the [documentation](https://lisk.com/documentation/lisk-sdk/). diff --git a/examples/poa-sidechain/bin/run b/examples/poa-sidechain/bin/run new file mode 100755 index 00000000000..283c01038f5 --- /dev/null +++ b/examples/poa-sidechain/bin/run @@ -0,0 +1,6 @@ +#!/usr/bin/env node + +require('@oclif/core') + .run() + .then(require('@oclif/core/flush')) + .catch(require('@oclif/core/handle')); diff --git a/examples/poa-sidechain/bin/run.cmd b/examples/poa-sidechain/bin/run.cmd new file mode 100644 index 00000000000..cf40b543c96 --- /dev/null +++ b/examples/poa-sidechain/bin/run.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\run" %* \ No newline at end of file diff --git a/examples/poa-sidechain/config/alphanet/config.json b/examples/poa-sidechain/config/alphanet/config.json new file mode 100644 index 00000000000..62319a04e38 --- /dev/null +++ b/examples/poa-sidechain/config/alphanet/config.json @@ -0,0 +1,44 @@ +{ + "system": { + "dataPath": "~/.lisk/pos-mainchain", + "enableMetrics": true + }, + "rpc": { + "modes": ["ipc"] + }, + "genesis": { + "block": { + "fromFile": "./config/genesis_block.blob" + }, + "blockTime": 10, + "bftBatchSize": 103, + "chainID": "04000000", + "maxTransactionsSize": 15360 + }, + "generator": { + "keys": {} + }, + "network": { + "version": "1.0", + "seedPeers": [ + { + "ip": "127.0.0.1", + "port": 7667 + } + ], + "port": 7667 + }, + "transactionPool": { + "maxTransactions": 4096, + "maxTransactionsPerAccount": 64, + "transactionExpiryTime": 10800000, + "minEntranceFeePriority": "0", + "minReplacementFeeDifference": "10" + }, + "modules": {}, + "plugins": { + "reportMisbehavior": { + "encryptedPassphrase": "iterations=10&cipherText=5dea8b928a3ea2481ebc02499ae77679b7552189181ff189d4aa1f8d89e8d07bf31f7ebd1c66b620769f878629e1b90499506a6f752bf3323799e3a54600f8db02f504c44d&iv=37e0b1753b76a90ed0b8c319&salt=963c5b91d3f7ba02a9d001eed49b5836&tag=c3e30e8f3440ba3f5b6d9fbaccc8918d&version=1" + } + } +} diff --git a/examples/poa-sidechain/config/alphanet/dev-validators.json b/examples/poa-sidechain/config/alphanet/dev-validators.json new file mode 100644 index 00000000000..fe51b5a4a71 --- /dev/null +++ b/examples/poa-sidechain/config/alphanet/dev-validators.json @@ -0,0 +1,1652 @@ +{ + "keys": [ + { + "address": "lske5sqed53fdcs4m9et28f2k7u9fk6hno9bauday", + "keyPath": "m/44'/134'/0'", + "publicKey": "a3f96c50d0446220ef2f98240898515cbba8155730679ca35326d98dcfb680f0", + "privateKey": "d0b159fe5a7cc3d5f4b39a97621b514bc55b0a0f1aca8adeed2dd1899d93f103a3f96c50d0446220ef2f98240898515cbba8155730679ca35326d98dcfb680f0", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/0'", + "generatorKey": "b9e54121e5346cc04cc84bcf286d5e40d586ba5d39571daf57bd31bac3861a4a", + "generatorPrivateKey": "b3c4de7f7932275b7a465045e918337ffd7b7b229cef8eba28f706de8759da95b9e54121e5346cc04cc84bcf286d5e40d586ba5d39571daf57bd31bac3861a4a", + "blsKeyPath": "m/12381/134/0/0", + "blsKey": "92f020ce5e37befb86493a82686b0eedddb264350b0873cf1eeaa1fefe39d938f05f272452c1ef5e6ceb4d9b23687e31", + "blsProofOfPossession": "b92b11d66348e197c62d14af1453620d550c21d59ce572d95a03f0eaa0d0d195efbb2f2fd1577dc1a04ecdb453065d9d168ce7648bc5328e5ea47bb07d3ce6fd75f35ee51064a9903da8b90f7dc8ab4f2549b834cb5911b883097133f66b9ab9", + "blsPrivateKey": "463dd3413051366ee658c2524dd0bec85f8459bf6d70439685746406604f950d" + }, + "encrypted": {} + }, + { + "address": "lsk8dsngwh4n6hmf4unqb8gfqgkayabaqdvtq85ja", + "keyPath": "m/44'/134'/1'", + "publicKey": "0904c986211330582ef5e41ed9a2e7d6730bb7bdc59459a0caaaba55be4ec128", + "privateKey": "2475a8233503caade9542f2dd6c8c725f10bc03e3f809210b768f0a2320f06d50904c986211330582ef5e41ed9a2e7d6730bb7bdc59459a0caaaba55be4ec128", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/1'", + "generatorKey": "dd337fcb819073335382415bfdbf5e5b7e73126aafb0ac46479137328e72d438", + "generatorPrivateKey": "eaddefbdcb41468e73d7ae8e6c0b046de56f8829cbd3ea10c2abf0c74faa1598dd337fcb819073335382415bfdbf5e5b7e73126aafb0ac46479137328e72d438", + "blsKeyPath": "m/12381/134/0/1", + "blsKey": "aa5174668a4743d838fa3742092c744c3edd4ee64c535ce2a69eeae1c5f23029acd74853410867d873076639f4ce1cda", + "blsProofOfPossession": "ad79b935bd503402b83404125ef11fab81f4c6bef0688798473e430f892704b653209aaf81f16efca9965fad0850a3971662f33c25994568e1434f4f46901caa1c002cab18dff7337836617c372673714d63b01ec4db098f419c027015aa4c05", + "blsPrivateKey": "4856d774c133fc205f1950cb030eddc2286ba6662e8f5061d153a7b36d16781a" + }, + "encrypted": {} + }, + { + "address": "lskjtbchucvrd2s8qjo83e7trpem5edwa6dbjfczq", + "keyPath": "m/44'/134'/2'", + "publicKey": "b8d2422aa7ebf1f85031f0bac2403be1fb24e0196d3bbed33987d4769eb37411", + "privateKey": "03e7852c6f1c6fe5cd0c5f7e3a36e499a1e0207e867f74f5b5bc42bfcc888bc8b8d2422aa7ebf1f85031f0bac2403be1fb24e0196d3bbed33987d4769eb37411", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/2'", + "generatorKey": "3e63c0a5d4de4df114823934ceaa6c17a48e5a6650788cf1f63c826c984c0957", + "generatorPrivateKey": "c96d896fd601e71a61452465692e6f77c9f654af0c596d4d5a2285333ccc846e3e63c0a5d4de4df114823934ceaa6c17a48e5a6650788cf1f63c826c984c0957", + "blsKeyPath": "m/12381/134/0/2", + "blsKey": "8c141e5d769c22ec90122f42bef1d1e7af2d94c1da6844bd313fca2ccf0543eab5f8c6752dd47969dc34613801dfb293", + "blsProofOfPossession": "9681aa250d714befe61d71f239a9b4c09ee102addb3a5e2c884074c7ba763b5c21e53aa7b12518d32c9b874ba1910e7a0bf0bd23ae99f57f6f464403b1151b3521a7a369ff94118a436e6aa767bd462d9ca491dd3e253862c21ff078878c354e", + "blsPrivateKey": "05739256f97460ba695cb52abcc9f8d9d46d5ed052ccbb16c780c6fd44ac153b" + }, + "encrypted": {} + }, + { + "address": "lskau7uqo6afteazgyknmtotxdjgwr3p9gfr4yzke", + "keyPath": "m/44'/134'/3'", + "publicKey": "557f1b9647fd2aefa357fed8bead72d1b02e5151b57d3c32d4d3f808c0705026", + "privateKey": "985bc97b4b2aa91d590dde455c19c70818d97c56c7cfff790a1e0b71e3d15962557f1b9647fd2aefa357fed8bead72d1b02e5151b57d3c32d4d3f808c0705026", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/3'", + "generatorKey": "00245e599fdad13ed0b064c069c71c73caf868a4635c0143963a529807f8728c", + "generatorPrivateKey": "a4426b9facb99efcf6ad7702f02e3e57ea2dd6d5e4f5bbee25729595e012df8800245e599fdad13ed0b064c069c71c73caf868a4635c0143963a529807f8728c", + "blsKeyPath": "m/12381/134/0/3", + "blsKey": "aaec4e157b19c0a3f2965cc636f5f82cef9b3918c071e2c6e50f57ecb44587d58139595e8f4c1fc7f76b2f7c09b1b6d1", + "blsProofOfPossession": "866a031b5a2a6b0525053b2d870487ac2fd39cf2cf18ecf462bc19afc5ef52f129cf88624fac73057c5375004492dbfb0b8cacb906b3a7daa4d7edf99f10ab15a90b3b328e8ad6701e838a88351fecdfb5b32eebeb80fdeb8c0345d1b5257d7b", + "blsPrivateKey": "43b132328eec8064dcbd62f038ad73e372c12d94fdedad5a35a95cdd0ad858e5" + }, + "encrypted": {} + }, + { + "address": "lsksdfqvkbqpc8eczj2s3dzkxnap5pguaxdw2227r", + "keyPath": "m/44'/134'/4'", + "publicKey": "e5e4834c2c7e949ac6e97512b5ff5d44822376b1e54cae8c326de0873c0b72ad", + "privateKey": "6f2b2f6ef42f417af916fb2a29ae8c8d0c572219d7420927c2dcd336e21c9115e5e4834c2c7e949ac6e97512b5ff5d44822376b1e54cae8c326de0873c0b72ad", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/4'", + "generatorKey": "21f9d60315c1baeb513b5f7324a1211723d36948b64806541b8855988f86111f", + "generatorPrivateKey": "c467e3bbc6af24568c8a8a8ee29055c2704aab14549dd99f1f1d1cfccdad384421f9d60315c1baeb513b5f7324a1211723d36948b64806541b8855988f86111f", + "blsKeyPath": "m/12381/134/0/4", + "blsKey": "84912d2f185c2058be9ed201d970f435a408c8bb3a36c430f007b69632efb2f663b51df383be6eedb80c8768a70822bb", + "blsProofOfPossession": "aafdb397226d3a4a4cc3b7ac906ae7e3601310bd5d0e20a0682364312937e8e3e0c3b5846a53ee536cac2a2b3f556bff06c65ef24a32495dee9d38ee5b2012113d8f032d8dd0f3f5d9af50dbd307d0e7f66aaa165620d5292da91306b0a39aad", + "blsPrivateKey": "16f43c470d46b9a10a461328c9ee629b045cfd469dc3cb9c1ac9ba85a5af5b8a" + }, + "encrypted": {} + }, + { + "address": "lskvq67zzev53sa6ozt39ft3dsmwxxztb7h29275k", + "keyPath": "m/44'/134'/5'", + "publicKey": "c1e3177d1433ece7f8fcb607edc37df4fd37284f46081f846ca7852735b4145b", + "privateKey": "4d108ede8bce4330260360341229c608fcdfdf07b262cfdbdc3cb49a560ba71cc1e3177d1433ece7f8fcb607edc37df4fd37284f46081f846ca7852735b4145b", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/5'", + "generatorKey": "8b65dce85de8ed215a91477627b365ec017a01cd5a715337f772ba42715cc794", + "generatorPrivateKey": "fbdd344d5e73d45c50298c109d34f0da4eee8ca8068f893110c6a4a86bba05778b65dce85de8ed215a91477627b365ec017a01cd5a715337f772ba42715cc794", + "blsKeyPath": "m/12381/134/0/5", + "blsKey": "9006fc2c9d159b6890047e9b26c700d8c504e17b6fe476a2a1ac1477357c68eee332be587da425e37e22332348ed8007", + "blsProofOfPossession": "945ac6db93666aa21934d84c6ad897fe1acf1d208a17ec46b0ddf26cf6d9cdccef7db9eac682195ec47cb8e7a069bbe10706a4e1cce2012aadd311dafb270c9c810d80bc82c2b6c34ce236efac552fa0904b96533772f98e202f4e6f47c97f09", + "blsPrivateKey": "4adf92c505124ff3ff4f3b36fff3a2ce3d60953dbcb34b4c43ea93b82e17f970" + }, + "encrypted": {} + }, + { + "address": "lskfjd3ymhyzedgneudo2bujnm25u7stu4qpa3jnd", + "keyPath": "m/44'/134'/6'", + "publicKey": "dc5adaa7cc6e0598a4a6347ce9cb3f213835d863c377410c3eafa8b718807aa3", + "privateKey": "2926701eccc5232d51ed98a2bc9cebdd687d8a3760d3c5adb8cae7a434dbab2ddc5adaa7cc6e0598a4a6347ce9cb3f213835d863c377410c3eafa8b718807aa3", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/6'", + "generatorKey": "326cb34aa214c4952f646d93af8cfbe58ec74db76db54484b5a23918cba8743b", + "generatorPrivateKey": "b3bf887c6a4a646e444c877d2299b2aa1328251d68af051328e88eb9872e8de4326cb34aa214c4952f646d93af8cfbe58ec74db76db54484b5a23918cba8743b", + "blsKeyPath": "m/12381/134/0/6", + "blsKey": "96a70c8b1343511359f7205313eac8c73b2838e25eda58cf8c13fa1d2689aee3df70522bcbd36e0bde958409b80cc8ee", + "blsProofOfPossession": "89564da089fcc38e4973cf34b5a8abbe8e822bb59f05633156d9dc0b10f2aad8d4621ea66023ec2a10d6d581927af3bc0746cd8293ea22c8db0068c127d38c4c2dcfe777ffc03e773083fd0036894cce7c2596301381941523f4f2ae97bb79e9", + "blsPrivateKey": "01fcace0a39a0f12057671c9ca88f41811ae7cc6c928c4a79cb5e7e3883c17f3" + }, + "encrypted": {} + }, + { + "address": "lskqw45qy3ph9rwgow86rudqa7e3vmb93db5e4yad", + "keyPath": "m/44'/134'/7'", + "publicKey": "e5c559e55dbb69328dc765d732e3df31b60d243d4c1a240a3d99af413e8958c6", + "privateKey": "26e75ae42bb589e181b38ce31911d3a63e2b0d3ae1be0b29d61971c986906687e5c559e55dbb69328dc765d732e3df31b60d243d4c1a240a3d99af413e8958c6", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/7'", + "generatorKey": "1314b7d167d5829fb535d15dfb5216e10ad2e5b6a349ae347aec77317b6aa73f", + "generatorPrivateKey": "de317ea0e11dde876b6ef8f37298a0608eb78e987380da4777137b4661f023921314b7d167d5829fb535d15dfb5216e10ad2e5b6a349ae347aec77317b6aa73f", + "blsKeyPath": "m/12381/134/0/7", + "blsKey": "b40065dfa219e40c65c07d516158d722ec695abc91411ce57550c77fa2119e52b56cb74db7a1d805b631752e8f6b80be", + "blsProofOfPossession": "b7085c15521303140512fdea858231a040534a4b0c1dbbdb002c8df233634270d33e51c3699cf4956d165c0183f29a32070d8f4e00433ebcdfcae337a5f09f2c971ba97d5b35413ce032d2ec4084ed79efc917bdb75ded139fc9433df884a18e", + "blsPrivateKey": "3f78ff58a0462d09c20249fdd8b16dafc09bf5d41669a7355aaea5e9705d1c46" + }, + "encrypted": {} + }, + { + "address": "lsk8vjsq5s8jan9c8y9tmgawd6cttuszbf6jmhvj5", + "keyPath": "m/44'/134'/8'", + "publicKey": "665b67a9bfa854ea7e58a1dbde618410d9c63e50204ac3a12a4cfdc44a903d95", + "privateKey": "e98c4711a330632bd012bb0d2f73e2b3d72635e3c13c54edd9b9de6dcd6fc73f665b67a9bfa854ea7e58a1dbde618410d9c63e50204ac3a12a4cfdc44a903d95", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/8'", + "generatorKey": "00110f493d122a73628a518842e99591b91def4ef9fbd58e1b6458950da5a776", + "generatorPrivateKey": "eace487ec72fbfc569c3680713146fc354678533fb06de639b6d8a0e658ac5e200110f493d122a73628a518842e99591b91def4ef9fbd58e1b6458950da5a776", + "blsKeyPath": "m/12381/134/0/8", + "blsKey": "837e0759968b1ed95789252d1e731d7b127c9a53a74e86f3ca3d65d71cf666f2208baa782a42c45d4132630100a59462", + "blsProofOfPossession": "b97607b1478f17877b4c8042530763894dd7b79f8bbf5ca0883d08b94dc8a11cc2c2a73123160e3b01da692fb071f5fe0d808426604b5ad8aadebda9b02710698158254f6f1d822c2c9bae5c081101806e9220d79c547391e6fc6d8f26094dc7", + "blsPrivateKey": "2cf343ea5097fe55d1d1f054a76dc2766c88acadb8b2156318fc5b56f76e5200" + }, + "encrypted": {} + }, + { + "address": "lskchcsq6pgnq6nwttwe9hyj67rb9936cf2ccjk3b", + "keyPath": "m/44'/134'/9'", + "publicKey": "2c40d2354c023409c24d16dce668ae26930a675b274ae8409a0c67a2f16672e0", + "privateKey": "b1863cba481c0b16ca83b0257d71964d1ade9cb2b6895f78c4686c793c7cf5842c40d2354c023409c24d16dce668ae26930a675b274ae8409a0c67a2f16672e0", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/9'", + "generatorKey": "be4e49ea7e57ede752ce33cb224f50277552f9085a551005255ee12a9b4ca68d", + "generatorPrivateKey": "8210871092519d73ea2e2645f57333d01bfdb7e553ef188b4d57e985e461be79be4e49ea7e57ede752ce33cb224f50277552f9085a551005255ee12a9b4ca68d", + "blsKeyPath": "m/12381/134/0/9", + "blsKey": "8fd004c33814c3b452d50b2bf6855eeb03e41552c6edd50b76dee57007a34cf987da1e06425cf498391e6831d1bf6851", + "blsProofOfPossession": "a0e34bdc7dc39e09f686d6712fd0e71c61c8d06dfedbdbb9ed77c821c22d6c87f87e39e48db79aa50c19904933abb11a0b07659317079ae8f2db6e27b9139ce0830faa8dad2dcae2079f64781b0516be825b2d84689080bb8219a5ec72ba80f7", + "blsPrivateKey": "3d5f026eb2fb39cecc763f052695f75cdf52d3382148abf49a03b6f84ef9f075" + }, + "encrypted": {} + }, + { + "address": "lskc22mfaqzo722aenb6yw7awx8f22nrn54skrj8b", + "keyPath": "m/44'/134'/10'", + "publicKey": "88da43d0f056dd666cf2a8ae37db58e28bba3ae0b954930674ebe5dc03311e99", + "privateKey": "f1c8bf737f8e537dcdf202e8de94e138945d9bf9bd70ed700fcd0247bda8104b88da43d0f056dd666cf2a8ae37db58e28bba3ae0b954930674ebe5dc03311e99", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/10'", + "generatorKey": "671c72129793eb5801273ff580ce3d4c78d89fc8b4fb95b090a9af0a9a647a41", + "generatorPrivateKey": "ef19cef8e2f025de4d923fb976f5dc5ab4d5fd0e1c935f3d44e8722e6a036ffd671c72129793eb5801273ff580ce3d4c78d89fc8b4fb95b090a9af0a9a647a41", + "blsKeyPath": "m/12381/134/0/10", + "blsKey": "a38d728c1c1023651b031835818d17d0665d1fbabd8e62da26ca53f290620c23fe928244bcbcbb67412344013017cb53", + "blsProofOfPossession": "b5d455bb358eff87779b296f23a2fc9abc9d8f3ecb8ed0d9af3e23066e653a58b189c11b4a3980eaeaaa85ffcc240795187f6e8a0e8e8a2837bc20d485e1d3159c2d581614d72f94bbd049e5a9f45c0302851c87aa3c3853d8962ed75d140234", + "blsPrivateKey": "2e3c200c9927504eaab6dcb3777d394aa0d5e7c8a85e09f102bfe84b311f6eb6" + }, + "encrypted": {} + }, + { + "address": "lskezdab747v9z78hgmcxsokeetcmbdrpj3gzrdcw", + "keyPath": "m/44'/134'/11'", + "publicKey": "46ddcc48cc566faedd278169c1327bef337e32044320291f452aa60327c2cd2f", + "privateKey": "2c4f8a875c3850d8aacdb2643ce32ac3a20d61e24c69c7cba1e6315592992e1846ddcc48cc566faedd278169c1327bef337e32044320291f452aa60327c2cd2f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/11'", + "generatorKey": "44de3820f1a1a7351953d2d000f29cb7bffecf30582a8b3da2cb80c83b9eceef", + "generatorPrivateKey": "e2fec1ce757b5865797955e9fbe074224b67ce9fe1e0f5df6ed633745da3540a44de3820f1a1a7351953d2d000f29cb7bffecf30582a8b3da2cb80c83b9eceef", + "blsKeyPath": "m/12381/134/0/11", + "blsKey": "a03ba0f1d6bf9378681b9d96dbe8176cc0ab2a424154cbbe325fc279d02cf58bc15de966cb1e272312ba2b6db31a7f05", + "blsProofOfPossession": "a20a8edd978fe911da6c933d486cb9af770179ef5ee21ad869c4c35e63103cfc2ac17350ee2d35b4bbd487193cdb33ab0116fdf2f078f289fae2922f6a7e372ef8ea543d52ae74ae395dccf2dec2c40e6596c807a14c9fce45b320321f68c612", + "blsPrivateKey": "6aa2aafb57bf3d0038bd7b0a9fd88632a6be33e51a8eeee87432d84b72dbbab0" + }, + "encrypted": {} + }, + { + "address": "lsknddzdw4xxej5znssc7aapej67s7g476osk7prc", + "keyPath": "m/44'/134'/12'", + "publicKey": "86ae660dcf148c829a17364f0fc9f7f61cb5efde7c10598923cfec376c346492", + "privateKey": "dd495f4d08928547ab5d2b39fc934e31a052181f338e0a723bc51f4305cd908c86ae660dcf148c829a17364f0fc9f7f61cb5efde7c10598923cfec376c346492", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/12'", + "generatorKey": "3c19943d614f67309dd989e2e1bdeade5ea53b0522eac3d46b9e7f68604a874d", + "generatorPrivateKey": "3fbbad2694492781f334e0a8c9a03827ce3139f5cf1c17fcf410a7d6ec0a3b653c19943d614f67309dd989e2e1bdeade5ea53b0522eac3d46b9e7f68604a874d", + "blsKeyPath": "m/12381/134/0/12", + "blsKey": "8ae81737f7b1678ece4b06db3ee1d633637da3c02cf646cdb0c7c1dae5f9eea41f2384fca8b0b12033d316ee78ea3e94", + "blsProofOfPossession": "a5150c19ac23dc15f660d9612be5f9591c1a5fc892e9f8b267de6bd39da84f254b6644e8c0f294900e5e9b7c9ecf3f260d902a56af7db5a59083eda08dd3ff083e2a07ba5d34f25312621f8686358dd2a50dcdc879eb0f9d50ff2fdc704e7d9a", + "blsPrivateKey": "0f0bb8d3299a807f35029011a71e366e134d6288a41d5cae85844b3f33e2b274" + }, + "encrypted": {} + }, + { + "address": "lskffxs3orv2au2juwa69hqtrmpcg9vq78cqbdjr4", + "keyPath": "m/44'/134'/13'", + "publicKey": "159c3170dfc8df2820e9c953ecceeaa8d8746af54687c4c266f654a3a1dd1714", + "privateKey": "d470a6f2a03a4bc359727bb957fea1efcb07ec0e07a143388d36b40d76f220c7159c3170dfc8df2820e9c953ecceeaa8d8746af54687c4c266f654a3a1dd1714", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/13'", + "generatorKey": "4e54056fabe183ab645962cf0b70e658d0eae506c4ade8756652ca7f76733227", + "generatorPrivateKey": "c35fe47d21ad0d2edc953eb17e27ce9532f30f35ba2d90e9ddfdacc06b1cfb124e54056fabe183ab645962cf0b70e658d0eae506c4ade8756652ca7f76733227", + "blsKeyPath": "m/12381/134/0/13", + "blsKey": "a3e2b645a315827618e58c1eb66dfef3744c8111a0c7b0e8535a3ec31d78ea2630646fea1da5609988c5d88997d663fb", + "blsProofOfPossession": "b55d1c525f96bba45cbefbcadad16279c9f61f790dfc3e3c824003139f9994200079faf573eddb863c6ba1fd9b7d7364146e3f20579b065355c75691e06be2c7304fe48d32fbfcb5ef38f8ecaa6905e9ca6a7c1124c45a6ab2b06668cb3decc9", + "blsPrivateKey": "58ef88d198c15101e9813bb963807ad43453422c76ff0a645e44851b482f417f" + }, + "encrypted": {} + }, + { + "address": "lskf7a93qr84d9a6ga543wernvxbsrpvtp299c5mj", + "keyPath": "m/44'/134'/14'", + "publicKey": "37002d59f3e5b66cac1a0598ea21c3360059afbd6bc6f298939cdae03a3db882", + "privateKey": "6c8d002f2b58e11940eb5c79fae119574ccda401c71cc8b451d2783d0286f91e37002d59f3e5b66cac1a0598ea21c3360059afbd6bc6f298939cdae03a3db882", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/14'", + "generatorKey": "ac34c0731cddab10726e634cec30294f831af045a0614733ac683ccdb6bc7eab", + "generatorPrivateKey": "1c91906bbd73352db1e4f89344b0851462962db0a11864a63a8ecfd805182935ac34c0731cddab10726e634cec30294f831af045a0614733ac683ccdb6bc7eab", + "blsKeyPath": "m/12381/134/0/14", + "blsKey": "a7283bff41249c3d2a0f065a27448a4c5acefaece74e51ec432c418c4bc8e6f0eb60160feec4729b9c0b933e9ec5e528", + "blsProofOfPossession": "86f1ac081ee08568266dc39727540a5d50f03e544f73d9a3ca60d87cfe9b6718832e07b2720d42e0e818c5fe2d45099a0774af1e6b123b41a3eb7eb3a1443d248a535fe9ef93f0027a8e8f44686dc33d677b79251c22022675395a347d0f3dbb", + "blsPrivateKey": "1f14d0e79b00554226cd7655f10eb22d5a5452d23665a8d06219b303e9595211" + }, + "encrypted": {} + }, + { + "address": "lskfx88g3826a4qsyxm4w3fheyymfnucpsq36d326", + "keyPath": "m/44'/134'/15'", + "publicKey": "6877e45fbe5b009d364071a1d282ebab1c1e34307c92e698d1ffb6ceb98f09e3", + "privateKey": "2afa9923109b1d4111ccf8678ff62bd63dbc97f69b6fb251442ec6b9140170b96877e45fbe5b009d364071a1d282ebab1c1e34307c92e698d1ffb6ceb98f09e3", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/15'", + "generatorKey": "028a30837b7eec19b02b06c3c2f4065290285e40a4870a677664fee3fe76d9be", + "generatorPrivateKey": "7ff68b39611f7d7b8fdc05226846abfdbbdb62becfb15032db25fe9281ebc71e028a30837b7eec19b02b06c3c2f4065290285e40a4870a677664fee3fe76d9be", + "blsKeyPath": "m/12381/134/0/15", + "blsKey": "93bddb296ef4dd5c832486b4603c1ed13805d2df1c6c2f95c8af4ae38467f1e741c1c2fbbd5f8e927b54250bffdf8536", + "blsProofOfPossession": "923415dc1db9b46715d284bd2a3f12313a24c1352bf0dfcdce2e0e0475fe0343d5cc9e463d5f04b99cb367e30e89f1371280d5897a0103658d710b07f8d9d3d8754043241a753dce60f2bdadcb9249b334e6f5a395cabfdb187f2739b512d46f", + "blsPrivateKey": "21aa5cd0043608b6b020589a039bf5b66f32bd66c84f311f22c49a53c08d6b4d" + }, + "encrypted": {} + }, + { + "address": "lskux8ew6zq6zddya4u32towauvxmbe3x9hxvbzv4", + "keyPath": "m/44'/134'/16'", + "publicKey": "89e1bad75bed903096f63cfd6c27386f91b58910dd6fcbafcc66ac084b289702", + "privateKey": "b3552dadc9e7121c89f4a0eccdbfec423078af46a926913764c66496b3ed7fe689e1bad75bed903096f63cfd6c27386f91b58910dd6fcbafcc66ac084b289702", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/16'", + "generatorKey": "563aa06b554beea30fc4455ae51e0954051a3457315b2370fde9c22d3233b522", + "generatorPrivateKey": "34c7762f0fef6090c2832a3ccaf40ef373530e9930f46746d4e3f3236f627fe6563aa06b554beea30fc4455ae51e0954051a3457315b2370fde9c22d3233b522", + "blsKeyPath": "m/12381/134/0/16", + "blsKey": "94da5ec9da5eabf2ab184de1e0ee10f63f721897475acd59c3c53adc51a9b39b0f4fa28573fcc309e576dba658425dbd", + "blsProofOfPossession": "a672d269ec605e04065fc0da8e6f520d0273b1c57a754409d9fb25cef1be67b8583fa683e27c0284c31105045f395c0c142d0648420b9b209fa88fa13025ba2b3887e04e3fbae1db6e5941ade41713a4384c139e47e72a68c964c4a5c0886d25", + "blsPrivateKey": "651060d1b4a47d4f7c036e4649f84d42885db5ea5b4b26f04498ab805f4a2634" + }, + "encrypted": {} + }, + { + "address": "lskp2kubbnvgwhw588t3wp85wthe285r7e2m64w2d", + "keyPath": "m/44'/134'/17'", + "publicKey": "32ad0d0c9f9f5b2fa4605ff4c072ec4bcf2d64f0e0046fc9df247b5cad952a87", + "privateKey": "3c4fa6c215f89226083979c01be72633b7fdeae34a2679588dc6cb41cd811f8c32ad0d0c9f9f5b2fa4605ff4c072ec4bcf2d64f0e0046fc9df247b5cad952a87", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/17'", + "generatorKey": "56d64ef16324f92efce8b0a6ee98b2925dc485d45675b2012bbf6a96d7431a36", + "generatorPrivateKey": "a105df9082f9ab10633967414b3629bb9218587d8561dca4acde6fa414a890b956d64ef16324f92efce8b0a6ee98b2925dc485d45675b2012bbf6a96d7431a36", + "blsKeyPath": "m/12381/134/0/17", + "blsKey": "98f83f66e857d954d5c5a49403e5b3a622e1bb855d785845e72faf0f7dd03ed3fd2f787a38c57f6968accaf780fd41fe", + "blsProofOfPossession": "b3131f0229df11964daba47a79729542f10672b36db017002df90d2cc6a79c8b44d032935bd214bdf69a8db181e4315a15de71a2e6802442536143c3ace9886248d502d6f38f9ea5bad26d4cee729b909d6cbde541c35313598957ddda08de15", + "blsPrivateKey": "1a835401bf4776f55c3ef62c91506f5ae6a51343ab54e83179ffbeee53ad8e7c" + }, + "encrypted": {} + }, + { + "address": "lsknatyy4944pxukrhe38bww4bn3myzjp2af4sqgh", + "keyPath": "m/44'/134'/18'", + "publicKey": "4033f18959c6b6c51c5d60321691f462b491d00912c640d0bd5cd361e50758b9", + "privateKey": "2a7743838c3e637370fcd980a7f757d54b7ec2f417d339a384405fdcd0ac71724033f18959c6b6c51c5d60321691f462b491d00912c640d0bd5cd361e50758b9", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/18'", + "generatorKey": "f8d382ac4f19ffe2ac2fa91794b65dc4c03389cbb2ea65bab50379a12e0f98fb", + "generatorPrivateKey": "a7b7b85bab2f2d4471f3ff944b16ca636353f7d8af66f085d290ad14d8b62eeaf8d382ac4f19ffe2ac2fa91794b65dc4c03389cbb2ea65bab50379a12e0f98fb", + "blsKeyPath": "m/12381/134/0/18", + "blsKey": "b0d3f0d142131962d9ab7505a3ca078c1947d6bb2972174988feddc5d4d9727927ff79290af7e1180a913a375da9b618", + "blsProofOfPossession": "90f81a87982cb983aae8c240f12c77306501bf67dcb031161cb2787ecbecfdc0ca4e62365f750714b9b0a64c10411058105bef1a725ece1c0e7c45b7e1526494d5a02ceaa4f624116a91188e7ca2503e0ae17748b11b05cd79ccc204d20e418f", + "blsPrivateKey": "3f132150625f830a749f9d98639ecf79ef6796b22e31c1b3b0284961ea68fb37" + }, + "encrypted": {} + }, + { + "address": "lskwdqjhdgvqde9yrro4pfu464cumns3t5gyzutbm", + "keyPath": "m/44'/134'/19'", + "publicKey": "63b9114c5d10b1cb818e6c3b6e4adae2a3d95e1a32d78f2b2c31c02e41dbcbef", + "privateKey": "a8f11d66e15e48150ed4226e06090d308b87a52f1e3ef5e2ccf41320177830ae63b9114c5d10b1cb818e6c3b6e4adae2a3d95e1a32d78f2b2c31c02e41dbcbef", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/19'", + "generatorKey": "902b7ed4708c476c7f0e96825cb06f95cbc86953130575d2c4589d8e3dc2f69c", + "generatorPrivateKey": "922ac8b034a28c0941cf74105c9b3780d1a790b3321f163b203d678ef84d9c9e902b7ed4708c476c7f0e96825cb06f95cbc86953130575d2c4589d8e3dc2f69c", + "blsKeyPath": "m/12381/134/0/19", + "blsKey": "a397bb33263b2850758a1b144401b741c1278b302eb8d27be6c61363d9cedafcabe05fbd7d9ce5e75a7078972d397e9b", + "blsProofOfPossession": "b22ed60a951702ec7bfd85482e59703af76c4c79fe2d3a3b81e737d53746543587d2932fcd5559d56f6530bfe48d23f5093aa30f3e299733cb56151175d22e21895ada290521908536d71480f1066bbeec7ab803376a4a81e4d7ec3bb4d71dc0", + "blsPrivateKey": "0dac58ccfee182a3e2eeb2ca51ea8c8d9e7c5db1a6535fd3ef19b041096fa39a" + }, + "encrypted": {} + }, + { + "address": "lskewnnr5x7h3ckkmys8d4orvuyyqmf8odmud6qmg", + "keyPath": "m/44'/134'/20'", + "publicKey": "ca0ebbb82059cbcdabf64d9a69fbac54e1059c88a2c3edab7ea6aff700595f3d", + "privateKey": "5d574dc371a6503cbe75dd1c79a5de3b93c570d42f0b12a8b5edb8b265205668ca0ebbb82059cbcdabf64d9a69fbac54e1059c88a2c3edab7ea6aff700595f3d", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/20'", + "generatorKey": "bf5f4408df7a1cde279b3cfe7ba6c2e2600a4bb90d883b98ef8048ec344221e0", + "generatorPrivateKey": "bb82e9722b03ced00e2eefec45c84c54ec9a0627d679e02df5fe0933a1511899bf5f4408df7a1cde279b3cfe7ba6c2e2600a4bb90d883b98ef8048ec344221e0", + "blsKeyPath": "m/12381/134/0/20", + "blsKey": "81f3810e7567ba9e1aa9fab7d5914a1f2ac8b11d952872b398930836f80395c934bd6e71c291193458de7de4382c913f", + "blsProofOfPossession": "a67d9d0708496d13f45fa3d3940954bdfdfa69814554a5618a388cab03a5e82210171f06b72b03966c8a5bd8fe3b235e06de2fc4c45333395c8e10dba086a4f50efe3a7f87f741346c07b22de2ba49eedc521cf53fab31e2033175ff3ca00f08", + "blsPrivateKey": "28934cd2f129730f86b488c07bd390b67ae9642fb98c8c7d880bfc7daa44f863" + }, + "encrypted": {} + }, + { + "address": "lsk4nst5n99meqxndr684va7hhenw7q8sxs5depnb", + "keyPath": "m/44'/134'/21'", + "publicKey": "038d1b2d152be754c4140fa7386439a0b31ee8acf9d5d90cdbde9f39e1fd8ab9", + "privateKey": "e764112ca6647920370c68e381f82629356667db347d90fe9a3ec777c3151478038d1b2d152be754c4140fa7386439a0b31ee8acf9d5d90cdbde9f39e1fd8ab9", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/21'", + "generatorKey": "71ce039f0e4502ff56ca8d33f7ba5ba5392dd7915516b2d87eb777edef454377", + "generatorPrivateKey": "6e9ffbb5c17d86c3f54fc0c4fe8b48cbb3f7148dd8639304f94ed3be088f7da571ce039f0e4502ff56ca8d33f7ba5ba5392dd7915516b2d87eb777edef454377", + "blsKeyPath": "m/12381/134/0/21", + "blsKey": "a1a95b1526c3426ccd03f46199d452c5121481cc862a43bfe616c44662b9a7fa460fcdc5f97072754296e6da7023e078", + "blsProofOfPossession": "942c76c56af0112baa7a11bb8875a2336b321e85de56fd4267e97f3fb142445648a54c97ed22e5860fe5b0e5ef240599028d4009d091ad96ad727914532e45ff9eb44303b337f44bf5ed3ac796e6e22a9ee29138bada893f89f3bebc1a4daad5", + "blsPrivateKey": "11aa8b4f68e3d7c2c0d6081f8a207cbcb0dec199362e978aa8316e1a03410e02" + }, + "encrypted": {} + }, + { + "address": "lsksmpgg7mo4m6ekc9tgvgjr8kh5h6wmgtqvq6776", + "keyPath": "m/44'/134'/22'", + "publicKey": "e37c2947f15c02d4f6928aee7320c911ec269248f2dcd6e35f15d0e85e084a95", + "privateKey": "247b7f47bbf3be42e2bf801c6bf8c141973d8568239fd57d1ea7f3ce673bb8d7e37c2947f15c02d4f6928aee7320c911ec269248f2dcd6e35f15d0e85e084a95", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/22'", + "generatorKey": "f17b9b3bdee2ef63c8fb52d85ae07516133749a1d659bd032c3a078aca65ce7a", + "generatorPrivateKey": "48811bcc2a0c1cccdcbe7100863bfd435b904ad5607add183b43481cd1d19ae4f17b9b3bdee2ef63c8fb52d85ae07516133749a1d659bd032c3a078aca65ce7a", + "blsKeyPath": "m/12381/134/0/22", + "blsKey": "96aa1c639724f5559fb1ebbe5d218511fe0fbfe6681190cd953677c6b63c0e17ac5d9f09844845cfecbb4ab4bd5a5749", + "blsProofOfPossession": "82a60d6a2432fd15c7697094a89ed34a30dc2daa2b460bdb0fe3269362e1d85c79a3d2aa9ba3ffa5b1e80f983933c96f1402e95d34fb656d20f368428ba93539191319c70e6cf6f15c5cb9df9235d115d06e0e00d7a1bf64db1433ac6acb68a6", + "blsPrivateKey": "3aea7d1b6bb1026123989eca287cdd69d2caade596840b42c677ad05ef9fd259" + }, + "encrypted": {} + }, + { + "address": "lskf6f3zj4o9fnpt7wd4fowafv8buyd72sgt2864b", + "keyPath": "m/44'/134'/23'", + "publicKey": "2ecda8618228e5679127a028d832d344f658d4c6b654b1f44bb07c6ebed39568", + "privateKey": "38d40c0a9af6f4bcf6ef3ae1a4a2002c76dfacf4872664aea0628724c3990b392ecda8618228e5679127a028d832d344f658d4c6b654b1f44bb07c6ebed39568", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/23'", + "generatorKey": "8cab5125c910702b66a83240cf836b10a0f2dc3000536799300ed8f1ed9a26ac", + "generatorPrivateKey": "ee5bb2ad10169758a9adb196d5b038870e1f345f3f3588ff64bc6abc44e074718cab5125c910702b66a83240cf836b10a0f2dc3000536799300ed8f1ed9a26ac", + "blsKeyPath": "m/12381/134/0/23", + "blsKey": "92590fccb8c847a6957213682bb798d7d18a368515f070537e1f6cfd45d8dfc50863105db9d46189b92c0e0d009fe09d", + "blsProofOfPossession": "b0aa8214fd746ec04d9cc97e9641a7ad796ed12ef08c9227b5358cf3bd9f049af2ad5376055361c34d265e5d0cf3518d05113928f487bf17012d6ec4deb53e5112b72f2e4d8dc8eed4f68514a9c6bf735c9ccb9dade32ed589bea8e677135302", + "blsPrivateKey": "37aa79f3bad6f99cab62b65498dd3c1bb08efc8c99fca5e76d1ee65575a5e767" + }, + "encrypted": {} + }, + { + "address": "lskrgqnuqub85jzcocgjsgb5rexrxc32s9dajhm69", + "keyPath": "m/44'/134'/24'", + "publicKey": "7106c368f30be7c415f8259ada56e59d9af5a143ed0a03eb5988ae1a427d8ad0", + "privateKey": "8accd9d16d0a607b6425dd86f6d54e21f121919b66bc5b12157e861e8130e8457106c368f30be7c415f8259ada56e59d9af5a143ed0a03eb5988ae1a427d8ad0", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/24'", + "generatorKey": "55d4c0e745954f0fba9629b346055060418961e7edce58c77bf2bcfc7f753d42", + "generatorPrivateKey": "71ed13fc516989f54498bc28ed3b5119eef180666eb2574a07cdb56b492b876c55d4c0e745954f0fba9629b346055060418961e7edce58c77bf2bcfc7f753d42", + "blsKeyPath": "m/12381/134/0/24", + "blsKey": "ad250adf40b559d765bb51d65340fe38de9e4cbc839b6e6509d99bb9bb3f89be1bbb96d75f709f2ae9e715e6e6ce38a4", + "blsProofOfPossession": "8943f42818d3c3374d43d1aa0b427436f4edec3e760f07aea2990b99eb3ef69952d580df862ad9034062fab57c548164143bd3b77d16ae74fd8fb84518983dfd015146ac9d0503c858f0022591345c077656e5af22cc78f1d35a02ad1e74c8c4", + "blsPrivateKey": "0e4d854f9c5f345fea96ecb91625e50bf6bb69bb71016647574e71a7f2d762d2" + }, + "encrypted": {} + }, + { + "address": "lskjtc95w5wqh5gtymqh7dqadb6kbc9x2mwr4eq8d", + "keyPath": "m/44'/134'/25'", + "publicKey": "57162b1d7e5239fd93cc1f440d1493fff3582bc28eb14badf324e06756ed19f7", + "privateKey": "d0f245387c82d06e5595624ef96f13b8a0c1eb4430d6d606091afc4de365132e57162b1d7e5239fd93cc1f440d1493fff3582bc28eb14badf324e06756ed19f7", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/25'", + "generatorKey": "633e1696edbd9f2eb19683c4f7e0d4686fefb1a15772a1affdeb49a44d8c04f2", + "generatorPrivateKey": "c74dcc813c8011ef00936750155f3c06fae9382d25d716e81b9d35238f0d97a7633e1696edbd9f2eb19683c4f7e0d4686fefb1a15772a1affdeb49a44d8c04f2", + "blsKeyPath": "m/12381/134/0/25", + "blsKey": "a6e64df0d2d676f272253b3def004bb87276bf239596c4a5611f911aa51c4e401a9387c299b2b2b1d3f86ad7e5db0f0a", + "blsProofOfPossession": "92ff87e4dfebfdee0e5572e94f62c483a9b4465eada10c3a6bed32fc92374dbbe89eed00117ddb27bfbabc5e41d90d8a0701fd215caef0233eca660d7a0bccdaf064356edaab13aff404aeb5264d8b68ab0808115e09ef541168364806a62d49", + "blsPrivateKey": "3904de0fc9bcadab43d1b2d5f79cc197e59d96e99afa03da6acedac40ab3229a" + }, + "encrypted": {} + }, + { + "address": "lskwv3bh76epo42wvj6sdq8t7dbwar7xmm7h4k92m", + "keyPath": "m/44'/134'/26'", + "publicKey": "d22846c90b31913318a4e9d5e57cda760e1e35316d16fe8b43066c407c9b148a", + "privateKey": "fff5a4e22fb9473f23b9c8d5abe45175ccb2eae77710f8d99672280c685af3f2d22846c90b31913318a4e9d5e57cda760e1e35316d16fe8b43066c407c9b148a", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/26'", + "generatorKey": "f99c543eeba441fdb22c673fa81878269c3b69a6366d8d51fb6890f2eb3118b6", + "generatorPrivateKey": "0345913f3b2283ddb51285af6e9f2454fafe9d8f4438d5e60281b8753811476ff99c543eeba441fdb22c673fa81878269c3b69a6366d8d51fb6890f2eb3118b6", + "blsKeyPath": "m/12381/134/0/26", + "blsKey": "8ae82e86c2ae47fe55b3db422b5f6e8a8ecbf4a33a0e910b4cc53d1bef0d66e3d19e8474a97ba58e31798c604758b1d5", + "blsProofOfPossession": "9215a181382a5769652e3818238e58496ca1c80eb6282b000708b2c9c19464153fcc8a541d8aa32378186b61fdb2183d15828ffa20e49a0dae0cb05e8c106f894a7ee7190c6eb60874477da236c05a275187bded6ac5a9c98656eb2199f736fd", + "blsPrivateKey": "474a20eda00f30146da307c7bd171cd5b91ea5b6d44641d4677d39d9aa9bc27c" + }, + "encrypted": {} + }, + { + "address": "lskq5attbvu8s55ngwr3c5cv8392mqayvy4yyhpuy", + "keyPath": "m/44'/134'/27'", + "publicKey": "a1f052d86f89b7848e21eb71448d8c985a79c16e51ac7c76f72da5eb6480cf58", + "privateKey": "911fb9ae6147af11ee3fc36ade5a411a4c627d08eba07ac1d38c10855bfb2556a1f052d86f89b7848e21eb71448d8c985a79c16e51ac7c76f72da5eb6480cf58", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/27'", + "generatorKey": "1819bea0ff11aa0cde16c5b32736e7df274f9421d912a307526069fa119100ca", + "generatorPrivateKey": "9e5678be030e043e8ed9876ee4012cf293b95b44759d75a8a6ae8849901afc8e1819bea0ff11aa0cde16c5b32736e7df274f9421d912a307526069fa119100ca", + "blsKeyPath": "m/12381/134/0/27", + "blsKey": "957a970041ae9b29f33cd9baaf077f77049e664c8123b22fda3793252f71916c5df0b103ffad5cb75bdb2724d9ca3eba", + "blsProofOfPossession": "80d4fdac09ce195c9d685a751fb7cd9d4da7b9dc906348b4bb741ceb53f876afd0bceba75b36327a8cbd8bd3ca8ac2cc14b4fede3ce2cdac7f0bf0ad5e58840c64bdd0a0905cd6aa5da8acfcb33a931e469cadc27a42c2a04a62fd6ecca05091", + "blsPrivateKey": "1c73ac651be2f72f2be31639e6aad77493d00afa10b7138f60ab5d9da1abdb8f" + }, + "encrypted": {} + }, + { + "address": "lskdo2dmatrfwcnzoeohorwqbef4qngvojfdtkqpj", + "keyPath": "m/44'/134'/28'", + "publicKey": "ebd7440bf10d48e5d4601b5815b69c9d74fbdf9578db8477c94f4856b85a04ca", + "privateKey": "fa77c6df262210a67e6306b286b85d8fd77bed6fe33250c170e87e7cfdf0bc91ebd7440bf10d48e5d4601b5815b69c9d74fbdf9578db8477c94f4856b85a04ca", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/28'", + "generatorKey": "bbc7ca5acae1d53e0a44a212f4c77c7601ace0e489d936c0b6f26a9fbb03601e", + "generatorPrivateKey": "34d0d867fb2a43007f160ab304ca1d779871d60fca38e64e688d85cee4dd4331bbc7ca5acae1d53e0a44a212f4c77c7601ace0e489d936c0b6f26a9fbb03601e", + "blsKeyPath": "m/12381/134/0/28", + "blsKey": "82b478f1b884ee4c152490afc8b233d003745a58c236b00ecb3cea1022d59f04bf225266bbe5b0a5aa7da0a771a66acc", + "blsProofOfPossession": "ac4d05f93e3c374c83ab9cec2a5c67dff8a02298361584267968fad8f391af083b5041a020ce7a189fd8fdbf055a265c04f55e80a8dcf06e7b4e3358b347743f47d33bd5ee0cc4d4213995c46d6d4e1a61be929f571c1a0fa1c7dec805a85805", + "blsPrivateKey": "4fda60b27305f21237ae97d5f91c52455e10a242ec60997468b1d65d3f979d48" + }, + "encrypted": {} + }, + { + "address": "lskoq2bmkpfwmmbo3c9pzdby7wmwjvokgmpgbpcj3", + "keyPath": "m/44'/134'/29'", + "publicKey": "886ababad3572e81567a65320e1d4fca7de95ad69a305564be7625cfcedb531e", + "privateKey": "9f9ca7d38aa4db5b9a6e3c7f593f7862ca8cc87da5cdb0c88e3f3a45ceb882f5886ababad3572e81567a65320e1d4fca7de95ad69a305564be7625cfcedb531e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/29'", + "generatorKey": "8cda7b8df8975d781e053882a1373d190d5f8fd7c13ab528be8597b5d06ede57", + "generatorPrivateKey": "93771355236957f57b4bfabbc1d7e3c2cf72f5b0ef78e62471d455d44f13fffb8cda7b8df8975d781e053882a1373d190d5f8fd7c13ab528be8597b5d06ede57", + "blsKeyPath": "m/12381/134/0/29", + "blsKey": "882662250af65099ca817b2564576582981f23746f07be09ebc03ed6aa582a327d4156ff4a12851bce3ad77be854f937", + "blsProofOfPossession": "b73f34042d210b6cf0ba61b04e26bcb08e4d671a12df09e592c14c73ac55df09a01adf94b205b86a9ac9020cc719e93b0f890050891d9f8622346f45112ce502e26293a14c36501a8f1947c33fa38535d6eae6c4af6679296e76a105e899341d", + "blsPrivateKey": "130e7d4aedeaaf42ff9919b87496c80d0ef2cbe38a6e47ed7f7b8b4140a11700" + }, + "encrypted": {} + }, + { + "address": "lskgn7m77b769frqvgq7uko74wcrroqtcjv7nhv95", + "keyPath": "m/44'/134'/30'", + "publicKey": "c71ac98a32b133bc6fa8dbb6d42d87110f44fe4f3b74ca58fd60fa0d6010c285", + "privateKey": "83e39036d9000e4a92da3e96ae1a41b21d8ba158840447ac5bb7fc94db9bab9ec71ac98a32b133bc6fa8dbb6d42d87110f44fe4f3b74ca58fd60fa0d6010c285", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/30'", + "generatorKey": "0941ca2cfd9b1e0cc4bf0dbfd958d4b7d9f30af4c8626216999b88fc8a515d0a", + "generatorPrivateKey": "9b7b095990f701463a893d5534af10f3b850190ee94d3c5c114f50c82778a7bb0941ca2cfd9b1e0cc4bf0dbfd958d4b7d9f30af4c8626216999b88fc8a515d0a", + "blsKeyPath": "m/12381/134/0/30", + "blsKey": "8808cb1e4cb5c8ad18ad4a45e35388af4099993effb9069a28e56c5718944a3b4010ec1ef54b4faf4814fad854322468", + "blsProofOfPossession": "890995fe98a83721b0069aee00c2b264239b3b833b71f64a5f48b4340a969fbac1ffc0664264fbf5af626d37fb3fe6d403dc7ef0ec195cdab82e7615d73ad7a2d326a761fdcf18a6a83efc4f502c724a10ddd89f8b6981496c34b1b32f512781", + "blsPrivateKey": "00687a9dd373f8c15a883f678c6036273d34dadfb8236a840609ecbc67faa4b6" + }, + "encrypted": {} + }, + { + "address": "lskfmufdszf9ssqghf2yjkjeetyxy4v9wgawfv725", + "keyPath": "m/44'/134'/31'", + "publicKey": "81107e3e00332a827112444a1d53532e6e519acbf741ec3a58e318d6bfa05577", + "privateKey": "07ca3d10e8a88b2414ff218a849d8b66d84bd8e2290377f13b42cea907c77d7181107e3e00332a827112444a1d53532e6e519acbf741ec3a58e318d6bfa05577", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/31'", + "generatorKey": "24bab6ba79973ffaa8569af2cb69b8495d20f0c7ce674814ee0615d31abe9607", + "generatorPrivateKey": "58b70c32dea6cb47393427b3cb6c5581674e620bd771d946d4d05588c097749224bab6ba79973ffaa8569af2cb69b8495d20f0c7ce674814ee0615d31abe9607", + "blsKeyPath": "m/12381/134/0/31", + "blsKey": "96bed36ef328566d826a6f6b874ce441ad34373487b4bcc2d48d76f2dd453e418935a7b60578c43b9c4dc954e9331a3d", + "blsProofOfPossession": "b4d80456953b5111777a74931f5691a6e4c0bc4f4d552aeee9ed1002903b366abab12e2d596a4387933ec676058ae64e15d7b322786d19744281028753b621ed7d49b6e6bf87983267d3208c3dc5da983d845a7a2822da4a085446172e823b28", + "blsPrivateKey": "59c7cbf878eaf29c9e691f3c2d9bca2cf0fdec574bc037e1e156c730bf684b54" + }, + "encrypted": {} + }, + { + "address": "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "keyPath": "m/44'/134'/32'", + "publicKey": "9ea73410309a58c1f0c18d8821baa56ea2fd654215ae94d0e3ae808c7ad5e90f", + "privateKey": "64be15e273d24a39a7af8b674b6af47063c7db0b5ce61fbf9a1353e94a00cbfd9ea73410309a58c1f0c18d8821baa56ea2fd654215ae94d0e3ae808c7ad5e90f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/32'", + "generatorKey": "f07a86182356aee3fcfb37dcedbb6712c98319dc24b7be17cb322880d755b299", + "generatorPrivateKey": "6f3e9367328500bfaa95f7fd94e848fd6100f5e10bc77d439585185d20dea1dcf07a86182356aee3fcfb37dcedbb6712c98319dc24b7be17cb322880d755b299", + "blsKeyPath": "m/12381/134/0/32", + "blsKey": "b19c4385aaac82c4010cc8231233593dd479f90365186b0344c25c4e11c6c921f0c5b946028330ead690347216f65549", + "blsProofOfPossession": "b61a22f607f3652226a78747f3bb52c6d680e06a8041fc1d3a94a78fabf2895f23559059a44b0c64cd759d33e60a06060197246f6886679add69f6d306506336e15cdc7e9bde0aaca6e8191fb3535b5685ce8b3f33212441d311444a3d57fc66", + "blsPrivateKey": "4e29180852b97988e952ab7de895a55b14c283987a55f5df08cd1220b7d2df83" + }, + "encrypted": {} + }, + { + "address": "lskqxjqneh4mhkvvgga8wxtrky5ztzt6bh8rcvsvg", + "keyPath": "m/44'/134'/33'", + "publicKey": "7e4874d02ad84042e1fa3bfa61954d070308080f3cbecdf29d7fbfd66edb46a1", + "privateKey": "c17df1663305582bcc4b234e5de32a07e8c379970e101ffe3d787f082ed5f3d67e4874d02ad84042e1fa3bfa61954d070308080f3cbecdf29d7fbfd66edb46a1", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/33'", + "generatorKey": "a2b5e97ac5a5b3c3a7cd9b4401eca1f4e8da59fe567e229ea47e65bf40053402", + "generatorPrivateKey": "7e95bcfa2cb10e89f5036b3431446c5a55c115ffbe926443507943d48f8062b6a2b5e97ac5a5b3c3a7cd9b4401eca1f4e8da59fe567e229ea47e65bf40053402", + "blsKeyPath": "m/12381/134/0/33", + "blsKey": "abc1d1ef1f992a9fda45841079516169c879421f4260194c0a47e46afdb9f349c2a51e66e9f2ee8bf22231027584a6bd", + "blsProofOfPossession": "a16aa0fe3bfd5383c2fd874be4feb930f2c75f5d35d0e0ab314eb545a673aa1854ebfee7b15a026d5a9fb02842e54672149382f2898a0e12756bb949772b1316163ba774768c88fc90c2471afe94140d8d8f16974f2ebf050358cd98587b32ce", + "blsPrivateKey": "471a10414c7c89584cb2bf93a300426038301ce2b1197ab7f8752708beafc7e0" + }, + "encrypted": {} + }, + { + "address": "lskr8bmeh9q5brkctg8g44j82ootju82zu8porwvq", + "keyPath": "m/44'/134'/34'", + "publicKey": "1b62f211c18f7f707b41d0396f1a71ccfc7b27095728abb7aafda77c7d874857", + "privateKey": "6f11ae1da057f6681b404800e955b8b6ab43d742473f67e60af2e3aed04ff16e1b62f211c18f7f707b41d0396f1a71ccfc7b27095728abb7aafda77c7d874857", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/34'", + "generatorKey": "8062134a09cc464fe9465cda959b402a3d4506a1c44b3f5cba9661d42e912421", + "generatorPrivateKey": "daba1869775231db6c57d0d49ae8731693816165431889bb7506baad362d2ab58062134a09cc464fe9465cda959b402a3d4506a1c44b3f5cba9661d42e912421", + "blsKeyPath": "m/12381/134/0/34", + "blsKey": "a8271f9e8874eebb6d66dc139e984b6a6c71d2a7e23c6d7061bab7725e9c65f2e2123778130a2acd278f155440debde0", + "blsProofOfPossession": "84a3aeb2cc8329afc63f40d137b017ebcffe6df9e55bdaad8249408d01dad5025f1c83faecb53955ba5524df25b0d85e180f0335d0b5ac8c82c7f5fd0975002fe0231a83754c0034b07175afc426b17978870f8326cfe4694ff723e08d0b6a61", + "blsPrivateKey": "55416acd8c266c470540c3ed4abcbd22b1b936cffa4b8ce620bd9d8b63c0dfc8" + }, + "encrypted": {} + }, + { + "address": "lskbm49qcdcyqvavxkm69x22btvhwx6v27kfzghu3", + "keyPath": "m/44'/134'/35'", + "publicKey": "2a4aa6527e9f9bc2c3d3b4a9a22be543e95703593ed98989285e0b92ec6f3af2", + "privateKey": "134dad94b73ca57153ed7d9f37da7b94ae2f3b64d74a62e12524fe7bddf7c8af2a4aa6527e9f9bc2c3d3b4a9a22be543e95703593ed98989285e0b92ec6f3af2", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/35'", + "generatorKey": "4ec3ad70d3d35f0d684960e7938fab016d12c6c7cbb8312a8cff776dbaf2ca4a", + "generatorPrivateKey": "67bfc7dba3246b82db00c25ef844f5da3008439cefef1a9ee308accde7c7bfee4ec3ad70d3d35f0d684960e7938fab016d12c6c7cbb8312a8cff776dbaf2ca4a", + "blsKeyPath": "m/12381/134/0/35", + "blsKey": "80d7d0598d4e79ceea22c56d16e747cd5ef94469bd036945d14a5d1e06eb700f9f1099d10cfaddddf9e88ac4c9f1086a", + "blsProofOfPossession": "b7890264708b9d3341d90864f9120cd84090592a6bc5a419df94e86a638a0055e7dc3846cb89869cf46305611e49cea007711f35a5effd3099e56b5108a4103215a6ba9195c4694064ba661502e852b43e9593b0a60bcd2b567fc97565054500", + "blsPrivateKey": "1f7ad690ead2cbfc3d51e287d19158d2db2320c8498e72ff7ade0554383d0f01" + }, + "encrypted": {} + }, + { + "address": "lskatntynnut2eee2zxrpdzokrjmok43xczp2fme7", + "keyPath": "m/44'/134'/36'", + "publicKey": "8ee575c0773a3ec9164ad157b8de1b66fb30cc315e8ddb92d4f6eb007fe0f154", + "privateKey": "f012923591f4a0431781880c0adae26b162e035ffb3855e201d11903ba2d78cf8ee575c0773a3ec9164ad157b8de1b66fb30cc315e8ddb92d4f6eb007fe0f154", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/36'", + "generatorKey": "ce6bdb7380fa027c46edd15a072bbabd6b60fecb0e09589e20be560b333ca63e", + "generatorPrivateKey": "5b52fbe120967f200be5f0ba55608668cbe1a60b139f2aa646c0589fd295fcf9ce6bdb7380fa027c46edd15a072bbabd6b60fecb0e09589e20be560b333ca63e", + "blsKeyPath": "m/12381/134/0/36", + "blsKey": "97a4b205ac2b65a2f17ceb49a763393935021629068fe8a8c299e49b986e79ff8cc959a7343b5d00eae2783b825ffede", + "blsProofOfPossession": "8a86fbb8e59ff0de4f2d717ff3c7b0f3f9cb4b14f97deeffb907428666005e613b02cfac0bac4714389d898236de2d5a02df536b511675d2cbd37dcac6dc33bf4cf2d9d43cfa710b3c695bcb8cd29867477ccf3b1e5b9e3afaf7d8d4e50930ff", + "blsPrivateKey": "0fa3a86ad57f1ac10c478b2eea9c5379973316cd0484eadd1ba260da85ff908f" + }, + "encrypted": {} + }, + { + "address": "lskee8xh9oc78uhw5dhnaca9mbgmcgbwbnbarvd5d", + "keyPath": "m/44'/134'/37'", + "publicKey": "b5ca7fa887bfaab853a49e71c086023984c8ea089fd42ecf0a086810a2e6f78b", + "privateKey": "86afad2f4142a2d57e08fafaa6f1ed70af9a0831ef7d18e6ed89adaa61b66754b5ca7fa887bfaab853a49e71c086023984c8ea089fd42ecf0a086810a2e6f78b", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/37'", + "generatorKey": "aed740da1a7204422b92f733212398ce881c24a4cfe40edeea6a59a0f6453743", + "generatorPrivateKey": "71bf7039b3951c6742390e997201c7c5b13ad712f60f214846456c3f15342024aed740da1a7204422b92f733212398ce881c24a4cfe40edeea6a59a0f6453743", + "blsKeyPath": "m/12381/134/0/37", + "blsKey": "929d5be8abbc4ffd14fc5dc02ae62e51a4e8fff3fd7b5851ec3084136208ceac44366a7313447858e3814ddc4213d692", + "blsProofOfPossession": "88e7331baeba342eaa907cfd7a1b5bc839a70e78b0535d68c40ddc2e4d5157f8d1ff55d29243fe2375fcfef5c3a2133e0a0d11f8b58041278a1e9a3a9e7986f906201df48987e8f8eda2e6ee4452fe58b54805e2ca4cc256d8e42083b70f79e3", + "blsPrivateKey": "032de7290e108bb21cbd7e0084f5db140a2d365629b07cafea6c46a0c705775e" + }, + "encrypted": {} + }, + { + "address": "lskcuj9g99y36fc6em2f6zfrd83c6djsvcyzx9u3p", + "keyPath": "m/44'/134'/38'", + "publicKey": "1efe4983f0e29699afff6fa2917716b2599a88c23f21508b85a22f44c7ee1b62", + "privateKey": "9fd97aaf86fdd14e435e8b9356155d635e52fb7b885ea6e417cd7f8376720c761efe4983f0e29699afff6fa2917716b2599a88c23f21508b85a22f44c7ee1b62", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/38'", + "generatorKey": "80fb43e2c967cb9d050c0460d8a538f15f0ed3b16cb38e0414633f182d67a275", + "generatorPrivateKey": "c1aa3e4f44c0a57c27898b9055be4dc7d92b8ef0949ea812ed10eac89278978380fb43e2c967cb9d050c0460d8a538f15f0ed3b16cb38e0414633f182d67a275", + "blsKeyPath": "m/12381/134/0/38", + "blsKey": "b244cdcbc419d0efd741cd7117153f9ba1a5a914e1fa686e0f601a2d3f0a79ac765c45fb3a09a297e7bc0515562ceda5", + "blsProofOfPossession": "b7a186c0576deeacb7eb8db7fe2dcdb9652ea963d2ffe0a14ad90d7698f214948611a3866dfedcb6a8da3209fee4b94a025864f94c31e09192b6de2a71421e5b08d5ac906e77471d3643374a3d84f99d8b1315f44066c044b5cdbfdfeceef78c", + "blsPrivateKey": "0c629e3c91960c817e7993d8e2f7a567b1a704af52d08ba039b68b719bdd8247" + }, + "encrypted": {} + }, + { + "address": "lskuueow44w67rte7uoryn855hp5kw48szuhe5qmc", + "keyPath": "m/44'/134'/39'", + "publicKey": "6b01a532bd79010ee18fb75732356208d96c0524c257913b2b2ad903d55dde13", + "privateKey": "6e7c3feb90fb9f0d50d8892c491a60e9c165bc66c3e5e189f431977a0b6e7fdd6b01a532bd79010ee18fb75732356208d96c0524c257913b2b2ad903d55dde13", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/39'", + "generatorKey": "ebe1d6189c7015d175414db9621a602b0912826c1eb1aab09e69bb33ca8fcda5", + "generatorPrivateKey": "30af73eed356c281a256d2a8c94c3b0eb8676078bddc3cda67a1e8d42a44f3f2ebe1d6189c7015d175414db9621a602b0912826c1eb1aab09e69bb33ca8fcda5", + "blsKeyPath": "m/12381/134/0/39", + "blsKey": "b7c47fbb0d7e3793460949c9dd6120a310eb52de67f6cde55c022b05dd5053074c8a0e562896a482c787eb2eea82353f", + "blsProofOfPossession": "a265237ff848fe7acb4c84b6f68008ee7ec917a7a11c050f630b834e5caf22a447de94de0e7c52d03b18e003e5f9a3f2091cb5a78817ba42a7e19c714af47ad0b94824c5b90862059ed3042446143c56c4df011389eb42dfa2daa58df677d473", + "blsPrivateKey": "67cbba27c5ab5ef4f50f963cfa680bb745e565a7b26cd6a3755ece6ff0e238fe" + }, + "encrypted": {} + }, + { + "address": "lskm8g9dshwfcmfq9ctbrjm9zvb58h5c7y9ecstky", + "keyPath": "m/44'/134'/40'", + "publicKey": "4a4a974345c653a5f83e6f24f40ab4757bf07dc4f19d8070faa9852120f57549", + "privateKey": "80f077113e432f2360676c28392aad1f73012f62053c95e9fd411c9a3e9a32d44a4a974345c653a5f83e6f24f40ab4757bf07dc4f19d8070faa9852120f57549", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/40'", + "generatorKey": "497a5b80edc6b9b5cca4ca73fd0523dbd51e41c1af5f893e301cfa91d997573a", + "generatorPrivateKey": "4a7e5a09ed1049e59a3e3d10a27dca47b0f3ad8efbe25ba554de7e2e63cd522e497a5b80edc6b9b5cca4ca73fd0523dbd51e41c1af5f893e301cfa91d997573a", + "blsKeyPath": "m/12381/134/0/40", + "blsKey": "8e3f9dd02f46bbb01ec1ffbe173b6a28baa3ffaca943afe51c18dc5220256a3994cd0b0389c835988a64076b4e81c837", + "blsProofOfPossession": "980f00e7752adccb907eaea0fc31ce62dcaff9bf1c6b7066c5071829c91456a8d1e266cb0a9ef4916ffbd09295508a350d21e9123e5cc1c00d3ef65f5493c93c5b993e9768960d4210849743dc2b995657cb0aee7d46d6482e3545b89f06f895", + "blsPrivateKey": "2b67cf8da21f38b44a13674b270c912b50d3c74981d76e354558da1c1f2c829d" + }, + "encrypted": {} + }, + { + "address": "lskhamuapyyfckyg5v8u5o4jjw9bvr5bog7rgx8an", + "keyPath": "m/44'/134'/41'", + "publicKey": "439ad025289bc36c9bcaf79a04116d1cdc5ee87fd5ecb93be83ce761d69c7733", + "privateKey": "3f2353712bd5e51be220f1632571a451a9f357a4f7e292fbea8d9f7a52c8167e439ad025289bc36c9bcaf79a04116d1cdc5ee87fd5ecb93be83ce761d69c7733", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/41'", + "generatorKey": "d051790a70ffdf5bd80dc9cec003f8261128be1fc2135990accb13caeb3ed588", + "generatorPrivateKey": "ca0202c84b1675a89a53758e639447336b52042309014c9def9d84bdf5c5e229d051790a70ffdf5bd80dc9cec003f8261128be1fc2135990accb13caeb3ed588", + "blsKeyPath": "m/12381/134/0/41", + "blsKey": "a2fc837b51e6dd740fc1530e6713b0f8c04e646e91da849517901f24d9bcc78c360223f1ad3692de2e96444008a67e03", + "blsProofOfPossession": "82d6fee11dc1561ffb5f36bf07acdffb95e5c329f7adc0b8937bec191350d7c4a158c7592a179ed86b9c0e20159e903100495fcd3fb5bee481e053775b232f8e0fce602e8ec6edf0fe8ba90c06e6215d7c73e88a626d2fe63c6422826489d72a", + "blsPrivateKey": "1cc66f8abe734f69e212c028ddc5e8a5266f16bb92cbd23a11a2701374108a11" + }, + "encrypted": {} + }, + { + "address": "lskrzuuu8gkp5bxrbbz9hdjxw2yhnpxdkdz3j8rxr", + "keyPath": "m/44'/134'/42'", + "publicKey": "3de31d0eccc3e0d5c0a017a4066108ea909b7b9b97a046d55ea207b94d9f7570", + "privateKey": "67cecd53def499f8e0eb3c9cdbb4e330e2f5b4133e30e5f5398d40f966b8c0ee3de31d0eccc3e0d5c0a017a4066108ea909b7b9b97a046d55ea207b94d9f7570", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/42'", + "generatorKey": "d454f04eb0e05c980f6a3427e98d73493665860ba7a29eb915cfc0b8daae2849", + "generatorPrivateKey": "406b400c1bfa9d0462ef8fc4100a7f918c16a3823f1dff057cd7028d6865cfe9d454f04eb0e05c980f6a3427e98d73493665860ba7a29eb915cfc0b8daae2849", + "blsKeyPath": "m/12381/134/0/42", + "blsKey": "b1b4ba05e7116670be55b6d9fc28574d142824175a1e3d1cdafa37f193c342eba1a85d8520a9fd962811fe63a5a2d048", + "blsProofOfPossession": "99f7e39908f0cabbfd156c78a903d6968c455f5edbcb878525abe1217674d9745da87057f1fa93ccff79632253d5b4fd0c6301b0b9eb0e07fdd4c0abc99da0229ceb4a03b0da237657e445a7bbf6877689bfc027d65f24f05982dc2aeb34c72d", + "blsPrivateKey": "6cda6e97b66b400de912562e266710fe0df80ab4c6c9d91c9f2cf03e4e0a3834" + }, + "encrypted": {} + }, + { + "address": "lsk8dz47g5s7qxbyy46qvkrykfoj7wg7rb5ohy97c", + "keyPath": "m/44'/134'/43'", + "publicKey": "1261a41de66aaea2d66bc2b4ad5b7d25fbe013c11aae160bad70378b6049fdca", + "privateKey": "a80610578bf678af963bffabc131a791a590830abce950d15b95bae03ed5bd1c1261a41de66aaea2d66bc2b4ad5b7d25fbe013c11aae160bad70378b6049fdca", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/43'", + "generatorKey": "567e1e27c02293d7c190a1eb203c2daf1935a9901de66df73f8e4eeae6907d04", + "generatorPrivateKey": "72be4840bd46fc9566a1741499fce3fb9152e01ea28df6f1e834f35ba3d14f09567e1e27c02293d7c190a1eb203c2daf1935a9901de66df73f8e4eeae6907d04", + "blsKeyPath": "m/12381/134/0/43", + "blsKey": "a2f8fdf2b80c987ae61634125c54469928728ecb993bab3db892725b16b41ec48c36056eeee2a1c9b073d12bdf917684", + "blsProofOfPossession": "abded9f3ad588edba52b7b2a4b3ff25f630aefae0d7a91827bc1fb7b8cba36d27c310a7a58a4a66ed9a8d90ffc0aae6e17718b1fa3f8e7305498e740d531460702a7dce1e32c19e18849c786c26a30e29b464c7202dd64d021c1eef643de519a", + "blsPrivateKey": "2d11ddcb18798ed85425c100ee31309725153e3ddc769531dcc8939b9ba135b5" + }, + "encrypted": {} + }, + { + "address": "lsktn6hodzd7v4kzgpd56osqjfwnzhu4mdyokynum", + "keyPath": "m/44'/134'/44'", + "publicKey": "441132064a0a5cffb2d28f4306fdf4c784e6bcd0f72a8b0e2e70f11812afd9aa", + "privateKey": "50b8e65ecc714b5a02b3ad6e6769e4dbd8ed4b9fc87f2d0876f1c9d705af49ce441132064a0a5cffb2d28f4306fdf4c784e6bcd0f72a8b0e2e70f11812afd9aa", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/44'", + "generatorKey": "6158b2a5b662ce05c7864dff4c2aecf6109cdea1be703a79147450b082ea242d", + "generatorPrivateKey": "59e643809298d20fe0789fa76ce08a150c1d75602a8c5939b6dc468700ef2fc26158b2a5b662ce05c7864dff4c2aecf6109cdea1be703a79147450b082ea242d", + "blsKeyPath": "m/12381/134/0/44", + "blsKey": "a97efbc836dd4028813063912bcadb52fdb8e4d2ba04d7bbb477d2a97e16167c5fa6ba75e482cd7a7d476d78fed1550b", + "blsProofOfPossession": "995df23eececc27026f62816bfd07d71696e2dc5751bafb03d50bd9c66d388c562d6c1357300e4d51e5522edc3cb5ae217b3607795baa0209c6e63db01b4b7c28452c15db1366764abb9d886d0a908da07d3b7b2612e263d95721ffccefb4aa4", + "blsPrivateKey": "5b4e861123695a603833f8b442e474692b7b197e38c5be4a45a2e04244ed9582" + }, + "encrypted": {} + }, + { + "address": "lsk5pmheu78re567zd5dnddzh2c3jzn7bwcrjd7dy", + "keyPath": "m/44'/134'/45'", + "publicKey": "1f1b9cea61290f9b2380893ab949c6831315d6c2610371573de28cce16167595", + "privateKey": "d8165b1dbf9e5eb9d710739aaa552b4083d59f3a22c549b8141508a014edcc311f1b9cea61290f9b2380893ab949c6831315d6c2610371573de28cce16167595", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/45'", + "generatorKey": "62c37caa9ecdb3874354e7f780cb4463ad190bc31e75e552cb07b9bafc658f2c", + "generatorPrivateKey": "2ddf26bf710c8ed14e327cce8b8f5e196a3d43d731c1d007554f4d052edf5baa62c37caa9ecdb3874354e7f780cb4463ad190bc31e75e552cb07b9bafc658f2c", + "blsKeyPath": "m/12381/134/0/45", + "blsKey": "809c35a2a1f510fb574a223474fb6b588daca95ab1b9b04f4f0dcdcd4581f05914eb1b9683d21997899ebf730d82a8a7", + "blsProofOfPossession": "a2fd6eca6018825969d8b9de58e6594149c5114cea9c27997f2ec67b923cbe562454caa5a5e956b3eb5ea0c5bd9b0196137d4646e21b51bd21503dde474d510f62654bb7ffd141fa3462997bc6662f2893cff7d917eb07f2985dae860723bd46", + "blsPrivateKey": "692a0a8a17a80c888ef3ef9e5c7e5c11b6bf65250a03f3d22455a81c39480d6a" + }, + "encrypted": {} + }, + { + "address": "lskwdkhf2ew9ov65v7srpq2mdq48rmrgp492z3pkn", + "keyPath": "m/44'/134'/46'", + "publicKey": "9cdd0974356c09da1f6234c8f7e3ad8a08ba0e2828cbac81dddfc3f36d54ef11", + "privateKey": "510675c85299b7a430cabfab2b73a3103639c832b40cd42fa3fe6094c54353759cdd0974356c09da1f6234c8f7e3ad8a08ba0e2828cbac81dddfc3f36d54ef11", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/46'", + "generatorKey": "cc83f488c03e58d083927601658d234ffd12b5cb6fe3151206f699d031dc4161", + "generatorPrivateKey": "19d6c31d57d04b6861a868f0032d2e3f2788a06be4ee3642def28bbe1f3f3404cc83f488c03e58d083927601658d234ffd12b5cb6fe3151206f699d031dc4161", + "blsKeyPath": "m/12381/134/0/46", + "blsKey": "8c5b12f5b7aeafb07e14c5264e7f7ecf46b3ba0e6f12619e19271a733e06e913044ea2e5c955eef3567fcc2d842bc24a", + "blsProofOfPossession": "82237a5371179107af8c53ef19bf3e0d055b70ddb689763e0a8ac6d82884d12c2155166af4aa92b66fa64b6a6d2bbe7602a118d597345dc100bd6983f072b9d8da7bd0699b0f3cb51f1ec5a9f2e2feb76030125272325e7f5885399f1d26c5ac", + "blsPrivateKey": "379e94dcd6dad43376c0a0b2a4461fbcfe0bf25d99082a6000b8a52da62648c7" + }, + "encrypted": {} + }, + { + "address": "lsk67y3t2sqd7kka2agtcdm68oqvmvyw94nrjqz7f", + "keyPath": "m/44'/134'/47'", + "publicKey": "6200bdd255930cb10bbd1421d1a849298f1dc5e5dd8e8d00167bfa461745ed81", + "privateKey": "3df9184e5f715bf11494a223865c143376080ebaecd91dc8df2657e5593e52126200bdd255930cb10bbd1421d1a849298f1dc5e5dd8e8d00167bfa461745ed81", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/47'", + "generatorKey": "74f7ff53b55eda8fe9c11d66f7533c27714b121a5918a66c19b309e1c93dc3ed", + "generatorPrivateKey": "38ad961657b3d0e09b61e908362616bef7c86d2ea3b00b1f2f5b325d851ed35374f7ff53b55eda8fe9c11d66f7533c27714b121a5918a66c19b309e1c93dc3ed", + "blsKeyPath": "m/12381/134/0/47", + "blsKey": "a6d6aa277ab636486b7d879e90c541b4952264e18b8a214f58d32226fcc774a8e5bdac69223902424110cbda4ab58907", + "blsProofOfPossession": "a5b91b5e3881a36ea1b209f1cc09ab447e365b111e7529a88981e4e44c4a05eaee0507ff80460453e23187116510dc770d517e16aafc1de2aae2393ddd2e26cbe6fd096b65ba48cb6dacd0862d6c39b394117a596c0a1c9bae8d9b538d6e6dfa", + "blsPrivateKey": "0784ce0bba95107e6d4b8372f850e42ed3ea5f2a4cbc8931349bb6509e1e69f1" + }, + "encrypted": {} + }, + { + "address": "lskowvmbgn4oye4hae3keyjuzta4t499zqkjqydfd", + "keyPath": "m/44'/134'/48'", + "publicKey": "72d227ab88f971ed5da047f0a037ef302b8bb8dd3243f19bcf7f366484262a6f", + "privateKey": "b29ada34b8eea59af00ac9816ffbec398c2654ff21a7d95fc833d180b462ab9c72d227ab88f971ed5da047f0a037ef302b8bb8dd3243f19bcf7f366484262a6f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/48'", + "generatorKey": "f926fbec6d2e461af7c58d87754524abd26ab1f617d73348ba1318d371f7cac0", + "generatorPrivateKey": "f99b68a87d6a0fbedee01e277f2c9ac0381868fd48b3dbe91687cb2ae0b3f45ef926fbec6d2e461af7c58d87754524abd26ab1f617d73348ba1318d371f7cac0", + "blsKeyPath": "m/12381/134/0/48", + "blsKey": "ac304b4ad4fdac88bf975496edc43af0e324120984d5a12ac073b3e3e80c593470b6aa4f10b9897451bd6ee6f569a2af", + "blsProofOfPossession": "b08e154f3db163391dcbef182a63ad51d56521951307b9bcc60f12c83babeb5eef80b6d8503848acf9bc864adaa82bd610e3145dd77debdfcaa8e1e15f13e6da1d5bcfca4234b46208900c6ce35d0147534a7abc728504d731f286edc31a3ae3", + "blsPrivateKey": "5fba886b2e721c7d3165f301c3f6d3722e140f36b2e3b45a53999486bcef94bd" + }, + "encrypted": {} + }, + { + "address": "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "keyPath": "m/44'/134'/49'", + "publicKey": "8baada3c82ea9bf2dc8113c02b90ae5c461eec9329322bf0ed6cbeee104c1583", + "privateKey": "2a3ead5a95ca66f56dc6e4a0f65ee3ee56417b2b1535a93a5c05d2f3471d8a078baada3c82ea9bf2dc8113c02b90ae5c461eec9329322bf0ed6cbeee104c1583", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/49'", + "generatorKey": "761b647f4cb146f168e41658d1dfe0e9c01e5d64b15e5c033d230210f7e0aaa8", + "generatorPrivateKey": "2f672b0ced7c82df2ac79fece05ec6d580b41a4dce590cca6ce68670e6485993761b647f4cb146f168e41658d1dfe0e9c01e5d64b15e5c033d230210f7e0aaa8", + "blsKeyPath": "m/12381/134/0/49", + "blsKey": "b61f2da61bf5837450dcbc3bca0d6cc4fe2ba97f0325e5ee63f879e28aa9ea4dd9979f583e30236fb519a84a9cb27975", + "blsProofOfPossession": "807bca29a9eea5717c1802aebff8c29ad3f198a369081999512d31c887d8beba1a591d80a87b1122a5d9501b737188f805f3ef9a77acd051576805981cd0c5ba6e9761b5065f4d48f0e579982b45a1e35b3c282d27bb6e04262005835107a16b", + "blsPrivateKey": "69e9d76531c5655493d7711602556385a3f5bbfbb6bbcb7beaef2c9609f561cd" + }, + "encrypted": {} + }, + { + "address": "lsksy7x68enrmjxjb8copn5m8csys6rjejx56pjqt", + "keyPath": "m/44'/134'/50'", + "publicKey": "2dba645a063a638489186b825e0c9a9f03628b13e64ad79e9d813b8f6351a308", + "privateKey": "617f7f85f1969c785830105cf75d510d1f1ecf777d5a81468b019da740adb2f52dba645a063a638489186b825e0c9a9f03628b13e64ad79e9d813b8f6351a308", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/50'", + "generatorKey": "7fb2d69906c5076fa314a4e817ce424bbd4a7a21305cec93a12d31a1589dc90c", + "generatorPrivateKey": "324425049aeff2f1b885fc968c247931703e70b8836b789e3c3b05521e12f6ee7fb2d69906c5076fa314a4e817ce424bbd4a7a21305cec93a12d31a1589dc90c", + "blsKeyPath": "m/12381/134/0/50", + "blsKey": "8a08bdac4af80e0d37ce01094440a82a7e5ac9ec893f9a7870d26a4ec52db8932f36384bc7c3d3e03232ddb7bcd1eef5", + "blsProofOfPossession": "b999cf63290a85f96f0f78326c0eb24c3acce4c2307e1a2f1d621cc75f621ccab510e42aade9b6347e95661475230fbb059cd9e4e22ae17ac73dee58a370159bc6b525ab579de9502b761010e97f6d00f60ddfed05e76a5df3dfe33866c1ebe5", + "blsPrivateKey": "5eb911d435b193fac588ef12f503da2151ae4d0999a2c716a74b5596f56ef66a" + }, + "encrypted": {} + }, + { + "address": "lsktas5pgp3tofv4ke4f2kayw9uyrqpnbf55bw5hm", + "keyPath": "m/44'/134'/51'", + "publicKey": "a0e9cf9d02e72d6ca04e26605d6b271ab8cf0e1ce0f8a3381d7cea5d33774176", + "privateKey": "e794dc66cfffe91f218982d55dd702f1aaec240f660abcc3a46fede53afc26cca0e9cf9d02e72d6ca04e26605d6b271ab8cf0e1ce0f8a3381d7cea5d33774176", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/51'", + "generatorKey": "8307181cf9d1f621261e8a97a5b3b77d64a9a1f589a2c14e42b2380d9c2d6297", + "generatorPrivateKey": "2b9b806af478989e386268a7f0b60692c787c4595369ca5aeac9c69062165eb38307181cf9d1f621261e8a97a5b3b77d64a9a1f589a2c14e42b2380d9c2d6297", + "blsKeyPath": "m/12381/134/0/51", + "blsKey": "a77de9989b5fab42dca028637f401953b9e0fd6cd61dc2fb978daafdb5478ac77d67a37135c67a2178b44e5a35a1fddc", + "blsProofOfPossession": "acafd4f724cd7b9dcaf166aaf212122360f76c2faf4d146e8d0014653c0fe09f750690ea2b9ac6df96300301fb020d3b04c1b79965cc8929e18bd93190a366851033a901e05850770cb69fc28146db719f1ac232a7947ead59e8d584eb3ddb79", + "blsPrivateKey": "611ec2b3cf68944b55c1c6984e0117a257b8978b6e4db51627a92c0806ec335a" + }, + "encrypted": {} + }, + { + "address": "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "keyPath": "m/44'/134'/52'", + "publicKey": "1f40e49cb0fd9fde88cc854973379fe86610bec02dd2029de291080283967350", + "privateKey": "3fa4b30dfbc3fa41e7564edf87e11356b1518572ddd2b39b8ca527ffa30f15d81f40e49cb0fd9fde88cc854973379fe86610bec02dd2029de291080283967350", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/52'", + "generatorKey": "0cc6c469088fb2163262ac41787ea4a81da50d92fd510299ba66e5a2b02d5a05", + "generatorPrivateKey": "24473a6a678d3aec6ef7a75387591473d422d48af5b2db095e8417f3818b27590cc6c469088fb2163262ac41787ea4a81da50d92fd510299ba66e5a2b02d5a05", + "blsKeyPath": "m/12381/134/0/52", + "blsKey": "a5ca55e9a0ab81d48eaad2960bd3ea259527cf85fe62cc80cfd8400dbd2511725c06c3a597868dcc257bbc279e2b3e92", + "blsProofOfPossession": "a092cff10ea18ec3dcf3f6e41cd38537e00602e35107067ace7ab7c97a2ae1de531ebea7fc0c22e8dbcee1f981c439930c7cae474a996b153a66b0cb34e66c6041348aaeb4763413afffe0d947da90424065ee573b3683edbb1e51f9a278ae82", + "blsPrivateKey": "35d93ad8f5faa1e1cbe72ebb42bee49a2219c7d6e30c25742916db086464e8a0" + }, + "encrypted": {} + }, + { + "address": "lsk56hpjtt5b8w3h2qgckr57txuw95ja29rsonweo", + "keyPath": "m/44'/134'/53'", + "publicKey": "777fcc4ed76d3a3f1984421cd9be283e6f7e3d3197c8c753d200a1bcef04b0f2", + "privateKey": "c4f107bf103ff5b3f226f612fc0e80b957549051ff9d665ad8ab9fb1b5e29ffe777fcc4ed76d3a3f1984421cd9be283e6f7e3d3197c8c753d200a1bcef04b0f2", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/53'", + "generatorKey": "d19ee9537ed38f537c2e8be0fb491331575f8e4050dc4a74ccee3244714d5969", + "generatorPrivateKey": "806c6f33920afe19a27e7f677358c72417ae0a2f51766608b83e8c351015eeb4d19ee9537ed38f537c2e8be0fb491331575f8e4050dc4a74ccee3244714d5969", + "blsKeyPath": "m/12381/134/0/53", + "blsKey": "906653b7a74dc35499e0c02f10a9d092e7dae70e5376287b5533c7a52ade678784956e6bcbb67a11239bbfa977743a1f", + "blsProofOfPossession": "a5bdd92d340281c01d90224ca58a13cc429dc47ea9d2ef6226b023ff926a43ff0a50a82028e1fc20e9faa380136f5dde00a70d7170a8de3246e39b7787771e41271351dcbf4f88b6d40dac77b2e3324a371f9fc08d1fad90fe3e5cd61caae5d8", + "blsPrivateKey": "22cde771d9674061cdaf1040d121aec3e6911b1facc29a66cd869c72cce1642f" + }, + "encrypted": {} + }, + { + "address": "lsk8netwcxgkpew8g5as2bkwbfraetf8neud25ktc", + "keyPath": "m/44'/134'/54'", + "publicKey": "b85e4331ffa96a18e48980200bed9ea7abca9ed16f5902633db46d7516ab72b0", + "privateKey": "5fc331b9319c13b85921a395cadbd79709f19cda4ffbb220b6f8d9f8961dcfb2b85e4331ffa96a18e48980200bed9ea7abca9ed16f5902633db46d7516ab72b0", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/54'", + "generatorKey": "fa7af9f8623b324e6c021b7a0899d980a41dd2de86c35cab530751eaa9e55a0a", + "generatorPrivateKey": "39793207a2f6c4cd2e32c90c2a951ae37dff4b1bb392710a4ec14863ed838faffa7af9f8623b324e6c021b7a0899d980a41dd2de86c35cab530751eaa9e55a0a", + "blsKeyPath": "m/12381/134/0/54", + "blsKey": "a3aa25a2385666122df82fa74096f30560c270b1ef981ff459e25cb5819d50a2edd8c315bf17a6a1af8d88c0e9325e50", + "blsProofOfPossession": "b543e0716990a65727b51489c90495289bae983d3a4439fe68826c2175b4396d37da0ff03910b369335377de097088720b77646a3fdf196e95c54f2ca6bd414327231996bc2dba0c1dcc7a77b8be10b84a4ef8947a0e4ba22aa09a6c025521e6", + "blsPrivateKey": "16748b6923af2e11d23c14082cdec97c9259ea163e8c232760a5151795310d5b" + }, + "encrypted": {} + }, + { + "address": "lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk", + "keyPath": "m/44'/134'/55'", + "publicKey": "a716cd8c8361700c75fabd0dfb213b611ee0b819c0bd97b20432e92f614d25c8", + "privateKey": "e63e8439fde83b57cb7d9809230fb722c527914200a7aec07bf083af1ac2ba30a716cd8c8361700c75fabd0dfb213b611ee0b819c0bd97b20432e92f614d25c8", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/55'", + "generatorKey": "91fdf7f2a3eb93e493f736a4f9fce0e1df082836bf6d06e739bb3b0e1690fada", + "generatorPrivateKey": "f5f7d8320408c3e1cf03f7d0428d07abd6a21c9bead4255f2d7d9c52eed08d9691fdf7f2a3eb93e493f736a4f9fce0e1df082836bf6d06e739bb3b0e1690fada", + "blsKeyPath": "m/12381/134/0/55", + "blsKey": "a84b3fc0a53fcb07c6057442cf11b37ef0a3d3216fc8e245f9cbf43c13193515f0de3ab9ef4f6b0e04ecdb4df212d96a", + "blsProofOfPossession": "b3de21449917e17d5eadb5211c192ee23e7df8becad8488c521dcfb0c67df64a81561653d92805b4bebae9e5b5bdef8717f1259eaeb55bd1e7eafad3d74efe20181b4ac84bb7582b637e605fe78f10eb03b2a4acbff49809e86d89aebc6076b9", + "blsPrivateKey": "3509a406fafebe2fc14186370e6bf54bc957246902b4405efba31a381220c11f" + }, + "encrypted": {} + }, + { + "address": "lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6", + "keyPath": "m/44'/134'/56'", + "publicKey": "3f24a6c7a72e7158f3440d269f0e6e8c634f4afb4c7fdf0fd3645411b9996784", + "privateKey": "c466fff076de166acde289385af11ce2150090bf73edaa6e6ab0981365d550a43f24a6c7a72e7158f3440d269f0e6e8c634f4afb4c7fdf0fd3645411b9996784", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/56'", + "generatorKey": "b53ef930d84d3ce5b4947c2502da06bcbc0fb2c71ee96f3b3a35340516712c71", + "generatorPrivateKey": "3803f627ec148e6c38f91bfc22525d375abda4b339e92e17839f66f298526755b53ef930d84d3ce5b4947c2502da06bcbc0fb2c71ee96f3b3a35340516712c71", + "blsKeyPath": "m/12381/134/0/56", + "blsKey": "8d4151757d14b1a30f7088f0bb1505bfd94a471872d565de563dbce32f696cb77afcc026170c343d0329ad554df564f6", + "blsProofOfPossession": "90df1472d40c6d1279bc96b0639ff0b8ae8cef80a0538ef00b9fc3bf7816a541d2eb9349fb6a6f1a07d80504bdf105ac0726e6b01ef75a863cafaf5356dbc03ea1c90387f79d3adf15c8a44614d80e42e7a964df2eca83a871cd378f39513414", + "blsPrivateKey": "6c9825590e74d865175bee6b34b7ce3bc302dcb040fa8cb7880a052c0f73d257" + }, + "encrypted": {} + }, + { + "address": "lskduxr23bn9pajg8antj6fzaxc7hqpdmomoyshae", + "keyPath": "m/44'/134'/57'", + "publicKey": "aa07b8f76eb58b4c284e1a573a2c40f89019c7f37026ee07b33bc2807ce9f4da", + "privateKey": "7ae45bfd25e3a72e634374dd8aceb2c3fe303904d1685763af7021eefdcda13eaa07b8f76eb58b4c284e1a573a2c40f89019c7f37026ee07b33bc2807ce9f4da", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/57'", + "generatorKey": "9b4db295e88468a37e49445443fdc364321d620dc57afe8a5a14f07ce0717055", + "generatorPrivateKey": "1ecca92ec11addd0bce634823b07878229fc2b592a6ccc8fb5d824aa4a787bd59b4db295e88468a37e49445443fdc364321d620dc57afe8a5a14f07ce0717055", + "blsKeyPath": "m/12381/134/0/57", + "blsKey": "b067f711431b1bee09000b1c27fe39a29a5603471a6993d47bf56ece01a17fa4b00e92da90d80689ed2635e7e0f90891", + "blsProofOfPossession": "91f3d5519f94424fd59c120c05d9f2f34d8cb39e092e2a354f5a7d48e7f2e23b6a21b39a7a131954320d5dbeb0a419f10304fb857fae695c180f9dedd18ffa73082af5a6ca0c62c273915cd337570ecd8649157c8dc8836d758fe1e51f4faa3f", + "blsPrivateKey": "39df532310be25d730586eceeaa25ba14093c96facbec12a75a90bea1564dedd" + }, + "encrypted": {} + }, + { + "address": "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "keyPath": "m/44'/134'/58'", + "publicKey": "e23148e07a0ae9f9982a3d716821b8762fa0a50cb3cc18b6a7796aeb27e8a9b1", + "privateKey": "8f7a1af93d3ddcfb23124c9970719390847c13ece831e86924ed8cb7fa4cf7afe23148e07a0ae9f9982a3d716821b8762fa0a50cb3cc18b6a7796aeb27e8a9b1", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/58'", + "generatorKey": "73de0a02eee8076cb64f8bc0591326bdd7447d85a24d501307d98aa912ebc766", + "generatorPrivateKey": "9da05ad478e3b6cdda6143d579e8d4514085306b9874249ffce5cb49bd854d9d73de0a02eee8076cb64f8bc0591326bdd7447d85a24d501307d98aa912ebc766", + "blsKeyPath": "m/12381/134/0/58", + "blsKey": "8c4167537d75e68a60e3cd208b63cfae1ffe5c13315e10a6100fcbd34ede8e38f705391c186f32f8a93df5ff3913d45f", + "blsProofOfPossession": "929e7eb36a9a379fd5cbcce326e166f897e5dfd036a5127ecaea4f5973566e24031a3aebaf131265764d642e9d435c3d0a5fb8d27b8c65e97960667b5b42f63ac34f42482afe60843eb174bd75e2eaac560bfa1935656688d013bb8087071610", + "blsPrivateKey": "5eee5d9f688bbd779526348dc125c2d325a3e861f836fb9c0f96d2661fd0b8a0" + }, + "encrypted": {} + }, + { + "address": "lsk2xxvfxaqpm42wr9reokucegh3quypqg9w9aqfo", + "keyPath": "m/44'/134'/59'", + "publicKey": "06353d9f52953ceef0138ef8b74b5cfd180adb80c88ea2e389d7f35d38b5ce61", + "privateKey": "bd3629194f166f3f80a2f3f75d144ad52da1952b6e6244382cbb2b3638546ba606353d9f52953ceef0138ef8b74b5cfd180adb80c88ea2e389d7f35d38b5ce61", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/59'", + "generatorKey": "621d52ac19aba86c4feef94c67ae62cfa3f6ac192177ae37be2e6b3205449c0a", + "generatorPrivateKey": "a0cd4e1e5a506682fe0471cc6c28ad979ff8a99872236a02d552c9b036c361ec621d52ac19aba86c4feef94c67ae62cfa3f6ac192177ae37be2e6b3205449c0a", + "blsKeyPath": "m/12381/134/0/59", + "blsKey": "81f7700c2115434acaf61e88b836be11986476751d6c02617d1087e7bb45798ac56929cb5f71c890c6159ff4d71cd1b3", + "blsProofOfPossession": "8bc04a899be3a7ac99e2ddda6567a0b01e21aaea8daf4848821e8233cbe80610a2f670922865f424e878add1de8c978e1913f95308a50693fbc88e991e6bcac3bfef8a1d03f89bb4dfd9c991cbf1c613f85203dfacc4376057f085967f2a7283", + "blsPrivateKey": "08550cb1c6fafbef49a1e66cfb10d1db62eeb66402376cef0875ea0a528e50ad" + }, + "encrypted": {} + }, + { + "address": "lskfowbrr5mdkenm2fcg2hhu76q3vhs74k692vv28", + "keyPath": "m/44'/134'/60'", + "publicKey": "797138977ed2153364f00bd497162c957506ca8fe023bc25ed8cdcfdf8392b29", + "privateKey": "c68c41607847bdacb39a919de4d1e00ab8daf35ae0b9a7b4f9a3d6a4e7486330797138977ed2153364f00bd497162c957506ca8fe023bc25ed8cdcfdf8392b29", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/60'", + "generatorKey": "5812017e0d25131165ebc256f39ccece115fb58ad5fe0766f78054f912832d6c", + "generatorPrivateKey": "1433e065e36926ca8f4e74f66997fb917efab9855d7e49a4fa085e8d0c3dc24b5812017e0d25131165ebc256f39ccece115fb58ad5fe0766f78054f912832d6c", + "blsKeyPath": "m/12381/134/0/60", + "blsKey": "b57835b4d3285a134730de7b29361998787c2b4853e7a5e15032b516335e81c0797a51d00e032585efa05c27d2345a1d", + "blsProofOfPossession": "8d9b7510b3332a22635815b809c3e1ef96427a20f15b3f41112af74a9aa1a401d83d625dc5081f51aefee7591d52afaf1451e78e4f3efe29ec171b8239af73fd87b2e8a1aaa8b701c3e5bcb0d609f098738d29e0af57ea010953297c9c9e19d9", + "blsPrivateKey": "3731e7bfbaa3ffeb747497395b0a9354bf9677bdb503941fe3ec362ff69aaca5" + }, + "encrypted": {} + }, + { + "address": "lska4qegdqzmsndn5hdn5jngy6nnt9qxjekkkd5jz", + "keyPath": "m/44'/134'/61'", + "publicKey": "a8b8d44f041f77679c1a6566459642204ea60f44a4a9fa6bb874b022b5129d4a", + "privateKey": "841d84cae4cb700430490a5ecb153fe968b15739d286573bb6c5ce8ccd183555a8b8d44f041f77679c1a6566459642204ea60f44a4a9fa6bb874b022b5129d4a", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/61'", + "generatorKey": "965e86fdfcdcd64879efe23705506faeb4dfc4244f93d47f4bf444966d2a0f3d", + "generatorPrivateKey": "0dba4efd2e90744941a1733afa5d7316d9a0f2ee57b396c094fbc6f7e105242f965e86fdfcdcd64879efe23705506faeb4dfc4244f93d47f4bf444966d2a0f3d", + "blsKeyPath": "m/12381/134/0/61", + "blsKey": "90f87fd2122689c54bcd8fb859c5b36d4b583272043deba66199ad181ca2c38cf48d453c46ec881e03d2b7e2e63e3684", + "blsProofOfPossession": "add6eb668bebf90fdd80b01cb83a31b02577b200c85845bd5260d7851c02d21aaaf6d040e6d6f27a8690c9598f92ba240cdbb6d7896d7a777c484d30ab48d71b1aee1b07083dc5d11a94416c4cf85e33ec3899b40e6222ac888104f80b8d96c5", + "blsPrivateKey": "2d7d6cbdceed7b7b2dffd74c276ebf255f5df7d5e4952134da5d34d0feeb01cd" + }, + "encrypted": {} + }, + { + "address": "lska6rtf7ndbgbx7d8puaaf3heqsqnudkdhvoabdm", + "keyPath": "m/44'/134'/62'", + "publicKey": "d6b2f2bb26d71390e2df1df211bd36fa91fa437871923d007f3aa747e3bc9dbb", + "privateKey": "66eac1338aabb25c5d66bd58763c56dd439f255e8567ecde038a5e35bf3459c3d6b2f2bb26d71390e2df1df211bd36fa91fa437871923d007f3aa747e3bc9dbb", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/62'", + "generatorKey": "f8252b40a65be6f5f6d0be446da5ab434bdc0a921fd0956b0672ea4a218d2d7a", + "generatorPrivateKey": "b7ddf78c537e6a808236f5361496cb44be3ca2cba0f2c7e0a20bb068748e8578f8252b40a65be6f5f6d0be446da5ab434bdc0a921fd0956b0672ea4a218d2d7a", + "blsKeyPath": "m/12381/134/0/62", + "blsKey": "a94d3cbfde92550eccede718499df12f33a8ec9a4b386e4ca423161d667862f45fb06397b12dc6a6cbafc14b1cfad26b", + "blsProofOfPossession": "a474ee16d276d3478e1b7005960d41c0e271652f29c3178230b7fdf395801dd62196294b7695b3ccad63887558e0f27d0b121738a42cfe9acab07e6763577ad87eccb5b1d0cd725cb4a32225e79e864c238ce3c56b6db8960ce9fda82828d5ba", + "blsPrivateKey": "0d1e5bc7255af552aa839931ec5cdf194a0296bd070c4d181ff43467f4beeaa6" + }, + "encrypted": {} + }, + { + "address": "lskrga27zfbamdcntpbxxt7sezvmubyxv9vnw2upk", + "keyPath": "m/44'/134'/63'", + "publicKey": "09b005266e78ac5cfc18a3d304403cf141842bf58c50dd754f2a20b0a18331a3", + "privateKey": "e9a9bccf06cd7dda82c50bc34b2156c4d0834749c6769d3363c0009ade5dd86109b005266e78ac5cfc18a3d304403cf141842bf58c50dd754f2a20b0a18331a3", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/63'", + "generatorKey": "d2b31ed942359b0c9cb696cae874a2dbdd6e24915dd8a5882c7c042eac1e6831", + "generatorPrivateKey": "656a2e7db1f694fc6872fd1bfe2318503bcfd3dbd841a0de9170ef5da80ebfddd2b31ed942359b0c9cb696cae874a2dbdd6e24915dd8a5882c7c042eac1e6831", + "blsKeyPath": "m/12381/134/0/63", + "blsKey": "997583cd4f633aa5aa5e616a75d9edc370d5e6eb77e2418c13648b435b0182cdb7787c7ca91ed3939b403fe59041890b", + "blsProofOfPossession": "95324d44556e3c61bd307a40c2ef7f3d988e0ea561e5ece2d2809cf078db232caea9df8b35d8411238fddfe83a6978a70ae88e29fa5b6322b73f7fc9756daf52aa6369e5e69c5b2304871bd324e8125a698e360e3d5f1ad20136370b8d9808ea", + "blsPrivateKey": "24325a46b06e684f9cfb351a4f5a5a62a419754e1a77b8ca39b6814c20655c27" + }, + "encrypted": {} + }, + { + "address": "lskw95u4yqs35jpeourx4jsgdur2br7b9nq88b4g2", + "keyPath": "m/44'/134'/64'", + "publicKey": "a6279f18be02a54af37dc4228fc731e63219a289c1cfb1607b18adf685976f9c", + "privateKey": "384a6c7cc4f39a566ba8e016508824bd5f39d25b2bfdad5c66377e521edbb92ba6279f18be02a54af37dc4228fc731e63219a289c1cfb1607b18adf685976f9c", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/64'", + "generatorKey": "e2f80871a5220be51352427077f6e93c2294d88be6b731b535d2ce9371274e7b", + "generatorPrivateKey": "eb6e2fd2214a11149332ff01b5b823c96f8e85ddb2342b7a1c03a974111791aee2f80871a5220be51352427077f6e93c2294d88be6b731b535d2ce9371274e7b", + "blsKeyPath": "m/12381/134/0/64", + "blsKey": "a58edccfbcbc35d6f9fec1535329a114cc5a2118945098c0f201345ab7de78d36a32014dbe701faf7d32b24f7a696d9e", + "blsProofOfPossession": "999cf3232240944ff9a14e6c4680fae450be8c0ed43fdbf8f92e7873b5482f88229768fdcfd86e22767ec1df3b5fa2fc0b08202ee4a343bfb19c8c8eabf74d44fa73c4517ad0a102faf4ae6fe87cd766d860408b51d31dadcc5674c92908c7ee", + "blsPrivateKey": "6f6ab0c40cc4959ffa99e9a202496527eecaf86d489943abb7b24828b1c7ea8a" + }, + "encrypted": {} + }, + { + "address": "lskk2vnyd5dq3ekexog6us6zcze9r64wk456zvj9a", + "keyPath": "m/44'/134'/65'", + "publicKey": "e550523682ba9bb8d8856cbf4870fa86402a4b21a3205dc1296de556354c9586", + "privateKey": "d30db1751e16265341b23a8f9e66dd31628916b4123cb52057180f148f18e6c0e550523682ba9bb8d8856cbf4870fa86402a4b21a3205dc1296de556354c9586", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/65'", + "generatorKey": "7ff8b45c5f6239306af0194ee41e047669e33338be3f8e6c786d90fb905c8b6a", + "generatorPrivateKey": "befa9db63277650972e0ba0427c4e5c912d7376c3e9ce8924a3397678c0c77037ff8b45c5f6239306af0194ee41e047669e33338be3f8e6c786d90fb905c8b6a", + "blsKeyPath": "m/12381/134/0/65", + "blsKey": "8739c54fb8452db4ff1857649a4144dae29f7bbd3275aaa8f0f2559095a09510e38bb0155bd01d01349e7f1392132e41", + "blsProofOfPossession": "b78a813e912849e2583d6e774740f2bef3115f1d23576d206ba15bf0c64404b48208e7b2b5becfe2386fc1ad686094251707a7bf8902a10b8ffd207394ad26b64f7a0c5bb7bfc737fd836b160bf16c4d14dcc343dbc8ff7993391795ded7e448", + "blsPrivateKey": "03fb0362a91d49d5325eb3cf24970da76d434a1585108ccf49baa283651d361c" + }, + "encrypted": {} + }, + { + "address": "lskk8yh4h2rkp3yegr5xuea62qbos6q8xd6h3wys2", + "keyPath": "m/44'/134'/66'", + "publicKey": "b265367283f1d3955366d56c9055da26fb2df23bf81022a0998dad49bebf3e42", + "privateKey": "7ad7a0c9f37312088626a5367c1d03ed941f0b476cfeaedb47613730d7295149b265367283f1d3955366d56c9055da26fb2df23bf81022a0998dad49bebf3e42", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/66'", + "generatorKey": "db1c7c22ee495ad3553394dca00c62b85e78b58e78ca68bfe5027b3346f6c854", + "generatorPrivateKey": "893644ce73b8651f23cd00c7e012ab6d7447d8c4ddd609619442ef10c9948417db1c7c22ee495ad3553394dca00c62b85e78b58e78ca68bfe5027b3346f6c854", + "blsKeyPath": "m/12381/134/0/66", + "blsKey": "95087210c7145581fd8dc397ed12ecc2eb703eaa19dd837d7c8c54cf625ba00bf88608aa89170d703c77f7dcf6707398", + "blsProofOfPossession": "b09816fd6ec0b666e1f61bde72069057a11fc78d7fe8b85873b6d909aee15d74c637076e149ff279c587efa4e6a468900e2c4a857bc55978ea292189737f95e7026514ec5e9a117f31b8339d8becf3af1bd2555df6d8f2372b54b7381ff355ed", + "blsPrivateKey": "71b1abe986e2287ad69c55edb0f9c80336c5220cb31e2ed6c728a58a925d81ac" + }, + "encrypted": {} + }, + { + "address": "lskk33a2z28ak9yy6eunbmodnynoehtyra5o4jzkn", + "keyPath": "m/44'/134'/67'", + "publicKey": "6b0d646e18db8b55ac1a6f49a05f17cdb4880cf99fed2415f3076d6022d70112", + "privateKey": "69c69d0e1906a079416cd965b32aea01de7fca2cf838336d596ecf005c4b83e26b0d646e18db8b55ac1a6f49a05f17cdb4880cf99fed2415f3076d6022d70112", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/67'", + "generatorKey": "689639f5e3808cc0efd5f8d48ca6ee6f9a7a1bd5f5776832cc9b448cff5d0aa9", + "generatorPrivateKey": "0e064b38b2c1d1f3db99a14bf07a3c48138f0e3bed3fea0d0aaa4377535985f4689639f5e3808cc0efd5f8d48ca6ee6f9a7a1bd5f5776832cc9b448cff5d0aa9", + "blsKeyPath": "m/12381/134/0/67", + "blsKey": "a1dff3e7486e27eb2bc99d4343b57e06fb8b52f8c7b6ec6d539889afcf0c221fbadcfca65f2ad7351beb8a51e67513fd", + "blsProofOfPossession": "b6447c9e317179a9160ea0c11c2ff49c11e0300332c2c0ec0bf81e936af231ffc3b6628da3e01eda821ff15e9a523f3204b32fd4fcce988c2b73b56609709dfd25ec9df9e33dee073f9d26a82d268569d117ecbf7985e012a975fa7d3ad5e4fd", + "blsPrivateKey": "4ba51a2b3505cbde5211c1a46608e6cd4eccfc9f5d53e473927d9dc34e1ae5e1" + }, + "encrypted": {} + }, + { + "address": "lskrxweey4ak83ek36go6okoxr6bxrepdv3y52k3y", + "keyPath": "m/44'/134'/68'", + "publicKey": "4a96ff97a29898a3bae678346f38d1ed6ab7ae22db602d28e8de6c7b15f91c86", + "privateKey": "c9da3d67a88c09783e5f4aa5a0f15063dc11690e83dcae2d1ab838efd6b739dd4a96ff97a29898a3bae678346f38d1ed6ab7ae22db602d28e8de6c7b15f91c86", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/68'", + "generatorKey": "21120ef22b7df438e06b3862d3f0ab99d5704b3c61c45a544c64c908da8955ad", + "generatorPrivateKey": "1576f20a78dcd0be1a7ad4d6ad85f762b255c662f976cf3ae00486ac28664a0621120ef22b7df438e06b3862d3f0ab99d5704b3c61c45a544c64c908da8955ad", + "blsKeyPath": "m/12381/134/0/68", + "blsKey": "8422c22feba709265c30a7b86a9ee9832d6b32fa4c9dc091c390e1b15e278f9009dc5d70868a56dace1ff622e9e634d7", + "blsProofOfPossession": "871ed33b68172b0ce40a3ec98d6fa9b3fd77245c2c1cb7f1071101cb459d53b05fc0168597148f976ceb1ded71999da8094fd8783cf27d1e21f9b965164573c0ca849210bd1e99f4706ca6f43636f9ea535c333a36c4267a598dc58c7c7fc108", + "blsPrivateKey": "177461dd8db1a3800214ac50efeaf2c8a1ff0c6e14fda158219c795909aef58e" + }, + "encrypted": {} + }, + { + "address": "lsku4ftwo3dvgygbnn58octduj6458h5eep2aea6e", + "keyPath": "m/44'/134'/69'", + "publicKey": "0a9f66755890c7a3b305985e5a061726ef98e0b362228a3df8d478e6c1182d58", + "privateKey": "73dd75b1544474d94bf51584c5f9604b4a44408df83930720c2e030aafc56bb30a9f66755890c7a3b305985e5a061726ef98e0b362228a3df8d478e6c1182d58", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/69'", + "generatorKey": "894289ef63ad9f51868d06e700c5dc9cac7af2e6601a99449134926cfdbb4340", + "generatorPrivateKey": "75a5ae8b87cd93c5e27d59898421a59d20e11489e036d8c813a70f39f74641b9894289ef63ad9f51868d06e700c5dc9cac7af2e6601a99449134926cfdbb4340", + "blsKeyPath": "m/12381/134/0/69", + "blsKey": "b9dc37e370cdbab50fe906b675551194e80705f5549ec07f32b95b85ec1ee1b149d156e649ebe1eac57bcc2ce9db3e56", + "blsProofOfPossession": "abefcbf20c53c10ac15054527c2ca691994f0b5cf60444aef49ba4e39312774eaa073be6b887ca5792bbfd53adc7ec3d0b0f6b34ec8a8f2fb6708d5a9d3de242f5fcccc3c3cddcfc5eb8be5aa13c333d114c091f594736e7a43d7d9212d0063d", + "blsPrivateKey": "52943b813516a5a2c72e8d7c68ee11c8d4b0e52be6ded1e18bcfaae70fc558aa" + }, + "encrypted": {} + }, + { + "address": "lskvcgy7ccuokarwqde8m8ztrur92cob6ju5quy4n", + "keyPath": "m/44'/134'/70'", + "publicKey": "26e064c253e23911282d58b71d68e507b28e4c62f50db256b1babf649a65d62e", + "privateKey": "3a5f45a46b59f9017a60bde8f4c35cd6fe98fddb15ea40b149fbc15c29aee69b26e064c253e23911282d58b71d68e507b28e4c62f50db256b1babf649a65d62e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/70'", + "generatorKey": "83cca7ee3c7145d8022b54fab14505f6f65ed9ac933e3591de4a45d4f2298adb", + "generatorPrivateKey": "2f96617872a88de29161446d351382da43989ef67375ac840f434ad14b2b0ba783cca7ee3c7145d8022b54fab14505f6f65ed9ac933e3591de4a45d4f2298adb", + "blsKeyPath": "m/12381/134/0/70", + "blsKey": "87cf21c4649e7f2d83aa0dd0435f73f157cbbaf32352997c5ebc7004ff3f8d72f880048c824cb98493a7ad09f4f561aa", + "blsProofOfPossession": "92d1948d5d8faec69c6a389548900952014f5803f0eedc480e291bfd8fe6f31231e43fd4bd47817bdbca96e5104b92d2097df4362b94a583a1a24bbdd0382a681b5603d6b3bbfca854d5beccd45c2ebec24623666032f30fb3858b236bfcbd14", + "blsPrivateKey": "70d4a30e49639fd5e56b98f5c3aab01f775cbd7749b3543813aa5f9398ab4759" + }, + "encrypted": {} + }, + { + "address": "lskmwac26bhz5s5wo7h79dpyucckxku8jw5descbg", + "keyPath": "m/44'/134'/71'", + "publicKey": "5deaa3bebf3bb6ef06028679c43874bde94079c5fe90218926feb874236f7838", + "privateKey": "c57064b98f00dbae3e434af2055c8d60b55614e22b5dde66046b84d1ef0541b25deaa3bebf3bb6ef06028679c43874bde94079c5fe90218926feb874236f7838", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/71'", + "generatorKey": "a7340ac2220b35dd5c97e6ea45c48cfdfcaccc4c59abf9b7f316df8a1bd7e8b2", + "generatorPrivateKey": "0aac0c1c562feedc175e66b41f9cf4f874525f87a64063ff8cd3aa0b5039ead5a7340ac2220b35dd5c97e6ea45c48cfdfcaccc4c59abf9b7f316df8a1bd7e8b2", + "blsKeyPath": "m/12381/134/0/71", + "blsKey": "adeefe5ec24b210986ae56ac2d1eea5b5447e38d7c9657d4948ee2d9b312a247ba40964a58c3fc14e5fd7137602e631c", + "blsProofOfPossession": "8ffe03e68c8b3ec929a4934d61091ac1c8f42446076a7ef6e8141082ebf71fd3153c35c1745619a08defb0ca8fbe583a15190f88dbd93d22d3c4eaf3fd60fa2d9cdcd8824bdd289111ca7d537563b0e2fa7ad06cad40bc2ce17277a63a3138b2", + "blsPrivateKey": "3e6edc54aa3da90b6bb09e0ef243a6c8088050cb44d575eada89d8dcd11a05fb" + }, + "encrypted": {} + }, + { + "address": "lskqg9k3joyv9ouhjfysscame66hovq42yeev7ug7", + "keyPath": "m/44'/134'/72'", + "publicKey": "ba444a11029a29eea3046cc2bc6ed4cbdda38a80894ef6d0ad71af78f8fa9161", + "privateKey": "ebbb0c49f9f82b67003a96d9a53d295b0b4d4f69f4fbfdc3b777f2aaf68b621bba444a11029a29eea3046cc2bc6ed4cbdda38a80894ef6d0ad71af78f8fa9161", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/72'", + "generatorKey": "a9568912797914f590413c3156c9cff93c9c14193b01e7bf248195bbe8c1af19", + "generatorPrivateKey": "c128a9bd6b5e8e2edecaf7a82a03e7fc5097196cd8272b962572573285d40a21a9568912797914f590413c3156c9cff93c9c14193b01e7bf248195bbe8c1af19", + "blsKeyPath": "m/12381/134/0/72", + "blsKey": "86f828da4b3c129eb54d95bef7975281b30dd811f252b5792998718355c599aeca3dbb222678ee0af84b13f5af2400b3", + "blsProofOfPossession": "8e062f48ead9234b710dbcfebbb2e502ddff68e3d5be19a8e7e89b2141c76caeeae233999009f24f7b6e65f3774ef6cd09de9d5c0bb59a60ff6cb31b276f0172e35f89061f3c2d700543de5cf4d6e613ff6ba7d41c1379d6baefd844ef4cb517", + "blsPrivateKey": "545273aa4f588f3368a39d10f36f2b76d191c93ee01c35f348cb1357ce43e09a" + }, + "encrypted": {} + }, + { + "address": "lsknax33n2ohy872rdkfp4ud7nsv8eamwt6utw5nb", + "keyPath": "m/44'/134'/73'", + "publicKey": "7c3a54ed0b6a766af4069f53299fc2979eda629553c57d973a3e4aedb76a88cc", + "privateKey": "4c28bf9d8deb396a9db0ed5d08dda0e9cd9fddc08274a8d5c2ba357ae80e92337c3a54ed0b6a766af4069f53299fc2979eda629553c57d973a3e4aedb76a88cc", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/73'", + "generatorKey": "473d332bb27f1dab55191233884f37aaf17545b1883554b1457b2dfac7c02b0a", + "generatorPrivateKey": "ee95f0d24719c537c4a7c804dd8321a812499d97de85773a4cb7a38cff78ea54473d332bb27f1dab55191233884f37aaf17545b1883554b1457b2dfac7c02b0a", + "blsKeyPath": "m/12381/134/0/73", + "blsKey": "b29e90de05487e087cb37f34213ccc49edef8936aa15001686f947dd26b2e4c71b0c094c633067c75d3d0879c0347a45", + "blsProofOfPossession": "9866cd99328ae5d1a14f899b95782b828b404c941853f4d0f0f56a113867f9f44b177af5c6eddec16b42c405967e52c90e3c2b0acf4921fd7ad27bdca498980aec0d37923e95d56555190caed7644ac158b392af052a49a8d1df626ea3a5f034", + "blsPrivateKey": "5db5e9de794a02c507674c7092e742c70db374920078d08a77b156202acbf926" + }, + "encrypted": {} + }, + { + "address": "lskhbcq7mps5hhea5736qaggyupdsmgdj8ufzdojp", + "keyPath": "m/44'/134'/74'", + "publicKey": "e2fae8f54453c97775dd80a117fdb786852b52081d4a3f2ab1c58935a678e32f", + "privateKey": "1715d190aea38e22522d2ca170513fdb724e7b2f20799877bac79265e6775b0be2fae8f54453c97775dd80a117fdb786852b52081d4a3f2ab1c58935a678e32f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/74'", + "generatorKey": "29e5cf287cb9c12b2bb77ef9dc673728132f9e3affef2d0de0d7db7905937435", + "generatorPrivateKey": "1b6fbfe2da1efefdd35891902ef7963aa4ac8c918a7e2d44a253f96c541b74e029e5cf287cb9c12b2bb77ef9dc673728132f9e3affef2d0de0d7db7905937435", + "blsKeyPath": "m/12381/134/0/74", + "blsKey": "ab0bf8a74c846dbd47c9e679ba26a9c0e5a7a5902b4f66cee7065b7487eba30262e4e5f0ee78d616d007021df3fbc945", + "blsProofOfPossession": "b159e28ea39b1119e4018ea19777497e1d3c4a58d1c2ecc22aa5b2efe60572cb32ff30bbeda9ce28b235fb55ab15aec206f094f37ff9a78a0931d55799c1c74a19bacfa8a4172ba078d7cad4f663a4708e47981044b1893c712c3707196451fb", + "blsPrivateKey": "158e26816907da1dbfb1a7d6c4d10c38c73bc4365883dac8fdcb5b58eb4f0eb7" + }, + "encrypted": {} + }, + { + "address": "lskbr5cnd8rjeaot7gtfo79fsywx4nb68b29xeqrh", + "keyPath": "m/44'/134'/75'", + "publicKey": "7a0cdc2106afb1bdb3cecd23175287bbcfc97225e1a775a687f97a342e9a62a4", + "privateKey": "f5b4d9ca72fc037e4f6bc8abcb454a6b336bd9269011432c3d7726e095d687b37a0cdc2106afb1bdb3cecd23175287bbcfc97225e1a775a687f97a342e9a62a4", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/75'", + "generatorKey": "552ea15981e9fa54f2b65c409e8d32c350435893744fb9937875b1ec0e3025eb", + "generatorPrivateKey": "888faa5eba1aae717ef317909f53fe87c95b0988ab079aac6fbd456ff1882f55552ea15981e9fa54f2b65c409e8d32c350435893744fb9937875b1ec0e3025eb", + "blsKeyPath": "m/12381/134/0/75", + "blsKey": "968afa71f5ba87783db371242b48962a93c91f17ec6fe2b52260c43b7db62462fc88de889445390024abbb1de1ff87ee", + "blsProofOfPossession": "b3a05e96a9fc1ba05cb80ba48e8f92e6d6d282408d77b16557dd0c8bff8bc963539d5a355cb1544e35269c4fc58f5c0816b4bc3e215d6441f06b9d2e6cd48ad5f08c5bfb35f359fe25ebcc382985bcefce0698bd3a89e655706e46e394c83693", + "blsPrivateKey": "5e5a64d90e0995efcae6083bf22d0cc3b40a9e9c14e9bbe8ebb8f0e534365ce6" + }, + "encrypted": {} + }, + { + "address": "lskq6j6w8bv4s4to8ty6rz88y2cwcx76o4wcdnsdq", + "keyPath": "m/44'/134'/76'", + "publicKey": "529ef3e0a77482bc7b22d3308833dc30a50e230f74dee3a62987ae4f9867ed5a", + "privateKey": "752ef6fd81a5f932022291c51e1fd6409e5765600582b2d3d563e952c88e116c529ef3e0a77482bc7b22d3308833dc30a50e230f74dee3a62987ae4f9867ed5a", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/76'", + "generatorKey": "6c99048cae450de8735dd410a5c8b0e4655afaebcc2c155503f890af51e067c2", + "generatorPrivateKey": "627f7390b4c6a2e4426e40e8fc35742f9c72fe14d537faacc992c5d4564805fe6c99048cae450de8735dd410a5c8b0e4655afaebcc2c155503f890af51e067c2", + "blsKeyPath": "m/12381/134/0/76", + "blsKey": "95274c1b15467d43a3b8a3a632a8fb7e1a2efbdf92559ef52ea6ff1b0ba1c7cc2f75ef357b2dc7f0130dc9c04aeaf4db", + "blsProofOfPossession": "a24ef42b04be7bcd65d8434b04f7118bf9566a0d3a36c732cf5b508ccdc12855754663bdb32c5d871eee8a0774a1331a14f25f3aeb6bddee7efaebd2214e19b7cca9f3d3bc7eed93b85b15f0a626117f24361d65688dfbe7267141f13d323d63", + "blsPrivateKey": "2746cbe68b23a69706e0cf73dfcf1ce9a8cd0bde00fcb07d5f611020747fd20a" + }, + "encrypted": {} + }, + { + "address": "lsk3oz8mycgs86jehbmpmb83n8z3ctxou47h7r9bs", + "keyPath": "m/44'/134'/77'", + "publicKey": "28c6e872795eec98a1475aad17e78f8f47baa1794a5226334f7a89ac0911be44", + "privateKey": "35b4345634c91e8ef15d6ce6d3a8038effde85dd1defd8ccc4075a313837c79e28c6e872795eec98a1475aad17e78f8f47baa1794a5226334f7a89ac0911be44", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/77'", + "generatorKey": "b9bbcd67194a7091a517faf37a7ec0fda068c4ac0dcbb8ddf526de97e67716a4", + "generatorPrivateKey": "bbdd4ce2c5eb36fd31682db37f725c02b29ef7847f5485c8798262145c607e4fb9bbcd67194a7091a517faf37a7ec0fda068c4ac0dcbb8ddf526de97e67716a4", + "blsKeyPath": "m/12381/134/0/77", + "blsKey": "8ffe1e957047e7dd979e8bcac9fcea9411ed3be947679ce26a36725b08da51ed2fa19e7f7c6bed701bf3e33a6f787b8a", + "blsProofOfPossession": "89177926eb5ed8d2be150884e0cc4eaf02a040a3ebb0af9df6922d8d7fc58da4777cc6591d3d43570ce6410077d087fe097cb30f28a164d22216859988f44ef88bc7f4a2134f882d044e4ee66d135a31cd063934cf6b4e820fcff3bbfc5b27c9", + "blsPrivateKey": "04431be991b3beb33410c5f95fd52dce7fefcac451c2dfac73562f9b439632fa" + }, + "encrypted": {} + }, + { + "address": "lsksu2u78jmmx7jgu3k8vxcmsv48x3746cts9xejf", + "keyPath": "m/44'/134'/78'", + "publicKey": "6643c7547befc7c019e96b6a3d1ff738cef395bedb5338318efdb5a07a16d259", + "privateKey": "f43e8314b17e5ce791cf07a9a4cdd21688495edba6a65e838e0641e9c974a5786643c7547befc7c019e96b6a3d1ff738cef395bedb5338318efdb5a07a16d259", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/78'", + "generatorKey": "37df5572ddb12b67b9aa5191ba9baf9d76a50307fbe188924766225d86958dbd", + "generatorPrivateKey": "5b65e4fdcce39eaeac5a4216ed37e62b793a5eb62eef2a1c28007c0db5826cfc37df5572ddb12b67b9aa5191ba9baf9d76a50307fbe188924766225d86958dbd", + "blsKeyPath": "m/12381/134/0/78", + "blsKey": "884b03c63f8d095165b67cb23131ca1053cbc73739549aa2ee21ca0b2b925994855dd46a81ebc3dedb309ceadd013f8e", + "blsProofOfPossession": "b4879cd844644b1a21f1676bf671854afb1536c5a330c1fef26b2669238efa373f70815e01028506b5cf6b75fe77e79e0efb6ef74e8111c7f1a189d4b0bf4c867190aa57e670b53dff5951a29eaaceda788ed674acdf33eff228278dc61c3cd2", + "blsPrivateKey": "0702deacefa1cedc12296f4fa5ceb618dd4f481a0f86adde2a7ae292a4da68e8" + }, + "encrypted": {} + }, + { + "address": "lskaw28kpqyffwzb8pcy47nangwwbyxjgnnvh9sfw", + "keyPath": "m/44'/134'/79'", + "publicKey": "b9fac5757bfb5f0fffb3825958f1cbfe0359d128df881ca191af00fd4243ef6c", + "privateKey": "fc34f5ee0bf978c4cd98583f6c789909bf63054535da80d388356722b63ac88fb9fac5757bfb5f0fffb3825958f1cbfe0359d128df881ca191af00fd4243ef6c", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/79'", + "generatorKey": "bfe46727c386585d8d59c02efbe48d4c1a919ff07b87267156ab96e10ac730b2", + "generatorPrivateKey": "eee04fe7d9fd8f4f6710ed5b98672707cbafd9f3a8d9f11f399230686fc5ce46bfe46727c386585d8d59c02efbe48d4c1a919ff07b87267156ab96e10ac730b2", + "blsKeyPath": "m/12381/134/0/79", + "blsKey": "b279e1a3a5edcd1045682e7029045b70dffbae55c49b14391b9f776750193269b4fd1d9f0807d9ee66e264e08ecd97cf", + "blsProofOfPossession": "83a5128e710b91ab91f7726223120b389c1f77735c9c1d408c466b7f0484b020f0d2d50edc36d49e410141d8a509b132059142e250f145810eefce03dfdda25aa84214d30cdfb6ca11a929337bf53dfe4c675117c06e4a67206119ed1e2b2b9a", + "blsPrivateKey": "6837f740126f55e5a1ecbba4d8281c171c73ae1f20e5efe54d6b6a5da2cca543" + }, + "encrypted": {} + }, + { + "address": "lsk7drqfofanzn9rf7g59a2jha5ses3rswmc26hpw", + "keyPath": "m/44'/134'/80'", + "publicKey": "1f96630d57c8ceb77d50e80931148d2fb8c66ab5d5c030f35e6fdd3bc3f0af78", + "privateKey": "8d9919a3df297df65b2f0b4565b405374b472e6d1933d790d1f0f81f841303c11f96630d57c8ceb77d50e80931148d2fb8c66ab5d5c030f35e6fdd3bc3f0af78", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/80'", + "generatorKey": "71d5b4b08ea0b7a0ff95f779aec53590a3bcb5a87fc770334f8c9ee57fdd79d9", + "generatorPrivateKey": "8c3f82e435cd1f5de4dccc93740243bb8b87e4cacb9833a8124f7016e35607b171d5b4b08ea0b7a0ff95f779aec53590a3bcb5a87fc770334f8c9ee57fdd79d9", + "blsKeyPath": "m/12381/134/0/80", + "blsKey": "a6d6315e85e8138de21f94d0c5c6f4c2515d493b17653156745155b25f9f121f6d13e7c36a57fa5002a9aa0a0b282394", + "blsProofOfPossession": "ac38044b8d84ed22d42da3a240b7c2dd16fbdf3b03655226b46b6eea46256a3ee33232771d67da1a4df6717476349647077f5cb29715333d8c55f5b6ba70c77af1944ac54c913445da29c99dd441e36d9def69c0e9709ce062ac70e4d15628a9", + "blsPrivateKey": "414e6ea6a1cdde39a74d5d4f4debed95fb523099ee5b50da5b12579bf62a7beb" + }, + "encrypted": {} + }, + { + "address": "lskayo6b7wmd3prq8fauwr52tj9ordadwrvuh5hn7", + "keyPath": "m/44'/134'/81'", + "publicKey": "af72e830f5beb4f4947f9b34574df647ccd1c2047a67f36b288b51c17a4b926d", + "privateKey": "5de29c553a012a687761d0716008b865985684796068682590d15257d258c779af72e830f5beb4f4947f9b34574df647ccd1c2047a67f36b288b51c17a4b926d", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/81'", + "generatorKey": "5ec5a5a2c91414f5cc5e3354b58671e624bc88a39fdc8f128593daa06545d6cf", + "generatorPrivateKey": "ed2c37ad4313b5b994299586dd207e22f061dc2dcac3fcfe209a2242aa96f1e35ec5a5a2c91414f5cc5e3354b58671e624bc88a39fdc8f128593daa06545d6cf", + "blsKeyPath": "m/12381/134/0/81", + "blsKey": "881fa9b753cb2f89d267e0615cbd1ad9664d331f21d89cef2131686b0af55112fe1ad4df7f2c085f78142e75d90d2cab", + "blsProofOfPossession": "898471d3356573d6445906d973f1876f1e38570b6dc9c875c88138b302806c071efbe327f66c6646f02c134c3b1b019d0227bc83acd0ca10f65adf1b8fad7c9cb383909a015fd1d678c6272e5317da58d45b89fc1c954641a61169bf1c1a1728", + "blsPrivateKey": "13003be69f241b8534150263ba8842d41a795e644f6ccfb074f0f40a2c2c5b55" + }, + "encrypted": {} + }, + { + "address": "lsk966m5mv2xk8hassrq5b8nz97qmy3nh348y6zf7", + "keyPath": "m/44'/134'/82'", + "publicKey": "2dffbbb67b9fbce2146f5ce4778d237e7081771c0094b4e0774782509a7dfb6e", + "privateKey": "df0b951a2aefa073080cabae402057853e9b8ebc862b6e298fc0899e0153bdef2dffbbb67b9fbce2146f5ce4778d237e7081771c0094b4e0774782509a7dfb6e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/82'", + "generatorKey": "875d9a84adcf997034d5ab6189a063d9817da3a6c8599cc46c84b70b5081b18b", + "generatorPrivateKey": "25a3d63c742c8b5fb168cf2c8af45a8778fee8f87f709279bd9d35d7cbe6c4ad875d9a84adcf997034d5ab6189a063d9817da3a6c8599cc46c84b70b5081b18b", + "blsKeyPath": "m/12381/134/0/82", + "blsKey": "b847749ece25a2ef51427de371b4efc2342fb38a2c5822b941c1dbf43c3f8dabf5dc0e1620d2bdafb597d697e30ab801", + "blsProofOfPossession": "831a557a972e0ed1a9cdab88a13fea899ce1b7e6475ee2d42a1a1faa09fe9042eaab3bd8b14f2faf4ecff84780b8db6719e8d6bc8917ada1f77182b2fb4a40b544c02486fe0394b8fcc72ac69fcdf3d6c0920469225bf0ad2e047fc68b9376a3", + "blsPrivateKey": "6a934defd6cfe5fc5936d88349dd6a89afb2e8607d1f0c78f6526f5ab363a4d4" + }, + "encrypted": {} + }, + { + "address": "lsk37kucto34knfhumezkx3qdwhmbrqfonjmck59z", + "keyPath": "m/44'/134'/83'", + "publicKey": "958708971b228881efe4180d3c2ca4037fb97a2292dc23f6d8a1ccc433779f7d", + "privateKey": "4dd2f4daa47f5ab0443fe7b781d637b409c6613c0129bf6bfb9882c09f202bed958708971b228881efe4180d3c2ca4037fb97a2292dc23f6d8a1ccc433779f7d", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/83'", + "generatorKey": "edec02268c216d131fa9ec045049e6ac1526f48da772a34b1536c88c5af223da", + "generatorPrivateKey": "3a092f3763a23f8ff72b4f9a11075d385bed74bdd2d3c16c14e742ace9d7e28bedec02268c216d131fa9ec045049e6ac1526f48da772a34b1536c88c5af223da", + "blsKeyPath": "m/12381/134/0/83", + "blsKey": "94c8d9240de83f6b09905756fae29c2c3aa9092649776ebe037f20011b3bff835944eae63b2dcf6c3861f11d457a875e", + "blsProofOfPossession": "9900c9235a0365b9a0b5dce686903737cc4aaa76e8f9e47367954b07ee3a0c0ab51351cd746966556ddcc53e69eabe0c025195d1d3a6788d69c1820bd1fecc096eea09770fe43f86f898c6182ce3057fcd52b43ce096a07b4da3f2369353988e", + "blsPrivateKey": "07324357227d9af227a9adc8365933b1a0799282e033f2ad85c39e80f4a7e18a" + }, + "encrypted": {} + }, + { + "address": "lsk5y2q2tn35xrnpdc4oag8sa3ktdacmdcahvwqot", + "keyPath": "m/44'/134'/84'", + "publicKey": "1556035a614d4560066996288ca75dbcaeb5bfbffe935da23208cf8fb1d30157", + "privateKey": "fd45b5940c96ea5873baf5f5253eb214477023c63545dc7d5b281393de9aaa8a1556035a614d4560066996288ca75dbcaeb5bfbffe935da23208cf8fb1d30157", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/84'", + "generatorKey": "4ae9069cbc0e2371b037342010c5ddbd9c6d4a8c8d0a9eae59bc6a3796866119", + "generatorPrivateKey": "16d9d5a00068bbf424aa7e9d660a0993b4a260bffb25907799175a8a9d8896ba4ae9069cbc0e2371b037342010c5ddbd9c6d4a8c8d0a9eae59bc6a3796866119", + "blsKeyPath": "m/12381/134/0/84", + "blsKey": "b8396076f1ae032b572145f01ea0a3b5418f226afb0496930cb68250ca59b16fe2fb6dadacd88132b9dcd19a07d7f773", + "blsProofOfPossession": "a096515a639c004e7aecee3e88ddbb572163b914de63b528db584b27fe6a0267eb95213ccbebea849a720f1f717871ff191a4cf52c9d0a4db57cfcf8f2453d22cd432a5fe64dcb45982abe84343608a8b22740f7f3fbdfe1000fede5f0a08db3", + "blsPrivateKey": "6e893accf873971fa56db1cb2aba3efb919b41ad88db4b8189a910f6e79689a6" + }, + "encrypted": {} + }, + { + "address": "lsk6quzyfffe2xhukyq4vjwnebmnapvsgj4we7bad", + "keyPath": "m/44'/134'/85'", + "publicKey": "a9142d10c269a0c4682f153d570ea3d880031db76be7363f03a368f461e58290", + "privateKey": "117cf51251f9966fbcfc7c421d8ed2704f2e347985aef71142bc9cefd18095bea9142d10c269a0c4682f153d570ea3d880031db76be7363f03a368f461e58290", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/85'", + "generatorKey": "b5308c34412c54e4b8358b5fca16396084004ee37c6824c1ad751cbe8e50e24f", + "generatorPrivateKey": "be0eef0d6ba7e57c9366787d3706335179db8f891164388e0a9acbc13eb8590ab5308c34412c54e4b8358b5fca16396084004ee37c6824c1ad751cbe8e50e24f", + "blsKeyPath": "m/12381/134/0/85", + "blsKey": "b422e4fa8ab196e0bcc49f956ab3b5c13dc14442864dca80118dea7329308e7f7aa7547df293c826a29ef4bbfe517778", + "blsProofOfPossession": "8ce0fe2bf47180e74f315fda7bfdb376a277f394667c88661dbefcc57100af1d0a06d36ef406f7abc0282a1cb8f5091505d759a40739b11b4a1fd0060e2066edd79ad417168a977f1a59206ddac4bbabaf70feda572bb19c17b9d9034bfe28b1", + "blsPrivateKey": "6e196953fefb89d7a1aad387fc99756391b7adfb5590da079605ac95d4caaaea" + }, + "encrypted": {} + }, + { + "address": "lskvpnf7a2eg5wpxrx9p2tnnxm8y7a7emfj8c3gst", + "keyPath": "m/44'/134'/86'", + "publicKey": "1cfae47a4f613770c5dd321052cc81b569e685d71bdb7da9d4a95d8a035ed05f", + "privateKey": "1c16a7a0fbd0b063cca49264d18bfae921e038dd1fda6600e54a6588ecb093521cfae47a4f613770c5dd321052cc81b569e685d71bdb7da9d4a95d8a035ed05f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/86'", + "generatorKey": "1d224ad4cf64a3db52b2509c5b63365db970f34c8e09babf4af8135d9234f91f", + "generatorPrivateKey": "34f86863e752c3e15b3d4a18826d55d8300fc00b31d2cc0c12999f72d90dc1c81d224ad4cf64a3db52b2509c5b63365db970f34c8e09babf4af8135d9234f91f", + "blsKeyPath": "m/12381/134/0/86", + "blsKey": "86bc497e250f34a664a3330788292ee901aa286e10fcb280a4a151a8741bc0d154b947a4d3cd9bc5b552917211081466", + "blsProofOfPossession": "97a20b81bdcbc7a4f228bc00894d53d55fbb2c53960f0ddc0cfa0f77395a33858a9907079773ad50a220cbdb49bc1d171250df83dd70572c4691eb280ae99d4501b289676b6bb0ad0e859b525752015bf5113e49050a8c70853470f2dd7e9344", + "blsPrivateKey": "6c4e85a20db21bc06ae05a2edebe13688400611e830b77fdb62bde3b1ecb715d" + }, + "encrypted": {} + }, + { + "address": "lskkqjdxujqmjn2woqjs6txv3trzh6s5gsr882scp", + "keyPath": "m/44'/134'/87'", + "publicKey": "006ab84d7246fa450123b5a476a6ecb8622ac38a06ef87948bd5b4dce0ac5c61", + "privateKey": "dd04565d95cfb8abdfdacf4ff62f93c28861dc6d0d9f927a4f18a170d04481ad006ab84d7246fa450123b5a476a6ecb8622ac38a06ef87948bd5b4dce0ac5c61", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/87'", + "generatorKey": "c0aa7af3198f0e3a6bf35c5be38e0f181827735b1c3a635e8db05b80b3647054", + "generatorPrivateKey": "0c046bcc79d3af083cb9d7fecffd601f20be44c786a3bd29461e37d1c06b7f8fc0aa7af3198f0e3a6bf35c5be38e0f181827735b1c3a635e8db05b80b3647054", + "blsKeyPath": "m/12381/134/0/87", + "blsKey": "95acb59c54e53f09d7aac37c2db59c6df0ebb1e38120690a9035c715dc9862995472c72e9f48bfb05e920494dc17e9bb", + "blsProofOfPossession": "8798b4e143b15d10965194d0350d95c374d214d14f6a0c750a1a1699f1221388f01d00c6b708167fc7fcf355591abe370ed45c55306fdc372d26432cba8efc1f83238c1f2e669111656ba61b4bff391786713c28f7d1c6e717fbe98aec2dfda3", + "blsPrivateKey": "0251ae54a957ebe5cec7315592870cf6944434934a811eed219c1e42662f37f0" + }, + "encrypted": {} + }, + { + "address": "lskvwy3xvehhpfh2aekcaro5sk36vp5z5kns2zaqt", + "keyPath": "m/44'/134'/88'", + "publicKey": "80828e04067b8630864b6a21c6c998c6ac5ee744644125e5905a08ccc9f01bf1", + "privateKey": "310c22882aeee8d4d9c5fa47613684cf4b5c4fff2343d35904b4d4757103dda780828e04067b8630864b6a21c6c998c6ac5ee744644125e5905a08ccc9f01bf1", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/88'", + "generatorKey": "20a50d60059dff36a6f6c922f55b018d288ba1f9df5120eeb8fa8e3745a800ec", + "generatorPrivateKey": "a01f3582e3adf093686463ce0f5652a821eb9ad00216d67efef465a95df153af20a50d60059dff36a6f6c922f55b018d288ba1f9df5120eeb8fa8e3745a800ec", + "blsKeyPath": "m/12381/134/0/88", + "blsKey": "96482192c99ac4569b2d139670e566ca5ccf41f39d50b7ddcf69d790bcd556e797614ecb3dda2017e5e3ac2bab4e82d0", + "blsProofOfPossession": "865e6e88cf91b061b92f2d499936f384c9a3df52de5717661b66c4fd5150f1b171350c6abeab96fb905b6294ca7694420728022d84f4c31180f903a6ab8b5b8153fdcf65d46c8a018e65c0459e64c931b6544b6f00e673c30f2a82402fe8be3c", + "blsPrivateKey": "4f5694686955714b3a71244e647c1463545af4f93ef556c8417fdabb429e554b" + }, + "encrypted": {} + }, + { + "address": "lskym4rrvgax9ubgqz6944z9q3t6quo5ugw33j3kr", + "keyPath": "m/44'/134'/89'", + "publicKey": "c5e49e11ab7f218a99d98f47f6df27c6a8a4aa1489a8a48cc54e448700125aaa", + "privateKey": "bc7226156e4882cca468daad1c4fff4dee9efb36b7c861d315b6babbd55a8323c5e49e11ab7f218a99d98f47f6df27c6a8a4aa1489a8a48cc54e448700125aaa", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/89'", + "generatorKey": "4514d1723eed164b3792f1950d3b1c7a1067441ba207cce8d9bdd6f436a119fe", + "generatorPrivateKey": "f9e9f39940de3d64a3c93ee626df1169a8f6b5bcbb3b97ed9328ff9b02e22ff34514d1723eed164b3792f1950d3b1c7a1067441ba207cce8d9bdd6f436a119fe", + "blsKeyPath": "m/12381/134/0/89", + "blsKey": "a5963aa24ed05e95d19fd9de35ae6f523aad987ab2b9897216091e798e15f5062e9734b11fcacd6b8f312162ddc10940", + "blsProofOfPossession": "8a1ae28d6d70bfa0dbcc694c811c05ac6e697a17f41d45a32e1cb5b225bd42de7c1043f4af3c17d92641c4d017569e2302dad3e32493294831da564a07154e5098129639deb89743d1146f8e01f9f6f32f382905707051467242b646d86bad05", + "blsPrivateKey": "6b15b3a0f1484c2db866606cf0c6cd8270c3ff294118d7d34ec3d0fa3d9c3d5e" + }, + "encrypted": {} + }, + { + "address": "lskmc9nhajmkqczvaeob872h9mefnw63mcec84qzd", + "keyPath": "m/44'/134'/90'", + "publicKey": "55a02f49309f5ba1ff6c55c4b5fae4d966cc17cc30e769a42ce4bc7d5c3706c6", + "privateKey": "7766e85d16e1fda134af1e4e323365f7dcc1282a49b4b08b0ff82363cb07062655a02f49309f5ba1ff6c55c4b5fae4d966cc17cc30e769a42ce4bc7d5c3706c6", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/90'", + "generatorKey": "b67f0a9ad61ad6867b54aaaed6036001485d7a7ba13770aed786b34241f37cda", + "generatorPrivateKey": "c6b7a360f60b7e2b554a47b6d51f01e9e33ea7a9fcd2254ce23af34cf08a1f3cb67f0a9ad61ad6867b54aaaed6036001485d7a7ba13770aed786b34241f37cda", + "blsKeyPath": "m/12381/134/0/90", + "blsKey": "a029f74eaf914e3dfd828502f224fff7311a964d11eb1c335eebadc38b5c20a98f79bfc53ccf6ee3630cfa282e88489d", + "blsProofOfPossession": "b5cd13eac543928db25ebb9d69dfaacc04a0d41924f2010a6f04b2457523a5a423a9c49756dbcb969a7b2c49ddcc7c710ada766fdddaedbff02f68e2b75108f111f4078d2705f06551ef524f201d50ac32c423d04a7e6e7c6c8a64d70c013ec3", + "blsPrivateKey": "40726625c04da9fb36a758b0859ec1a77d546750e454bf45dc2c77b1cc1fbb49" + }, + "encrypted": {} + }, + { + "address": "lskf5sf93qyn28wfzqvr74eca3tywuuzq6xf32p7f", + "keyPath": "m/44'/134'/91'", + "publicKey": "674d283554e152216de9a42e979924ff9b05b3e39ed5072026fc8710b4fdd926", + "privateKey": "1a72b8481a589c55ba26d2805e16b58f234b243c2c87a0c39d757ec1238e66b8674d283554e152216de9a42e979924ff9b05b3e39ed5072026fc8710b4fdd926", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/91'", + "generatorKey": "d05b69bda8b5cd103c620a814cbab2f2a131dcfda6bd4cd568155ddb1afd423b", + "generatorPrivateKey": "58d029150eeb456c86e0c2aea034d210c4d356278b4102707e2b7e4bfadcff05d05b69bda8b5cd103c620a814cbab2f2a131dcfda6bd4cd568155ddb1afd423b", + "blsKeyPath": "m/12381/134/0/91", + "blsKey": "947456674b5616341cc932afb30e42973dd17582a81e5fe958277efc828535cd7c9c778410c52e069ed23e4cf629814a", + "blsProofOfPossession": "872ce3383378215d3be299f32196e9cb2ae1f9e06101afbb9e7709eafb37eca8548f156bbdfbb120c2d06fdbfdf5455107f2c818bfbc9b4e9f5fb4c50f79b24f5fc84f9e137b286d71c3d588a7af684d36bf701425b25ece2d9fbacbadb58f4e", + "blsPrivateKey": "7122afff2e9ebeadc8575a12f8cfd205b04c9c04eb3f90a354ae4ecc8479b54c" + }, + "encrypted": {} + }, + { + "address": "lskos7tnf5jx4e6jq4bf5z4gwo2ow5he4khn75gpo", + "keyPath": "m/44'/134'/92'", + "publicKey": "3d1a78899766f0662536e49af492f961fb3f1eb22f3172dad04b30c4302af87e", + "privateKey": "fe473f20516d7fa871fa0787ffdc42eafa848619ecffd3fc57de2c8aa6f1f13c3d1a78899766f0662536e49af492f961fb3f1eb22f3172dad04b30c4302af87e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/92'", + "generatorKey": "d5781773a9b07a569a0d87c0bf82103fd459a2185fc32f5c312a663c5bc65784", + "generatorPrivateKey": "e78ae7b42d3d6e7df38f69f3b25db40b31923b4fc088b8793ff9a8f07ef9ecf9d5781773a9b07a569a0d87c0bf82103fd459a2185fc32f5c312a663c5bc65784", + "blsKeyPath": "m/12381/134/0/92", + "blsKey": "87971b8a0520e08dc8dbb8114de7ecd44e98844c9179585806e8a1edaae1190ea85e6471767e90074d87d1dfbafc983c", + "blsProofOfPossession": "ac1fa23a608ce0be52ada7759c4631a5e3c7828a2a622c718b67c4d8996eeed61c382ec319ff2c608290c141ef741ba013f7567bf95cdfb29295dea31adb440f5d856f5688fdd553f47a06ab5692ee5fb99e5a50b329fe4406bfefb924b5665c", + "blsPrivateKey": "36d1ee8a349ef4cdc983bb55ef2fca9415f2f9ecf72df9a26e4138b534979852" + }, + "encrypted": {} + }, + { + "address": "lsk5rtz6s352qyt9vggx7uyo5b4p2ommfxz36w7ma", + "keyPath": "m/44'/134'/93'", + "publicKey": "b73d459c979435a84e70ea70bb18e14f312afe49af535ff4c9cd0f3a6d4cbd1e", + "privateKey": "dcb8988276c8aa0424bbb764125504f83b944d5422fe5b721fa8e5db29d08920b73d459c979435a84e70ea70bb18e14f312afe49af535ff4c9cd0f3a6d4cbd1e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/93'", + "generatorKey": "d1f10929b1eab8232be9df3b792496eb56bcb5c0a8c2fd04e3be1fab26c7980e", + "generatorPrivateKey": "95c19ccad9cc85f4b8776e2ce5d12c646b6cb6bd60d2d2b89089d664f97ebbabd1f10929b1eab8232be9df3b792496eb56bcb5c0a8c2fd04e3be1fab26c7980e", + "blsKeyPath": "m/12381/134/0/93", + "blsKey": "8f96883db13e4f43e7280d8a58e7642228f46c375853a17e8cdb34fdeaf4e363a82678d2f54a8630218e097ba39d4370", + "blsProofOfPossession": "91a2efa4a407f63eb9157a4f4378bf6dfb4fc6d5d2714c2ee81f49ac90bc5dc3f1b72051a1fa1615f2e2d694cf17c27c1429e94bebc023feea2a405f7a8343dcc567636d15ac95ef84b1c673298becb766e036d9869e2113d9f4602f6e6092dd", + "blsPrivateKey": "5cffd4aceca113ca008c1d7603eabbbb0f0ba6f3595abf97b875e6687a5c9633" + }, + "encrypted": {} + }, + { + "address": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "keyPath": "m/44'/134'/94'", + "publicKey": "150cdf5f275aa57cae604f22f14ac2b9635ac52cd1a911a9c253842a880413fb", + "privateKey": "da4abca8970207329ad32eeee64d12e16e729cbbc75effbf3007c28f0da7071e150cdf5f275aa57cae604f22f14ac2b9635ac52cd1a911a9c253842a880413fb", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/94'", + "generatorKey": "3f44b319b82443eabb300eba5a2f323d72e44d9d2d5ed0b21a24051595582dd5", + "generatorPrivateKey": "51d9322ce03caa96cd576f48888c9a284b3e9e8f05a9a5a6395563997fecd6f03f44b319b82443eabb300eba5a2f323d72e44d9d2d5ed0b21a24051595582dd5", + "blsKeyPath": "m/12381/134/0/94", + "blsKey": "a6689556554e528964141d813c184ad4ec5c3564260d2709606c845f0c684b4bb5ff77054acb6eb8184a40fcd783670b", + "blsProofOfPossession": "831e87337aa9d7129b42ac2ac6d355395b07829148f3a4570293cb8ea00593cbbd1933a9393d8f5c4028f74c0d6c29511526e76d082fd2207f65e653129a29f22787cf19d4efe50ff43651e16463f868714354d6860e62dcd715858c4c53fc51", + "blsPrivateKey": "3980fcb82cccfce71cb76fb8860b4ef554b434db8f1a2a73578080223202802a" + }, + "encrypted": {} + }, + { + "address": "lskrskxmbv7s4czgxz5wtdqkay87ts2mfmu4ufcaw", + "keyPath": "m/44'/134'/95'", + "publicKey": "c215430e686f7f722aaa33a9652104ea23f3355906f77bc5a9e7940ab70b6fdc", + "privateKey": "e97d7dc3b6f3f0ea4445d1c3087af59d2e96b60646cce4bb417501430ae5ce91c215430e686f7f722aaa33a9652104ea23f3355906f77bc5a9e7940ab70b6fdc", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/95'", + "generatorKey": "07614fd5036d099a3caf004d46a083d12df2024fc03ef29cec22e58d1f78531f", + "generatorPrivateKey": "45569843c81a8513089ba0c1ef12c436a4397b7ed1e0fb045a6c0c0a7ec8027807614fd5036d099a3caf004d46a083d12df2024fc03ef29cec22e58d1f78531f", + "blsKeyPath": "m/12381/134/0/95", + "blsKey": "98c4f0e2b01f1b6ed07035fe46c17a40fe5409b1461a2b697afaf869e2f8c88b2db297b9a149208109bab2da195235c0", + "blsProofOfPossession": "8dad459d6b312d4a6767695029525e95f04e3ee083de85d0db5d818d15d32ef7aecb57f608c2c10355e3ca6dba8018e5192862d80f00fe1f71fd396d81d6a7649221c50bc8336efd12dc1cc13ee3c3898617971244af6a8da5ccd9224c9ea2f9", + "blsPrivateKey": "4601428462ce9b60ec00563894972ff082ff16691e45edbfef67dae7c300d2d3" + }, + "encrypted": {} + }, + { + "address": "lskjnr8jmvz45dj9z47jbky9sadh3us3rd8tdn7ww", + "keyPath": "m/44'/134'/96'", + "publicKey": "c8c2b511a2c7e697ccb8e8332e343e2db6ebbd88068422e1539011bbed669221", + "privateKey": "6841ae7fddd9f1895fcf65734faa7792f9138c9854c6786b0938f4419ee00316c8c2b511a2c7e697ccb8e8332e343e2db6ebbd88068422e1539011bbed669221", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/96'", + "generatorKey": "25ae368be016caae7066a6ce9f2ad8e4220d328ffb860a6d275d878f4882c70c", + "generatorPrivateKey": "ffd8857840f0d6c52693d21a194f1a419fe0b78b9fa4b90b1fab570ee16073da25ae368be016caae7066a6ce9f2ad8e4220d328ffb860a6d275d878f4882c70c", + "blsKeyPath": "m/12381/134/0/96", + "blsKey": "8ce6c9d2ed4f223635e3bd85476f0d56cdbb5e4090ae22b10a7fabd08d231193cf6d9c4f5b400eb4b310ef270811e424", + "blsProofOfPossession": "b896aabbcc1a165adaec26feb72fc580d4a6512dd09df40b4333381d2536b5ac36d22e91469a976ae446a6291792cb6a141013baaaae12faff26d06c6a6b722a28635c72d49fcd50ac910ca01d760e80892fc5757a18597cd1ce7f16dbabd195", + "blsPrivateKey": "47320a453378fdf5463d3a0b930fedc913ea61562b0f2eb5dc402fcdcbba9bef" + }, + "encrypted": {} + }, + { + "address": "lskmadcfr9p3qgx8upeac6xkmk8fjss7atw8p8s2a", + "keyPath": "m/44'/134'/97'", + "publicKey": "f9d11d99d4862ff2bfac4bc2306f238274cac119bc990d325732c82a09011678", + "privateKey": "1fd11f9dd4d51518021e84016507c9611ff81227fde8f51b022e57fdad05fe53f9d11d99d4862ff2bfac4bc2306f238274cac119bc990d325732c82a09011678", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/97'", + "generatorKey": "ebeb7f828aaa40ab6040e914b66b6f5d76964a0579bd29bf98c2641547f229f6", + "generatorPrivateKey": "0a48d7c8fd894f9625adb370496bdc77738a431ac859741a6e249500981c6affebeb7f828aaa40ab6040e914b66b6f5d76964a0579bd29bf98c2641547f229f6", + "blsKeyPath": "m/12381/134/0/97", + "blsKey": "a13d3a62d053b3a092d736f3c96c89fb982924b9cfd1e8283c4ced5a537732718e73c6c86c94ddd416eb94a753366b7f", + "blsProofOfPossession": "950583faae3492f5d15f9ad72bad982b2f513956cc1259e16e28ef2e18f7db3df1bf1cbab7350e390ac5a8785c574fe30878784e6c5d50668184c4c92bda196432034a7e092d9e62736ca543e1b7e594ccf6b81d37c17fabf73b846b67a0bc8f", + "blsPrivateKey": "390cc059245031c463d51a4904d080a495aa779bfe1fec5bea9e670a5211a832" + }, + "encrypted": {} + }, + { + "address": "lsknyuj2wnn95w8svk7jo38jwxhpnrx7cj3vo4vjc", + "keyPath": "m/44'/134'/98'", + "publicKey": "5bea76165e8cae84bfb3b2b65d00aa4fd63a00b6153654b5f88e27add708e04a", + "privateKey": "44c0c9eee20e7e8fc1a57564e32d8616868e76956b51794496cc3f8194c7ed0a5bea76165e8cae84bfb3b2b65d00aa4fd63a00b6153654b5f88e27add708e04a", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/98'", + "generatorKey": "4325779e64521ded42c0e2e873c16b753433d0e7f9a1e046e27a0fae9378d9c9", + "generatorPrivateKey": "3b1fe311327d7e65009c2cf5fc067f59abc2bae1aee6838158108e61d7bfa2ad4325779e64521ded42c0e2e873c16b753433d0e7f9a1e046e27a0fae9378d9c9", + "blsKeyPath": "m/12381/134/0/98", + "blsKey": "a0fb290e74bce8c5858dc1b615bac542d2280a477912ae06b8d4f07c6d451eae44a47cae6a7a1fb5cedea9efe2d4e5a5", + "blsProofOfPossession": "8b1a7d2b1566ce81c8ac2b8c88b6966b960462d0fa4e54554f53ab184c31c72c65fce904aff79d4235dd3e16e8eed2780e083a31a432e70a538de1b81d8a8a49d31bdd361f357d57fe4568d1b506492fc72f42d4b344ecfac2d560bbd2214621", + "blsPrivateKey": "3308c88c2a602c8d5cb7a84d9e70e08fc97a4e95ac27f18360496270173c27d8" + }, + "encrypted": {} + }, + { + "address": "lskrccyjmc8cybh9n3kgencq8u7fh796v2zfraco9", + "keyPath": "m/44'/134'/99'", + "publicKey": "0996481caf431af4f6ba452010898aa72b04f15115192b6b25a7e14feeee1a0c", + "privateKey": "2df44d979b4c374c2021b7dd16890943b1e2a76ba94297d35aa18023001072ef0996481caf431af4f6ba452010898aa72b04f15115192b6b25a7e14feeee1a0c", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/99'", + "generatorKey": "bf9ebe25faae5a874d97ad1772ad062ca52f63e48d806ef641e025a963224200", + "generatorPrivateKey": "18120516aa855a5be57ae46b20c7ac0efb66f9b2813ce6832e309302ea6920aebf9ebe25faae5a874d97ad1772ad062ca52f63e48d806ef641e025a963224200", + "blsKeyPath": "m/12381/134/0/99", + "blsKey": "8b436ed371b7af11b31347c12321d90a427e9aa8d93275a27faedcbe2dd06c5dce1e1a4a03b0ae030e5cd0106a942cd8", + "blsProofOfPossession": "b1dcf2ff65ba4096611f392fb56d104754927cba14ec3d193ebcf7d6eaab062c7ab770c512e815c7d52c37fa9b8622400df7939f4bbeb8566beebce1b13d67562f7bb6a01f988a501e4ef691b544cd05796010b614014ec3036b171c7392cd7d", + "blsPrivateKey": "39032c0f523eb58f549d1e5bdd0f1b38ea435bc0e26fb8a9458ca9908919980c" + }, + "encrypted": {} + }, + { + "address": "lsk3dzjyndh43tdc6vugbdqhfpt3k9juethuzsmdk", + "keyPath": "m/44'/134'/100'", + "publicKey": "c515bd1d0c9c09d3ce40eeca511489b8ed7c2ec1bc03bd5611f3a6b47c16469c", + "privateKey": "a8faeeba2da8b823b014d37165a1bfc26e74507641a65c742ae6e5cf96fb31d4c515bd1d0c9c09d3ce40eeca511489b8ed7c2ec1bc03bd5611f3a6b47c16469c", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/100'", + "generatorKey": "9f1c361befb0ae35de28e8f0e25efe75ede78aa26c703625cc17e7fe2e7208f3", + "generatorPrivateKey": "eb79f34b330f6efe29593cba5a5a8a369cfd1bd0887689020387c536e44da5249f1c361befb0ae35de28e8f0e25efe75ede78aa26c703625cc17e7fe2e7208f3", + "blsKeyPath": "m/12381/134/0/100", + "blsKey": "a1782a5f280f9894cea555d6f355c1f23e0581140c64f20ae469edd6ace7dcb6266227feecf002c2b508766e730c6f4f", + "blsProofOfPossession": "84e053bb01b22997e46ce4cbece0f5478e27cd49786cc36b1459c8930ea408e663bc725184197eb726fadf6988503c9b01be391ca3eb16587137cf5a3941717837baec7869896bae401bb513359485142778a52638429328f06a4469b7e21bb0", + "blsPrivateKey": "306651c1b7494c98b3d190fbf54b2247b9a456cb21eaadf3a0a668d740f6bdba" + }, + "encrypted": {} + }, + { + "address": "lskyunb64dg4x72ue8mzte7cbev8j4nucf9je2sh9", + "keyPath": "m/44'/134'/101'", + "publicKey": "e1383015621226361ac69c33c6b4e6148a30b08736ae0e043055b1ee9c2ad163", + "privateKey": "540473e6d615a2ebf88f99ad6387fa80b90b8847cb77fcfe09e4fb1e8a2bd6b0e1383015621226361ac69c33c6b4e6148a30b08736ae0e043055b1ee9c2ad163", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/101'", + "generatorKey": "a9b0c063fee99a903a55da57e3d16f069145e414b62e25dbbf218bd608a61f7c", + "generatorPrivateKey": "c545eee8e84f1ce916cefa07dd86818165e7187f9b33cd487060ab6944951847a9b0c063fee99a903a55da57e3d16f069145e414b62e25dbbf218bd608a61f7c", + "blsKeyPath": "m/12381/134/0/101", + "blsKey": "870db2da31a9471077677bd9a7529ee7523bdd64fdba46c514e94aa52e940566479cfdab29b07c1573aff6ba7040c684", + "blsProofOfPossession": "acbe270292cfaa154f256a83c9bdde889a9205c85c5ff0f41dae586dccc7f29f0464fbc087a5c5adb3cb4eca3b95bc14187db64cccd24e98d3e75215b69bd2bd0b357834c1ccacbdf91556fa59a86d04d1fc8aaa3be2ae5256aea3bd36d26942", + "blsPrivateKey": "4f2fdd4bb6fd739b02dea4a44ad1c4d8fa126c1ed1ebefc6f0016abd8e2c1a9c" + }, + "encrypted": {} + }, + { + "address": "lskoys3dpcyx5hkr7u2fenpjrbyd69tuyu5ar4dgy", + "keyPath": "m/44'/134'/102'", + "publicKey": "f3388194bea3a10bfb3b0b89d47417450ce078b147b7d68c7feee57f0e5d8492", + "privateKey": "3b2dfd3635ebd2c1b8b139193322422ee8ffdeba6a5ec385bb3f8fc4913a19cef3388194bea3a10bfb3b0b89d47417450ce078b147b7d68c7feee57f0e5d8492", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/102'", + "generatorKey": "3efa1c0a728a9741555b84ff1d80aedfcaf85370e1602890d7ba610bf33500bb", + "generatorPrivateKey": "f06fc00decaf4f11f2f714788f28ed0a25228a08dc002e49e16945d3e9aa2fc63efa1c0a728a9741555b84ff1d80aedfcaf85370e1602890d7ba610bf33500bb", + "blsKeyPath": "m/12381/134/0/102", + "blsKey": "a4f78f9b10c5671cca5aa2526708b95bdec56f3e404fc6c6403de83338940dfcc8d6836ba3d98566d314d34438a042d3", + "blsProofOfPossession": "91a1d0b501b7ab2caa5d240eae92c8c0ccbf296ebd3dd9d03aac1ca569f803091ec5ab57b7f6c34ad1aeb9aee0ccc17a1911c8e7a9ca681a6b803bf27e303f59dcfa32f678c4bb35189a8b7e0a3af43771ec841bd2ab32a96cb2eab0a1c2ad94", + "blsPrivateKey": "074ab003ca5c16efdcab7e925a317e657d9fdfbdb6e97bb856f1389df5599264" + }, + "encrypted": {} + } + ] +} diff --git a/examples/poa-sidechain/config/alphanet/genesis_assets.json b/examples/poa-sidechain/config/alphanet/genesis_assets.json new file mode 100644 index 00000000000..d575139c811 --- /dev/null +++ b/examples/poa-sidechain/config/alphanet/genesis_assets.json @@ -0,0 +1,2799 @@ +{ + "assets": [ + { + "module": "interoperability", + "data": { + "ownChainName": "lisk_mainchain", + "ownChainNonce": 0, + "chainInfos": [], + "terminatedStateAccounts": [], + "terminatedOutboxAccounts": [] + }, + "schema": { + "$id": "/interoperability/module/genesis", + "type": "object", + "required": [ + "ownChainName", + "ownChainNonce", + "chainInfos", + "terminatedStateAccounts", + "terminatedOutboxAccounts" + ], + "properties": { + "ownChainName": { + "dataType": "string", + "maxLength": 32, + "fieldNumber": 1 + }, + "ownChainNonce": { + "dataType": "uint64", + "fieldNumber": 2 + }, + "chainInfos": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["chainID", "chainData", "channelData", "chainValidators"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "chainData": { + "$id": "/modules/interoperability/chainData", + "type": "object", + "required": ["name", "lastCertificate", "status"], + "properties": { + "name": { + "dataType": "string", + "minLength": 1, + "maxLength": 32, + "fieldNumber": 1 + }, + "lastCertificate": { + "type": "object", + "fieldNumber": 2, + "required": ["height", "timestamp", "stateRoot", "validatorsHash"], + "properties": { + "height": { + "dataType": "uint32", + "fieldNumber": 1 + }, + "timestamp": { + "dataType": "uint32", + "fieldNumber": 2 + }, + "stateRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 3 + }, + "validatorsHash": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 4 + } + } + }, + "status": { + "dataType": "uint32", + "fieldNumber": 3 + } + }, + "fieldNumber": 2 + }, + "channelData": { + "$id": "/modules/interoperability/channel", + "type": "object", + "required": [ + "inbox", + "outbox", + "partnerChainOutboxRoot", + "messageFeeTokenID", + "minReturnFeePerByte" + ], + "properties": { + "inbox": { + "type": "object", + "fieldNumber": 1, + "required": ["appendPath", "size", "root"], + "properties": { + "appendPath": { + "type": "array", + "items": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32 + }, + "fieldNumber": 1 + }, + "size": { + "fieldNumber": 2, + "dataType": "uint32" + }, + "root": { + "fieldNumber": 3, + "dataType": "bytes", + "minLength": 32, + "maxLength": 32 + } + } + }, + "outbox": { + "type": "object", + "fieldNumber": 2, + "required": ["appendPath", "size", "root"], + "properties": { + "appendPath": { + "type": "array", + "items": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32 + }, + "fieldNumber": 1 + }, + "size": { + "fieldNumber": 2, + "dataType": "uint32" + }, + "root": { + "fieldNumber": 3, + "dataType": "bytes", + "minLength": 32, + "maxLength": 32 + } + } + }, + "partnerChainOutboxRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 3 + }, + "messageFeeTokenID": { + "dataType": "bytes", + "minLength": 8, + "maxLength": 8, + "fieldNumber": 4 + }, + "minReturnFeePerByte": { + "dataType": "uint64", + "fieldNumber": 5 + } + }, + "fieldNumber": 3 + }, + "chainValidators": { + "$id": "/modules/interoperability/chainValidators", + "type": "object", + "required": ["activeValidators", "certificateThreshold"], + "properties": { + "activeValidators": { + "type": "array", + "fieldNumber": 1, + "minItems": 1, + "maxItems": 199, + "items": { + "type": "object", + "required": ["blsKey", "bftWeight"], + "properties": { + "blsKey": { + "dataType": "bytes", + "minLength": 48, + "maxLength": 48, + "fieldNumber": 1 + }, + "bftWeight": { + "dataType": "uint64", + "fieldNumber": 2 + } + } + } + }, + "certificateThreshold": { + "dataType": "uint64", + "fieldNumber": 2 + } + }, + "fieldNumber": 4 + } + } + } + }, + "terminatedStateAccounts": { + "type": "array", + "fieldNumber": 4, + "items": { + "type": "object", + "required": ["chainID", "terminatedStateAccount"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "terminatedStateAccount": { + "$id": "/modules/interoperability/terminatedState", + "type": "object", + "required": ["stateRoot", "mainchainStateRoot", "initialized"], + "properties": { + "stateRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 1 + }, + "mainchainStateRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 2 + }, + "initialized": { + "dataType": "boolean", + "fieldNumber": 3 + } + }, + "fieldNumber": 2 + } + } + } + }, + "terminatedOutboxAccounts": { + "type": "array", + "fieldNumber": 5, + "items": { + "type": "object", + "required": ["chainID", "terminatedOutboxAccount"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "terminatedOutboxAccount": { + "$id": "/modules/interoperability/terminatedOutbox", + "type": "object", + "required": ["outboxRoot", "outboxSize", "partnerChainInboxSize"], + "properties": { + "outboxRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 1 + }, + "outboxSize": { + "dataType": "uint32", + "fieldNumber": 2 + }, + "partnerChainInboxSize": { + "dataType": "uint32", + "fieldNumber": 3 + } + }, + "fieldNumber": 2 + } + } + } + } + } + } + }, + { + "module": "token", + "data": { + "userSubstore": [ + { + "address": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskvcgy7ccuokarwqde8m8ztrur92cob6ju5quy4n", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskvpnf7a2eg5wpxrx9p2tnnxm8y7a7emfj8c3gst", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskvq67zzev53sa6ozt39ft3dsmwxxztb7h29275k", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskvwy3xvehhpfh2aekcaro5sk36vp5z5kns2zaqt", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskcuj9g99y36fc6em2f6zfrd83c6djsvcyzx9u3p", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskc22mfaqzo722aenb6yw7awx8f22nrn54skrj8b", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskchcsq6pgnq6nwttwe9hyj67rb9936cf2ccjk3b", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskp2kubbnvgwhw588t3wp85wthe285r7e2m64w2d", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskmc9nhajmkqczvaeob872h9mefnw63mcec84qzd", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskm8g9dshwfcmfq9ctbrjm9zvb58h5c7y9ecstky", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskmwac26bhz5s5wo7h79dpyucckxku8jw5descbg", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskmadcfr9p3qgx8upeac6xkmk8fjss7atw8p8s2a", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskbm49qcdcyqvavxkm69x22btvhwx6v27kfzghu3", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskbr5cnd8rjeaot7gtfo79fsywx4nb68b29xeqrh", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsknyuj2wnn95w8svk7jo38jwxhpnrx7cj3vo4vjc", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsknax33n2ohy872rdkfp4ud7nsv8eamwt6utw5nb", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsknatyy4944pxukrhe38bww4bn3myzjp2af4sqgh", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsknddzdw4xxej5znssc7aapej67s7g476osk7prc", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk3oz8mycgs86jehbmpmb83n8z3ctxou47h7r9bs", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk37kucto34knfhumezkx3qdwhmbrqfonjmck59z", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk3dzjyndh43tdc6vugbdqhfpt3k9juethuzsmdk", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk4nst5n99meqxndr684va7hhenw7q8sxs5depnb", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk67y3t2sqd7kka2agtcdm68oqvmvyw94nrjqz7f", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk6quzyfffe2xhukyq4vjwnebmnapvsgj4we7bad", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk5pmheu78re567zd5dnddzh2c3jzn7bwcrjd7dy", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk56hpjtt5b8w3h2qgckr57txuw95ja29rsonweo", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk5y2q2tn35xrnpdc4oag8sa3ktdacmdcahvwqot", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk5rtz6s352qyt9vggx7uyo5b4p2ommfxz36w7ma", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskoys3dpcyx5hkr7u2fenpjrbyd69tuyu5ar4dgy", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskoq2bmkpfwmmbo3c9pzdby7wmwjvokgmpgbpcj3", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskowvmbgn4oye4hae3keyjuzta4t499zqkjqydfd", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskos7tnf5jx4e6jq4bf5z4gwo2ow5he4khn75gpo", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk966m5mv2xk8hassrq5b8nz97qmy3nh348y6zf7", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk7drqfofanzn9rf7g59a2jha5ses3rswmc26hpw", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8vjsq5s8jan9c8y9tmgawd6cttuszbf6jmhvj5", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8netwcxgkpew8g5as2bkwbfraetf8neud25ktc", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8dz47g5s7qxbyy46qvkrykfoj7wg7rb5ohy97c", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8dsngwh4n6hmf4unqb8gfqgkayabaqdvtq85ja", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskux8ew6zq6zddya4u32towauvxmbe3x9hxvbzv4", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsku4ftwo3dvgygbnn58octduj6458h5eep2aea6e", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskuueow44w67rte7uoryn855hp5kw48szuhe5qmc", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskym4rrvgax9ubgqz6944z9q3t6quo5ugw33j3kr", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskyunb64dg4x72ue8mzte7cbev8j4nucf9je2sh9", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrzuuu8gkp5bxrbbz9hdjxw2yhnpxdkdz3j8rxr", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrxweey4ak83ek36go6okoxr6bxrepdv3y52k3y", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrccyjmc8cybh9n3kgencq8u7fh796v2zfraco9", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskr8bmeh9q5brkctg8g44j82ootju82zu8porwvq", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrskxmbv7s4czgxz5wtdqkay87ts2mfmu4ufcaw", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrgqnuqub85jzcocgjsgb5rexrxc32s9dajhm69", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrga27zfbamdcntpbxxt7sezvmubyxv9vnw2upk", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsktn6hodzd7v4kzgpd56osqjfwnzhu4mdyokynum", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsktas5pgp3tofv4ke4f2kayw9uyrqpnbf55bw5hm", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskk33a2z28ak9yy6eunbmodnynoehtyra5o4jzkn", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskk8yh4h2rkp3yegr5xuea62qbos6q8xd6h3wys2", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskkqjdxujqmjn2woqjs6txv3trzh6s5gsr882scp", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskk2vnyd5dq3ekexog6us6zcze9r64wk456zvj9a", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskqxjqneh4mhkvvgga8wxtrky5ztzt6bh8rcvsvg", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskq6j6w8bv4s4to8ty6rz88y2cwcx76o4wcdnsdq", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskq5attbvu8s55ngwr3c5cv8392mqayvy4yyhpuy", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskqw45qy3ph9rwgow86rudqa7e3vmb93db5e4yad", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskqg9k3joyv9ouhjfysscame66hovq42yeev7ug7", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskezdab747v9z78hgmcxsokeetcmbdrpj3gzrdcw", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lske5sqed53fdcs4m9et28f2k7u9fk6hno9bauday", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskee8xh9oc78uhw5dhnaca9mbgmcgbwbnbarvd5d", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskewnnr5x7h3ckkmys8d4orvuyyqmf8odmud6qmg", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskwv3bh76epo42wvj6sdq8t7dbwar7xmm7h4k92m", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskw95u4yqs35jpeourx4jsgdur2br7b9nq88b4g2", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskwdkhf2ew9ov65v7srpq2mdq48rmrgp492z3pkn", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskwdqjhdgvqde9yrro4pfu464cumns3t5gyzutbm", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk2xxvfxaqpm42wr9reokucegh3quypqg9w9aqfo", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lska4qegdqzmsndn5hdn5jngy6nnt9qxjekkkd5jz", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lska6rtf7ndbgbx7d8puaaf3heqsqnudkdhvoabdm", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskau7uqo6afteazgyknmtotxdjgwr3p9gfr4yzke", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskayo6b7wmd3prq8fauwr52tj9ordadwrvuh5hn7", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskatntynnut2eee2zxrpdzokrjmok43xczp2fme7", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskaw28kpqyffwzb8pcy47nangwwbyxjgnnvh9sfw", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskdo2dmatrfwcnzoeohorwqbef4qngvojfdtkqpj", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskduxr23bn9pajg8antj6fzaxc7hqpdmomoyshae", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsksmpgg7mo4m6ekc9tgvgjr8kh5h6wmgtqvq6776", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsksu2u78jmmx7jgu3k8vxcmsv48x3746cts9xejf", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsksy7x68enrmjxjb8copn5m8csys6rjejx56pjqt", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsksdfqvkbqpc8eczj2s3dzkxnap5pguaxdw2227r", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskjnr8jmvz45dj9z47jbky9sadh3us3rd8tdn7ww", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskjtc95w5wqh5gtymqh7dqadb6kbc9x2mwr4eq8d", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskjtbchucvrd2s8qjo83e7trpem5edwa6dbjfczq", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskhbcq7mps5hhea5736qaggyupdsmgdj8ufzdojp", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskhamuapyyfckyg5v8u5o4jjw9bvr5bog7rgx8an", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskfx88g3826a4qsyxm4w3fheyymfnucpsq36d326", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskfmufdszf9ssqghf2yjkjeetyxy4v9wgawfv725", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskf6f3zj4o9fnpt7wd4fowafv8buyd72sgt2864b", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskf5sf93qyn28wfzqvr74eca3tywuuzq6xf32p7f", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskfowbrr5mdkenm2fcg2hhu76q3vhs74k692vv28", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskf7a93qr84d9a6ga543wernvxbsrpvtp299c5mj", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskfjd3ymhyzedgneudo2bujnm25u7stu4qpa3jnd", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskffxs3orv2au2juwa69hqtrmpcg9vq78cqbdjr4", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskgn7m77b769frqvgq7uko74wcrroqtcjv7nhv95", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + } + ], + "supplySubstore": [ + { + "tokenID": "0400000000000000", + "totalSupply": "10300000000000000" + } + ], + "escrowSubstore": [], + "supportedTokensSubstore": [] + }, + "schema": { + "$id": "/token/module/genesis", + "type": "object", + "required": ["userSubstore", "supplySubstore", "escrowSubstore", "supportedTokensSubstore"], + "properties": { + "userSubstore": { + "type": "array", + "fieldNumber": 1, + "items": { + "type": "object", + "required": ["address", "tokenID", "availableBalance", "lockedBalances"], + "properties": { + "address": { + "dataType": "bytes", + "format": "lisk32", + "fieldNumber": 1 + }, + "tokenID": { + "dataType": "bytes", + "fieldNumber": 2, + "minLength": 8, + "maxLength": 8 + }, + "availableBalance": { + "dataType": "uint64", + "fieldNumber": 3 + }, + "lockedBalances": { + "type": "array", + "fieldNumber": 4, + "items": { + "type": "object", + "required": ["module", "amount"], + "properties": { + "module": { + "dataType": "string", + "fieldNumber": 1 + }, + "amount": { + "dataType": "uint64", + "fieldNumber": 2 + } + } + } + } + } + } + }, + "supplySubstore": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["tokenID", "totalSupply"], + "properties": { + "tokenID": { + "dataType": "bytes", + "fieldNumber": 1, + "minLength": 8, + "maxLength": 8 + }, + "totalSupply": { + "dataType": "uint64", + "fieldNumber": 2 + } + } + } + }, + "escrowSubstore": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["escrowChainID", "tokenID", "amount"], + "properties": { + "escrowChainID": { + "dataType": "bytes", + "fieldNumber": 1, + "minLength": 4, + "maxLength": 4 + }, + "tokenID": { + "dataType": "bytes", + "fieldNumber": 2, + "minLength": 8, + "maxLength": 8 + }, + "amount": { + "dataType": "uint64", + "fieldNumber": 3 + } + } + } + }, + "supportedTokensSubstore": { + "type": "array", + "fieldNumber": 4, + "items": { + "type": "object", + "required": ["chainID", "supportedTokenIDs"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "supportedTokenIDs": { + "type": "array", + "fieldNumber": 2, + "items": { + "dataType": "bytes", + "minLength": 8, + "maxLength": 8 + } + } + } + } + } + } + } + }, + { + "module": "pos", + "data": { + "validators": [ + { + "address": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "name": "genesis_0", + "blsKey": "a6689556554e528964141d813c184ad4ec5c3564260d2709606c845f0c684b4bb5ff77054acb6eb8184a40fcd783670b", + "proofOfPossession": "831e87337aa9d7129b42ac2ac6d355395b07829148f3a4570293cb8ea00593cbbd1933a9393d8f5c4028f74c0d6c29511526e76d082fd2207f65e653129a29f22787cf19d4efe50ff43651e16463f868714354d6860e62dcd715858c4c53fc51", + "generatorKey": "3f44b319b82443eabb300eba5a2f323d72e44d9d2d5ed0b21a24051595582dd5", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "name": "genesis_1", + "blsKey": "8c4167537d75e68a60e3cd208b63cfae1ffe5c13315e10a6100fcbd34ede8e38f705391c186f32f8a93df5ff3913d45f", + "proofOfPossession": "929e7eb36a9a379fd5cbcce326e166f897e5dfd036a5127ecaea4f5973566e24031a3aebaf131265764d642e9d435c3d0a5fb8d27b8c65e97960667b5b42f63ac34f42482afe60843eb174bd75e2eaac560bfa1935656688d013bb8087071610", + "generatorKey": "73de0a02eee8076cb64f8bc0591326bdd7447d85a24d501307d98aa912ebc766", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "name": "genesis_2", + "blsKey": "b61f2da61bf5837450dcbc3bca0d6cc4fe2ba97f0325e5ee63f879e28aa9ea4dd9979f583e30236fb519a84a9cb27975", + "proofOfPossession": "807bca29a9eea5717c1802aebff8c29ad3f198a369081999512d31c887d8beba1a591d80a87b1122a5d9501b737188f805f3ef9a77acd051576805981cd0c5ba6e9761b5065f4d48f0e579982b45a1e35b3c282d27bb6e04262005835107a16b", + "generatorKey": "761b647f4cb146f168e41658d1dfe0e9c01e5d64b15e5c033d230210f7e0aaa8", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "name": "genesis_3", + "blsKey": "b19c4385aaac82c4010cc8231233593dd479f90365186b0344c25c4e11c6c921f0c5b946028330ead690347216f65549", + "proofOfPossession": "b61a22f607f3652226a78747f3bb52c6d680e06a8041fc1d3a94a78fabf2895f23559059a44b0c64cd759d33e60a06060197246f6886679add69f6d306506336e15cdc7e9bde0aaca6e8191fb3535b5685ce8b3f33212441d311444a3d57fc66", + "generatorKey": "f07a86182356aee3fcfb37dcedbb6712c98319dc24b7be17cb322880d755b299", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "name": "genesis_4", + "blsKey": "a5ca55e9a0ab81d48eaad2960bd3ea259527cf85fe62cc80cfd8400dbd2511725c06c3a597868dcc257bbc279e2b3e92", + "proofOfPossession": "a092cff10ea18ec3dcf3f6e41cd38537e00602e35107067ace7ab7c97a2ae1de531ebea7fc0c22e8dbcee1f981c439930c7cae474a996b153a66b0cb34e66c6041348aaeb4763413afffe0d947da90424065ee573b3683edbb1e51f9a278ae82", + "generatorKey": "0cc6c469088fb2163262ac41787ea4a81da50d92fd510299ba66e5a2b02d5a05", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskvcgy7ccuokarwqde8m8ztrur92cob6ju5quy4n", + "name": "genesis_5", + "blsKey": "87cf21c4649e7f2d83aa0dd0435f73f157cbbaf32352997c5ebc7004ff3f8d72f880048c824cb98493a7ad09f4f561aa", + "proofOfPossession": "92d1948d5d8faec69c6a389548900952014f5803f0eedc480e291bfd8fe6f31231e43fd4bd47817bdbca96e5104b92d2097df4362b94a583a1a24bbdd0382a681b5603d6b3bbfca854d5beccd45c2ebec24623666032f30fb3858b236bfcbd14", + "generatorKey": "83cca7ee3c7145d8022b54fab14505f6f65ed9ac933e3591de4a45d4f2298adb", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskvpnf7a2eg5wpxrx9p2tnnxm8y7a7emfj8c3gst", + "name": "genesis_6", + "blsKey": "86bc497e250f34a664a3330788292ee901aa286e10fcb280a4a151a8741bc0d154b947a4d3cd9bc5b552917211081466", + "proofOfPossession": "97a20b81bdcbc7a4f228bc00894d53d55fbb2c53960f0ddc0cfa0f77395a33858a9907079773ad50a220cbdb49bc1d171250df83dd70572c4691eb280ae99d4501b289676b6bb0ad0e859b525752015bf5113e49050a8c70853470f2dd7e9344", + "generatorKey": "1d224ad4cf64a3db52b2509c5b63365db970f34c8e09babf4af8135d9234f91f", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskvq67zzev53sa6ozt39ft3dsmwxxztb7h29275k", + "name": "genesis_7", + "blsKey": "9006fc2c9d159b6890047e9b26c700d8c504e17b6fe476a2a1ac1477357c68eee332be587da425e37e22332348ed8007", + "proofOfPossession": "945ac6db93666aa21934d84c6ad897fe1acf1d208a17ec46b0ddf26cf6d9cdccef7db9eac682195ec47cb8e7a069bbe10706a4e1cce2012aadd311dafb270c9c810d80bc82c2b6c34ce236efac552fa0904b96533772f98e202f4e6f47c97f09", + "generatorKey": "8b65dce85de8ed215a91477627b365ec017a01cd5a715337f772ba42715cc794", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskvwy3xvehhpfh2aekcaro5sk36vp5z5kns2zaqt", + "name": "genesis_8", + "blsKey": "96482192c99ac4569b2d139670e566ca5ccf41f39d50b7ddcf69d790bcd556e797614ecb3dda2017e5e3ac2bab4e82d0", + "proofOfPossession": "865e6e88cf91b061b92f2d499936f384c9a3df52de5717661b66c4fd5150f1b171350c6abeab96fb905b6294ca7694420728022d84f4c31180f903a6ab8b5b8153fdcf65d46c8a018e65c0459e64c931b6544b6f00e673c30f2a82402fe8be3c", + "generatorKey": "20a50d60059dff36a6f6c922f55b018d288ba1f9df5120eeb8fa8e3745a800ec", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskcuj9g99y36fc6em2f6zfrd83c6djsvcyzx9u3p", + "name": "genesis_9", + "blsKey": "b244cdcbc419d0efd741cd7117153f9ba1a5a914e1fa686e0f601a2d3f0a79ac765c45fb3a09a297e7bc0515562ceda5", + "proofOfPossession": "b7a186c0576deeacb7eb8db7fe2dcdb9652ea963d2ffe0a14ad90d7698f214948611a3866dfedcb6a8da3209fee4b94a025864f94c31e09192b6de2a71421e5b08d5ac906e77471d3643374a3d84f99d8b1315f44066c044b5cdbfdfeceef78c", + "generatorKey": "80fb43e2c967cb9d050c0460d8a538f15f0ed3b16cb38e0414633f182d67a275", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskc22mfaqzo722aenb6yw7awx8f22nrn54skrj8b", + "name": "genesis_10", + "blsKey": "a38d728c1c1023651b031835818d17d0665d1fbabd8e62da26ca53f290620c23fe928244bcbcbb67412344013017cb53", + "proofOfPossession": "b5d455bb358eff87779b296f23a2fc9abc9d8f3ecb8ed0d9af3e23066e653a58b189c11b4a3980eaeaaa85ffcc240795187f6e8a0e8e8a2837bc20d485e1d3159c2d581614d72f94bbd049e5a9f45c0302851c87aa3c3853d8962ed75d140234", + "generatorKey": "671c72129793eb5801273ff580ce3d4c78d89fc8b4fb95b090a9af0a9a647a41", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskchcsq6pgnq6nwttwe9hyj67rb9936cf2ccjk3b", + "name": "genesis_11", + "blsKey": "8fd004c33814c3b452d50b2bf6855eeb03e41552c6edd50b76dee57007a34cf987da1e06425cf498391e6831d1bf6851", + "proofOfPossession": "a0e34bdc7dc39e09f686d6712fd0e71c61c8d06dfedbdbb9ed77c821c22d6c87f87e39e48db79aa50c19904933abb11a0b07659317079ae8f2db6e27b9139ce0830faa8dad2dcae2079f64781b0516be825b2d84689080bb8219a5ec72ba80f7", + "generatorKey": "be4e49ea7e57ede752ce33cb224f50277552f9085a551005255ee12a9b4ca68d", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskp2kubbnvgwhw588t3wp85wthe285r7e2m64w2d", + "name": "genesis_12", + "blsKey": "98f83f66e857d954d5c5a49403e5b3a622e1bb855d785845e72faf0f7dd03ed3fd2f787a38c57f6968accaf780fd41fe", + "proofOfPossession": "b3131f0229df11964daba47a79729542f10672b36db017002df90d2cc6a79c8b44d032935bd214bdf69a8db181e4315a15de71a2e6802442536143c3ace9886248d502d6f38f9ea5bad26d4cee729b909d6cbde541c35313598957ddda08de15", + "generatorKey": "56d64ef16324f92efce8b0a6ee98b2925dc485d45675b2012bbf6a96d7431a36", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskmc9nhajmkqczvaeob872h9mefnw63mcec84qzd", + "name": "genesis_13", + "blsKey": "a029f74eaf914e3dfd828502f224fff7311a964d11eb1c335eebadc38b5c20a98f79bfc53ccf6ee3630cfa282e88489d", + "proofOfPossession": "b5cd13eac543928db25ebb9d69dfaacc04a0d41924f2010a6f04b2457523a5a423a9c49756dbcb969a7b2c49ddcc7c710ada766fdddaedbff02f68e2b75108f111f4078d2705f06551ef524f201d50ac32c423d04a7e6e7c6c8a64d70c013ec3", + "generatorKey": "b67f0a9ad61ad6867b54aaaed6036001485d7a7ba13770aed786b34241f37cda", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskm8g9dshwfcmfq9ctbrjm9zvb58h5c7y9ecstky", + "name": "genesis_14", + "blsKey": "8e3f9dd02f46bbb01ec1ffbe173b6a28baa3ffaca943afe51c18dc5220256a3994cd0b0389c835988a64076b4e81c837", + "proofOfPossession": "980f00e7752adccb907eaea0fc31ce62dcaff9bf1c6b7066c5071829c91456a8d1e266cb0a9ef4916ffbd09295508a350d21e9123e5cc1c00d3ef65f5493c93c5b993e9768960d4210849743dc2b995657cb0aee7d46d6482e3545b89f06f895", + "generatorKey": "497a5b80edc6b9b5cca4ca73fd0523dbd51e41c1af5f893e301cfa91d997573a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskmwac26bhz5s5wo7h79dpyucckxku8jw5descbg", + "name": "genesis_15", + "blsKey": "adeefe5ec24b210986ae56ac2d1eea5b5447e38d7c9657d4948ee2d9b312a247ba40964a58c3fc14e5fd7137602e631c", + "proofOfPossession": "8ffe03e68c8b3ec929a4934d61091ac1c8f42446076a7ef6e8141082ebf71fd3153c35c1745619a08defb0ca8fbe583a15190f88dbd93d22d3c4eaf3fd60fa2d9cdcd8824bdd289111ca7d537563b0e2fa7ad06cad40bc2ce17277a63a3138b2", + "generatorKey": "a7340ac2220b35dd5c97e6ea45c48cfdfcaccc4c59abf9b7f316df8a1bd7e8b2", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskmadcfr9p3qgx8upeac6xkmk8fjss7atw8p8s2a", + "name": "genesis_16", + "blsKey": "a13d3a62d053b3a092d736f3c96c89fb982924b9cfd1e8283c4ced5a537732718e73c6c86c94ddd416eb94a753366b7f", + "proofOfPossession": "950583faae3492f5d15f9ad72bad982b2f513956cc1259e16e28ef2e18f7db3df1bf1cbab7350e390ac5a8785c574fe30878784e6c5d50668184c4c92bda196432034a7e092d9e62736ca543e1b7e594ccf6b81d37c17fabf73b846b67a0bc8f", + "generatorKey": "ebeb7f828aaa40ab6040e914b66b6f5d76964a0579bd29bf98c2641547f229f6", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskbm49qcdcyqvavxkm69x22btvhwx6v27kfzghu3", + "name": "genesis_17", + "blsKey": "80d7d0598d4e79ceea22c56d16e747cd5ef94469bd036945d14a5d1e06eb700f9f1099d10cfaddddf9e88ac4c9f1086a", + "proofOfPossession": "b7890264708b9d3341d90864f9120cd84090592a6bc5a419df94e86a638a0055e7dc3846cb89869cf46305611e49cea007711f35a5effd3099e56b5108a4103215a6ba9195c4694064ba661502e852b43e9593b0a60bcd2b567fc97565054500", + "generatorKey": "4ec3ad70d3d35f0d684960e7938fab016d12c6c7cbb8312a8cff776dbaf2ca4a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskbr5cnd8rjeaot7gtfo79fsywx4nb68b29xeqrh", + "name": "genesis_18", + "blsKey": "968afa71f5ba87783db371242b48962a93c91f17ec6fe2b52260c43b7db62462fc88de889445390024abbb1de1ff87ee", + "proofOfPossession": "b3a05e96a9fc1ba05cb80ba48e8f92e6d6d282408d77b16557dd0c8bff8bc963539d5a355cb1544e35269c4fc58f5c0816b4bc3e215d6441f06b9d2e6cd48ad5f08c5bfb35f359fe25ebcc382985bcefce0698bd3a89e655706e46e394c83693", + "generatorKey": "552ea15981e9fa54f2b65c409e8d32c350435893744fb9937875b1ec0e3025eb", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsknyuj2wnn95w8svk7jo38jwxhpnrx7cj3vo4vjc", + "name": "genesis_19", + "blsKey": "a0fb290e74bce8c5858dc1b615bac542d2280a477912ae06b8d4f07c6d451eae44a47cae6a7a1fb5cedea9efe2d4e5a5", + "proofOfPossession": "8b1a7d2b1566ce81c8ac2b8c88b6966b960462d0fa4e54554f53ab184c31c72c65fce904aff79d4235dd3e16e8eed2780e083a31a432e70a538de1b81d8a8a49d31bdd361f357d57fe4568d1b506492fc72f42d4b344ecfac2d560bbd2214621", + "generatorKey": "4325779e64521ded42c0e2e873c16b753433d0e7f9a1e046e27a0fae9378d9c9", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsknax33n2ohy872rdkfp4ud7nsv8eamwt6utw5nb", + "name": "genesis_20", + "blsKey": "b29e90de05487e087cb37f34213ccc49edef8936aa15001686f947dd26b2e4c71b0c094c633067c75d3d0879c0347a45", + "proofOfPossession": "9866cd99328ae5d1a14f899b95782b828b404c941853f4d0f0f56a113867f9f44b177af5c6eddec16b42c405967e52c90e3c2b0acf4921fd7ad27bdca498980aec0d37923e95d56555190caed7644ac158b392af052a49a8d1df626ea3a5f034", + "generatorKey": "473d332bb27f1dab55191233884f37aaf17545b1883554b1457b2dfac7c02b0a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsknatyy4944pxukrhe38bww4bn3myzjp2af4sqgh", + "name": "genesis_21", + "blsKey": "b0d3f0d142131962d9ab7505a3ca078c1947d6bb2972174988feddc5d4d9727927ff79290af7e1180a913a375da9b618", + "proofOfPossession": "90f81a87982cb983aae8c240f12c77306501bf67dcb031161cb2787ecbecfdc0ca4e62365f750714b9b0a64c10411058105bef1a725ece1c0e7c45b7e1526494d5a02ceaa4f624116a91188e7ca2503e0ae17748b11b05cd79ccc204d20e418f", + "generatorKey": "f8d382ac4f19ffe2ac2fa91794b65dc4c03389cbb2ea65bab50379a12e0f98fb", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsknddzdw4xxej5znssc7aapej67s7g476osk7prc", + "name": "genesis_22", + "blsKey": "8ae81737f7b1678ece4b06db3ee1d633637da3c02cf646cdb0c7c1dae5f9eea41f2384fca8b0b12033d316ee78ea3e94", + "proofOfPossession": "a5150c19ac23dc15f660d9612be5f9591c1a5fc892e9f8b267de6bd39da84f254b6644e8c0f294900e5e9b7c9ecf3f260d902a56af7db5a59083eda08dd3ff083e2a07ba5d34f25312621f8686358dd2a50dcdc879eb0f9d50ff2fdc704e7d9a", + "generatorKey": "3c19943d614f67309dd989e2e1bdeade5ea53b0522eac3d46b9e7f68604a874d", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk3oz8mycgs86jehbmpmb83n8z3ctxou47h7r9bs", + "name": "genesis_23", + "blsKey": "8ffe1e957047e7dd979e8bcac9fcea9411ed3be947679ce26a36725b08da51ed2fa19e7f7c6bed701bf3e33a6f787b8a", + "proofOfPossession": "89177926eb5ed8d2be150884e0cc4eaf02a040a3ebb0af9df6922d8d7fc58da4777cc6591d3d43570ce6410077d087fe097cb30f28a164d22216859988f44ef88bc7f4a2134f882d044e4ee66d135a31cd063934cf6b4e820fcff3bbfc5b27c9", + "generatorKey": "b9bbcd67194a7091a517faf37a7ec0fda068c4ac0dcbb8ddf526de97e67716a4", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk37kucto34knfhumezkx3qdwhmbrqfonjmck59z", + "name": "genesis_24", + "blsKey": "94c8d9240de83f6b09905756fae29c2c3aa9092649776ebe037f20011b3bff835944eae63b2dcf6c3861f11d457a875e", + "proofOfPossession": "9900c9235a0365b9a0b5dce686903737cc4aaa76e8f9e47367954b07ee3a0c0ab51351cd746966556ddcc53e69eabe0c025195d1d3a6788d69c1820bd1fecc096eea09770fe43f86f898c6182ce3057fcd52b43ce096a07b4da3f2369353988e", + "generatorKey": "edec02268c216d131fa9ec045049e6ac1526f48da772a34b1536c88c5af223da", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk3dzjyndh43tdc6vugbdqhfpt3k9juethuzsmdk", + "name": "genesis_25", + "blsKey": "a1782a5f280f9894cea555d6f355c1f23e0581140c64f20ae469edd6ace7dcb6266227feecf002c2b508766e730c6f4f", + "proofOfPossession": "84e053bb01b22997e46ce4cbece0f5478e27cd49786cc36b1459c8930ea408e663bc725184197eb726fadf6988503c9b01be391ca3eb16587137cf5a3941717837baec7869896bae401bb513359485142778a52638429328f06a4469b7e21bb0", + "generatorKey": "9f1c361befb0ae35de28e8f0e25efe75ede78aa26c703625cc17e7fe2e7208f3", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk4nst5n99meqxndr684va7hhenw7q8sxs5depnb", + "name": "genesis_26", + "blsKey": "a1a95b1526c3426ccd03f46199d452c5121481cc862a43bfe616c44662b9a7fa460fcdc5f97072754296e6da7023e078", + "proofOfPossession": "942c76c56af0112baa7a11bb8875a2336b321e85de56fd4267e97f3fb142445648a54c97ed22e5860fe5b0e5ef240599028d4009d091ad96ad727914532e45ff9eb44303b337f44bf5ed3ac796e6e22a9ee29138bada893f89f3bebc1a4daad5", + "generatorKey": "71ce039f0e4502ff56ca8d33f7ba5ba5392dd7915516b2d87eb777edef454377", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk67y3t2sqd7kka2agtcdm68oqvmvyw94nrjqz7f", + "name": "genesis_27", + "blsKey": "a6d6aa277ab636486b7d879e90c541b4952264e18b8a214f58d32226fcc774a8e5bdac69223902424110cbda4ab58907", + "proofOfPossession": "a5b91b5e3881a36ea1b209f1cc09ab447e365b111e7529a88981e4e44c4a05eaee0507ff80460453e23187116510dc770d517e16aafc1de2aae2393ddd2e26cbe6fd096b65ba48cb6dacd0862d6c39b394117a596c0a1c9bae8d9b538d6e6dfa", + "generatorKey": "74f7ff53b55eda8fe9c11d66f7533c27714b121a5918a66c19b309e1c93dc3ed", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk6quzyfffe2xhukyq4vjwnebmnapvsgj4we7bad", + "name": "genesis_28", + "blsKey": "b422e4fa8ab196e0bcc49f956ab3b5c13dc14442864dca80118dea7329308e7f7aa7547df293c826a29ef4bbfe517778", + "proofOfPossession": "8ce0fe2bf47180e74f315fda7bfdb376a277f394667c88661dbefcc57100af1d0a06d36ef406f7abc0282a1cb8f5091505d759a40739b11b4a1fd0060e2066edd79ad417168a977f1a59206ddac4bbabaf70feda572bb19c17b9d9034bfe28b1", + "generatorKey": "b5308c34412c54e4b8358b5fca16396084004ee37c6824c1ad751cbe8e50e24f", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk5pmheu78re567zd5dnddzh2c3jzn7bwcrjd7dy", + "name": "genesis_29", + "blsKey": "809c35a2a1f510fb574a223474fb6b588daca95ab1b9b04f4f0dcdcd4581f05914eb1b9683d21997899ebf730d82a8a7", + "proofOfPossession": "a2fd6eca6018825969d8b9de58e6594149c5114cea9c27997f2ec67b923cbe562454caa5a5e956b3eb5ea0c5bd9b0196137d4646e21b51bd21503dde474d510f62654bb7ffd141fa3462997bc6662f2893cff7d917eb07f2985dae860723bd46", + "generatorKey": "62c37caa9ecdb3874354e7f780cb4463ad190bc31e75e552cb07b9bafc658f2c", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk56hpjtt5b8w3h2qgckr57txuw95ja29rsonweo", + "name": "genesis_30", + "blsKey": "906653b7a74dc35499e0c02f10a9d092e7dae70e5376287b5533c7a52ade678784956e6bcbb67a11239bbfa977743a1f", + "proofOfPossession": "a5bdd92d340281c01d90224ca58a13cc429dc47ea9d2ef6226b023ff926a43ff0a50a82028e1fc20e9faa380136f5dde00a70d7170a8de3246e39b7787771e41271351dcbf4f88b6d40dac77b2e3324a371f9fc08d1fad90fe3e5cd61caae5d8", + "generatorKey": "d19ee9537ed38f537c2e8be0fb491331575f8e4050dc4a74ccee3244714d5969", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk5y2q2tn35xrnpdc4oag8sa3ktdacmdcahvwqot", + "name": "genesis_31", + "blsKey": "b8396076f1ae032b572145f01ea0a3b5418f226afb0496930cb68250ca59b16fe2fb6dadacd88132b9dcd19a07d7f773", + "proofOfPossession": "a096515a639c004e7aecee3e88ddbb572163b914de63b528db584b27fe6a0267eb95213ccbebea849a720f1f717871ff191a4cf52c9d0a4db57cfcf8f2453d22cd432a5fe64dcb45982abe84343608a8b22740f7f3fbdfe1000fede5f0a08db3", + "generatorKey": "4ae9069cbc0e2371b037342010c5ddbd9c6d4a8c8d0a9eae59bc6a3796866119", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk5rtz6s352qyt9vggx7uyo5b4p2ommfxz36w7ma", + "name": "genesis_32", + "blsKey": "8f96883db13e4f43e7280d8a58e7642228f46c375853a17e8cdb34fdeaf4e363a82678d2f54a8630218e097ba39d4370", + "proofOfPossession": "91a2efa4a407f63eb9157a4f4378bf6dfb4fc6d5d2714c2ee81f49ac90bc5dc3f1b72051a1fa1615f2e2d694cf17c27c1429e94bebc023feea2a405f7a8343dcc567636d15ac95ef84b1c673298becb766e036d9869e2113d9f4602f6e6092dd", + "generatorKey": "d1f10929b1eab8232be9df3b792496eb56bcb5c0a8c2fd04e3be1fab26c7980e", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskoys3dpcyx5hkr7u2fenpjrbyd69tuyu5ar4dgy", + "name": "genesis_33", + "blsKey": "a4f78f9b10c5671cca5aa2526708b95bdec56f3e404fc6c6403de83338940dfcc8d6836ba3d98566d314d34438a042d3", + "proofOfPossession": "91a1d0b501b7ab2caa5d240eae92c8c0ccbf296ebd3dd9d03aac1ca569f803091ec5ab57b7f6c34ad1aeb9aee0ccc17a1911c8e7a9ca681a6b803bf27e303f59dcfa32f678c4bb35189a8b7e0a3af43771ec841bd2ab32a96cb2eab0a1c2ad94", + "generatorKey": "3efa1c0a728a9741555b84ff1d80aedfcaf85370e1602890d7ba610bf33500bb", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskoq2bmkpfwmmbo3c9pzdby7wmwjvokgmpgbpcj3", + "name": "genesis_34", + "blsKey": "882662250af65099ca817b2564576582981f23746f07be09ebc03ed6aa582a327d4156ff4a12851bce3ad77be854f937", + "proofOfPossession": "b73f34042d210b6cf0ba61b04e26bcb08e4d671a12df09e592c14c73ac55df09a01adf94b205b86a9ac9020cc719e93b0f890050891d9f8622346f45112ce502e26293a14c36501a8f1947c33fa38535d6eae6c4af6679296e76a105e899341d", + "generatorKey": "8cda7b8df8975d781e053882a1373d190d5f8fd7c13ab528be8597b5d06ede57", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskowvmbgn4oye4hae3keyjuzta4t499zqkjqydfd", + "name": "genesis_35", + "blsKey": "ac304b4ad4fdac88bf975496edc43af0e324120984d5a12ac073b3e3e80c593470b6aa4f10b9897451bd6ee6f569a2af", + "proofOfPossession": "b08e154f3db163391dcbef182a63ad51d56521951307b9bcc60f12c83babeb5eef80b6d8503848acf9bc864adaa82bd610e3145dd77debdfcaa8e1e15f13e6da1d5bcfca4234b46208900c6ce35d0147534a7abc728504d731f286edc31a3ae3", + "generatorKey": "f926fbec6d2e461af7c58d87754524abd26ab1f617d73348ba1318d371f7cac0", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskos7tnf5jx4e6jq4bf5z4gwo2ow5he4khn75gpo", + "name": "genesis_36", + "blsKey": "87971b8a0520e08dc8dbb8114de7ecd44e98844c9179585806e8a1edaae1190ea85e6471767e90074d87d1dfbafc983c", + "proofOfPossession": "ac1fa23a608ce0be52ada7759c4631a5e3c7828a2a622c718b67c4d8996eeed61c382ec319ff2c608290c141ef741ba013f7567bf95cdfb29295dea31adb440f5d856f5688fdd553f47a06ab5692ee5fb99e5a50b329fe4406bfefb924b5665c", + "generatorKey": "d5781773a9b07a569a0d87c0bf82103fd459a2185fc32f5c312a663c5bc65784", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk966m5mv2xk8hassrq5b8nz97qmy3nh348y6zf7", + "name": "genesis_37", + "blsKey": "b847749ece25a2ef51427de371b4efc2342fb38a2c5822b941c1dbf43c3f8dabf5dc0e1620d2bdafb597d697e30ab801", + "proofOfPossession": "831a557a972e0ed1a9cdab88a13fea899ce1b7e6475ee2d42a1a1faa09fe9042eaab3bd8b14f2faf4ecff84780b8db6719e8d6bc8917ada1f77182b2fb4a40b544c02486fe0394b8fcc72ac69fcdf3d6c0920469225bf0ad2e047fc68b9376a3", + "generatorKey": "875d9a84adcf997034d5ab6189a063d9817da3a6c8599cc46c84b70b5081b18b", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk7drqfofanzn9rf7g59a2jha5ses3rswmc26hpw", + "name": "genesis_38", + "blsKey": "a6d6315e85e8138de21f94d0c5c6f4c2515d493b17653156745155b25f9f121f6d13e7c36a57fa5002a9aa0a0b282394", + "proofOfPossession": "ac38044b8d84ed22d42da3a240b7c2dd16fbdf3b03655226b46b6eea46256a3ee33232771d67da1a4df6717476349647077f5cb29715333d8c55f5b6ba70c77af1944ac54c913445da29c99dd441e36d9def69c0e9709ce062ac70e4d15628a9", + "generatorKey": "71d5b4b08ea0b7a0ff95f779aec53590a3bcb5a87fc770334f8c9ee57fdd79d9", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk8vjsq5s8jan9c8y9tmgawd6cttuszbf6jmhvj5", + "name": "genesis_39", + "blsKey": "837e0759968b1ed95789252d1e731d7b127c9a53a74e86f3ca3d65d71cf666f2208baa782a42c45d4132630100a59462", + "proofOfPossession": "b97607b1478f17877b4c8042530763894dd7b79f8bbf5ca0883d08b94dc8a11cc2c2a73123160e3b01da692fb071f5fe0d808426604b5ad8aadebda9b02710698158254f6f1d822c2c9bae5c081101806e9220d79c547391e6fc6d8f26094dc7", + "generatorKey": "00110f493d122a73628a518842e99591b91def4ef9fbd58e1b6458950da5a776", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk8netwcxgkpew8g5as2bkwbfraetf8neud25ktc", + "name": "genesis_40", + "blsKey": "a3aa25a2385666122df82fa74096f30560c270b1ef981ff459e25cb5819d50a2edd8c315bf17a6a1af8d88c0e9325e50", + "proofOfPossession": "b543e0716990a65727b51489c90495289bae983d3a4439fe68826c2175b4396d37da0ff03910b369335377de097088720b77646a3fdf196e95c54f2ca6bd414327231996bc2dba0c1dcc7a77b8be10b84a4ef8947a0e4ba22aa09a6c025521e6", + "generatorKey": "fa7af9f8623b324e6c021b7a0899d980a41dd2de86c35cab530751eaa9e55a0a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk", + "name": "genesis_41", + "blsKey": "a84b3fc0a53fcb07c6057442cf11b37ef0a3d3216fc8e245f9cbf43c13193515f0de3ab9ef4f6b0e04ecdb4df212d96a", + "proofOfPossession": "b3de21449917e17d5eadb5211c192ee23e7df8becad8488c521dcfb0c67df64a81561653d92805b4bebae9e5b5bdef8717f1259eaeb55bd1e7eafad3d74efe20181b4ac84bb7582b637e605fe78f10eb03b2a4acbff49809e86d89aebc6076b9", + "generatorKey": "91fdf7f2a3eb93e493f736a4f9fce0e1df082836bf6d06e739bb3b0e1690fada", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk8dz47g5s7qxbyy46qvkrykfoj7wg7rb5ohy97c", + "name": "genesis_42", + "blsKey": "a2f8fdf2b80c987ae61634125c54469928728ecb993bab3db892725b16b41ec48c36056eeee2a1c9b073d12bdf917684", + "proofOfPossession": "abded9f3ad588edba52b7b2a4b3ff25f630aefae0d7a91827bc1fb7b8cba36d27c310a7a58a4a66ed9a8d90ffc0aae6e17718b1fa3f8e7305498e740d531460702a7dce1e32c19e18849c786c26a30e29b464c7202dd64d021c1eef643de519a", + "generatorKey": "567e1e27c02293d7c190a1eb203c2daf1935a9901de66df73f8e4eeae6907d04", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk8dsngwh4n6hmf4unqb8gfqgkayabaqdvtq85ja", + "name": "genesis_43", + "blsKey": "aa5174668a4743d838fa3742092c744c3edd4ee64c535ce2a69eeae1c5f23029acd74853410867d873076639f4ce1cda", + "proofOfPossession": "ad79b935bd503402b83404125ef11fab81f4c6bef0688798473e430f892704b653209aaf81f16efca9965fad0850a3971662f33c25994568e1434f4f46901caa1c002cab18dff7337836617c372673714d63b01ec4db098f419c027015aa4c05", + "generatorKey": "dd337fcb819073335382415bfdbf5e5b7e73126aafb0ac46479137328e72d438", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskux8ew6zq6zddya4u32towauvxmbe3x9hxvbzv4", + "name": "genesis_44", + "blsKey": "94da5ec9da5eabf2ab184de1e0ee10f63f721897475acd59c3c53adc51a9b39b0f4fa28573fcc309e576dba658425dbd", + "proofOfPossession": "a672d269ec605e04065fc0da8e6f520d0273b1c57a754409d9fb25cef1be67b8583fa683e27c0284c31105045f395c0c142d0648420b9b209fa88fa13025ba2b3887e04e3fbae1db6e5941ade41713a4384c139e47e72a68c964c4a5c0886d25", + "generatorKey": "563aa06b554beea30fc4455ae51e0954051a3457315b2370fde9c22d3233b522", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsku4ftwo3dvgygbnn58octduj6458h5eep2aea6e", + "name": "genesis_45", + "blsKey": "b9dc37e370cdbab50fe906b675551194e80705f5549ec07f32b95b85ec1ee1b149d156e649ebe1eac57bcc2ce9db3e56", + "proofOfPossession": "abefcbf20c53c10ac15054527c2ca691994f0b5cf60444aef49ba4e39312774eaa073be6b887ca5792bbfd53adc7ec3d0b0f6b34ec8a8f2fb6708d5a9d3de242f5fcccc3c3cddcfc5eb8be5aa13c333d114c091f594736e7a43d7d9212d0063d", + "generatorKey": "894289ef63ad9f51868d06e700c5dc9cac7af2e6601a99449134926cfdbb4340", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskuueow44w67rte7uoryn855hp5kw48szuhe5qmc", + "name": "genesis_46", + "blsKey": "b7c47fbb0d7e3793460949c9dd6120a310eb52de67f6cde55c022b05dd5053074c8a0e562896a482c787eb2eea82353f", + "proofOfPossession": "a265237ff848fe7acb4c84b6f68008ee7ec917a7a11c050f630b834e5caf22a447de94de0e7c52d03b18e003e5f9a3f2091cb5a78817ba42a7e19c714af47ad0b94824c5b90862059ed3042446143c56c4df011389eb42dfa2daa58df677d473", + "generatorKey": "ebe1d6189c7015d175414db9621a602b0912826c1eb1aab09e69bb33ca8fcda5", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskym4rrvgax9ubgqz6944z9q3t6quo5ugw33j3kr", + "name": "genesis_47", + "blsKey": "a5963aa24ed05e95d19fd9de35ae6f523aad987ab2b9897216091e798e15f5062e9734b11fcacd6b8f312162ddc10940", + "proofOfPossession": "8a1ae28d6d70bfa0dbcc694c811c05ac6e697a17f41d45a32e1cb5b225bd42de7c1043f4af3c17d92641c4d017569e2302dad3e32493294831da564a07154e5098129639deb89743d1146f8e01f9f6f32f382905707051467242b646d86bad05", + "generatorKey": "4514d1723eed164b3792f1950d3b1c7a1067441ba207cce8d9bdd6f436a119fe", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskyunb64dg4x72ue8mzte7cbev8j4nucf9je2sh9", + "name": "genesis_48", + "blsKey": "870db2da31a9471077677bd9a7529ee7523bdd64fdba46c514e94aa52e940566479cfdab29b07c1573aff6ba7040c684", + "proofOfPossession": "acbe270292cfaa154f256a83c9bdde889a9205c85c5ff0f41dae586dccc7f29f0464fbc087a5c5adb3cb4eca3b95bc14187db64cccd24e98d3e75215b69bd2bd0b357834c1ccacbdf91556fa59a86d04d1fc8aaa3be2ae5256aea3bd36d26942", + "generatorKey": "a9b0c063fee99a903a55da57e3d16f069145e414b62e25dbbf218bd608a61f7c", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskrzuuu8gkp5bxrbbz9hdjxw2yhnpxdkdz3j8rxr", + "name": "genesis_49", + "blsKey": "b1b4ba05e7116670be55b6d9fc28574d142824175a1e3d1cdafa37f193c342eba1a85d8520a9fd962811fe63a5a2d048", + "proofOfPossession": "99f7e39908f0cabbfd156c78a903d6968c455f5edbcb878525abe1217674d9745da87057f1fa93ccff79632253d5b4fd0c6301b0b9eb0e07fdd4c0abc99da0229ceb4a03b0da237657e445a7bbf6877689bfc027d65f24f05982dc2aeb34c72d", + "generatorKey": "d454f04eb0e05c980f6a3427e98d73493665860ba7a29eb915cfc0b8daae2849", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskrxweey4ak83ek36go6okoxr6bxrepdv3y52k3y", + "name": "genesis_50", + "blsKey": "8422c22feba709265c30a7b86a9ee9832d6b32fa4c9dc091c390e1b15e278f9009dc5d70868a56dace1ff622e9e634d7", + "proofOfPossession": "871ed33b68172b0ce40a3ec98d6fa9b3fd77245c2c1cb7f1071101cb459d53b05fc0168597148f976ceb1ded71999da8094fd8783cf27d1e21f9b965164573c0ca849210bd1e99f4706ca6f43636f9ea535c333a36c4267a598dc58c7c7fc108", + "generatorKey": "21120ef22b7df438e06b3862d3f0ab99d5704b3c61c45a544c64c908da8955ad", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskrccyjmc8cybh9n3kgencq8u7fh796v2zfraco9", + "name": "genesis_51", + "blsKey": "8b436ed371b7af11b31347c12321d90a427e9aa8d93275a27faedcbe2dd06c5dce1e1a4a03b0ae030e5cd0106a942cd8", + "proofOfPossession": "b1dcf2ff65ba4096611f392fb56d104754927cba14ec3d193ebcf7d6eaab062c7ab770c512e815c7d52c37fa9b8622400df7939f4bbeb8566beebce1b13d67562f7bb6a01f988a501e4ef691b544cd05796010b614014ec3036b171c7392cd7d", + "generatorKey": "bf9ebe25faae5a874d97ad1772ad062ca52f63e48d806ef641e025a963224200", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskr8bmeh9q5brkctg8g44j82ootju82zu8porwvq", + "name": "genesis_52", + "blsKey": "a8271f9e8874eebb6d66dc139e984b6a6c71d2a7e23c6d7061bab7725e9c65f2e2123778130a2acd278f155440debde0", + "proofOfPossession": "84a3aeb2cc8329afc63f40d137b017ebcffe6df9e55bdaad8249408d01dad5025f1c83faecb53955ba5524df25b0d85e180f0335d0b5ac8c82c7f5fd0975002fe0231a83754c0034b07175afc426b17978870f8326cfe4694ff723e08d0b6a61", + "generatorKey": "8062134a09cc464fe9465cda959b402a3d4506a1c44b3f5cba9661d42e912421", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskrskxmbv7s4czgxz5wtdqkay87ts2mfmu4ufcaw", + "name": "genesis_53", + "blsKey": "98c4f0e2b01f1b6ed07035fe46c17a40fe5409b1461a2b697afaf869e2f8c88b2db297b9a149208109bab2da195235c0", + "proofOfPossession": "8dad459d6b312d4a6767695029525e95f04e3ee083de85d0db5d818d15d32ef7aecb57f608c2c10355e3ca6dba8018e5192862d80f00fe1f71fd396d81d6a7649221c50bc8336efd12dc1cc13ee3c3898617971244af6a8da5ccd9224c9ea2f9", + "generatorKey": "07614fd5036d099a3caf004d46a083d12df2024fc03ef29cec22e58d1f78531f", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskrgqnuqub85jzcocgjsgb5rexrxc32s9dajhm69", + "name": "genesis_54", + "blsKey": "ad250adf40b559d765bb51d65340fe38de9e4cbc839b6e6509d99bb9bb3f89be1bbb96d75f709f2ae9e715e6e6ce38a4", + "proofOfPossession": "8943f42818d3c3374d43d1aa0b427436f4edec3e760f07aea2990b99eb3ef69952d580df862ad9034062fab57c548164143bd3b77d16ae74fd8fb84518983dfd015146ac9d0503c858f0022591345c077656e5af22cc78f1d35a02ad1e74c8c4", + "generatorKey": "55d4c0e745954f0fba9629b346055060418961e7edce58c77bf2bcfc7f753d42", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskrga27zfbamdcntpbxxt7sezvmubyxv9vnw2upk", + "name": "genesis_55", + "blsKey": "997583cd4f633aa5aa5e616a75d9edc370d5e6eb77e2418c13648b435b0182cdb7787c7ca91ed3939b403fe59041890b", + "proofOfPossession": "95324d44556e3c61bd307a40c2ef7f3d988e0ea561e5ece2d2809cf078db232caea9df8b35d8411238fddfe83a6978a70ae88e29fa5b6322b73f7fc9756daf52aa6369e5e69c5b2304871bd324e8125a698e360e3d5f1ad20136370b8d9808ea", + "generatorKey": "d2b31ed942359b0c9cb696cae874a2dbdd6e24915dd8a5882c7c042eac1e6831", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsktn6hodzd7v4kzgpd56osqjfwnzhu4mdyokynum", + "name": "genesis_56", + "blsKey": "a97efbc836dd4028813063912bcadb52fdb8e4d2ba04d7bbb477d2a97e16167c5fa6ba75e482cd7a7d476d78fed1550b", + "proofOfPossession": "995df23eececc27026f62816bfd07d71696e2dc5751bafb03d50bd9c66d388c562d6c1357300e4d51e5522edc3cb5ae217b3607795baa0209c6e63db01b4b7c28452c15db1366764abb9d886d0a908da07d3b7b2612e263d95721ffccefb4aa4", + "generatorKey": "6158b2a5b662ce05c7864dff4c2aecf6109cdea1be703a79147450b082ea242d", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsktas5pgp3tofv4ke4f2kayw9uyrqpnbf55bw5hm", + "name": "genesis_57", + "blsKey": "a77de9989b5fab42dca028637f401953b9e0fd6cd61dc2fb978daafdb5478ac77d67a37135c67a2178b44e5a35a1fddc", + "proofOfPossession": "acafd4f724cd7b9dcaf166aaf212122360f76c2faf4d146e8d0014653c0fe09f750690ea2b9ac6df96300301fb020d3b04c1b79965cc8929e18bd93190a366851033a901e05850770cb69fc28146db719f1ac232a7947ead59e8d584eb3ddb79", + "generatorKey": "8307181cf9d1f621261e8a97a5b3b77d64a9a1f589a2c14e42b2380d9c2d6297", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskk33a2z28ak9yy6eunbmodnynoehtyra5o4jzkn", + "name": "genesis_58", + "blsKey": "a1dff3e7486e27eb2bc99d4343b57e06fb8b52f8c7b6ec6d539889afcf0c221fbadcfca65f2ad7351beb8a51e67513fd", + "proofOfPossession": "b6447c9e317179a9160ea0c11c2ff49c11e0300332c2c0ec0bf81e936af231ffc3b6628da3e01eda821ff15e9a523f3204b32fd4fcce988c2b73b56609709dfd25ec9df9e33dee073f9d26a82d268569d117ecbf7985e012a975fa7d3ad5e4fd", + "generatorKey": "689639f5e3808cc0efd5f8d48ca6ee6f9a7a1bd5f5776832cc9b448cff5d0aa9", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskk8yh4h2rkp3yegr5xuea62qbos6q8xd6h3wys2", + "name": "genesis_59", + "blsKey": "95087210c7145581fd8dc397ed12ecc2eb703eaa19dd837d7c8c54cf625ba00bf88608aa89170d703c77f7dcf6707398", + "proofOfPossession": "b09816fd6ec0b666e1f61bde72069057a11fc78d7fe8b85873b6d909aee15d74c637076e149ff279c587efa4e6a468900e2c4a857bc55978ea292189737f95e7026514ec5e9a117f31b8339d8becf3af1bd2555df6d8f2372b54b7381ff355ed", + "generatorKey": "db1c7c22ee495ad3553394dca00c62b85e78b58e78ca68bfe5027b3346f6c854", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskkqjdxujqmjn2woqjs6txv3trzh6s5gsr882scp", + "name": "genesis_60", + "blsKey": "95acb59c54e53f09d7aac37c2db59c6df0ebb1e38120690a9035c715dc9862995472c72e9f48bfb05e920494dc17e9bb", + "proofOfPossession": "8798b4e143b15d10965194d0350d95c374d214d14f6a0c750a1a1699f1221388f01d00c6b708167fc7fcf355591abe370ed45c55306fdc372d26432cba8efc1f83238c1f2e669111656ba61b4bff391786713c28f7d1c6e717fbe98aec2dfda3", + "generatorKey": "c0aa7af3198f0e3a6bf35c5be38e0f181827735b1c3a635e8db05b80b3647054", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskk2vnyd5dq3ekexog6us6zcze9r64wk456zvj9a", + "name": "genesis_61", + "blsKey": "8739c54fb8452db4ff1857649a4144dae29f7bbd3275aaa8f0f2559095a09510e38bb0155bd01d01349e7f1392132e41", + "proofOfPossession": "b78a813e912849e2583d6e774740f2bef3115f1d23576d206ba15bf0c64404b48208e7b2b5becfe2386fc1ad686094251707a7bf8902a10b8ffd207394ad26b64f7a0c5bb7bfc737fd836b160bf16c4d14dcc343dbc8ff7993391795ded7e448", + "generatorKey": "7ff8b45c5f6239306af0194ee41e047669e33338be3f8e6c786d90fb905c8b6a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6", + "name": "genesis_62", + "blsKey": "8d4151757d14b1a30f7088f0bb1505bfd94a471872d565de563dbce32f696cb77afcc026170c343d0329ad554df564f6", + "proofOfPossession": "90df1472d40c6d1279bc96b0639ff0b8ae8cef80a0538ef00b9fc3bf7816a541d2eb9349fb6a6f1a07d80504bdf105ac0726e6b01ef75a863cafaf5356dbc03ea1c90387f79d3adf15c8a44614d80e42e7a964df2eca83a871cd378f39513414", + "generatorKey": "b53ef930d84d3ce5b4947c2502da06bcbc0fb2c71ee96f3b3a35340516712c71", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskqxjqneh4mhkvvgga8wxtrky5ztzt6bh8rcvsvg", + "name": "genesis_63", + "blsKey": "abc1d1ef1f992a9fda45841079516169c879421f4260194c0a47e46afdb9f349c2a51e66e9f2ee8bf22231027584a6bd", + "proofOfPossession": "a16aa0fe3bfd5383c2fd874be4feb930f2c75f5d35d0e0ab314eb545a673aa1854ebfee7b15a026d5a9fb02842e54672149382f2898a0e12756bb949772b1316163ba774768c88fc90c2471afe94140d8d8f16974f2ebf050358cd98587b32ce", + "generatorKey": "a2b5e97ac5a5b3c3a7cd9b4401eca1f4e8da59fe567e229ea47e65bf40053402", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskq6j6w8bv4s4to8ty6rz88y2cwcx76o4wcdnsdq", + "name": "genesis_64", + "blsKey": "95274c1b15467d43a3b8a3a632a8fb7e1a2efbdf92559ef52ea6ff1b0ba1c7cc2f75ef357b2dc7f0130dc9c04aeaf4db", + "proofOfPossession": "a24ef42b04be7bcd65d8434b04f7118bf9566a0d3a36c732cf5b508ccdc12855754663bdb32c5d871eee8a0774a1331a14f25f3aeb6bddee7efaebd2214e19b7cca9f3d3bc7eed93b85b15f0a626117f24361d65688dfbe7267141f13d323d63", + "generatorKey": "6c99048cae450de8735dd410a5c8b0e4655afaebcc2c155503f890af51e067c2", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskq5attbvu8s55ngwr3c5cv8392mqayvy4yyhpuy", + "name": "genesis_65", + "blsKey": "957a970041ae9b29f33cd9baaf077f77049e664c8123b22fda3793252f71916c5df0b103ffad5cb75bdb2724d9ca3eba", + "proofOfPossession": "80d4fdac09ce195c9d685a751fb7cd9d4da7b9dc906348b4bb741ceb53f876afd0bceba75b36327a8cbd8bd3ca8ac2cc14b4fede3ce2cdac7f0bf0ad5e58840c64bdd0a0905cd6aa5da8acfcb33a931e469cadc27a42c2a04a62fd6ecca05091", + "generatorKey": "1819bea0ff11aa0cde16c5b32736e7df274f9421d912a307526069fa119100ca", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskqw45qy3ph9rwgow86rudqa7e3vmb93db5e4yad", + "name": "genesis_66", + "blsKey": "b40065dfa219e40c65c07d516158d722ec695abc91411ce57550c77fa2119e52b56cb74db7a1d805b631752e8f6b80be", + "proofOfPossession": "b7085c15521303140512fdea858231a040534a4b0c1dbbdb002c8df233634270d33e51c3699cf4956d165c0183f29a32070d8f4e00433ebcdfcae337a5f09f2c971ba97d5b35413ce032d2ec4084ed79efc917bdb75ded139fc9433df884a18e", + "generatorKey": "1314b7d167d5829fb535d15dfb5216e10ad2e5b6a349ae347aec77317b6aa73f", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskqg9k3joyv9ouhjfysscame66hovq42yeev7ug7", + "name": "genesis_67", + "blsKey": "86f828da4b3c129eb54d95bef7975281b30dd811f252b5792998718355c599aeca3dbb222678ee0af84b13f5af2400b3", + "proofOfPossession": "8e062f48ead9234b710dbcfebbb2e502ddff68e3d5be19a8e7e89b2141c76caeeae233999009f24f7b6e65f3774ef6cd09de9d5c0bb59a60ff6cb31b276f0172e35f89061f3c2d700543de5cf4d6e613ff6ba7d41c1379d6baefd844ef4cb517", + "generatorKey": "a9568912797914f590413c3156c9cff93c9c14193b01e7bf248195bbe8c1af19", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskezdab747v9z78hgmcxsokeetcmbdrpj3gzrdcw", + "name": "genesis_68", + "blsKey": "a03ba0f1d6bf9378681b9d96dbe8176cc0ab2a424154cbbe325fc279d02cf58bc15de966cb1e272312ba2b6db31a7f05", + "proofOfPossession": "a20a8edd978fe911da6c933d486cb9af770179ef5ee21ad869c4c35e63103cfc2ac17350ee2d35b4bbd487193cdb33ab0116fdf2f078f289fae2922f6a7e372ef8ea543d52ae74ae395dccf2dec2c40e6596c807a14c9fce45b320321f68c612", + "generatorKey": "44de3820f1a1a7351953d2d000f29cb7bffecf30582a8b3da2cb80c83b9eceef", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lske5sqed53fdcs4m9et28f2k7u9fk6hno9bauday", + "name": "genesis_69", + "blsKey": "92f020ce5e37befb86493a82686b0eedddb264350b0873cf1eeaa1fefe39d938f05f272452c1ef5e6ceb4d9b23687e31", + "proofOfPossession": "b92b11d66348e197c62d14af1453620d550c21d59ce572d95a03f0eaa0d0d195efbb2f2fd1577dc1a04ecdb453065d9d168ce7648bc5328e5ea47bb07d3ce6fd75f35ee51064a9903da8b90f7dc8ab4f2549b834cb5911b883097133f66b9ab9", + "generatorKey": "b9e54121e5346cc04cc84bcf286d5e40d586ba5d39571daf57bd31bac3861a4a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskee8xh9oc78uhw5dhnaca9mbgmcgbwbnbarvd5d", + "name": "genesis_70", + "blsKey": "929d5be8abbc4ffd14fc5dc02ae62e51a4e8fff3fd7b5851ec3084136208ceac44366a7313447858e3814ddc4213d692", + "proofOfPossession": "88e7331baeba342eaa907cfd7a1b5bc839a70e78b0535d68c40ddc2e4d5157f8d1ff55d29243fe2375fcfef5c3a2133e0a0d11f8b58041278a1e9a3a9e7986f906201df48987e8f8eda2e6ee4452fe58b54805e2ca4cc256d8e42083b70f79e3", + "generatorKey": "aed740da1a7204422b92f733212398ce881c24a4cfe40edeea6a59a0f6453743", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskewnnr5x7h3ckkmys8d4orvuyyqmf8odmud6qmg", + "name": "genesis_71", + "blsKey": "81f3810e7567ba9e1aa9fab7d5914a1f2ac8b11d952872b398930836f80395c934bd6e71c291193458de7de4382c913f", + "proofOfPossession": "a67d9d0708496d13f45fa3d3940954bdfdfa69814554a5618a388cab03a5e82210171f06b72b03966c8a5bd8fe3b235e06de2fc4c45333395c8e10dba086a4f50efe3a7f87f741346c07b22de2ba49eedc521cf53fab31e2033175ff3ca00f08", + "generatorKey": "bf5f4408df7a1cde279b3cfe7ba6c2e2600a4bb90d883b98ef8048ec344221e0", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskwv3bh76epo42wvj6sdq8t7dbwar7xmm7h4k92m", + "name": "genesis_72", + "blsKey": "8ae82e86c2ae47fe55b3db422b5f6e8a8ecbf4a33a0e910b4cc53d1bef0d66e3d19e8474a97ba58e31798c604758b1d5", + "proofOfPossession": "9215a181382a5769652e3818238e58496ca1c80eb6282b000708b2c9c19464153fcc8a541d8aa32378186b61fdb2183d15828ffa20e49a0dae0cb05e8c106f894a7ee7190c6eb60874477da236c05a275187bded6ac5a9c98656eb2199f736fd", + "generatorKey": "f99c543eeba441fdb22c673fa81878269c3b69a6366d8d51fb6890f2eb3118b6", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskw95u4yqs35jpeourx4jsgdur2br7b9nq88b4g2", + "name": "genesis_73", + "blsKey": "a58edccfbcbc35d6f9fec1535329a114cc5a2118945098c0f201345ab7de78d36a32014dbe701faf7d32b24f7a696d9e", + "proofOfPossession": "999cf3232240944ff9a14e6c4680fae450be8c0ed43fdbf8f92e7873b5482f88229768fdcfd86e22767ec1df3b5fa2fc0b08202ee4a343bfb19c8c8eabf74d44fa73c4517ad0a102faf4ae6fe87cd766d860408b51d31dadcc5674c92908c7ee", + "generatorKey": "e2f80871a5220be51352427077f6e93c2294d88be6b731b535d2ce9371274e7b", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskwdkhf2ew9ov65v7srpq2mdq48rmrgp492z3pkn", + "name": "genesis_74", + "blsKey": "8c5b12f5b7aeafb07e14c5264e7f7ecf46b3ba0e6f12619e19271a733e06e913044ea2e5c955eef3567fcc2d842bc24a", + "proofOfPossession": "82237a5371179107af8c53ef19bf3e0d055b70ddb689763e0a8ac6d82884d12c2155166af4aa92b66fa64b6a6d2bbe7602a118d597345dc100bd6983f072b9d8da7bd0699b0f3cb51f1ec5a9f2e2feb76030125272325e7f5885399f1d26c5ac", + "generatorKey": "cc83f488c03e58d083927601658d234ffd12b5cb6fe3151206f699d031dc4161", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskwdqjhdgvqde9yrro4pfu464cumns3t5gyzutbm", + "name": "genesis_75", + "blsKey": "a397bb33263b2850758a1b144401b741c1278b302eb8d27be6c61363d9cedafcabe05fbd7d9ce5e75a7078972d397e9b", + "proofOfPossession": "b22ed60a951702ec7bfd85482e59703af76c4c79fe2d3a3b81e737d53746543587d2932fcd5559d56f6530bfe48d23f5093aa30f3e299733cb56151175d22e21895ada290521908536d71480f1066bbeec7ab803376a4a81e4d7ec3bb4d71dc0", + "generatorKey": "902b7ed4708c476c7f0e96825cb06f95cbc86953130575d2c4589d8e3dc2f69c", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsk2xxvfxaqpm42wr9reokucegh3quypqg9w9aqfo", + "name": "genesis_76", + "blsKey": "81f7700c2115434acaf61e88b836be11986476751d6c02617d1087e7bb45798ac56929cb5f71c890c6159ff4d71cd1b3", + "proofOfPossession": "8bc04a899be3a7ac99e2ddda6567a0b01e21aaea8daf4848821e8233cbe80610a2f670922865f424e878add1de8c978e1913f95308a50693fbc88e991e6bcac3bfef8a1d03f89bb4dfd9c991cbf1c613f85203dfacc4376057f085967f2a7283", + "generatorKey": "621d52ac19aba86c4feef94c67ae62cfa3f6ac192177ae37be2e6b3205449c0a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lska4qegdqzmsndn5hdn5jngy6nnt9qxjekkkd5jz", + "name": "genesis_77", + "blsKey": "90f87fd2122689c54bcd8fb859c5b36d4b583272043deba66199ad181ca2c38cf48d453c46ec881e03d2b7e2e63e3684", + "proofOfPossession": "add6eb668bebf90fdd80b01cb83a31b02577b200c85845bd5260d7851c02d21aaaf6d040e6d6f27a8690c9598f92ba240cdbb6d7896d7a777c484d30ab48d71b1aee1b07083dc5d11a94416c4cf85e33ec3899b40e6222ac888104f80b8d96c5", + "generatorKey": "965e86fdfcdcd64879efe23705506faeb4dfc4244f93d47f4bf444966d2a0f3d", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lska6rtf7ndbgbx7d8puaaf3heqsqnudkdhvoabdm", + "name": "genesis_78", + "blsKey": "a94d3cbfde92550eccede718499df12f33a8ec9a4b386e4ca423161d667862f45fb06397b12dc6a6cbafc14b1cfad26b", + "proofOfPossession": "a474ee16d276d3478e1b7005960d41c0e271652f29c3178230b7fdf395801dd62196294b7695b3ccad63887558e0f27d0b121738a42cfe9acab07e6763577ad87eccb5b1d0cd725cb4a32225e79e864c238ce3c56b6db8960ce9fda82828d5ba", + "generatorKey": "f8252b40a65be6f5f6d0be446da5ab434bdc0a921fd0956b0672ea4a218d2d7a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskau7uqo6afteazgyknmtotxdjgwr3p9gfr4yzke", + "name": "genesis_79", + "blsKey": "aaec4e157b19c0a3f2965cc636f5f82cef9b3918c071e2c6e50f57ecb44587d58139595e8f4c1fc7f76b2f7c09b1b6d1", + "proofOfPossession": "866a031b5a2a6b0525053b2d870487ac2fd39cf2cf18ecf462bc19afc5ef52f129cf88624fac73057c5375004492dbfb0b8cacb906b3a7daa4d7edf99f10ab15a90b3b328e8ad6701e838a88351fecdfb5b32eebeb80fdeb8c0345d1b5257d7b", + "generatorKey": "00245e599fdad13ed0b064c069c71c73caf868a4635c0143963a529807f8728c", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskayo6b7wmd3prq8fauwr52tj9ordadwrvuh5hn7", + "name": "genesis_80", + "blsKey": "881fa9b753cb2f89d267e0615cbd1ad9664d331f21d89cef2131686b0af55112fe1ad4df7f2c085f78142e75d90d2cab", + "proofOfPossession": "898471d3356573d6445906d973f1876f1e38570b6dc9c875c88138b302806c071efbe327f66c6646f02c134c3b1b019d0227bc83acd0ca10f65adf1b8fad7c9cb383909a015fd1d678c6272e5317da58d45b89fc1c954641a61169bf1c1a1728", + "generatorKey": "5ec5a5a2c91414f5cc5e3354b58671e624bc88a39fdc8f128593daa06545d6cf", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskatntynnut2eee2zxrpdzokrjmok43xczp2fme7", + "name": "genesis_81", + "blsKey": "97a4b205ac2b65a2f17ceb49a763393935021629068fe8a8c299e49b986e79ff8cc959a7343b5d00eae2783b825ffede", + "proofOfPossession": "8a86fbb8e59ff0de4f2d717ff3c7b0f3f9cb4b14f97deeffb907428666005e613b02cfac0bac4714389d898236de2d5a02df536b511675d2cbd37dcac6dc33bf4cf2d9d43cfa710b3c695bcb8cd29867477ccf3b1e5b9e3afaf7d8d4e50930ff", + "generatorKey": "ce6bdb7380fa027c46edd15a072bbabd6b60fecb0e09589e20be560b333ca63e", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskaw28kpqyffwzb8pcy47nangwwbyxjgnnvh9sfw", + "name": "genesis_82", + "blsKey": "b279e1a3a5edcd1045682e7029045b70dffbae55c49b14391b9f776750193269b4fd1d9f0807d9ee66e264e08ecd97cf", + "proofOfPossession": "83a5128e710b91ab91f7726223120b389c1f77735c9c1d408c466b7f0484b020f0d2d50edc36d49e410141d8a509b132059142e250f145810eefce03dfdda25aa84214d30cdfb6ca11a929337bf53dfe4c675117c06e4a67206119ed1e2b2b9a", + "generatorKey": "bfe46727c386585d8d59c02efbe48d4c1a919ff07b87267156ab96e10ac730b2", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskdo2dmatrfwcnzoeohorwqbef4qngvojfdtkqpj", + "name": "genesis_83", + "blsKey": "82b478f1b884ee4c152490afc8b233d003745a58c236b00ecb3cea1022d59f04bf225266bbe5b0a5aa7da0a771a66acc", + "proofOfPossession": "ac4d05f93e3c374c83ab9cec2a5c67dff8a02298361584267968fad8f391af083b5041a020ce7a189fd8fdbf055a265c04f55e80a8dcf06e7b4e3358b347743f47d33bd5ee0cc4d4213995c46d6d4e1a61be929f571c1a0fa1c7dec805a85805", + "generatorKey": "bbc7ca5acae1d53e0a44a212f4c77c7601ace0e489d936c0b6f26a9fbb03601e", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskduxr23bn9pajg8antj6fzaxc7hqpdmomoyshae", + "name": "genesis_84", + "blsKey": "b067f711431b1bee09000b1c27fe39a29a5603471a6993d47bf56ece01a17fa4b00e92da90d80689ed2635e7e0f90891", + "proofOfPossession": "91f3d5519f94424fd59c120c05d9f2f34d8cb39e092e2a354f5a7d48e7f2e23b6a21b39a7a131954320d5dbeb0a419f10304fb857fae695c180f9dedd18ffa73082af5a6ca0c62c273915cd337570ecd8649157c8dc8836d758fe1e51f4faa3f", + "generatorKey": "9b4db295e88468a37e49445443fdc364321d620dc57afe8a5a14f07ce0717055", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsksmpgg7mo4m6ekc9tgvgjr8kh5h6wmgtqvq6776", + "name": "genesis_85", + "blsKey": "96aa1c639724f5559fb1ebbe5d218511fe0fbfe6681190cd953677c6b63c0e17ac5d9f09844845cfecbb4ab4bd5a5749", + "proofOfPossession": "82a60d6a2432fd15c7697094a89ed34a30dc2daa2b460bdb0fe3269362e1d85c79a3d2aa9ba3ffa5b1e80f983933c96f1402e95d34fb656d20f368428ba93539191319c70e6cf6f15c5cb9df9235d115d06e0e00d7a1bf64db1433ac6acb68a6", + "generatorKey": "f17b9b3bdee2ef63c8fb52d85ae07516133749a1d659bd032c3a078aca65ce7a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsksu2u78jmmx7jgu3k8vxcmsv48x3746cts9xejf", + "name": "genesis_86", + "blsKey": "884b03c63f8d095165b67cb23131ca1053cbc73739549aa2ee21ca0b2b925994855dd46a81ebc3dedb309ceadd013f8e", + "proofOfPossession": "b4879cd844644b1a21f1676bf671854afb1536c5a330c1fef26b2669238efa373f70815e01028506b5cf6b75fe77e79e0efb6ef74e8111c7f1a189d4b0bf4c867190aa57e670b53dff5951a29eaaceda788ed674acdf33eff228278dc61c3cd2", + "generatorKey": "37df5572ddb12b67b9aa5191ba9baf9d76a50307fbe188924766225d86958dbd", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsksy7x68enrmjxjb8copn5m8csys6rjejx56pjqt", + "name": "genesis_87", + "blsKey": "8a08bdac4af80e0d37ce01094440a82a7e5ac9ec893f9a7870d26a4ec52db8932f36384bc7c3d3e03232ddb7bcd1eef5", + "proofOfPossession": "b999cf63290a85f96f0f78326c0eb24c3acce4c2307e1a2f1d621cc75f621ccab510e42aade9b6347e95661475230fbb059cd9e4e22ae17ac73dee58a370159bc6b525ab579de9502b761010e97f6d00f60ddfed05e76a5df3dfe33866c1ebe5", + "generatorKey": "7fb2d69906c5076fa314a4e817ce424bbd4a7a21305cec93a12d31a1589dc90c", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lsksdfqvkbqpc8eczj2s3dzkxnap5pguaxdw2227r", + "name": "genesis_88", + "blsKey": "84912d2f185c2058be9ed201d970f435a408c8bb3a36c430f007b69632efb2f663b51df383be6eedb80c8768a70822bb", + "proofOfPossession": "aafdb397226d3a4a4cc3b7ac906ae7e3601310bd5d0e20a0682364312937e8e3e0c3b5846a53ee536cac2a2b3f556bff06c65ef24a32495dee9d38ee5b2012113d8f032d8dd0f3f5d9af50dbd307d0e7f66aaa165620d5292da91306b0a39aad", + "generatorKey": "21f9d60315c1baeb513b5f7324a1211723d36948b64806541b8855988f86111f", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskjnr8jmvz45dj9z47jbky9sadh3us3rd8tdn7ww", + "name": "genesis_89", + "blsKey": "8ce6c9d2ed4f223635e3bd85476f0d56cdbb5e4090ae22b10a7fabd08d231193cf6d9c4f5b400eb4b310ef270811e424", + "proofOfPossession": "b896aabbcc1a165adaec26feb72fc580d4a6512dd09df40b4333381d2536b5ac36d22e91469a976ae446a6291792cb6a141013baaaae12faff26d06c6a6b722a28635c72d49fcd50ac910ca01d760e80892fc5757a18597cd1ce7f16dbabd195", + "generatorKey": "25ae368be016caae7066a6ce9f2ad8e4220d328ffb860a6d275d878f4882c70c", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskjtc95w5wqh5gtymqh7dqadb6kbc9x2mwr4eq8d", + "name": "genesis_90", + "blsKey": "a6e64df0d2d676f272253b3def004bb87276bf239596c4a5611f911aa51c4e401a9387c299b2b2b1d3f86ad7e5db0f0a", + "proofOfPossession": "92ff87e4dfebfdee0e5572e94f62c483a9b4465eada10c3a6bed32fc92374dbbe89eed00117ddb27bfbabc5e41d90d8a0701fd215caef0233eca660d7a0bccdaf064356edaab13aff404aeb5264d8b68ab0808115e09ef541168364806a62d49", + "generatorKey": "633e1696edbd9f2eb19683c4f7e0d4686fefb1a15772a1affdeb49a44d8c04f2", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskjtbchucvrd2s8qjo83e7trpem5edwa6dbjfczq", + "name": "genesis_91", + "blsKey": "8c141e5d769c22ec90122f42bef1d1e7af2d94c1da6844bd313fca2ccf0543eab5f8c6752dd47969dc34613801dfb293", + "proofOfPossession": "9681aa250d714befe61d71f239a9b4c09ee102addb3a5e2c884074c7ba763b5c21e53aa7b12518d32c9b874ba1910e7a0bf0bd23ae99f57f6f464403b1151b3521a7a369ff94118a436e6aa767bd462d9ca491dd3e253862c21ff078878c354e", + "generatorKey": "3e63c0a5d4de4df114823934ceaa6c17a48e5a6650788cf1f63c826c984c0957", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskhbcq7mps5hhea5736qaggyupdsmgdj8ufzdojp", + "name": "genesis_92", + "blsKey": "ab0bf8a74c846dbd47c9e679ba26a9c0e5a7a5902b4f66cee7065b7487eba30262e4e5f0ee78d616d007021df3fbc945", + "proofOfPossession": "b159e28ea39b1119e4018ea19777497e1d3c4a58d1c2ecc22aa5b2efe60572cb32ff30bbeda9ce28b235fb55ab15aec206f094f37ff9a78a0931d55799c1c74a19bacfa8a4172ba078d7cad4f663a4708e47981044b1893c712c3707196451fb", + "generatorKey": "29e5cf287cb9c12b2bb77ef9dc673728132f9e3affef2d0de0d7db7905937435", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskhamuapyyfckyg5v8u5o4jjw9bvr5bog7rgx8an", + "name": "genesis_93", + "blsKey": "a2fc837b51e6dd740fc1530e6713b0f8c04e646e91da849517901f24d9bcc78c360223f1ad3692de2e96444008a67e03", + "proofOfPossession": "82d6fee11dc1561ffb5f36bf07acdffb95e5c329f7adc0b8937bec191350d7c4a158c7592a179ed86b9c0e20159e903100495fcd3fb5bee481e053775b232f8e0fce602e8ec6edf0fe8ba90c06e6215d7c73e88a626d2fe63c6422826489d72a", + "generatorKey": "d051790a70ffdf5bd80dc9cec003f8261128be1fc2135990accb13caeb3ed588", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskfx88g3826a4qsyxm4w3fheyymfnucpsq36d326", + "name": "genesis_94", + "blsKey": "93bddb296ef4dd5c832486b4603c1ed13805d2df1c6c2f95c8af4ae38467f1e741c1c2fbbd5f8e927b54250bffdf8536", + "proofOfPossession": "923415dc1db9b46715d284bd2a3f12313a24c1352bf0dfcdce2e0e0475fe0343d5cc9e463d5f04b99cb367e30e89f1371280d5897a0103658d710b07f8d9d3d8754043241a753dce60f2bdadcb9249b334e6f5a395cabfdb187f2739b512d46f", + "generatorKey": "028a30837b7eec19b02b06c3c2f4065290285e40a4870a677664fee3fe76d9be", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskfmufdszf9ssqghf2yjkjeetyxy4v9wgawfv725", + "name": "genesis_95", + "blsKey": "96bed36ef328566d826a6f6b874ce441ad34373487b4bcc2d48d76f2dd453e418935a7b60578c43b9c4dc954e9331a3d", + "proofOfPossession": "b4d80456953b5111777a74931f5691a6e4c0bc4f4d552aeee9ed1002903b366abab12e2d596a4387933ec676058ae64e15d7b322786d19744281028753b621ed7d49b6e6bf87983267d3208c3dc5da983d845a7a2822da4a085446172e823b28", + "generatorKey": "24bab6ba79973ffaa8569af2cb69b8495d20f0c7ce674814ee0615d31abe9607", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskf6f3zj4o9fnpt7wd4fowafv8buyd72sgt2864b", + "name": "genesis_96", + "blsKey": "92590fccb8c847a6957213682bb798d7d18a368515f070537e1f6cfd45d8dfc50863105db9d46189b92c0e0d009fe09d", + "proofOfPossession": "b0aa8214fd746ec04d9cc97e9641a7ad796ed12ef08c9227b5358cf3bd9f049af2ad5376055361c34d265e5d0cf3518d05113928f487bf17012d6ec4deb53e5112b72f2e4d8dc8eed4f68514a9c6bf735c9ccb9dade32ed589bea8e677135302", + "generatorKey": "8cab5125c910702b66a83240cf836b10a0f2dc3000536799300ed8f1ed9a26ac", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskf5sf93qyn28wfzqvr74eca3tywuuzq6xf32p7f", + "name": "genesis_97", + "blsKey": "947456674b5616341cc932afb30e42973dd17582a81e5fe958277efc828535cd7c9c778410c52e069ed23e4cf629814a", + "proofOfPossession": "872ce3383378215d3be299f32196e9cb2ae1f9e06101afbb9e7709eafb37eca8548f156bbdfbb120c2d06fdbfdf5455107f2c818bfbc9b4e9f5fb4c50f79b24f5fc84f9e137b286d71c3d588a7af684d36bf701425b25ece2d9fbacbadb58f4e", + "generatorKey": "d05b69bda8b5cd103c620a814cbab2f2a131dcfda6bd4cd568155ddb1afd423b", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskfowbrr5mdkenm2fcg2hhu76q3vhs74k692vv28", + "name": "genesis_98", + "blsKey": "b57835b4d3285a134730de7b29361998787c2b4853e7a5e15032b516335e81c0797a51d00e032585efa05c27d2345a1d", + "proofOfPossession": "8d9b7510b3332a22635815b809c3e1ef96427a20f15b3f41112af74a9aa1a401d83d625dc5081f51aefee7591d52afaf1451e78e4f3efe29ec171b8239af73fd87b2e8a1aaa8b701c3e5bcb0d609f098738d29e0af57ea010953297c9c9e19d9", + "generatorKey": "5812017e0d25131165ebc256f39ccece115fb58ad5fe0766f78054f912832d6c", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskf7a93qr84d9a6ga543wernvxbsrpvtp299c5mj", + "name": "genesis_99", + "blsKey": "a7283bff41249c3d2a0f065a27448a4c5acefaece74e51ec432c418c4bc8e6f0eb60160feec4729b9c0b933e9ec5e528", + "proofOfPossession": "86f1ac081ee08568266dc39727540a5d50f03e544f73d9a3ca60d87cfe9b6718832e07b2720d42e0e818c5fe2d45099a0774af1e6b123b41a3eb7eb3a1443d248a535fe9ef93f0027a8e8f44686dc33d677b79251c22022675395a347d0f3dbb", + "generatorKey": "ac34c0731cddab10726e634cec30294f831af045a0614733ac683ccdb6bc7eab", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskfjd3ymhyzedgneudo2bujnm25u7stu4qpa3jnd", + "name": "genesis_100", + "blsKey": "96a70c8b1343511359f7205313eac8c73b2838e25eda58cf8c13fa1d2689aee3df70522bcbd36e0bde958409b80cc8ee", + "proofOfPossession": "89564da089fcc38e4973cf34b5a8abbe8e822bb59f05633156d9dc0b10f2aad8d4621ea66023ec2a10d6d581927af3bc0746cd8293ea22c8db0068c127d38c4c2dcfe777ffc03e773083fd0036894cce7c2596301381941523f4f2ae97bb79e9", + "generatorKey": "326cb34aa214c4952f646d93af8cfbe58ec74db76db54484b5a23918cba8743b", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskffxs3orv2au2juwa69hqtrmpcg9vq78cqbdjr4", + "name": "genesis_101", + "blsKey": "a3e2b645a315827618e58c1eb66dfef3744c8111a0c7b0e8535a3ec31d78ea2630646fea1da5609988c5d88997d663fb", + "proofOfPossession": "b55d1c525f96bba45cbefbcadad16279c9f61f790dfc3e3c824003139f9994200079faf573eddb863c6ba1fd9b7d7364146e3f20579b065355c75691e06be2c7304fe48d32fbfcb5ef38f8ecaa6905e9ca6a7c1124c45a6ab2b06668cb3decc9", + "generatorKey": "4e54056fabe183ab645962cf0b70e658d0eae506c4ade8756652ca7f76733227", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + }, + { + "address": "lskgn7m77b769frqvgq7uko74wcrroqtcjv7nhv95", + "name": "genesis_102", + "blsKey": "8808cb1e4cb5c8ad18ad4a45e35388af4099993effb9069a28e56c5718944a3b4010ec1ef54b4faf4814fad854322468", + "proofOfPossession": "890995fe98a83721b0069aee00c2b264239b3b833b71f64a5f48b4340a969fbac1ffc0664264fbf5af626d37fb3fe6d403dc7ef0ec195cdab82e7615d73ad7a2d326a761fdcf18a6a83efc4f502c724a10ddd89f8b6981496c34b1b32f512781", + "generatorKey": "0941ca2cfd9b1e0cc4bf0dbfd958d4b7d9f30af4c8626216999b88fc8a515d0a", + "lastGeneratedHeight": 0, + "isBanned": false, + "reportMisbehaviorHeights": [], + "consecutiveMissedBlocks": 0, + "commission": 0, + "lastCommissionIncreaseHeight": 0, + "sharingCoefficients": [] + } + ], + "stakers": [], + "genesisData": { + "initRounds": 3, + "initValidators": [ + "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "lskvcgy7ccuokarwqde8m8ztrur92cob6ju5quy4n", + "lskvpnf7a2eg5wpxrx9p2tnnxm8y7a7emfj8c3gst", + "lskvq67zzev53sa6ozt39ft3dsmwxxztb7h29275k", + "lskvwy3xvehhpfh2aekcaro5sk36vp5z5kns2zaqt", + "lskcuj9g99y36fc6em2f6zfrd83c6djsvcyzx9u3p", + "lskc22mfaqzo722aenb6yw7awx8f22nrn54skrj8b", + "lskchcsq6pgnq6nwttwe9hyj67rb9936cf2ccjk3b", + "lskp2kubbnvgwhw588t3wp85wthe285r7e2m64w2d", + "lskmc9nhajmkqczvaeob872h9mefnw63mcec84qzd", + "lskm8g9dshwfcmfq9ctbrjm9zvb58h5c7y9ecstky", + "lskmwac26bhz5s5wo7h79dpyucckxku8jw5descbg", + "lskmadcfr9p3qgx8upeac6xkmk8fjss7atw8p8s2a", + "lskbm49qcdcyqvavxkm69x22btvhwx6v27kfzghu3", + "lskbr5cnd8rjeaot7gtfo79fsywx4nb68b29xeqrh", + "lsknyuj2wnn95w8svk7jo38jwxhpnrx7cj3vo4vjc", + "lsknax33n2ohy872rdkfp4ud7nsv8eamwt6utw5nb", + "lsknatyy4944pxukrhe38bww4bn3myzjp2af4sqgh", + "lsknddzdw4xxej5znssc7aapej67s7g476osk7prc", + "lsk3oz8mycgs86jehbmpmb83n8z3ctxou47h7r9bs", + "lsk37kucto34knfhumezkx3qdwhmbrqfonjmck59z", + "lsk3dzjyndh43tdc6vugbdqhfpt3k9juethuzsmdk", + "lsk4nst5n99meqxndr684va7hhenw7q8sxs5depnb", + "lsk67y3t2sqd7kka2agtcdm68oqvmvyw94nrjqz7f", + "lsk6quzyfffe2xhukyq4vjwnebmnapvsgj4we7bad", + "lsk5pmheu78re567zd5dnddzh2c3jzn7bwcrjd7dy", + "lsk56hpjtt5b8w3h2qgckr57txuw95ja29rsonweo", + "lsk5y2q2tn35xrnpdc4oag8sa3ktdacmdcahvwqot", + "lsk5rtz6s352qyt9vggx7uyo5b4p2ommfxz36w7ma", + "lskoys3dpcyx5hkr7u2fenpjrbyd69tuyu5ar4dgy", + "lskoq2bmkpfwmmbo3c9pzdby7wmwjvokgmpgbpcj3", + "lskowvmbgn4oye4hae3keyjuzta4t499zqkjqydfd", + "lskos7tnf5jx4e6jq4bf5z4gwo2ow5he4khn75gpo", + "lsk966m5mv2xk8hassrq5b8nz97qmy3nh348y6zf7", + "lsk7drqfofanzn9rf7g59a2jha5ses3rswmc26hpw", + "lsk8vjsq5s8jan9c8y9tmgawd6cttuszbf6jmhvj5", + "lsk8netwcxgkpew8g5as2bkwbfraetf8neud25ktc", + "lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk", + "lsk8dz47g5s7qxbyy46qvkrykfoj7wg7rb5ohy97c", + "lsk8dsngwh4n6hmf4unqb8gfqgkayabaqdvtq85ja", + "lskux8ew6zq6zddya4u32towauvxmbe3x9hxvbzv4", + "lsku4ftwo3dvgygbnn58octduj6458h5eep2aea6e", + "lskuueow44w67rte7uoryn855hp5kw48szuhe5qmc", + "lskym4rrvgax9ubgqz6944z9q3t6quo5ugw33j3kr", + "lskyunb64dg4x72ue8mzte7cbev8j4nucf9je2sh9", + "lskrzuuu8gkp5bxrbbz9hdjxw2yhnpxdkdz3j8rxr", + "lskrxweey4ak83ek36go6okoxr6bxrepdv3y52k3y", + "lskrccyjmc8cybh9n3kgencq8u7fh796v2zfraco9", + "lskr8bmeh9q5brkctg8g44j82ootju82zu8porwvq", + "lskrskxmbv7s4czgxz5wtdqkay87ts2mfmu4ufcaw", + "lskrgqnuqub85jzcocgjsgb5rexrxc32s9dajhm69", + "lskrga27zfbamdcntpbxxt7sezvmubyxv9vnw2upk", + "lsktn6hodzd7v4kzgpd56osqjfwnzhu4mdyokynum", + "lsktas5pgp3tofv4ke4f2kayw9uyrqpnbf55bw5hm", + "lskk33a2z28ak9yy6eunbmodnynoehtyra5o4jzkn", + "lskk8yh4h2rkp3yegr5xuea62qbos6q8xd6h3wys2", + "lskkqjdxujqmjn2woqjs6txv3trzh6s5gsr882scp", + "lskk2vnyd5dq3ekexog6us6zcze9r64wk456zvj9a", + "lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6", + "lskqxjqneh4mhkvvgga8wxtrky5ztzt6bh8rcvsvg", + "lskq6j6w8bv4s4to8ty6rz88y2cwcx76o4wcdnsdq", + "lskq5attbvu8s55ngwr3c5cv8392mqayvy4yyhpuy", + "lskqw45qy3ph9rwgow86rudqa7e3vmb93db5e4yad", + "lskqg9k3joyv9ouhjfysscame66hovq42yeev7ug7", + "lskezdab747v9z78hgmcxsokeetcmbdrpj3gzrdcw", + "lske5sqed53fdcs4m9et28f2k7u9fk6hno9bauday", + "lskee8xh9oc78uhw5dhnaca9mbgmcgbwbnbarvd5d", + "lskewnnr5x7h3ckkmys8d4orvuyyqmf8odmud6qmg", + "lskwv3bh76epo42wvj6sdq8t7dbwar7xmm7h4k92m", + "lskw95u4yqs35jpeourx4jsgdur2br7b9nq88b4g2", + "lskwdkhf2ew9ov65v7srpq2mdq48rmrgp492z3pkn", + "lskwdqjhdgvqde9yrro4pfu464cumns3t5gyzutbm", + "lsk2xxvfxaqpm42wr9reokucegh3quypqg9w9aqfo", + "lska4qegdqzmsndn5hdn5jngy6nnt9qxjekkkd5jz", + "lska6rtf7ndbgbx7d8puaaf3heqsqnudkdhvoabdm", + "lskau7uqo6afteazgyknmtotxdjgwr3p9gfr4yzke", + "lskayo6b7wmd3prq8fauwr52tj9ordadwrvuh5hn7", + "lskatntynnut2eee2zxrpdzokrjmok43xczp2fme7", + "lskaw28kpqyffwzb8pcy47nangwwbyxjgnnvh9sfw", + "lskdo2dmatrfwcnzoeohorwqbef4qngvojfdtkqpj", + "lskduxr23bn9pajg8antj6fzaxc7hqpdmomoyshae", + "lsksmpgg7mo4m6ekc9tgvgjr8kh5h6wmgtqvq6776", + "lsksu2u78jmmx7jgu3k8vxcmsv48x3746cts9xejf", + "lsksy7x68enrmjxjb8copn5m8csys6rjejx56pjqt", + "lsksdfqvkbqpc8eczj2s3dzkxnap5pguaxdw2227r", + "lskjnr8jmvz45dj9z47jbky9sadh3us3rd8tdn7ww", + "lskjtc95w5wqh5gtymqh7dqadb6kbc9x2mwr4eq8d", + "lskjtbchucvrd2s8qjo83e7trpem5edwa6dbjfczq", + "lskhbcq7mps5hhea5736qaggyupdsmgdj8ufzdojp", + "lskhamuapyyfckyg5v8u5o4jjw9bvr5bog7rgx8an", + "lskfx88g3826a4qsyxm4w3fheyymfnucpsq36d326", + "lskfmufdszf9ssqghf2yjkjeetyxy4v9wgawfv725", + "lskf6f3zj4o9fnpt7wd4fowafv8buyd72sgt2864b", + "lskf5sf93qyn28wfzqvr74eca3tywuuzq6xf32p7f", + "lskfowbrr5mdkenm2fcg2hhu76q3vhs74k692vv28", + "lskf7a93qr84d9a6ga543wernvxbsrpvtp299c5mj", + "lskfjd3ymhyzedgneudo2bujnm25u7stu4qpa3jnd" + ] + } + }, + "schema": { + "$id": "/pos/module/genesis", + "type": "object", + "required": ["validators", "stakers", "genesisData"], + "properties": { + "validators": { + "type": "array", + "fieldNumber": 1, + "items": { + "type": "object", + "required": [ + "address", + "name", + "blsKey", + "proofOfPossession", + "generatorKey", + "lastGeneratedHeight", + "isBanned", + "reportMisbehaviorHeights", + "consecutiveMissedBlocks", + "commission", + "lastCommissionIncreaseHeight", + "sharingCoefficients" + ], + "properties": { + "address": { + "dataType": "bytes", + "format": "lisk32", + "fieldNumber": 1 + }, + "name": { + "dataType": "string", + "fieldNumber": 2, + "minLength": 1, + "maxLength": 20 + }, + "blsKey": { + "dataType": "bytes", + "fieldNumber": 3, + "minLength": 48, + "maxLength": 48 + }, + "proofOfPossession": { + "dataType": "bytes", + "fieldNumber": 4, + "minLength": 96, + "maxLength": 96 + }, + "generatorKey": { + "dataType": "bytes", + "fieldNumber": 5, + "minLength": 32, + "maxLength": 32 + }, + "lastGeneratedHeight": { + "dataType": "uint32", + "fieldNumber": 6 + }, + "isBanned": { + "dataType": "boolean", + "fieldNumber": 7 + }, + "reportMisbehaviorHeights": { + "type": "array", + "fieldNumber": 8, + "items": { + "dataType": "uint32" + } + }, + "consecutiveMissedBlocks": { + "dataType": "uint32", + "fieldNumber": 9 + }, + "commission": { + "dataType": "uint32", + "fieldNumber": 10 + }, + "lastCommissionIncreaseHeight": { + "dataType": "uint32", + "fieldNumber": 11 + }, + "sharingCoefficients": { + "type": "array", + "fieldNumber": 12, + "items": { + "type": "object", + "required": ["tokenID", "coefficient"], + "properties": { + "tokenID": { + "dataType": "bytes", + "fieldNumber": 1 + }, + "coefficient": { + "dataType": "bytes", + "fieldNumber": 2 + } + } + } + } + } + } + }, + "stakers": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["address", "stakes", "pendingUnlocks"], + "properties": { + "address": { + "dataType": "bytes", + "format": "lisk32", + "fieldNumber": 1 + }, + "stakes": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["validatorAddress", "amount", "sharingCoefficients"], + "properties": { + "validatorAddress": { + "dataType": "bytes", + "fieldNumber": 1 + }, + "amount": { + "dataType": "uint64", + "fieldNumber": 2 + }, + "sharingCoefficients": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["tokenID", "coefficient"], + "properties": { + "tokenID": { + "dataType": "bytes", + "fieldNumber": 1 + }, + "coefficient": { + "dataType": "bytes", + "fieldNumber": 2 + } + } + } + } + } + } + }, + "pendingUnlocks": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["validatorAddress", "amount", "unstakeHeight"], + "properties": { + "validatorAddress": { + "dataType": "bytes", + "fieldNumber": 1, + "minLength": 20, + "maxLength": 20 + }, + "amount": { + "dataType": "uint64", + "fieldNumber": 2 + }, + "unstakeHeight": { + "dataType": "uint32", + "fieldNumber": 3 + } + } + } + } + } + } + }, + "genesisData": { + "type": "object", + "fieldNumber": 3, + "required": ["initRounds", "initValidators"], + "properties": { + "initRounds": { + "dataType": "uint32", + "fieldNumber": 1 + }, + "initValidators": { + "type": "array", + "fieldNumber": 2, + "items": { + "dataType": "bytes", + "format": "lisk32" + } + } + } + } + } + } + } + ] +} diff --git a/examples/poa-sidechain/config/alphanet/genesis_block.blob b/examples/poa-sidechain/config/alphanet/genesis_block.blob new file mode 100644 index 00000000000..9a23b2e52bf Binary files /dev/null and b/examples/poa-sidechain/config/alphanet/genesis_block.blob differ diff --git a/examples/poa-sidechain/config/alphanet/passphrase.json b/examples/poa-sidechain/config/alphanet/passphrase.json new file mode 100644 index 00000000000..df473b32c19 --- /dev/null +++ b/examples/poa-sidechain/config/alphanet/passphrase.json @@ -0,0 +1,3 @@ +{ + "passphrase": "economy cliff diamond van multiply general visa picture actor teach cruel tree adjust quit maid hurry fence peace glare library curve soap cube must" +} diff --git a/examples/poa-sidechain/config/default/config.json b/examples/poa-sidechain/config/default/config.json new file mode 100644 index 00000000000..013ea647b53 --- /dev/null +++ b/examples/poa-sidechain/config/default/config.json @@ -0,0 +1,46 @@ +{ + "system": { + "dataPath": "~/.lisk/pos-mainchain", + "enableMetrics": true + }, + "rpc": { + "modes": ["ipc"] + }, + "genesis": { + "block": { + "fromFile": "./config/genesis_block.blob" + }, + "blockTime": 10, + "bftBatchSize": 103, + "chainID": "04000000", + "maxTransactionsSize": 15360 + }, + "generator": { + "keys": { + "fromFile": "./config/dev-validators.json" + } + }, + "network": { + "version": "1.0", + "seedPeers": [ + { + "ip": "127.0.0.1", + "port": 7667 + } + ], + "port": 7667 + }, + "transactionPool": { + "maxTransactions": 4096, + "maxTransactionsPerAccount": 64, + "transactionExpiryTime": 10800000, + "minEntranceFeePriority": "0", + "minReplacementFeeDifference": "10" + }, + "modules": {}, + "plugins": { + "reportMisbehavior": { + "encryptedPassphrase": "iterations=10&cipherText=5dea8b928a3ea2481ebc02499ae77679b7552189181ff189d4aa1f8d89e8d07bf31f7ebd1c66b620769f878629e1b90499506a6f752bf3323799e3a54600f8db02f504c44d&iv=37e0b1753b76a90ed0b8c319&salt=963c5b91d3f7ba02a9d001eed49b5836&tag=c3e30e8f3440ba3f5b6d9fbaccc8918d&version=1" + } + } +} diff --git a/examples/poa-sidechain/config/default/dev-validators.json b/examples/poa-sidechain/config/default/dev-validators.json new file mode 100644 index 00000000000..fe51b5a4a71 --- /dev/null +++ b/examples/poa-sidechain/config/default/dev-validators.json @@ -0,0 +1,1652 @@ +{ + "keys": [ + { + "address": "lske5sqed53fdcs4m9et28f2k7u9fk6hno9bauday", + "keyPath": "m/44'/134'/0'", + "publicKey": "a3f96c50d0446220ef2f98240898515cbba8155730679ca35326d98dcfb680f0", + "privateKey": "d0b159fe5a7cc3d5f4b39a97621b514bc55b0a0f1aca8adeed2dd1899d93f103a3f96c50d0446220ef2f98240898515cbba8155730679ca35326d98dcfb680f0", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/0'", + "generatorKey": "b9e54121e5346cc04cc84bcf286d5e40d586ba5d39571daf57bd31bac3861a4a", + "generatorPrivateKey": "b3c4de7f7932275b7a465045e918337ffd7b7b229cef8eba28f706de8759da95b9e54121e5346cc04cc84bcf286d5e40d586ba5d39571daf57bd31bac3861a4a", + "blsKeyPath": "m/12381/134/0/0", + "blsKey": "92f020ce5e37befb86493a82686b0eedddb264350b0873cf1eeaa1fefe39d938f05f272452c1ef5e6ceb4d9b23687e31", + "blsProofOfPossession": "b92b11d66348e197c62d14af1453620d550c21d59ce572d95a03f0eaa0d0d195efbb2f2fd1577dc1a04ecdb453065d9d168ce7648bc5328e5ea47bb07d3ce6fd75f35ee51064a9903da8b90f7dc8ab4f2549b834cb5911b883097133f66b9ab9", + "blsPrivateKey": "463dd3413051366ee658c2524dd0bec85f8459bf6d70439685746406604f950d" + }, + "encrypted": {} + }, + { + "address": "lsk8dsngwh4n6hmf4unqb8gfqgkayabaqdvtq85ja", + "keyPath": "m/44'/134'/1'", + "publicKey": "0904c986211330582ef5e41ed9a2e7d6730bb7bdc59459a0caaaba55be4ec128", + "privateKey": "2475a8233503caade9542f2dd6c8c725f10bc03e3f809210b768f0a2320f06d50904c986211330582ef5e41ed9a2e7d6730bb7bdc59459a0caaaba55be4ec128", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/1'", + "generatorKey": "dd337fcb819073335382415bfdbf5e5b7e73126aafb0ac46479137328e72d438", + "generatorPrivateKey": "eaddefbdcb41468e73d7ae8e6c0b046de56f8829cbd3ea10c2abf0c74faa1598dd337fcb819073335382415bfdbf5e5b7e73126aafb0ac46479137328e72d438", + "blsKeyPath": "m/12381/134/0/1", + "blsKey": "aa5174668a4743d838fa3742092c744c3edd4ee64c535ce2a69eeae1c5f23029acd74853410867d873076639f4ce1cda", + "blsProofOfPossession": "ad79b935bd503402b83404125ef11fab81f4c6bef0688798473e430f892704b653209aaf81f16efca9965fad0850a3971662f33c25994568e1434f4f46901caa1c002cab18dff7337836617c372673714d63b01ec4db098f419c027015aa4c05", + "blsPrivateKey": "4856d774c133fc205f1950cb030eddc2286ba6662e8f5061d153a7b36d16781a" + }, + "encrypted": {} + }, + { + "address": "lskjtbchucvrd2s8qjo83e7trpem5edwa6dbjfczq", + "keyPath": "m/44'/134'/2'", + "publicKey": "b8d2422aa7ebf1f85031f0bac2403be1fb24e0196d3bbed33987d4769eb37411", + "privateKey": "03e7852c6f1c6fe5cd0c5f7e3a36e499a1e0207e867f74f5b5bc42bfcc888bc8b8d2422aa7ebf1f85031f0bac2403be1fb24e0196d3bbed33987d4769eb37411", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/2'", + "generatorKey": "3e63c0a5d4de4df114823934ceaa6c17a48e5a6650788cf1f63c826c984c0957", + "generatorPrivateKey": "c96d896fd601e71a61452465692e6f77c9f654af0c596d4d5a2285333ccc846e3e63c0a5d4de4df114823934ceaa6c17a48e5a6650788cf1f63c826c984c0957", + "blsKeyPath": "m/12381/134/0/2", + "blsKey": "8c141e5d769c22ec90122f42bef1d1e7af2d94c1da6844bd313fca2ccf0543eab5f8c6752dd47969dc34613801dfb293", + "blsProofOfPossession": "9681aa250d714befe61d71f239a9b4c09ee102addb3a5e2c884074c7ba763b5c21e53aa7b12518d32c9b874ba1910e7a0bf0bd23ae99f57f6f464403b1151b3521a7a369ff94118a436e6aa767bd462d9ca491dd3e253862c21ff078878c354e", + "blsPrivateKey": "05739256f97460ba695cb52abcc9f8d9d46d5ed052ccbb16c780c6fd44ac153b" + }, + "encrypted": {} + }, + { + "address": "lskau7uqo6afteazgyknmtotxdjgwr3p9gfr4yzke", + "keyPath": "m/44'/134'/3'", + "publicKey": "557f1b9647fd2aefa357fed8bead72d1b02e5151b57d3c32d4d3f808c0705026", + "privateKey": "985bc97b4b2aa91d590dde455c19c70818d97c56c7cfff790a1e0b71e3d15962557f1b9647fd2aefa357fed8bead72d1b02e5151b57d3c32d4d3f808c0705026", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/3'", + "generatorKey": "00245e599fdad13ed0b064c069c71c73caf868a4635c0143963a529807f8728c", + "generatorPrivateKey": "a4426b9facb99efcf6ad7702f02e3e57ea2dd6d5e4f5bbee25729595e012df8800245e599fdad13ed0b064c069c71c73caf868a4635c0143963a529807f8728c", + "blsKeyPath": "m/12381/134/0/3", + "blsKey": "aaec4e157b19c0a3f2965cc636f5f82cef9b3918c071e2c6e50f57ecb44587d58139595e8f4c1fc7f76b2f7c09b1b6d1", + "blsProofOfPossession": "866a031b5a2a6b0525053b2d870487ac2fd39cf2cf18ecf462bc19afc5ef52f129cf88624fac73057c5375004492dbfb0b8cacb906b3a7daa4d7edf99f10ab15a90b3b328e8ad6701e838a88351fecdfb5b32eebeb80fdeb8c0345d1b5257d7b", + "blsPrivateKey": "43b132328eec8064dcbd62f038ad73e372c12d94fdedad5a35a95cdd0ad858e5" + }, + "encrypted": {} + }, + { + "address": "lsksdfqvkbqpc8eczj2s3dzkxnap5pguaxdw2227r", + "keyPath": "m/44'/134'/4'", + "publicKey": "e5e4834c2c7e949ac6e97512b5ff5d44822376b1e54cae8c326de0873c0b72ad", + "privateKey": "6f2b2f6ef42f417af916fb2a29ae8c8d0c572219d7420927c2dcd336e21c9115e5e4834c2c7e949ac6e97512b5ff5d44822376b1e54cae8c326de0873c0b72ad", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/4'", + "generatorKey": "21f9d60315c1baeb513b5f7324a1211723d36948b64806541b8855988f86111f", + "generatorPrivateKey": "c467e3bbc6af24568c8a8a8ee29055c2704aab14549dd99f1f1d1cfccdad384421f9d60315c1baeb513b5f7324a1211723d36948b64806541b8855988f86111f", + "blsKeyPath": "m/12381/134/0/4", + "blsKey": "84912d2f185c2058be9ed201d970f435a408c8bb3a36c430f007b69632efb2f663b51df383be6eedb80c8768a70822bb", + "blsProofOfPossession": "aafdb397226d3a4a4cc3b7ac906ae7e3601310bd5d0e20a0682364312937e8e3e0c3b5846a53ee536cac2a2b3f556bff06c65ef24a32495dee9d38ee5b2012113d8f032d8dd0f3f5d9af50dbd307d0e7f66aaa165620d5292da91306b0a39aad", + "blsPrivateKey": "16f43c470d46b9a10a461328c9ee629b045cfd469dc3cb9c1ac9ba85a5af5b8a" + }, + "encrypted": {} + }, + { + "address": "lskvq67zzev53sa6ozt39ft3dsmwxxztb7h29275k", + "keyPath": "m/44'/134'/5'", + "publicKey": "c1e3177d1433ece7f8fcb607edc37df4fd37284f46081f846ca7852735b4145b", + "privateKey": "4d108ede8bce4330260360341229c608fcdfdf07b262cfdbdc3cb49a560ba71cc1e3177d1433ece7f8fcb607edc37df4fd37284f46081f846ca7852735b4145b", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/5'", + "generatorKey": "8b65dce85de8ed215a91477627b365ec017a01cd5a715337f772ba42715cc794", + "generatorPrivateKey": "fbdd344d5e73d45c50298c109d34f0da4eee8ca8068f893110c6a4a86bba05778b65dce85de8ed215a91477627b365ec017a01cd5a715337f772ba42715cc794", + "blsKeyPath": "m/12381/134/0/5", + "blsKey": "9006fc2c9d159b6890047e9b26c700d8c504e17b6fe476a2a1ac1477357c68eee332be587da425e37e22332348ed8007", + "blsProofOfPossession": "945ac6db93666aa21934d84c6ad897fe1acf1d208a17ec46b0ddf26cf6d9cdccef7db9eac682195ec47cb8e7a069bbe10706a4e1cce2012aadd311dafb270c9c810d80bc82c2b6c34ce236efac552fa0904b96533772f98e202f4e6f47c97f09", + "blsPrivateKey": "4adf92c505124ff3ff4f3b36fff3a2ce3d60953dbcb34b4c43ea93b82e17f970" + }, + "encrypted": {} + }, + { + "address": "lskfjd3ymhyzedgneudo2bujnm25u7stu4qpa3jnd", + "keyPath": "m/44'/134'/6'", + "publicKey": "dc5adaa7cc6e0598a4a6347ce9cb3f213835d863c377410c3eafa8b718807aa3", + "privateKey": "2926701eccc5232d51ed98a2bc9cebdd687d8a3760d3c5adb8cae7a434dbab2ddc5adaa7cc6e0598a4a6347ce9cb3f213835d863c377410c3eafa8b718807aa3", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/6'", + "generatorKey": "326cb34aa214c4952f646d93af8cfbe58ec74db76db54484b5a23918cba8743b", + "generatorPrivateKey": "b3bf887c6a4a646e444c877d2299b2aa1328251d68af051328e88eb9872e8de4326cb34aa214c4952f646d93af8cfbe58ec74db76db54484b5a23918cba8743b", + "blsKeyPath": "m/12381/134/0/6", + "blsKey": "96a70c8b1343511359f7205313eac8c73b2838e25eda58cf8c13fa1d2689aee3df70522bcbd36e0bde958409b80cc8ee", + "blsProofOfPossession": "89564da089fcc38e4973cf34b5a8abbe8e822bb59f05633156d9dc0b10f2aad8d4621ea66023ec2a10d6d581927af3bc0746cd8293ea22c8db0068c127d38c4c2dcfe777ffc03e773083fd0036894cce7c2596301381941523f4f2ae97bb79e9", + "blsPrivateKey": "01fcace0a39a0f12057671c9ca88f41811ae7cc6c928c4a79cb5e7e3883c17f3" + }, + "encrypted": {} + }, + { + "address": "lskqw45qy3ph9rwgow86rudqa7e3vmb93db5e4yad", + "keyPath": "m/44'/134'/7'", + "publicKey": "e5c559e55dbb69328dc765d732e3df31b60d243d4c1a240a3d99af413e8958c6", + "privateKey": "26e75ae42bb589e181b38ce31911d3a63e2b0d3ae1be0b29d61971c986906687e5c559e55dbb69328dc765d732e3df31b60d243d4c1a240a3d99af413e8958c6", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/7'", + "generatorKey": "1314b7d167d5829fb535d15dfb5216e10ad2e5b6a349ae347aec77317b6aa73f", + "generatorPrivateKey": "de317ea0e11dde876b6ef8f37298a0608eb78e987380da4777137b4661f023921314b7d167d5829fb535d15dfb5216e10ad2e5b6a349ae347aec77317b6aa73f", + "blsKeyPath": "m/12381/134/0/7", + "blsKey": "b40065dfa219e40c65c07d516158d722ec695abc91411ce57550c77fa2119e52b56cb74db7a1d805b631752e8f6b80be", + "blsProofOfPossession": "b7085c15521303140512fdea858231a040534a4b0c1dbbdb002c8df233634270d33e51c3699cf4956d165c0183f29a32070d8f4e00433ebcdfcae337a5f09f2c971ba97d5b35413ce032d2ec4084ed79efc917bdb75ded139fc9433df884a18e", + "blsPrivateKey": "3f78ff58a0462d09c20249fdd8b16dafc09bf5d41669a7355aaea5e9705d1c46" + }, + "encrypted": {} + }, + { + "address": "lsk8vjsq5s8jan9c8y9tmgawd6cttuszbf6jmhvj5", + "keyPath": "m/44'/134'/8'", + "publicKey": "665b67a9bfa854ea7e58a1dbde618410d9c63e50204ac3a12a4cfdc44a903d95", + "privateKey": "e98c4711a330632bd012bb0d2f73e2b3d72635e3c13c54edd9b9de6dcd6fc73f665b67a9bfa854ea7e58a1dbde618410d9c63e50204ac3a12a4cfdc44a903d95", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/8'", + "generatorKey": "00110f493d122a73628a518842e99591b91def4ef9fbd58e1b6458950da5a776", + "generatorPrivateKey": "eace487ec72fbfc569c3680713146fc354678533fb06de639b6d8a0e658ac5e200110f493d122a73628a518842e99591b91def4ef9fbd58e1b6458950da5a776", + "blsKeyPath": "m/12381/134/0/8", + "blsKey": "837e0759968b1ed95789252d1e731d7b127c9a53a74e86f3ca3d65d71cf666f2208baa782a42c45d4132630100a59462", + "blsProofOfPossession": "b97607b1478f17877b4c8042530763894dd7b79f8bbf5ca0883d08b94dc8a11cc2c2a73123160e3b01da692fb071f5fe0d808426604b5ad8aadebda9b02710698158254f6f1d822c2c9bae5c081101806e9220d79c547391e6fc6d8f26094dc7", + "blsPrivateKey": "2cf343ea5097fe55d1d1f054a76dc2766c88acadb8b2156318fc5b56f76e5200" + }, + "encrypted": {} + }, + { + "address": "lskchcsq6pgnq6nwttwe9hyj67rb9936cf2ccjk3b", + "keyPath": "m/44'/134'/9'", + "publicKey": "2c40d2354c023409c24d16dce668ae26930a675b274ae8409a0c67a2f16672e0", + "privateKey": "b1863cba481c0b16ca83b0257d71964d1ade9cb2b6895f78c4686c793c7cf5842c40d2354c023409c24d16dce668ae26930a675b274ae8409a0c67a2f16672e0", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/9'", + "generatorKey": "be4e49ea7e57ede752ce33cb224f50277552f9085a551005255ee12a9b4ca68d", + "generatorPrivateKey": "8210871092519d73ea2e2645f57333d01bfdb7e553ef188b4d57e985e461be79be4e49ea7e57ede752ce33cb224f50277552f9085a551005255ee12a9b4ca68d", + "blsKeyPath": "m/12381/134/0/9", + "blsKey": "8fd004c33814c3b452d50b2bf6855eeb03e41552c6edd50b76dee57007a34cf987da1e06425cf498391e6831d1bf6851", + "blsProofOfPossession": "a0e34bdc7dc39e09f686d6712fd0e71c61c8d06dfedbdbb9ed77c821c22d6c87f87e39e48db79aa50c19904933abb11a0b07659317079ae8f2db6e27b9139ce0830faa8dad2dcae2079f64781b0516be825b2d84689080bb8219a5ec72ba80f7", + "blsPrivateKey": "3d5f026eb2fb39cecc763f052695f75cdf52d3382148abf49a03b6f84ef9f075" + }, + "encrypted": {} + }, + { + "address": "lskc22mfaqzo722aenb6yw7awx8f22nrn54skrj8b", + "keyPath": "m/44'/134'/10'", + "publicKey": "88da43d0f056dd666cf2a8ae37db58e28bba3ae0b954930674ebe5dc03311e99", + "privateKey": "f1c8bf737f8e537dcdf202e8de94e138945d9bf9bd70ed700fcd0247bda8104b88da43d0f056dd666cf2a8ae37db58e28bba3ae0b954930674ebe5dc03311e99", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/10'", + "generatorKey": "671c72129793eb5801273ff580ce3d4c78d89fc8b4fb95b090a9af0a9a647a41", + "generatorPrivateKey": "ef19cef8e2f025de4d923fb976f5dc5ab4d5fd0e1c935f3d44e8722e6a036ffd671c72129793eb5801273ff580ce3d4c78d89fc8b4fb95b090a9af0a9a647a41", + "blsKeyPath": "m/12381/134/0/10", + "blsKey": "a38d728c1c1023651b031835818d17d0665d1fbabd8e62da26ca53f290620c23fe928244bcbcbb67412344013017cb53", + "blsProofOfPossession": "b5d455bb358eff87779b296f23a2fc9abc9d8f3ecb8ed0d9af3e23066e653a58b189c11b4a3980eaeaaa85ffcc240795187f6e8a0e8e8a2837bc20d485e1d3159c2d581614d72f94bbd049e5a9f45c0302851c87aa3c3853d8962ed75d140234", + "blsPrivateKey": "2e3c200c9927504eaab6dcb3777d394aa0d5e7c8a85e09f102bfe84b311f6eb6" + }, + "encrypted": {} + }, + { + "address": "lskezdab747v9z78hgmcxsokeetcmbdrpj3gzrdcw", + "keyPath": "m/44'/134'/11'", + "publicKey": "46ddcc48cc566faedd278169c1327bef337e32044320291f452aa60327c2cd2f", + "privateKey": "2c4f8a875c3850d8aacdb2643ce32ac3a20d61e24c69c7cba1e6315592992e1846ddcc48cc566faedd278169c1327bef337e32044320291f452aa60327c2cd2f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/11'", + "generatorKey": "44de3820f1a1a7351953d2d000f29cb7bffecf30582a8b3da2cb80c83b9eceef", + "generatorPrivateKey": "e2fec1ce757b5865797955e9fbe074224b67ce9fe1e0f5df6ed633745da3540a44de3820f1a1a7351953d2d000f29cb7bffecf30582a8b3da2cb80c83b9eceef", + "blsKeyPath": "m/12381/134/0/11", + "blsKey": "a03ba0f1d6bf9378681b9d96dbe8176cc0ab2a424154cbbe325fc279d02cf58bc15de966cb1e272312ba2b6db31a7f05", + "blsProofOfPossession": "a20a8edd978fe911da6c933d486cb9af770179ef5ee21ad869c4c35e63103cfc2ac17350ee2d35b4bbd487193cdb33ab0116fdf2f078f289fae2922f6a7e372ef8ea543d52ae74ae395dccf2dec2c40e6596c807a14c9fce45b320321f68c612", + "blsPrivateKey": "6aa2aafb57bf3d0038bd7b0a9fd88632a6be33e51a8eeee87432d84b72dbbab0" + }, + "encrypted": {} + }, + { + "address": "lsknddzdw4xxej5znssc7aapej67s7g476osk7prc", + "keyPath": "m/44'/134'/12'", + "publicKey": "86ae660dcf148c829a17364f0fc9f7f61cb5efde7c10598923cfec376c346492", + "privateKey": "dd495f4d08928547ab5d2b39fc934e31a052181f338e0a723bc51f4305cd908c86ae660dcf148c829a17364f0fc9f7f61cb5efde7c10598923cfec376c346492", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/12'", + "generatorKey": "3c19943d614f67309dd989e2e1bdeade5ea53b0522eac3d46b9e7f68604a874d", + "generatorPrivateKey": "3fbbad2694492781f334e0a8c9a03827ce3139f5cf1c17fcf410a7d6ec0a3b653c19943d614f67309dd989e2e1bdeade5ea53b0522eac3d46b9e7f68604a874d", + "blsKeyPath": "m/12381/134/0/12", + "blsKey": "8ae81737f7b1678ece4b06db3ee1d633637da3c02cf646cdb0c7c1dae5f9eea41f2384fca8b0b12033d316ee78ea3e94", + "blsProofOfPossession": "a5150c19ac23dc15f660d9612be5f9591c1a5fc892e9f8b267de6bd39da84f254b6644e8c0f294900e5e9b7c9ecf3f260d902a56af7db5a59083eda08dd3ff083e2a07ba5d34f25312621f8686358dd2a50dcdc879eb0f9d50ff2fdc704e7d9a", + "blsPrivateKey": "0f0bb8d3299a807f35029011a71e366e134d6288a41d5cae85844b3f33e2b274" + }, + "encrypted": {} + }, + { + "address": "lskffxs3orv2au2juwa69hqtrmpcg9vq78cqbdjr4", + "keyPath": "m/44'/134'/13'", + "publicKey": "159c3170dfc8df2820e9c953ecceeaa8d8746af54687c4c266f654a3a1dd1714", + "privateKey": "d470a6f2a03a4bc359727bb957fea1efcb07ec0e07a143388d36b40d76f220c7159c3170dfc8df2820e9c953ecceeaa8d8746af54687c4c266f654a3a1dd1714", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/13'", + "generatorKey": "4e54056fabe183ab645962cf0b70e658d0eae506c4ade8756652ca7f76733227", + "generatorPrivateKey": "c35fe47d21ad0d2edc953eb17e27ce9532f30f35ba2d90e9ddfdacc06b1cfb124e54056fabe183ab645962cf0b70e658d0eae506c4ade8756652ca7f76733227", + "blsKeyPath": "m/12381/134/0/13", + "blsKey": "a3e2b645a315827618e58c1eb66dfef3744c8111a0c7b0e8535a3ec31d78ea2630646fea1da5609988c5d88997d663fb", + "blsProofOfPossession": "b55d1c525f96bba45cbefbcadad16279c9f61f790dfc3e3c824003139f9994200079faf573eddb863c6ba1fd9b7d7364146e3f20579b065355c75691e06be2c7304fe48d32fbfcb5ef38f8ecaa6905e9ca6a7c1124c45a6ab2b06668cb3decc9", + "blsPrivateKey": "58ef88d198c15101e9813bb963807ad43453422c76ff0a645e44851b482f417f" + }, + "encrypted": {} + }, + { + "address": "lskf7a93qr84d9a6ga543wernvxbsrpvtp299c5mj", + "keyPath": "m/44'/134'/14'", + "publicKey": "37002d59f3e5b66cac1a0598ea21c3360059afbd6bc6f298939cdae03a3db882", + "privateKey": "6c8d002f2b58e11940eb5c79fae119574ccda401c71cc8b451d2783d0286f91e37002d59f3e5b66cac1a0598ea21c3360059afbd6bc6f298939cdae03a3db882", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/14'", + "generatorKey": "ac34c0731cddab10726e634cec30294f831af045a0614733ac683ccdb6bc7eab", + "generatorPrivateKey": "1c91906bbd73352db1e4f89344b0851462962db0a11864a63a8ecfd805182935ac34c0731cddab10726e634cec30294f831af045a0614733ac683ccdb6bc7eab", + "blsKeyPath": "m/12381/134/0/14", + "blsKey": "a7283bff41249c3d2a0f065a27448a4c5acefaece74e51ec432c418c4bc8e6f0eb60160feec4729b9c0b933e9ec5e528", + "blsProofOfPossession": "86f1ac081ee08568266dc39727540a5d50f03e544f73d9a3ca60d87cfe9b6718832e07b2720d42e0e818c5fe2d45099a0774af1e6b123b41a3eb7eb3a1443d248a535fe9ef93f0027a8e8f44686dc33d677b79251c22022675395a347d0f3dbb", + "blsPrivateKey": "1f14d0e79b00554226cd7655f10eb22d5a5452d23665a8d06219b303e9595211" + }, + "encrypted": {} + }, + { + "address": "lskfx88g3826a4qsyxm4w3fheyymfnucpsq36d326", + "keyPath": "m/44'/134'/15'", + "publicKey": "6877e45fbe5b009d364071a1d282ebab1c1e34307c92e698d1ffb6ceb98f09e3", + "privateKey": "2afa9923109b1d4111ccf8678ff62bd63dbc97f69b6fb251442ec6b9140170b96877e45fbe5b009d364071a1d282ebab1c1e34307c92e698d1ffb6ceb98f09e3", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/15'", + "generatorKey": "028a30837b7eec19b02b06c3c2f4065290285e40a4870a677664fee3fe76d9be", + "generatorPrivateKey": "7ff68b39611f7d7b8fdc05226846abfdbbdb62becfb15032db25fe9281ebc71e028a30837b7eec19b02b06c3c2f4065290285e40a4870a677664fee3fe76d9be", + "blsKeyPath": "m/12381/134/0/15", + "blsKey": "93bddb296ef4dd5c832486b4603c1ed13805d2df1c6c2f95c8af4ae38467f1e741c1c2fbbd5f8e927b54250bffdf8536", + "blsProofOfPossession": "923415dc1db9b46715d284bd2a3f12313a24c1352bf0dfcdce2e0e0475fe0343d5cc9e463d5f04b99cb367e30e89f1371280d5897a0103658d710b07f8d9d3d8754043241a753dce60f2bdadcb9249b334e6f5a395cabfdb187f2739b512d46f", + "blsPrivateKey": "21aa5cd0043608b6b020589a039bf5b66f32bd66c84f311f22c49a53c08d6b4d" + }, + "encrypted": {} + }, + { + "address": "lskux8ew6zq6zddya4u32towauvxmbe3x9hxvbzv4", + "keyPath": "m/44'/134'/16'", + "publicKey": "89e1bad75bed903096f63cfd6c27386f91b58910dd6fcbafcc66ac084b289702", + "privateKey": "b3552dadc9e7121c89f4a0eccdbfec423078af46a926913764c66496b3ed7fe689e1bad75bed903096f63cfd6c27386f91b58910dd6fcbafcc66ac084b289702", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/16'", + "generatorKey": "563aa06b554beea30fc4455ae51e0954051a3457315b2370fde9c22d3233b522", + "generatorPrivateKey": "34c7762f0fef6090c2832a3ccaf40ef373530e9930f46746d4e3f3236f627fe6563aa06b554beea30fc4455ae51e0954051a3457315b2370fde9c22d3233b522", + "blsKeyPath": "m/12381/134/0/16", + "blsKey": "94da5ec9da5eabf2ab184de1e0ee10f63f721897475acd59c3c53adc51a9b39b0f4fa28573fcc309e576dba658425dbd", + "blsProofOfPossession": "a672d269ec605e04065fc0da8e6f520d0273b1c57a754409d9fb25cef1be67b8583fa683e27c0284c31105045f395c0c142d0648420b9b209fa88fa13025ba2b3887e04e3fbae1db6e5941ade41713a4384c139e47e72a68c964c4a5c0886d25", + "blsPrivateKey": "651060d1b4a47d4f7c036e4649f84d42885db5ea5b4b26f04498ab805f4a2634" + }, + "encrypted": {} + }, + { + "address": "lskp2kubbnvgwhw588t3wp85wthe285r7e2m64w2d", + "keyPath": "m/44'/134'/17'", + "publicKey": "32ad0d0c9f9f5b2fa4605ff4c072ec4bcf2d64f0e0046fc9df247b5cad952a87", + "privateKey": "3c4fa6c215f89226083979c01be72633b7fdeae34a2679588dc6cb41cd811f8c32ad0d0c9f9f5b2fa4605ff4c072ec4bcf2d64f0e0046fc9df247b5cad952a87", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/17'", + "generatorKey": "56d64ef16324f92efce8b0a6ee98b2925dc485d45675b2012bbf6a96d7431a36", + "generatorPrivateKey": "a105df9082f9ab10633967414b3629bb9218587d8561dca4acde6fa414a890b956d64ef16324f92efce8b0a6ee98b2925dc485d45675b2012bbf6a96d7431a36", + "blsKeyPath": "m/12381/134/0/17", + "blsKey": "98f83f66e857d954d5c5a49403e5b3a622e1bb855d785845e72faf0f7dd03ed3fd2f787a38c57f6968accaf780fd41fe", + "blsProofOfPossession": "b3131f0229df11964daba47a79729542f10672b36db017002df90d2cc6a79c8b44d032935bd214bdf69a8db181e4315a15de71a2e6802442536143c3ace9886248d502d6f38f9ea5bad26d4cee729b909d6cbde541c35313598957ddda08de15", + "blsPrivateKey": "1a835401bf4776f55c3ef62c91506f5ae6a51343ab54e83179ffbeee53ad8e7c" + }, + "encrypted": {} + }, + { + "address": "lsknatyy4944pxukrhe38bww4bn3myzjp2af4sqgh", + "keyPath": "m/44'/134'/18'", + "publicKey": "4033f18959c6b6c51c5d60321691f462b491d00912c640d0bd5cd361e50758b9", + "privateKey": "2a7743838c3e637370fcd980a7f757d54b7ec2f417d339a384405fdcd0ac71724033f18959c6b6c51c5d60321691f462b491d00912c640d0bd5cd361e50758b9", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/18'", + "generatorKey": "f8d382ac4f19ffe2ac2fa91794b65dc4c03389cbb2ea65bab50379a12e0f98fb", + "generatorPrivateKey": "a7b7b85bab2f2d4471f3ff944b16ca636353f7d8af66f085d290ad14d8b62eeaf8d382ac4f19ffe2ac2fa91794b65dc4c03389cbb2ea65bab50379a12e0f98fb", + "blsKeyPath": "m/12381/134/0/18", + "blsKey": "b0d3f0d142131962d9ab7505a3ca078c1947d6bb2972174988feddc5d4d9727927ff79290af7e1180a913a375da9b618", + "blsProofOfPossession": "90f81a87982cb983aae8c240f12c77306501bf67dcb031161cb2787ecbecfdc0ca4e62365f750714b9b0a64c10411058105bef1a725ece1c0e7c45b7e1526494d5a02ceaa4f624116a91188e7ca2503e0ae17748b11b05cd79ccc204d20e418f", + "blsPrivateKey": "3f132150625f830a749f9d98639ecf79ef6796b22e31c1b3b0284961ea68fb37" + }, + "encrypted": {} + }, + { + "address": "lskwdqjhdgvqde9yrro4pfu464cumns3t5gyzutbm", + "keyPath": "m/44'/134'/19'", + "publicKey": "63b9114c5d10b1cb818e6c3b6e4adae2a3d95e1a32d78f2b2c31c02e41dbcbef", + "privateKey": "a8f11d66e15e48150ed4226e06090d308b87a52f1e3ef5e2ccf41320177830ae63b9114c5d10b1cb818e6c3b6e4adae2a3d95e1a32d78f2b2c31c02e41dbcbef", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/19'", + "generatorKey": "902b7ed4708c476c7f0e96825cb06f95cbc86953130575d2c4589d8e3dc2f69c", + "generatorPrivateKey": "922ac8b034a28c0941cf74105c9b3780d1a790b3321f163b203d678ef84d9c9e902b7ed4708c476c7f0e96825cb06f95cbc86953130575d2c4589d8e3dc2f69c", + "blsKeyPath": "m/12381/134/0/19", + "blsKey": "a397bb33263b2850758a1b144401b741c1278b302eb8d27be6c61363d9cedafcabe05fbd7d9ce5e75a7078972d397e9b", + "blsProofOfPossession": "b22ed60a951702ec7bfd85482e59703af76c4c79fe2d3a3b81e737d53746543587d2932fcd5559d56f6530bfe48d23f5093aa30f3e299733cb56151175d22e21895ada290521908536d71480f1066bbeec7ab803376a4a81e4d7ec3bb4d71dc0", + "blsPrivateKey": "0dac58ccfee182a3e2eeb2ca51ea8c8d9e7c5db1a6535fd3ef19b041096fa39a" + }, + "encrypted": {} + }, + { + "address": "lskewnnr5x7h3ckkmys8d4orvuyyqmf8odmud6qmg", + "keyPath": "m/44'/134'/20'", + "publicKey": "ca0ebbb82059cbcdabf64d9a69fbac54e1059c88a2c3edab7ea6aff700595f3d", + "privateKey": "5d574dc371a6503cbe75dd1c79a5de3b93c570d42f0b12a8b5edb8b265205668ca0ebbb82059cbcdabf64d9a69fbac54e1059c88a2c3edab7ea6aff700595f3d", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/20'", + "generatorKey": "bf5f4408df7a1cde279b3cfe7ba6c2e2600a4bb90d883b98ef8048ec344221e0", + "generatorPrivateKey": "bb82e9722b03ced00e2eefec45c84c54ec9a0627d679e02df5fe0933a1511899bf5f4408df7a1cde279b3cfe7ba6c2e2600a4bb90d883b98ef8048ec344221e0", + "blsKeyPath": "m/12381/134/0/20", + "blsKey": "81f3810e7567ba9e1aa9fab7d5914a1f2ac8b11d952872b398930836f80395c934bd6e71c291193458de7de4382c913f", + "blsProofOfPossession": "a67d9d0708496d13f45fa3d3940954bdfdfa69814554a5618a388cab03a5e82210171f06b72b03966c8a5bd8fe3b235e06de2fc4c45333395c8e10dba086a4f50efe3a7f87f741346c07b22de2ba49eedc521cf53fab31e2033175ff3ca00f08", + "blsPrivateKey": "28934cd2f129730f86b488c07bd390b67ae9642fb98c8c7d880bfc7daa44f863" + }, + "encrypted": {} + }, + { + "address": "lsk4nst5n99meqxndr684va7hhenw7q8sxs5depnb", + "keyPath": "m/44'/134'/21'", + "publicKey": "038d1b2d152be754c4140fa7386439a0b31ee8acf9d5d90cdbde9f39e1fd8ab9", + "privateKey": "e764112ca6647920370c68e381f82629356667db347d90fe9a3ec777c3151478038d1b2d152be754c4140fa7386439a0b31ee8acf9d5d90cdbde9f39e1fd8ab9", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/21'", + "generatorKey": "71ce039f0e4502ff56ca8d33f7ba5ba5392dd7915516b2d87eb777edef454377", + "generatorPrivateKey": "6e9ffbb5c17d86c3f54fc0c4fe8b48cbb3f7148dd8639304f94ed3be088f7da571ce039f0e4502ff56ca8d33f7ba5ba5392dd7915516b2d87eb777edef454377", + "blsKeyPath": "m/12381/134/0/21", + "blsKey": "a1a95b1526c3426ccd03f46199d452c5121481cc862a43bfe616c44662b9a7fa460fcdc5f97072754296e6da7023e078", + "blsProofOfPossession": "942c76c56af0112baa7a11bb8875a2336b321e85de56fd4267e97f3fb142445648a54c97ed22e5860fe5b0e5ef240599028d4009d091ad96ad727914532e45ff9eb44303b337f44bf5ed3ac796e6e22a9ee29138bada893f89f3bebc1a4daad5", + "blsPrivateKey": "11aa8b4f68e3d7c2c0d6081f8a207cbcb0dec199362e978aa8316e1a03410e02" + }, + "encrypted": {} + }, + { + "address": "lsksmpgg7mo4m6ekc9tgvgjr8kh5h6wmgtqvq6776", + "keyPath": "m/44'/134'/22'", + "publicKey": "e37c2947f15c02d4f6928aee7320c911ec269248f2dcd6e35f15d0e85e084a95", + "privateKey": "247b7f47bbf3be42e2bf801c6bf8c141973d8568239fd57d1ea7f3ce673bb8d7e37c2947f15c02d4f6928aee7320c911ec269248f2dcd6e35f15d0e85e084a95", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/22'", + "generatorKey": "f17b9b3bdee2ef63c8fb52d85ae07516133749a1d659bd032c3a078aca65ce7a", + "generatorPrivateKey": "48811bcc2a0c1cccdcbe7100863bfd435b904ad5607add183b43481cd1d19ae4f17b9b3bdee2ef63c8fb52d85ae07516133749a1d659bd032c3a078aca65ce7a", + "blsKeyPath": "m/12381/134/0/22", + "blsKey": "96aa1c639724f5559fb1ebbe5d218511fe0fbfe6681190cd953677c6b63c0e17ac5d9f09844845cfecbb4ab4bd5a5749", + "blsProofOfPossession": "82a60d6a2432fd15c7697094a89ed34a30dc2daa2b460bdb0fe3269362e1d85c79a3d2aa9ba3ffa5b1e80f983933c96f1402e95d34fb656d20f368428ba93539191319c70e6cf6f15c5cb9df9235d115d06e0e00d7a1bf64db1433ac6acb68a6", + "blsPrivateKey": "3aea7d1b6bb1026123989eca287cdd69d2caade596840b42c677ad05ef9fd259" + }, + "encrypted": {} + }, + { + "address": "lskf6f3zj4o9fnpt7wd4fowafv8buyd72sgt2864b", + "keyPath": "m/44'/134'/23'", + "publicKey": "2ecda8618228e5679127a028d832d344f658d4c6b654b1f44bb07c6ebed39568", + "privateKey": "38d40c0a9af6f4bcf6ef3ae1a4a2002c76dfacf4872664aea0628724c3990b392ecda8618228e5679127a028d832d344f658d4c6b654b1f44bb07c6ebed39568", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/23'", + "generatorKey": "8cab5125c910702b66a83240cf836b10a0f2dc3000536799300ed8f1ed9a26ac", + "generatorPrivateKey": "ee5bb2ad10169758a9adb196d5b038870e1f345f3f3588ff64bc6abc44e074718cab5125c910702b66a83240cf836b10a0f2dc3000536799300ed8f1ed9a26ac", + "blsKeyPath": "m/12381/134/0/23", + "blsKey": "92590fccb8c847a6957213682bb798d7d18a368515f070537e1f6cfd45d8dfc50863105db9d46189b92c0e0d009fe09d", + "blsProofOfPossession": "b0aa8214fd746ec04d9cc97e9641a7ad796ed12ef08c9227b5358cf3bd9f049af2ad5376055361c34d265e5d0cf3518d05113928f487bf17012d6ec4deb53e5112b72f2e4d8dc8eed4f68514a9c6bf735c9ccb9dade32ed589bea8e677135302", + "blsPrivateKey": "37aa79f3bad6f99cab62b65498dd3c1bb08efc8c99fca5e76d1ee65575a5e767" + }, + "encrypted": {} + }, + { + "address": "lskrgqnuqub85jzcocgjsgb5rexrxc32s9dajhm69", + "keyPath": "m/44'/134'/24'", + "publicKey": "7106c368f30be7c415f8259ada56e59d9af5a143ed0a03eb5988ae1a427d8ad0", + "privateKey": "8accd9d16d0a607b6425dd86f6d54e21f121919b66bc5b12157e861e8130e8457106c368f30be7c415f8259ada56e59d9af5a143ed0a03eb5988ae1a427d8ad0", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/24'", + "generatorKey": "55d4c0e745954f0fba9629b346055060418961e7edce58c77bf2bcfc7f753d42", + "generatorPrivateKey": "71ed13fc516989f54498bc28ed3b5119eef180666eb2574a07cdb56b492b876c55d4c0e745954f0fba9629b346055060418961e7edce58c77bf2bcfc7f753d42", + "blsKeyPath": "m/12381/134/0/24", + "blsKey": "ad250adf40b559d765bb51d65340fe38de9e4cbc839b6e6509d99bb9bb3f89be1bbb96d75f709f2ae9e715e6e6ce38a4", + "blsProofOfPossession": "8943f42818d3c3374d43d1aa0b427436f4edec3e760f07aea2990b99eb3ef69952d580df862ad9034062fab57c548164143bd3b77d16ae74fd8fb84518983dfd015146ac9d0503c858f0022591345c077656e5af22cc78f1d35a02ad1e74c8c4", + "blsPrivateKey": "0e4d854f9c5f345fea96ecb91625e50bf6bb69bb71016647574e71a7f2d762d2" + }, + "encrypted": {} + }, + { + "address": "lskjtc95w5wqh5gtymqh7dqadb6kbc9x2mwr4eq8d", + "keyPath": "m/44'/134'/25'", + "publicKey": "57162b1d7e5239fd93cc1f440d1493fff3582bc28eb14badf324e06756ed19f7", + "privateKey": "d0f245387c82d06e5595624ef96f13b8a0c1eb4430d6d606091afc4de365132e57162b1d7e5239fd93cc1f440d1493fff3582bc28eb14badf324e06756ed19f7", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/25'", + "generatorKey": "633e1696edbd9f2eb19683c4f7e0d4686fefb1a15772a1affdeb49a44d8c04f2", + "generatorPrivateKey": "c74dcc813c8011ef00936750155f3c06fae9382d25d716e81b9d35238f0d97a7633e1696edbd9f2eb19683c4f7e0d4686fefb1a15772a1affdeb49a44d8c04f2", + "blsKeyPath": "m/12381/134/0/25", + "blsKey": "a6e64df0d2d676f272253b3def004bb87276bf239596c4a5611f911aa51c4e401a9387c299b2b2b1d3f86ad7e5db0f0a", + "blsProofOfPossession": "92ff87e4dfebfdee0e5572e94f62c483a9b4465eada10c3a6bed32fc92374dbbe89eed00117ddb27bfbabc5e41d90d8a0701fd215caef0233eca660d7a0bccdaf064356edaab13aff404aeb5264d8b68ab0808115e09ef541168364806a62d49", + "blsPrivateKey": "3904de0fc9bcadab43d1b2d5f79cc197e59d96e99afa03da6acedac40ab3229a" + }, + "encrypted": {} + }, + { + "address": "lskwv3bh76epo42wvj6sdq8t7dbwar7xmm7h4k92m", + "keyPath": "m/44'/134'/26'", + "publicKey": "d22846c90b31913318a4e9d5e57cda760e1e35316d16fe8b43066c407c9b148a", + "privateKey": "fff5a4e22fb9473f23b9c8d5abe45175ccb2eae77710f8d99672280c685af3f2d22846c90b31913318a4e9d5e57cda760e1e35316d16fe8b43066c407c9b148a", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/26'", + "generatorKey": "f99c543eeba441fdb22c673fa81878269c3b69a6366d8d51fb6890f2eb3118b6", + "generatorPrivateKey": "0345913f3b2283ddb51285af6e9f2454fafe9d8f4438d5e60281b8753811476ff99c543eeba441fdb22c673fa81878269c3b69a6366d8d51fb6890f2eb3118b6", + "blsKeyPath": "m/12381/134/0/26", + "blsKey": "8ae82e86c2ae47fe55b3db422b5f6e8a8ecbf4a33a0e910b4cc53d1bef0d66e3d19e8474a97ba58e31798c604758b1d5", + "blsProofOfPossession": "9215a181382a5769652e3818238e58496ca1c80eb6282b000708b2c9c19464153fcc8a541d8aa32378186b61fdb2183d15828ffa20e49a0dae0cb05e8c106f894a7ee7190c6eb60874477da236c05a275187bded6ac5a9c98656eb2199f736fd", + "blsPrivateKey": "474a20eda00f30146da307c7bd171cd5b91ea5b6d44641d4677d39d9aa9bc27c" + }, + "encrypted": {} + }, + { + "address": "lskq5attbvu8s55ngwr3c5cv8392mqayvy4yyhpuy", + "keyPath": "m/44'/134'/27'", + "publicKey": "a1f052d86f89b7848e21eb71448d8c985a79c16e51ac7c76f72da5eb6480cf58", + "privateKey": "911fb9ae6147af11ee3fc36ade5a411a4c627d08eba07ac1d38c10855bfb2556a1f052d86f89b7848e21eb71448d8c985a79c16e51ac7c76f72da5eb6480cf58", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/27'", + "generatorKey": "1819bea0ff11aa0cde16c5b32736e7df274f9421d912a307526069fa119100ca", + "generatorPrivateKey": "9e5678be030e043e8ed9876ee4012cf293b95b44759d75a8a6ae8849901afc8e1819bea0ff11aa0cde16c5b32736e7df274f9421d912a307526069fa119100ca", + "blsKeyPath": "m/12381/134/0/27", + "blsKey": "957a970041ae9b29f33cd9baaf077f77049e664c8123b22fda3793252f71916c5df0b103ffad5cb75bdb2724d9ca3eba", + "blsProofOfPossession": "80d4fdac09ce195c9d685a751fb7cd9d4da7b9dc906348b4bb741ceb53f876afd0bceba75b36327a8cbd8bd3ca8ac2cc14b4fede3ce2cdac7f0bf0ad5e58840c64bdd0a0905cd6aa5da8acfcb33a931e469cadc27a42c2a04a62fd6ecca05091", + "blsPrivateKey": "1c73ac651be2f72f2be31639e6aad77493d00afa10b7138f60ab5d9da1abdb8f" + }, + "encrypted": {} + }, + { + "address": "lskdo2dmatrfwcnzoeohorwqbef4qngvojfdtkqpj", + "keyPath": "m/44'/134'/28'", + "publicKey": "ebd7440bf10d48e5d4601b5815b69c9d74fbdf9578db8477c94f4856b85a04ca", + "privateKey": "fa77c6df262210a67e6306b286b85d8fd77bed6fe33250c170e87e7cfdf0bc91ebd7440bf10d48e5d4601b5815b69c9d74fbdf9578db8477c94f4856b85a04ca", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/28'", + "generatorKey": "bbc7ca5acae1d53e0a44a212f4c77c7601ace0e489d936c0b6f26a9fbb03601e", + "generatorPrivateKey": "34d0d867fb2a43007f160ab304ca1d779871d60fca38e64e688d85cee4dd4331bbc7ca5acae1d53e0a44a212f4c77c7601ace0e489d936c0b6f26a9fbb03601e", + "blsKeyPath": "m/12381/134/0/28", + "blsKey": "82b478f1b884ee4c152490afc8b233d003745a58c236b00ecb3cea1022d59f04bf225266bbe5b0a5aa7da0a771a66acc", + "blsProofOfPossession": "ac4d05f93e3c374c83ab9cec2a5c67dff8a02298361584267968fad8f391af083b5041a020ce7a189fd8fdbf055a265c04f55e80a8dcf06e7b4e3358b347743f47d33bd5ee0cc4d4213995c46d6d4e1a61be929f571c1a0fa1c7dec805a85805", + "blsPrivateKey": "4fda60b27305f21237ae97d5f91c52455e10a242ec60997468b1d65d3f979d48" + }, + "encrypted": {} + }, + { + "address": "lskoq2bmkpfwmmbo3c9pzdby7wmwjvokgmpgbpcj3", + "keyPath": "m/44'/134'/29'", + "publicKey": "886ababad3572e81567a65320e1d4fca7de95ad69a305564be7625cfcedb531e", + "privateKey": "9f9ca7d38aa4db5b9a6e3c7f593f7862ca8cc87da5cdb0c88e3f3a45ceb882f5886ababad3572e81567a65320e1d4fca7de95ad69a305564be7625cfcedb531e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/29'", + "generatorKey": "8cda7b8df8975d781e053882a1373d190d5f8fd7c13ab528be8597b5d06ede57", + "generatorPrivateKey": "93771355236957f57b4bfabbc1d7e3c2cf72f5b0ef78e62471d455d44f13fffb8cda7b8df8975d781e053882a1373d190d5f8fd7c13ab528be8597b5d06ede57", + "blsKeyPath": "m/12381/134/0/29", + "blsKey": "882662250af65099ca817b2564576582981f23746f07be09ebc03ed6aa582a327d4156ff4a12851bce3ad77be854f937", + "blsProofOfPossession": "b73f34042d210b6cf0ba61b04e26bcb08e4d671a12df09e592c14c73ac55df09a01adf94b205b86a9ac9020cc719e93b0f890050891d9f8622346f45112ce502e26293a14c36501a8f1947c33fa38535d6eae6c4af6679296e76a105e899341d", + "blsPrivateKey": "130e7d4aedeaaf42ff9919b87496c80d0ef2cbe38a6e47ed7f7b8b4140a11700" + }, + "encrypted": {} + }, + { + "address": "lskgn7m77b769frqvgq7uko74wcrroqtcjv7nhv95", + "keyPath": "m/44'/134'/30'", + "publicKey": "c71ac98a32b133bc6fa8dbb6d42d87110f44fe4f3b74ca58fd60fa0d6010c285", + "privateKey": "83e39036d9000e4a92da3e96ae1a41b21d8ba158840447ac5bb7fc94db9bab9ec71ac98a32b133bc6fa8dbb6d42d87110f44fe4f3b74ca58fd60fa0d6010c285", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/30'", + "generatorKey": "0941ca2cfd9b1e0cc4bf0dbfd958d4b7d9f30af4c8626216999b88fc8a515d0a", + "generatorPrivateKey": "9b7b095990f701463a893d5534af10f3b850190ee94d3c5c114f50c82778a7bb0941ca2cfd9b1e0cc4bf0dbfd958d4b7d9f30af4c8626216999b88fc8a515d0a", + "blsKeyPath": "m/12381/134/0/30", + "blsKey": "8808cb1e4cb5c8ad18ad4a45e35388af4099993effb9069a28e56c5718944a3b4010ec1ef54b4faf4814fad854322468", + "blsProofOfPossession": "890995fe98a83721b0069aee00c2b264239b3b833b71f64a5f48b4340a969fbac1ffc0664264fbf5af626d37fb3fe6d403dc7ef0ec195cdab82e7615d73ad7a2d326a761fdcf18a6a83efc4f502c724a10ddd89f8b6981496c34b1b32f512781", + "blsPrivateKey": "00687a9dd373f8c15a883f678c6036273d34dadfb8236a840609ecbc67faa4b6" + }, + "encrypted": {} + }, + { + "address": "lskfmufdszf9ssqghf2yjkjeetyxy4v9wgawfv725", + "keyPath": "m/44'/134'/31'", + "publicKey": "81107e3e00332a827112444a1d53532e6e519acbf741ec3a58e318d6bfa05577", + "privateKey": "07ca3d10e8a88b2414ff218a849d8b66d84bd8e2290377f13b42cea907c77d7181107e3e00332a827112444a1d53532e6e519acbf741ec3a58e318d6bfa05577", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/31'", + "generatorKey": "24bab6ba79973ffaa8569af2cb69b8495d20f0c7ce674814ee0615d31abe9607", + "generatorPrivateKey": "58b70c32dea6cb47393427b3cb6c5581674e620bd771d946d4d05588c097749224bab6ba79973ffaa8569af2cb69b8495d20f0c7ce674814ee0615d31abe9607", + "blsKeyPath": "m/12381/134/0/31", + "blsKey": "96bed36ef328566d826a6f6b874ce441ad34373487b4bcc2d48d76f2dd453e418935a7b60578c43b9c4dc954e9331a3d", + "blsProofOfPossession": "b4d80456953b5111777a74931f5691a6e4c0bc4f4d552aeee9ed1002903b366abab12e2d596a4387933ec676058ae64e15d7b322786d19744281028753b621ed7d49b6e6bf87983267d3208c3dc5da983d845a7a2822da4a085446172e823b28", + "blsPrivateKey": "59c7cbf878eaf29c9e691f3c2d9bca2cf0fdec574bc037e1e156c730bf684b54" + }, + "encrypted": {} + }, + { + "address": "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "keyPath": "m/44'/134'/32'", + "publicKey": "9ea73410309a58c1f0c18d8821baa56ea2fd654215ae94d0e3ae808c7ad5e90f", + "privateKey": "64be15e273d24a39a7af8b674b6af47063c7db0b5ce61fbf9a1353e94a00cbfd9ea73410309a58c1f0c18d8821baa56ea2fd654215ae94d0e3ae808c7ad5e90f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/32'", + "generatorKey": "f07a86182356aee3fcfb37dcedbb6712c98319dc24b7be17cb322880d755b299", + "generatorPrivateKey": "6f3e9367328500bfaa95f7fd94e848fd6100f5e10bc77d439585185d20dea1dcf07a86182356aee3fcfb37dcedbb6712c98319dc24b7be17cb322880d755b299", + "blsKeyPath": "m/12381/134/0/32", + "blsKey": "b19c4385aaac82c4010cc8231233593dd479f90365186b0344c25c4e11c6c921f0c5b946028330ead690347216f65549", + "blsProofOfPossession": "b61a22f607f3652226a78747f3bb52c6d680e06a8041fc1d3a94a78fabf2895f23559059a44b0c64cd759d33e60a06060197246f6886679add69f6d306506336e15cdc7e9bde0aaca6e8191fb3535b5685ce8b3f33212441d311444a3d57fc66", + "blsPrivateKey": "4e29180852b97988e952ab7de895a55b14c283987a55f5df08cd1220b7d2df83" + }, + "encrypted": {} + }, + { + "address": "lskqxjqneh4mhkvvgga8wxtrky5ztzt6bh8rcvsvg", + "keyPath": "m/44'/134'/33'", + "publicKey": "7e4874d02ad84042e1fa3bfa61954d070308080f3cbecdf29d7fbfd66edb46a1", + "privateKey": "c17df1663305582bcc4b234e5de32a07e8c379970e101ffe3d787f082ed5f3d67e4874d02ad84042e1fa3bfa61954d070308080f3cbecdf29d7fbfd66edb46a1", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/33'", + "generatorKey": "a2b5e97ac5a5b3c3a7cd9b4401eca1f4e8da59fe567e229ea47e65bf40053402", + "generatorPrivateKey": "7e95bcfa2cb10e89f5036b3431446c5a55c115ffbe926443507943d48f8062b6a2b5e97ac5a5b3c3a7cd9b4401eca1f4e8da59fe567e229ea47e65bf40053402", + "blsKeyPath": "m/12381/134/0/33", + "blsKey": "abc1d1ef1f992a9fda45841079516169c879421f4260194c0a47e46afdb9f349c2a51e66e9f2ee8bf22231027584a6bd", + "blsProofOfPossession": "a16aa0fe3bfd5383c2fd874be4feb930f2c75f5d35d0e0ab314eb545a673aa1854ebfee7b15a026d5a9fb02842e54672149382f2898a0e12756bb949772b1316163ba774768c88fc90c2471afe94140d8d8f16974f2ebf050358cd98587b32ce", + "blsPrivateKey": "471a10414c7c89584cb2bf93a300426038301ce2b1197ab7f8752708beafc7e0" + }, + "encrypted": {} + }, + { + "address": "lskr8bmeh9q5brkctg8g44j82ootju82zu8porwvq", + "keyPath": "m/44'/134'/34'", + "publicKey": "1b62f211c18f7f707b41d0396f1a71ccfc7b27095728abb7aafda77c7d874857", + "privateKey": "6f11ae1da057f6681b404800e955b8b6ab43d742473f67e60af2e3aed04ff16e1b62f211c18f7f707b41d0396f1a71ccfc7b27095728abb7aafda77c7d874857", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/34'", + "generatorKey": "8062134a09cc464fe9465cda959b402a3d4506a1c44b3f5cba9661d42e912421", + "generatorPrivateKey": "daba1869775231db6c57d0d49ae8731693816165431889bb7506baad362d2ab58062134a09cc464fe9465cda959b402a3d4506a1c44b3f5cba9661d42e912421", + "blsKeyPath": "m/12381/134/0/34", + "blsKey": "a8271f9e8874eebb6d66dc139e984b6a6c71d2a7e23c6d7061bab7725e9c65f2e2123778130a2acd278f155440debde0", + "blsProofOfPossession": "84a3aeb2cc8329afc63f40d137b017ebcffe6df9e55bdaad8249408d01dad5025f1c83faecb53955ba5524df25b0d85e180f0335d0b5ac8c82c7f5fd0975002fe0231a83754c0034b07175afc426b17978870f8326cfe4694ff723e08d0b6a61", + "blsPrivateKey": "55416acd8c266c470540c3ed4abcbd22b1b936cffa4b8ce620bd9d8b63c0dfc8" + }, + "encrypted": {} + }, + { + "address": "lskbm49qcdcyqvavxkm69x22btvhwx6v27kfzghu3", + "keyPath": "m/44'/134'/35'", + "publicKey": "2a4aa6527e9f9bc2c3d3b4a9a22be543e95703593ed98989285e0b92ec6f3af2", + "privateKey": "134dad94b73ca57153ed7d9f37da7b94ae2f3b64d74a62e12524fe7bddf7c8af2a4aa6527e9f9bc2c3d3b4a9a22be543e95703593ed98989285e0b92ec6f3af2", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/35'", + "generatorKey": "4ec3ad70d3d35f0d684960e7938fab016d12c6c7cbb8312a8cff776dbaf2ca4a", + "generatorPrivateKey": "67bfc7dba3246b82db00c25ef844f5da3008439cefef1a9ee308accde7c7bfee4ec3ad70d3d35f0d684960e7938fab016d12c6c7cbb8312a8cff776dbaf2ca4a", + "blsKeyPath": "m/12381/134/0/35", + "blsKey": "80d7d0598d4e79ceea22c56d16e747cd5ef94469bd036945d14a5d1e06eb700f9f1099d10cfaddddf9e88ac4c9f1086a", + "blsProofOfPossession": "b7890264708b9d3341d90864f9120cd84090592a6bc5a419df94e86a638a0055e7dc3846cb89869cf46305611e49cea007711f35a5effd3099e56b5108a4103215a6ba9195c4694064ba661502e852b43e9593b0a60bcd2b567fc97565054500", + "blsPrivateKey": "1f7ad690ead2cbfc3d51e287d19158d2db2320c8498e72ff7ade0554383d0f01" + }, + "encrypted": {} + }, + { + "address": "lskatntynnut2eee2zxrpdzokrjmok43xczp2fme7", + "keyPath": "m/44'/134'/36'", + "publicKey": "8ee575c0773a3ec9164ad157b8de1b66fb30cc315e8ddb92d4f6eb007fe0f154", + "privateKey": "f012923591f4a0431781880c0adae26b162e035ffb3855e201d11903ba2d78cf8ee575c0773a3ec9164ad157b8de1b66fb30cc315e8ddb92d4f6eb007fe0f154", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/36'", + "generatorKey": "ce6bdb7380fa027c46edd15a072bbabd6b60fecb0e09589e20be560b333ca63e", + "generatorPrivateKey": "5b52fbe120967f200be5f0ba55608668cbe1a60b139f2aa646c0589fd295fcf9ce6bdb7380fa027c46edd15a072bbabd6b60fecb0e09589e20be560b333ca63e", + "blsKeyPath": "m/12381/134/0/36", + "blsKey": "97a4b205ac2b65a2f17ceb49a763393935021629068fe8a8c299e49b986e79ff8cc959a7343b5d00eae2783b825ffede", + "blsProofOfPossession": "8a86fbb8e59ff0de4f2d717ff3c7b0f3f9cb4b14f97deeffb907428666005e613b02cfac0bac4714389d898236de2d5a02df536b511675d2cbd37dcac6dc33bf4cf2d9d43cfa710b3c695bcb8cd29867477ccf3b1e5b9e3afaf7d8d4e50930ff", + "blsPrivateKey": "0fa3a86ad57f1ac10c478b2eea9c5379973316cd0484eadd1ba260da85ff908f" + }, + "encrypted": {} + }, + { + "address": "lskee8xh9oc78uhw5dhnaca9mbgmcgbwbnbarvd5d", + "keyPath": "m/44'/134'/37'", + "publicKey": "b5ca7fa887bfaab853a49e71c086023984c8ea089fd42ecf0a086810a2e6f78b", + "privateKey": "86afad2f4142a2d57e08fafaa6f1ed70af9a0831ef7d18e6ed89adaa61b66754b5ca7fa887bfaab853a49e71c086023984c8ea089fd42ecf0a086810a2e6f78b", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/37'", + "generatorKey": "aed740da1a7204422b92f733212398ce881c24a4cfe40edeea6a59a0f6453743", + "generatorPrivateKey": "71bf7039b3951c6742390e997201c7c5b13ad712f60f214846456c3f15342024aed740da1a7204422b92f733212398ce881c24a4cfe40edeea6a59a0f6453743", + "blsKeyPath": "m/12381/134/0/37", + "blsKey": "929d5be8abbc4ffd14fc5dc02ae62e51a4e8fff3fd7b5851ec3084136208ceac44366a7313447858e3814ddc4213d692", + "blsProofOfPossession": "88e7331baeba342eaa907cfd7a1b5bc839a70e78b0535d68c40ddc2e4d5157f8d1ff55d29243fe2375fcfef5c3a2133e0a0d11f8b58041278a1e9a3a9e7986f906201df48987e8f8eda2e6ee4452fe58b54805e2ca4cc256d8e42083b70f79e3", + "blsPrivateKey": "032de7290e108bb21cbd7e0084f5db140a2d365629b07cafea6c46a0c705775e" + }, + "encrypted": {} + }, + { + "address": "lskcuj9g99y36fc6em2f6zfrd83c6djsvcyzx9u3p", + "keyPath": "m/44'/134'/38'", + "publicKey": "1efe4983f0e29699afff6fa2917716b2599a88c23f21508b85a22f44c7ee1b62", + "privateKey": "9fd97aaf86fdd14e435e8b9356155d635e52fb7b885ea6e417cd7f8376720c761efe4983f0e29699afff6fa2917716b2599a88c23f21508b85a22f44c7ee1b62", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/38'", + "generatorKey": "80fb43e2c967cb9d050c0460d8a538f15f0ed3b16cb38e0414633f182d67a275", + "generatorPrivateKey": "c1aa3e4f44c0a57c27898b9055be4dc7d92b8ef0949ea812ed10eac89278978380fb43e2c967cb9d050c0460d8a538f15f0ed3b16cb38e0414633f182d67a275", + "blsKeyPath": "m/12381/134/0/38", + "blsKey": "b244cdcbc419d0efd741cd7117153f9ba1a5a914e1fa686e0f601a2d3f0a79ac765c45fb3a09a297e7bc0515562ceda5", + "blsProofOfPossession": "b7a186c0576deeacb7eb8db7fe2dcdb9652ea963d2ffe0a14ad90d7698f214948611a3866dfedcb6a8da3209fee4b94a025864f94c31e09192b6de2a71421e5b08d5ac906e77471d3643374a3d84f99d8b1315f44066c044b5cdbfdfeceef78c", + "blsPrivateKey": "0c629e3c91960c817e7993d8e2f7a567b1a704af52d08ba039b68b719bdd8247" + }, + "encrypted": {} + }, + { + "address": "lskuueow44w67rte7uoryn855hp5kw48szuhe5qmc", + "keyPath": "m/44'/134'/39'", + "publicKey": "6b01a532bd79010ee18fb75732356208d96c0524c257913b2b2ad903d55dde13", + "privateKey": "6e7c3feb90fb9f0d50d8892c491a60e9c165bc66c3e5e189f431977a0b6e7fdd6b01a532bd79010ee18fb75732356208d96c0524c257913b2b2ad903d55dde13", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/39'", + "generatorKey": "ebe1d6189c7015d175414db9621a602b0912826c1eb1aab09e69bb33ca8fcda5", + "generatorPrivateKey": "30af73eed356c281a256d2a8c94c3b0eb8676078bddc3cda67a1e8d42a44f3f2ebe1d6189c7015d175414db9621a602b0912826c1eb1aab09e69bb33ca8fcda5", + "blsKeyPath": "m/12381/134/0/39", + "blsKey": "b7c47fbb0d7e3793460949c9dd6120a310eb52de67f6cde55c022b05dd5053074c8a0e562896a482c787eb2eea82353f", + "blsProofOfPossession": "a265237ff848fe7acb4c84b6f68008ee7ec917a7a11c050f630b834e5caf22a447de94de0e7c52d03b18e003e5f9a3f2091cb5a78817ba42a7e19c714af47ad0b94824c5b90862059ed3042446143c56c4df011389eb42dfa2daa58df677d473", + "blsPrivateKey": "67cbba27c5ab5ef4f50f963cfa680bb745e565a7b26cd6a3755ece6ff0e238fe" + }, + "encrypted": {} + }, + { + "address": "lskm8g9dshwfcmfq9ctbrjm9zvb58h5c7y9ecstky", + "keyPath": "m/44'/134'/40'", + "publicKey": "4a4a974345c653a5f83e6f24f40ab4757bf07dc4f19d8070faa9852120f57549", + "privateKey": "80f077113e432f2360676c28392aad1f73012f62053c95e9fd411c9a3e9a32d44a4a974345c653a5f83e6f24f40ab4757bf07dc4f19d8070faa9852120f57549", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/40'", + "generatorKey": "497a5b80edc6b9b5cca4ca73fd0523dbd51e41c1af5f893e301cfa91d997573a", + "generatorPrivateKey": "4a7e5a09ed1049e59a3e3d10a27dca47b0f3ad8efbe25ba554de7e2e63cd522e497a5b80edc6b9b5cca4ca73fd0523dbd51e41c1af5f893e301cfa91d997573a", + "blsKeyPath": "m/12381/134/0/40", + "blsKey": "8e3f9dd02f46bbb01ec1ffbe173b6a28baa3ffaca943afe51c18dc5220256a3994cd0b0389c835988a64076b4e81c837", + "blsProofOfPossession": "980f00e7752adccb907eaea0fc31ce62dcaff9bf1c6b7066c5071829c91456a8d1e266cb0a9ef4916ffbd09295508a350d21e9123e5cc1c00d3ef65f5493c93c5b993e9768960d4210849743dc2b995657cb0aee7d46d6482e3545b89f06f895", + "blsPrivateKey": "2b67cf8da21f38b44a13674b270c912b50d3c74981d76e354558da1c1f2c829d" + }, + "encrypted": {} + }, + { + "address": "lskhamuapyyfckyg5v8u5o4jjw9bvr5bog7rgx8an", + "keyPath": "m/44'/134'/41'", + "publicKey": "439ad025289bc36c9bcaf79a04116d1cdc5ee87fd5ecb93be83ce761d69c7733", + "privateKey": "3f2353712bd5e51be220f1632571a451a9f357a4f7e292fbea8d9f7a52c8167e439ad025289bc36c9bcaf79a04116d1cdc5ee87fd5ecb93be83ce761d69c7733", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/41'", + "generatorKey": "d051790a70ffdf5bd80dc9cec003f8261128be1fc2135990accb13caeb3ed588", + "generatorPrivateKey": "ca0202c84b1675a89a53758e639447336b52042309014c9def9d84bdf5c5e229d051790a70ffdf5bd80dc9cec003f8261128be1fc2135990accb13caeb3ed588", + "blsKeyPath": "m/12381/134/0/41", + "blsKey": "a2fc837b51e6dd740fc1530e6713b0f8c04e646e91da849517901f24d9bcc78c360223f1ad3692de2e96444008a67e03", + "blsProofOfPossession": "82d6fee11dc1561ffb5f36bf07acdffb95e5c329f7adc0b8937bec191350d7c4a158c7592a179ed86b9c0e20159e903100495fcd3fb5bee481e053775b232f8e0fce602e8ec6edf0fe8ba90c06e6215d7c73e88a626d2fe63c6422826489d72a", + "blsPrivateKey": "1cc66f8abe734f69e212c028ddc5e8a5266f16bb92cbd23a11a2701374108a11" + }, + "encrypted": {} + }, + { + "address": "lskrzuuu8gkp5bxrbbz9hdjxw2yhnpxdkdz3j8rxr", + "keyPath": "m/44'/134'/42'", + "publicKey": "3de31d0eccc3e0d5c0a017a4066108ea909b7b9b97a046d55ea207b94d9f7570", + "privateKey": "67cecd53def499f8e0eb3c9cdbb4e330e2f5b4133e30e5f5398d40f966b8c0ee3de31d0eccc3e0d5c0a017a4066108ea909b7b9b97a046d55ea207b94d9f7570", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/42'", + "generatorKey": "d454f04eb0e05c980f6a3427e98d73493665860ba7a29eb915cfc0b8daae2849", + "generatorPrivateKey": "406b400c1bfa9d0462ef8fc4100a7f918c16a3823f1dff057cd7028d6865cfe9d454f04eb0e05c980f6a3427e98d73493665860ba7a29eb915cfc0b8daae2849", + "blsKeyPath": "m/12381/134/0/42", + "blsKey": "b1b4ba05e7116670be55b6d9fc28574d142824175a1e3d1cdafa37f193c342eba1a85d8520a9fd962811fe63a5a2d048", + "blsProofOfPossession": "99f7e39908f0cabbfd156c78a903d6968c455f5edbcb878525abe1217674d9745da87057f1fa93ccff79632253d5b4fd0c6301b0b9eb0e07fdd4c0abc99da0229ceb4a03b0da237657e445a7bbf6877689bfc027d65f24f05982dc2aeb34c72d", + "blsPrivateKey": "6cda6e97b66b400de912562e266710fe0df80ab4c6c9d91c9f2cf03e4e0a3834" + }, + "encrypted": {} + }, + { + "address": "lsk8dz47g5s7qxbyy46qvkrykfoj7wg7rb5ohy97c", + "keyPath": "m/44'/134'/43'", + "publicKey": "1261a41de66aaea2d66bc2b4ad5b7d25fbe013c11aae160bad70378b6049fdca", + "privateKey": "a80610578bf678af963bffabc131a791a590830abce950d15b95bae03ed5bd1c1261a41de66aaea2d66bc2b4ad5b7d25fbe013c11aae160bad70378b6049fdca", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/43'", + "generatorKey": "567e1e27c02293d7c190a1eb203c2daf1935a9901de66df73f8e4eeae6907d04", + "generatorPrivateKey": "72be4840bd46fc9566a1741499fce3fb9152e01ea28df6f1e834f35ba3d14f09567e1e27c02293d7c190a1eb203c2daf1935a9901de66df73f8e4eeae6907d04", + "blsKeyPath": "m/12381/134/0/43", + "blsKey": "a2f8fdf2b80c987ae61634125c54469928728ecb993bab3db892725b16b41ec48c36056eeee2a1c9b073d12bdf917684", + "blsProofOfPossession": "abded9f3ad588edba52b7b2a4b3ff25f630aefae0d7a91827bc1fb7b8cba36d27c310a7a58a4a66ed9a8d90ffc0aae6e17718b1fa3f8e7305498e740d531460702a7dce1e32c19e18849c786c26a30e29b464c7202dd64d021c1eef643de519a", + "blsPrivateKey": "2d11ddcb18798ed85425c100ee31309725153e3ddc769531dcc8939b9ba135b5" + }, + "encrypted": {} + }, + { + "address": "lsktn6hodzd7v4kzgpd56osqjfwnzhu4mdyokynum", + "keyPath": "m/44'/134'/44'", + "publicKey": "441132064a0a5cffb2d28f4306fdf4c784e6bcd0f72a8b0e2e70f11812afd9aa", + "privateKey": "50b8e65ecc714b5a02b3ad6e6769e4dbd8ed4b9fc87f2d0876f1c9d705af49ce441132064a0a5cffb2d28f4306fdf4c784e6bcd0f72a8b0e2e70f11812afd9aa", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/44'", + "generatorKey": "6158b2a5b662ce05c7864dff4c2aecf6109cdea1be703a79147450b082ea242d", + "generatorPrivateKey": "59e643809298d20fe0789fa76ce08a150c1d75602a8c5939b6dc468700ef2fc26158b2a5b662ce05c7864dff4c2aecf6109cdea1be703a79147450b082ea242d", + "blsKeyPath": "m/12381/134/0/44", + "blsKey": "a97efbc836dd4028813063912bcadb52fdb8e4d2ba04d7bbb477d2a97e16167c5fa6ba75e482cd7a7d476d78fed1550b", + "blsProofOfPossession": "995df23eececc27026f62816bfd07d71696e2dc5751bafb03d50bd9c66d388c562d6c1357300e4d51e5522edc3cb5ae217b3607795baa0209c6e63db01b4b7c28452c15db1366764abb9d886d0a908da07d3b7b2612e263d95721ffccefb4aa4", + "blsPrivateKey": "5b4e861123695a603833f8b442e474692b7b197e38c5be4a45a2e04244ed9582" + }, + "encrypted": {} + }, + { + "address": "lsk5pmheu78re567zd5dnddzh2c3jzn7bwcrjd7dy", + "keyPath": "m/44'/134'/45'", + "publicKey": "1f1b9cea61290f9b2380893ab949c6831315d6c2610371573de28cce16167595", + "privateKey": "d8165b1dbf9e5eb9d710739aaa552b4083d59f3a22c549b8141508a014edcc311f1b9cea61290f9b2380893ab949c6831315d6c2610371573de28cce16167595", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/45'", + "generatorKey": "62c37caa9ecdb3874354e7f780cb4463ad190bc31e75e552cb07b9bafc658f2c", + "generatorPrivateKey": "2ddf26bf710c8ed14e327cce8b8f5e196a3d43d731c1d007554f4d052edf5baa62c37caa9ecdb3874354e7f780cb4463ad190bc31e75e552cb07b9bafc658f2c", + "blsKeyPath": "m/12381/134/0/45", + "blsKey": "809c35a2a1f510fb574a223474fb6b588daca95ab1b9b04f4f0dcdcd4581f05914eb1b9683d21997899ebf730d82a8a7", + "blsProofOfPossession": "a2fd6eca6018825969d8b9de58e6594149c5114cea9c27997f2ec67b923cbe562454caa5a5e956b3eb5ea0c5bd9b0196137d4646e21b51bd21503dde474d510f62654bb7ffd141fa3462997bc6662f2893cff7d917eb07f2985dae860723bd46", + "blsPrivateKey": "692a0a8a17a80c888ef3ef9e5c7e5c11b6bf65250a03f3d22455a81c39480d6a" + }, + "encrypted": {} + }, + { + "address": "lskwdkhf2ew9ov65v7srpq2mdq48rmrgp492z3pkn", + "keyPath": "m/44'/134'/46'", + "publicKey": "9cdd0974356c09da1f6234c8f7e3ad8a08ba0e2828cbac81dddfc3f36d54ef11", + "privateKey": "510675c85299b7a430cabfab2b73a3103639c832b40cd42fa3fe6094c54353759cdd0974356c09da1f6234c8f7e3ad8a08ba0e2828cbac81dddfc3f36d54ef11", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/46'", + "generatorKey": "cc83f488c03e58d083927601658d234ffd12b5cb6fe3151206f699d031dc4161", + "generatorPrivateKey": "19d6c31d57d04b6861a868f0032d2e3f2788a06be4ee3642def28bbe1f3f3404cc83f488c03e58d083927601658d234ffd12b5cb6fe3151206f699d031dc4161", + "blsKeyPath": "m/12381/134/0/46", + "blsKey": "8c5b12f5b7aeafb07e14c5264e7f7ecf46b3ba0e6f12619e19271a733e06e913044ea2e5c955eef3567fcc2d842bc24a", + "blsProofOfPossession": "82237a5371179107af8c53ef19bf3e0d055b70ddb689763e0a8ac6d82884d12c2155166af4aa92b66fa64b6a6d2bbe7602a118d597345dc100bd6983f072b9d8da7bd0699b0f3cb51f1ec5a9f2e2feb76030125272325e7f5885399f1d26c5ac", + "blsPrivateKey": "379e94dcd6dad43376c0a0b2a4461fbcfe0bf25d99082a6000b8a52da62648c7" + }, + "encrypted": {} + }, + { + "address": "lsk67y3t2sqd7kka2agtcdm68oqvmvyw94nrjqz7f", + "keyPath": "m/44'/134'/47'", + "publicKey": "6200bdd255930cb10bbd1421d1a849298f1dc5e5dd8e8d00167bfa461745ed81", + "privateKey": "3df9184e5f715bf11494a223865c143376080ebaecd91dc8df2657e5593e52126200bdd255930cb10bbd1421d1a849298f1dc5e5dd8e8d00167bfa461745ed81", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/47'", + "generatorKey": "74f7ff53b55eda8fe9c11d66f7533c27714b121a5918a66c19b309e1c93dc3ed", + "generatorPrivateKey": "38ad961657b3d0e09b61e908362616bef7c86d2ea3b00b1f2f5b325d851ed35374f7ff53b55eda8fe9c11d66f7533c27714b121a5918a66c19b309e1c93dc3ed", + "blsKeyPath": "m/12381/134/0/47", + "blsKey": "a6d6aa277ab636486b7d879e90c541b4952264e18b8a214f58d32226fcc774a8e5bdac69223902424110cbda4ab58907", + "blsProofOfPossession": "a5b91b5e3881a36ea1b209f1cc09ab447e365b111e7529a88981e4e44c4a05eaee0507ff80460453e23187116510dc770d517e16aafc1de2aae2393ddd2e26cbe6fd096b65ba48cb6dacd0862d6c39b394117a596c0a1c9bae8d9b538d6e6dfa", + "blsPrivateKey": "0784ce0bba95107e6d4b8372f850e42ed3ea5f2a4cbc8931349bb6509e1e69f1" + }, + "encrypted": {} + }, + { + "address": "lskowvmbgn4oye4hae3keyjuzta4t499zqkjqydfd", + "keyPath": "m/44'/134'/48'", + "publicKey": "72d227ab88f971ed5da047f0a037ef302b8bb8dd3243f19bcf7f366484262a6f", + "privateKey": "b29ada34b8eea59af00ac9816ffbec398c2654ff21a7d95fc833d180b462ab9c72d227ab88f971ed5da047f0a037ef302b8bb8dd3243f19bcf7f366484262a6f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/48'", + "generatorKey": "f926fbec6d2e461af7c58d87754524abd26ab1f617d73348ba1318d371f7cac0", + "generatorPrivateKey": "f99b68a87d6a0fbedee01e277f2c9ac0381868fd48b3dbe91687cb2ae0b3f45ef926fbec6d2e461af7c58d87754524abd26ab1f617d73348ba1318d371f7cac0", + "blsKeyPath": "m/12381/134/0/48", + "blsKey": "ac304b4ad4fdac88bf975496edc43af0e324120984d5a12ac073b3e3e80c593470b6aa4f10b9897451bd6ee6f569a2af", + "blsProofOfPossession": "b08e154f3db163391dcbef182a63ad51d56521951307b9bcc60f12c83babeb5eef80b6d8503848acf9bc864adaa82bd610e3145dd77debdfcaa8e1e15f13e6da1d5bcfca4234b46208900c6ce35d0147534a7abc728504d731f286edc31a3ae3", + "blsPrivateKey": "5fba886b2e721c7d3165f301c3f6d3722e140f36b2e3b45a53999486bcef94bd" + }, + "encrypted": {} + }, + { + "address": "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "keyPath": "m/44'/134'/49'", + "publicKey": "8baada3c82ea9bf2dc8113c02b90ae5c461eec9329322bf0ed6cbeee104c1583", + "privateKey": "2a3ead5a95ca66f56dc6e4a0f65ee3ee56417b2b1535a93a5c05d2f3471d8a078baada3c82ea9bf2dc8113c02b90ae5c461eec9329322bf0ed6cbeee104c1583", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/49'", + "generatorKey": "761b647f4cb146f168e41658d1dfe0e9c01e5d64b15e5c033d230210f7e0aaa8", + "generatorPrivateKey": "2f672b0ced7c82df2ac79fece05ec6d580b41a4dce590cca6ce68670e6485993761b647f4cb146f168e41658d1dfe0e9c01e5d64b15e5c033d230210f7e0aaa8", + "blsKeyPath": "m/12381/134/0/49", + "blsKey": "b61f2da61bf5837450dcbc3bca0d6cc4fe2ba97f0325e5ee63f879e28aa9ea4dd9979f583e30236fb519a84a9cb27975", + "blsProofOfPossession": "807bca29a9eea5717c1802aebff8c29ad3f198a369081999512d31c887d8beba1a591d80a87b1122a5d9501b737188f805f3ef9a77acd051576805981cd0c5ba6e9761b5065f4d48f0e579982b45a1e35b3c282d27bb6e04262005835107a16b", + "blsPrivateKey": "69e9d76531c5655493d7711602556385a3f5bbfbb6bbcb7beaef2c9609f561cd" + }, + "encrypted": {} + }, + { + "address": "lsksy7x68enrmjxjb8copn5m8csys6rjejx56pjqt", + "keyPath": "m/44'/134'/50'", + "publicKey": "2dba645a063a638489186b825e0c9a9f03628b13e64ad79e9d813b8f6351a308", + "privateKey": "617f7f85f1969c785830105cf75d510d1f1ecf777d5a81468b019da740adb2f52dba645a063a638489186b825e0c9a9f03628b13e64ad79e9d813b8f6351a308", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/50'", + "generatorKey": "7fb2d69906c5076fa314a4e817ce424bbd4a7a21305cec93a12d31a1589dc90c", + "generatorPrivateKey": "324425049aeff2f1b885fc968c247931703e70b8836b789e3c3b05521e12f6ee7fb2d69906c5076fa314a4e817ce424bbd4a7a21305cec93a12d31a1589dc90c", + "blsKeyPath": "m/12381/134/0/50", + "blsKey": "8a08bdac4af80e0d37ce01094440a82a7e5ac9ec893f9a7870d26a4ec52db8932f36384bc7c3d3e03232ddb7bcd1eef5", + "blsProofOfPossession": "b999cf63290a85f96f0f78326c0eb24c3acce4c2307e1a2f1d621cc75f621ccab510e42aade9b6347e95661475230fbb059cd9e4e22ae17ac73dee58a370159bc6b525ab579de9502b761010e97f6d00f60ddfed05e76a5df3dfe33866c1ebe5", + "blsPrivateKey": "5eb911d435b193fac588ef12f503da2151ae4d0999a2c716a74b5596f56ef66a" + }, + "encrypted": {} + }, + { + "address": "lsktas5pgp3tofv4ke4f2kayw9uyrqpnbf55bw5hm", + "keyPath": "m/44'/134'/51'", + "publicKey": "a0e9cf9d02e72d6ca04e26605d6b271ab8cf0e1ce0f8a3381d7cea5d33774176", + "privateKey": "e794dc66cfffe91f218982d55dd702f1aaec240f660abcc3a46fede53afc26cca0e9cf9d02e72d6ca04e26605d6b271ab8cf0e1ce0f8a3381d7cea5d33774176", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/51'", + "generatorKey": "8307181cf9d1f621261e8a97a5b3b77d64a9a1f589a2c14e42b2380d9c2d6297", + "generatorPrivateKey": "2b9b806af478989e386268a7f0b60692c787c4595369ca5aeac9c69062165eb38307181cf9d1f621261e8a97a5b3b77d64a9a1f589a2c14e42b2380d9c2d6297", + "blsKeyPath": "m/12381/134/0/51", + "blsKey": "a77de9989b5fab42dca028637f401953b9e0fd6cd61dc2fb978daafdb5478ac77d67a37135c67a2178b44e5a35a1fddc", + "blsProofOfPossession": "acafd4f724cd7b9dcaf166aaf212122360f76c2faf4d146e8d0014653c0fe09f750690ea2b9ac6df96300301fb020d3b04c1b79965cc8929e18bd93190a366851033a901e05850770cb69fc28146db719f1ac232a7947ead59e8d584eb3ddb79", + "blsPrivateKey": "611ec2b3cf68944b55c1c6984e0117a257b8978b6e4db51627a92c0806ec335a" + }, + "encrypted": {} + }, + { + "address": "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "keyPath": "m/44'/134'/52'", + "publicKey": "1f40e49cb0fd9fde88cc854973379fe86610bec02dd2029de291080283967350", + "privateKey": "3fa4b30dfbc3fa41e7564edf87e11356b1518572ddd2b39b8ca527ffa30f15d81f40e49cb0fd9fde88cc854973379fe86610bec02dd2029de291080283967350", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/52'", + "generatorKey": "0cc6c469088fb2163262ac41787ea4a81da50d92fd510299ba66e5a2b02d5a05", + "generatorPrivateKey": "24473a6a678d3aec6ef7a75387591473d422d48af5b2db095e8417f3818b27590cc6c469088fb2163262ac41787ea4a81da50d92fd510299ba66e5a2b02d5a05", + "blsKeyPath": "m/12381/134/0/52", + "blsKey": "a5ca55e9a0ab81d48eaad2960bd3ea259527cf85fe62cc80cfd8400dbd2511725c06c3a597868dcc257bbc279e2b3e92", + "blsProofOfPossession": "a092cff10ea18ec3dcf3f6e41cd38537e00602e35107067ace7ab7c97a2ae1de531ebea7fc0c22e8dbcee1f981c439930c7cae474a996b153a66b0cb34e66c6041348aaeb4763413afffe0d947da90424065ee573b3683edbb1e51f9a278ae82", + "blsPrivateKey": "35d93ad8f5faa1e1cbe72ebb42bee49a2219c7d6e30c25742916db086464e8a0" + }, + "encrypted": {} + }, + { + "address": "lsk56hpjtt5b8w3h2qgckr57txuw95ja29rsonweo", + "keyPath": "m/44'/134'/53'", + "publicKey": "777fcc4ed76d3a3f1984421cd9be283e6f7e3d3197c8c753d200a1bcef04b0f2", + "privateKey": "c4f107bf103ff5b3f226f612fc0e80b957549051ff9d665ad8ab9fb1b5e29ffe777fcc4ed76d3a3f1984421cd9be283e6f7e3d3197c8c753d200a1bcef04b0f2", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/53'", + "generatorKey": "d19ee9537ed38f537c2e8be0fb491331575f8e4050dc4a74ccee3244714d5969", + "generatorPrivateKey": "806c6f33920afe19a27e7f677358c72417ae0a2f51766608b83e8c351015eeb4d19ee9537ed38f537c2e8be0fb491331575f8e4050dc4a74ccee3244714d5969", + "blsKeyPath": "m/12381/134/0/53", + "blsKey": "906653b7a74dc35499e0c02f10a9d092e7dae70e5376287b5533c7a52ade678784956e6bcbb67a11239bbfa977743a1f", + "blsProofOfPossession": "a5bdd92d340281c01d90224ca58a13cc429dc47ea9d2ef6226b023ff926a43ff0a50a82028e1fc20e9faa380136f5dde00a70d7170a8de3246e39b7787771e41271351dcbf4f88b6d40dac77b2e3324a371f9fc08d1fad90fe3e5cd61caae5d8", + "blsPrivateKey": "22cde771d9674061cdaf1040d121aec3e6911b1facc29a66cd869c72cce1642f" + }, + "encrypted": {} + }, + { + "address": "lsk8netwcxgkpew8g5as2bkwbfraetf8neud25ktc", + "keyPath": "m/44'/134'/54'", + "publicKey": "b85e4331ffa96a18e48980200bed9ea7abca9ed16f5902633db46d7516ab72b0", + "privateKey": "5fc331b9319c13b85921a395cadbd79709f19cda4ffbb220b6f8d9f8961dcfb2b85e4331ffa96a18e48980200bed9ea7abca9ed16f5902633db46d7516ab72b0", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/54'", + "generatorKey": "fa7af9f8623b324e6c021b7a0899d980a41dd2de86c35cab530751eaa9e55a0a", + "generatorPrivateKey": "39793207a2f6c4cd2e32c90c2a951ae37dff4b1bb392710a4ec14863ed838faffa7af9f8623b324e6c021b7a0899d980a41dd2de86c35cab530751eaa9e55a0a", + "blsKeyPath": "m/12381/134/0/54", + "blsKey": "a3aa25a2385666122df82fa74096f30560c270b1ef981ff459e25cb5819d50a2edd8c315bf17a6a1af8d88c0e9325e50", + "blsProofOfPossession": "b543e0716990a65727b51489c90495289bae983d3a4439fe68826c2175b4396d37da0ff03910b369335377de097088720b77646a3fdf196e95c54f2ca6bd414327231996bc2dba0c1dcc7a77b8be10b84a4ef8947a0e4ba22aa09a6c025521e6", + "blsPrivateKey": "16748b6923af2e11d23c14082cdec97c9259ea163e8c232760a5151795310d5b" + }, + "encrypted": {} + }, + { + "address": "lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk", + "keyPath": "m/44'/134'/55'", + "publicKey": "a716cd8c8361700c75fabd0dfb213b611ee0b819c0bd97b20432e92f614d25c8", + "privateKey": "e63e8439fde83b57cb7d9809230fb722c527914200a7aec07bf083af1ac2ba30a716cd8c8361700c75fabd0dfb213b611ee0b819c0bd97b20432e92f614d25c8", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/55'", + "generatorKey": "91fdf7f2a3eb93e493f736a4f9fce0e1df082836bf6d06e739bb3b0e1690fada", + "generatorPrivateKey": "f5f7d8320408c3e1cf03f7d0428d07abd6a21c9bead4255f2d7d9c52eed08d9691fdf7f2a3eb93e493f736a4f9fce0e1df082836bf6d06e739bb3b0e1690fada", + "blsKeyPath": "m/12381/134/0/55", + "blsKey": "a84b3fc0a53fcb07c6057442cf11b37ef0a3d3216fc8e245f9cbf43c13193515f0de3ab9ef4f6b0e04ecdb4df212d96a", + "blsProofOfPossession": "b3de21449917e17d5eadb5211c192ee23e7df8becad8488c521dcfb0c67df64a81561653d92805b4bebae9e5b5bdef8717f1259eaeb55bd1e7eafad3d74efe20181b4ac84bb7582b637e605fe78f10eb03b2a4acbff49809e86d89aebc6076b9", + "blsPrivateKey": "3509a406fafebe2fc14186370e6bf54bc957246902b4405efba31a381220c11f" + }, + "encrypted": {} + }, + { + "address": "lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6", + "keyPath": "m/44'/134'/56'", + "publicKey": "3f24a6c7a72e7158f3440d269f0e6e8c634f4afb4c7fdf0fd3645411b9996784", + "privateKey": "c466fff076de166acde289385af11ce2150090bf73edaa6e6ab0981365d550a43f24a6c7a72e7158f3440d269f0e6e8c634f4afb4c7fdf0fd3645411b9996784", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/56'", + "generatorKey": "b53ef930d84d3ce5b4947c2502da06bcbc0fb2c71ee96f3b3a35340516712c71", + "generatorPrivateKey": "3803f627ec148e6c38f91bfc22525d375abda4b339e92e17839f66f298526755b53ef930d84d3ce5b4947c2502da06bcbc0fb2c71ee96f3b3a35340516712c71", + "blsKeyPath": "m/12381/134/0/56", + "blsKey": "8d4151757d14b1a30f7088f0bb1505bfd94a471872d565de563dbce32f696cb77afcc026170c343d0329ad554df564f6", + "blsProofOfPossession": "90df1472d40c6d1279bc96b0639ff0b8ae8cef80a0538ef00b9fc3bf7816a541d2eb9349fb6a6f1a07d80504bdf105ac0726e6b01ef75a863cafaf5356dbc03ea1c90387f79d3adf15c8a44614d80e42e7a964df2eca83a871cd378f39513414", + "blsPrivateKey": "6c9825590e74d865175bee6b34b7ce3bc302dcb040fa8cb7880a052c0f73d257" + }, + "encrypted": {} + }, + { + "address": "lskduxr23bn9pajg8antj6fzaxc7hqpdmomoyshae", + "keyPath": "m/44'/134'/57'", + "publicKey": "aa07b8f76eb58b4c284e1a573a2c40f89019c7f37026ee07b33bc2807ce9f4da", + "privateKey": "7ae45bfd25e3a72e634374dd8aceb2c3fe303904d1685763af7021eefdcda13eaa07b8f76eb58b4c284e1a573a2c40f89019c7f37026ee07b33bc2807ce9f4da", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/57'", + "generatorKey": "9b4db295e88468a37e49445443fdc364321d620dc57afe8a5a14f07ce0717055", + "generatorPrivateKey": "1ecca92ec11addd0bce634823b07878229fc2b592a6ccc8fb5d824aa4a787bd59b4db295e88468a37e49445443fdc364321d620dc57afe8a5a14f07ce0717055", + "blsKeyPath": "m/12381/134/0/57", + "blsKey": "b067f711431b1bee09000b1c27fe39a29a5603471a6993d47bf56ece01a17fa4b00e92da90d80689ed2635e7e0f90891", + "blsProofOfPossession": "91f3d5519f94424fd59c120c05d9f2f34d8cb39e092e2a354f5a7d48e7f2e23b6a21b39a7a131954320d5dbeb0a419f10304fb857fae695c180f9dedd18ffa73082af5a6ca0c62c273915cd337570ecd8649157c8dc8836d758fe1e51f4faa3f", + "blsPrivateKey": "39df532310be25d730586eceeaa25ba14093c96facbec12a75a90bea1564dedd" + }, + "encrypted": {} + }, + { + "address": "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "keyPath": "m/44'/134'/58'", + "publicKey": "e23148e07a0ae9f9982a3d716821b8762fa0a50cb3cc18b6a7796aeb27e8a9b1", + "privateKey": "8f7a1af93d3ddcfb23124c9970719390847c13ece831e86924ed8cb7fa4cf7afe23148e07a0ae9f9982a3d716821b8762fa0a50cb3cc18b6a7796aeb27e8a9b1", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/58'", + "generatorKey": "73de0a02eee8076cb64f8bc0591326bdd7447d85a24d501307d98aa912ebc766", + "generatorPrivateKey": "9da05ad478e3b6cdda6143d579e8d4514085306b9874249ffce5cb49bd854d9d73de0a02eee8076cb64f8bc0591326bdd7447d85a24d501307d98aa912ebc766", + "blsKeyPath": "m/12381/134/0/58", + "blsKey": "8c4167537d75e68a60e3cd208b63cfae1ffe5c13315e10a6100fcbd34ede8e38f705391c186f32f8a93df5ff3913d45f", + "blsProofOfPossession": "929e7eb36a9a379fd5cbcce326e166f897e5dfd036a5127ecaea4f5973566e24031a3aebaf131265764d642e9d435c3d0a5fb8d27b8c65e97960667b5b42f63ac34f42482afe60843eb174bd75e2eaac560bfa1935656688d013bb8087071610", + "blsPrivateKey": "5eee5d9f688bbd779526348dc125c2d325a3e861f836fb9c0f96d2661fd0b8a0" + }, + "encrypted": {} + }, + { + "address": "lsk2xxvfxaqpm42wr9reokucegh3quypqg9w9aqfo", + "keyPath": "m/44'/134'/59'", + "publicKey": "06353d9f52953ceef0138ef8b74b5cfd180adb80c88ea2e389d7f35d38b5ce61", + "privateKey": "bd3629194f166f3f80a2f3f75d144ad52da1952b6e6244382cbb2b3638546ba606353d9f52953ceef0138ef8b74b5cfd180adb80c88ea2e389d7f35d38b5ce61", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/59'", + "generatorKey": "621d52ac19aba86c4feef94c67ae62cfa3f6ac192177ae37be2e6b3205449c0a", + "generatorPrivateKey": "a0cd4e1e5a506682fe0471cc6c28ad979ff8a99872236a02d552c9b036c361ec621d52ac19aba86c4feef94c67ae62cfa3f6ac192177ae37be2e6b3205449c0a", + "blsKeyPath": "m/12381/134/0/59", + "blsKey": "81f7700c2115434acaf61e88b836be11986476751d6c02617d1087e7bb45798ac56929cb5f71c890c6159ff4d71cd1b3", + "blsProofOfPossession": "8bc04a899be3a7ac99e2ddda6567a0b01e21aaea8daf4848821e8233cbe80610a2f670922865f424e878add1de8c978e1913f95308a50693fbc88e991e6bcac3bfef8a1d03f89bb4dfd9c991cbf1c613f85203dfacc4376057f085967f2a7283", + "blsPrivateKey": "08550cb1c6fafbef49a1e66cfb10d1db62eeb66402376cef0875ea0a528e50ad" + }, + "encrypted": {} + }, + { + "address": "lskfowbrr5mdkenm2fcg2hhu76q3vhs74k692vv28", + "keyPath": "m/44'/134'/60'", + "publicKey": "797138977ed2153364f00bd497162c957506ca8fe023bc25ed8cdcfdf8392b29", + "privateKey": "c68c41607847bdacb39a919de4d1e00ab8daf35ae0b9a7b4f9a3d6a4e7486330797138977ed2153364f00bd497162c957506ca8fe023bc25ed8cdcfdf8392b29", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/60'", + "generatorKey": "5812017e0d25131165ebc256f39ccece115fb58ad5fe0766f78054f912832d6c", + "generatorPrivateKey": "1433e065e36926ca8f4e74f66997fb917efab9855d7e49a4fa085e8d0c3dc24b5812017e0d25131165ebc256f39ccece115fb58ad5fe0766f78054f912832d6c", + "blsKeyPath": "m/12381/134/0/60", + "blsKey": "b57835b4d3285a134730de7b29361998787c2b4853e7a5e15032b516335e81c0797a51d00e032585efa05c27d2345a1d", + "blsProofOfPossession": "8d9b7510b3332a22635815b809c3e1ef96427a20f15b3f41112af74a9aa1a401d83d625dc5081f51aefee7591d52afaf1451e78e4f3efe29ec171b8239af73fd87b2e8a1aaa8b701c3e5bcb0d609f098738d29e0af57ea010953297c9c9e19d9", + "blsPrivateKey": "3731e7bfbaa3ffeb747497395b0a9354bf9677bdb503941fe3ec362ff69aaca5" + }, + "encrypted": {} + }, + { + "address": "lska4qegdqzmsndn5hdn5jngy6nnt9qxjekkkd5jz", + "keyPath": "m/44'/134'/61'", + "publicKey": "a8b8d44f041f77679c1a6566459642204ea60f44a4a9fa6bb874b022b5129d4a", + "privateKey": "841d84cae4cb700430490a5ecb153fe968b15739d286573bb6c5ce8ccd183555a8b8d44f041f77679c1a6566459642204ea60f44a4a9fa6bb874b022b5129d4a", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/61'", + "generatorKey": "965e86fdfcdcd64879efe23705506faeb4dfc4244f93d47f4bf444966d2a0f3d", + "generatorPrivateKey": "0dba4efd2e90744941a1733afa5d7316d9a0f2ee57b396c094fbc6f7e105242f965e86fdfcdcd64879efe23705506faeb4dfc4244f93d47f4bf444966d2a0f3d", + "blsKeyPath": "m/12381/134/0/61", + "blsKey": "90f87fd2122689c54bcd8fb859c5b36d4b583272043deba66199ad181ca2c38cf48d453c46ec881e03d2b7e2e63e3684", + "blsProofOfPossession": "add6eb668bebf90fdd80b01cb83a31b02577b200c85845bd5260d7851c02d21aaaf6d040e6d6f27a8690c9598f92ba240cdbb6d7896d7a777c484d30ab48d71b1aee1b07083dc5d11a94416c4cf85e33ec3899b40e6222ac888104f80b8d96c5", + "blsPrivateKey": "2d7d6cbdceed7b7b2dffd74c276ebf255f5df7d5e4952134da5d34d0feeb01cd" + }, + "encrypted": {} + }, + { + "address": "lska6rtf7ndbgbx7d8puaaf3heqsqnudkdhvoabdm", + "keyPath": "m/44'/134'/62'", + "publicKey": "d6b2f2bb26d71390e2df1df211bd36fa91fa437871923d007f3aa747e3bc9dbb", + "privateKey": "66eac1338aabb25c5d66bd58763c56dd439f255e8567ecde038a5e35bf3459c3d6b2f2bb26d71390e2df1df211bd36fa91fa437871923d007f3aa747e3bc9dbb", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/62'", + "generatorKey": "f8252b40a65be6f5f6d0be446da5ab434bdc0a921fd0956b0672ea4a218d2d7a", + "generatorPrivateKey": "b7ddf78c537e6a808236f5361496cb44be3ca2cba0f2c7e0a20bb068748e8578f8252b40a65be6f5f6d0be446da5ab434bdc0a921fd0956b0672ea4a218d2d7a", + "blsKeyPath": "m/12381/134/0/62", + "blsKey": "a94d3cbfde92550eccede718499df12f33a8ec9a4b386e4ca423161d667862f45fb06397b12dc6a6cbafc14b1cfad26b", + "blsProofOfPossession": "a474ee16d276d3478e1b7005960d41c0e271652f29c3178230b7fdf395801dd62196294b7695b3ccad63887558e0f27d0b121738a42cfe9acab07e6763577ad87eccb5b1d0cd725cb4a32225e79e864c238ce3c56b6db8960ce9fda82828d5ba", + "blsPrivateKey": "0d1e5bc7255af552aa839931ec5cdf194a0296bd070c4d181ff43467f4beeaa6" + }, + "encrypted": {} + }, + { + "address": "lskrga27zfbamdcntpbxxt7sezvmubyxv9vnw2upk", + "keyPath": "m/44'/134'/63'", + "publicKey": "09b005266e78ac5cfc18a3d304403cf141842bf58c50dd754f2a20b0a18331a3", + "privateKey": "e9a9bccf06cd7dda82c50bc34b2156c4d0834749c6769d3363c0009ade5dd86109b005266e78ac5cfc18a3d304403cf141842bf58c50dd754f2a20b0a18331a3", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/63'", + "generatorKey": "d2b31ed942359b0c9cb696cae874a2dbdd6e24915dd8a5882c7c042eac1e6831", + "generatorPrivateKey": "656a2e7db1f694fc6872fd1bfe2318503bcfd3dbd841a0de9170ef5da80ebfddd2b31ed942359b0c9cb696cae874a2dbdd6e24915dd8a5882c7c042eac1e6831", + "blsKeyPath": "m/12381/134/0/63", + "blsKey": "997583cd4f633aa5aa5e616a75d9edc370d5e6eb77e2418c13648b435b0182cdb7787c7ca91ed3939b403fe59041890b", + "blsProofOfPossession": "95324d44556e3c61bd307a40c2ef7f3d988e0ea561e5ece2d2809cf078db232caea9df8b35d8411238fddfe83a6978a70ae88e29fa5b6322b73f7fc9756daf52aa6369e5e69c5b2304871bd324e8125a698e360e3d5f1ad20136370b8d9808ea", + "blsPrivateKey": "24325a46b06e684f9cfb351a4f5a5a62a419754e1a77b8ca39b6814c20655c27" + }, + "encrypted": {} + }, + { + "address": "lskw95u4yqs35jpeourx4jsgdur2br7b9nq88b4g2", + "keyPath": "m/44'/134'/64'", + "publicKey": "a6279f18be02a54af37dc4228fc731e63219a289c1cfb1607b18adf685976f9c", + "privateKey": "384a6c7cc4f39a566ba8e016508824bd5f39d25b2bfdad5c66377e521edbb92ba6279f18be02a54af37dc4228fc731e63219a289c1cfb1607b18adf685976f9c", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/64'", + "generatorKey": "e2f80871a5220be51352427077f6e93c2294d88be6b731b535d2ce9371274e7b", + "generatorPrivateKey": "eb6e2fd2214a11149332ff01b5b823c96f8e85ddb2342b7a1c03a974111791aee2f80871a5220be51352427077f6e93c2294d88be6b731b535d2ce9371274e7b", + "blsKeyPath": "m/12381/134/0/64", + "blsKey": "a58edccfbcbc35d6f9fec1535329a114cc5a2118945098c0f201345ab7de78d36a32014dbe701faf7d32b24f7a696d9e", + "blsProofOfPossession": "999cf3232240944ff9a14e6c4680fae450be8c0ed43fdbf8f92e7873b5482f88229768fdcfd86e22767ec1df3b5fa2fc0b08202ee4a343bfb19c8c8eabf74d44fa73c4517ad0a102faf4ae6fe87cd766d860408b51d31dadcc5674c92908c7ee", + "blsPrivateKey": "6f6ab0c40cc4959ffa99e9a202496527eecaf86d489943abb7b24828b1c7ea8a" + }, + "encrypted": {} + }, + { + "address": "lskk2vnyd5dq3ekexog6us6zcze9r64wk456zvj9a", + "keyPath": "m/44'/134'/65'", + "publicKey": "e550523682ba9bb8d8856cbf4870fa86402a4b21a3205dc1296de556354c9586", + "privateKey": "d30db1751e16265341b23a8f9e66dd31628916b4123cb52057180f148f18e6c0e550523682ba9bb8d8856cbf4870fa86402a4b21a3205dc1296de556354c9586", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/65'", + "generatorKey": "7ff8b45c5f6239306af0194ee41e047669e33338be3f8e6c786d90fb905c8b6a", + "generatorPrivateKey": "befa9db63277650972e0ba0427c4e5c912d7376c3e9ce8924a3397678c0c77037ff8b45c5f6239306af0194ee41e047669e33338be3f8e6c786d90fb905c8b6a", + "blsKeyPath": "m/12381/134/0/65", + "blsKey": "8739c54fb8452db4ff1857649a4144dae29f7bbd3275aaa8f0f2559095a09510e38bb0155bd01d01349e7f1392132e41", + "blsProofOfPossession": "b78a813e912849e2583d6e774740f2bef3115f1d23576d206ba15bf0c64404b48208e7b2b5becfe2386fc1ad686094251707a7bf8902a10b8ffd207394ad26b64f7a0c5bb7bfc737fd836b160bf16c4d14dcc343dbc8ff7993391795ded7e448", + "blsPrivateKey": "03fb0362a91d49d5325eb3cf24970da76d434a1585108ccf49baa283651d361c" + }, + "encrypted": {} + }, + { + "address": "lskk8yh4h2rkp3yegr5xuea62qbos6q8xd6h3wys2", + "keyPath": "m/44'/134'/66'", + "publicKey": "b265367283f1d3955366d56c9055da26fb2df23bf81022a0998dad49bebf3e42", + "privateKey": "7ad7a0c9f37312088626a5367c1d03ed941f0b476cfeaedb47613730d7295149b265367283f1d3955366d56c9055da26fb2df23bf81022a0998dad49bebf3e42", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/66'", + "generatorKey": "db1c7c22ee495ad3553394dca00c62b85e78b58e78ca68bfe5027b3346f6c854", + "generatorPrivateKey": "893644ce73b8651f23cd00c7e012ab6d7447d8c4ddd609619442ef10c9948417db1c7c22ee495ad3553394dca00c62b85e78b58e78ca68bfe5027b3346f6c854", + "blsKeyPath": "m/12381/134/0/66", + "blsKey": "95087210c7145581fd8dc397ed12ecc2eb703eaa19dd837d7c8c54cf625ba00bf88608aa89170d703c77f7dcf6707398", + "blsProofOfPossession": "b09816fd6ec0b666e1f61bde72069057a11fc78d7fe8b85873b6d909aee15d74c637076e149ff279c587efa4e6a468900e2c4a857bc55978ea292189737f95e7026514ec5e9a117f31b8339d8becf3af1bd2555df6d8f2372b54b7381ff355ed", + "blsPrivateKey": "71b1abe986e2287ad69c55edb0f9c80336c5220cb31e2ed6c728a58a925d81ac" + }, + "encrypted": {} + }, + { + "address": "lskk33a2z28ak9yy6eunbmodnynoehtyra5o4jzkn", + "keyPath": "m/44'/134'/67'", + "publicKey": "6b0d646e18db8b55ac1a6f49a05f17cdb4880cf99fed2415f3076d6022d70112", + "privateKey": "69c69d0e1906a079416cd965b32aea01de7fca2cf838336d596ecf005c4b83e26b0d646e18db8b55ac1a6f49a05f17cdb4880cf99fed2415f3076d6022d70112", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/67'", + "generatorKey": "689639f5e3808cc0efd5f8d48ca6ee6f9a7a1bd5f5776832cc9b448cff5d0aa9", + "generatorPrivateKey": "0e064b38b2c1d1f3db99a14bf07a3c48138f0e3bed3fea0d0aaa4377535985f4689639f5e3808cc0efd5f8d48ca6ee6f9a7a1bd5f5776832cc9b448cff5d0aa9", + "blsKeyPath": "m/12381/134/0/67", + "blsKey": "a1dff3e7486e27eb2bc99d4343b57e06fb8b52f8c7b6ec6d539889afcf0c221fbadcfca65f2ad7351beb8a51e67513fd", + "blsProofOfPossession": "b6447c9e317179a9160ea0c11c2ff49c11e0300332c2c0ec0bf81e936af231ffc3b6628da3e01eda821ff15e9a523f3204b32fd4fcce988c2b73b56609709dfd25ec9df9e33dee073f9d26a82d268569d117ecbf7985e012a975fa7d3ad5e4fd", + "blsPrivateKey": "4ba51a2b3505cbde5211c1a46608e6cd4eccfc9f5d53e473927d9dc34e1ae5e1" + }, + "encrypted": {} + }, + { + "address": "lskrxweey4ak83ek36go6okoxr6bxrepdv3y52k3y", + "keyPath": "m/44'/134'/68'", + "publicKey": "4a96ff97a29898a3bae678346f38d1ed6ab7ae22db602d28e8de6c7b15f91c86", + "privateKey": "c9da3d67a88c09783e5f4aa5a0f15063dc11690e83dcae2d1ab838efd6b739dd4a96ff97a29898a3bae678346f38d1ed6ab7ae22db602d28e8de6c7b15f91c86", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/68'", + "generatorKey": "21120ef22b7df438e06b3862d3f0ab99d5704b3c61c45a544c64c908da8955ad", + "generatorPrivateKey": "1576f20a78dcd0be1a7ad4d6ad85f762b255c662f976cf3ae00486ac28664a0621120ef22b7df438e06b3862d3f0ab99d5704b3c61c45a544c64c908da8955ad", + "blsKeyPath": "m/12381/134/0/68", + "blsKey": "8422c22feba709265c30a7b86a9ee9832d6b32fa4c9dc091c390e1b15e278f9009dc5d70868a56dace1ff622e9e634d7", + "blsProofOfPossession": "871ed33b68172b0ce40a3ec98d6fa9b3fd77245c2c1cb7f1071101cb459d53b05fc0168597148f976ceb1ded71999da8094fd8783cf27d1e21f9b965164573c0ca849210bd1e99f4706ca6f43636f9ea535c333a36c4267a598dc58c7c7fc108", + "blsPrivateKey": "177461dd8db1a3800214ac50efeaf2c8a1ff0c6e14fda158219c795909aef58e" + }, + "encrypted": {} + }, + { + "address": "lsku4ftwo3dvgygbnn58octduj6458h5eep2aea6e", + "keyPath": "m/44'/134'/69'", + "publicKey": "0a9f66755890c7a3b305985e5a061726ef98e0b362228a3df8d478e6c1182d58", + "privateKey": "73dd75b1544474d94bf51584c5f9604b4a44408df83930720c2e030aafc56bb30a9f66755890c7a3b305985e5a061726ef98e0b362228a3df8d478e6c1182d58", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/69'", + "generatorKey": "894289ef63ad9f51868d06e700c5dc9cac7af2e6601a99449134926cfdbb4340", + "generatorPrivateKey": "75a5ae8b87cd93c5e27d59898421a59d20e11489e036d8c813a70f39f74641b9894289ef63ad9f51868d06e700c5dc9cac7af2e6601a99449134926cfdbb4340", + "blsKeyPath": "m/12381/134/0/69", + "blsKey": "b9dc37e370cdbab50fe906b675551194e80705f5549ec07f32b95b85ec1ee1b149d156e649ebe1eac57bcc2ce9db3e56", + "blsProofOfPossession": "abefcbf20c53c10ac15054527c2ca691994f0b5cf60444aef49ba4e39312774eaa073be6b887ca5792bbfd53adc7ec3d0b0f6b34ec8a8f2fb6708d5a9d3de242f5fcccc3c3cddcfc5eb8be5aa13c333d114c091f594736e7a43d7d9212d0063d", + "blsPrivateKey": "52943b813516a5a2c72e8d7c68ee11c8d4b0e52be6ded1e18bcfaae70fc558aa" + }, + "encrypted": {} + }, + { + "address": "lskvcgy7ccuokarwqde8m8ztrur92cob6ju5quy4n", + "keyPath": "m/44'/134'/70'", + "publicKey": "26e064c253e23911282d58b71d68e507b28e4c62f50db256b1babf649a65d62e", + "privateKey": "3a5f45a46b59f9017a60bde8f4c35cd6fe98fddb15ea40b149fbc15c29aee69b26e064c253e23911282d58b71d68e507b28e4c62f50db256b1babf649a65d62e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/70'", + "generatorKey": "83cca7ee3c7145d8022b54fab14505f6f65ed9ac933e3591de4a45d4f2298adb", + "generatorPrivateKey": "2f96617872a88de29161446d351382da43989ef67375ac840f434ad14b2b0ba783cca7ee3c7145d8022b54fab14505f6f65ed9ac933e3591de4a45d4f2298adb", + "blsKeyPath": "m/12381/134/0/70", + "blsKey": "87cf21c4649e7f2d83aa0dd0435f73f157cbbaf32352997c5ebc7004ff3f8d72f880048c824cb98493a7ad09f4f561aa", + "blsProofOfPossession": "92d1948d5d8faec69c6a389548900952014f5803f0eedc480e291bfd8fe6f31231e43fd4bd47817bdbca96e5104b92d2097df4362b94a583a1a24bbdd0382a681b5603d6b3bbfca854d5beccd45c2ebec24623666032f30fb3858b236bfcbd14", + "blsPrivateKey": "70d4a30e49639fd5e56b98f5c3aab01f775cbd7749b3543813aa5f9398ab4759" + }, + "encrypted": {} + }, + { + "address": "lskmwac26bhz5s5wo7h79dpyucckxku8jw5descbg", + "keyPath": "m/44'/134'/71'", + "publicKey": "5deaa3bebf3bb6ef06028679c43874bde94079c5fe90218926feb874236f7838", + "privateKey": "c57064b98f00dbae3e434af2055c8d60b55614e22b5dde66046b84d1ef0541b25deaa3bebf3bb6ef06028679c43874bde94079c5fe90218926feb874236f7838", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/71'", + "generatorKey": "a7340ac2220b35dd5c97e6ea45c48cfdfcaccc4c59abf9b7f316df8a1bd7e8b2", + "generatorPrivateKey": "0aac0c1c562feedc175e66b41f9cf4f874525f87a64063ff8cd3aa0b5039ead5a7340ac2220b35dd5c97e6ea45c48cfdfcaccc4c59abf9b7f316df8a1bd7e8b2", + "blsKeyPath": "m/12381/134/0/71", + "blsKey": "adeefe5ec24b210986ae56ac2d1eea5b5447e38d7c9657d4948ee2d9b312a247ba40964a58c3fc14e5fd7137602e631c", + "blsProofOfPossession": "8ffe03e68c8b3ec929a4934d61091ac1c8f42446076a7ef6e8141082ebf71fd3153c35c1745619a08defb0ca8fbe583a15190f88dbd93d22d3c4eaf3fd60fa2d9cdcd8824bdd289111ca7d537563b0e2fa7ad06cad40bc2ce17277a63a3138b2", + "blsPrivateKey": "3e6edc54aa3da90b6bb09e0ef243a6c8088050cb44d575eada89d8dcd11a05fb" + }, + "encrypted": {} + }, + { + "address": "lskqg9k3joyv9ouhjfysscame66hovq42yeev7ug7", + "keyPath": "m/44'/134'/72'", + "publicKey": "ba444a11029a29eea3046cc2bc6ed4cbdda38a80894ef6d0ad71af78f8fa9161", + "privateKey": "ebbb0c49f9f82b67003a96d9a53d295b0b4d4f69f4fbfdc3b777f2aaf68b621bba444a11029a29eea3046cc2bc6ed4cbdda38a80894ef6d0ad71af78f8fa9161", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/72'", + "generatorKey": "a9568912797914f590413c3156c9cff93c9c14193b01e7bf248195bbe8c1af19", + "generatorPrivateKey": "c128a9bd6b5e8e2edecaf7a82a03e7fc5097196cd8272b962572573285d40a21a9568912797914f590413c3156c9cff93c9c14193b01e7bf248195bbe8c1af19", + "blsKeyPath": "m/12381/134/0/72", + "blsKey": "86f828da4b3c129eb54d95bef7975281b30dd811f252b5792998718355c599aeca3dbb222678ee0af84b13f5af2400b3", + "blsProofOfPossession": "8e062f48ead9234b710dbcfebbb2e502ddff68e3d5be19a8e7e89b2141c76caeeae233999009f24f7b6e65f3774ef6cd09de9d5c0bb59a60ff6cb31b276f0172e35f89061f3c2d700543de5cf4d6e613ff6ba7d41c1379d6baefd844ef4cb517", + "blsPrivateKey": "545273aa4f588f3368a39d10f36f2b76d191c93ee01c35f348cb1357ce43e09a" + }, + "encrypted": {} + }, + { + "address": "lsknax33n2ohy872rdkfp4ud7nsv8eamwt6utw5nb", + "keyPath": "m/44'/134'/73'", + "publicKey": "7c3a54ed0b6a766af4069f53299fc2979eda629553c57d973a3e4aedb76a88cc", + "privateKey": "4c28bf9d8deb396a9db0ed5d08dda0e9cd9fddc08274a8d5c2ba357ae80e92337c3a54ed0b6a766af4069f53299fc2979eda629553c57d973a3e4aedb76a88cc", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/73'", + "generatorKey": "473d332bb27f1dab55191233884f37aaf17545b1883554b1457b2dfac7c02b0a", + "generatorPrivateKey": "ee95f0d24719c537c4a7c804dd8321a812499d97de85773a4cb7a38cff78ea54473d332bb27f1dab55191233884f37aaf17545b1883554b1457b2dfac7c02b0a", + "blsKeyPath": "m/12381/134/0/73", + "blsKey": "b29e90de05487e087cb37f34213ccc49edef8936aa15001686f947dd26b2e4c71b0c094c633067c75d3d0879c0347a45", + "blsProofOfPossession": "9866cd99328ae5d1a14f899b95782b828b404c941853f4d0f0f56a113867f9f44b177af5c6eddec16b42c405967e52c90e3c2b0acf4921fd7ad27bdca498980aec0d37923e95d56555190caed7644ac158b392af052a49a8d1df626ea3a5f034", + "blsPrivateKey": "5db5e9de794a02c507674c7092e742c70db374920078d08a77b156202acbf926" + }, + "encrypted": {} + }, + { + "address": "lskhbcq7mps5hhea5736qaggyupdsmgdj8ufzdojp", + "keyPath": "m/44'/134'/74'", + "publicKey": "e2fae8f54453c97775dd80a117fdb786852b52081d4a3f2ab1c58935a678e32f", + "privateKey": "1715d190aea38e22522d2ca170513fdb724e7b2f20799877bac79265e6775b0be2fae8f54453c97775dd80a117fdb786852b52081d4a3f2ab1c58935a678e32f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/74'", + "generatorKey": "29e5cf287cb9c12b2bb77ef9dc673728132f9e3affef2d0de0d7db7905937435", + "generatorPrivateKey": "1b6fbfe2da1efefdd35891902ef7963aa4ac8c918a7e2d44a253f96c541b74e029e5cf287cb9c12b2bb77ef9dc673728132f9e3affef2d0de0d7db7905937435", + "blsKeyPath": "m/12381/134/0/74", + "blsKey": "ab0bf8a74c846dbd47c9e679ba26a9c0e5a7a5902b4f66cee7065b7487eba30262e4e5f0ee78d616d007021df3fbc945", + "blsProofOfPossession": "b159e28ea39b1119e4018ea19777497e1d3c4a58d1c2ecc22aa5b2efe60572cb32ff30bbeda9ce28b235fb55ab15aec206f094f37ff9a78a0931d55799c1c74a19bacfa8a4172ba078d7cad4f663a4708e47981044b1893c712c3707196451fb", + "blsPrivateKey": "158e26816907da1dbfb1a7d6c4d10c38c73bc4365883dac8fdcb5b58eb4f0eb7" + }, + "encrypted": {} + }, + { + "address": "lskbr5cnd8rjeaot7gtfo79fsywx4nb68b29xeqrh", + "keyPath": "m/44'/134'/75'", + "publicKey": "7a0cdc2106afb1bdb3cecd23175287bbcfc97225e1a775a687f97a342e9a62a4", + "privateKey": "f5b4d9ca72fc037e4f6bc8abcb454a6b336bd9269011432c3d7726e095d687b37a0cdc2106afb1bdb3cecd23175287bbcfc97225e1a775a687f97a342e9a62a4", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/75'", + "generatorKey": "552ea15981e9fa54f2b65c409e8d32c350435893744fb9937875b1ec0e3025eb", + "generatorPrivateKey": "888faa5eba1aae717ef317909f53fe87c95b0988ab079aac6fbd456ff1882f55552ea15981e9fa54f2b65c409e8d32c350435893744fb9937875b1ec0e3025eb", + "blsKeyPath": "m/12381/134/0/75", + "blsKey": "968afa71f5ba87783db371242b48962a93c91f17ec6fe2b52260c43b7db62462fc88de889445390024abbb1de1ff87ee", + "blsProofOfPossession": "b3a05e96a9fc1ba05cb80ba48e8f92e6d6d282408d77b16557dd0c8bff8bc963539d5a355cb1544e35269c4fc58f5c0816b4bc3e215d6441f06b9d2e6cd48ad5f08c5bfb35f359fe25ebcc382985bcefce0698bd3a89e655706e46e394c83693", + "blsPrivateKey": "5e5a64d90e0995efcae6083bf22d0cc3b40a9e9c14e9bbe8ebb8f0e534365ce6" + }, + "encrypted": {} + }, + { + "address": "lskq6j6w8bv4s4to8ty6rz88y2cwcx76o4wcdnsdq", + "keyPath": "m/44'/134'/76'", + "publicKey": "529ef3e0a77482bc7b22d3308833dc30a50e230f74dee3a62987ae4f9867ed5a", + "privateKey": "752ef6fd81a5f932022291c51e1fd6409e5765600582b2d3d563e952c88e116c529ef3e0a77482bc7b22d3308833dc30a50e230f74dee3a62987ae4f9867ed5a", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/76'", + "generatorKey": "6c99048cae450de8735dd410a5c8b0e4655afaebcc2c155503f890af51e067c2", + "generatorPrivateKey": "627f7390b4c6a2e4426e40e8fc35742f9c72fe14d537faacc992c5d4564805fe6c99048cae450de8735dd410a5c8b0e4655afaebcc2c155503f890af51e067c2", + "blsKeyPath": "m/12381/134/0/76", + "blsKey": "95274c1b15467d43a3b8a3a632a8fb7e1a2efbdf92559ef52ea6ff1b0ba1c7cc2f75ef357b2dc7f0130dc9c04aeaf4db", + "blsProofOfPossession": "a24ef42b04be7bcd65d8434b04f7118bf9566a0d3a36c732cf5b508ccdc12855754663bdb32c5d871eee8a0774a1331a14f25f3aeb6bddee7efaebd2214e19b7cca9f3d3bc7eed93b85b15f0a626117f24361d65688dfbe7267141f13d323d63", + "blsPrivateKey": "2746cbe68b23a69706e0cf73dfcf1ce9a8cd0bde00fcb07d5f611020747fd20a" + }, + "encrypted": {} + }, + { + "address": "lsk3oz8mycgs86jehbmpmb83n8z3ctxou47h7r9bs", + "keyPath": "m/44'/134'/77'", + "publicKey": "28c6e872795eec98a1475aad17e78f8f47baa1794a5226334f7a89ac0911be44", + "privateKey": "35b4345634c91e8ef15d6ce6d3a8038effde85dd1defd8ccc4075a313837c79e28c6e872795eec98a1475aad17e78f8f47baa1794a5226334f7a89ac0911be44", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/77'", + "generatorKey": "b9bbcd67194a7091a517faf37a7ec0fda068c4ac0dcbb8ddf526de97e67716a4", + "generatorPrivateKey": "bbdd4ce2c5eb36fd31682db37f725c02b29ef7847f5485c8798262145c607e4fb9bbcd67194a7091a517faf37a7ec0fda068c4ac0dcbb8ddf526de97e67716a4", + "blsKeyPath": "m/12381/134/0/77", + "blsKey": "8ffe1e957047e7dd979e8bcac9fcea9411ed3be947679ce26a36725b08da51ed2fa19e7f7c6bed701bf3e33a6f787b8a", + "blsProofOfPossession": "89177926eb5ed8d2be150884e0cc4eaf02a040a3ebb0af9df6922d8d7fc58da4777cc6591d3d43570ce6410077d087fe097cb30f28a164d22216859988f44ef88bc7f4a2134f882d044e4ee66d135a31cd063934cf6b4e820fcff3bbfc5b27c9", + "blsPrivateKey": "04431be991b3beb33410c5f95fd52dce7fefcac451c2dfac73562f9b439632fa" + }, + "encrypted": {} + }, + { + "address": "lsksu2u78jmmx7jgu3k8vxcmsv48x3746cts9xejf", + "keyPath": "m/44'/134'/78'", + "publicKey": "6643c7547befc7c019e96b6a3d1ff738cef395bedb5338318efdb5a07a16d259", + "privateKey": "f43e8314b17e5ce791cf07a9a4cdd21688495edba6a65e838e0641e9c974a5786643c7547befc7c019e96b6a3d1ff738cef395bedb5338318efdb5a07a16d259", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/78'", + "generatorKey": "37df5572ddb12b67b9aa5191ba9baf9d76a50307fbe188924766225d86958dbd", + "generatorPrivateKey": "5b65e4fdcce39eaeac5a4216ed37e62b793a5eb62eef2a1c28007c0db5826cfc37df5572ddb12b67b9aa5191ba9baf9d76a50307fbe188924766225d86958dbd", + "blsKeyPath": "m/12381/134/0/78", + "blsKey": "884b03c63f8d095165b67cb23131ca1053cbc73739549aa2ee21ca0b2b925994855dd46a81ebc3dedb309ceadd013f8e", + "blsProofOfPossession": "b4879cd844644b1a21f1676bf671854afb1536c5a330c1fef26b2669238efa373f70815e01028506b5cf6b75fe77e79e0efb6ef74e8111c7f1a189d4b0bf4c867190aa57e670b53dff5951a29eaaceda788ed674acdf33eff228278dc61c3cd2", + "blsPrivateKey": "0702deacefa1cedc12296f4fa5ceb618dd4f481a0f86adde2a7ae292a4da68e8" + }, + "encrypted": {} + }, + { + "address": "lskaw28kpqyffwzb8pcy47nangwwbyxjgnnvh9sfw", + "keyPath": "m/44'/134'/79'", + "publicKey": "b9fac5757bfb5f0fffb3825958f1cbfe0359d128df881ca191af00fd4243ef6c", + "privateKey": "fc34f5ee0bf978c4cd98583f6c789909bf63054535da80d388356722b63ac88fb9fac5757bfb5f0fffb3825958f1cbfe0359d128df881ca191af00fd4243ef6c", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/79'", + "generatorKey": "bfe46727c386585d8d59c02efbe48d4c1a919ff07b87267156ab96e10ac730b2", + "generatorPrivateKey": "eee04fe7d9fd8f4f6710ed5b98672707cbafd9f3a8d9f11f399230686fc5ce46bfe46727c386585d8d59c02efbe48d4c1a919ff07b87267156ab96e10ac730b2", + "blsKeyPath": "m/12381/134/0/79", + "blsKey": "b279e1a3a5edcd1045682e7029045b70dffbae55c49b14391b9f776750193269b4fd1d9f0807d9ee66e264e08ecd97cf", + "blsProofOfPossession": "83a5128e710b91ab91f7726223120b389c1f77735c9c1d408c466b7f0484b020f0d2d50edc36d49e410141d8a509b132059142e250f145810eefce03dfdda25aa84214d30cdfb6ca11a929337bf53dfe4c675117c06e4a67206119ed1e2b2b9a", + "blsPrivateKey": "6837f740126f55e5a1ecbba4d8281c171c73ae1f20e5efe54d6b6a5da2cca543" + }, + "encrypted": {} + }, + { + "address": "lsk7drqfofanzn9rf7g59a2jha5ses3rswmc26hpw", + "keyPath": "m/44'/134'/80'", + "publicKey": "1f96630d57c8ceb77d50e80931148d2fb8c66ab5d5c030f35e6fdd3bc3f0af78", + "privateKey": "8d9919a3df297df65b2f0b4565b405374b472e6d1933d790d1f0f81f841303c11f96630d57c8ceb77d50e80931148d2fb8c66ab5d5c030f35e6fdd3bc3f0af78", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/80'", + "generatorKey": "71d5b4b08ea0b7a0ff95f779aec53590a3bcb5a87fc770334f8c9ee57fdd79d9", + "generatorPrivateKey": "8c3f82e435cd1f5de4dccc93740243bb8b87e4cacb9833a8124f7016e35607b171d5b4b08ea0b7a0ff95f779aec53590a3bcb5a87fc770334f8c9ee57fdd79d9", + "blsKeyPath": "m/12381/134/0/80", + "blsKey": "a6d6315e85e8138de21f94d0c5c6f4c2515d493b17653156745155b25f9f121f6d13e7c36a57fa5002a9aa0a0b282394", + "blsProofOfPossession": "ac38044b8d84ed22d42da3a240b7c2dd16fbdf3b03655226b46b6eea46256a3ee33232771d67da1a4df6717476349647077f5cb29715333d8c55f5b6ba70c77af1944ac54c913445da29c99dd441e36d9def69c0e9709ce062ac70e4d15628a9", + "blsPrivateKey": "414e6ea6a1cdde39a74d5d4f4debed95fb523099ee5b50da5b12579bf62a7beb" + }, + "encrypted": {} + }, + { + "address": "lskayo6b7wmd3prq8fauwr52tj9ordadwrvuh5hn7", + "keyPath": "m/44'/134'/81'", + "publicKey": "af72e830f5beb4f4947f9b34574df647ccd1c2047a67f36b288b51c17a4b926d", + "privateKey": "5de29c553a012a687761d0716008b865985684796068682590d15257d258c779af72e830f5beb4f4947f9b34574df647ccd1c2047a67f36b288b51c17a4b926d", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/81'", + "generatorKey": "5ec5a5a2c91414f5cc5e3354b58671e624bc88a39fdc8f128593daa06545d6cf", + "generatorPrivateKey": "ed2c37ad4313b5b994299586dd207e22f061dc2dcac3fcfe209a2242aa96f1e35ec5a5a2c91414f5cc5e3354b58671e624bc88a39fdc8f128593daa06545d6cf", + "blsKeyPath": "m/12381/134/0/81", + "blsKey": "881fa9b753cb2f89d267e0615cbd1ad9664d331f21d89cef2131686b0af55112fe1ad4df7f2c085f78142e75d90d2cab", + "blsProofOfPossession": "898471d3356573d6445906d973f1876f1e38570b6dc9c875c88138b302806c071efbe327f66c6646f02c134c3b1b019d0227bc83acd0ca10f65adf1b8fad7c9cb383909a015fd1d678c6272e5317da58d45b89fc1c954641a61169bf1c1a1728", + "blsPrivateKey": "13003be69f241b8534150263ba8842d41a795e644f6ccfb074f0f40a2c2c5b55" + }, + "encrypted": {} + }, + { + "address": "lsk966m5mv2xk8hassrq5b8nz97qmy3nh348y6zf7", + "keyPath": "m/44'/134'/82'", + "publicKey": "2dffbbb67b9fbce2146f5ce4778d237e7081771c0094b4e0774782509a7dfb6e", + "privateKey": "df0b951a2aefa073080cabae402057853e9b8ebc862b6e298fc0899e0153bdef2dffbbb67b9fbce2146f5ce4778d237e7081771c0094b4e0774782509a7dfb6e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/82'", + "generatorKey": "875d9a84adcf997034d5ab6189a063d9817da3a6c8599cc46c84b70b5081b18b", + "generatorPrivateKey": "25a3d63c742c8b5fb168cf2c8af45a8778fee8f87f709279bd9d35d7cbe6c4ad875d9a84adcf997034d5ab6189a063d9817da3a6c8599cc46c84b70b5081b18b", + "blsKeyPath": "m/12381/134/0/82", + "blsKey": "b847749ece25a2ef51427de371b4efc2342fb38a2c5822b941c1dbf43c3f8dabf5dc0e1620d2bdafb597d697e30ab801", + "blsProofOfPossession": "831a557a972e0ed1a9cdab88a13fea899ce1b7e6475ee2d42a1a1faa09fe9042eaab3bd8b14f2faf4ecff84780b8db6719e8d6bc8917ada1f77182b2fb4a40b544c02486fe0394b8fcc72ac69fcdf3d6c0920469225bf0ad2e047fc68b9376a3", + "blsPrivateKey": "6a934defd6cfe5fc5936d88349dd6a89afb2e8607d1f0c78f6526f5ab363a4d4" + }, + "encrypted": {} + }, + { + "address": "lsk37kucto34knfhumezkx3qdwhmbrqfonjmck59z", + "keyPath": "m/44'/134'/83'", + "publicKey": "958708971b228881efe4180d3c2ca4037fb97a2292dc23f6d8a1ccc433779f7d", + "privateKey": "4dd2f4daa47f5ab0443fe7b781d637b409c6613c0129bf6bfb9882c09f202bed958708971b228881efe4180d3c2ca4037fb97a2292dc23f6d8a1ccc433779f7d", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/83'", + "generatorKey": "edec02268c216d131fa9ec045049e6ac1526f48da772a34b1536c88c5af223da", + "generatorPrivateKey": "3a092f3763a23f8ff72b4f9a11075d385bed74bdd2d3c16c14e742ace9d7e28bedec02268c216d131fa9ec045049e6ac1526f48da772a34b1536c88c5af223da", + "blsKeyPath": "m/12381/134/0/83", + "blsKey": "94c8d9240de83f6b09905756fae29c2c3aa9092649776ebe037f20011b3bff835944eae63b2dcf6c3861f11d457a875e", + "blsProofOfPossession": "9900c9235a0365b9a0b5dce686903737cc4aaa76e8f9e47367954b07ee3a0c0ab51351cd746966556ddcc53e69eabe0c025195d1d3a6788d69c1820bd1fecc096eea09770fe43f86f898c6182ce3057fcd52b43ce096a07b4da3f2369353988e", + "blsPrivateKey": "07324357227d9af227a9adc8365933b1a0799282e033f2ad85c39e80f4a7e18a" + }, + "encrypted": {} + }, + { + "address": "lsk5y2q2tn35xrnpdc4oag8sa3ktdacmdcahvwqot", + "keyPath": "m/44'/134'/84'", + "publicKey": "1556035a614d4560066996288ca75dbcaeb5bfbffe935da23208cf8fb1d30157", + "privateKey": "fd45b5940c96ea5873baf5f5253eb214477023c63545dc7d5b281393de9aaa8a1556035a614d4560066996288ca75dbcaeb5bfbffe935da23208cf8fb1d30157", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/84'", + "generatorKey": "4ae9069cbc0e2371b037342010c5ddbd9c6d4a8c8d0a9eae59bc6a3796866119", + "generatorPrivateKey": "16d9d5a00068bbf424aa7e9d660a0993b4a260bffb25907799175a8a9d8896ba4ae9069cbc0e2371b037342010c5ddbd9c6d4a8c8d0a9eae59bc6a3796866119", + "blsKeyPath": "m/12381/134/0/84", + "blsKey": "b8396076f1ae032b572145f01ea0a3b5418f226afb0496930cb68250ca59b16fe2fb6dadacd88132b9dcd19a07d7f773", + "blsProofOfPossession": "a096515a639c004e7aecee3e88ddbb572163b914de63b528db584b27fe6a0267eb95213ccbebea849a720f1f717871ff191a4cf52c9d0a4db57cfcf8f2453d22cd432a5fe64dcb45982abe84343608a8b22740f7f3fbdfe1000fede5f0a08db3", + "blsPrivateKey": "6e893accf873971fa56db1cb2aba3efb919b41ad88db4b8189a910f6e79689a6" + }, + "encrypted": {} + }, + { + "address": "lsk6quzyfffe2xhukyq4vjwnebmnapvsgj4we7bad", + "keyPath": "m/44'/134'/85'", + "publicKey": "a9142d10c269a0c4682f153d570ea3d880031db76be7363f03a368f461e58290", + "privateKey": "117cf51251f9966fbcfc7c421d8ed2704f2e347985aef71142bc9cefd18095bea9142d10c269a0c4682f153d570ea3d880031db76be7363f03a368f461e58290", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/85'", + "generatorKey": "b5308c34412c54e4b8358b5fca16396084004ee37c6824c1ad751cbe8e50e24f", + "generatorPrivateKey": "be0eef0d6ba7e57c9366787d3706335179db8f891164388e0a9acbc13eb8590ab5308c34412c54e4b8358b5fca16396084004ee37c6824c1ad751cbe8e50e24f", + "blsKeyPath": "m/12381/134/0/85", + "blsKey": "b422e4fa8ab196e0bcc49f956ab3b5c13dc14442864dca80118dea7329308e7f7aa7547df293c826a29ef4bbfe517778", + "blsProofOfPossession": "8ce0fe2bf47180e74f315fda7bfdb376a277f394667c88661dbefcc57100af1d0a06d36ef406f7abc0282a1cb8f5091505d759a40739b11b4a1fd0060e2066edd79ad417168a977f1a59206ddac4bbabaf70feda572bb19c17b9d9034bfe28b1", + "blsPrivateKey": "6e196953fefb89d7a1aad387fc99756391b7adfb5590da079605ac95d4caaaea" + }, + "encrypted": {} + }, + { + "address": "lskvpnf7a2eg5wpxrx9p2tnnxm8y7a7emfj8c3gst", + "keyPath": "m/44'/134'/86'", + "publicKey": "1cfae47a4f613770c5dd321052cc81b569e685d71bdb7da9d4a95d8a035ed05f", + "privateKey": "1c16a7a0fbd0b063cca49264d18bfae921e038dd1fda6600e54a6588ecb093521cfae47a4f613770c5dd321052cc81b569e685d71bdb7da9d4a95d8a035ed05f", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/86'", + "generatorKey": "1d224ad4cf64a3db52b2509c5b63365db970f34c8e09babf4af8135d9234f91f", + "generatorPrivateKey": "34f86863e752c3e15b3d4a18826d55d8300fc00b31d2cc0c12999f72d90dc1c81d224ad4cf64a3db52b2509c5b63365db970f34c8e09babf4af8135d9234f91f", + "blsKeyPath": "m/12381/134/0/86", + "blsKey": "86bc497e250f34a664a3330788292ee901aa286e10fcb280a4a151a8741bc0d154b947a4d3cd9bc5b552917211081466", + "blsProofOfPossession": "97a20b81bdcbc7a4f228bc00894d53d55fbb2c53960f0ddc0cfa0f77395a33858a9907079773ad50a220cbdb49bc1d171250df83dd70572c4691eb280ae99d4501b289676b6bb0ad0e859b525752015bf5113e49050a8c70853470f2dd7e9344", + "blsPrivateKey": "6c4e85a20db21bc06ae05a2edebe13688400611e830b77fdb62bde3b1ecb715d" + }, + "encrypted": {} + }, + { + "address": "lskkqjdxujqmjn2woqjs6txv3trzh6s5gsr882scp", + "keyPath": "m/44'/134'/87'", + "publicKey": "006ab84d7246fa450123b5a476a6ecb8622ac38a06ef87948bd5b4dce0ac5c61", + "privateKey": "dd04565d95cfb8abdfdacf4ff62f93c28861dc6d0d9f927a4f18a170d04481ad006ab84d7246fa450123b5a476a6ecb8622ac38a06ef87948bd5b4dce0ac5c61", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/87'", + "generatorKey": "c0aa7af3198f0e3a6bf35c5be38e0f181827735b1c3a635e8db05b80b3647054", + "generatorPrivateKey": "0c046bcc79d3af083cb9d7fecffd601f20be44c786a3bd29461e37d1c06b7f8fc0aa7af3198f0e3a6bf35c5be38e0f181827735b1c3a635e8db05b80b3647054", + "blsKeyPath": "m/12381/134/0/87", + "blsKey": "95acb59c54e53f09d7aac37c2db59c6df0ebb1e38120690a9035c715dc9862995472c72e9f48bfb05e920494dc17e9bb", + "blsProofOfPossession": "8798b4e143b15d10965194d0350d95c374d214d14f6a0c750a1a1699f1221388f01d00c6b708167fc7fcf355591abe370ed45c55306fdc372d26432cba8efc1f83238c1f2e669111656ba61b4bff391786713c28f7d1c6e717fbe98aec2dfda3", + "blsPrivateKey": "0251ae54a957ebe5cec7315592870cf6944434934a811eed219c1e42662f37f0" + }, + "encrypted": {} + }, + { + "address": "lskvwy3xvehhpfh2aekcaro5sk36vp5z5kns2zaqt", + "keyPath": "m/44'/134'/88'", + "publicKey": "80828e04067b8630864b6a21c6c998c6ac5ee744644125e5905a08ccc9f01bf1", + "privateKey": "310c22882aeee8d4d9c5fa47613684cf4b5c4fff2343d35904b4d4757103dda780828e04067b8630864b6a21c6c998c6ac5ee744644125e5905a08ccc9f01bf1", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/88'", + "generatorKey": "20a50d60059dff36a6f6c922f55b018d288ba1f9df5120eeb8fa8e3745a800ec", + "generatorPrivateKey": "a01f3582e3adf093686463ce0f5652a821eb9ad00216d67efef465a95df153af20a50d60059dff36a6f6c922f55b018d288ba1f9df5120eeb8fa8e3745a800ec", + "blsKeyPath": "m/12381/134/0/88", + "blsKey": "96482192c99ac4569b2d139670e566ca5ccf41f39d50b7ddcf69d790bcd556e797614ecb3dda2017e5e3ac2bab4e82d0", + "blsProofOfPossession": "865e6e88cf91b061b92f2d499936f384c9a3df52de5717661b66c4fd5150f1b171350c6abeab96fb905b6294ca7694420728022d84f4c31180f903a6ab8b5b8153fdcf65d46c8a018e65c0459e64c931b6544b6f00e673c30f2a82402fe8be3c", + "blsPrivateKey": "4f5694686955714b3a71244e647c1463545af4f93ef556c8417fdabb429e554b" + }, + "encrypted": {} + }, + { + "address": "lskym4rrvgax9ubgqz6944z9q3t6quo5ugw33j3kr", + "keyPath": "m/44'/134'/89'", + "publicKey": "c5e49e11ab7f218a99d98f47f6df27c6a8a4aa1489a8a48cc54e448700125aaa", + "privateKey": "bc7226156e4882cca468daad1c4fff4dee9efb36b7c861d315b6babbd55a8323c5e49e11ab7f218a99d98f47f6df27c6a8a4aa1489a8a48cc54e448700125aaa", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/89'", + "generatorKey": "4514d1723eed164b3792f1950d3b1c7a1067441ba207cce8d9bdd6f436a119fe", + "generatorPrivateKey": "f9e9f39940de3d64a3c93ee626df1169a8f6b5bcbb3b97ed9328ff9b02e22ff34514d1723eed164b3792f1950d3b1c7a1067441ba207cce8d9bdd6f436a119fe", + "blsKeyPath": "m/12381/134/0/89", + "blsKey": "a5963aa24ed05e95d19fd9de35ae6f523aad987ab2b9897216091e798e15f5062e9734b11fcacd6b8f312162ddc10940", + "blsProofOfPossession": "8a1ae28d6d70bfa0dbcc694c811c05ac6e697a17f41d45a32e1cb5b225bd42de7c1043f4af3c17d92641c4d017569e2302dad3e32493294831da564a07154e5098129639deb89743d1146f8e01f9f6f32f382905707051467242b646d86bad05", + "blsPrivateKey": "6b15b3a0f1484c2db866606cf0c6cd8270c3ff294118d7d34ec3d0fa3d9c3d5e" + }, + "encrypted": {} + }, + { + "address": "lskmc9nhajmkqczvaeob872h9mefnw63mcec84qzd", + "keyPath": "m/44'/134'/90'", + "publicKey": "55a02f49309f5ba1ff6c55c4b5fae4d966cc17cc30e769a42ce4bc7d5c3706c6", + "privateKey": "7766e85d16e1fda134af1e4e323365f7dcc1282a49b4b08b0ff82363cb07062655a02f49309f5ba1ff6c55c4b5fae4d966cc17cc30e769a42ce4bc7d5c3706c6", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/90'", + "generatorKey": "b67f0a9ad61ad6867b54aaaed6036001485d7a7ba13770aed786b34241f37cda", + "generatorPrivateKey": "c6b7a360f60b7e2b554a47b6d51f01e9e33ea7a9fcd2254ce23af34cf08a1f3cb67f0a9ad61ad6867b54aaaed6036001485d7a7ba13770aed786b34241f37cda", + "blsKeyPath": "m/12381/134/0/90", + "blsKey": "a029f74eaf914e3dfd828502f224fff7311a964d11eb1c335eebadc38b5c20a98f79bfc53ccf6ee3630cfa282e88489d", + "blsProofOfPossession": "b5cd13eac543928db25ebb9d69dfaacc04a0d41924f2010a6f04b2457523a5a423a9c49756dbcb969a7b2c49ddcc7c710ada766fdddaedbff02f68e2b75108f111f4078d2705f06551ef524f201d50ac32c423d04a7e6e7c6c8a64d70c013ec3", + "blsPrivateKey": "40726625c04da9fb36a758b0859ec1a77d546750e454bf45dc2c77b1cc1fbb49" + }, + "encrypted": {} + }, + { + "address": "lskf5sf93qyn28wfzqvr74eca3tywuuzq6xf32p7f", + "keyPath": "m/44'/134'/91'", + "publicKey": "674d283554e152216de9a42e979924ff9b05b3e39ed5072026fc8710b4fdd926", + "privateKey": "1a72b8481a589c55ba26d2805e16b58f234b243c2c87a0c39d757ec1238e66b8674d283554e152216de9a42e979924ff9b05b3e39ed5072026fc8710b4fdd926", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/91'", + "generatorKey": "d05b69bda8b5cd103c620a814cbab2f2a131dcfda6bd4cd568155ddb1afd423b", + "generatorPrivateKey": "58d029150eeb456c86e0c2aea034d210c4d356278b4102707e2b7e4bfadcff05d05b69bda8b5cd103c620a814cbab2f2a131dcfda6bd4cd568155ddb1afd423b", + "blsKeyPath": "m/12381/134/0/91", + "blsKey": "947456674b5616341cc932afb30e42973dd17582a81e5fe958277efc828535cd7c9c778410c52e069ed23e4cf629814a", + "blsProofOfPossession": "872ce3383378215d3be299f32196e9cb2ae1f9e06101afbb9e7709eafb37eca8548f156bbdfbb120c2d06fdbfdf5455107f2c818bfbc9b4e9f5fb4c50f79b24f5fc84f9e137b286d71c3d588a7af684d36bf701425b25ece2d9fbacbadb58f4e", + "blsPrivateKey": "7122afff2e9ebeadc8575a12f8cfd205b04c9c04eb3f90a354ae4ecc8479b54c" + }, + "encrypted": {} + }, + { + "address": "lskos7tnf5jx4e6jq4bf5z4gwo2ow5he4khn75gpo", + "keyPath": "m/44'/134'/92'", + "publicKey": "3d1a78899766f0662536e49af492f961fb3f1eb22f3172dad04b30c4302af87e", + "privateKey": "fe473f20516d7fa871fa0787ffdc42eafa848619ecffd3fc57de2c8aa6f1f13c3d1a78899766f0662536e49af492f961fb3f1eb22f3172dad04b30c4302af87e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/92'", + "generatorKey": "d5781773a9b07a569a0d87c0bf82103fd459a2185fc32f5c312a663c5bc65784", + "generatorPrivateKey": "e78ae7b42d3d6e7df38f69f3b25db40b31923b4fc088b8793ff9a8f07ef9ecf9d5781773a9b07a569a0d87c0bf82103fd459a2185fc32f5c312a663c5bc65784", + "blsKeyPath": "m/12381/134/0/92", + "blsKey": "87971b8a0520e08dc8dbb8114de7ecd44e98844c9179585806e8a1edaae1190ea85e6471767e90074d87d1dfbafc983c", + "blsProofOfPossession": "ac1fa23a608ce0be52ada7759c4631a5e3c7828a2a622c718b67c4d8996eeed61c382ec319ff2c608290c141ef741ba013f7567bf95cdfb29295dea31adb440f5d856f5688fdd553f47a06ab5692ee5fb99e5a50b329fe4406bfefb924b5665c", + "blsPrivateKey": "36d1ee8a349ef4cdc983bb55ef2fca9415f2f9ecf72df9a26e4138b534979852" + }, + "encrypted": {} + }, + { + "address": "lsk5rtz6s352qyt9vggx7uyo5b4p2ommfxz36w7ma", + "keyPath": "m/44'/134'/93'", + "publicKey": "b73d459c979435a84e70ea70bb18e14f312afe49af535ff4c9cd0f3a6d4cbd1e", + "privateKey": "dcb8988276c8aa0424bbb764125504f83b944d5422fe5b721fa8e5db29d08920b73d459c979435a84e70ea70bb18e14f312afe49af535ff4c9cd0f3a6d4cbd1e", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/93'", + "generatorKey": "d1f10929b1eab8232be9df3b792496eb56bcb5c0a8c2fd04e3be1fab26c7980e", + "generatorPrivateKey": "95c19ccad9cc85f4b8776e2ce5d12c646b6cb6bd60d2d2b89089d664f97ebbabd1f10929b1eab8232be9df3b792496eb56bcb5c0a8c2fd04e3be1fab26c7980e", + "blsKeyPath": "m/12381/134/0/93", + "blsKey": "8f96883db13e4f43e7280d8a58e7642228f46c375853a17e8cdb34fdeaf4e363a82678d2f54a8630218e097ba39d4370", + "blsProofOfPossession": "91a2efa4a407f63eb9157a4f4378bf6dfb4fc6d5d2714c2ee81f49ac90bc5dc3f1b72051a1fa1615f2e2d694cf17c27c1429e94bebc023feea2a405f7a8343dcc567636d15ac95ef84b1c673298becb766e036d9869e2113d9f4602f6e6092dd", + "blsPrivateKey": "5cffd4aceca113ca008c1d7603eabbbb0f0ba6f3595abf97b875e6687a5c9633" + }, + "encrypted": {} + }, + { + "address": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "keyPath": "m/44'/134'/94'", + "publicKey": "150cdf5f275aa57cae604f22f14ac2b9635ac52cd1a911a9c253842a880413fb", + "privateKey": "da4abca8970207329ad32eeee64d12e16e729cbbc75effbf3007c28f0da7071e150cdf5f275aa57cae604f22f14ac2b9635ac52cd1a911a9c253842a880413fb", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/94'", + "generatorKey": "3f44b319b82443eabb300eba5a2f323d72e44d9d2d5ed0b21a24051595582dd5", + "generatorPrivateKey": "51d9322ce03caa96cd576f48888c9a284b3e9e8f05a9a5a6395563997fecd6f03f44b319b82443eabb300eba5a2f323d72e44d9d2d5ed0b21a24051595582dd5", + "blsKeyPath": "m/12381/134/0/94", + "blsKey": "a6689556554e528964141d813c184ad4ec5c3564260d2709606c845f0c684b4bb5ff77054acb6eb8184a40fcd783670b", + "blsProofOfPossession": "831e87337aa9d7129b42ac2ac6d355395b07829148f3a4570293cb8ea00593cbbd1933a9393d8f5c4028f74c0d6c29511526e76d082fd2207f65e653129a29f22787cf19d4efe50ff43651e16463f868714354d6860e62dcd715858c4c53fc51", + "blsPrivateKey": "3980fcb82cccfce71cb76fb8860b4ef554b434db8f1a2a73578080223202802a" + }, + "encrypted": {} + }, + { + "address": "lskrskxmbv7s4czgxz5wtdqkay87ts2mfmu4ufcaw", + "keyPath": "m/44'/134'/95'", + "publicKey": "c215430e686f7f722aaa33a9652104ea23f3355906f77bc5a9e7940ab70b6fdc", + "privateKey": "e97d7dc3b6f3f0ea4445d1c3087af59d2e96b60646cce4bb417501430ae5ce91c215430e686f7f722aaa33a9652104ea23f3355906f77bc5a9e7940ab70b6fdc", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/95'", + "generatorKey": "07614fd5036d099a3caf004d46a083d12df2024fc03ef29cec22e58d1f78531f", + "generatorPrivateKey": "45569843c81a8513089ba0c1ef12c436a4397b7ed1e0fb045a6c0c0a7ec8027807614fd5036d099a3caf004d46a083d12df2024fc03ef29cec22e58d1f78531f", + "blsKeyPath": "m/12381/134/0/95", + "blsKey": "98c4f0e2b01f1b6ed07035fe46c17a40fe5409b1461a2b697afaf869e2f8c88b2db297b9a149208109bab2da195235c0", + "blsProofOfPossession": "8dad459d6b312d4a6767695029525e95f04e3ee083de85d0db5d818d15d32ef7aecb57f608c2c10355e3ca6dba8018e5192862d80f00fe1f71fd396d81d6a7649221c50bc8336efd12dc1cc13ee3c3898617971244af6a8da5ccd9224c9ea2f9", + "blsPrivateKey": "4601428462ce9b60ec00563894972ff082ff16691e45edbfef67dae7c300d2d3" + }, + "encrypted": {} + }, + { + "address": "lskjnr8jmvz45dj9z47jbky9sadh3us3rd8tdn7ww", + "keyPath": "m/44'/134'/96'", + "publicKey": "c8c2b511a2c7e697ccb8e8332e343e2db6ebbd88068422e1539011bbed669221", + "privateKey": "6841ae7fddd9f1895fcf65734faa7792f9138c9854c6786b0938f4419ee00316c8c2b511a2c7e697ccb8e8332e343e2db6ebbd88068422e1539011bbed669221", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/96'", + "generatorKey": "25ae368be016caae7066a6ce9f2ad8e4220d328ffb860a6d275d878f4882c70c", + "generatorPrivateKey": "ffd8857840f0d6c52693d21a194f1a419fe0b78b9fa4b90b1fab570ee16073da25ae368be016caae7066a6ce9f2ad8e4220d328ffb860a6d275d878f4882c70c", + "blsKeyPath": "m/12381/134/0/96", + "blsKey": "8ce6c9d2ed4f223635e3bd85476f0d56cdbb5e4090ae22b10a7fabd08d231193cf6d9c4f5b400eb4b310ef270811e424", + "blsProofOfPossession": "b896aabbcc1a165adaec26feb72fc580d4a6512dd09df40b4333381d2536b5ac36d22e91469a976ae446a6291792cb6a141013baaaae12faff26d06c6a6b722a28635c72d49fcd50ac910ca01d760e80892fc5757a18597cd1ce7f16dbabd195", + "blsPrivateKey": "47320a453378fdf5463d3a0b930fedc913ea61562b0f2eb5dc402fcdcbba9bef" + }, + "encrypted": {} + }, + { + "address": "lskmadcfr9p3qgx8upeac6xkmk8fjss7atw8p8s2a", + "keyPath": "m/44'/134'/97'", + "publicKey": "f9d11d99d4862ff2bfac4bc2306f238274cac119bc990d325732c82a09011678", + "privateKey": "1fd11f9dd4d51518021e84016507c9611ff81227fde8f51b022e57fdad05fe53f9d11d99d4862ff2bfac4bc2306f238274cac119bc990d325732c82a09011678", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/97'", + "generatorKey": "ebeb7f828aaa40ab6040e914b66b6f5d76964a0579bd29bf98c2641547f229f6", + "generatorPrivateKey": "0a48d7c8fd894f9625adb370496bdc77738a431ac859741a6e249500981c6affebeb7f828aaa40ab6040e914b66b6f5d76964a0579bd29bf98c2641547f229f6", + "blsKeyPath": "m/12381/134/0/97", + "blsKey": "a13d3a62d053b3a092d736f3c96c89fb982924b9cfd1e8283c4ced5a537732718e73c6c86c94ddd416eb94a753366b7f", + "blsProofOfPossession": "950583faae3492f5d15f9ad72bad982b2f513956cc1259e16e28ef2e18f7db3df1bf1cbab7350e390ac5a8785c574fe30878784e6c5d50668184c4c92bda196432034a7e092d9e62736ca543e1b7e594ccf6b81d37c17fabf73b846b67a0bc8f", + "blsPrivateKey": "390cc059245031c463d51a4904d080a495aa779bfe1fec5bea9e670a5211a832" + }, + "encrypted": {} + }, + { + "address": "lsknyuj2wnn95w8svk7jo38jwxhpnrx7cj3vo4vjc", + "keyPath": "m/44'/134'/98'", + "publicKey": "5bea76165e8cae84bfb3b2b65d00aa4fd63a00b6153654b5f88e27add708e04a", + "privateKey": "44c0c9eee20e7e8fc1a57564e32d8616868e76956b51794496cc3f8194c7ed0a5bea76165e8cae84bfb3b2b65d00aa4fd63a00b6153654b5f88e27add708e04a", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/98'", + "generatorKey": "4325779e64521ded42c0e2e873c16b753433d0e7f9a1e046e27a0fae9378d9c9", + "generatorPrivateKey": "3b1fe311327d7e65009c2cf5fc067f59abc2bae1aee6838158108e61d7bfa2ad4325779e64521ded42c0e2e873c16b753433d0e7f9a1e046e27a0fae9378d9c9", + "blsKeyPath": "m/12381/134/0/98", + "blsKey": "a0fb290e74bce8c5858dc1b615bac542d2280a477912ae06b8d4f07c6d451eae44a47cae6a7a1fb5cedea9efe2d4e5a5", + "blsProofOfPossession": "8b1a7d2b1566ce81c8ac2b8c88b6966b960462d0fa4e54554f53ab184c31c72c65fce904aff79d4235dd3e16e8eed2780e083a31a432e70a538de1b81d8a8a49d31bdd361f357d57fe4568d1b506492fc72f42d4b344ecfac2d560bbd2214621", + "blsPrivateKey": "3308c88c2a602c8d5cb7a84d9e70e08fc97a4e95ac27f18360496270173c27d8" + }, + "encrypted": {} + }, + { + "address": "lskrccyjmc8cybh9n3kgencq8u7fh796v2zfraco9", + "keyPath": "m/44'/134'/99'", + "publicKey": "0996481caf431af4f6ba452010898aa72b04f15115192b6b25a7e14feeee1a0c", + "privateKey": "2df44d979b4c374c2021b7dd16890943b1e2a76ba94297d35aa18023001072ef0996481caf431af4f6ba452010898aa72b04f15115192b6b25a7e14feeee1a0c", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/99'", + "generatorKey": "bf9ebe25faae5a874d97ad1772ad062ca52f63e48d806ef641e025a963224200", + "generatorPrivateKey": "18120516aa855a5be57ae46b20c7ac0efb66f9b2813ce6832e309302ea6920aebf9ebe25faae5a874d97ad1772ad062ca52f63e48d806ef641e025a963224200", + "blsKeyPath": "m/12381/134/0/99", + "blsKey": "8b436ed371b7af11b31347c12321d90a427e9aa8d93275a27faedcbe2dd06c5dce1e1a4a03b0ae030e5cd0106a942cd8", + "blsProofOfPossession": "b1dcf2ff65ba4096611f392fb56d104754927cba14ec3d193ebcf7d6eaab062c7ab770c512e815c7d52c37fa9b8622400df7939f4bbeb8566beebce1b13d67562f7bb6a01f988a501e4ef691b544cd05796010b614014ec3036b171c7392cd7d", + "blsPrivateKey": "39032c0f523eb58f549d1e5bdd0f1b38ea435bc0e26fb8a9458ca9908919980c" + }, + "encrypted": {} + }, + { + "address": "lsk3dzjyndh43tdc6vugbdqhfpt3k9juethuzsmdk", + "keyPath": "m/44'/134'/100'", + "publicKey": "c515bd1d0c9c09d3ce40eeca511489b8ed7c2ec1bc03bd5611f3a6b47c16469c", + "privateKey": "a8faeeba2da8b823b014d37165a1bfc26e74507641a65c742ae6e5cf96fb31d4c515bd1d0c9c09d3ce40eeca511489b8ed7c2ec1bc03bd5611f3a6b47c16469c", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/100'", + "generatorKey": "9f1c361befb0ae35de28e8f0e25efe75ede78aa26c703625cc17e7fe2e7208f3", + "generatorPrivateKey": "eb79f34b330f6efe29593cba5a5a8a369cfd1bd0887689020387c536e44da5249f1c361befb0ae35de28e8f0e25efe75ede78aa26c703625cc17e7fe2e7208f3", + "blsKeyPath": "m/12381/134/0/100", + "blsKey": "a1782a5f280f9894cea555d6f355c1f23e0581140c64f20ae469edd6ace7dcb6266227feecf002c2b508766e730c6f4f", + "blsProofOfPossession": "84e053bb01b22997e46ce4cbece0f5478e27cd49786cc36b1459c8930ea408e663bc725184197eb726fadf6988503c9b01be391ca3eb16587137cf5a3941717837baec7869896bae401bb513359485142778a52638429328f06a4469b7e21bb0", + "blsPrivateKey": "306651c1b7494c98b3d190fbf54b2247b9a456cb21eaadf3a0a668d740f6bdba" + }, + "encrypted": {} + }, + { + "address": "lskyunb64dg4x72ue8mzte7cbev8j4nucf9je2sh9", + "keyPath": "m/44'/134'/101'", + "publicKey": "e1383015621226361ac69c33c6b4e6148a30b08736ae0e043055b1ee9c2ad163", + "privateKey": "540473e6d615a2ebf88f99ad6387fa80b90b8847cb77fcfe09e4fb1e8a2bd6b0e1383015621226361ac69c33c6b4e6148a30b08736ae0e043055b1ee9c2ad163", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/101'", + "generatorKey": "a9b0c063fee99a903a55da57e3d16f069145e414b62e25dbbf218bd608a61f7c", + "generatorPrivateKey": "c545eee8e84f1ce916cefa07dd86818165e7187f9b33cd487060ab6944951847a9b0c063fee99a903a55da57e3d16f069145e414b62e25dbbf218bd608a61f7c", + "blsKeyPath": "m/12381/134/0/101", + "blsKey": "870db2da31a9471077677bd9a7529ee7523bdd64fdba46c514e94aa52e940566479cfdab29b07c1573aff6ba7040c684", + "blsProofOfPossession": "acbe270292cfaa154f256a83c9bdde889a9205c85c5ff0f41dae586dccc7f29f0464fbc087a5c5adb3cb4eca3b95bc14187db64cccd24e98d3e75215b69bd2bd0b357834c1ccacbdf91556fa59a86d04d1fc8aaa3be2ae5256aea3bd36d26942", + "blsPrivateKey": "4f2fdd4bb6fd739b02dea4a44ad1c4d8fa126c1ed1ebefc6f0016abd8e2c1a9c" + }, + "encrypted": {} + }, + { + "address": "lskoys3dpcyx5hkr7u2fenpjrbyd69tuyu5ar4dgy", + "keyPath": "m/44'/134'/102'", + "publicKey": "f3388194bea3a10bfb3b0b89d47417450ce078b147b7d68c7feee57f0e5d8492", + "privateKey": "3b2dfd3635ebd2c1b8b139193322422ee8ffdeba6a5ec385bb3f8fc4913a19cef3388194bea3a10bfb3b0b89d47417450ce078b147b7d68c7feee57f0e5d8492", + "plain": { + "generatorKeyPath": "m/25519'/134'/0'/102'", + "generatorKey": "3efa1c0a728a9741555b84ff1d80aedfcaf85370e1602890d7ba610bf33500bb", + "generatorPrivateKey": "f06fc00decaf4f11f2f714788f28ed0a25228a08dc002e49e16945d3e9aa2fc63efa1c0a728a9741555b84ff1d80aedfcaf85370e1602890d7ba610bf33500bb", + "blsKeyPath": "m/12381/134/0/102", + "blsKey": "a4f78f9b10c5671cca5aa2526708b95bdec56f3e404fc6c6403de83338940dfcc8d6836ba3d98566d314d34438a042d3", + "blsProofOfPossession": "91a1d0b501b7ab2caa5d240eae92c8c0ccbf296ebd3dd9d03aac1ca569f803091ec5ab57b7f6c34ad1aeb9aee0ccc17a1911c8e7a9ca681a6b803bf27e303f59dcfa32f678c4bb35189a8b7e0a3af43771ec841bd2ab32a96cb2eab0a1c2ad94", + "blsPrivateKey": "074ab003ca5c16efdcab7e925a317e657d9fdfbdb6e97bb856f1389df5599264" + }, + "encrypted": {} + } + ] +} diff --git a/examples/poa-sidechain/config/default/genesis_assets.json b/examples/poa-sidechain/config/default/genesis_assets.json new file mode 100644 index 00000000000..9435695a597 --- /dev/null +++ b/examples/poa-sidechain/config/default/genesis_assets.json @@ -0,0 +1,1876 @@ +{ + "assets": [ + { + "module": "interoperability", + "data": { + "ownChainName": "", + "ownChainNonce": 0, + "chainInfos": [], + "terminatedStateAccounts": [], + "terminatedOutboxAccounts": [] + }, + "schema": { + "$id": "/interoperability/module/genesis", + "type": "object", + "required": [ + "ownChainName", + "ownChainNonce", + "chainInfos", + "terminatedStateAccounts", + "terminatedOutboxAccounts" + ], + "properties": { + "ownChainName": { + "dataType": "string", + "maxLength": 32, + "fieldNumber": 1 + }, + "ownChainNonce": { + "dataType": "uint64", + "fieldNumber": 2 + }, + "chainInfos": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["chainID", "chainData", "channelData", "chainValidators"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "chainData": { + "$id": "/modules/interoperability/chainData", + "type": "object", + "required": ["name", "lastCertificate", "status"], + "properties": { + "name": { + "dataType": "string", + "minLength": 1, + "maxLength": 32, + "fieldNumber": 1 + }, + "lastCertificate": { + "type": "object", + "fieldNumber": 2, + "required": ["height", "timestamp", "stateRoot", "validatorsHash"], + "properties": { + "height": { + "dataType": "uint32", + "fieldNumber": 1 + }, + "timestamp": { + "dataType": "uint32", + "fieldNumber": 2 + }, + "stateRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 3 + }, + "validatorsHash": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 4 + } + } + }, + "status": { + "dataType": "uint32", + "fieldNumber": 3 + } + }, + "fieldNumber": 2 + }, + "channelData": { + "$id": "/modules/interoperability/channel", + "type": "object", + "required": [ + "inbox", + "outbox", + "partnerChainOutboxRoot", + "messageFeeTokenID", + "minReturnFeePerByte" + ], + "properties": { + "inbox": { + "type": "object", + "fieldNumber": 1, + "required": ["appendPath", "size", "root"], + "properties": { + "appendPath": { + "type": "array", + "items": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32 + }, + "fieldNumber": 1 + }, + "size": { + "fieldNumber": 2, + "dataType": "uint32" + }, + "root": { + "fieldNumber": 3, + "dataType": "bytes", + "minLength": 32, + "maxLength": 32 + } + } + }, + "outbox": { + "type": "object", + "fieldNumber": 2, + "required": ["appendPath", "size", "root"], + "properties": { + "appendPath": { + "type": "array", + "items": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32 + }, + "fieldNumber": 1 + }, + "size": { + "fieldNumber": 2, + "dataType": "uint32" + }, + "root": { + "fieldNumber": 3, + "dataType": "bytes", + "minLength": 32, + "maxLength": 32 + } + } + }, + "partnerChainOutboxRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 3 + }, + "messageFeeTokenID": { + "dataType": "bytes", + "minLength": 8, + "maxLength": 8, + "fieldNumber": 4 + }, + "minReturnFeePerByte": { + "dataType": "uint64", + "fieldNumber": 5 + } + }, + "fieldNumber": 3 + }, + "chainValidators": { + "$id": "/modules/interoperability/chainValidators", + "type": "object", + "required": ["activeValidators", "certificateThreshold"], + "properties": { + "activeValidators": { + "type": "array", + "fieldNumber": 1, + "minItems": 1, + "maxItems": 199, + "items": { + "type": "object", + "required": ["blsKey", "bftWeight"], + "properties": { + "blsKey": { + "dataType": "bytes", + "minLength": 48, + "maxLength": 48, + "fieldNumber": 1 + }, + "bftWeight": { + "dataType": "uint64", + "fieldNumber": 2 + } + } + } + }, + "certificateThreshold": { + "dataType": "uint64", + "fieldNumber": 2 + } + }, + "fieldNumber": 4 + } + } + } + }, + "terminatedStateAccounts": { + "type": "array", + "fieldNumber": 4, + "items": { + "type": "object", + "required": ["chainID", "terminatedStateAccount"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "terminatedStateAccount": { + "$id": "/modules/interoperability/terminatedState", + "type": "object", + "required": ["stateRoot", "mainchainStateRoot", "initialized"], + "properties": { + "stateRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 1 + }, + "mainchainStateRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 2 + }, + "initialized": { + "dataType": "boolean", + "fieldNumber": 3 + } + }, + "fieldNumber": 2 + } + } + } + }, + "terminatedOutboxAccounts": { + "type": "array", + "fieldNumber": 5, + "items": { + "type": "object", + "required": ["chainID", "terminatedOutboxAccount"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "terminatedOutboxAccount": { + "$id": "/modules/interoperability/terminatedOutbox", + "type": "object", + "required": ["outboxRoot", "outboxSize", "partnerChainInboxSize"], + "properties": { + "outboxRoot": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 1 + }, + "outboxSize": { + "dataType": "uint32", + "fieldNumber": 2 + }, + "partnerChainInboxSize": { + "dataType": "uint32", + "fieldNumber": 3 + } + }, + "fieldNumber": 2 + } + } + } + } + } + } + }, + { + "module": "token", + "data": { + "userSubstore": [ + { + "address": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskvcgy7ccuokarwqde8m8ztrur92cob6ju5quy4n", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskvpnf7a2eg5wpxrx9p2tnnxm8y7a7emfj8c3gst", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskvq67zzev53sa6ozt39ft3dsmwxxztb7h29275k", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskvwy3xvehhpfh2aekcaro5sk36vp5z5kns2zaqt", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskcuj9g99y36fc6em2f6zfrd83c6djsvcyzx9u3p", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskc22mfaqzo722aenb6yw7awx8f22nrn54skrj8b", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskchcsq6pgnq6nwttwe9hyj67rb9936cf2ccjk3b", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskp2kubbnvgwhw588t3wp85wthe285r7e2m64w2d", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskmc9nhajmkqczvaeob872h9mefnw63mcec84qzd", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskm8g9dshwfcmfq9ctbrjm9zvb58h5c7y9ecstky", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskmwac26bhz5s5wo7h79dpyucckxku8jw5descbg", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskmadcfr9p3qgx8upeac6xkmk8fjss7atw8p8s2a", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskbm49qcdcyqvavxkm69x22btvhwx6v27kfzghu3", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskbr5cnd8rjeaot7gtfo79fsywx4nb68b29xeqrh", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsknyuj2wnn95w8svk7jo38jwxhpnrx7cj3vo4vjc", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsknax33n2ohy872rdkfp4ud7nsv8eamwt6utw5nb", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsknatyy4944pxukrhe38bww4bn3myzjp2af4sqgh", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsknddzdw4xxej5znssc7aapej67s7g476osk7prc", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk3oz8mycgs86jehbmpmb83n8z3ctxou47h7r9bs", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk37kucto34knfhumezkx3qdwhmbrqfonjmck59z", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk3dzjyndh43tdc6vugbdqhfpt3k9juethuzsmdk", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk4nst5n99meqxndr684va7hhenw7q8sxs5depnb", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk67y3t2sqd7kka2agtcdm68oqvmvyw94nrjqz7f", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk6quzyfffe2xhukyq4vjwnebmnapvsgj4we7bad", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk5pmheu78re567zd5dnddzh2c3jzn7bwcrjd7dy", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk56hpjtt5b8w3h2qgckr57txuw95ja29rsonweo", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk5y2q2tn35xrnpdc4oag8sa3ktdacmdcahvwqot", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk5rtz6s352qyt9vggx7uyo5b4p2ommfxz36w7ma", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskoys3dpcyx5hkr7u2fenpjrbyd69tuyu5ar4dgy", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskoq2bmkpfwmmbo3c9pzdby7wmwjvokgmpgbpcj3", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskowvmbgn4oye4hae3keyjuzta4t499zqkjqydfd", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskos7tnf5jx4e6jq4bf5z4gwo2ow5he4khn75gpo", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk966m5mv2xk8hassrq5b8nz97qmy3nh348y6zf7", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk7drqfofanzn9rf7g59a2jha5ses3rswmc26hpw", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8vjsq5s8jan9c8y9tmgawd6cttuszbf6jmhvj5", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8netwcxgkpew8g5as2bkwbfraetf8neud25ktc", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8dz47g5s7qxbyy46qvkrykfoj7wg7rb5ohy97c", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk8dsngwh4n6hmf4unqb8gfqgkayabaqdvtq85ja", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskux8ew6zq6zddya4u32towauvxmbe3x9hxvbzv4", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsku4ftwo3dvgygbnn58octduj6458h5eep2aea6e", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskuueow44w67rte7uoryn855hp5kw48szuhe5qmc", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskym4rrvgax9ubgqz6944z9q3t6quo5ugw33j3kr", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskyunb64dg4x72ue8mzte7cbev8j4nucf9je2sh9", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrzuuu8gkp5bxrbbz9hdjxw2yhnpxdkdz3j8rxr", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrxweey4ak83ek36go6okoxr6bxrepdv3y52k3y", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrccyjmc8cybh9n3kgencq8u7fh796v2zfraco9", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskr8bmeh9q5brkctg8g44j82ootju82zu8porwvq", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrskxmbv7s4czgxz5wtdqkay87ts2mfmu4ufcaw", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrgqnuqub85jzcocgjsgb5rexrxc32s9dajhm69", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskrga27zfbamdcntpbxxt7sezvmubyxv9vnw2upk", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsktn6hodzd7v4kzgpd56osqjfwnzhu4mdyokynum", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsktas5pgp3tofv4ke4f2kayw9uyrqpnbf55bw5hm", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskk33a2z28ak9yy6eunbmodnynoehtyra5o4jzkn", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskk8yh4h2rkp3yegr5xuea62qbos6q8xd6h3wys2", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskkqjdxujqmjn2woqjs6txv3trzh6s5gsr882scp", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskk2vnyd5dq3ekexog6us6zcze9r64wk456zvj9a", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskqxjqneh4mhkvvgga8wxtrky5ztzt6bh8rcvsvg", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskq6j6w8bv4s4to8ty6rz88y2cwcx76o4wcdnsdq", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskq5attbvu8s55ngwr3c5cv8392mqayvy4yyhpuy", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskqw45qy3ph9rwgow86rudqa7e3vmb93db5e4yad", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskqg9k3joyv9ouhjfysscame66hovq42yeev7ug7", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskezdab747v9z78hgmcxsokeetcmbdrpj3gzrdcw", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lske5sqed53fdcs4m9et28f2k7u9fk6hno9bauday", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskee8xh9oc78uhw5dhnaca9mbgmcgbwbnbarvd5d", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskewnnr5x7h3ckkmys8d4orvuyyqmf8odmud6qmg", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskwv3bh76epo42wvj6sdq8t7dbwar7xmm7h4k92m", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskw95u4yqs35jpeourx4jsgdur2br7b9nq88b4g2", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskwdkhf2ew9ov65v7srpq2mdq48rmrgp492z3pkn", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskwdqjhdgvqde9yrro4pfu464cumns3t5gyzutbm", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsk2xxvfxaqpm42wr9reokucegh3quypqg9w9aqfo", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lska4qegdqzmsndn5hdn5jngy6nnt9qxjekkkd5jz", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lska6rtf7ndbgbx7d8puaaf3heqsqnudkdhvoabdm", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskau7uqo6afteazgyknmtotxdjgwr3p9gfr4yzke", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskayo6b7wmd3prq8fauwr52tj9ordadwrvuh5hn7", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskatntynnut2eee2zxrpdzokrjmok43xczp2fme7", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskaw28kpqyffwzb8pcy47nangwwbyxjgnnvh9sfw", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskdo2dmatrfwcnzoeohorwqbef4qngvojfdtkqpj", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskduxr23bn9pajg8antj6fzaxc7hqpdmomoyshae", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsksmpgg7mo4m6ekc9tgvgjr8kh5h6wmgtqvq6776", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsksu2u78jmmx7jgu3k8vxcmsv48x3746cts9xejf", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsksy7x68enrmjxjb8copn5m8csys6rjejx56pjqt", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lsksdfqvkbqpc8eczj2s3dzkxnap5pguaxdw2227r", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskjnr8jmvz45dj9z47jbky9sadh3us3rd8tdn7ww", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskjtc95w5wqh5gtymqh7dqadb6kbc9x2mwr4eq8d", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskjtbchucvrd2s8qjo83e7trpem5edwa6dbjfczq", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskhbcq7mps5hhea5736qaggyupdsmgdj8ufzdojp", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskhamuapyyfckyg5v8u5o4jjw9bvr5bog7rgx8an", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskfx88g3826a4qsyxm4w3fheyymfnucpsq36d326", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskfmufdszf9ssqghf2yjkjeetyxy4v9wgawfv725", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskf6f3zj4o9fnpt7wd4fowafv8buyd72sgt2864b", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskf5sf93qyn28wfzqvr74eca3tywuuzq6xf32p7f", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskfowbrr5mdkenm2fcg2hhu76q3vhs74k692vv28", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskf7a93qr84d9a6ga543wernvxbsrpvtp299c5mj", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskfjd3ymhyzedgneudo2bujnm25u7stu4qpa3jnd", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskffxs3orv2au2juwa69hqtrmpcg9vq78cqbdjr4", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + }, + { + "address": "lskgn7m77b769frqvgq7uko74wcrroqtcjv7nhv95", + "tokenID": "0400000000000000", + "availableBalance": "100000000000000", + "lockedBalances": [] + } + ], + "supplySubstore": [ + { + "tokenID": "0400000000000000", + "totalSupply": "10300000000000000" + } + ], + "escrowSubstore": [], + "supportedTokensSubstore": [] + }, + "schema": { + "$id": "/token/module/genesis", + "type": "object", + "required": ["userSubstore", "supplySubstore", "escrowSubstore", "supportedTokensSubstore"], + "properties": { + "userSubstore": { + "type": "array", + "fieldNumber": 1, + "items": { + "type": "object", + "required": ["address", "tokenID", "availableBalance", "lockedBalances"], + "properties": { + "address": { + "dataType": "bytes", + "format": "lisk32", + "fieldNumber": 1 + }, + "tokenID": { + "dataType": "bytes", + "fieldNumber": 2, + "minLength": 8, + "maxLength": 8 + }, + "availableBalance": { + "dataType": "uint64", + "fieldNumber": 3 + }, + "lockedBalances": { + "type": "array", + "fieldNumber": 4, + "items": { + "type": "object", + "required": ["module", "amount"], + "properties": { + "module": { + "dataType": "string", + "fieldNumber": 1 + }, + "amount": { + "dataType": "uint64", + "fieldNumber": 2 + } + } + } + } + } + } + }, + "supplySubstore": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["tokenID", "totalSupply"], + "properties": { + "tokenID": { + "dataType": "bytes", + "fieldNumber": 1, + "minLength": 8, + "maxLength": 8 + }, + "totalSupply": { + "dataType": "uint64", + "fieldNumber": 2 + } + } + } + }, + "escrowSubstore": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["escrowChainID", "tokenID", "amount"], + "properties": { + "escrowChainID": { + "dataType": "bytes", + "fieldNumber": 1, + "minLength": 4, + "maxLength": 4 + }, + "tokenID": { + "dataType": "bytes", + "fieldNumber": 2, + "minLength": 8, + "maxLength": 8 + }, + "amount": { + "dataType": "uint64", + "fieldNumber": 3 + } + } + } + }, + "supportedTokensSubstore": { + "type": "array", + "fieldNumber": 4, + "items": { + "type": "object", + "required": ["chainID", "supportedTokenIDs"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "supportedTokenIDs": { + "type": "array", + "fieldNumber": 2, + "items": { + "dataType": "bytes", + "minLength": 8, + "maxLength": 8 + } + } + } + } + } + } + } + }, + { + "module": "poa", + "data": { + "validators": [ + { + "address": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "name": "genesis_0", + "blsKey": "a6689556554e528964141d813c184ad4ec5c3564260d2709606c845f0c684b4bb5ff77054acb6eb8184a40fcd783670b", + "proofOfPossession": "831e87337aa9d7129b42ac2ac6d355395b07829148f3a4570293cb8ea00593cbbd1933a9393d8f5c4028f74c0d6c29511526e76d082fd2207f65e653129a29f22787cf19d4efe50ff43651e16463f868714354d6860e62dcd715858c4c53fc51", + "generatorKey": "3f44b319b82443eabb300eba5a2f323d72e44d9d2d5ed0b21a24051595582dd5" + }, + { + "address": "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "name": "genesis_1", + "blsKey": "8c4167537d75e68a60e3cd208b63cfae1ffe5c13315e10a6100fcbd34ede8e38f705391c186f32f8a93df5ff3913d45f", + "proofOfPossession": "929e7eb36a9a379fd5cbcce326e166f897e5dfd036a5127ecaea4f5973566e24031a3aebaf131265764d642e9d435c3d0a5fb8d27b8c65e97960667b5b42f63ac34f42482afe60843eb174bd75e2eaac560bfa1935656688d013bb8087071610", + "generatorKey": "73de0a02eee8076cb64f8bc0591326bdd7447d85a24d501307d98aa912ebc766" + }, + { + "address": "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "name": "genesis_2", + "blsKey": "b61f2da61bf5837450dcbc3bca0d6cc4fe2ba97f0325e5ee63f879e28aa9ea4dd9979f583e30236fb519a84a9cb27975", + "proofOfPossession": "807bca29a9eea5717c1802aebff8c29ad3f198a369081999512d31c887d8beba1a591d80a87b1122a5d9501b737188f805f3ef9a77acd051576805981cd0c5ba6e9761b5065f4d48f0e579982b45a1e35b3c282d27bb6e04262005835107a16b", + "generatorKey": "761b647f4cb146f168e41658d1dfe0e9c01e5d64b15e5c033d230210f7e0aaa8" + }, + { + "address": "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "name": "genesis_3", + "blsKey": "b19c4385aaac82c4010cc8231233593dd479f90365186b0344c25c4e11c6c921f0c5b946028330ead690347216f65549", + "proofOfPossession": "b61a22f607f3652226a78747f3bb52c6d680e06a8041fc1d3a94a78fabf2895f23559059a44b0c64cd759d33e60a06060197246f6886679add69f6d306506336e15cdc7e9bde0aaca6e8191fb3535b5685ce8b3f33212441d311444a3d57fc66", + "generatorKey": "f07a86182356aee3fcfb37dcedbb6712c98319dc24b7be17cb322880d755b299" + }, + { + "address": "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "name": "genesis_4", + "blsKey": "a5ca55e9a0ab81d48eaad2960bd3ea259527cf85fe62cc80cfd8400dbd2511725c06c3a597868dcc257bbc279e2b3e92", + "proofOfPossession": "a092cff10ea18ec3dcf3f6e41cd38537e00602e35107067ace7ab7c97a2ae1de531ebea7fc0c22e8dbcee1f981c439930c7cae474a996b153a66b0cb34e66c6041348aaeb4763413afffe0d947da90424065ee573b3683edbb1e51f9a278ae82", + "generatorKey": "0cc6c469088fb2163262ac41787ea4a81da50d92fd510299ba66e5a2b02d5a05" + }, + { + "address": "lskvcgy7ccuokarwqde8m8ztrur92cob6ju5quy4n", + "name": "genesis_5", + "blsKey": "87cf21c4649e7f2d83aa0dd0435f73f157cbbaf32352997c5ebc7004ff3f8d72f880048c824cb98493a7ad09f4f561aa", + "proofOfPossession": "92d1948d5d8faec69c6a389548900952014f5803f0eedc480e291bfd8fe6f31231e43fd4bd47817bdbca96e5104b92d2097df4362b94a583a1a24bbdd0382a681b5603d6b3bbfca854d5beccd45c2ebec24623666032f30fb3858b236bfcbd14", + "generatorKey": "83cca7ee3c7145d8022b54fab14505f6f65ed9ac933e3591de4a45d4f2298adb" + }, + { + "address": "lskvpnf7a2eg5wpxrx9p2tnnxm8y7a7emfj8c3gst", + "name": "genesis_6", + "blsKey": "86bc497e250f34a664a3330788292ee901aa286e10fcb280a4a151a8741bc0d154b947a4d3cd9bc5b552917211081466", + "proofOfPossession": "97a20b81bdcbc7a4f228bc00894d53d55fbb2c53960f0ddc0cfa0f77395a33858a9907079773ad50a220cbdb49bc1d171250df83dd70572c4691eb280ae99d4501b289676b6bb0ad0e859b525752015bf5113e49050a8c70853470f2dd7e9344", + "generatorKey": "1d224ad4cf64a3db52b2509c5b63365db970f34c8e09babf4af8135d9234f91f" + }, + { + "address": "lskvq67zzev53sa6ozt39ft3dsmwxxztb7h29275k", + "name": "genesis_7", + "blsKey": "9006fc2c9d159b6890047e9b26c700d8c504e17b6fe476a2a1ac1477357c68eee332be587da425e37e22332348ed8007", + "proofOfPossession": "945ac6db93666aa21934d84c6ad897fe1acf1d208a17ec46b0ddf26cf6d9cdccef7db9eac682195ec47cb8e7a069bbe10706a4e1cce2012aadd311dafb270c9c810d80bc82c2b6c34ce236efac552fa0904b96533772f98e202f4e6f47c97f09", + "generatorKey": "8b65dce85de8ed215a91477627b365ec017a01cd5a715337f772ba42715cc794" + }, + { + "address": "lskvwy3xvehhpfh2aekcaro5sk36vp5z5kns2zaqt", + "name": "genesis_8", + "blsKey": "96482192c99ac4569b2d139670e566ca5ccf41f39d50b7ddcf69d790bcd556e797614ecb3dda2017e5e3ac2bab4e82d0", + "proofOfPossession": "865e6e88cf91b061b92f2d499936f384c9a3df52de5717661b66c4fd5150f1b171350c6abeab96fb905b6294ca7694420728022d84f4c31180f903a6ab8b5b8153fdcf65d46c8a018e65c0459e64c931b6544b6f00e673c30f2a82402fe8be3c", + "generatorKey": "20a50d60059dff36a6f6c922f55b018d288ba1f9df5120eeb8fa8e3745a800ec" + }, + { + "address": "lskcuj9g99y36fc6em2f6zfrd83c6djsvcyzx9u3p", + "name": "genesis_9", + "blsKey": "b244cdcbc419d0efd741cd7117153f9ba1a5a914e1fa686e0f601a2d3f0a79ac765c45fb3a09a297e7bc0515562ceda5", + "proofOfPossession": "b7a186c0576deeacb7eb8db7fe2dcdb9652ea963d2ffe0a14ad90d7698f214948611a3866dfedcb6a8da3209fee4b94a025864f94c31e09192b6de2a71421e5b08d5ac906e77471d3643374a3d84f99d8b1315f44066c044b5cdbfdfeceef78c", + "generatorKey": "80fb43e2c967cb9d050c0460d8a538f15f0ed3b16cb38e0414633f182d67a275" + }, + { + "address": "lskc22mfaqzo722aenb6yw7awx8f22nrn54skrj8b", + "name": "genesis_10", + "blsKey": "a38d728c1c1023651b031835818d17d0665d1fbabd8e62da26ca53f290620c23fe928244bcbcbb67412344013017cb53", + "proofOfPossession": "b5d455bb358eff87779b296f23a2fc9abc9d8f3ecb8ed0d9af3e23066e653a58b189c11b4a3980eaeaaa85ffcc240795187f6e8a0e8e8a2837bc20d485e1d3159c2d581614d72f94bbd049e5a9f45c0302851c87aa3c3853d8962ed75d140234", + "generatorKey": "671c72129793eb5801273ff580ce3d4c78d89fc8b4fb95b090a9af0a9a647a41" + }, + { + "address": "lskchcsq6pgnq6nwttwe9hyj67rb9936cf2ccjk3b", + "name": "genesis_11", + "blsKey": "8fd004c33814c3b452d50b2bf6855eeb03e41552c6edd50b76dee57007a34cf987da1e06425cf498391e6831d1bf6851", + "proofOfPossession": "a0e34bdc7dc39e09f686d6712fd0e71c61c8d06dfedbdbb9ed77c821c22d6c87f87e39e48db79aa50c19904933abb11a0b07659317079ae8f2db6e27b9139ce0830faa8dad2dcae2079f64781b0516be825b2d84689080bb8219a5ec72ba80f7", + "generatorKey": "be4e49ea7e57ede752ce33cb224f50277552f9085a551005255ee12a9b4ca68d" + }, + { + "address": "lskp2kubbnvgwhw588t3wp85wthe285r7e2m64w2d", + "name": "genesis_12", + "blsKey": "98f83f66e857d954d5c5a49403e5b3a622e1bb855d785845e72faf0f7dd03ed3fd2f787a38c57f6968accaf780fd41fe", + "proofOfPossession": "b3131f0229df11964daba47a79729542f10672b36db017002df90d2cc6a79c8b44d032935bd214bdf69a8db181e4315a15de71a2e6802442536143c3ace9886248d502d6f38f9ea5bad26d4cee729b909d6cbde541c35313598957ddda08de15", + "generatorKey": "56d64ef16324f92efce8b0a6ee98b2925dc485d45675b2012bbf6a96d7431a36" + }, + { + "address": "lskmc9nhajmkqczvaeob872h9mefnw63mcec84qzd", + "name": "genesis_13", + "blsKey": "a029f74eaf914e3dfd828502f224fff7311a964d11eb1c335eebadc38b5c20a98f79bfc53ccf6ee3630cfa282e88489d", + "proofOfPossession": "b5cd13eac543928db25ebb9d69dfaacc04a0d41924f2010a6f04b2457523a5a423a9c49756dbcb969a7b2c49ddcc7c710ada766fdddaedbff02f68e2b75108f111f4078d2705f06551ef524f201d50ac32c423d04a7e6e7c6c8a64d70c013ec3", + "generatorKey": "b67f0a9ad61ad6867b54aaaed6036001485d7a7ba13770aed786b34241f37cda" + }, + { + "address": "lskm8g9dshwfcmfq9ctbrjm9zvb58h5c7y9ecstky", + "name": "genesis_14", + "blsKey": "8e3f9dd02f46bbb01ec1ffbe173b6a28baa3ffaca943afe51c18dc5220256a3994cd0b0389c835988a64076b4e81c837", + "proofOfPossession": "980f00e7752adccb907eaea0fc31ce62dcaff9bf1c6b7066c5071829c91456a8d1e266cb0a9ef4916ffbd09295508a350d21e9123e5cc1c00d3ef65f5493c93c5b993e9768960d4210849743dc2b995657cb0aee7d46d6482e3545b89f06f895", + "generatorKey": "497a5b80edc6b9b5cca4ca73fd0523dbd51e41c1af5f893e301cfa91d997573a" + }, + { + "address": "lskmwac26bhz5s5wo7h79dpyucckxku8jw5descbg", + "name": "genesis_15", + "blsKey": "adeefe5ec24b210986ae56ac2d1eea5b5447e38d7c9657d4948ee2d9b312a247ba40964a58c3fc14e5fd7137602e631c", + "proofOfPossession": "8ffe03e68c8b3ec929a4934d61091ac1c8f42446076a7ef6e8141082ebf71fd3153c35c1745619a08defb0ca8fbe583a15190f88dbd93d22d3c4eaf3fd60fa2d9cdcd8824bdd289111ca7d537563b0e2fa7ad06cad40bc2ce17277a63a3138b2", + "generatorKey": "a7340ac2220b35dd5c97e6ea45c48cfdfcaccc4c59abf9b7f316df8a1bd7e8b2" + }, + { + "address": "lskmadcfr9p3qgx8upeac6xkmk8fjss7atw8p8s2a", + "name": "genesis_16", + "blsKey": "a13d3a62d053b3a092d736f3c96c89fb982924b9cfd1e8283c4ced5a537732718e73c6c86c94ddd416eb94a753366b7f", + "proofOfPossession": "950583faae3492f5d15f9ad72bad982b2f513956cc1259e16e28ef2e18f7db3df1bf1cbab7350e390ac5a8785c574fe30878784e6c5d50668184c4c92bda196432034a7e092d9e62736ca543e1b7e594ccf6b81d37c17fabf73b846b67a0bc8f", + "generatorKey": "ebeb7f828aaa40ab6040e914b66b6f5d76964a0579bd29bf98c2641547f229f6" + }, + { + "address": "lskbm49qcdcyqvavxkm69x22btvhwx6v27kfzghu3", + "name": "genesis_17", + "blsKey": "80d7d0598d4e79ceea22c56d16e747cd5ef94469bd036945d14a5d1e06eb700f9f1099d10cfaddddf9e88ac4c9f1086a", + "proofOfPossession": "b7890264708b9d3341d90864f9120cd84090592a6bc5a419df94e86a638a0055e7dc3846cb89869cf46305611e49cea007711f35a5effd3099e56b5108a4103215a6ba9195c4694064ba661502e852b43e9593b0a60bcd2b567fc97565054500", + "generatorKey": "4ec3ad70d3d35f0d684960e7938fab016d12c6c7cbb8312a8cff776dbaf2ca4a" + }, + { + "address": "lskbr5cnd8rjeaot7gtfo79fsywx4nb68b29xeqrh", + "name": "genesis_18", + "blsKey": "968afa71f5ba87783db371242b48962a93c91f17ec6fe2b52260c43b7db62462fc88de889445390024abbb1de1ff87ee", + "proofOfPossession": "b3a05e96a9fc1ba05cb80ba48e8f92e6d6d282408d77b16557dd0c8bff8bc963539d5a355cb1544e35269c4fc58f5c0816b4bc3e215d6441f06b9d2e6cd48ad5f08c5bfb35f359fe25ebcc382985bcefce0698bd3a89e655706e46e394c83693", + "generatorKey": "552ea15981e9fa54f2b65c409e8d32c350435893744fb9937875b1ec0e3025eb" + }, + { + "address": "lsknyuj2wnn95w8svk7jo38jwxhpnrx7cj3vo4vjc", + "name": "genesis_19", + "blsKey": "a0fb290e74bce8c5858dc1b615bac542d2280a477912ae06b8d4f07c6d451eae44a47cae6a7a1fb5cedea9efe2d4e5a5", + "proofOfPossession": "8b1a7d2b1566ce81c8ac2b8c88b6966b960462d0fa4e54554f53ab184c31c72c65fce904aff79d4235dd3e16e8eed2780e083a31a432e70a538de1b81d8a8a49d31bdd361f357d57fe4568d1b506492fc72f42d4b344ecfac2d560bbd2214621", + "generatorKey": "4325779e64521ded42c0e2e873c16b753433d0e7f9a1e046e27a0fae9378d9c9" + }, + { + "address": "lsknax33n2ohy872rdkfp4ud7nsv8eamwt6utw5nb", + "name": "genesis_20", + "blsKey": "b29e90de05487e087cb37f34213ccc49edef8936aa15001686f947dd26b2e4c71b0c094c633067c75d3d0879c0347a45", + "proofOfPossession": "9866cd99328ae5d1a14f899b95782b828b404c941853f4d0f0f56a113867f9f44b177af5c6eddec16b42c405967e52c90e3c2b0acf4921fd7ad27bdca498980aec0d37923e95d56555190caed7644ac158b392af052a49a8d1df626ea3a5f034", + "generatorKey": "473d332bb27f1dab55191233884f37aaf17545b1883554b1457b2dfac7c02b0a" + }, + { + "address": "lsknatyy4944pxukrhe38bww4bn3myzjp2af4sqgh", + "name": "genesis_21", + "blsKey": "b0d3f0d142131962d9ab7505a3ca078c1947d6bb2972174988feddc5d4d9727927ff79290af7e1180a913a375da9b618", + "proofOfPossession": "90f81a87982cb983aae8c240f12c77306501bf67dcb031161cb2787ecbecfdc0ca4e62365f750714b9b0a64c10411058105bef1a725ece1c0e7c45b7e1526494d5a02ceaa4f624116a91188e7ca2503e0ae17748b11b05cd79ccc204d20e418f", + "generatorKey": "f8d382ac4f19ffe2ac2fa91794b65dc4c03389cbb2ea65bab50379a12e0f98fb" + }, + { + "address": "lsknddzdw4xxej5znssc7aapej67s7g476osk7prc", + "name": "genesis_22", + "blsKey": "8ae81737f7b1678ece4b06db3ee1d633637da3c02cf646cdb0c7c1dae5f9eea41f2384fca8b0b12033d316ee78ea3e94", + "proofOfPossession": "a5150c19ac23dc15f660d9612be5f9591c1a5fc892e9f8b267de6bd39da84f254b6644e8c0f294900e5e9b7c9ecf3f260d902a56af7db5a59083eda08dd3ff083e2a07ba5d34f25312621f8686358dd2a50dcdc879eb0f9d50ff2fdc704e7d9a", + "generatorKey": "3c19943d614f67309dd989e2e1bdeade5ea53b0522eac3d46b9e7f68604a874d" + }, + { + "address": "lsk3oz8mycgs86jehbmpmb83n8z3ctxou47h7r9bs", + "name": "genesis_23", + "blsKey": "8ffe1e957047e7dd979e8bcac9fcea9411ed3be947679ce26a36725b08da51ed2fa19e7f7c6bed701bf3e33a6f787b8a", + "proofOfPossession": "89177926eb5ed8d2be150884e0cc4eaf02a040a3ebb0af9df6922d8d7fc58da4777cc6591d3d43570ce6410077d087fe097cb30f28a164d22216859988f44ef88bc7f4a2134f882d044e4ee66d135a31cd063934cf6b4e820fcff3bbfc5b27c9", + "generatorKey": "b9bbcd67194a7091a517faf37a7ec0fda068c4ac0dcbb8ddf526de97e67716a4" + }, + { + "address": "lsk37kucto34knfhumezkx3qdwhmbrqfonjmck59z", + "name": "genesis_24", + "blsKey": "94c8d9240de83f6b09905756fae29c2c3aa9092649776ebe037f20011b3bff835944eae63b2dcf6c3861f11d457a875e", + "proofOfPossession": "9900c9235a0365b9a0b5dce686903737cc4aaa76e8f9e47367954b07ee3a0c0ab51351cd746966556ddcc53e69eabe0c025195d1d3a6788d69c1820bd1fecc096eea09770fe43f86f898c6182ce3057fcd52b43ce096a07b4da3f2369353988e", + "generatorKey": "edec02268c216d131fa9ec045049e6ac1526f48da772a34b1536c88c5af223da" + }, + { + "address": "lsk3dzjyndh43tdc6vugbdqhfpt3k9juethuzsmdk", + "name": "genesis_25", + "blsKey": "a1782a5f280f9894cea555d6f355c1f23e0581140c64f20ae469edd6ace7dcb6266227feecf002c2b508766e730c6f4f", + "proofOfPossession": "84e053bb01b22997e46ce4cbece0f5478e27cd49786cc36b1459c8930ea408e663bc725184197eb726fadf6988503c9b01be391ca3eb16587137cf5a3941717837baec7869896bae401bb513359485142778a52638429328f06a4469b7e21bb0", + "generatorKey": "9f1c361befb0ae35de28e8f0e25efe75ede78aa26c703625cc17e7fe2e7208f3" + }, + { + "address": "lsk4nst5n99meqxndr684va7hhenw7q8sxs5depnb", + "name": "genesis_26", + "blsKey": "a1a95b1526c3426ccd03f46199d452c5121481cc862a43bfe616c44662b9a7fa460fcdc5f97072754296e6da7023e078", + "proofOfPossession": "942c76c56af0112baa7a11bb8875a2336b321e85de56fd4267e97f3fb142445648a54c97ed22e5860fe5b0e5ef240599028d4009d091ad96ad727914532e45ff9eb44303b337f44bf5ed3ac796e6e22a9ee29138bada893f89f3bebc1a4daad5", + "generatorKey": "71ce039f0e4502ff56ca8d33f7ba5ba5392dd7915516b2d87eb777edef454377" + }, + { + "address": "lsk67y3t2sqd7kka2agtcdm68oqvmvyw94nrjqz7f", + "name": "genesis_27", + "blsKey": "a6d6aa277ab636486b7d879e90c541b4952264e18b8a214f58d32226fcc774a8e5bdac69223902424110cbda4ab58907", + "proofOfPossession": "a5b91b5e3881a36ea1b209f1cc09ab447e365b111e7529a88981e4e44c4a05eaee0507ff80460453e23187116510dc770d517e16aafc1de2aae2393ddd2e26cbe6fd096b65ba48cb6dacd0862d6c39b394117a596c0a1c9bae8d9b538d6e6dfa", + "generatorKey": "74f7ff53b55eda8fe9c11d66f7533c27714b121a5918a66c19b309e1c93dc3ed" + }, + { + "address": "lsk6quzyfffe2xhukyq4vjwnebmnapvsgj4we7bad", + "name": "genesis_28", + "blsKey": "b422e4fa8ab196e0bcc49f956ab3b5c13dc14442864dca80118dea7329308e7f7aa7547df293c826a29ef4bbfe517778", + "proofOfPossession": "8ce0fe2bf47180e74f315fda7bfdb376a277f394667c88661dbefcc57100af1d0a06d36ef406f7abc0282a1cb8f5091505d759a40739b11b4a1fd0060e2066edd79ad417168a977f1a59206ddac4bbabaf70feda572bb19c17b9d9034bfe28b1", + "generatorKey": "b5308c34412c54e4b8358b5fca16396084004ee37c6824c1ad751cbe8e50e24f" + }, + { + "address": "lsk5pmheu78re567zd5dnddzh2c3jzn7bwcrjd7dy", + "name": "genesis_29", + "blsKey": "809c35a2a1f510fb574a223474fb6b588daca95ab1b9b04f4f0dcdcd4581f05914eb1b9683d21997899ebf730d82a8a7", + "proofOfPossession": "a2fd6eca6018825969d8b9de58e6594149c5114cea9c27997f2ec67b923cbe562454caa5a5e956b3eb5ea0c5bd9b0196137d4646e21b51bd21503dde474d510f62654bb7ffd141fa3462997bc6662f2893cff7d917eb07f2985dae860723bd46", + "generatorKey": "62c37caa9ecdb3874354e7f780cb4463ad190bc31e75e552cb07b9bafc658f2c" + }, + { + "address": "lsk56hpjtt5b8w3h2qgckr57txuw95ja29rsonweo", + "name": "genesis_30", + "blsKey": "906653b7a74dc35499e0c02f10a9d092e7dae70e5376287b5533c7a52ade678784956e6bcbb67a11239bbfa977743a1f", + "proofOfPossession": "a5bdd92d340281c01d90224ca58a13cc429dc47ea9d2ef6226b023ff926a43ff0a50a82028e1fc20e9faa380136f5dde00a70d7170a8de3246e39b7787771e41271351dcbf4f88b6d40dac77b2e3324a371f9fc08d1fad90fe3e5cd61caae5d8", + "generatorKey": "d19ee9537ed38f537c2e8be0fb491331575f8e4050dc4a74ccee3244714d5969" + }, + { + "address": "lsk5y2q2tn35xrnpdc4oag8sa3ktdacmdcahvwqot", + "name": "genesis_31", + "blsKey": "b8396076f1ae032b572145f01ea0a3b5418f226afb0496930cb68250ca59b16fe2fb6dadacd88132b9dcd19a07d7f773", + "proofOfPossession": "a096515a639c004e7aecee3e88ddbb572163b914de63b528db584b27fe6a0267eb95213ccbebea849a720f1f717871ff191a4cf52c9d0a4db57cfcf8f2453d22cd432a5fe64dcb45982abe84343608a8b22740f7f3fbdfe1000fede5f0a08db3", + "generatorKey": "4ae9069cbc0e2371b037342010c5ddbd9c6d4a8c8d0a9eae59bc6a3796866119" + }, + { + "address": "lsk5rtz6s352qyt9vggx7uyo5b4p2ommfxz36w7ma", + "name": "genesis_32", + "blsKey": "8f96883db13e4f43e7280d8a58e7642228f46c375853a17e8cdb34fdeaf4e363a82678d2f54a8630218e097ba39d4370", + "proofOfPossession": "91a2efa4a407f63eb9157a4f4378bf6dfb4fc6d5d2714c2ee81f49ac90bc5dc3f1b72051a1fa1615f2e2d694cf17c27c1429e94bebc023feea2a405f7a8343dcc567636d15ac95ef84b1c673298becb766e036d9869e2113d9f4602f6e6092dd", + "generatorKey": "d1f10929b1eab8232be9df3b792496eb56bcb5c0a8c2fd04e3be1fab26c7980e" + }, + { + "address": "lskoys3dpcyx5hkr7u2fenpjrbyd69tuyu5ar4dgy", + "name": "genesis_33", + "blsKey": "a4f78f9b10c5671cca5aa2526708b95bdec56f3e404fc6c6403de83338940dfcc8d6836ba3d98566d314d34438a042d3", + "proofOfPossession": "91a1d0b501b7ab2caa5d240eae92c8c0ccbf296ebd3dd9d03aac1ca569f803091ec5ab57b7f6c34ad1aeb9aee0ccc17a1911c8e7a9ca681a6b803bf27e303f59dcfa32f678c4bb35189a8b7e0a3af43771ec841bd2ab32a96cb2eab0a1c2ad94", + "generatorKey": "3efa1c0a728a9741555b84ff1d80aedfcaf85370e1602890d7ba610bf33500bb" + }, + { + "address": "lskoq2bmkpfwmmbo3c9pzdby7wmwjvokgmpgbpcj3", + "name": "genesis_34", + "blsKey": "882662250af65099ca817b2564576582981f23746f07be09ebc03ed6aa582a327d4156ff4a12851bce3ad77be854f937", + "proofOfPossession": "b73f34042d210b6cf0ba61b04e26bcb08e4d671a12df09e592c14c73ac55df09a01adf94b205b86a9ac9020cc719e93b0f890050891d9f8622346f45112ce502e26293a14c36501a8f1947c33fa38535d6eae6c4af6679296e76a105e899341d", + "generatorKey": "8cda7b8df8975d781e053882a1373d190d5f8fd7c13ab528be8597b5d06ede57" + }, + { + "address": "lskowvmbgn4oye4hae3keyjuzta4t499zqkjqydfd", + "name": "genesis_35", + "blsKey": "ac304b4ad4fdac88bf975496edc43af0e324120984d5a12ac073b3e3e80c593470b6aa4f10b9897451bd6ee6f569a2af", + "proofOfPossession": "b08e154f3db163391dcbef182a63ad51d56521951307b9bcc60f12c83babeb5eef80b6d8503848acf9bc864adaa82bd610e3145dd77debdfcaa8e1e15f13e6da1d5bcfca4234b46208900c6ce35d0147534a7abc728504d731f286edc31a3ae3", + "generatorKey": "f926fbec6d2e461af7c58d87754524abd26ab1f617d73348ba1318d371f7cac0" + }, + { + "address": "lskos7tnf5jx4e6jq4bf5z4gwo2ow5he4khn75gpo", + "name": "genesis_36", + "blsKey": "87971b8a0520e08dc8dbb8114de7ecd44e98844c9179585806e8a1edaae1190ea85e6471767e90074d87d1dfbafc983c", + "proofOfPossession": "ac1fa23a608ce0be52ada7759c4631a5e3c7828a2a622c718b67c4d8996eeed61c382ec319ff2c608290c141ef741ba013f7567bf95cdfb29295dea31adb440f5d856f5688fdd553f47a06ab5692ee5fb99e5a50b329fe4406bfefb924b5665c", + "generatorKey": "d5781773a9b07a569a0d87c0bf82103fd459a2185fc32f5c312a663c5bc65784" + }, + { + "address": "lsk966m5mv2xk8hassrq5b8nz97qmy3nh348y6zf7", + "name": "genesis_37", + "blsKey": "b847749ece25a2ef51427de371b4efc2342fb38a2c5822b941c1dbf43c3f8dabf5dc0e1620d2bdafb597d697e30ab801", + "proofOfPossession": "831a557a972e0ed1a9cdab88a13fea899ce1b7e6475ee2d42a1a1faa09fe9042eaab3bd8b14f2faf4ecff84780b8db6719e8d6bc8917ada1f77182b2fb4a40b544c02486fe0394b8fcc72ac69fcdf3d6c0920469225bf0ad2e047fc68b9376a3", + "generatorKey": "875d9a84adcf997034d5ab6189a063d9817da3a6c8599cc46c84b70b5081b18b" + }, + { + "address": "lsk7drqfofanzn9rf7g59a2jha5ses3rswmc26hpw", + "name": "genesis_38", + "blsKey": "a6d6315e85e8138de21f94d0c5c6f4c2515d493b17653156745155b25f9f121f6d13e7c36a57fa5002a9aa0a0b282394", + "proofOfPossession": "ac38044b8d84ed22d42da3a240b7c2dd16fbdf3b03655226b46b6eea46256a3ee33232771d67da1a4df6717476349647077f5cb29715333d8c55f5b6ba70c77af1944ac54c913445da29c99dd441e36d9def69c0e9709ce062ac70e4d15628a9", + "generatorKey": "71d5b4b08ea0b7a0ff95f779aec53590a3bcb5a87fc770334f8c9ee57fdd79d9" + }, + { + "address": "lsk8vjsq5s8jan9c8y9tmgawd6cttuszbf6jmhvj5", + "name": "genesis_39", + "blsKey": "837e0759968b1ed95789252d1e731d7b127c9a53a74e86f3ca3d65d71cf666f2208baa782a42c45d4132630100a59462", + "proofOfPossession": "b97607b1478f17877b4c8042530763894dd7b79f8bbf5ca0883d08b94dc8a11cc2c2a73123160e3b01da692fb071f5fe0d808426604b5ad8aadebda9b02710698158254f6f1d822c2c9bae5c081101806e9220d79c547391e6fc6d8f26094dc7", + "generatorKey": "00110f493d122a73628a518842e99591b91def4ef9fbd58e1b6458950da5a776" + }, + { + "address": "lsk8netwcxgkpew8g5as2bkwbfraetf8neud25ktc", + "name": "genesis_40", + "blsKey": "a3aa25a2385666122df82fa74096f30560c270b1ef981ff459e25cb5819d50a2edd8c315bf17a6a1af8d88c0e9325e50", + "proofOfPossession": "b543e0716990a65727b51489c90495289bae983d3a4439fe68826c2175b4396d37da0ff03910b369335377de097088720b77646a3fdf196e95c54f2ca6bd414327231996bc2dba0c1dcc7a77b8be10b84a4ef8947a0e4ba22aa09a6c025521e6", + "generatorKey": "fa7af9f8623b324e6c021b7a0899d980a41dd2de86c35cab530751eaa9e55a0a" + }, + { + "address": "lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk", + "name": "genesis_41", + "blsKey": "a84b3fc0a53fcb07c6057442cf11b37ef0a3d3216fc8e245f9cbf43c13193515f0de3ab9ef4f6b0e04ecdb4df212d96a", + "proofOfPossession": "b3de21449917e17d5eadb5211c192ee23e7df8becad8488c521dcfb0c67df64a81561653d92805b4bebae9e5b5bdef8717f1259eaeb55bd1e7eafad3d74efe20181b4ac84bb7582b637e605fe78f10eb03b2a4acbff49809e86d89aebc6076b9", + "generatorKey": "91fdf7f2a3eb93e493f736a4f9fce0e1df082836bf6d06e739bb3b0e1690fada" + }, + { + "address": "lsk8dz47g5s7qxbyy46qvkrykfoj7wg7rb5ohy97c", + "name": "genesis_42", + "blsKey": "a2f8fdf2b80c987ae61634125c54469928728ecb993bab3db892725b16b41ec48c36056eeee2a1c9b073d12bdf917684", + "proofOfPossession": "abded9f3ad588edba52b7b2a4b3ff25f630aefae0d7a91827bc1fb7b8cba36d27c310a7a58a4a66ed9a8d90ffc0aae6e17718b1fa3f8e7305498e740d531460702a7dce1e32c19e18849c786c26a30e29b464c7202dd64d021c1eef643de519a", + "generatorKey": "567e1e27c02293d7c190a1eb203c2daf1935a9901de66df73f8e4eeae6907d04" + }, + { + "address": "lsk8dsngwh4n6hmf4unqb8gfqgkayabaqdvtq85ja", + "name": "genesis_43", + "blsKey": "aa5174668a4743d838fa3742092c744c3edd4ee64c535ce2a69eeae1c5f23029acd74853410867d873076639f4ce1cda", + "proofOfPossession": "ad79b935bd503402b83404125ef11fab81f4c6bef0688798473e430f892704b653209aaf81f16efca9965fad0850a3971662f33c25994568e1434f4f46901caa1c002cab18dff7337836617c372673714d63b01ec4db098f419c027015aa4c05", + "generatorKey": "dd337fcb819073335382415bfdbf5e5b7e73126aafb0ac46479137328e72d438" + }, + { + "address": "lskux8ew6zq6zddya4u32towauvxmbe3x9hxvbzv4", + "name": "genesis_44", + "blsKey": "94da5ec9da5eabf2ab184de1e0ee10f63f721897475acd59c3c53adc51a9b39b0f4fa28573fcc309e576dba658425dbd", + "proofOfPossession": "a672d269ec605e04065fc0da8e6f520d0273b1c57a754409d9fb25cef1be67b8583fa683e27c0284c31105045f395c0c142d0648420b9b209fa88fa13025ba2b3887e04e3fbae1db6e5941ade41713a4384c139e47e72a68c964c4a5c0886d25", + "generatorKey": "563aa06b554beea30fc4455ae51e0954051a3457315b2370fde9c22d3233b522" + }, + { + "address": "lsku4ftwo3dvgygbnn58octduj6458h5eep2aea6e", + "name": "genesis_45", + "blsKey": "b9dc37e370cdbab50fe906b675551194e80705f5549ec07f32b95b85ec1ee1b149d156e649ebe1eac57bcc2ce9db3e56", + "proofOfPossession": "abefcbf20c53c10ac15054527c2ca691994f0b5cf60444aef49ba4e39312774eaa073be6b887ca5792bbfd53adc7ec3d0b0f6b34ec8a8f2fb6708d5a9d3de242f5fcccc3c3cddcfc5eb8be5aa13c333d114c091f594736e7a43d7d9212d0063d", + "generatorKey": "894289ef63ad9f51868d06e700c5dc9cac7af2e6601a99449134926cfdbb4340" + }, + { + "address": "lskuueow44w67rte7uoryn855hp5kw48szuhe5qmc", + "name": "genesis_46", + "blsKey": "b7c47fbb0d7e3793460949c9dd6120a310eb52de67f6cde55c022b05dd5053074c8a0e562896a482c787eb2eea82353f", + "proofOfPossession": "a265237ff848fe7acb4c84b6f68008ee7ec917a7a11c050f630b834e5caf22a447de94de0e7c52d03b18e003e5f9a3f2091cb5a78817ba42a7e19c714af47ad0b94824c5b90862059ed3042446143c56c4df011389eb42dfa2daa58df677d473", + "generatorKey": "ebe1d6189c7015d175414db9621a602b0912826c1eb1aab09e69bb33ca8fcda5" + }, + { + "address": "lskym4rrvgax9ubgqz6944z9q3t6quo5ugw33j3kr", + "name": "genesis_47", + "blsKey": "a5963aa24ed05e95d19fd9de35ae6f523aad987ab2b9897216091e798e15f5062e9734b11fcacd6b8f312162ddc10940", + "proofOfPossession": "8a1ae28d6d70bfa0dbcc694c811c05ac6e697a17f41d45a32e1cb5b225bd42de7c1043f4af3c17d92641c4d017569e2302dad3e32493294831da564a07154e5098129639deb89743d1146f8e01f9f6f32f382905707051467242b646d86bad05", + "generatorKey": "4514d1723eed164b3792f1950d3b1c7a1067441ba207cce8d9bdd6f436a119fe" + }, + { + "address": "lskyunb64dg4x72ue8mzte7cbev8j4nucf9je2sh9", + "name": "genesis_48", + "blsKey": "870db2da31a9471077677bd9a7529ee7523bdd64fdba46c514e94aa52e940566479cfdab29b07c1573aff6ba7040c684", + "proofOfPossession": "acbe270292cfaa154f256a83c9bdde889a9205c85c5ff0f41dae586dccc7f29f0464fbc087a5c5adb3cb4eca3b95bc14187db64cccd24e98d3e75215b69bd2bd0b357834c1ccacbdf91556fa59a86d04d1fc8aaa3be2ae5256aea3bd36d26942", + "generatorKey": "a9b0c063fee99a903a55da57e3d16f069145e414b62e25dbbf218bd608a61f7c" + }, + { + "address": "lskrzuuu8gkp5bxrbbz9hdjxw2yhnpxdkdz3j8rxr", + "name": "genesis_49", + "blsKey": "b1b4ba05e7116670be55b6d9fc28574d142824175a1e3d1cdafa37f193c342eba1a85d8520a9fd962811fe63a5a2d048", + "proofOfPossession": "99f7e39908f0cabbfd156c78a903d6968c455f5edbcb878525abe1217674d9745da87057f1fa93ccff79632253d5b4fd0c6301b0b9eb0e07fdd4c0abc99da0229ceb4a03b0da237657e445a7bbf6877689bfc027d65f24f05982dc2aeb34c72d", + "generatorKey": "d454f04eb0e05c980f6a3427e98d73493665860ba7a29eb915cfc0b8daae2849" + }, + { + "address": "lskrxweey4ak83ek36go6okoxr6bxrepdv3y52k3y", + "name": "genesis_50", + "blsKey": "8422c22feba709265c30a7b86a9ee9832d6b32fa4c9dc091c390e1b15e278f9009dc5d70868a56dace1ff622e9e634d7", + "proofOfPossession": "871ed33b68172b0ce40a3ec98d6fa9b3fd77245c2c1cb7f1071101cb459d53b05fc0168597148f976ceb1ded71999da8094fd8783cf27d1e21f9b965164573c0ca849210bd1e99f4706ca6f43636f9ea535c333a36c4267a598dc58c7c7fc108", + "generatorKey": "21120ef22b7df438e06b3862d3f0ab99d5704b3c61c45a544c64c908da8955ad" + }, + { + "address": "lskrccyjmc8cybh9n3kgencq8u7fh796v2zfraco9", + "name": "genesis_51", + "blsKey": "8b436ed371b7af11b31347c12321d90a427e9aa8d93275a27faedcbe2dd06c5dce1e1a4a03b0ae030e5cd0106a942cd8", + "proofOfPossession": "b1dcf2ff65ba4096611f392fb56d104754927cba14ec3d193ebcf7d6eaab062c7ab770c512e815c7d52c37fa9b8622400df7939f4bbeb8566beebce1b13d67562f7bb6a01f988a501e4ef691b544cd05796010b614014ec3036b171c7392cd7d", + "generatorKey": "bf9ebe25faae5a874d97ad1772ad062ca52f63e48d806ef641e025a963224200" + }, + { + "address": "lskr8bmeh9q5brkctg8g44j82ootju82zu8porwvq", + "name": "genesis_52", + "blsKey": "a8271f9e8874eebb6d66dc139e984b6a6c71d2a7e23c6d7061bab7725e9c65f2e2123778130a2acd278f155440debde0", + "proofOfPossession": "84a3aeb2cc8329afc63f40d137b017ebcffe6df9e55bdaad8249408d01dad5025f1c83faecb53955ba5524df25b0d85e180f0335d0b5ac8c82c7f5fd0975002fe0231a83754c0034b07175afc426b17978870f8326cfe4694ff723e08d0b6a61", + "generatorKey": "8062134a09cc464fe9465cda959b402a3d4506a1c44b3f5cba9661d42e912421" + }, + { + "address": "lskrskxmbv7s4czgxz5wtdqkay87ts2mfmu4ufcaw", + "name": "genesis_53", + "blsKey": "98c4f0e2b01f1b6ed07035fe46c17a40fe5409b1461a2b697afaf869e2f8c88b2db297b9a149208109bab2da195235c0", + "proofOfPossession": "8dad459d6b312d4a6767695029525e95f04e3ee083de85d0db5d818d15d32ef7aecb57f608c2c10355e3ca6dba8018e5192862d80f00fe1f71fd396d81d6a7649221c50bc8336efd12dc1cc13ee3c3898617971244af6a8da5ccd9224c9ea2f9", + "generatorKey": "07614fd5036d099a3caf004d46a083d12df2024fc03ef29cec22e58d1f78531f" + }, + { + "address": "lskrgqnuqub85jzcocgjsgb5rexrxc32s9dajhm69", + "name": "genesis_54", + "blsKey": "ad250adf40b559d765bb51d65340fe38de9e4cbc839b6e6509d99bb9bb3f89be1bbb96d75f709f2ae9e715e6e6ce38a4", + "proofOfPossession": "8943f42818d3c3374d43d1aa0b427436f4edec3e760f07aea2990b99eb3ef69952d580df862ad9034062fab57c548164143bd3b77d16ae74fd8fb84518983dfd015146ac9d0503c858f0022591345c077656e5af22cc78f1d35a02ad1e74c8c4", + "generatorKey": "55d4c0e745954f0fba9629b346055060418961e7edce58c77bf2bcfc7f753d42" + }, + { + "address": "lskrga27zfbamdcntpbxxt7sezvmubyxv9vnw2upk", + "name": "genesis_55", + "blsKey": "997583cd4f633aa5aa5e616a75d9edc370d5e6eb77e2418c13648b435b0182cdb7787c7ca91ed3939b403fe59041890b", + "proofOfPossession": "95324d44556e3c61bd307a40c2ef7f3d988e0ea561e5ece2d2809cf078db232caea9df8b35d8411238fddfe83a6978a70ae88e29fa5b6322b73f7fc9756daf52aa6369e5e69c5b2304871bd324e8125a698e360e3d5f1ad20136370b8d9808ea", + "generatorKey": "d2b31ed942359b0c9cb696cae874a2dbdd6e24915dd8a5882c7c042eac1e6831" + }, + { + "address": "lsktn6hodzd7v4kzgpd56osqjfwnzhu4mdyokynum", + "name": "genesis_56", + "blsKey": "a97efbc836dd4028813063912bcadb52fdb8e4d2ba04d7bbb477d2a97e16167c5fa6ba75e482cd7a7d476d78fed1550b", + "proofOfPossession": "995df23eececc27026f62816bfd07d71696e2dc5751bafb03d50bd9c66d388c562d6c1357300e4d51e5522edc3cb5ae217b3607795baa0209c6e63db01b4b7c28452c15db1366764abb9d886d0a908da07d3b7b2612e263d95721ffccefb4aa4", + "generatorKey": "6158b2a5b662ce05c7864dff4c2aecf6109cdea1be703a79147450b082ea242d" + }, + { + "address": "lsktas5pgp3tofv4ke4f2kayw9uyrqpnbf55bw5hm", + "name": "genesis_57", + "blsKey": "a77de9989b5fab42dca028637f401953b9e0fd6cd61dc2fb978daafdb5478ac77d67a37135c67a2178b44e5a35a1fddc", + "proofOfPossession": "acafd4f724cd7b9dcaf166aaf212122360f76c2faf4d146e8d0014653c0fe09f750690ea2b9ac6df96300301fb020d3b04c1b79965cc8929e18bd93190a366851033a901e05850770cb69fc28146db719f1ac232a7947ead59e8d584eb3ddb79", + "generatorKey": "8307181cf9d1f621261e8a97a5b3b77d64a9a1f589a2c14e42b2380d9c2d6297" + }, + { + "address": "lskk33a2z28ak9yy6eunbmodnynoehtyra5o4jzkn", + "name": "genesis_58", + "blsKey": "a1dff3e7486e27eb2bc99d4343b57e06fb8b52f8c7b6ec6d539889afcf0c221fbadcfca65f2ad7351beb8a51e67513fd", + "proofOfPossession": "b6447c9e317179a9160ea0c11c2ff49c11e0300332c2c0ec0bf81e936af231ffc3b6628da3e01eda821ff15e9a523f3204b32fd4fcce988c2b73b56609709dfd25ec9df9e33dee073f9d26a82d268569d117ecbf7985e012a975fa7d3ad5e4fd", + "generatorKey": "689639f5e3808cc0efd5f8d48ca6ee6f9a7a1bd5f5776832cc9b448cff5d0aa9" + }, + { + "address": "lskk8yh4h2rkp3yegr5xuea62qbos6q8xd6h3wys2", + "name": "genesis_59", + "blsKey": "95087210c7145581fd8dc397ed12ecc2eb703eaa19dd837d7c8c54cf625ba00bf88608aa89170d703c77f7dcf6707398", + "proofOfPossession": "b09816fd6ec0b666e1f61bde72069057a11fc78d7fe8b85873b6d909aee15d74c637076e149ff279c587efa4e6a468900e2c4a857bc55978ea292189737f95e7026514ec5e9a117f31b8339d8becf3af1bd2555df6d8f2372b54b7381ff355ed", + "generatorKey": "db1c7c22ee495ad3553394dca00c62b85e78b58e78ca68bfe5027b3346f6c854" + }, + { + "address": "lskkqjdxujqmjn2woqjs6txv3trzh6s5gsr882scp", + "name": "genesis_60", + "blsKey": "95acb59c54e53f09d7aac37c2db59c6df0ebb1e38120690a9035c715dc9862995472c72e9f48bfb05e920494dc17e9bb", + "proofOfPossession": "8798b4e143b15d10965194d0350d95c374d214d14f6a0c750a1a1699f1221388f01d00c6b708167fc7fcf355591abe370ed45c55306fdc372d26432cba8efc1f83238c1f2e669111656ba61b4bff391786713c28f7d1c6e717fbe98aec2dfda3", + "generatorKey": "c0aa7af3198f0e3a6bf35c5be38e0f181827735b1c3a635e8db05b80b3647054" + }, + { + "address": "lskk2vnyd5dq3ekexog6us6zcze9r64wk456zvj9a", + "name": "genesis_61", + "blsKey": "8739c54fb8452db4ff1857649a4144dae29f7bbd3275aaa8f0f2559095a09510e38bb0155bd01d01349e7f1392132e41", + "proofOfPossession": "b78a813e912849e2583d6e774740f2bef3115f1d23576d206ba15bf0c64404b48208e7b2b5becfe2386fc1ad686094251707a7bf8902a10b8ffd207394ad26b64f7a0c5bb7bfc737fd836b160bf16c4d14dcc343dbc8ff7993391795ded7e448", + "generatorKey": "7ff8b45c5f6239306af0194ee41e047669e33338be3f8e6c786d90fb905c8b6a" + }, + { + "address": "lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6", + "name": "genesis_62", + "blsKey": "8d4151757d14b1a30f7088f0bb1505bfd94a471872d565de563dbce32f696cb77afcc026170c343d0329ad554df564f6", + "proofOfPossession": "90df1472d40c6d1279bc96b0639ff0b8ae8cef80a0538ef00b9fc3bf7816a541d2eb9349fb6a6f1a07d80504bdf105ac0726e6b01ef75a863cafaf5356dbc03ea1c90387f79d3adf15c8a44614d80e42e7a964df2eca83a871cd378f39513414", + "generatorKey": "b53ef930d84d3ce5b4947c2502da06bcbc0fb2c71ee96f3b3a35340516712c71" + }, + { + "address": "lskqxjqneh4mhkvvgga8wxtrky5ztzt6bh8rcvsvg", + "name": "genesis_63", + "blsKey": "abc1d1ef1f992a9fda45841079516169c879421f4260194c0a47e46afdb9f349c2a51e66e9f2ee8bf22231027584a6bd", + "proofOfPossession": "a16aa0fe3bfd5383c2fd874be4feb930f2c75f5d35d0e0ab314eb545a673aa1854ebfee7b15a026d5a9fb02842e54672149382f2898a0e12756bb949772b1316163ba774768c88fc90c2471afe94140d8d8f16974f2ebf050358cd98587b32ce", + "generatorKey": "a2b5e97ac5a5b3c3a7cd9b4401eca1f4e8da59fe567e229ea47e65bf40053402" + }, + { + "address": "lskq6j6w8bv4s4to8ty6rz88y2cwcx76o4wcdnsdq", + "name": "genesis_64", + "blsKey": "95274c1b15467d43a3b8a3a632a8fb7e1a2efbdf92559ef52ea6ff1b0ba1c7cc2f75ef357b2dc7f0130dc9c04aeaf4db", + "proofOfPossession": "a24ef42b04be7bcd65d8434b04f7118bf9566a0d3a36c732cf5b508ccdc12855754663bdb32c5d871eee8a0774a1331a14f25f3aeb6bddee7efaebd2214e19b7cca9f3d3bc7eed93b85b15f0a626117f24361d65688dfbe7267141f13d323d63", + "generatorKey": "6c99048cae450de8735dd410a5c8b0e4655afaebcc2c155503f890af51e067c2" + }, + { + "address": "lskq5attbvu8s55ngwr3c5cv8392mqayvy4yyhpuy", + "name": "genesis_65", + "blsKey": "957a970041ae9b29f33cd9baaf077f77049e664c8123b22fda3793252f71916c5df0b103ffad5cb75bdb2724d9ca3eba", + "proofOfPossession": "80d4fdac09ce195c9d685a751fb7cd9d4da7b9dc906348b4bb741ceb53f876afd0bceba75b36327a8cbd8bd3ca8ac2cc14b4fede3ce2cdac7f0bf0ad5e58840c64bdd0a0905cd6aa5da8acfcb33a931e469cadc27a42c2a04a62fd6ecca05091", + "generatorKey": "1819bea0ff11aa0cde16c5b32736e7df274f9421d912a307526069fa119100ca" + }, + { + "address": "lskqw45qy3ph9rwgow86rudqa7e3vmb93db5e4yad", + "name": "genesis_66", + "blsKey": "b40065dfa219e40c65c07d516158d722ec695abc91411ce57550c77fa2119e52b56cb74db7a1d805b631752e8f6b80be", + "proofOfPossession": "b7085c15521303140512fdea858231a040534a4b0c1dbbdb002c8df233634270d33e51c3699cf4956d165c0183f29a32070d8f4e00433ebcdfcae337a5f09f2c971ba97d5b35413ce032d2ec4084ed79efc917bdb75ded139fc9433df884a18e", + "generatorKey": "1314b7d167d5829fb535d15dfb5216e10ad2e5b6a349ae347aec77317b6aa73f" + }, + { + "address": "lskqg9k3joyv9ouhjfysscame66hovq42yeev7ug7", + "name": "genesis_67", + "blsKey": "86f828da4b3c129eb54d95bef7975281b30dd811f252b5792998718355c599aeca3dbb222678ee0af84b13f5af2400b3", + "proofOfPossession": "8e062f48ead9234b710dbcfebbb2e502ddff68e3d5be19a8e7e89b2141c76caeeae233999009f24f7b6e65f3774ef6cd09de9d5c0bb59a60ff6cb31b276f0172e35f89061f3c2d700543de5cf4d6e613ff6ba7d41c1379d6baefd844ef4cb517", + "generatorKey": "a9568912797914f590413c3156c9cff93c9c14193b01e7bf248195bbe8c1af19" + }, + { + "address": "lskezdab747v9z78hgmcxsokeetcmbdrpj3gzrdcw", + "name": "genesis_68", + "blsKey": "a03ba0f1d6bf9378681b9d96dbe8176cc0ab2a424154cbbe325fc279d02cf58bc15de966cb1e272312ba2b6db31a7f05", + "proofOfPossession": "a20a8edd978fe911da6c933d486cb9af770179ef5ee21ad869c4c35e63103cfc2ac17350ee2d35b4bbd487193cdb33ab0116fdf2f078f289fae2922f6a7e372ef8ea543d52ae74ae395dccf2dec2c40e6596c807a14c9fce45b320321f68c612", + "generatorKey": "44de3820f1a1a7351953d2d000f29cb7bffecf30582a8b3da2cb80c83b9eceef" + }, + { + "address": "lske5sqed53fdcs4m9et28f2k7u9fk6hno9bauday", + "name": "genesis_69", + "blsKey": "92f020ce5e37befb86493a82686b0eedddb264350b0873cf1eeaa1fefe39d938f05f272452c1ef5e6ceb4d9b23687e31", + "proofOfPossession": "b92b11d66348e197c62d14af1453620d550c21d59ce572d95a03f0eaa0d0d195efbb2f2fd1577dc1a04ecdb453065d9d168ce7648bc5328e5ea47bb07d3ce6fd75f35ee51064a9903da8b90f7dc8ab4f2549b834cb5911b883097133f66b9ab9", + "generatorKey": "b9e54121e5346cc04cc84bcf286d5e40d586ba5d39571daf57bd31bac3861a4a" + }, + { + "address": "lskee8xh9oc78uhw5dhnaca9mbgmcgbwbnbarvd5d", + "name": "genesis_70", + "blsKey": "929d5be8abbc4ffd14fc5dc02ae62e51a4e8fff3fd7b5851ec3084136208ceac44366a7313447858e3814ddc4213d692", + "proofOfPossession": "88e7331baeba342eaa907cfd7a1b5bc839a70e78b0535d68c40ddc2e4d5157f8d1ff55d29243fe2375fcfef5c3a2133e0a0d11f8b58041278a1e9a3a9e7986f906201df48987e8f8eda2e6ee4452fe58b54805e2ca4cc256d8e42083b70f79e3", + "generatorKey": "aed740da1a7204422b92f733212398ce881c24a4cfe40edeea6a59a0f6453743" + }, + { + "address": "lskewnnr5x7h3ckkmys8d4orvuyyqmf8odmud6qmg", + "name": "genesis_71", + "blsKey": "81f3810e7567ba9e1aa9fab7d5914a1f2ac8b11d952872b398930836f80395c934bd6e71c291193458de7de4382c913f", + "proofOfPossession": "a67d9d0708496d13f45fa3d3940954bdfdfa69814554a5618a388cab03a5e82210171f06b72b03966c8a5bd8fe3b235e06de2fc4c45333395c8e10dba086a4f50efe3a7f87f741346c07b22de2ba49eedc521cf53fab31e2033175ff3ca00f08", + "generatorKey": "bf5f4408df7a1cde279b3cfe7ba6c2e2600a4bb90d883b98ef8048ec344221e0" + }, + { + "address": "lskwv3bh76epo42wvj6sdq8t7dbwar7xmm7h4k92m", + "name": "genesis_72", + "blsKey": "8ae82e86c2ae47fe55b3db422b5f6e8a8ecbf4a33a0e910b4cc53d1bef0d66e3d19e8474a97ba58e31798c604758b1d5", + "proofOfPossession": "9215a181382a5769652e3818238e58496ca1c80eb6282b000708b2c9c19464153fcc8a541d8aa32378186b61fdb2183d15828ffa20e49a0dae0cb05e8c106f894a7ee7190c6eb60874477da236c05a275187bded6ac5a9c98656eb2199f736fd", + "generatorKey": "f99c543eeba441fdb22c673fa81878269c3b69a6366d8d51fb6890f2eb3118b6" + }, + { + "address": "lskw95u4yqs35jpeourx4jsgdur2br7b9nq88b4g2", + "name": "genesis_73", + "blsKey": "a58edccfbcbc35d6f9fec1535329a114cc5a2118945098c0f201345ab7de78d36a32014dbe701faf7d32b24f7a696d9e", + "proofOfPossession": "999cf3232240944ff9a14e6c4680fae450be8c0ed43fdbf8f92e7873b5482f88229768fdcfd86e22767ec1df3b5fa2fc0b08202ee4a343bfb19c8c8eabf74d44fa73c4517ad0a102faf4ae6fe87cd766d860408b51d31dadcc5674c92908c7ee", + "generatorKey": "e2f80871a5220be51352427077f6e93c2294d88be6b731b535d2ce9371274e7b" + }, + { + "address": "lskwdkhf2ew9ov65v7srpq2mdq48rmrgp492z3pkn", + "name": "genesis_74", + "blsKey": "8c5b12f5b7aeafb07e14c5264e7f7ecf46b3ba0e6f12619e19271a733e06e913044ea2e5c955eef3567fcc2d842bc24a", + "proofOfPossession": "82237a5371179107af8c53ef19bf3e0d055b70ddb689763e0a8ac6d82884d12c2155166af4aa92b66fa64b6a6d2bbe7602a118d597345dc100bd6983f072b9d8da7bd0699b0f3cb51f1ec5a9f2e2feb76030125272325e7f5885399f1d26c5ac", + "generatorKey": "cc83f488c03e58d083927601658d234ffd12b5cb6fe3151206f699d031dc4161" + }, + { + "address": "lskwdqjhdgvqde9yrro4pfu464cumns3t5gyzutbm", + "name": "genesis_75", + "blsKey": "a397bb33263b2850758a1b144401b741c1278b302eb8d27be6c61363d9cedafcabe05fbd7d9ce5e75a7078972d397e9b", + "proofOfPossession": "b22ed60a951702ec7bfd85482e59703af76c4c79fe2d3a3b81e737d53746543587d2932fcd5559d56f6530bfe48d23f5093aa30f3e299733cb56151175d22e21895ada290521908536d71480f1066bbeec7ab803376a4a81e4d7ec3bb4d71dc0", + "generatorKey": "902b7ed4708c476c7f0e96825cb06f95cbc86953130575d2c4589d8e3dc2f69c" + }, + { + "address": "lsk2xxvfxaqpm42wr9reokucegh3quypqg9w9aqfo", + "name": "genesis_76", + "blsKey": "81f7700c2115434acaf61e88b836be11986476751d6c02617d1087e7bb45798ac56929cb5f71c890c6159ff4d71cd1b3", + "proofOfPossession": "8bc04a899be3a7ac99e2ddda6567a0b01e21aaea8daf4848821e8233cbe80610a2f670922865f424e878add1de8c978e1913f95308a50693fbc88e991e6bcac3bfef8a1d03f89bb4dfd9c991cbf1c613f85203dfacc4376057f085967f2a7283", + "generatorKey": "621d52ac19aba86c4feef94c67ae62cfa3f6ac192177ae37be2e6b3205449c0a" + }, + { + "address": "lska4qegdqzmsndn5hdn5jngy6nnt9qxjekkkd5jz", + "name": "genesis_77", + "blsKey": "90f87fd2122689c54bcd8fb859c5b36d4b583272043deba66199ad181ca2c38cf48d453c46ec881e03d2b7e2e63e3684", + "proofOfPossession": "add6eb668bebf90fdd80b01cb83a31b02577b200c85845bd5260d7851c02d21aaaf6d040e6d6f27a8690c9598f92ba240cdbb6d7896d7a777c484d30ab48d71b1aee1b07083dc5d11a94416c4cf85e33ec3899b40e6222ac888104f80b8d96c5", + "generatorKey": "965e86fdfcdcd64879efe23705506faeb4dfc4244f93d47f4bf444966d2a0f3d" + }, + { + "address": "lska6rtf7ndbgbx7d8puaaf3heqsqnudkdhvoabdm", + "name": "genesis_78", + "blsKey": "a94d3cbfde92550eccede718499df12f33a8ec9a4b386e4ca423161d667862f45fb06397b12dc6a6cbafc14b1cfad26b", + "proofOfPossession": "a474ee16d276d3478e1b7005960d41c0e271652f29c3178230b7fdf395801dd62196294b7695b3ccad63887558e0f27d0b121738a42cfe9acab07e6763577ad87eccb5b1d0cd725cb4a32225e79e864c238ce3c56b6db8960ce9fda82828d5ba", + "generatorKey": "f8252b40a65be6f5f6d0be446da5ab434bdc0a921fd0956b0672ea4a218d2d7a" + }, + { + "address": "lskau7uqo6afteazgyknmtotxdjgwr3p9gfr4yzke", + "name": "genesis_79", + "blsKey": "aaec4e157b19c0a3f2965cc636f5f82cef9b3918c071e2c6e50f57ecb44587d58139595e8f4c1fc7f76b2f7c09b1b6d1", + "proofOfPossession": "866a031b5a2a6b0525053b2d870487ac2fd39cf2cf18ecf462bc19afc5ef52f129cf88624fac73057c5375004492dbfb0b8cacb906b3a7daa4d7edf99f10ab15a90b3b328e8ad6701e838a88351fecdfb5b32eebeb80fdeb8c0345d1b5257d7b", + "generatorKey": "00245e599fdad13ed0b064c069c71c73caf868a4635c0143963a529807f8728c" + }, + { + "address": "lskayo6b7wmd3prq8fauwr52tj9ordadwrvuh5hn7", + "name": "genesis_80", + "blsKey": "881fa9b753cb2f89d267e0615cbd1ad9664d331f21d89cef2131686b0af55112fe1ad4df7f2c085f78142e75d90d2cab", + "proofOfPossession": "898471d3356573d6445906d973f1876f1e38570b6dc9c875c88138b302806c071efbe327f66c6646f02c134c3b1b019d0227bc83acd0ca10f65adf1b8fad7c9cb383909a015fd1d678c6272e5317da58d45b89fc1c954641a61169bf1c1a1728", + "generatorKey": "5ec5a5a2c91414f5cc5e3354b58671e624bc88a39fdc8f128593daa06545d6cf" + }, + { + "address": "lskatntynnut2eee2zxrpdzokrjmok43xczp2fme7", + "name": "genesis_81", + "blsKey": "97a4b205ac2b65a2f17ceb49a763393935021629068fe8a8c299e49b986e79ff8cc959a7343b5d00eae2783b825ffede", + "proofOfPossession": "8a86fbb8e59ff0de4f2d717ff3c7b0f3f9cb4b14f97deeffb907428666005e613b02cfac0bac4714389d898236de2d5a02df536b511675d2cbd37dcac6dc33bf4cf2d9d43cfa710b3c695bcb8cd29867477ccf3b1e5b9e3afaf7d8d4e50930ff", + "generatorKey": "ce6bdb7380fa027c46edd15a072bbabd6b60fecb0e09589e20be560b333ca63e" + }, + { + "address": "lskaw28kpqyffwzb8pcy47nangwwbyxjgnnvh9sfw", + "name": "genesis_82", + "blsKey": "b279e1a3a5edcd1045682e7029045b70dffbae55c49b14391b9f776750193269b4fd1d9f0807d9ee66e264e08ecd97cf", + "proofOfPossession": "83a5128e710b91ab91f7726223120b389c1f77735c9c1d408c466b7f0484b020f0d2d50edc36d49e410141d8a509b132059142e250f145810eefce03dfdda25aa84214d30cdfb6ca11a929337bf53dfe4c675117c06e4a67206119ed1e2b2b9a", + "generatorKey": "bfe46727c386585d8d59c02efbe48d4c1a919ff07b87267156ab96e10ac730b2" + }, + { + "address": "lskdo2dmatrfwcnzoeohorwqbef4qngvojfdtkqpj", + "name": "genesis_83", + "blsKey": "82b478f1b884ee4c152490afc8b233d003745a58c236b00ecb3cea1022d59f04bf225266bbe5b0a5aa7da0a771a66acc", + "proofOfPossession": "ac4d05f93e3c374c83ab9cec2a5c67dff8a02298361584267968fad8f391af083b5041a020ce7a189fd8fdbf055a265c04f55e80a8dcf06e7b4e3358b347743f47d33bd5ee0cc4d4213995c46d6d4e1a61be929f571c1a0fa1c7dec805a85805", + "generatorKey": "bbc7ca5acae1d53e0a44a212f4c77c7601ace0e489d936c0b6f26a9fbb03601e" + }, + { + "address": "lskduxr23bn9pajg8antj6fzaxc7hqpdmomoyshae", + "name": "genesis_84", + "blsKey": "b067f711431b1bee09000b1c27fe39a29a5603471a6993d47bf56ece01a17fa4b00e92da90d80689ed2635e7e0f90891", + "proofOfPossession": "91f3d5519f94424fd59c120c05d9f2f34d8cb39e092e2a354f5a7d48e7f2e23b6a21b39a7a131954320d5dbeb0a419f10304fb857fae695c180f9dedd18ffa73082af5a6ca0c62c273915cd337570ecd8649157c8dc8836d758fe1e51f4faa3f", + "generatorKey": "9b4db295e88468a37e49445443fdc364321d620dc57afe8a5a14f07ce0717055" + }, + { + "address": "lsksmpgg7mo4m6ekc9tgvgjr8kh5h6wmgtqvq6776", + "name": "genesis_85", + "blsKey": "96aa1c639724f5559fb1ebbe5d218511fe0fbfe6681190cd953677c6b63c0e17ac5d9f09844845cfecbb4ab4bd5a5749", + "proofOfPossession": "82a60d6a2432fd15c7697094a89ed34a30dc2daa2b460bdb0fe3269362e1d85c79a3d2aa9ba3ffa5b1e80f983933c96f1402e95d34fb656d20f368428ba93539191319c70e6cf6f15c5cb9df9235d115d06e0e00d7a1bf64db1433ac6acb68a6", + "generatorKey": "f17b9b3bdee2ef63c8fb52d85ae07516133749a1d659bd032c3a078aca65ce7a" + }, + { + "address": "lsksu2u78jmmx7jgu3k8vxcmsv48x3746cts9xejf", + "name": "genesis_86", + "blsKey": "884b03c63f8d095165b67cb23131ca1053cbc73739549aa2ee21ca0b2b925994855dd46a81ebc3dedb309ceadd013f8e", + "proofOfPossession": "b4879cd844644b1a21f1676bf671854afb1536c5a330c1fef26b2669238efa373f70815e01028506b5cf6b75fe77e79e0efb6ef74e8111c7f1a189d4b0bf4c867190aa57e670b53dff5951a29eaaceda788ed674acdf33eff228278dc61c3cd2", + "generatorKey": "37df5572ddb12b67b9aa5191ba9baf9d76a50307fbe188924766225d86958dbd" + }, + { + "address": "lsksy7x68enrmjxjb8copn5m8csys6rjejx56pjqt", + "name": "genesis_87", + "blsKey": "8a08bdac4af80e0d37ce01094440a82a7e5ac9ec893f9a7870d26a4ec52db8932f36384bc7c3d3e03232ddb7bcd1eef5", + "proofOfPossession": "b999cf63290a85f96f0f78326c0eb24c3acce4c2307e1a2f1d621cc75f621ccab510e42aade9b6347e95661475230fbb059cd9e4e22ae17ac73dee58a370159bc6b525ab579de9502b761010e97f6d00f60ddfed05e76a5df3dfe33866c1ebe5", + "generatorKey": "7fb2d69906c5076fa314a4e817ce424bbd4a7a21305cec93a12d31a1589dc90c" + }, + { + "address": "lsksdfqvkbqpc8eczj2s3dzkxnap5pguaxdw2227r", + "name": "genesis_88", + "blsKey": "84912d2f185c2058be9ed201d970f435a408c8bb3a36c430f007b69632efb2f663b51df383be6eedb80c8768a70822bb", + "proofOfPossession": "aafdb397226d3a4a4cc3b7ac906ae7e3601310bd5d0e20a0682364312937e8e3e0c3b5846a53ee536cac2a2b3f556bff06c65ef24a32495dee9d38ee5b2012113d8f032d8dd0f3f5d9af50dbd307d0e7f66aaa165620d5292da91306b0a39aad", + "generatorKey": "21f9d60315c1baeb513b5f7324a1211723d36948b64806541b8855988f86111f" + }, + { + "address": "lskjnr8jmvz45dj9z47jbky9sadh3us3rd8tdn7ww", + "name": "genesis_89", + "blsKey": "8ce6c9d2ed4f223635e3bd85476f0d56cdbb5e4090ae22b10a7fabd08d231193cf6d9c4f5b400eb4b310ef270811e424", + "proofOfPossession": "b896aabbcc1a165adaec26feb72fc580d4a6512dd09df40b4333381d2536b5ac36d22e91469a976ae446a6291792cb6a141013baaaae12faff26d06c6a6b722a28635c72d49fcd50ac910ca01d760e80892fc5757a18597cd1ce7f16dbabd195", + "generatorKey": "25ae368be016caae7066a6ce9f2ad8e4220d328ffb860a6d275d878f4882c70c" + }, + { + "address": "lskjtc95w5wqh5gtymqh7dqadb6kbc9x2mwr4eq8d", + "name": "genesis_90", + "blsKey": "a6e64df0d2d676f272253b3def004bb87276bf239596c4a5611f911aa51c4e401a9387c299b2b2b1d3f86ad7e5db0f0a", + "proofOfPossession": "92ff87e4dfebfdee0e5572e94f62c483a9b4465eada10c3a6bed32fc92374dbbe89eed00117ddb27bfbabc5e41d90d8a0701fd215caef0233eca660d7a0bccdaf064356edaab13aff404aeb5264d8b68ab0808115e09ef541168364806a62d49", + "generatorKey": "633e1696edbd9f2eb19683c4f7e0d4686fefb1a15772a1affdeb49a44d8c04f2" + }, + { + "address": "lskjtbchucvrd2s8qjo83e7trpem5edwa6dbjfczq", + "name": "genesis_91", + "blsKey": "8c141e5d769c22ec90122f42bef1d1e7af2d94c1da6844bd313fca2ccf0543eab5f8c6752dd47969dc34613801dfb293", + "proofOfPossession": "9681aa250d714befe61d71f239a9b4c09ee102addb3a5e2c884074c7ba763b5c21e53aa7b12518d32c9b874ba1910e7a0bf0bd23ae99f57f6f464403b1151b3521a7a369ff94118a436e6aa767bd462d9ca491dd3e253862c21ff078878c354e", + "generatorKey": "3e63c0a5d4de4df114823934ceaa6c17a48e5a6650788cf1f63c826c984c0957" + }, + { + "address": "lskhbcq7mps5hhea5736qaggyupdsmgdj8ufzdojp", + "name": "genesis_92", + "blsKey": "ab0bf8a74c846dbd47c9e679ba26a9c0e5a7a5902b4f66cee7065b7487eba30262e4e5f0ee78d616d007021df3fbc945", + "proofOfPossession": "b159e28ea39b1119e4018ea19777497e1d3c4a58d1c2ecc22aa5b2efe60572cb32ff30bbeda9ce28b235fb55ab15aec206f094f37ff9a78a0931d55799c1c74a19bacfa8a4172ba078d7cad4f663a4708e47981044b1893c712c3707196451fb", + "generatorKey": "29e5cf287cb9c12b2bb77ef9dc673728132f9e3affef2d0de0d7db7905937435" + }, + { + "address": "lskhamuapyyfckyg5v8u5o4jjw9bvr5bog7rgx8an", + "name": "genesis_93", + "blsKey": "a2fc837b51e6dd740fc1530e6713b0f8c04e646e91da849517901f24d9bcc78c360223f1ad3692de2e96444008a67e03", + "proofOfPossession": "82d6fee11dc1561ffb5f36bf07acdffb95e5c329f7adc0b8937bec191350d7c4a158c7592a179ed86b9c0e20159e903100495fcd3fb5bee481e053775b232f8e0fce602e8ec6edf0fe8ba90c06e6215d7c73e88a626d2fe63c6422826489d72a", + "generatorKey": "d051790a70ffdf5bd80dc9cec003f8261128be1fc2135990accb13caeb3ed588" + }, + { + "address": "lskfx88g3826a4qsyxm4w3fheyymfnucpsq36d326", + "name": "genesis_94", + "blsKey": "93bddb296ef4dd5c832486b4603c1ed13805d2df1c6c2f95c8af4ae38467f1e741c1c2fbbd5f8e927b54250bffdf8536", + "proofOfPossession": "923415dc1db9b46715d284bd2a3f12313a24c1352bf0dfcdce2e0e0475fe0343d5cc9e463d5f04b99cb367e30e89f1371280d5897a0103658d710b07f8d9d3d8754043241a753dce60f2bdadcb9249b334e6f5a395cabfdb187f2739b512d46f", + "generatorKey": "028a30837b7eec19b02b06c3c2f4065290285e40a4870a677664fee3fe76d9be" + }, + { + "address": "lskfmufdszf9ssqghf2yjkjeetyxy4v9wgawfv725", + "name": "genesis_95", + "blsKey": "96bed36ef328566d826a6f6b874ce441ad34373487b4bcc2d48d76f2dd453e418935a7b60578c43b9c4dc954e9331a3d", + "proofOfPossession": "b4d80456953b5111777a74931f5691a6e4c0bc4f4d552aeee9ed1002903b366abab12e2d596a4387933ec676058ae64e15d7b322786d19744281028753b621ed7d49b6e6bf87983267d3208c3dc5da983d845a7a2822da4a085446172e823b28", + "generatorKey": "24bab6ba79973ffaa8569af2cb69b8495d20f0c7ce674814ee0615d31abe9607" + }, + { + "address": "lskf6f3zj4o9fnpt7wd4fowafv8buyd72sgt2864b", + "name": "genesis_96", + "blsKey": "92590fccb8c847a6957213682bb798d7d18a368515f070537e1f6cfd45d8dfc50863105db9d46189b92c0e0d009fe09d", + "proofOfPossession": "b0aa8214fd746ec04d9cc97e9641a7ad796ed12ef08c9227b5358cf3bd9f049af2ad5376055361c34d265e5d0cf3518d05113928f487bf17012d6ec4deb53e5112b72f2e4d8dc8eed4f68514a9c6bf735c9ccb9dade32ed589bea8e677135302", + "generatorKey": "8cab5125c910702b66a83240cf836b10a0f2dc3000536799300ed8f1ed9a26ac" + }, + { + "address": "lskf5sf93qyn28wfzqvr74eca3tywuuzq6xf32p7f", + "name": "genesis_97", + "blsKey": "947456674b5616341cc932afb30e42973dd17582a81e5fe958277efc828535cd7c9c778410c52e069ed23e4cf629814a", + "proofOfPossession": "872ce3383378215d3be299f32196e9cb2ae1f9e06101afbb9e7709eafb37eca8548f156bbdfbb120c2d06fdbfdf5455107f2c818bfbc9b4e9f5fb4c50f79b24f5fc84f9e137b286d71c3d588a7af684d36bf701425b25ece2d9fbacbadb58f4e", + "generatorKey": "d05b69bda8b5cd103c620a814cbab2f2a131dcfda6bd4cd568155ddb1afd423b" + }, + { + "address": "lskfowbrr5mdkenm2fcg2hhu76q3vhs74k692vv28", + "name": "genesis_98", + "blsKey": "b57835b4d3285a134730de7b29361998787c2b4853e7a5e15032b516335e81c0797a51d00e032585efa05c27d2345a1d", + "proofOfPossession": "8d9b7510b3332a22635815b809c3e1ef96427a20f15b3f41112af74a9aa1a401d83d625dc5081f51aefee7591d52afaf1451e78e4f3efe29ec171b8239af73fd87b2e8a1aaa8b701c3e5bcb0d609f098738d29e0af57ea010953297c9c9e19d9", + "generatorKey": "5812017e0d25131165ebc256f39ccece115fb58ad5fe0766f78054f912832d6c" + }, + { + "address": "lskf7a93qr84d9a6ga543wernvxbsrpvtp299c5mj", + "name": "genesis_99", + "blsKey": "a7283bff41249c3d2a0f065a27448a4c5acefaece74e51ec432c418c4bc8e6f0eb60160feec4729b9c0b933e9ec5e528", + "proofOfPossession": "86f1ac081ee08568266dc39727540a5d50f03e544f73d9a3ca60d87cfe9b6718832e07b2720d42e0e818c5fe2d45099a0774af1e6b123b41a3eb7eb3a1443d248a535fe9ef93f0027a8e8f44686dc33d677b79251c22022675395a347d0f3dbb", + "generatorKey": "ac34c0731cddab10726e634cec30294f831af045a0614733ac683ccdb6bc7eab" + }, + { + "address": "lskfjd3ymhyzedgneudo2bujnm25u7stu4qpa3jnd", + "name": "genesis_100", + "blsKey": "96a70c8b1343511359f7205313eac8c73b2838e25eda58cf8c13fa1d2689aee3df70522bcbd36e0bde958409b80cc8ee", + "proofOfPossession": "89564da089fcc38e4973cf34b5a8abbe8e822bb59f05633156d9dc0b10f2aad8d4621ea66023ec2a10d6d581927af3bc0746cd8293ea22c8db0068c127d38c4c2dcfe777ffc03e773083fd0036894cce7c2596301381941523f4f2ae97bb79e9", + "generatorKey": "326cb34aa214c4952f646d93af8cfbe58ec74db76db54484b5a23918cba8743b" + }, + { + "address": "lskffxs3orv2au2juwa69hqtrmpcg9vq78cqbdjr4", + "name": "genesis_101", + "blsKey": "a3e2b645a315827618e58c1eb66dfef3744c8111a0c7b0e8535a3ec31d78ea2630646fea1da5609988c5d88997d663fb", + "proofOfPossession": "b55d1c525f96bba45cbefbcadad16279c9f61f790dfc3e3c824003139f9994200079faf573eddb863c6ba1fd9b7d7364146e3f20579b065355c75691e06be2c7304fe48d32fbfcb5ef38f8ecaa6905e9ca6a7c1124c45a6ab2b06668cb3decc9", + "generatorKey": "4e54056fabe183ab645962cf0b70e658d0eae506c4ade8756652ca7f76733227" + }, + { + "address": "lskgn7m77b769frqvgq7uko74wcrroqtcjv7nhv95", + "name": "genesis_102", + "blsKey": "8808cb1e4cb5c8ad18ad4a45e35388af4099993effb9069a28e56c5718944a3b4010ec1ef54b4faf4814fad854322468", + "proofOfPossession": "890995fe98a83721b0069aee00c2b264239b3b833b71f64a5f48b4340a969fbac1ffc0664264fbf5af626d37fb3fe6d403dc7ef0ec195cdab82e7615d73ad7a2d326a761fdcf18a6a83efc4f502c724a10ddd89f8b6981496c34b1b32f512781", + "generatorKey": "0941ca2cfd9b1e0cc4bf0dbfd958d4b7d9f30af4c8626216999b88fc8a515d0a" + } + ], + "snapshotSubstore": { + "activeValidators": [ + { + "address": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "weight": 20 + }, + { + "address": "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "weight": 20 + }, + { + "address": "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "weight": 20 + }, + { + "address": "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "weight": 20 + }, + { + "address": "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "weight": 20 + } + ], + "threshold": 68 + } + }, + "schema": { + "$id": "/poa/genesis/genesisPoAStoreSchema", + "type": "object", + "required": ["validators", "snapshotSubstore"], + "properties": { + "validators": { + "type": "array", + "fieldNumber": 1, + "items": { + "type": "object", + "required": ["address", "name", "blsKey", "proofOfPossession", "generatorKey"], + "properties": { + "address": { + "dataType": "bytes", + "format": "lisk32", + "fieldNumber": 1 + }, + "name": { + "dataType": "string", + "minLength": 1, + "maxLength": 40, + "fieldNumber": 2 + }, + "blsKey": { + "dataType": "bytes", + "minLength": 48, + "maxLength": 48, + "fieldNumber": 3 + }, + "proofOfPossession": { + "dataType": "bytes", + "minLength": 96, + "maxLength": 96, + "fieldNumber": 4 + }, + "generatorKey": { + "dataType": "bytes", + "minLength": 32, + "maxLength": 32, + "fieldNumber": 5 + } + } + } + }, + "snapshotSubstore": { + "type": "object", + "fieldNumber": 2, + "properties": { + "activeValidators": { + "type": "array", + "fieldNumber": 1, + "items": { + "type": "object", + "required": ["address", "weight"], + "properties": { + "address": { + "dataType": "bytes", + "format": "lisk32", + "fieldNumber": 1 + }, + "weight": { + "dataType": "uint64", + "fieldNumber": 2 + } + } + }, + "minItems": 1, + "maxItems": 199 + }, + "threshold": { + "dataType": "uint64", + "fieldNumber": 2 + } + }, + "required": ["activeValidators", "threshold"] + } + } + } + } + ] +} diff --git a/examples/poa-sidechain/config/default/genesis_block.blob b/examples/poa-sidechain/config/default/genesis_block.blob new file mode 100644 index 00000000000..ba62c0c6958 Binary files /dev/null and b/examples/poa-sidechain/config/default/genesis_block.blob differ diff --git a/examples/poa-sidechain/config/default/passphrase.json b/examples/poa-sidechain/config/default/passphrase.json new file mode 100644 index 00000000000..df473b32c19 --- /dev/null +++ b/examples/poa-sidechain/config/default/passphrase.json @@ -0,0 +1,3 @@ +{ + "passphrase": "economy cliff diamond van multiply general visa picture actor teach cruel tree adjust quit maid hurry fence peace glare library curve soap cube must" +} diff --git a/examples/poa-sidechain/ecosystem.config.js b/examples/poa-sidechain/ecosystem.config.js new file mode 100644 index 00000000000..195a0ea6358 --- /dev/null +++ b/examples/poa-sidechain/ecosystem.config.js @@ -0,0 +1,35 @@ +const os = require('os'); +const path = require('path'); +const num = 10; + +const followers = new Array(num).fill(0).map((_, i) => ({ + name: `follower_${i}`, + script: './bin/run', + args: 'start --api-http --api-ws', + interpreter: 'node', + env: { + LISK_LOG_LEVEL: 'debug', + LISK_NETWORK: 'alphanet', + LISK_PORT: 7667 + i + 1, + LISK_API_WS_PORT: 7887 + i + 1, + LISK_SEED_PEERS: `127.0.0.1:7667`, + LISK_DATA_PATH: path.join(os.homedir(), '.lisk', 'ex-pos-mainchain', `follower_${i}`), + }, +})); + +module.exports = { + apps: [ + { + name: 'seed', + script: './bin/run', + args: 'start --api-http --api-ws', + interpreter: 'node', + env: { + LISK_LOG_LEVEL: 'debug', + LISK_NETWORK: 'default', + LISK_DATA_PATH: path.join(os.homedir(), '.lisk', 'ex-pos-mainchain', 'seed'), + }, + }, + ...followers, + ], +}; diff --git a/examples/poa-sidechain/jest.config.js b/examples/poa-sidechain/jest.config.js new file mode 100644 index 00000000000..766e9586f0a --- /dev/null +++ b/examples/poa-sidechain/jest.config.js @@ -0,0 +1,41 @@ +module.exports = { + testMatch: ['/test/**/?(*.)+(spec|test).+(ts|tsx|js)'], + setupFilesAfterEnv: ['/test/_setup.js'], + transform: { + '^.+\\.(ts|tsx)$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.json', + }, + ], + }, + verbose: false, + collectCoverage: false, + coverageReporters: ['json'], + coverageDirectory: '.coverage', + /** + * restoreMocks [boolean] + * + * Default: false + * + * Automatically restore mock state between every test. + * Equivalent to calling jest.restoreAllMocks() between each test. + * This will lead to any mocks having their fake implementations removed + * and restores their initial implementation. + */ + restoreMocks: true, + resetMocks: true, + + /** + * resetModules [boolean] + * + * Default: false + * + * By default, each test file gets its own independent module registry. + * Enabling resetModules goes a step further and resets the module registry before running each individual test. + * This is useful to isolate modules for every test so that local module state doesn't conflict between tests. + * This can be done programmatically using jest.resetModules(). + */ + resetModules: true, + testEnvironment: 'node', +}; diff --git a/examples/poa-sidechain/package.json b/examples/poa-sidechain/package.json new file mode 100755 index 00000000000..3138922a691 --- /dev/null +++ b/examples/poa-sidechain/package.json @@ -0,0 +1,158 @@ +{ + "name": "poa-sidechain", + "private": true, + "version": "0.1.0", + "description": "Lisk-SDK Application", + "author": "Lisk Foundation <admin@lisk.com>, lightcurve GmbH <admin@lightcurve.io>", + "license": "Apache-2.0", + "keywords": [ + "blockchain", + "lisk", + "nodejs", + "javascript", + "typescript" + ], + "homepage": "", + "repository": {}, + "engines": { + "node": ">=18.12.0 <=18", + "npm": ">=8.1.0" + }, + "main": "dist/index.js", + "scripts": { + "lint": "eslint --ext .ts .", + "lint:fix": "eslint --fix --ext .js,.ts .", + "format": "prettier --write '**/*'", + "prepack": "oclif manifest && oclif readme --multi --dir=docs/commands && npm shrinkwrap && npm prune --production && npm shrinkwrap", + "prebuild": "if test -d dist; then rm -r dist; fi; rm -f tsconfig.tsbuildinfo; rm -f npm-shrinkwrap.json", + "start": "echo Run \"./bin/run start\" to start the app", + "build": "tsc", + "test": "jest --passWithNoTests", + "test:coverage": "jest --passWithNoTests --coverage=true --coverage-reporters=text", + "test:ci": "jest --passWithNoTests --coverage=true --coverage-reporters=json", + "version": "oclif readme --multi --dir=docs/commands && git add README.md docs", + "prepublishOnly": "npm ci && npm run lint && npm run build", + "app": "./bin/run start --api-http --api-ipc", + "init-genesis": "./bin/run genesis-block:create --output config/default --assets-file ./config/default/genesis_assets.json", + "updateAuthority": "./node_modules/.bin/ts-node scripts/updateAuthority.ts" + }, + "bin": { + "poa-sidechain": "./bin/run" + }, + "lisk": { + "addressPrefix": "lsk" + }, + "oclif": { + "bin": "poa-sidechain", + "commands": "./dist/commands", + "plugins": [ + "@oclif/plugin-help", + "@oclif/plugin-autocomplete", + "@oclif/plugin-version" + ], + "additionalHelpFlags": [ + "-h" + ], + "additionalVersionFlags": [ + "-v" + ], + "topicSeparator": " ", + "topics": { + "account": { + "description": "Commands relating to poa-sidechain accounts." + }, + "block": { + "description": "Commands relating to poa-sidechain blocks." + }, + "blockchain": { + "description": "Commands relating to poa-sidechain blockchain data." + }, + "console": { + "description": "poa-sidechain interactive REPL session to run commands." + }, + "config": { + "description": "Commands relating to poa-sidechain node configuration." + }, + "endpoint": { + "description": "Commands relating to endpoints." + }, + "keys": { + "description": "Commands relating to account, generator and bls keys." + }, + "generator": { + "description": "Commands relating to poa-sidechain forging and generator-info data." + }, + "hash-onion": { + "description": "Create hash onions to be used by the forger." + }, + "system": { + "description": "Commands relating to poa-sidechain node." + }, + "passphrase": { + "description": "Commands relating to poa-sidechain passphrases." + }, + "sdk": { + "description": "Commands relating to Lisk SDK development." + }, + "transaction": { + "description": "Commands relating to poa-sidechain transactions." + } + } + }, + "files": [ + "/bin", + "/npm-shrinkwrap.json", + "/oclif.manifest.json", + "/dist", + "/config", + "/docs" + ], + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "dependencies": { + "@liskhq/lisk-framework-chain-connector-plugin": "^0.2.0-rc.0", + "@liskhq/lisk-framework-dashboard-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-faucet-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-forger-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-monitor-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.5.0-rc.0", + "@oclif/core": "1.20.4", + "@oclif/plugin-autocomplete": "1.3.6", + "@oclif/plugin-help": "5.1.19", + "@oclif/plugin-version": "1.1.3", + "axios": "1.2.0", + "fs-extra": "11.1.0", + "inquirer": "8.2.5", + "lisk-commander": "^6.1.0-rc.0", + "lisk-sdk": "^6.1.0-rc.0", + "tar": "6.1.12", + "tslib": "2.4.1" + }, + "devDependencies": { + "@types/fs-extra": "9.0.13", + "@types/jest": "29.2.3", + "@types/jest-when": "3.5.2", + "@types/node": "18.15.3", + "@types/tar": "6.1.3", + "@typescript-eslint/eslint-plugin": "5.44.0", + "@typescript-eslint/parser": "5.44.0", + "eslint": "8.28.0", + "eslint-config-lisk-base": "2.0.1", + "eslint-plugin-import": "2.26.0", + "eslint-plugin-jest": "27.1.6", + "globby": "10.0.2", + "husky": "4.2.5", + "jest": "29.3.1", + "jest-extended": "3.2.0", + "jest-when": "3.5.2", + "lint-staged": "10.2.11", + "oclif": "3.2.31", + "prettier": "2.8.0", + "ts-jest": "29.0.3", + "ts-node": "10.9.1", + "typescript": "5.0.2" + } +} diff --git a/examples/poa-sidechain/scripts/extern_types.ts b/examples/poa-sidechain/scripts/extern_types.ts new file mode 100644 index 00000000000..079c85ca4c4 --- /dev/null +++ b/examples/poa-sidechain/scripts/extern_types.ts @@ -0,0 +1,42 @@ +import { GenesisConfig } from 'lisk-sdk'; +export interface ValidatorJSON { + address: string; + bftWeight: string; + generatorKey: string; + blsKey: string; +} + +export interface NodeInfo { + readonly version: string; + readonly networkVersion: string; + readonly chainID: string; + readonly lastBlockID: string; + readonly height: number; + readonly genesisHeight: number; + readonly finalizedHeight: number; + readonly syncing: boolean; + readonly unconfirmedTransactions: number; + readonly genesis: GenesisConfig; + readonly network: { + readonly port: number; + readonly hostIp?: string; + readonly seedPeers: { + readonly ip: string; + readonly port: number; + }[]; + readonly blacklistedIPs?: string[]; + readonly fixedPeers?: string[]; + readonly whitelistedPeers?: { + readonly ip: string; + readonly port: number; + }[]; + }; +} + +export interface BFTParametersJSON { + prevoteThreshold: string; + precommitThreshold: string; + certificateThreshold: string; + validators: ValidatorJSON[]; + validatorsHash: string; +} diff --git a/examples/poa-sidechain/scripts/schema.ts b/examples/poa-sidechain/scripts/schema.ts new file mode 100644 index 00000000000..05a672db1ec --- /dev/null +++ b/examples/poa-sidechain/scripts/schema.ts @@ -0,0 +1,39 @@ +import { NUM_BYTES_ADDRESS } from 'lisk-framework/dist-node/modules/poa/constants'; + +const validator = { + type: 'object', + required: ['address', 'weight'], + properties: { + address: { + dataType: 'bytes', + minLength: NUM_BYTES_ADDRESS, + maxLength: NUM_BYTES_ADDRESS, + fieldNumber: 1, + }, + weight: { + dataType: 'uint64', + fieldNumber: 2, + }, + }, +}; + +export const updateAuthorityWithoutSigSchema = { + $id: '/poa-sidechain/command/updateAuthority', + type: 'object', + required: ['newValidators', 'threshold', 'validatorsUpdateNonce'], + properties: { + newValidators: { + type: 'array', + fieldNumber: 1, + items: validator, + }, + threshold: { + dataType: 'uint64', + fieldNumber: 2, + }, + validatorsUpdateNonce: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; diff --git a/examples/poa-sidechain/scripts/updateAuthority.json b/examples/poa-sidechain/scripts/updateAuthority.json new file mode 100644 index 00000000000..ed0b1c351f7 --- /dev/null +++ b/examples/poa-sidechain/scripts/updateAuthority.json @@ -0,0 +1,28 @@ +{ + "newValidators": [ + { + "address": "lskzbqjmwmd32sx8ya56saa4gk7tkco953btm24t8", + "weight": 20 + }, + { + "address": "lskzot8pzdcvjhpjwrhq3dkkbf499ok7mhwkrvsq3", + "weight": 20 + }, + { + "address": "lskz89nmk8tuwt93yzqm6wu2jxjdaftr9d5detn8v", + "weight": 20 + }, + { + "address": "lskx2hume2sg9grrnj94cpqkjummtz2mpcgc8dhoe", + "weight": 20 + }, + { + "address": "lskxa4895zkxjspdvu3e5eujash7okvnkkpr8xsr5", + "weight": 15 + } + ], + "threshold": "68", + "validatorsUpdateNonce": 0, + "signature": "87a20b81bdcbc7a4f228bc00894d53d55fbb2c53960f0ddc0cfa0f77395a33858a9907079773ad50a220cbdb49bc1d171250df83dd70572c4691eb280ae99d4501b289676b6bb0ad0e859b525752015bf5113e49050a8c70853470f2dd7e9344", + "aggregationBits": "1d224ad4cf64a3db52b2509c5b63365db970f34c8e09babf4af8135d9234f91f" +} diff --git a/examples/poa-sidechain/scripts/updateAuthority.ts b/examples/poa-sidechain/scripts/updateAuthority.ts new file mode 100644 index 00000000000..c1b771aa43d --- /dev/null +++ b/examples/poa-sidechain/scripts/updateAuthority.ts @@ -0,0 +1,105 @@ +import { writeFileSync, readFileSync } from 'fs-extra'; +import { codec, cryptography, apiClient } from 'lisk-sdk'; +import { NodeInfo } from './extern_types'; +import { keys as validatorsKeys } from '../config/default/dev-validators.json'; +import { MESSAGE_TAG_POA } from 'lisk-framework/dist-node/modules/poa/constants'; +import { updateAuthorityWithoutSigSchema } from './schema'; + +(async () => { + const { bls } = cryptography; + + const client = await apiClient.createIPCClient('~/.lisk/poa-sidechain'); + const nodeInfo = await client.invoke('system_getNodeInfo'); + // Get active validators from mainchain + + const paramsJSON = JSON.parse(readFileSync('./scripts/updateAuthority.json', 'utf-8')); + const genesis = JSON.parse(readFileSync('./config/default/genesis_assets.json', 'utf-8')); + + const chainID = Buffer.from(nodeInfo.chainID, 'hex'); + + const params = { + newValidators: paramsJSON.newValidators.map(validator => ({ + address: cryptography.address.getAddressFromLisk32Address(validator.address), + weight: validator.weight, + })), + threshold: paramsJSON.threshold, + validatorsUpdateNonce: paramsJSON.validatorsUpdateNonce, + }; + + const message = codec.encode(updateAuthorityWithoutSigSchema, params); + + // console.log(message); + + const snapshotSubstore = genesis.assets.filter(module => module.module === 'poa')[0].data + .snapshotSubstore; + + const activeValidatorsWithPrivateKey: { blsPublicKey: Buffer; blsPrivateKey: Buffer }[] = []; + for (const validator of snapshotSubstore.activeValidators) { + const validatorInfo = validatorsKeys.find( + configValidator => configValidator.address === validator.address, + ); + if (validatorInfo) { + activeValidatorsWithPrivateKey.push({ + blsPublicKey: Buffer.from(validatorInfo.plain.blsKey, 'hex'), + blsPrivateKey: Buffer.from(validatorInfo.plain.blsPrivateKey, 'hex'), + }); + } + } + activeValidatorsWithPrivateKey.sort((a, b) => a.blsPublicKey.compare(b.blsPublicKey)); + + const keys: Buffer[] = []; + const weights: bigint[] = []; + const validatorSignatures: { publicKey: Buffer; signature: Buffer }[] = []; + // Sign with each active validator + for (const validator of activeValidatorsWithPrivateKey) { + keys.push(validator.blsPublicKey); + weights.push(BigInt(20)); + const signature = bls.signData(MESSAGE_TAG_POA, chainID, message, validator.blsPrivateKey); + validatorSignatures.push({ publicKey: validator.blsPublicKey, signature }); + } + + const publicKeysList = activeValidatorsWithPrivateKey.map(v => v.blsPublicKey); + console.log('Total active sidechain validators:', validatorSignatures.length); + + const { aggregationBits, signature } = bls.createAggSig(publicKeysList, validatorSignatures); + + const verifyResult = bls.verifyWeightedAggSig( + // validatorsInfos.map(validatorInfo => validatorInfo.key), + // aggregationBits, + // signature, + // MESSAGE_TAG_POA, + // context.chainID, + // message, + // validatorsInfos.map(validatorInfo => validatorInfo.weight), + // snapshot0.threshold, + + keys, + aggregationBits, + signature, + MESSAGE_TAG_POA, + chainID, + message, + weights, + BigInt(68), + ); + console.log('==SIGNATURE VERIFICATION RESULT====', verifyResult); + + writeFileSync( + './updateAuthority_signed.json', + JSON.stringify({ + ...paramsJSON, + newValidators: paramsJSON.newValidators.map(validator => ({ + address: cryptography.address + .getAddressFromLisk32Address(validator.address) + .toString('hex'), + weight: validator.weight, + })), + signature: signature.toString('hex'), + aggregationBits: aggregationBits.toString('hex'), + }), + ); + + console.log('UpdateAuthority file is created at ./updateAuthority_signed successfully.'); + + process.exit(0); +})(); diff --git a/examples/poa-sidechain/scripts/updateKey.json b/examples/poa-sidechain/scripts/updateKey.json new file mode 100644 index 00000000000..b97cf124c29 --- /dev/null +++ b/examples/poa-sidechain/scripts/updateKey.json @@ -0,0 +1,3 @@ +{ + "generatorKey": "1d224ad4cf64a3db52b2509c5b63365db970f34c8e09babf4af8135d9234f91f" +} diff --git a/examples/poa-sidechain/src/app/app.ts b/examples/poa-sidechain/src/app/app.ts new file mode 100644 index 00000000000..42f6724245f --- /dev/null +++ b/examples/poa-sidechain/src/app/app.ts @@ -0,0 +1,10 @@ +import { Application, PartialApplicationConfig } from 'lisk-sdk'; +import { registerModules } from './modules'; +import { registerPlugins } from './plugins'; + +export const getApplication = (config: PartialApplicationConfig): Application => { + const app = registerModules(config); + registerPlugins(app); + + return app; +}; diff --git a/examples/poa-sidechain/src/app/index.ts b/examples/poa-sidechain/src/app/index.ts new file mode 100644 index 00000000000..ff8b4c56321 --- /dev/null +++ b/examples/poa-sidechain/src/app/index.ts @@ -0,0 +1 @@ +export default {}; diff --git a/examples/poa-sidechain/src/app/modules.ts b/examples/poa-sidechain/src/app/modules.ts new file mode 100644 index 00000000000..bc8306a7c45 --- /dev/null +++ b/examples/poa-sidechain/src/app/modules.ts @@ -0,0 +1,44 @@ +import { + Application, + AuthModule, + FeeModule, + PartialApplicationConfig, + PoAModule, + RandomModule, + RewardModule, + SidechainInteroperabilityModule, + TokenModule, + ValidatorsModule, +} from 'lisk-sdk'; + +export const registerModules = (config: PartialApplicationConfig): Application => { + const application = new Application(config); + // create module instances + const authModule = new AuthModule(); + const tokenModule = new TokenModule(); + const feeModule = new FeeModule(); + const rewardModule = new RewardModule(); + const randomModule = new RandomModule(); + const validatorModule = new ValidatorsModule(); + const poaModule = new PoAModule(); + const interoperabilityModule = new SidechainInteroperabilityModule(); + + interoperabilityModule.addDependencies(validatorModule.method, tokenModule.method); + rewardModule.addDependencies(tokenModule.method, randomModule.method); + feeModule.addDependencies(tokenModule.method, interoperabilityModule.method); + poaModule.addDependencies(validatorModule.method, feeModule.method, randomModule.method); + + interoperabilityModule.registerInteroperableModule(tokenModule); + interoperabilityModule.registerInteroperableModule(feeModule); + + // Register modules in the sequence defined in LIP0063 https://github.com/LiskHQ/lips/blob/main/proposals/lip-0063.md#modules + application.registerModule(authModule); + application.registerModule(validatorModule); + application.registerModule(tokenModule); + application.registerModule(feeModule); + application.registerModule(interoperabilityModule); + application.registerModule(poaModule); + application.registerModule(randomModule); + + return application; +}; diff --git a/examples/poa-sidechain/src/app/modules/.gitkeep b/examples/poa-sidechain/src/app/modules/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/poa-sidechain/src/app/plugins.ts b/examples/poa-sidechain/src/app/plugins.ts new file mode 100644 index 00000000000..51a493fe23b --- /dev/null +++ b/examples/poa-sidechain/src/app/plugins.ts @@ -0,0 +1,4 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { Application } from 'lisk-sdk'; + +export const registerPlugins = (_app: Application): void => {}; diff --git a/examples/poa-sidechain/src/app/plugins/.gitkeep b/examples/poa-sidechain/src/app/plugins/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/poa-sidechain/src/commands/block/get.ts b/examples/poa-sidechain/src/commands/block/get.ts new file mode 100644 index 00000000000..c1c1c385807 --- /dev/null +++ b/examples/poa-sidechain/src/commands/block/get.ts @@ -0,0 +1 @@ +export { BlockGetCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/blockchain/export.ts b/examples/poa-sidechain/src/commands/blockchain/export.ts new file mode 100644 index 00000000000..3af8131165b --- /dev/null +++ b/examples/poa-sidechain/src/commands/blockchain/export.ts @@ -0,0 +1 @@ +export { BlockchainExportCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/blockchain/hash.ts b/examples/poa-sidechain/src/commands/blockchain/hash.ts new file mode 100644 index 00000000000..d5161d903bf --- /dev/null +++ b/examples/poa-sidechain/src/commands/blockchain/hash.ts @@ -0,0 +1 @@ +export { BlockchainHashCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/blockchain/import.ts b/examples/poa-sidechain/src/commands/blockchain/import.ts new file mode 100644 index 00000000000..50faa4ad859 --- /dev/null +++ b/examples/poa-sidechain/src/commands/blockchain/import.ts @@ -0,0 +1 @@ +export { BlockchainImportCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/blockchain/reset.ts b/examples/poa-sidechain/src/commands/blockchain/reset.ts new file mode 100644 index 00000000000..3131c161f30 --- /dev/null +++ b/examples/poa-sidechain/src/commands/blockchain/reset.ts @@ -0,0 +1 @@ +export { BlockchainResetCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/config/create.ts b/examples/poa-sidechain/src/commands/config/create.ts new file mode 100644 index 00000000000..103acf9d4d3 --- /dev/null +++ b/examples/poa-sidechain/src/commands/config/create.ts @@ -0,0 +1 @@ +export { ConfigCreateCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/config/show.ts b/examples/poa-sidechain/src/commands/config/show.ts new file mode 100644 index 00000000000..3b4ad3084eb --- /dev/null +++ b/examples/poa-sidechain/src/commands/config/show.ts @@ -0,0 +1 @@ +export { ConfigShowCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/console.ts b/examples/poa-sidechain/src/commands/console.ts new file mode 100644 index 00000000000..03a4a4f200a --- /dev/null +++ b/examples/poa-sidechain/src/commands/console.ts @@ -0,0 +1 @@ +export { ConsoleCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/endpoint/invoke.ts b/examples/poa-sidechain/src/commands/endpoint/invoke.ts new file mode 100644 index 00000000000..99794488428 --- /dev/null +++ b/examples/poa-sidechain/src/commands/endpoint/invoke.ts @@ -0,0 +1 @@ +export { InvokeCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/endpoint/list.ts b/examples/poa-sidechain/src/commands/endpoint/list.ts new file mode 100755 index 00000000000..72823301bd3 --- /dev/null +++ b/examples/poa-sidechain/src/commands/endpoint/list.ts @@ -0,0 +1 @@ +export { ListCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/generator/disable.ts b/examples/poa-sidechain/src/commands/generator/disable.ts new file mode 100644 index 00000000000..5d9ed476298 --- /dev/null +++ b/examples/poa-sidechain/src/commands/generator/disable.ts @@ -0,0 +1 @@ +export { GeneratorDisableCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/generator/enable.ts b/examples/poa-sidechain/src/commands/generator/enable.ts new file mode 100644 index 00000000000..a10141e171b --- /dev/null +++ b/examples/poa-sidechain/src/commands/generator/enable.ts @@ -0,0 +1 @@ +export { GeneratorEnableCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/generator/export.ts b/examples/poa-sidechain/src/commands/generator/export.ts new file mode 100644 index 00000000000..0f2f768f74e --- /dev/null +++ b/examples/poa-sidechain/src/commands/generator/export.ts @@ -0,0 +1 @@ +export { GeneratorExportCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/generator/import.ts b/examples/poa-sidechain/src/commands/generator/import.ts new file mode 100644 index 00000000000..4028f7e37cd --- /dev/null +++ b/examples/poa-sidechain/src/commands/generator/import.ts @@ -0,0 +1 @@ +export { GeneratorImportCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/generator/status.ts b/examples/poa-sidechain/src/commands/generator/status.ts new file mode 100644 index 00000000000..31038a3dad8 --- /dev/null +++ b/examples/poa-sidechain/src/commands/generator/status.ts @@ -0,0 +1 @@ +export { GeneratorStatusCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/genesis-block/create.ts b/examples/poa-sidechain/src/commands/genesis-block/create.ts new file mode 100644 index 00000000000..ad2d13f7575 --- /dev/null +++ b/examples/poa-sidechain/src/commands/genesis-block/create.ts @@ -0,0 +1,15 @@ +import { BaseGenesisBlockCommand } from 'lisk-commander'; +import { Application, PartialApplicationConfig } from 'lisk-sdk'; +import { join } from 'path'; +import { getApplication } from '../../app/app'; + +export class GenesisBlockCommand extends BaseGenesisBlockCommand { + public getApplication(config: PartialApplicationConfig): Application { + const app = getApplication(config); + return app; + } + + public getApplicationConfigDir(): string { + return join(__dirname, '../../../config'); + } +} diff --git a/examples/poa-sidechain/src/commands/hash-onion.ts b/examples/poa-sidechain/src/commands/hash-onion.ts new file mode 100644 index 00000000000..3a96cf04cf8 --- /dev/null +++ b/examples/poa-sidechain/src/commands/hash-onion.ts @@ -0,0 +1 @@ +export { HashOnionCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/keys/create.ts b/examples/poa-sidechain/src/commands/keys/create.ts new file mode 100644 index 00000000000..7a4d6261c10 --- /dev/null +++ b/examples/poa-sidechain/src/commands/keys/create.ts @@ -0,0 +1 @@ +export { KeysCreateCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/keys/encrypt.ts b/examples/poa-sidechain/src/commands/keys/encrypt.ts new file mode 100644 index 00000000000..42ff9418f11 --- /dev/null +++ b/examples/poa-sidechain/src/commands/keys/encrypt.ts @@ -0,0 +1 @@ +export { KeysEncryptCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/keys/export.ts b/examples/poa-sidechain/src/commands/keys/export.ts new file mode 100644 index 00000000000..598306ad7f9 --- /dev/null +++ b/examples/poa-sidechain/src/commands/keys/export.ts @@ -0,0 +1 @@ +export { KeysExportCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/keys/import.ts b/examples/poa-sidechain/src/commands/keys/import.ts new file mode 100644 index 00000000000..56e53adeb30 --- /dev/null +++ b/examples/poa-sidechain/src/commands/keys/import.ts @@ -0,0 +1 @@ +export { KeysImportCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/passphrase/create.ts b/examples/poa-sidechain/src/commands/passphrase/create.ts new file mode 100644 index 00000000000..87c8db87659 --- /dev/null +++ b/examples/poa-sidechain/src/commands/passphrase/create.ts @@ -0,0 +1 @@ +export { PassphraseCreateCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/passphrase/decrypt.ts b/examples/poa-sidechain/src/commands/passphrase/decrypt.ts new file mode 100644 index 00000000000..1119f9fbfb1 --- /dev/null +++ b/examples/poa-sidechain/src/commands/passphrase/decrypt.ts @@ -0,0 +1 @@ +export { PassphraseDecryptCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/passphrase/encrypt.ts b/examples/poa-sidechain/src/commands/passphrase/encrypt.ts new file mode 100644 index 00000000000..3d614b09f95 --- /dev/null +++ b/examples/poa-sidechain/src/commands/passphrase/encrypt.ts @@ -0,0 +1 @@ +export { PassphraseEncryptCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/start.ts b/examples/poa-sidechain/src/commands/start.ts new file mode 100644 index 00000000000..0aefab8d5d3 --- /dev/null +++ b/examples/poa-sidechain/src/commands/start.ts @@ -0,0 +1,136 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +import { Flags as flagParser } from '@oclif/core'; +import { BaseStartCommand } from 'lisk-commander'; +import { Application, ApplicationConfig, PartialApplicationConfig } from 'lisk-sdk'; +import { ForgerPlugin } from '@liskhq/lisk-framework-forger-plugin'; +import { MonitorPlugin } from '@liskhq/lisk-framework-monitor-plugin'; +import { ReportMisbehaviorPlugin } from '@liskhq/lisk-framework-report-misbehavior-plugin'; +import { DashboardPlugin } from '@liskhq/lisk-framework-dashboard-plugin'; +import { FaucetPlugin } from '@liskhq/lisk-framework-faucet-plugin'; +import { ChainConnectorPlugin } from '@liskhq/lisk-framework-chain-connector-plugin'; +import { join } from 'path'; +import { getApplication } from '../app/app'; + +interface Flags { + [key: string]: string | number | boolean | undefined; +} + +const setPluginConfig = (config: ApplicationConfig, flags: Flags): void => { + if (flags['monitor-plugin-port'] !== undefined) { + config.plugins[MonitorPlugin.name] = config.plugins[MonitorPlugin.name] ?? {}; + config.plugins[MonitorPlugin.name].port = flags['monitor-plugin-port']; + } + if ( + flags['monitor-plugin-whitelist'] !== undefined && + typeof flags['monitor-plugin-whitelist'] === 'string' + ) { + config.plugins[MonitorPlugin.name] = config.plugins[MonitorPlugin.name] ?? {}; + config.plugins[MonitorPlugin.name].whiteList = flags['monitor-plugin-whitelist'] + .split(',') + .filter(Boolean); + } + if (flags['faucet-plugin-port'] !== undefined) { + config.plugins[FaucetPlugin.name] = config.plugins[FaucetPlugin.name] ?? {}; + config.plugins[FaucetPlugin.name].port = flags['faucet-plugin-port']; + } + if (flags['dashboard-plugin-port'] !== undefined) { + config.plugins[DashboardPlugin.name] = config.plugins[DashboardPlugin.name] ?? {}; + config.plugins[DashboardPlugin.name].port = flags['dashboard-plugin-port']; + } +}; + +export class StartCommand extends BaseStartCommand { + static flags: any = { + ...BaseStartCommand.flags, + 'enable-forger-plugin': flagParser.boolean({ + description: + 'Enable Forger Plugin. Environment variable "LISK_ENABLE_FORGER_PLUGIN" can also be used.', + env: 'LISK_ENABLE_FORGER_PLUGIN', + default: false, + }), + 'enable-monitor-plugin': flagParser.boolean({ + description: + 'Enable Monitor Plugin. Environment variable "LISK_ENABLE_MONITOR_PLUGIN" can also be used.', + env: 'LISK_ENABLE_MONITOR_PLUGIN', + default: false, + }), + 'monitor-plugin-port': flagParser.integer({ + description: + 'Port to be used for Monitor Plugin. Environment variable "LISK_MONITOR_PLUGIN_PORT" can also be used.', + env: 'LISK_MONITOR_PLUGIN_PORT', + dependsOn: ['enable-monitor-plugin'], + }), + 'monitor-plugin-whitelist': flagParser.string({ + description: + 'List of IPs in comma separated value to allow the connection. Environment variable "LISK_MONITOR_PLUGIN_WHITELIST" can also be used.', + env: 'LISK_MONITOR_PLUGIN_WHITELIST', + dependsOn: ['enable-monitor-plugin'], + }), + 'enable-report-misbehavior-plugin': flagParser.boolean({ + description: + 'Enable ReportMisbehavior Plugin. Environment variable "LISK_ENABLE_REPORT_MISBEHAVIOR_PLUGIN" can also be used.', + env: 'LISK_ENABLE_MONITOR_PLUGIN', + default: false, + }), + 'enable-faucet-plugin': flagParser.boolean({ + description: + 'Enable Faucet Plugin. Environment variable "LISK_ENABLE_FAUCET_PLUGIN" can also be used.', + env: 'LISK_ENABLE_FAUCET_PLUGIN', + default: false, + }), + 'faucet-plugin-port': flagParser.integer({ + description: + 'Port to be used for Faucet Plugin. Environment variable "LISK_FAUCET_PLUGIN_PORT" can also be used.', + env: 'LISK_FAUCET_PLUGIN_PORT', + dependsOn: ['enable-faucet-plugin'], + }), + 'enable-dashboard-plugin': flagParser.boolean({ + description: + 'Enable Dashboard Plugin. Environment variable "LISK_ENABLE_DASHBOARD_PLUGIN" can also be used.', + env: 'LISK_ENABLE_DASHBOARD_PLUGIN', + default: false, + }), + 'dashboard-plugin-port': flagParser.integer({ + description: + 'Port to be used for Dashboard Plugin. Environment variable "LISK_DASHBOARD_PLUGIN_PORT" can also be used.', + env: 'LISK_DASHBOARD_PLUGIN_PORT', + dependsOn: ['enable-dashboard-plugin'], + }), + }; + + public async getApplication(config: PartialApplicationConfig): Promise { + /* eslint-disable @typescript-eslint/no-unsafe-call */ + const { flags } = await this.parse(StartCommand); + // Set Plugins Config + setPluginConfig(config as ApplicationConfig, flags); + const app = getApplication(config); + + if (flags['enable-forger-plugin']) { + app.registerPlugin(new ForgerPlugin(), { loadAsChildProcess: true }); + } + if (flags['enable-monitor-plugin']) { + app.registerPlugin(new MonitorPlugin(), { loadAsChildProcess: true }); + } + if (flags['enable-report-misbehavior-plugin']) { + app.registerPlugin(new ReportMisbehaviorPlugin(), { loadAsChildProcess: true }); + } + if (flags['enable-faucet-plugin']) { + app.registerPlugin(new FaucetPlugin(), { loadAsChildProcess: true }); + } + if (flags['enable-dashboard-plugin']) { + app.registerPlugin(new DashboardPlugin(), { loadAsChildProcess: true }); + } + if (flags['enable-chain-connector-plugin']) { + app.registerPlugin(new ChainConnectorPlugin(), { loadAsChildProcess: true }); + } + + return app; + } + + public getApplicationConfigDir(): string { + return join(__dirname, '../../config'); + } +} diff --git a/examples/poa-sidechain/src/commands/system/metadata.ts b/examples/poa-sidechain/src/commands/system/metadata.ts new file mode 100644 index 00000000000..e3f72a6982e --- /dev/null +++ b/examples/poa-sidechain/src/commands/system/metadata.ts @@ -0,0 +1 @@ +export { NodeMetadataCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/system/node-info.ts b/examples/poa-sidechain/src/commands/system/node-info.ts new file mode 100644 index 00000000000..5b44ac03ce9 --- /dev/null +++ b/examples/poa-sidechain/src/commands/system/node-info.ts @@ -0,0 +1 @@ +export { NodeInfoCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/transaction/create.ts b/examples/poa-sidechain/src/commands/transaction/create.ts new file mode 100644 index 00000000000..4c7cbb76768 --- /dev/null +++ b/examples/poa-sidechain/src/commands/transaction/create.ts @@ -0,0 +1,22 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +import { TransactionCreateCommand } from 'lisk-commander'; +import { Application, PartialApplicationConfig } from 'lisk-sdk'; +import { getApplication } from '../../app/app'; + +type CreateFlags = typeof TransactionCreateCommand.flags & { + [key: string]: Record; +}; + +export class CreateCommand extends TransactionCreateCommand { + static flags: CreateFlags = { + ...TransactionCreateCommand.flags, + }; + + static args = [...TransactionCreateCommand.args]; + + public getApplication(config: PartialApplicationConfig): Application { + const app = getApplication(config); + return app; + } +} diff --git a/examples/poa-sidechain/src/commands/transaction/get.ts b/examples/poa-sidechain/src/commands/transaction/get.ts new file mode 100644 index 00000000000..a537b15ae8b --- /dev/null +++ b/examples/poa-sidechain/src/commands/transaction/get.ts @@ -0,0 +1 @@ +export { TransactionGetCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/transaction/send.ts b/examples/poa-sidechain/src/commands/transaction/send.ts new file mode 100644 index 00000000000..e6754332cd8 --- /dev/null +++ b/examples/poa-sidechain/src/commands/transaction/send.ts @@ -0,0 +1 @@ +export { TransactionSendCommand } from 'lisk-commander'; diff --git a/examples/poa-sidechain/src/commands/transaction/sign.ts b/examples/poa-sidechain/src/commands/transaction/sign.ts new file mode 100644 index 00000000000..b55093102a7 --- /dev/null +++ b/examples/poa-sidechain/src/commands/transaction/sign.ts @@ -0,0 +1,20 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +import { TransactionSignCommand } from 'lisk-commander'; +import { Application, PartialApplicationConfig } from 'lisk-sdk'; +import { getApplication } from '../../app/app'; + +type SignFlags = typeof TransactionSignCommand.flags & { [key: string]: Record }; + +export class SignCommand extends TransactionSignCommand { + static flags: SignFlags = { + ...TransactionSignCommand.flags, + }; + + static args = [...TransactionSignCommand.args]; + + public getApplication(config: PartialApplicationConfig): Application { + const app = getApplication(config); + return app; + } +} diff --git a/examples/poa-sidechain/test/.eslintrc.js b/examples/poa-sidechain/test/.eslintrc.js new file mode 100644 index 00000000000..f93c4465d72 --- /dev/null +++ b/examples/poa-sidechain/test/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['lisk-base/ts-jest'], + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, +}; diff --git a/examples/poa-sidechain/test/_setup.js b/examples/poa-sidechain/test/_setup.js new file mode 100644 index 00000000000..aab218d192e --- /dev/null +++ b/examples/poa-sidechain/test/_setup.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const matchers = require('jest-extended'); + +expect.extend(matchers); diff --git a/examples/poa-sidechain/test/integration/.gitkeep b/examples/poa-sidechain/test/integration/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/poa-sidechain/test/network/.gitkeep b/examples/poa-sidechain/test/network/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/poa-sidechain/test/tsconfig.json b/examples/poa-sidechain/test/tsconfig.json new file mode 100644 index 00000000000..c0c763f8f4d --- /dev/null +++ b/examples/poa-sidechain/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "../" + }, + "include": ["../src/**/*", "./**/*", "../package.json", "../config/**/*.json"] +} diff --git a/examples/poa-sidechain/test/types.ts b/examples/poa-sidechain/test/types.ts new file mode 100644 index 00000000000..deff13c121c --- /dev/null +++ b/examples/poa-sidechain/test/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + * + */ + +export type Awaited = T extends Promise ? U : T; diff --git a/examples/poa-sidechain/test/unit/modules/.gitkeep b/examples/poa-sidechain/test/unit/modules/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/poa-sidechain/test/utils/config.ts b/examples/poa-sidechain/test/utils/config.ts new file mode 100644 index 00000000000..5b37b43e3d9 --- /dev/null +++ b/examples/poa-sidechain/test/utils/config.ts @@ -0,0 +1,10 @@ +import { Config } from '@oclif/core'; + +import pJSON = require('../../package.json'); + +export const getConfig = async () => { + const config = await Config.load(); + config.pjson.lisk = { addressPrefix: 'lsk' }; + config.pjson.version = pJSON.version; + return config; +}; diff --git a/examples/poa-sidechain/tsconfig.json b/examples/poa-sidechain/tsconfig.json new file mode 100644 index 00000000000..42faa2a792d --- /dev/null +++ b/examples/poa-sidechain/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "target": "es2019", + "module": "commonjs", + "moduleResolution": "node", + "newLine": "lf", + "importHelpers": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "pretty": true, + "removeComments": true, + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "composite": true, + "declaration": true, + "noImplicitAny": false, + "skipLibCheck": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*.ts", "./src/**/*.json"] +} diff --git a/examples/pos-mainchain/.eslintignore b/examples/pos-mainchain/.eslintignore index 00a15e70c20..eee8bf98e19 100644 --- a/examples/pos-mainchain/.eslintignore +++ b/examples/pos-mainchain/.eslintignore @@ -12,3 +12,4 @@ scripts config test/_setup.js ecosystem.config.js +src/app/app.ts diff --git a/examples/pos-mainchain/config/default/genesis_assets.json b/examples/pos-mainchain/config/default/genesis_assets.json index d575139c811..b92a196593d 100644 --- a/examples/pos-mainchain/config/default/genesis_assets.json +++ b/examples/pos-mainchain/config/default/genesis_assets.json @@ -1041,6 +1041,93 @@ } } }, + { + "module": "nft", + "data": { + "nftSubstore": [], + "supportedNFTsSubstore": [] + }, + "schema": { + "$id": "/nft/module/genesis", + "type": "object", + "required": ["nftSubstore", "supportedNFTsSubstore"], + "properties": { + "nftSubstore": { + "type": "array", + "fieldNumber": 1, + "items": { + "type": "object", + "required": ["nftID", "owner", "attributesArray"], + "properties": { + "nftID": { + "dataType": "bytes", + "minLength": 16, + "maxLength": 16, + "fieldNumber": 1 + }, + "owner": { + "dataType": "bytes", + "fieldNumber": 2 + }, + "attributesArray": { + "type": "array", + "fieldNumber": 3, + "items": { + "type": "object", + "required": ["module", "attributes"], + "properties": { + "module": { + "dataType": "string", + "minLength": 1, + "maxLength": 32, + "pattern": "^[a-zA-Z0-9]*$", + "fieldNumber": 1 + }, + "attributes": { + "dataType": "bytes", + "fieldNumber": 2 + } + } + } + } + } + } + }, + "supportedNFTsSubstore": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["chainID", "supportedCollectionIDArray"], + "properties": { + "chainID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + }, + "supportedCollectionIDArray": { + "type": "array", + "fieldNumber": 2, + "items": { + "type": "object", + "required": ["collectionID"], + "properties": { + "collectionID": { + "dataType": "bytes", + "minLength": 4, + "maxLength": 4, + "fieldNumber": 1 + } + } + } + } + } + } + } + } + } + }, { "module": "pos", "data": { diff --git a/examples/pos-mainchain/config/default/genesis_block.blob b/examples/pos-mainchain/config/default/genesis_block.blob index bce45900c87..04137f4422a 100644 Binary files a/examples/pos-mainchain/config/default/genesis_block.blob and b/examples/pos-mainchain/config/default/genesis_block.blob differ diff --git a/examples/pos-mainchain/package.json b/examples/pos-mainchain/package.json index 1fe23bb0767..f6d41b26d05 100755 --- a/examples/pos-mainchain/package.json +++ b/examples/pos-mainchain/package.json @@ -114,11 +114,13 @@ } }, "dependencies": { - "@liskhq/lisk-framework-dashboard-plugin": "^0.3.0-rc.0", - "@liskhq/lisk-framework-faucet-plugin": "^0.3.0-rc.0", - "@liskhq/lisk-framework-forger-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-monitor-plugin": "^0.4.0-rc.0", - "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-chain-connector-plugin": "^0.2.0-rc.0", + "@liskhq/lisk-framework-dashboard-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-faucet-plugin": "^0.4.0-rc.0", + "@liskhq/lisk-framework-forger-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-monitor-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-framework-report-misbehavior-plugin": "^0.5.0-rc.0", + "@liskhq/lisk-validator": "^0.8.0-rc.0", "@oclif/core": "1.20.4", "@oclif/plugin-autocomplete": "1.3.6", "@oclif/plugin-help": "5.1.19", @@ -126,8 +128,8 @@ "axios": "1.2.0", "fs-extra": "11.1.0", "inquirer": "8.2.5", - "lisk-commander": "^6.0.0-rc.0", - "lisk-sdk": "^6.0.0-rc.0", + "lisk-commander": "^6.1.0-rc.0", + "lisk-sdk": "^6.1.0-rc.0", "tar": "6.1.12", "tslib": "2.4.1" }, diff --git a/examples/pos-mainchain/src/app/app.ts b/examples/pos-mainchain/src/app/app.ts index d4c1f2407cb..6a99bf61049 100644 --- a/examples/pos-mainchain/src/app/app.ts +++ b/examples/pos-mainchain/src/app/app.ts @@ -1,11 +1,19 @@ -import { Application, PartialApplicationConfig } from 'lisk-sdk'; -import { registerModules } from './modules'; -import { registerPlugins } from './plugins'; +import { Application, PartialApplicationConfig, NFTModule } from 'lisk-sdk'; +import { TestNftModule } from './modules/testNft/module'; export const getApplication = (config: PartialApplicationConfig): Application => { - const { app } = Application.defaultApplication(config, true); - registerModules(app); - registerPlugins(app); + const { app, method } = Application.defaultApplication(config, true); + const nftModule = new NFTModule(); + const testNftModule = new TestNftModule(); + const interoperabilityModule = app['_registeredModules'].find( + mod => mod.name === 'interoperability', + ); + interoperabilityModule.registerInteroperableModule(nftModule); + nftModule.addDependencies(method.interoperability, method.fee, method.token); + testNftModule.addDependencies(nftModule.method); + + app.registerModule(nftModule); + app.registerModule(testNftModule); return app; }; diff --git a/examples/pos-mainchain/src/app/modules/testNft/commands/destroy_nft.ts b/examples/pos-mainchain/src/app/modules/testNft/commands/destroy_nft.ts new file mode 100644 index 00000000000..822ad1b174f --- /dev/null +++ b/examples/pos-mainchain/src/app/modules/testNft/commands/destroy_nft.ts @@ -0,0 +1,36 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCommand, CommandExecuteContext, NFTMethod } from 'lisk-sdk'; +import { destroyNftParamsSchema } from '../schema'; + +interface Params { + address: Buffer; + nftID: Buffer; +} + +export class DestroyNftCommand extends BaseCommand { + private _nftMethod!: NFTMethod; + public schema = destroyNftParamsSchema; + + public init(args: { nftMethod: NFTMethod }): void { + this._nftMethod = args.nftMethod; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + await this._nftMethod.destroy(context.getMethodContext(), params.address, params.nftID); + } +} diff --git a/examples/pos-mainchain/src/app/modules/testNft/commands/mint_nft.ts b/examples/pos-mainchain/src/app/modules/testNft/commands/mint_nft.ts new file mode 100644 index 00000000000..bc5638846d4 --- /dev/null +++ b/examples/pos-mainchain/src/app/modules/testNft/commands/mint_nft.ts @@ -0,0 +1,43 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCommand, CommandExecuteContext, NFTMethod } from 'lisk-sdk'; +import { NFTAttributes } from '../types'; +import { mintNftParamsSchema } from '../schema'; + +interface Params { + address: Buffer; + collectionID: Buffer; + attributesArray: NFTAttributes[]; +} + +export class MintNftCommand extends BaseCommand { + private _nftMethod!: NFTMethod; + public schema = mintNftParamsSchema; + + public init(args: { nftMethod: NFTMethod }): void { + this._nftMethod = args.nftMethod; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + await this._nftMethod.create( + context.getMethodContext(), + params.address, + params.collectionID, + params.attributesArray, + ); + } +} diff --git a/examples/pos-mainchain/src/app/modules/testNft/constants.ts b/examples/pos-mainchain/src/app/modules/testNft/constants.ts new file mode 100644 index 00000000000..a0150fad36f --- /dev/null +++ b/examples/pos-mainchain/src/app/modules/testNft/constants.ts @@ -0,0 +1,18 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export const LENGTH_COLLECTION_ID = 4; +export const MIN_LENGTH_MODULE_NAME = 1; +export const MAX_LENGTH_MODULE_NAME = 32; +export const LENGTH_NFT_ID = 16; diff --git a/examples/pos-mainchain/src/app/modules/testNft/endpoint.ts b/examples/pos-mainchain/src/app/modules/testNft/endpoint.ts new file mode 100644 index 00000000000..1d091013741 --- /dev/null +++ b/examples/pos-mainchain/src/app/modules/testNft/endpoint.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEndpoint } from 'lisk-sdk'; + +export class TestNftEndpoint extends BaseEndpoint {} diff --git a/examples/pos-mainchain/src/app/modules/testNft/method.ts b/examples/pos-mainchain/src/app/modules/testNft/method.ts new file mode 100644 index 00000000000..5bab789e7f1 --- /dev/null +++ b/examples/pos-mainchain/src/app/modules/testNft/method.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseMethod } from 'lisk-sdk'; + +export class TestNftMethod extends BaseMethod {} diff --git a/examples/pos-mainchain/src/app/modules/testNft/module.ts b/examples/pos-mainchain/src/app/modules/testNft/module.ts new file mode 100644 index 00000000000..a228abff3af --- /dev/null +++ b/examples/pos-mainchain/src/app/modules/testNft/module.ts @@ -0,0 +1,56 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseModule, ModuleInitArgs, ModuleMetadata, NFTMethod } from 'lisk-sdk'; +import { TestNftEndpoint } from './endpoint'; +import { TestNftMethod } from './method'; +import { MintNftCommand } from './commands/mint_nft'; +import { DestroyNftCommand } from './commands/destroy_nft'; + +export class TestNftModule extends BaseModule { + public endpoint = new TestNftEndpoint(this.stores, this.offchainStores); + public method = new TestNftMethod(this.stores, this.events); + public mintNftCommand = new MintNftCommand(this.stores, this.events); + public destroyNftCommand = new DestroyNftCommand(this.stores, this.events); + public commands = [this.mintNftCommand, this.destroyNftCommand]; + + private _nftMethod!: NFTMethod; + + public addDependencies(nftMethod: NFTMethod) { + this._nftMethod = nftMethod; + } + + public metadata(): ModuleMetadata { + return { + ...this.baseMetadata(), + endpoints: [], + commands: this.commands.map(command => ({ + name: command.name, + params: command.schema, + })), + events: [], + assets: [], + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async init(_args: ModuleInitArgs) { + this.mintNftCommand.init({ + nftMethod: this._nftMethod, + }); + this.destroyNftCommand.init({ + nftMethod: this._nftMethod, + }); + } +} diff --git a/examples/pos-mainchain/src/app/modules/testNft/schema.ts b/examples/pos-mainchain/src/app/modules/testNft/schema.ts new file mode 100644 index 00000000000..c183e9fb8ff --- /dev/null +++ b/examples/pos-mainchain/src/app/modules/testNft/schema.ts @@ -0,0 +1,79 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { + LENGTH_COLLECTION_ID, + LENGTH_NFT_ID, + MAX_LENGTH_MODULE_NAME, + MIN_LENGTH_MODULE_NAME, +} from './constants'; + +export const mintNftParamsSchema = { + $id: '/lisk/nftTransferParams', + type: 'object', + required: ['address', 'collectionID', 'attributesArray'], + properties: { + address: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + collectionID: { + dataType: 'bytes', + minLength: LENGTH_COLLECTION_ID, + maxLength: LENGTH_COLLECTION_ID, + fieldNumber: 2, + }, + attributesArray: { + type: 'array', + fieldNumber: 4, + items: { + type: 'object', + required: ['module', 'attributes'], + properties: { + module: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + pattern: '^[a-zA-Z0-9]*$', + fieldNumber: 1, + }, + attributes: { + dataType: 'bytes', + fieldNumber: 2, + }, + }, + }, + }, + }, +}; + +export const destroyNftParamsSchema = { + $id: '/lisk/nftDestroyParams', + type: 'object', + required: ['address', 'nftID'], + properties: { + address: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 2, + }, + }, +}; diff --git a/examples/pos-mainchain/src/app/modules/testNft/types.ts b/examples/pos-mainchain/src/app/modules/testNft/types.ts new file mode 100644 index 00000000000..8d1af2d969a --- /dev/null +++ b/examples/pos-mainchain/src/app/modules/testNft/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export interface NFTAttributes { + module: string; + attributes: Buffer; +} diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/README.md b/framework-plugins/lisk-framework-chain-connector-plugin/README.md new file mode 100644 index 00000000000..475e243a079 --- /dev/null +++ b/framework-plugins/lisk-framework-chain-connector-plugin/README.md @@ -0,0 +1,78 @@ +# @liskhq/lisk-framework-chain-connector-plugin + +@liskhq/lisk-framework-chain-connector-plugin is a plugin for creating and sending Cross-Chain-Update (CCU) Transactions. + +Cross-chain update transactions are the carriers of the information transmitted between chains. By posting a cross-chain update, the receiving chain gets the information required about the advancement of the sending chain. The transaction can also include cross-chain messages and thus serves as an envelope for messages from one chain to another. + +## Installation + +```sh +$ npm install --save @liskhq/lisk-framework-chain-connector-plugin +``` + +## Config Options + +``` +{ + receivingChainID: string, + receivingChainWsURL?: string, + receivingChainIPCPath?: string, + ccuFrequency: number, + encryptedPrivateKey: string, + ccuFee: string, + isSaveCCU: boolean, + maxCCUSize: number, + registrationHeight: number, + ccuSaveLimit: number +} +``` + +## Parameters + +| Param | Required? | Description | +| ----------------------- | --------- | ------------------------------------------------------------------------------ | +| `receivingChainID` | **Y** | Chain ID of the receiving chain | +| `receivingChainWsURL` | **N** | The WS url of a receiving node | +| `receivingChainIPCPath` | **N** | The IPC path of a receiving node | +| `ccuFrequency` | **Y** | Number of blocks after which a CCU should be created | +| `encryptedPrivateKey` | **Y** | Encrypted privateKey of the relayer | +| `ccuFee` | **Y** | Fee to be paid for each CCU transaction | +| `isSaveCCU` | **Y** | Flag for the user to either save or send a CCU on creation. Send is by default | +| `maxCCUSize` | **Y** | Maximum size of CCU to be allowed | +| `registrationHeight` | **Y** | Height at the time of registration on the receiving chain | +| `ccuSaveLimit` | **Y** | Number of CCUs to save | + +## Usage + +Start your Lisk SDK with `--enable-chain-connector-plugin` flag, i.e. + +```sh + $ ./bin/run start --enable-chain-connector-plugin +``` + +## Documentation + +[Setting up a relayer node](https://lisk.com/documentation/beta/run-blockchain/setup-relayer.html#installing-the-chain-connector-plugin): Details SDK Doc for setting up node with Chain Connector Plugin. + +[LIP-53 # CCU Properties](https://github.com/LiskHQ/lips/blob/main/proposals/lip-0053.md#cross-chain-update-transaction-properties): Explaination of CCU Properties from LIP-53. + +[Interoperability Example](https://github.com/LiskHQ/lisk-sdk/tree/release/6.1.0/examples/interop): Example of Interoperability with 2 sidechains and 1 mainchain, Chain Connector Plugin enabled. + +## License + +Copyright 2016-2023 Lisk Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +[lisk core github]: https://github.com/LiskHQ/lisk +[lisk documentation site]: https://lisk.com/documentation/lisk-sdk/v6/references/typedoc/modules/_liskhq_lisk_framework_chain_connector_plugin.html diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/package.json b/framework-plugins/lisk-framework-chain-connector-plugin/package.json index 809c64596f3..44e38d5e9d3 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/package.json +++ b/framework-plugins/lisk-framework-chain-connector-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-framework-chain-connector-plugin", - "version": "0.1.0-rc.5", + "version": "0.2.0-rc.0", "description": "A plugin used by a relayer node to automatically create and submit Cross Chain Transaction by aggregating off-chain information of a chain", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "dependencies": { "debug": "4.3.4", "fs-extra": "11.1.0", - "lisk-sdk": "^6.0.0-rc.5" + "lisk-sdk": "^6.1.0-rc.0" }, "devDependencies": { "@types/jest": "29.2.3", diff --git a/framework-plugins/lisk-framework-chain-connector-plugin/src/utils.ts b/framework-plugins/lisk-framework-chain-connector-plugin/src/utils.ts index 780a4cdc071..b1f223b9778 100644 --- a/framework-plugins/lisk-framework-chain-connector-plugin/src/utils.ts +++ b/framework-plugins/lisk-framework-chain-connector-plugin/src/utils.ts @@ -45,7 +45,7 @@ interface BFTParametersWithoutGeneratorKey extends Omit { - const networkID = chainID.slice(0, 1); + const networkID = chainID.subarray(0, 1); // 3 bytes for remaining chainID bytes return Buffer.concat([networkID, Buffer.alloc(CHAIN_ID_LENGTH - 1, 0)]); }; diff --git a/framework-plugins/lisk-framework-dashboard-plugin/package.json b/framework-plugins/lisk-framework-dashboard-plugin/package.json index 7712fe0b35b..fa58c3f2114 100644 --- a/framework-plugins/lisk-framework-dashboard-plugin/package.json +++ b/framework-plugins/lisk-framework-dashboard-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-framework-dashboard-plugin", - "version": "0.3.0-rc.5", + "version": "0.4.0-rc.0", "description": "A plugin for interacting with a newly developed blockchain application.", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -46,10 +46,10 @@ }, "dependencies": { "@csstools/normalize.css": "12.0.0", - "@liskhq/lisk-client": "^6.0.0-rc.4", + "@liskhq/lisk-client": "^6.1.0-rc.0", "express": "4.18.2", "json-format-highlight": "1.0.4", - "lisk-sdk": "^6.0.0-rc.5", + "lisk-sdk": "^6.1.0-rc.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", diff --git a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/CopiableText/CopiableText.module.scss b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/CopiableText/CopiableText.module.scss index eb1d03e1577..41707b21572 100644 --- a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/CopiableText/CopiableText.module.scss +++ b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/CopiableText/CopiableText.module.scss @@ -5,6 +5,7 @@ } .copyText { + white-space: nowrap; overflow-wrap: unset !important; overflow: hidden; text-overflow: ellipsis; diff --git a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/dialogs/AccountDialog.tsx b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/dialogs/AccountDialog.tsx index 943ea324025..7f6591169f8 100644 --- a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/dialogs/AccountDialog.tsx +++ b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/dialogs/AccountDialog.tsx @@ -40,13 +40,13 @@ const AccountDialog: React.FC = props => { Lisk32 address - {account.address} + Public Key - {account.publicKey} + @@ -54,7 +54,7 @@ const AccountDialog: React.FC = props => { Passphrase - {account.passphrase} + diff --git a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/BlockWidget.tsx b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/BlockWidget.tsx index 9b1a5c9c059..7bbf24c6702 100644 --- a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/BlockWidget.tsx +++ b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/BlockWidget.tsx @@ -54,12 +54,10 @@ const BlockWidget: React.FC = props => { {blocks.map(block => ( - {block.header.id} + - - {block.header.generatorAddress} - + {block.header.height} diff --git a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/MyAccountWidget.tsx b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/MyAccountWidget.tsx index 9fdc7078fc2..a1349d2703c 100644 --- a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/MyAccountWidget.tsx +++ b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/MyAccountWidget.tsx @@ -44,21 +44,27 @@ const MyAccountWidget: React.FC = props => { - Binary addresss + Lisk32 address Public Key + + Passphrase + {accounts.map((account: Account) => ( handleClick(account)} key={account.address}> - {account.address} + + + + - {account.publicKey} + ))} diff --git a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/TransactionWidget.tsx b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/TransactionWidget.tsx index ff11d903e5c..1e3529fb98c 100644 --- a/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/TransactionWidget.tsx +++ b/framework-plugins/lisk-framework-dashboard-plugin/src/ui/components/widgets/TransactionWidget.tsx @@ -77,12 +77,10 @@ const TransactionWidget: React.FC = props => { {transactions.map(transaction => ( - {transaction.id} + - - {transaction.senderPublicKey} - + diff --git a/framework-plugins/lisk-framework-faucet-plugin/package.json b/framework-plugins/lisk-framework-faucet-plugin/package.json index 8658e9b788b..671d42c727f 100644 --- a/framework-plugins/lisk-framework-faucet-plugin/package.json +++ b/framework-plugins/lisk-framework-faucet-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-framework-faucet-plugin", - "version": "0.3.0-rc.5", + "version": "0.4.0-rc.0", "description": "A plugin for distributing testnet tokens from a newly developed blockchain application.", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -47,15 +47,15 @@ }, "dependencies": { "@csstools/normalize.css": "12.0.0", - "@liskhq/lisk-api-client": "^6.0.0-rc.4", - "@liskhq/lisk-client": "^6.0.0-rc.4", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-transactions": "^6.0.0-rc.2", + "@liskhq/lisk-api-client": "^6.1.0-rc.0", + "@liskhq/lisk-client": "^6.1.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-transactions": "^6.1.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", - "@liskhq/lisk-validator": "^0.8.0-rc.2", + "@liskhq/lisk-validator": "^0.9.0-rc.0", "axios": "1.2.0", "express": "4.18.2", - "lisk-sdk": "^6.0.0-rc.5", + "lisk-sdk": "^6.1.0-rc.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0" diff --git a/framework-plugins/lisk-framework-forger-plugin/package.json b/framework-plugins/lisk-framework-forger-plugin/package.json index ff624cff1b3..97ccbea5c66 100644 --- a/framework-plugins/lisk-framework-forger-plugin/package.json +++ b/framework-plugins/lisk-framework-forger-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-framework-forger-plugin", - "version": "0.4.0-rc.5", + "version": "0.5.0-rc.0", "description": "A plugin for lisk-framework that monitors configured validators forging activity and stakers information.", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -40,10 +40,10 @@ "dependencies": { "debug": "4.3.4", "fs-extra": "11.1.0", - "lisk-sdk": "^6.0.0-rc.5" + "lisk-sdk": "^6.1.0-rc.0" }, "devDependencies": { - "@liskhq/lisk-api-client": "^6.0.0-rc.4", + "@liskhq/lisk-api-client": "^6.1.0-rc.0", "@types/debug": "4.1.5", "@types/jest": "29.2.3", "@types/jest-when": "3.5.2", diff --git a/framework-plugins/lisk-framework-monitor-plugin/package.json b/framework-plugins/lisk-framework-monitor-plugin/package.json index 8b120f46ae5..37e8b420c2f 100644 --- a/framework-plugins/lisk-framework-monitor-plugin/package.json +++ b/framework-plugins/lisk-framework-monitor-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-framework-monitor-plugin", - "version": "0.4.0-rc.5", + "version": "0.5.0-rc.0", "description": "A plugin for lisk-framework that provides network statistics of the running node", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -40,7 +40,7 @@ "express": "4.18.2", "express-rate-limit": "6.7.0", "ip": "1.1.5", - "lisk-sdk": "^6.0.0-rc.5" + "lisk-sdk": "^6.1.0-rc.0" }, "devDependencies": { "@types/cors": "2.8.12", diff --git a/framework-plugins/lisk-framework-report-misbehavior-plugin/package.json b/framework-plugins/lisk-framework-report-misbehavior-plugin/package.json index 303dc25ad50..f6004a01db4 100644 --- a/framework-plugins/lisk-framework-report-misbehavior-plugin/package.json +++ b/framework-plugins/lisk-framework-report-misbehavior-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@liskhq/lisk-framework-report-misbehavior-plugin", - "version": "0.4.0-rc.5", + "version": "0.5.0-rc.0", "description": "A plugin for lisk-framework that provides automatic detection of validator misbehavior and sends a reportValidatorMisbehaviorTransaction to the running node", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -38,9 +38,9 @@ "build:check": "node -e \"require('./dist-node')\"" }, "dependencies": { - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", "fs-extra": "11.1.0", - "lisk-sdk": "^6.0.0-rc.5" + "lisk-sdk": "^6.1.0-rc.0" }, "devDependencies": { "@types/jest": "29.2.3", diff --git a/framework/package.json b/framework/package.json index cedecb538ff..d875120eb52 100644 --- a/framework/package.json +++ b/framework/package.json @@ -1,6 +1,6 @@ { "name": "lisk-framework", - "version": "0.11.0-rc.5", + "version": "0.12.0-rc.0", "description": "Lisk blockchain application platform", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -42,17 +42,17 @@ }, "dependencies": { "@chainsafe/blst": "0.2.9", - "@liskhq/lisk-api-client": "^6.0.0-rc.4", - "@liskhq/lisk-chain": "^0.5.0-rc.4", - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-db": "0.3.10", - "@liskhq/lisk-p2p": "^0.9.0-rc.2", - "@liskhq/lisk-transaction-pool": "^0.7.0-rc.2", - "@liskhq/lisk-transactions": "^6.0.0-rc.2", - "@liskhq/lisk-tree": "^0.4.0-rc.2", + "@liskhq/lisk-api-client": "^6.1.0-rc.0", + "@liskhq/lisk-chain": "^0.6.0-rc.0", + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-db": "0.3.7", + "@liskhq/lisk-p2p": "^0.10.0-rc.0", + "@liskhq/lisk-transaction-pool": "^0.8.0-rc.0", + "@liskhq/lisk-transactions": "^6.1.0-rc.0", + "@liskhq/lisk-tree": "^0.5.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", - "@liskhq/lisk-validator": "^0.8.0-rc.2", + "@liskhq/lisk-validator": "^0.9.0-rc.0", "bunyan": "1.8.15", "debug": "4.3.4", "eventemitter2": "6.4.9", @@ -64,7 +64,7 @@ "zeromq": "6.0.0-beta.6" }, "devDependencies": { - "@liskhq/lisk-passphrase": "^4.0.0-rc.0", + "@liskhq/lisk-passphrase": "^4.1.0-rc.0", "@types/bunyan": "1.8.6", "@types/jest": "29.2.3", "@types/jest-when": "3.5.2", diff --git a/framework/src/engine/bft/constants.ts b/framework/src/engine/bft/constants.ts index 90fddc54f2b..9c1f563fcf5 100644 --- a/framework/src/engine/bft/constants.ts +++ b/framework/src/engine/bft/constants.ts @@ -19,8 +19,8 @@ export const MODULE_STORE_PREFIX_BFT = Buffer.from([0, 0, 0, 0]); export const ED25519_PUBLIC_KEY_LENGTH = 32; export const BLS_PUBLIC_KEY_LENGTH = 48; export const EMPTY_BLS_KEY = Buffer.alloc(BLS_PUBLIC_KEY_LENGTH, 0); -export const STORE_PREFIX_BFT_PARAMETERS = 0x0000; -export const STORE_PREFIX_BFT_VOTES = 0x8000; +export const STORE_PREFIX_BFT_PARAMETERS = Buffer.from([0x00, 0x00]); +export const STORE_PREFIX_BFT_VOTES = Buffer.from([0x80, 0x00]); export const EMPTY_KEY = Buffer.alloc(0); export const MAX_UINT32 = 2 ** 32 - 1; diff --git a/framework/src/engine/consensus/consensus.ts b/framework/src/engine/consensus/consensus.ts index 6030b233ed8..31646c73d6c 100644 --- a/framework/src/engine/consensus/consensus.ts +++ b/framework/src/engine/consensus/consensus.ts @@ -35,7 +35,7 @@ import { ApplyPenaltyError } from '../../errors'; import { AbortError, ApplyPenaltyAndRestartError, RestartError } from './synchronizer/errors'; import { BlockExecutor } from './synchronizer/type'; import { Network } from '../network'; -import { NetworkEndpoint, EndpointArgs } from './network_endpoint'; +import { NetworkEndpoint } from './network_endpoint'; import { LegacyNetworkEndpoint } from '../legacy/network_endpoint'; import { EventPostBlockData, postBlockEventSchema } from './schema'; import { @@ -158,7 +158,7 @@ export class Consensus { network: this._network, db: this._db, commitPool: this._commitPool, - } as EndpointArgs); // TODO: Remove casting in issue where commitPool is added here + }); this._legacyEndpoint = new LegacyNetworkEndpoint({ logger: this._logger, network: this._network, diff --git a/framework/src/engine/generator/generator_store.ts b/framework/src/engine/generator/generator_store.ts index d5e7bb94cae..afa92f0d8fe 100644 --- a/framework/src/engine/generator/generator_store.ts +++ b/framework/src/engine/generator/generator_store.ts @@ -71,7 +71,7 @@ export class GeneratorStore { const pairs: KeyValue[] = []; stream .on('data', ({ key, value }: { key: Buffer; value: Buffer }) => { - pairs.push({ key: key.slice(this._prefix.length), value }); + pairs.push({ key: key.subarray(this._prefix.length), value }); }) .on('error', error => { reject(error); diff --git a/framework/src/index.ts b/framework/src/index.ts index 1f970471c70..653c3574842 100644 --- a/framework/src/index.ts +++ b/framework/src/index.ts @@ -67,6 +67,7 @@ export { genesisTokenStoreSchema as tokenGenesisStoreSchema, CROSS_CHAIN_COMMAND_NAME_TRANSFER, } from './modules/token'; +export { NFTModule, NFTMethod } from './modules/nft'; export { PoSMethod, PoSModule, @@ -143,6 +144,7 @@ export { RewardMethod, RewardModule } from './modules/reward'; export { DynamicRewardMethod, DynamicRewardModule } from './modules/dynamic_reward'; export { FeeMethod, FeeModule } from './modules/fee'; export { RandomMethod, RandomModule } from './modules/random'; +export { PoAModule, PoAMethod } from './modules/poa'; export { NamedRegistry } from './modules/named_registry'; export { GenesisBlockExecuteContext, diff --git a/framework/src/modules/base_offchain_store.ts b/framework/src/modules/base_offchain_store.ts index 67f7c00d615..b1ab320ca58 100644 --- a/framework/src/modules/base_offchain_store.ts +++ b/framework/src/modules/base_offchain_store.ts @@ -42,14 +42,14 @@ export abstract class BaseOffchainStore { public constructor(moduleName: string, version = 0) { this._version = version; - this._storePrefix = utils.hash(Buffer.from(moduleName, 'utf-8')).slice(0, 4); + this._storePrefix = utils.hash(Buffer.from(moduleName, 'utf-8')).subarray(0, 4); // eslint-disable-next-line no-bitwise this._storePrefix[0] &= 0x7f; const versionBuffer = Buffer.alloc(2); versionBuffer.writeUInt16BE(this._version, 0); this._subStorePrefix = utils .hash(Buffer.concat([Buffer.from(this.name, 'utf-8'), versionBuffer])) - .slice(0, 2); + .subarray(0, 2); } public async get(ctx: ImmutableOffchainStoreGetter, key: Buffer): Promise { diff --git a/framework/src/modules/base_store.ts b/framework/src/modules/base_store.ts index 13138a313fa..9272a2fdc41 100644 --- a/framework/src/modules/base_store.ts +++ b/framework/src/modules/base_store.ts @@ -11,8 +11,7 @@ * * Removal or modification of this copyright notice is prohibited. */ -import { emptySchema } from '@liskhq/lisk-codec'; -import { Schema } from '@liskhq/lisk-codec'; +import { Schema, emptySchema } from '@liskhq/lisk-codec'; import { utils } from '@liskhq/lisk-cryptography'; import { IterateOptions } from '@liskhq/lisk-db'; import { ImmutableSubStore, SubStore } from '../state_machine/types'; @@ -27,7 +26,7 @@ export interface StoreGetter { // LIP: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0040.md#module-store-prefix-1 export const computeStorePrefix = (name: string): Buffer => { - const prefix = utils.hash(Buffer.from(name, 'utf-8')).slice(0, 4); + const prefix = utils.hash(Buffer.from(name, 'utf-8')).subarray(0, 4); // eslint-disable-next-line no-bitwise prefix[0] &= 0x7f; return prefix; diff --git a/framework/src/modules/fee/module.ts b/framework/src/modules/fee/module.ts index d13e0f97e5a..8bfc6a7f3fe 100644 --- a/framework/src/modules/fee/module.ts +++ b/framework/src/modules/fee/module.ts @@ -104,7 +104,6 @@ export class FeeModule extends BaseInteroperableModule { this._feePoolAddress = moduleConfig.feePoolAddress; } - // eslint-disable-next-line @typescript-eslint/require-await public async verifyTransaction(context: TransactionVerifyContext): Promise { const { getMethodContext, transaction, header } = context; diff --git a/framework/src/modules/interoperability/base_interoperability_method.ts b/framework/src/modules/interoperability/base_interoperability_method.ts index baedbec6b50..ac60c77d502 100644 --- a/framework/src/modules/interoperability/base_interoperability_method.ts +++ b/framework/src/modules/interoperability/base_interoperability_method.ts @@ -37,14 +37,7 @@ import { getMainchainID } from './utils'; export abstract class BaseInteroperabilityMethod< T extends BaseInteroperabilityInternalMethod, > extends BaseMethod { - protected _tokenMethod!: TokenMethod & { - payMessageFee: ( - context: MethodContext, - payFromAddress: Buffer, - fee: bigint, - receivingChainID: Buffer, - ) => Promise; - }; + protected _tokenMethod!: TokenMethod; public constructor( stores: NamedRegistry, @@ -55,17 +48,7 @@ export abstract class BaseInteroperabilityMethod< super(stores, events); } - public addDependencies( - tokenMethod: TokenMethod & { - // TODO: Remove this after token module update - payMessageFee: ( - context: MethodContext, - payFromAddress: Buffer, - fee: bigint, - receivingChainID: Buffer, - ) => Promise; - }, - ) { + public addDependencies(tokenMethod: TokenMethod) { this._tokenMethod = tokenMethod; } diff --git a/framework/src/modules/interoperability/base_state_recovery.ts b/framework/src/modules/interoperability/base_state_recovery.ts index 0cd116393d8..62b0f08ac95 100644 --- a/framework/src/modules/interoperability/base_state_recovery.ts +++ b/framework/src/modules/interoperability/base_state_recovery.ts @@ -78,7 +78,7 @@ export class BaseStateRecoveryCommand< if (!moduleMethod.recover) { return { status: VerifyStatus.FAIL, - error: new Error('Module is not recoverable.'), + error: new Error("Module is not recoverable, as it doesn't have a recover method."), }; } @@ -93,7 +93,7 @@ export class BaseStateRecoveryCommand< if (!objectUtils.bufferArrayUniqueItems(queryKeys)) { return { status: VerifyStatus.FAIL, - error: new Error('Recovered store keys are not pairwise distinct.'), + error: new Error('Recoverable store keys are not pairwise distinct.'), }; } @@ -160,7 +160,7 @@ export class BaseStateRecoveryCommand< }); storeQueriesUpdate.push({ key: Buffer.concat([storePrefix, entry.substorePrefix, utils.hash(entry.storeKey)]), - value: RECOVERED_STORE_VALUE, + value: RECOVERED_STORE_VALUE, // The value is set to a constant without known pre-image. bitmap: entry.bitmap, }); } catch (err) { diff --git a/framework/src/modules/interoperability/mainchain/commands/terminate_sidechain_for_liveness.ts b/framework/src/modules/interoperability/mainchain/commands/terminate_sidechain_for_liveness.ts index 73263756ce7..74392c6766c 100644 --- a/framework/src/modules/interoperability/mainchain/commands/terminate_sidechain_for_liveness.ts +++ b/framework/src/modules/interoperability/mainchain/commands/terminate_sidechain_for_liveness.ts @@ -23,6 +23,7 @@ import { ChainAccountStore, ChainStatus } from '../../stores/chain_account'; import { TerminateSidechainForLivenessParams } from '../../types'; import { MainchainInteroperabilityInternalMethod } from '../internal_method'; +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0054.md#liveness-termination-command-1 export class TerminateSidechainForLivenessCommand extends BaseInteroperabilityCommand { public schema = terminateSidechainForLivenessParamsSchema; @@ -30,28 +31,24 @@ export class TerminateSidechainForLivenessCommand extends BaseInteroperabilityCo context: CommandVerifyContext, ): Promise { const { params } = context; - const doesChainAccountExist = await this.stores - .get(ChainAccountStore) - .has(context, params.chainID); - if (!doesChainAccountExist) { + const chainAccount = await this.stores + .get(ChainAccountStore) + .getOrUndefined(context, params.chainID); + if (!chainAccount) { throw new Error('Chain account does not exist.'); } - const chainAccount = await this.stores.get(ChainAccountStore).get(context, params.chainID); - - // The commands fails if the sidechain is already terminated. if (chainAccount.status === ChainStatus.TERMINATED) { throw new Error('Sidechain is already terminated.'); } // Or if the sidechain did not violate the liveness condition. - const isChainAccountLive = await this.internalMethod.isLive( + const live = await this.internalMethod.isLive( context, params.chainID, context.header.timestamp, ); - - if (isChainAccountLive) { + if (live) { throw new Error('Sidechain did not violate the liveness condition.'); } diff --git a/framework/src/modules/interoperability/mainchain/endpoint.ts b/framework/src/modules/interoperability/mainchain/endpoint.ts index e4170f3074e..dab73450f9b 100644 --- a/framework/src/modules/interoperability/mainchain/endpoint.ts +++ b/framework/src/modules/interoperability/mainchain/endpoint.ts @@ -42,8 +42,8 @@ export class MainchainInteroperabilityEndpoint extends BaseInteroperabilityEndpo validator.validate(isChainIDAvailableRequestSchema, context.params); const chainID = Buffer.from(context.params.chainID as string, 'hex'); const ownChainAccount = await this.stores.get(OwnChainAccountStore).get(context, EMPTY_BYTES); - const networkID = chainID.slice(0, 1); - const ownChainNetworkID = ownChainAccount.chainID.slice(0, 1); + const networkID = chainID.subarray(0, 1); + const ownChainNetworkID = ownChainAccount.chainID.subarray(0, 1); // Only mainchain network IDs are available if (!networkID.equals(ownChainNetworkID)) { return { diff --git a/framework/src/modules/interoperability/mainchain/module.ts b/framework/src/modules/interoperability/mainchain/module.ts index 9620ed015fd..708775d5295 100644 --- a/framework/src/modules/interoperability/mainchain/module.ts +++ b/framework/src/modules/interoperability/mainchain/module.ts @@ -261,15 +261,7 @@ export class MainchainInteroperabilityModule extends BaseInteroperabilityModule throw new Error(`ownChainName must be equal to ${CHAIN_NAME_MAINCHAIN}.`); } - // if chainInfos is empty, then ownChainNonce == 0 - // If chainInfos is non-empty, ownChainNonce > 0 - if (chainInfos.length === 0 && ownChainNonce !== BigInt(0)) { - throw new Error(`ownChainNonce must be 0 if chainInfos is empty.`); - } else if (chainInfos.length !== 0 && ownChainNonce <= BigInt(0)) { - throw new Error(`ownChainNonce must be positive if chainInfos is not empty.`); - } - - this._verifyChainInfos(ctx, chainInfos, terminatedStateAccounts); + this._verifyChainInfos(ctx, chainInfos, ownChainNonce, terminatedStateAccounts); this._verifyTerminatedStateAccounts(chainInfos, terminatedStateAccounts, mainchainID); this._verifyTerminatedOutboxAccounts( chainInfos, @@ -284,8 +276,17 @@ export class MainchainInteroperabilityModule extends BaseInteroperabilityModule private _verifyChainInfos( ctx: GenesisBlockExecuteContext, chainInfos: ChainInfo[], + ownChainNonce: bigint, terminatedStateAccounts: TerminatedStateAccountWithChainID[], ) { + // if chainInfos is empty, then ownChainNonce == 0 + // If chainInfos is non-empty, ownChainNonce > 0 + if (chainInfos.length === 0 && ownChainNonce !== BigInt(0)) { + throw new Error(`ownChainNonce must be 0 if chainInfos is empty.`); + } else if (chainInfos.length !== 0 && ownChainNonce <= 0) { + throw new Error(`ownChainNonce must be positive if chainInfos is not empty.`); + } + // Each entry chainInfo in chainInfos has a unique chainInfo.chainID const chainIDs = chainInfos.map(info => info.chainID); if (!objectUtils.bufferArrayUniqueItems(chainIDs)) { @@ -424,9 +425,9 @@ export class MainchainInteroperabilityModule extends BaseInteroperabilityModule terminatedStateAccounts.find(a => a.chainID.equals(outboxAccount.chainID)) === undefined ) { throw new Error( - `Each entry outboxAccount in terminatedOutboxAccounts must have a corresponding entry in terminatedStateAccount. outboxAccount with chainID: ${outboxAccount.chainID.toString( + `outboxAccount with chainID: ${outboxAccount.chainID.toString( 'hex', - )} does not exist in terminatedStateAccounts`, + )} must have a corresponding entry in terminatedStateAccounts.`, ); } } diff --git a/framework/src/modules/interoperability/schemas.ts b/framework/src/modules/interoperability/schemas.ts index a9eff070965..9e1804d3844 100644 --- a/framework/src/modules/interoperability/schemas.ts +++ b/framework/src/modules/interoperability/schemas.ts @@ -545,6 +545,7 @@ export const stateRecoveryInitParamsSchema = { }, }; +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0054.md#parameters-2 export const terminateSidechainForLivenessParamsSchema = { $id: '/modules/interoperability/mainchain/terminateSidechainForLiveness', type: 'object', diff --git a/framework/src/modules/interoperability/sidechain/commands/initialize_state_recovery.ts b/framework/src/modules/interoperability/sidechain/commands/initialize_state_recovery.ts index c4319d01ce4..5533a5e2f80 100644 --- a/framework/src/modules/interoperability/sidechain/commands/initialize_state_recovery.ts +++ b/framework/src/modules/interoperability/sidechain/commands/initialize_state_recovery.ts @@ -33,6 +33,7 @@ import { getMainchainID } from '../../utils'; import { SidechainInteroperabilityInternalMethod } from '../internal_method'; import { InvalidSMTVerificationEvent } from '../../events/invalid_smt_verification'; +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0054.md#state-recovery-initialization-command export class InitializeStateRecoveryCommand extends BaseInteroperabilityCommand { public schema = stateRecoveryInitParamsSchema; @@ -106,19 +107,23 @@ export class InitializeStateRecoveryCommand extends BaseInteroperabilityCommand< const smt = new SparseMerkleTree(); let stateRoot: Buffer; + // it will help whether error is for input chainID or mainchainID + let msg; if (terminatedStateAccountExists) { const terminatedStateAccount = await terminatedStateSubstore.get(context, chainID); stateRoot = terminatedStateAccount.mainchainStateRoot; + msg = `given chainID: ${chainID.toString('hex')}.`; } else { const mainchainID = getMainchainID(context.chainID); const mainchainAccount = await this.stores.get(ChainAccountStore).get(context, mainchainID); stateRoot = mainchainAccount.lastCertificate.stateRoot; + msg = `mainchainID: ${mainchainID.toString('hex')}`; } const verified = await smt.verifyInclusionProof(stateRoot, [queryKey], proofOfInclusion); if (!verified) { this.events.get(InvalidSMTVerificationEvent).error(context); - throw new Error('State recovery initialization proof of inclusion is not valid.'); + throw new Error(`State recovery initialization proof of inclusion is not valid for ${msg}.`); } const deserializedSidechainAccount = codec.decode( diff --git a/framework/src/modules/interoperability/sidechain/module.ts b/framework/src/modules/interoperability/sidechain/module.ts index 6a3cd0a93d1..08796d0c185 100644 --- a/framework/src/modules/interoperability/sidechain/module.ts +++ b/framework/src/modules/interoperability/sidechain/module.ts @@ -237,8 +237,13 @@ export class SidechainInteroperabilityModule extends BaseInteroperabilityModule ctx: GenesisBlockExecuteContext, genesisInteroperability: GenesisInteroperability, ) { - const { ownChainName, ownChainNonce, chainInfos, terminatedStateAccounts } = - genesisInteroperability; + const { + ownChainName, + ownChainNonce, + chainInfos, + terminatedStateAccounts, + terminatedOutboxAccounts, + } = genesisInteroperability; // If chainInfos is empty, then check that: // @@ -257,6 +262,9 @@ export class SidechainInteroperabilityModule extends BaseInteroperabilityModule if (terminatedStateAccounts.length !== 0) { throw new Error(`terminatedStateAccounts must be empty, ${ifChainInfosIsEmpty}.`); } + if (terminatedOutboxAccounts.length !== 0) { + throw new Error(`terminatedOutboxAccounts must be empty, ${ifChainInfosIsEmpty}.`); + } } else { // ownChainName // has length between MIN_CHAIN_NAME_LENGTH and MAX_CHAIN_NAME_LENGTH, @@ -267,7 +275,7 @@ export class SidechainInteroperabilityModule extends BaseInteroperabilityModule ownChainName.length > MAX_CHAIN_NAME_LENGTH // will only run if not already applied in schema ) { throw new Error( - `ownChainName.length must be between ${MIN_CHAIN_NAME_LENGTH} and ${MAX_CHAIN_NAME_LENGTH}`, + `ownChainName.length must be inclusively between ${MIN_CHAIN_NAME_LENGTH} and ${MAX_CHAIN_NAME_LENGTH}.`, ); } // CAUTION! @@ -337,7 +345,7 @@ export class SidechainInteroperabilityModule extends BaseInteroperabilityModule if (terminatedStateAccount.initialized) { if (terminatedStateAccount.stateRoot.equals(EMPTY_HASH)) { throw new Error( - `stateAccount.stateRoot mst be not equal to "${EMPTY_HASH.toString( + `stateAccount.stateRoot must not be equal to "${EMPTY_HASH.toString( 'hex', )}", if initialized is true.`, ); @@ -360,7 +368,7 @@ export class SidechainInteroperabilityModule extends BaseInteroperabilityModule } if (terminatedStateAccount.mainchainStateRoot.equals(EMPTY_HASH)) { throw new Error( - `terminatedStateAccount.mainchainStateRoot must be not equal to "${EMPTY_HASH.toString( + `terminatedStateAccount.mainchainStateRoot must not be equal to "${EMPTY_HASH.toString( 'hex', )}", if initialized is false.`, ); diff --git a/framework/src/modules/interoperability/utils.ts b/framework/src/modules/interoperability/utils.ts index 228c2a170e0..b4a43d7951f 100644 --- a/framework/src/modules/interoperability/utils.ts +++ b/framework/src/modules/interoperability/utils.ts @@ -182,14 +182,13 @@ export const verifyLivenessConditionForRegisteredChains = ( }; export const getMainchainID = (chainID: Buffer): Buffer => { - const networkID = chainID.slice(0, 1); + const networkID = chainID.subarray(0, 1); // 3 bytes for remaining chainID bytes return Buffer.concat([networkID, Buffer.alloc(CHAIN_ID_LENGTH - 1, 0)]); }; -// TODO: Update to use Token method after merging development export const getTokenIDLSK = (chainID: Buffer): Buffer => { - const networkID = chainID.slice(0, 1); + const networkID = chainID.subarray(0, 1); // 3 bytes for remaining chainID bytes return Buffer.concat([networkID, Buffer.alloc(7, 0)]); }; diff --git a/framework/src/modules/nft/cc_commands/.gitkeep b/framework/src/modules/nft/cc_commands/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/framework/src/modules/nft/cc_commands/cc_transfer.ts b/framework/src/modules/nft/cc_commands/cc_transfer.ts new file mode 100644 index 00000000000..ae4884b76fd --- /dev/null +++ b/framework/src/modules/nft/cc_commands/cc_transfer.ts @@ -0,0 +1,176 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec } from '@liskhq/lisk-codec'; +import { validator } from '@liskhq/lisk-validator'; +import { CCTransferMessageParams, crossChainNFTTransferMessageParamsSchema } from '../schemas'; +import { NFTAttributes, NFTStore } from '../stores/nft'; +import { NFTMethod } from '../method'; +import { + CCM_STATUS_CODE_OK, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + FEE_CREATE_NFT, + NftEventResult, +} from '../constants'; +import { InternalMethod } from '../internal_method'; +import { BaseCCCommand } from '../../interoperability/base_cc_command'; +import { CrossChainMessageContext } from '../../interoperability/types'; +import { CCMStatusCode, MAX_RESERVED_ERROR_STATUS } from '../../interoperability/constants'; +import { FeeMethod } from '../types'; +import { EscrowStore } from '../stores/escrow'; +import { CcmTransferEvent } from '../events/ccm_transfer'; + +export class CrossChainTransferCommand extends BaseCCCommand { + public schema = crossChainNFTTransferMessageParamsSchema; + private _method!: NFTMethod; + private _internalMethod!: InternalMethod; + private _feeMethod!: FeeMethod; + + public get name(): string { + return CROSS_CHAIN_COMMAND_NAME_TRANSFER; + } + + public init(args: { method: NFTMethod; internalMethod: InternalMethod; feeMethod: FeeMethod }) { + this._method = args.method; + this._internalMethod = args.internalMethod; + this._feeMethod = args.feeMethod; + } + + public async verify(context: CrossChainMessageContext): Promise { + const { ccm, getMethodContext } = context; + const params = codec.decode( + crossChainNFTTransferMessageParamsSchema, + ccm.params, + ); + + if (ccm.status > MAX_RESERVED_ERROR_STATUS) { + throw new Error('Invalid CCM error code'); + } + + const { nftID } = params; + const { sendingChainID } = ccm; + const nftChainID = this._method.getChainID(nftID); + const ownChainID = context.chainID; + + if (![ownChainID, sendingChainID].some(allowedChainID => nftChainID.equals(allowedChainID))) { + throw new Error('NFT is not native to either the sending chain or the receiving chain'); + } + + const nftStore = this.stores.get(NFTStore); + const nftExists = await nftStore.has(getMethodContext(), nftID); + + if (nftChainID.equals(ownChainID)) { + if (!nftExists) { + throw new Error('Non-existent entry in the NFT substore'); + } + + const nft = await nftStore.get(getMethodContext(), nftID); + if (!nft.owner.equals(sendingChainID)) { + throw new Error('NFT has not been properly escrowed'); + } + } + + if ( + !nftChainID.equals(ownChainID) && + (ccm.status === CCMStatusCode.MODULE_NOT_SUPPORTED || + ccm.status === CCMStatusCode.CROSS_CHAIN_COMMAND_NOT_SUPPORTED) + ) { + throw new Error('Module or cross-chain command not supported'); + } + + if (!nftChainID.equals(ownChainID) && nftExists) { + throw new Error('NFT substore entry already exists'); + } + } + + public async execute(context: CrossChainMessageContext): Promise { + const { ccm, getMethodContext } = context; + const params = codec.decode( + crossChainNFTTransferMessageParamsSchema, + ccm.params, + ); + validator.validate(crossChainNFTTransferMessageParamsSchema, params); + const { sendingChainID, status } = ccm; + const { nftID, senderAddress, attributesArray: receivedAttributes } = params; + const nftChainID = this._method.getChainID(nftID); + const ownChainID = context.chainID; + const nftStore = this.stores.get(NFTStore); + const escrowStore = this.stores.get(EscrowStore); + let recipientAddress: Buffer; + recipientAddress = params.recipientAddress; + + if (nftChainID.equals(ownChainID)) { + const storeData = await nftStore.get(getMethodContext(), nftID); + + if (status === CCM_STATUS_CODE_OK) { + const storedAttributes = storeData.attributesArray; + storeData.attributesArray = this._internalMethod.getNewAttributes( + nftID, + storedAttributes, + receivedAttributes, + ); + } else { + recipientAddress = senderAddress; + } + + await this._internalMethod.createNFTEntry( + getMethodContext(), + recipientAddress, + nftID, + storeData.attributesArray, + ); + await this._internalMethod.createUserEntry(getMethodContext(), recipientAddress, nftID); + await escrowStore.del(getMethodContext(), escrowStore.getKey(sendingChainID, nftID)); + } else { + const isSupported = await this._method.isNFTSupported(getMethodContext(), nftID); + if (!isSupported) { + this.events.get(CcmTransferEvent).error( + context, + { + senderAddress, + recipientAddress, + nftID, + receivingChainID: ccm.receivingChainID, + sendingChainID: ccm.sendingChainID, + }, + NftEventResult.RESULT_NFT_NOT_SUPPORTED, + ); + throw new Error('Non-supported NFT'); + } + + this._feeMethod.payFee(getMethodContext(), BigInt(FEE_CREATE_NFT)); + + if (status !== CCM_STATUS_CODE_OK) { + recipientAddress = senderAddress; + } + + await this._internalMethod.createNFTEntry( + getMethodContext(), + recipientAddress, + nftID, + receivedAttributes as NFTAttributes[], + ); + + await this._internalMethod.createUserEntry(getMethodContext(), recipientAddress, nftID); + } + + this.events.get(CcmTransferEvent).log(context, { + senderAddress, + recipientAddress, + nftID, + receivingChainID: ccm.receivingChainID, + sendingChainID: ccm.sendingChainID, + }); + } +} diff --git a/framework/src/modules/nft/cc_method.ts b/framework/src/modules/nft/cc_method.ts new file mode 100644 index 00000000000..91c38ea11ce --- /dev/null +++ b/framework/src/modules/nft/cc_method.ts @@ -0,0 +1,25 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCCMethod } from '../interoperability/base_cc_method'; +import { InteroperabilityMethod } from './types'; + +export class NFTInteroperableMethod extends BaseCCMethod { + // @ts-expect-error TODO: unused error. Remove when implementing. + private _interopMethod!: InteroperabilityMethod; + + public addDependencies(interoperabilityMethod: InteroperabilityMethod) { + this._interopMethod = interoperabilityMethod; + } +} diff --git a/framework/src/modules/nft/commands/transfer.ts b/framework/src/modules/nft/commands/transfer.ts new file mode 100644 index 00000000000..bd5ae39d2c8 --- /dev/null +++ b/framework/src/modules/nft/commands/transfer.ts @@ -0,0 +1,69 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { + CommandExecuteContext, + CommandVerifyContext, + VerificationResult, + VerifyStatus, +} from '../../../state_machine'; +import { BaseCommand } from '../../base_command'; +import { transferParamsSchema } from '../schemas'; +import { InternalMethod } from '../internal_method'; + +export interface TransferParams { + nftID: Buffer; + recipientAddress: Buffer; + data: string; +} + +export class TransferCommand extends BaseCommand { + public schema = transferParamsSchema; + private _internalMethod!: InternalMethod; + + public init(args: { internalMethod: InternalMethod }) { + this._internalMethod = args.internalMethod; + } + + public async verify(context: CommandVerifyContext): Promise { + const { params } = context; + + try { + await this._internalMethod.verifyTransfer( + context.getMethodContext(), + context.transaction.senderAddress, + params.nftID, + ); + } catch (error) { + return { + status: VerifyStatus.FAIL, + error: error as Error, + }; + } + + return { + status: VerifyStatus.OK, + }; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + await this._internalMethod.transfer( + context.getMethodContext(), + params.recipientAddress, + params.nftID, + ); + } +} diff --git a/framework/src/modules/nft/commands/transfer_cross_chain.ts b/framework/src/modules/nft/commands/transfer_cross_chain.ts new file mode 100644 index 00000000000..062919f3830 --- /dev/null +++ b/framework/src/modules/nft/commands/transfer_cross_chain.ts @@ -0,0 +1,93 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { crossChainTransferParamsSchema } from '../schemas'; +import { NFTMethod } from '../method'; +import { InteroperabilityMethod, TokenMethod } from '../types'; +import { BaseCommand } from '../../base_command'; +import { + CommandExecuteContext, + CommandVerifyContext, + VerificationResult, + VerifyStatus, +} from '../../../state_machine'; +import { InternalMethod } from '../internal_method'; + +export interface TransferCrossChainParams { + nftID: Buffer; + receivingChainID: Buffer; + recipientAddress: Buffer; + data: string; + messageFee: bigint; + includeAttributes: boolean; +} + +export class TransferCrossChainCommand extends BaseCommand { + public schema = crossChainTransferParamsSchema; + + private _internalMethod!: InternalMethod; + + public init(args: { + nftMethod: NFTMethod; + tokenMethod: TokenMethod; + interoperabilityMethod: InteroperabilityMethod; + internalMethod: InternalMethod; + }): void { + this._internalMethod = args.internalMethod; + } + + public async verify( + context: CommandVerifyContext, + ): Promise { + const { params } = context; + const { senderAddress } = context.transaction; + + try { + await this._internalMethod.verifyTransferCrossChain( + context.getMethodContext(), + senderAddress, + params.nftID, + context.chainID, + params.receivingChainID, + params.messageFee, + params.data, + ); + } catch (error) { + return { + status: VerifyStatus.FAIL, + error: error as Error, + }; + } + + return { + status: VerifyStatus.OK, + }; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + await this._internalMethod.transferCrossChain( + context.getMethodContext(), + context.transaction.senderAddress, + params.recipientAddress, + params.nftID, + params.receivingChainID, + params.messageFee, + params.data, + params.includeAttributes, + context.header.timestamp, + ); + } +} diff --git a/framework/src/modules/nft/constants.ts b/framework/src/modules/nft/constants.ts new file mode 100644 index 00000000000..52251e153ca --- /dev/null +++ b/framework/src/modules/nft/constants.ts @@ -0,0 +1,51 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export const LENGTH_CHAIN_ID = 4; +export const LENGTH_NFT_ID = 16; +export const LENGTH_COLLECTION_ID = 4; +export const LENGTH_INDEX = LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID; +export const MIN_LENGTH_MODULE_NAME = 1; +export const MAX_LENGTH_MODULE_NAME = 32; +export const LENGTH_ADDRESS = 20; +export const MODULE_NAME_NFT = 'nft'; +export const NFT_NOT_LOCKED = MODULE_NAME_NFT; +export const CROSS_CHAIN_COMMAND_NAME_TRANSFER = 'crossChainTransfer'; +export const CCM_STATUS_CODE_OK = 0; +export const EMPTY_BYTES = Buffer.alloc(0); +export const ALL_SUPPORTED_NFTS_KEY = EMPTY_BYTES; +export const FEE_CREATE_NFT = 5000000; +export const LENGTH_TOKEN_ID = 8; +export const MAX_LENGTH_DATA = 64; + +export const enum NftEventResult { + RESULT_SUCCESSFUL = 0, + RESULT_NFT_DOES_NOT_EXIST = 1, + RESULT_NFT_NOT_NATIVE = 2, + RESULT_NFT_NOT_SUPPORTED = 3, + RESULT_NFT_LOCKED = 4, + RESULT_NFT_NOT_LOCKED = 5, + RESULT_UNAUTHORIZED_UNLOCK = 6, + RESULT_NFT_ESCROWED = 7, + RESULT_NFT_NOT_ESCROWED = 8, + RESULT_INITIATED_BY_NONNATIVE_CHAIN = 9, + RESULT_INITIATED_BY_NONOWNER = 10, + RESULT_RECOVER_FAIL_INVALID_INPUTS = 11, + RESULT_INSUFFICIENT_BALANCE = 12, + RESULT_DATA_TOO_LONG = 13, + INVALID_RECEIVING_CHAIN = 14, + RESULT_INVALID_ACCOUNT = 15, +} + +export type NftErrorEventResult = Exclude; diff --git a/framework/src/modules/nft/endpoint.ts b/framework/src/modules/nft/endpoint.ts new file mode 100644 index 00000000000..01cea421482 --- /dev/null +++ b/framework/src/modules/nft/endpoint.ts @@ -0,0 +1,284 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import * as cryptography from '@liskhq/lisk-cryptography'; +import { validator } from '@liskhq/lisk-validator'; +import { BaseEndpoint } from '../base_endpoint'; +import { JSONObject, ModuleEndpointContext } from '../../types'; +import { + isCollectionIDSupportedRequestSchema, + getEscrowedNFTIDsRequestSchema, + getNFTRequestSchema, + getNFTsRequestSchema, + hasNFTRequestSchema, + isNFTSupportedRequestSchema, +} from './schemas'; +import { NFTStore } from './stores/nft'; +import { ALL_SUPPORTED_NFTS_KEY, LENGTH_ADDRESS, LENGTH_NFT_ID } from './constants'; +import { UserStore } from './stores/user'; +import { NFTJSON } from './types'; +import { SupportedNFTsStore } from './stores/supported_nfts'; +import { NFTMethod } from './method'; + +export class NFTEndpoint extends BaseEndpoint { + private _nftMethod!: NFTMethod; + + public addDependencies(nftMethod: NFTMethod) { + this._nftMethod = nftMethod; + } + + public async getNFTs( + context: ModuleEndpointContext, + ): Promise<{ nfts: JSONObject & { id: string }>[] }> { + validator.validate<{ address: string }>(getNFTsRequestSchema, context.params); + + const nftStore = this.stores.get(NFTStore); + + const owner = cryptography.address.getAddressFromLisk32Address(context.params.address); + + const allNFTs = await nftStore.iterate(context.getImmutableMethodContext(), { + gte: Buffer.alloc(LENGTH_NFT_ID, 0), + lte: Buffer.alloc(LENGTH_NFT_ID, 255), + }); + + const ownedNFTs = allNFTs.filter(nft => nft.value.owner.equals(owner)); + + const userStore = this.stores.get(UserStore); + + const nfts = []; + + for (const ownedNFT of ownedNFTs) { + const ownedNFTUserData = await userStore.get( + context.getImmutableMethodContext(), + userStore.getKey(owner, ownedNFT.key), + ); + + nfts.push({ + id: ownedNFT.key.toString('hex'), + attributesArray: ownedNFT.value.attributesArray.map(attribute => ({ + module: attribute.module, + attributes: attribute.attributes.toString('hex'), + })), + lockingModule: ownedNFTUserData.lockingModule, + }); + } + + return { nfts }; + } + + public async hasNFT(context: ModuleEndpointContext): Promise<{ hasNFT: boolean }> { + const { params } = context; + validator.validate<{ address: string; id: string }>(hasNFTRequestSchema, params); + + const nftID = Buffer.from(params.id, 'hex'); + const owner = cryptography.address.getAddressFromLisk32Address(params.address); + + const nftStore = this.stores.get(NFTStore); + const nftExists = await nftStore.has(context.getImmutableMethodContext(), nftID); + + if (!nftExists) { + return { hasNFT: nftExists }; + } + + const nftData = await nftStore.get(context.getImmutableMethodContext(), nftID); + + return { hasNFT: nftData.owner.equals(owner) }; + } + + public async getNFT(context: ModuleEndpointContext): Promise> { + const { params } = context; + validator.validate<{ id: string }>(getNFTRequestSchema, params); + + const nftID = Buffer.from(params.id, 'hex'); + const nftStore = this.stores.get(NFTStore); + const nftExists = await nftStore.has(context.getImmutableMethodContext(), nftID); + + if (!nftExists) { + throw new Error('NFT substore entry does not exist'); + } + + const userStore = this.stores.get(UserStore); + const nftData = await nftStore.get(context.getImmutableMethodContext(), nftID); + const owner = nftData.owner.toString('hex'); + const attributesArray = nftData.attributesArray.map(attribute => ({ + module: attribute.module, + attributes: attribute.attributes.toString('hex'), + })); + + if (nftData.owner.length === LENGTH_ADDRESS) { + const userExists = await userStore.has( + context.getImmutableMethodContext(), + userStore.getKey(nftData.owner, nftID), + ); + if (!userExists) { + throw new Error('User substore entry does not exist'); + } + const userData = await userStore.get( + context.getImmutableMethodContext(), + userStore.getKey(nftData.owner, nftID), + ); + + return { + owner, + attributesArray, + lockingModule: userData.lockingModule, + }; + } + + return { + owner, + attributesArray, + }; + } + + public async getSupportedCollectionIDs( + context: ModuleEndpointContext, + ): Promise<{ supportedCollectionIDs: string[] }> { + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + if (await supportedNFTsStore.has(context, ALL_SUPPORTED_NFTS_KEY)) { + return { supportedCollectionIDs: ['*'] }; + } + + const supportedCollectionIDs: string[] = []; + + supportedCollectionIDs.push(`${context.chainID.toString('hex')}********`); + + const supportedNFTsStoreData = await supportedNFTsStore.getAll(context); + for (const { key, value } of supportedNFTsStoreData) { + if (!value.supportedCollectionIDArray.length) { + supportedCollectionIDs.push(`${key.toString('hex')}********`); + } else { + const collectionIDs = value.supportedCollectionIDArray.map( + supportedCollectionID => + key.toString('hex') + supportedCollectionID.collectionID.toString('hex'), + ); + supportedCollectionIDs.push(...collectionIDs); + } + } + + return { supportedCollectionIDs }; + } + + public async isCollectionIDSupported( + context: ModuleEndpointContext, + ): Promise<{ isCollectionIDSupported: boolean }> { + const { params } = context; + + validator.validate<{ chainID: string; collectionID: string }>( + isCollectionIDSupportedRequestSchema, + params, + ); + + const chainID = Buffer.from(params.chainID, 'hex'); + const collectionID = Buffer.from(params.collectionID, 'hex'); + const nftID = Buffer.concat([chainID, collectionID, Buffer.alloc(8)]); + + const isNFTSupported = await this._nftMethod.isNFTSupported( + context.getImmutableMethodContext(), + nftID, + ); + + if (!isNFTSupported) { + return { isCollectionIDSupported: false }; + } + + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + + const supportedNFTsData = await supportedNFTsStore.get( + context.getImmutableMethodContext(), + chainID, + ); + + return { + isCollectionIDSupported: supportedNFTsData.supportedCollectionIDArray.some( + supportedCollection => supportedCollection.collectionID.equals(collectionID), + ), + }; + } + + public async getEscrowedNFTIDs( + context: ModuleEndpointContext, + ): Promise<{ escrowedNFTIDs: string[] }> { + const { params } = context; + + validator.validate<{ chainID: string }>(getEscrowedNFTIDsRequestSchema, params); + + const chainD = Buffer.from(params.chainID, 'hex'); + + const nftStore = this.stores.get(NFTStore); + + const allNFTs = await nftStore.iterate(context.getImmutableMethodContext(), { + gte: Buffer.alloc(LENGTH_NFT_ID, 0), + lte: Buffer.alloc(LENGTH_NFT_ID, 255), + }); + + return { + escrowedNFTIDs: allNFTs + .filter(nft => nft.value.owner.equals(chainD)) + .map(nft => nft.key.toString('hex')), + }; + } + + public async isNFTSupported( + context: ModuleEndpointContext, + ): Promise<{ isNFTSupported: boolean }> { + const { params } = context; + + validator.validate<{ nftID: string }>(isNFTSupportedRequestSchema, params); + + const nftID = Buffer.from(params.nftID, 'hex'); + let isNFTSupported = false; + + try { + isNFTSupported = await this._nftMethod.isNFTSupported( + context.getImmutableMethodContext(), + nftID, + ); + } catch (err) { + return { isNFTSupported }; + } + + return { isNFTSupported }; + } + + public async getSupportedNFTs( + context: ModuleEndpointContext, + ): Promise<{ supportedNFTs: string[] }> { + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + + const areAllNFTsSupported = await supportedNFTsStore.has(context, ALL_SUPPORTED_NFTS_KEY); + if (areAllNFTsSupported) { + return { + supportedNFTs: ['*'], + }; + } + + const supportedNFTs: string[] = []; + + const storeData = await supportedNFTsStore.getAll(context); + for (const { key, value } of storeData) { + if (!value.supportedCollectionIDArray.length) { + supportedNFTs.push(`${key.toString('hex')}********`); + } else { + for (const supportedCollectionID of value.supportedCollectionIDArray) { + supportedNFTs.push( + key.toString('hex') + supportedCollectionID.collectionID.toString('hex'), + ); + } + } + } + + return { supportedNFTs }; + } +} diff --git a/framework/src/modules/nft/error.ts b/framework/src/modules/nft/error.ts new file mode 100644 index 00000000000..4a99e52adb5 --- /dev/null +++ b/framework/src/modules/nft/error.ts @@ -0,0 +1,15 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export class NotFoundError extends Error {} diff --git a/framework/src/modules/nft/events/all_nfts_from_chain_suported.ts b/framework/src/modules/nft/events/all_nfts_from_chain_suported.ts new file mode 100644 index 00000000000..9798ac42b9c --- /dev/null +++ b/framework/src/modules/nft/events/all_nfts_from_chain_suported.ts @@ -0,0 +1,42 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_CHAIN_ID } from '../constants'; + +export interface AllNFTsFromChainSupportedEventData { + chainID: Buffer; +} + +export const allNFTsFromChainSupportedEventSchema = { + $id: '/nft/events/allNFTsFromChainSupported', + type: 'object', + required: ['chainID'], + properties: { + chainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 1, + }, + }, +}; + +export class AllNFTsFromChainSupportedEvent extends BaseEvent { + public schema = allNFTsFromChainSupportedEventSchema; + + public log(ctx: EventQueuer, chainID: Buffer): void { + this.add(ctx, { chainID }, [chainID]); + } +} diff --git a/framework/src/modules/nft/events/all_nfts_from_chain_support_removed.ts b/framework/src/modules/nft/events/all_nfts_from_chain_support_removed.ts new file mode 100644 index 00000000000..0fe1603823a --- /dev/null +++ b/framework/src/modules/nft/events/all_nfts_from_chain_support_removed.ts @@ -0,0 +1,42 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_CHAIN_ID } from '../constants'; + +export interface AllNFTsFromChainSupportRemovedEventData { + chainID: Buffer; +} + +export const allNFTsFromChainSupportRemovedEventSchema = { + $id: '/nft/events/allNFTsFromChainSupportRemoved', + type: 'object', + required: ['chainID'], + properties: { + chainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 1, + }, + }, +}; + +export class AllNFTsFromChainSupportRemovedEvent extends BaseEvent { + public schema = allNFTsFromChainSupportRemovedEventSchema; + + public log(ctx: EventQueuer, chainID: Buffer): void { + this.add(ctx, { chainID }, [chainID]); + } +} diff --git a/framework/src/modules/nft/events/all_nfts_from_collection_support_removed.ts b/framework/src/modules/nft/events/all_nfts_from_collection_support_removed.ts new file mode 100644 index 00000000000..388ae16d9aa --- /dev/null +++ b/framework/src/modules/nft/events/all_nfts_from_collection_support_removed.ts @@ -0,0 +1,49 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_CHAIN_ID, LENGTH_COLLECTION_ID } from '../constants'; + +export interface AllNFTsFromCollectionSupportRemovedEventData { + chainID: Buffer; + collectionID: Buffer; +} + +export const allNFTsFromCollectionSupportRemovedEventSchema = { + $id: '/nft/events/allNFTsFromCollectionSupportRemoved', + type: 'object', + required: ['chainID', 'collectionID'], + properties: { + chainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 1, + }, + collectionID: { + dataType: 'bytes', + minLength: LENGTH_COLLECTION_ID, + maxLength: LENGTH_COLLECTION_ID, + fieldNumber: 2, + }, + }, +}; + +export class AllNFTsFromCollectionSupportRemovedEvent extends BaseEvent { + public schema = allNFTsFromCollectionSupportRemovedEventSchema; + + public log(ctx: EventQueuer, data: AllNFTsFromCollectionSupportRemovedEventData): void { + this.add(ctx, data, [data.chainID, data.collectionID]); + } +} diff --git a/framework/src/modules/nft/events/all_nfts_from_collection_suppported.ts b/framework/src/modules/nft/events/all_nfts_from_collection_suppported.ts new file mode 100644 index 00000000000..9b82b4f1539 --- /dev/null +++ b/framework/src/modules/nft/events/all_nfts_from_collection_suppported.ts @@ -0,0 +1,49 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_CHAIN_ID, LENGTH_COLLECTION_ID } from '../constants'; + +export interface AllNFTsFromCollectionSupportedEventData { + chainID: Buffer; + collectionID: Buffer; +} + +export const allNFTsFromCollectionSupportedEventSchema = { + $id: '/nft/events/allNFTsFromCollectionSupported', + type: 'object', + required: ['chainID', 'collectionID'], + properties: { + chainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 1, + }, + collectionID: { + dataType: 'bytes', + minLength: LENGTH_COLLECTION_ID, + maxLength: LENGTH_COLLECTION_ID, + fieldNumber: 2, + }, + }, +}; + +export class AllNFTsFromCollectionSupportedEvent extends BaseEvent { + public schema = allNFTsFromCollectionSupportedEventSchema; + + public log(ctx: EventQueuer, data: AllNFTsFromCollectionSupportedEventData): void { + this.add(ctx, data, [data.chainID, data.collectionID]); + } +} diff --git a/framework/src/modules/nft/events/all_nfts_support_removed.ts b/framework/src/modules/nft/events/all_nfts_support_removed.ts new file mode 100644 index 00000000000..a15f6fbe6bf --- /dev/null +++ b/framework/src/modules/nft/events/all_nfts_support_removed.ts @@ -0,0 +1,21 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; + +export class AllNFTsSupportRemovedEvent extends BaseEvent { + public log(ctx: EventQueuer): void { + this.add(ctx, undefined); + } +} diff --git a/framework/src/modules/nft/events/all_nfts_supported.ts b/framework/src/modules/nft/events/all_nfts_supported.ts new file mode 100644 index 00000000000..80f1da06a20 --- /dev/null +++ b/framework/src/modules/nft/events/all_nfts_supported.ts @@ -0,0 +1,21 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; + +export class AllNFTsSupportedEvent extends BaseEvent { + public log(ctx: EventQueuer): void { + this.add(ctx, undefined); + } +} diff --git a/framework/src/modules/nft/events/ccm_transfer.ts b/framework/src/modules/nft/events/ccm_transfer.ts new file mode 100644 index 00000000000..be4e36df185 --- /dev/null +++ b/framework/src/modules/nft/events/ccm_transfer.ts @@ -0,0 +1,86 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_CHAIN_ID, LENGTH_NFT_ID, NftEventResult } from '../constants'; + +export interface CCMTransferEventData { + senderAddress: Buffer; + recipientAddress: Buffer; + nftID: Buffer; + receivingChainID: Buffer; + sendingChainID: Buffer; +} + +export const ccmTransferEventSchema = { + $id: '/nft/events/ccmTransfer', + type: 'object', + required: [ + 'senderAddress', + 'recipientAddress', + 'nftID', + 'receivingChainID', + 'sendingChainID', + 'result', + ], + properties: { + senderAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + recipientAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 2, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 3, + }, + receivingChainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 4, + }, + sendingChainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 5, + }, + result: { + dataType: 'uint32', + fieldNumber: 6, + }, + }, +}; + +export class CcmTransferEvent extends BaseEvent { + public schema = ccmTransferEventSchema; + + public log(ctx: EventQueuer, data: CCMTransferEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [ + data.senderAddress, + data.recipientAddress, + ]); + } + + public error(ctx: EventQueuer, data: CCMTransferEventData, result: NftEventResult): void { + this.add(ctx, { ...data, result }, [data.senderAddress, data.recipientAddress], true); + } +} diff --git a/framework/src/modules/nft/events/create.ts b/framework/src/modules/nft/events/create.ts new file mode 100644 index 00000000000..e84fd3fac4b --- /dev/null +++ b/framework/src/modules/nft/events/create.ts @@ -0,0 +1,55 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_NFT_ID, NftEventResult } from '../constants'; + +export interface CreateEventData { + address: Buffer; + nftID: Buffer; +} + +export const createEventSchema = { + $id: '/nft/events/create', + type: 'object', + required: ['address', 'nftID', 'result'], + properties: { + address: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 2, + }, + result: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; + +export class CreateEvent extends BaseEvent { + public schema = createEventSchema; + + public log(ctx: EventQueuer, data: CreateEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [ + data.address, + data.nftID, + ]); + } +} diff --git a/framework/src/modules/nft/events/destroy.ts b/framework/src/modules/nft/events/destroy.ts new file mode 100644 index 00000000000..500c5579f2c --- /dev/null +++ b/framework/src/modules/nft/events/destroy.ts @@ -0,0 +1,59 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_NFT_ID, NftErrorEventResult, NftEventResult } from '../constants'; + +export interface DestroyEventData { + address: Buffer; + nftID: Buffer; +} + +export const destroyEventSchema = { + $id: '/nft/events/destroy', + type: 'object', + required: ['address', 'nftID', 'result'], + properties: { + address: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 2, + }, + result: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; + +export class DestroyEvent extends BaseEvent { + public schema = destroyEventSchema; + + public log(ctx: EventQueuer, data: DestroyEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [ + data.address, + data.nftID, + ]); + } + + public error(ctx: EventQueuer, data: DestroyEventData, result: NftErrorEventResult): void { + this.add(ctx, { ...data, result }, [data.address, data.nftID], true); + } +} diff --git a/framework/src/modules/nft/events/lock.ts b/framework/src/modules/nft/events/lock.ts new file mode 100644 index 00000000000..74ef3cd050d --- /dev/null +++ b/framework/src/modules/nft/events/lock.ts @@ -0,0 +1,66 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { + LENGTH_NFT_ID, + MAX_LENGTH_MODULE_NAME, + MIN_LENGTH_MODULE_NAME, + NftErrorEventResult, + NftEventResult, +} from '../constants'; + +export interface LockEventData { + module: string; + nftID: Buffer; +} + +export const lockEventSchema = { + $id: '/nft/events/lock', + type: 'object', + required: ['module', 'nftID', 'result'], + properties: { + module: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + fieldNumber: 1, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 2, + }, + result: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; + +export class LockEvent extends BaseEvent { + public schema = lockEventSchema; + + public log(ctx: EventQueuer, data: LockEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [ + Buffer.from(data.module), + data.nftID, + ]); + } + + public error(ctx: EventQueuer, data: LockEventData, result: NftErrorEventResult) { + this.add(ctx, { ...data, result }, [Buffer.from(data.module), data.nftID], true); + } +} diff --git a/framework/src/modules/nft/events/recover.ts b/framework/src/modules/nft/events/recover.ts new file mode 100644 index 00000000000..3997f19e7ea --- /dev/null +++ b/framework/src/modules/nft/events/recover.ts @@ -0,0 +1,57 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_NFT_ID, LENGTH_CHAIN_ID, NftEventResult, NftErrorEventResult } from '../constants'; + +export interface RecoverEventData { + terminatedChainID: Buffer; + nftID: Buffer; +} + +export const recoverEventSchema = { + $id: '/nft/events/recover', + type: 'object', + required: ['terminatedChainID', 'nftID', 'result'], + properties: { + terminatedChainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 1, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 2, + }, + result: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; + +export class RecoverEvent extends BaseEvent { + public schema = recoverEventSchema; + + public log(ctx: EventQueuer, data: RecoverEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [data.nftID]); + } + + public error(ctx: EventQueuer, data: RecoverEventData, result: NftErrorEventResult): void { + this.add(ctx, { ...data, result }, [data.nftID], true); + } +} diff --git a/framework/src/modules/nft/events/set_attributes.ts b/framework/src/modules/nft/events/set_attributes.ts new file mode 100644 index 00000000000..a83d93d5ad5 --- /dev/null +++ b/framework/src/modules/nft/events/set_attributes.ts @@ -0,0 +1,57 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_NFT_ID, NftErrorEventResult, NftEventResult } from '../constants'; + +export interface SetAttributesEventData { + nftID: Buffer; + attributes: Buffer; +} + +export const setAttributesEventSchema = { + $id: '/nft/events/setAttributes', + type: 'object', + required: ['nftID', 'attributes', 'result'], + properties: { + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 1, + }, + attributes: { + dataType: 'bytes', + fieldNumber: 2, + }, + result: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; + +export class SetAttributesEvent extends BaseEvent< + SetAttributesEventData & { result: NftEventResult } +> { + public schema = setAttributesEventSchema; + + public log(ctx: EventQueuer, data: SetAttributesEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [data.nftID]); + } + + public error(ctx: EventQueuer, data: SetAttributesEventData, result: NftErrorEventResult): void { + this.add(ctx, { ...data, result }, [data.nftID], true); + } +} diff --git a/framework/src/modules/nft/events/transfer.ts b/framework/src/modules/nft/events/transfer.ts new file mode 100644 index 00000000000..591abc8c306 --- /dev/null +++ b/framework/src/modules/nft/events/transfer.ts @@ -0,0 +1,65 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_NFT_ID, NftErrorEventResult, NftEventResult } from '../constants'; + +export interface TransferEventData { + senderAddress: Buffer; + recipientAddress: Buffer; + nftID: Buffer; +} + +export const transferEventSchema = { + $id: '/nft/events/transfer', + type: 'object', + required: ['senderAddress', 'recipientAddress', 'nftID', 'result'], + properties: { + senderAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + recipientAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 2, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 3, + }, + result: { + dataType: 'uint32', + fieldNumber: 4, + }, + }, +}; + +export class TransferEvent extends BaseEvent { + public schema = transferEventSchema; + + public log(ctx: EventQueuer, data: TransferEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [ + data.senderAddress, + data.recipientAddress, + ]); + } + + public error(ctx: EventQueuer, data: TransferEventData, result: NftErrorEventResult): void { + this.add(ctx, { ...data, result }, [data.senderAddress, data.recipientAddress], true); + } +} diff --git a/framework/src/modules/nft/events/transfer_cross_chain.ts b/framework/src/modules/nft/events/transfer_cross_chain.ts new file mode 100644 index 00000000000..67423f07329 --- /dev/null +++ b/framework/src/modules/nft/events/transfer_cross_chain.ts @@ -0,0 +1,89 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { LENGTH_NFT_ID, LENGTH_CHAIN_ID, NftEventResult, NftErrorEventResult } from '../constants'; + +export interface TransferCrossChainEventData { + senderAddress: Buffer; + recipientAddress: Buffer; + receivingChainID: Buffer; + nftID: Buffer; + includeAttributes: boolean; +} + +export const transferCrossChainEventSchema = { + $id: '/nft/events/transferCrossChain', + type: 'object', + required: ['senderAddress', 'recipientAddress', 'nftID', 'receivingChainID', 'result'], + properties: { + senderAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 1, + }, + recipientAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 2, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 3, + }, + receivingChainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 4, + }, + includeAttributes: { + dataType: 'boolean', + fieldNumber: 5, + }, + result: { + dataType: 'uint32', + fieldNumber: 6, + }, + }, +}; + +export class TransferCrossChainEvent extends BaseEvent< + TransferCrossChainEventData & { result: NftEventResult } +> { + public schema = transferCrossChainEventSchema; + + public log(ctx: EventQueuer, data: TransferCrossChainEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [ + data.senderAddress, + data.recipientAddress, + data.receivingChainID, + ]); + } + + public error( + ctx: EventQueuer, + data: TransferCrossChainEventData, + result: NftErrorEventResult, + ): void { + this.add( + ctx, + { ...data, result }, + [data.senderAddress, data.recipientAddress, data.receivingChainID], + true, + ); + } +} diff --git a/framework/src/modules/nft/events/unlock.ts b/framework/src/modules/nft/events/unlock.ts new file mode 100644 index 00000000000..05e3647b331 --- /dev/null +++ b/framework/src/modules/nft/events/unlock.ts @@ -0,0 +1,66 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { + LENGTH_NFT_ID, + MAX_LENGTH_MODULE_NAME, + MIN_LENGTH_MODULE_NAME, + NftErrorEventResult, + NftEventResult, +} from '../constants'; + +export interface UnlockEventData { + module: string; + nftID: Buffer; +} + +export const unlockEventSchema = { + $id: '/nft/events/unlock', + type: 'object', + required: ['module', 'nftID', 'result'], + properties: { + module: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + fieldNumber: 1, + }, + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 2, + }, + result: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; + +export class UnlockEvent extends BaseEvent { + public schema = unlockEventSchema; + + public log(ctx: EventQueuer, data: UnlockEventData): void { + this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [ + Buffer.from(data.module), + data.nftID, + ]); + } + + public error(ctx: EventQueuer, data: UnlockEventData, result: NftErrorEventResult) { + this.add(ctx, { ...data, result }, [Buffer.from(data.module), data.nftID], true); + } +} diff --git a/framework/src/modules/nft/index.ts b/framework/src/modules/nft/index.ts new file mode 100644 index 00000000000..14063d827fe --- /dev/null +++ b/framework/src/modules/nft/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export { NFTModule } from './module'; +export { NFTMethod } from './method'; diff --git a/framework/src/modules/nft/internal_method.ts b/framework/src/modules/nft/internal_method.ts new file mode 100644 index 00000000000..626ae05d62a --- /dev/null +++ b/framework/src/modules/nft/internal_method.ts @@ -0,0 +1,340 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +/* eslint-disable max-classes-per-file */ +import { codec } from '@liskhq/lisk-codec'; +import { BaseMethod } from '../base_method'; +import { NFTStore, NFTAttributes } from './stores/nft'; +import { InteroperabilityMethod, ModuleConfig, NFTMethod, TokenMethod } from './types'; +import { ImmutableMethodContext, MethodContext } from '../../state_machine'; +import { TransferEvent } from './events/transfer'; +import { UserStore } from './stores/user'; +import { + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + MAX_LENGTH_DATA, + MODULE_NAME_NFT, + NFT_NOT_LOCKED, + NftErrorEventResult, + NftEventResult, +} from './constants'; +import { EscrowStore } from './stores/escrow'; +import { TransferCrossChainEvent } from './events/transfer_cross_chain'; +import { crossChainNFTTransferMessageParamsSchema } from './schemas'; +import { NotFoundError } from './error'; + +export class TransferVerifyError extends Error { + public code: NftErrorEventResult; + + public constructor(message: string, code: NftErrorEventResult) { + super(message); + this.code = code; + } +} + +export class InternalMethod extends BaseMethod { + private _config!: ModuleConfig; + private _nftMethod!: NFTMethod; + private _interoperabilityMethod!: InteroperabilityMethod; + private _tokenMethod!: TokenMethod; + + public init(config: ModuleConfig): void { + this._config = config; + } + + public addDependencies( + nftMethod: NFTMethod, + interoperabilityMethod: InteroperabilityMethod, + tokenMethod: TokenMethod, + ) { + this._nftMethod = nftMethod; + this._interoperabilityMethod = interoperabilityMethod; + this._tokenMethod = tokenMethod; + } + + public async createEscrowEntry( + methodContext: MethodContext, + receivingChainID: Buffer, + nftID: Buffer, + ): Promise { + const escrowStore = this.stores.get(EscrowStore); + + await escrowStore.set(methodContext, escrowStore.getKey(receivingChainID, nftID), {}); + } + + public async createUserEntry( + methodContext: MethodContext, + address: Buffer, + nftID: Buffer, + ): Promise { + const userStore = this.stores.get(UserStore); + + await userStore.set(methodContext, userStore.getKey(address, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + } + + public async createNFTEntry( + methodContext: MethodContext, + address: Buffer, + nftID: Buffer, + attributesArray: NFTAttributes[], + ): Promise { + const hasDuplicates = this.hasDuplicateModuleNames(attributesArray); + if (hasDuplicates) { + throw new Error('Invalid attributes array provided'); + } + + const nftStore = this.stores.get(NFTStore); + await nftStore.save(methodContext, nftID, { + owner: address, + attributesArray, + }); + } + + public hasDuplicateModuleNames(attributesArray: NFTAttributes[]): boolean { + const moduleNames = []; + for (const item of attributesArray) { + moduleNames.push(item.module); + } + + return new Set(moduleNames).size !== attributesArray.length; + } + + public async verifyTransfer( + immutableMethodContext: ImmutableMethodContext, + senderAddress: Buffer, + nftID: Buffer, + ) { + let nft; + try { + nft = await this._nftMethod.getNFT(immutableMethodContext, nftID); + } catch (error) { + if (error instanceof NotFoundError) { + throw new TransferVerifyError( + 'NFT does not exist', + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + } + throw error; + } + + if (this._nftMethod.isNFTEscrowed(nft)) { + throw new TransferVerifyError( + 'NFT is escrowed to another chain', + NftEventResult.RESULT_NFT_ESCROWED, + ); + } + + if (!nft.owner.equals(senderAddress)) { + throw new TransferVerifyError( + 'Transfer not initiated by the NFT owner', + NftEventResult.RESULT_INITIATED_BY_NONOWNER, + ); + } + + if (this._nftMethod.isNFTLocked(nft)) { + throw new TransferVerifyError( + 'Locked NFTs cannot be transferred', + NftEventResult.RESULT_NFT_LOCKED, + ); + } + } + + public async verifyTransferCrossChain( + immutableMethodContext: ImmutableMethodContext, + senderAddress: Buffer, + nftID: Buffer, + sendingChainID: Buffer, + receivingChainID: Buffer, + messageFee: bigint, + data: string, + ) { + let nft; + try { + nft = await this._nftMethod.getNFT(immutableMethodContext, nftID); + } catch (error) { + if (error instanceof NotFoundError) { + throw new TransferVerifyError( + 'NFT does not exist', + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + } + throw error; + } + + const nftChainID = this._nftMethod.getChainID(nftID); + const ownChainID = this.getOwnChainID(); + if (![ownChainID, receivingChainID].some(allowedChainID => nftChainID.equals(allowedChainID))) { + throw new TransferVerifyError( + 'NFT must be native to either the sending or the receiving chain', + NftEventResult.RESULT_NFT_NOT_NATIVE, + ); + } + + if (receivingChainID.equals(sendingChainID)) { + throw new TransferVerifyError( + 'Receiving chain cannot be the sending chain', + NftEventResult.INVALID_RECEIVING_CHAIN, + ); + } + + if (data.length > MAX_LENGTH_DATA) { + throw new TransferVerifyError('Data field is too long', NftEventResult.RESULT_DATA_TOO_LONG); + } + + if (this._nftMethod.isNFTEscrowed(nft)) { + throw new TransferVerifyError( + 'NFT is escrowed to another chain', + NftEventResult.RESULT_NFT_ESCROWED, + ); + } + + if (!nft.owner.equals(senderAddress)) { + throw new TransferVerifyError( + 'Transfer not initiated by the NFT owner', + NftEventResult.RESULT_INITIATED_BY_NONOWNER, + ); + } + + if (this._nftMethod.isNFTLocked(nft)) { + throw new TransferVerifyError( + 'Locked NFTs cannot be transferred', + NftEventResult.RESULT_NFT_LOCKED, + ); + } + + const messageFeeTokenID = await this._interoperabilityMethod.getMessageFeeTokenID( + immutableMethodContext, + receivingChainID, + ); + + const availableBalance = await this._tokenMethod.getAvailableBalance( + immutableMethodContext, + senderAddress, + messageFeeTokenID, + ); + if (availableBalance < messageFee) { + throw new TransferVerifyError( + 'Insufficient balance for the message fee', + NftEventResult.RESULT_INSUFFICIENT_BALANCE, + ); + } + } + + public async transfer( + methodContext: MethodContext, + recipientAddress: Buffer, + nftID: Buffer, + ): Promise { + const nftStore = this.stores.get(NFTStore); + const userStore = this.stores.get(UserStore); + + const nft = await nftStore.get(methodContext, nftID); + const senderAddress = nft.owner; + nft.owner = recipientAddress; + await nftStore.set(methodContext, nftID, nft); + + await userStore.del(methodContext, userStore.getKey(senderAddress, nftID)); + await this.createUserEntry(methodContext, recipientAddress, nftID); + + this.events.get(TransferEvent).log(methodContext, { + senderAddress, + recipientAddress, + nftID, + }); + } + + public async transferCrossChain( + methodContext: MethodContext, + senderAddress: Buffer, + recipientAddress: Buffer, + nftID: Buffer, + receivingChainID: Buffer, + messageFee: bigint, + data: string, + includeAttributes: boolean, + timestamp?: number, + ): Promise { + const chainID = this._nftMethod.getChainID(nftID); + const nftStore = this.stores.get(NFTStore); + const nft = await nftStore.get(methodContext, nftID); + + if (chainID.equals(this._config.ownChainID)) { + const escrowStore = this.stores.get(EscrowStore); + const userStore = this.stores.get(UserStore); + + nft.owner = receivingChainID; + await nftStore.save(methodContext, nftID, nft); + + await userStore.del(methodContext, userStore.getKey(senderAddress, nftID)); + + const escrowExists = await escrowStore.has( + methodContext, + escrowStore.getKey(receivingChainID, nftID), + ); + + if (!escrowExists) { + await this.createEscrowEntry(methodContext, receivingChainID, nftID); + } + } + + if (chainID.equals(receivingChainID)) { + await this._nftMethod.destroy(methodContext, senderAddress, nftID); + } + + let attributesArray: { module: string; attributes: Buffer }[] = []; + + if (includeAttributes) { + attributesArray = nft.attributesArray; + } + + this.events.get(TransferCrossChainEvent).log(methodContext, { + senderAddress, + recipientAddress, + nftID, + receivingChainID, + includeAttributes, + }); + + await this._interoperabilityMethod.send( + methodContext, + senderAddress, + MODULE_NAME_NFT, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + receivingChainID, + messageFee, + codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID, + senderAddress, + recipientAddress, + attributesArray, + data, + }), + timestamp, + ); + } + + public getOwnChainID(): Buffer { + return this._config.ownChainID; + } + + // template for custom module to be able to define their own logic as described in https://github.com/LiskHQ/lips/blob/main/proposals/lip-0052.md#attributes + public getNewAttributes( + _nftID: Buffer, + storedAttributes: NFTAttributes[], + _receivedAttributes: NFTAttributes[], + ): NFTAttributes[] { + return storedAttributes; + } +} diff --git a/framework/src/modules/nft/method.ts b/framework/src/modules/nft/method.ts new file mode 100644 index 00000000000..9695cfedd99 --- /dev/null +++ b/framework/src/modules/nft/method.ts @@ -0,0 +1,834 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { validator } from '@liskhq/lisk-validator'; +import { codec } from '@liskhq/lisk-codec'; +import { BaseMethod } from '../base_method'; +import { FeeMethod, ModuleConfig, NFT } from './types'; +import { NFTAttributes, NFTStore, NFTStoreData, nftStoreSchema } from './stores/nft'; +import { ImmutableMethodContext, MethodContext } from '../../state_machine'; +import { + ALL_SUPPORTED_NFTS_KEY, + FEE_CREATE_NFT, + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + LENGTH_COLLECTION_ID, + LENGTH_INDEX, + LENGTH_NFT_ID, + NFT_NOT_LOCKED, + NftEventResult, +} from './constants'; +import { UserStore } from './stores/user'; +import { DestroyEvent } from './events/destroy'; +import { SupportedNFTsStore } from './stores/supported_nfts'; +import { CreateEvent } from './events/create'; +import { LockEvent } from './events/lock'; +import { TransferEvent } from './events/transfer'; +import { InternalMethod, TransferVerifyError } from './internal_method'; +import { TransferCrossChainEvent } from './events/transfer_cross_chain'; +import { AllNFTsSupportedEvent } from './events/all_nfts_supported'; +import { AllNFTsSupportRemovedEvent } from './events/all_nfts_support_removed'; +import { AllNFTsFromChainSupportedEvent } from './events/all_nfts_from_chain_suported'; +import { AllNFTsFromCollectionSupportedEvent } from './events/all_nfts_from_collection_suppported'; +import { AllNFTsFromCollectionSupportRemovedEvent } from './events/all_nfts_from_collection_support_removed'; +import { AllNFTsFromChainSupportRemovedEvent } from './events/all_nfts_from_chain_support_removed'; +import { RecoverEvent } from './events/recover'; +import { EscrowStore } from './stores/escrow'; +import { SetAttributesEvent } from './events/set_attributes'; +import { NotFoundError } from './error'; +import { UnlockEvent } from './events/unlock'; + +export class NFTMethod extends BaseMethod { + private _config!: ModuleConfig; + private _internalMethod!: InternalMethod; + private _feeMethod!: FeeMethod; + + public init(config: ModuleConfig): void { + this._config = config; + } + + public addDependencies(internalMethod: InternalMethod, feeMethod: FeeMethod) { + this._internalMethod = internalMethod; + this._feeMethod = feeMethod; + } + + public getChainID(nftID: Buffer): Buffer { + if (nftID.length !== LENGTH_NFT_ID) { + throw new Error(`NFT ID must have length ${LENGTH_NFT_ID}`); + } + + return nftID.subarray(0, LENGTH_CHAIN_ID); + } + + public isNFTEscrowed(nft: NFT): boolean { + return nft.owner.length !== LENGTH_ADDRESS; + } + + public isNFTLocked(nft: NFT): boolean { + if (!nft.lockingModule) { + return false; + } + + return nft.lockingModule !== NFT_NOT_LOCKED; + } + + public async getNFT(methodContext: ImmutableMethodContext, nftID: Buffer): Promise { + const nftStore = this.stores.get(NFTStore); + const nftExists = await nftStore.has(methodContext, nftID); + + if (!nftExists) { + throw new NotFoundError('NFT substore entry does not exist'); + } + + const data = await nftStore.get(methodContext, nftID); + const { owner } = data; + + if (owner.length === LENGTH_ADDRESS) { + const userStore = this.stores.get(UserStore); + const userExists = await userStore.has(methodContext, userStore.getKey(owner, nftID)); + if (!userExists) { + throw new NotFoundError('User substore entry does not exist'); + } + const userData = await userStore.get(methodContext, userStore.getKey(owner, nftID)); + return { ...data, lockingModule: userData.lockingModule }; + } + + return data; + } + + public async destroy( + methodContext: MethodContext, + address: Buffer, + nftID: Buffer, + ): Promise { + let nft; + try { + nft = await this.getNFT(methodContext, nftID); + } catch (error) { + if (error instanceof NotFoundError) { + this.events.get(DestroyEvent).error( + methodContext, + { + address, + nftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + + throw new Error('NFT does not exist'); + } + throw error; + } + + if (this.isNFTEscrowed(nft)) { + this.events.get(DestroyEvent).error( + methodContext, + { + address, + nftID, + }, + NftEventResult.RESULT_NFT_ESCROWED, + ); + + throw new Error('NFT is escrowed to another chain'); + } + + if (!nft.owner.equals(address)) { + this.events.get(DestroyEvent).error( + methodContext, + { + address, + nftID, + }, + NftEventResult.RESULT_INITIATED_BY_NONOWNER, + ); + + throw new Error('Not initiated by the NFT owner'); + } + + if (this.isNFTLocked(nft)) { + this.events.get(DestroyEvent).error( + methodContext, + { + address, + nftID, + }, + NftEventResult.RESULT_NFT_LOCKED, + ); + + throw new Error('Locked NFTs cannot be destroyed'); + } + + const nftStore = this.stores.get(NFTStore); + const userStore = this.stores.get(UserStore); + await nftStore.del(methodContext, nftID); + await userStore.del(methodContext, userStore.getKey(nft.owner, nftID)); + + this.events.get(DestroyEvent).log(methodContext, { + address, + nftID, + }); + } + + public getCollectionID(nftID: Buffer): Buffer { + return nftID.subarray(LENGTH_CHAIN_ID, LENGTH_CHAIN_ID + LENGTH_COLLECTION_ID); + } + + public async isNFTSupported( + methodContext: ImmutableMethodContext, + nftID: Buffer, + ): Promise { + const nftChainID = this.getChainID(nftID); + if (nftChainID.equals(this._config.ownChainID)) { + return true; + } + + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + const supportForAllKeysExists = await supportedNFTsStore.has( + methodContext, + ALL_SUPPORTED_NFTS_KEY, + ); + if (supportForAllKeysExists) { + return true; + } + + const supportForNftChainIdExists = await supportedNFTsStore.has(methodContext, nftChainID); + if (supportForNftChainIdExists) { + const supportedNFTsStoreData = await supportedNFTsStore.get(methodContext, nftChainID); + if (supportedNFTsStoreData.supportedCollectionIDArray.length === 0) { + return true; + } + const collectionID = this.getCollectionID(nftID); + if ( + supportedNFTsStoreData.supportedCollectionIDArray.some(id => + collectionID.equals(id.collectionID), + ) + ) { + return true; + } + } + + return false; + } + + public async getNextAvailableIndex( + methodContext: MethodContext, + collectionID: Buffer, + ): Promise { + const nftStore = this.stores.get(NFTStore); + + const nftStoreData = await nftStore.iterate(methodContext, { + gte: Buffer.concat([this._config.ownChainID, collectionID, Buffer.alloc(LENGTH_INDEX, 0)]), + lte: Buffer.concat([this._config.ownChainID, collectionID, Buffer.alloc(LENGTH_INDEX, 255)]), + }); + + if (nftStoreData.length === 0) { + return BigInt(0); + } + + const latestKey = nftStoreData[nftStoreData.length - 1].key; + const indexBytes = latestKey.subarray(LENGTH_CHAIN_ID + LENGTH_COLLECTION_ID, LENGTH_NFT_ID); + const index = indexBytes.readBigUInt64BE(); + const largestIndex = BigInt(BigInt(2 ** 64) - BigInt(1)); + + if (index === largestIndex) { + throw new Error('No more available indexes'); + } + + return index + BigInt(1); + } + + public async create( + methodContext: MethodContext, + address: Buffer, + collectionID: Buffer, + attributesArray: NFTAttributes[], + ): Promise { + const index = await this.getNextAvailableIndex(methodContext, collectionID); + const indexBytes = Buffer.alloc(LENGTH_INDEX); + indexBytes.writeBigInt64BE(index); + + const nftID = Buffer.concat([this._config.ownChainID, collectionID, indexBytes]); + this._feeMethod.payFee(methodContext, BigInt(FEE_CREATE_NFT)); + + await this._internalMethod.createNFTEntry(methodContext, address, nftID, attributesArray); + + await this._internalMethod.createUserEntry(methodContext, address, nftID); + + this.events.get(CreateEvent).log(methodContext, { + address, + nftID, + }); + } + + public async lock(methodContext: MethodContext, module: string, nftID: Buffer): Promise { + if (module === NFT_NOT_LOCKED) { + throw new Error('Cannot be locked by NFT module'); + } + + let nft; + try { + nft = await this.getNFT(methodContext, nftID); + } catch (error) { + if (error instanceof NotFoundError) { + this.events.get(LockEvent).error( + methodContext, + { + module, + nftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + + throw new Error('NFT does not exist'); + } + throw error; + } + + if (this.isNFTEscrowed(nft)) { + this.events.get(LockEvent).error( + methodContext, + { + module, + nftID, + }, + NftEventResult.RESULT_NFT_ESCROWED, + ); + + throw new Error('NFT is escrowed to another chain'); + } + + if (this.isNFTLocked(nft)) { + this.events.get(LockEvent).error( + methodContext, + { + module, + nftID, + }, + NftEventResult.RESULT_NFT_LOCKED, + ); + + throw new Error('NFT is already locked'); + } + + const userStore = this.stores.get(UserStore); + await userStore.set(methodContext, userStore.getKey(nft.owner, nftID), { + lockingModule: module, + }); + + this.events.get(LockEvent).log(methodContext, { + module, + nftID, + }); + } + + public async unlock(methodContext: MethodContext, module: string, nftID: Buffer): Promise { + let nft; + try { + nft = await this.getNFT(methodContext, nftID); + } catch (error) { + if (error instanceof NotFoundError) { + this.events.get(UnlockEvent).error( + methodContext, + { + module, + nftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + + throw new Error('NFT does not exist'); + } + throw error; + } + + if (this.isNFTEscrowed(nft)) { + throw new Error('NFT is escrowed to another chain'); + } + + if (!this.isNFTLocked(nft)) { + this.events.get(UnlockEvent).error( + methodContext, + { + module, + nftID, + }, + NftEventResult.RESULT_NFT_NOT_LOCKED, + ); + + throw new Error('NFT is not locked'); + } + + if (nft.lockingModule !== module) { + this.events.get(UnlockEvent).error( + methodContext, + { + module, + nftID, + }, + NftEventResult.RESULT_UNAUTHORIZED_UNLOCK, + ); + + throw new Error('Unlocking NFT via module that did not lock it'); + } + + const userStore = this.stores.get(UserStore); + await userStore.set(methodContext, userStore.getKey(nft.owner, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + this.events.get(UnlockEvent).log(methodContext, { + module, + nftID, + }); + } + + public async transfer( + methodContext: MethodContext, + senderAddress: Buffer, + recipientAddress: Buffer, + nftID: Buffer, + ): Promise { + try { + await this._internalMethod.verifyTransfer(methodContext, senderAddress, nftID); + } catch (error) { + if (error instanceof TransferVerifyError) { + this.events.get(TransferEvent).error( + methodContext, + { + senderAddress, + recipientAddress, + nftID, + }, + error.code, + ); + } + + throw error; + } + + await this._internalMethod.transfer(methodContext, recipientAddress, nftID); + } + + public async transferCrossChain( + methodContext: MethodContext, + senderAddress: Buffer, + recipientAddress: Buffer, + nftID: Buffer, + receivingChainID: Buffer, + messageFee: bigint, + data: string, + includeAttributes: boolean, + ): Promise { + try { + await this._internalMethod.verifyTransferCrossChain( + methodContext, + senderAddress, + nftID, + this._internalMethod.getOwnChainID(), + receivingChainID, + messageFee, + data, + ); + } catch (error) { + if (error instanceof TransferVerifyError) { + this.events.get(TransferCrossChainEvent).error( + methodContext, + { + senderAddress, + recipientAddress, + receivingChainID, + nftID, + includeAttributes, + }, + error.code, + ); + } + + throw error; + } + + await this._internalMethod.transferCrossChain( + methodContext, + senderAddress, + recipientAddress, + nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ); + } + + public async supportAllNFTs(methodContext: MethodContext): Promise { + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + + const alreadySupported = await supportedNFTsStore.has(methodContext, ALL_SUPPORTED_NFTS_KEY); + + if (alreadySupported) { + return; + } + + const allSupportedNFTs = await supportedNFTsStore.getAll(methodContext); + + for (const { key } of allSupportedNFTs) { + await supportedNFTsStore.del(methodContext, key); + } + + await supportedNFTsStore.set(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + this.events.get(AllNFTsSupportedEvent).log(methodContext); + } + + public async removeSupportAllNFTs(methodContext: MethodContext): Promise { + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + + const allSupportedNFTs = await supportedNFTsStore.getAll(methodContext); + + for (const { key } of allSupportedNFTs) { + await supportedNFTsStore.del(methodContext, key); + } + + await supportedNFTsStore.del(methodContext, ALL_SUPPORTED_NFTS_KEY); + + this.events.get(AllNFTsSupportRemovedEvent).log(methodContext); + } + + public async supportAllNFTsFromChain( + methodContext: MethodContext, + chainID: Buffer, + ): Promise { + if (chainID.equals(this._config.ownChainID)) { + return; + } + + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + const allNFTsSuppported = await supportedNFTsStore.has(methodContext, ALL_SUPPORTED_NFTS_KEY); + + if (allNFTsSuppported) { + return; + } + + const chainSupportExists = await supportedNFTsStore.has(methodContext, chainID); + + if (chainSupportExists) { + const supportedCollections = await supportedNFTsStore.get(methodContext, chainID); + + if (supportedCollections.supportedCollectionIDArray.length === 0) { + return; + } + } + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + this.events.get(AllNFTsFromChainSupportedEvent).log(methodContext, chainID); + } + + public async removeSupportAllNFTsFromChain( + methodContext: MethodContext, + chainID: Buffer, + ): Promise { + if (chainID.equals(this._config.ownChainID)) { + throw new Error('Support for native NFTs cannot be removed'); + } + + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + + const allNFTsSupported = await supportedNFTsStore.has(methodContext, ALL_SUPPORTED_NFTS_KEY); + + if (allNFTsSupported) { + throw new Error('All NFTs from all chains are supported'); + } + + const isChainSupported = await supportedNFTsStore.has(methodContext, chainID); + + if (!isChainSupported) { + return; + } + + await supportedNFTsStore.del(methodContext, chainID); + + this.events.get(AllNFTsFromChainSupportRemovedEvent).log(methodContext, chainID); + } + + public async supportAllNFTsFromCollection( + methodContext: MethodContext, + chainID: Buffer, + collectionID: Buffer, + ): Promise { + if (chainID.equals(this._config.ownChainID)) { + return; + } + + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + const allNFTsSupported = await supportedNFTsStore.has(methodContext, ALL_SUPPORTED_NFTS_KEY); + + if (allNFTsSupported) { + return; + } + + const isChainSupported = await supportedNFTsStore.has(methodContext, chainID); + + let supportedChainData; + if (isChainSupported) { + supportedChainData = await supportedNFTsStore.get(methodContext, chainID); + + if (supportedChainData.supportedCollectionIDArray.length === 0) { + return; + } + + if ( + supportedChainData.supportedCollectionIDArray.some(collection => + collection.collectionID.equals(collectionID), + ) + ) { + return; + } + + supportedChainData.supportedCollectionIDArray.push({ collectionID }); + + await supportedNFTsStore.save(methodContext, chainID, supportedChainData); + + this.events.get(AllNFTsFromCollectionSupportedEvent).log(methodContext, { + chainID, + collectionID, + }); + + return; + } + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID, + }, + ], + }); + + this.events.get(AllNFTsFromCollectionSupportedEvent).log(methodContext, { + chainID, + collectionID, + }); + } + + public async removeSupportAllNFTsFromCollection( + methodContext: MethodContext, + chainID: Buffer, + collectionID: Buffer, + ): Promise { + if (chainID.equals(this._config.ownChainID)) { + throw new Error('Invalid operation. Support for native NFTs cannot be removed'); + } + + const supportedNFTsStore = this.stores.get(SupportedNFTsStore); + + const allNFTsSupported = await supportedNFTsStore.has(methodContext, ALL_SUPPORTED_NFTS_KEY); + + if (allNFTsSupported) { + throw new Error('All NFTs from all chains are supported'); + } + + const isChainSupported = await supportedNFTsStore.has(methodContext, chainID); + + if (!isChainSupported) { + return; + } + const supportedChainData = await supportedNFTsStore.get(methodContext, chainID); + + if (supportedChainData.supportedCollectionIDArray.length === 0) { + throw new Error('All NFTs from the specified chain are supported'); + } + + if ( + supportedChainData.supportedCollectionIDArray.some(supportedCollection => + supportedCollection.collectionID.equals(collectionID), + ) + ) { + supportedChainData.supportedCollectionIDArray = + supportedChainData.supportedCollectionIDArray.filter( + supportedCollection => !supportedCollection.collectionID.equals(collectionID), + ); + } + + if (supportedChainData.supportedCollectionIDArray.length === 0) { + await supportedNFTsStore.del(methodContext, chainID); + } else { + await supportedNFTsStore.save(methodContext, chainID, { + ...supportedChainData, + }); + } + + this.events.get(AllNFTsFromCollectionSupportRemovedEvent).log(methodContext, { + chainID, + collectionID, + }); + } + + public async recover( + methodContext: MethodContext, + terminatedChainID: Buffer, + substorePrefix: Buffer, + nftID: Buffer, + nft: Buffer, + ): Promise { + const nftStore = this.stores.get(NFTStore); + let isValidInput = true; + let decodedValue: NFTStoreData; + try { + decodedValue = codec.decode(nftStoreSchema, nft); + validator.validate(nftStoreSchema, decodedValue); + } catch (error) { + isValidInput = false; + } + + if ( + !substorePrefix.equals(nftStore.subStorePrefix) || + nftID.length !== LENGTH_NFT_ID || + !isValidInput + ) { + this.events.get(RecoverEvent).error( + methodContext, + { + terminatedChainID, + nftID, + }, + NftEventResult.RESULT_RECOVER_FAIL_INVALID_INPUTS, + ); + throw new Error('Invalid inputs'); + } + + const nftChainID = this.getChainID(nftID); + const ownChainID = this._internalMethod.getOwnChainID(); + if (!nftChainID.equals(ownChainID)) { + this.events.get(RecoverEvent).error( + methodContext, + { + terminatedChainID, + nftID, + }, + NftEventResult.RESULT_INITIATED_BY_NONNATIVE_CHAIN, + ); + throw new Error('Recovery called by a foreign chain'); + } + + let nftData; + try { + nftData = await this.getNFT(methodContext, nftID); + } catch (error) { + if (error instanceof NotFoundError) { + this.events.get(RecoverEvent).error( + methodContext, + { + terminatedChainID, + nftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + + throw new Error('NFT substore entry does not exist'); + } + throw error; + } + + if (!nftData.owner.equals(terminatedChainID)) { + this.events.get(RecoverEvent).error( + methodContext, + { + terminatedChainID, + nftID, + }, + NftEventResult.RESULT_NFT_NOT_ESCROWED, + ); + throw new Error('NFT was not escrowed to terminated chain'); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const storeValueOwner = decodedValue!.owner; + if (storeValueOwner.length !== LENGTH_ADDRESS) { + this.events.get(RecoverEvent).error( + methodContext, + { + terminatedChainID, + nftID, + }, + NftEventResult.RESULT_INVALID_ACCOUNT, + ); + throw new Error('Invalid account information'); + } + + const escrowStore = this.stores.get(EscrowStore); + nftData.owner = storeValueOwner; + const storedAttributes = nftData.attributesArray; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const receivedAttributes = decodedValue!.attributesArray; + nftData.attributesArray = this._internalMethod.getNewAttributes( + nftID, + storedAttributes, + receivedAttributes, + ); + await this._internalMethod.createNFTEntry( + methodContext, + nftData.owner, + nftID, + nftData.attributesArray, + ); + await this._internalMethod.createUserEntry(methodContext, nftData.owner, nftID); + await escrowStore.del(methodContext, escrowStore.getKey(terminatedChainID, nftID)); + + this.events.get(RecoverEvent).log(methodContext, { + terminatedChainID, + nftID, + }); + } + + public async setAttributes( + methodContext: MethodContext, + module: string, + nftID: Buffer, + attributes: Buffer, + ): Promise { + const nftStore = this.stores.get(NFTStore); + const nftExists = await nftStore.has(methodContext, nftID); + if (!nftExists) { + this.events.get(SetAttributesEvent).error( + methodContext, + { + nftID, + attributes, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + throw new Error('NFT substore entry does not exist'); + } + + const nft = await nftStore.get(methodContext, nftID); + const index = nft.attributesArray.findIndex(attr => attr.module === module); + if (index > -1) { + nft.attributesArray[index] = { module, attributes }; + } else { + nft.attributesArray.push({ module, attributes }); + } + + await this._internalMethod.createNFTEntry(methodContext, nft.owner, nftID, nft.attributesArray); + + this.events.get(SetAttributesEvent).log(methodContext, { + nftID, + attributes, + }); + } +} diff --git a/framework/src/modules/nft/module.ts b/framework/src/modules/nft/module.ts new file mode 100644 index 00000000000..7521b750b36 --- /dev/null +++ b/framework/src/modules/nft/module.ts @@ -0,0 +1,294 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { dataStructures } from '@liskhq/lisk-utils'; +import { codec } from '@liskhq/lisk-codec'; +import { validator } from '@liskhq/lisk-validator'; +import { GenesisBlockExecuteContext } from '../../state_machine'; +import { ModuleInitArgs, ModuleMetadata } from '../base_module'; +import { BaseInteroperableModule } from '../interoperability'; +import { InteroperabilityMethod, FeeMethod, GenesisNFTStore, TokenMethod } from './types'; +import { NFTInteroperableMethod } from './cc_method'; +import { NFTEndpoint } from './endpoint'; +import { AllNFTsFromChainSupportedEvent } from './events/all_nfts_from_chain_suported'; +import { AllNFTsFromChainSupportRemovedEvent } from './events/all_nfts_from_chain_support_removed'; +import { AllNFTsFromCollectionSupportRemovedEvent } from './events/all_nfts_from_collection_support_removed'; +import { AllNFTsFromCollectionSupportedEvent } from './events/all_nfts_from_collection_suppported'; +import { AllNFTsSupportRemovedEvent } from './events/all_nfts_support_removed'; +import { AllNFTsSupportedEvent } from './events/all_nfts_supported'; +import { CcmTransferEvent } from './events/ccm_transfer'; +import { CreateEvent } from './events/create'; +import { DestroyEvent } from './events/destroy'; +import { LockEvent } from './events/lock'; +import { RecoverEvent } from './events/recover'; +import { SetAttributesEvent } from './events/set_attributes'; +import { TransferEvent } from './events/transfer'; +import { TransferCrossChainEvent } from './events/transfer_cross_chain'; +import { UnlockEvent } from './events/unlock'; +import { InternalMethod } from './internal_method'; +import { NFTMethod } from './method'; +import { + isCollectionIDSupportedRequestSchema, + isCollectionIDSupportedResponseSchema, + getSupportedCollectionIDsResponseSchema, + getEscrowedNFTIDsRequestSchema, + getEscrowedNFTIDsResponseSchema, + getNFTRequestSchema, + getNFTResponseSchema, + getNFTsRequestSchema, + getNFTsResponseSchema, + hasNFTRequestSchema, + hasNFTResponseSchema, + isNFTSupportedRequestSchema, + isNFTSupportedResponseSchema, + genesisNFTStoreSchema, +} from './schemas'; +import { EscrowStore } from './stores/escrow'; +import { NFTStore } from './stores/nft'; +import { SupportedNFTsStore } from './stores/supported_nfts'; +import { UserStore } from './stores/user'; +import { CrossChainTransferCommand as CrossChainTransferMessageCommand } from './cc_commands/cc_transfer'; +import { TransferCrossChainCommand } from './commands/transfer_cross_chain'; +import { TransferCommand } from './commands/transfer'; +import { + ALL_SUPPORTED_NFTS_KEY, + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + MODULE_NAME_NFT, +} from './constants'; + +export class NFTModule extends BaseInteroperableModule { + public method = new NFTMethod(this.stores, this.events); + public endpoint = new NFTEndpoint(this.stores, this.offchainStores); + public crossChainMethod = new NFTInteroperableMethod(this.stores, this.events); + public crossChainTransferCommand = new CrossChainTransferMessageCommand(this.stores, this.events); + public crossChainCommand = [this.crossChainTransferCommand]; + + private readonly _transferCommand = new TransferCommand(this.stores, this.events); + private readonly _ccTransferCommand = new TransferCrossChainCommand(this.stores, this.events); + private readonly _internalMethod = new InternalMethod(this.stores, this.events); + private _interoperabilityMethod!: InteroperabilityMethod; + private _feeMethod!: FeeMethod; + private _tokenMethod!: TokenMethod; + + public commands = [this._transferCommand, this._ccTransferCommand]; + + public constructor() { + super(); + this.events.register(TransferEvent, new TransferEvent(this.name)); + this.events.register(TransferCrossChainEvent, new TransferCrossChainEvent(this.name)); + this.events.register(CcmTransferEvent, new CcmTransferEvent(this.name)); + this.events.register(CreateEvent, new CreateEvent(this.name)); + this.events.register(DestroyEvent, new DestroyEvent(this.name)); + this.events.register(DestroyEvent, new DestroyEvent(this.name)); + this.events.register(LockEvent, new LockEvent(this.name)); + this.events.register(UnlockEvent, new UnlockEvent(this.name)); + this.events.register(SetAttributesEvent, new SetAttributesEvent(this.name)); + this.events.register(RecoverEvent, new RecoverEvent(this.name)); + this.events.register(AllNFTsSupportedEvent, new AllNFTsSupportedEvent(this.name)); + this.events.register(AllNFTsSupportRemovedEvent, new AllNFTsSupportRemovedEvent(this.name)); + this.events.register( + AllNFTsFromChainSupportedEvent, + new AllNFTsFromChainSupportedEvent(this.name), + ); + this.events.register( + AllNFTsFromChainSupportRemovedEvent, + new AllNFTsFromChainSupportRemovedEvent(this.name), + ); + this.events.register( + AllNFTsFromCollectionSupportedEvent, + new AllNFTsFromCollectionSupportedEvent(this.name), + ); + this.events.register( + AllNFTsFromCollectionSupportRemovedEvent, + new AllNFTsFromCollectionSupportRemovedEvent(this.name), + ); + this.stores.register(NFTStore, new NFTStore(this.name, 0)); + this.stores.register(UserStore, new UserStore(this.name, 1)); + this.stores.register(EscrowStore, new EscrowStore(this.name, 2)); + this.stores.register(SupportedNFTsStore, new SupportedNFTsStore(this.name, 3)); + } + + public get name(): string { + return MODULE_NAME_NFT; + } + + public addDependencies( + interoperabilityMethod: InteroperabilityMethod, + feeMethod: FeeMethod, + tokenMethod: TokenMethod, + ) { + this._interoperabilityMethod = interoperabilityMethod; + this._feeMethod = feeMethod; + this._tokenMethod = tokenMethod; + this.method.addDependencies(this._internalMethod, feeMethod); + this._internalMethod.addDependencies(this.method, this._interoperabilityMethod, tokenMethod); + this.crossChainMethod.addDependencies(interoperabilityMethod); + this.endpoint.addDependencies(this.method); + } + + public metadata(): ModuleMetadata { + return { + ...this.baseMetadata(), + endpoints: [ + { + name: this.endpoint.isCollectionIDSupported.name, + request: isCollectionIDSupportedRequestSchema, + response: isCollectionIDSupportedResponseSchema, + }, + { + name: this.endpoint.getSupportedCollectionIDs.name, + response: getSupportedCollectionIDsResponseSchema, + }, + { + name: this.endpoint.getEscrowedNFTIDs.name, + request: getEscrowedNFTIDsRequestSchema, + response: getEscrowedNFTIDsResponseSchema, + }, + { + name: this.endpoint.getNFT.name, + request: getNFTRequestSchema, + response: getNFTResponseSchema, + }, + { + name: this.endpoint.getNFTs.name, + request: getNFTsRequestSchema, + response: getNFTsResponseSchema, + }, + { + name: this.endpoint.hasNFT.name, + request: hasNFTRequestSchema, + response: hasNFTResponseSchema, + }, + { + name: this.endpoint.isNFTSupported.name, + request: isNFTSupportedRequestSchema, + response: isNFTSupportedResponseSchema, + }, + ], + assets: [], + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async init(args: ModuleInitArgs) { + const ownChainID = Buffer.from(args.genesisConfig.chainID, 'hex'); + this._internalMethod.init({ ownChainID }); + this.method.init({ ownChainID }); + this.crossChainTransferCommand.init({ + method: this.method, + internalMethod: this._internalMethod, + feeMethod: this._feeMethod, + }); + + this._ccTransferCommand.init({ + internalMethod: this._internalMethod, + interoperabilityMethod: this._interoperabilityMethod, + nftMethod: this.method, + tokenMethod: this._tokenMethod, + }); + this._transferCommand.init({ internalMethod: this._internalMethod }); + } + + public async initGenesisState(context: GenesisBlockExecuteContext): Promise { + const assetBytes = context.assets.getAsset(this.name); + + if (!assetBytes) { + return; + } + + const genesisStore = codec.decode(genesisNFTStoreSchema, assetBytes); + validator.validate(genesisNFTStoreSchema, genesisStore); + + const nftIDKeySet = new dataStructures.BufferSet(); + + for (const nft of genesisStore.nftSubstore) { + if (![LENGTH_CHAIN_ID, LENGTH_ADDRESS].includes(nft.owner.length)) { + throw new Error(`nftID ${nft.nftID.toString('hex')} has invalid owner`); + } + + if (nftIDKeySet.has(nft.nftID)) { + throw new Error(`nftID ${nft.nftID.toString('hex')} duplicated`); + } + + const attributeSet: Record = {}; + + for (const attribute of nft.attributesArray) { + attributeSet[attribute.module] = (attributeSet[attribute.module] ?? 0) + 1; + + if (attributeSet[attribute.module] > 1) { + throw new Error( + `nftID ${nft.nftID.toString('hex')} has a duplicate attribute for ${ + attribute.module + } module`, + ); + } + } + + nftIDKeySet.add(nft.nftID); + } + + const allNFTsSupported = genesisStore.supportedNFTsSubstore.some(supportedNFTs => + supportedNFTs.chainID.equals(ALL_SUPPORTED_NFTS_KEY), + ); + + if (genesisStore.supportedNFTsSubstore.length > 1 && allNFTsSupported) { + throw new Error( + 'SupportedNFTsSubstore should contain only one entry if all NFTs are supported', + ); + } + + if ( + allNFTsSupported && + genesisStore.supportedNFTsSubstore[0].supportedCollectionIDArray.length !== 0 + ) { + throw new Error('supportedCollectionIDArray must be empty if all NFTs are supported'); + } + + const supportedChainsKeySet = new dataStructures.BufferSet(); + for (const supportedNFT of genesisStore.supportedNFTsSubstore) { + if (supportedChainsKeySet.has(supportedNFT.chainID)) { + throw new Error(`chainID ${supportedNFT.chainID.toString('hex')} duplicated`); + } + + supportedChainsKeySet.add(supportedNFT.chainID); + } + + for (const nft of genesisStore.nftSubstore) { + const { owner, nftID, attributesArray } = nft; + + await this._internalMethod.createNFTEntry( + context.getMethodContext(), + owner, + nftID, + attributesArray, + ); + + if (owner.length === LENGTH_CHAIN_ID) { + await this._internalMethod.createEscrowEntry(context.getMethodContext(), owner, nftID); + } else { + await this._internalMethod.createUserEntry(context.getMethodContext(), owner, nftID); + } + } + + for (const supportedNFT of genesisStore.supportedNFTsSubstore) { + const { chainID, supportedCollectionIDArray } = supportedNFT; + const supportedNFTsSubstore = this.stores.get(SupportedNFTsStore); + + await supportedNFTsSubstore.save(context, chainID, { + supportedCollectionIDArray, + }); + } + } +} diff --git a/framework/src/modules/nft/schemas.ts b/framework/src/modules/nft/schemas.ts new file mode 100644 index 00000000000..204a6535cec --- /dev/null +++ b/framework/src/modules/nft/schemas.ts @@ -0,0 +1,446 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { + LENGTH_CHAIN_ID, + LENGTH_COLLECTION_ID, + LENGTH_NFT_ID, + MAX_LENGTH_MODULE_NAME, + MIN_LENGTH_MODULE_NAME, + MAX_LENGTH_DATA, +} from './constants'; + +export const transferParamsSchema = { + $id: '/lisk/nftTransferParams', + type: 'object', + required: ['nftID', 'recipientAddress', 'data'], + properties: { + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 1, + }, + recipientAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 2, + }, + data: { + dataType: 'string', + minLength: 0, + maxLength: MAX_LENGTH_DATA, + fieldNumber: 3, + }, + }, +}; + +export const crossChainNFTTransferMessageParamsSchema = { + $id: '/lisk/crossChainNFTTransferMessageParamsSchmema', + type: 'object', + required: ['nftID', 'senderAddress', 'recipientAddress', 'attributesArray', 'data'], + properties: { + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 1, + }, + senderAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 2, + }, + recipientAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 3, + }, + attributesArray: { + type: 'array', + fieldNumber: 4, + items: { + type: 'object', + required: ['module', 'attributes'], + properties: { + module: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + pattern: '^[a-zA-Z0-9]*$', + fieldNumber: 1, + }, + attributes: { + dataType: 'bytes', + fieldNumber: 2, + }, + }, + }, + }, + data: { + dataType: 'string', + maxLength: MAX_LENGTH_DATA, + fieldNumber: 5, + }, + }, +}; + +export interface CCTransferMessageParams { + nftID: Buffer; + attributesArray: { module: string; attributes: Buffer }[]; + senderAddress: Buffer; + recipientAddress: Buffer; + data: string; +} + +export const crossChainTransferParamsSchema = { + $id: '/lisk/crossChainNFTTransferParamsSchema', + type: 'object', + required: [ + 'nftID', + 'receivingChainID', + 'recipientAddress', + 'data', + 'messageFee', + 'includeAttributes', + ], + properties: { + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 1, + }, + receivingChainID: { + dataType: 'bytes', + minLength: LENGTH_CHAIN_ID, + maxLength: LENGTH_CHAIN_ID, + fieldNumber: 2, + }, + recipientAddress: { + dataType: 'bytes', + format: 'lisk32', + fieldNumber: 3, + }, + data: { + dataType: 'string', + minLength: 0, + maxLength: MAX_LENGTH_DATA, + fieldNumber: 4, + }, + messageFee: { + dataType: 'uint64', + fieldNumber: 5, + }, + includeAttributes: { + dataType: 'boolean', + fieldNumber: 6, + }, + }, +}; + +export const getNFTsRequestSchema = { + $id: '/nft/endpoint/getNFTsRequest', + type: 'object', + properties: { + address: { + type: 'string', + format: 'lisk32', + }, + }, + required: ['address'], +}; + +export const getNFTsResponseSchema = { + $id: '/nft/endpoint/getNFTsResponse', + type: 'object', + properties: { + nfts: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'hex', + }, + attributesArray: { + type: 'array', + items: { + type: 'object', + properties: { + module: { + type: 'string', + }, + attributes: { + type: 'string', + format: 'hex', + }, + }, + }, + }, + lockingModule: { + type: 'string', + }, + }, + }, + }, + }, +}; + +export const hasNFTRequestSchema = { + $id: '/nft/endpoint/hasNFTRequest', + type: 'object', + properties: { + address: { + type: 'string', + format: 'lisk32', + }, + id: { + type: 'string', + format: 'hex', + minLength: LENGTH_NFT_ID * 2, + maxLength: LENGTH_NFT_ID * 2, + }, + }, + required: ['address', 'id'], +}; + +export const hasNFTResponseSchema = { + $id: '/nft/endpoint/hasNFTResponse', + type: 'object', + properties: { + hasNFT: { + type: 'boolean', + }, + }, +}; + +export const getNFTRequestSchema = { + $id: '/nft/endpoint/getNFTRequest', + type: 'object', + properties: { + id: { + type: 'string', + format: 'hex', + minLength: LENGTH_NFT_ID * 2, + maxLength: LENGTH_NFT_ID * 2, + }, + }, + required: ['id'], +}; + +export const getNFTResponseSchema = { + $id: '/nft/endpoint/getNFTResponse', + type: 'object', + properties: { + owner: { + type: 'string', + format: 'hex', + }, + attributesArray: { + type: 'array', + items: { + type: 'object', + properties: { + module: { + type: 'string', + }, + attributes: { + type: 'string', + format: 'hex', + }, + }, + }, + }, + lockingModule: { + type: 'string', + }, + }, +}; + +export const getSupportedCollectionIDsResponseSchema = { + $id: '/nft/endpoint/getSupportedCollectionIDsRespone', + type: 'object', + properties: { + supportedCollectionIDs: { + type: 'array', + items: { + type: 'string', + format: 'hex', + }, + }, + }, +}; + +export const isCollectionIDSupportedRequestSchema = { + $id: '/nft/endpoint/isCollectionIDSupportedRequest', + type: 'object', + properties: { + chainID: { + type: 'string', + format: 'hex', + minLength: LENGTH_CHAIN_ID * 2, + maxLength: LENGTH_CHAIN_ID * 2, + }, + collectionID: { + type: 'string', + format: 'hex', + minLength: LENGTH_COLLECTION_ID * 2, + maxLength: LENGTH_COLLECTION_ID * 2, + }, + }, + required: ['chainID', 'collectionID'], +}; + +export const isCollectionIDSupportedResponseSchema = { + $id: '/nft/endpoint/isCollectionIDSupportedResponse', + type: 'object', + properties: { + isCollectionIDSupported: { + type: 'boolean', + }, + }, +}; + +export const getEscrowedNFTIDsRequestSchema = { + $id: '/nft/endpoint/getEscrowedNFTIDsRequest', + type: 'object', + properties: { + chainID: { + type: 'string', + format: 'hex', + minLength: LENGTH_CHAIN_ID * 2, + maxLength: LENGTH_CHAIN_ID * 2, + }, + }, + required: ['chainID'], +}; + +export const getEscrowedNFTIDsResponseSchema = { + $id: '/nft/endpoint/getEscrowedNFTIDsResponse', + type: 'object', + properties: { + escrowedNFTIDs: { + type: 'array', + items: { + type: 'string', + format: 'hex', + }, + }, + }, +}; + +export const isNFTSupportedRequestSchema = { + $id: '/nft/endpoint/isNFTSupportedRequest', + type: 'object', + properties: { + nftID: { + type: 'string', + format: 'hex', + minLength: LENGTH_NFT_ID * 2, + maxLength: LENGTH_NFT_ID * 2, + }, + }, + required: ['nftID'], +}; + +export const isNFTSupportedResponseSchema = { + $id: '/nft/endpoint/isNFTSupportedResponse', + type: 'object', + properties: { + isNFTSupported: { + type: 'boolean', + }, + }, +}; + +export const genesisNFTStoreSchema = { + $id: '/nft/module/genesis', + type: 'object', + required: ['nftSubstore', 'supportedNFTsSubstore'], + properties: { + nftSubstore: { + type: 'array', + fieldNumber: 1, + items: { + type: 'object', + required: ['nftID', 'owner', 'attributesArray'], + properties: { + nftID: { + dataType: 'bytes', + minLength: LENGTH_NFT_ID, + maxLength: LENGTH_NFT_ID, + fieldNumber: 1, + }, + owner: { + dataType: 'bytes', + fieldNumber: 2, + }, + attributesArray: { + type: 'array', + fieldNumber: 3, + items: { + type: 'object', + required: ['module', 'attributes'], + properties: { + module: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + pattern: '^[a-zA-Z0-9]*$', + fieldNumber: 1, + }, + attributes: { + dataType: 'bytes', + fieldNumber: 2, + }, + }, + }, + }, + }, + }, + }, + supportedNFTsSubstore: { + type: 'array', + fieldNumber: 2, + items: { + type: 'object', + required: ['chainID', 'supportedCollectionIDArray'], + properties: { + chainID: { + dataType: 'bytes', + fieldNumber: 1, + }, + supportedCollectionIDArray: { + type: 'array', + fieldNumber: 2, + items: { + type: 'object', + required: ['collectionID'], + properties: { + collectionID: { + dataType: 'bytes', + minLength: LENGTH_COLLECTION_ID, + maxLength: LENGTH_COLLECTION_ID, + fieldNumber: 1, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/framework/src/modules/nft/stores/escrow.ts b/framework/src/modules/nft/stores/escrow.ts new file mode 100644 index 00000000000..b5d224088bd --- /dev/null +++ b/framework/src/modules/nft/stores/escrow.ts @@ -0,0 +1,32 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseStore } from '../../base_store'; + +export const escrowStoreSchema = { + $id: '/nft/store/escrow', + type: 'object', + required: [], + properties: {}, +}; + +type EscrowStoreData = Record; + +export class EscrowStore extends BaseStore { + public schema = escrowStoreSchema; + + public getKey(receivingChainID: Buffer, nftID: Buffer): Buffer { + return Buffer.concat([receivingChainID, nftID]); + } +} diff --git a/framework/src/modules/nft/stores/nft.ts b/framework/src/modules/nft/stores/nft.ts new file mode 100644 index 00000000000..ec931e7be7b --- /dev/null +++ b/framework/src/modules/nft/stores/nft.ts @@ -0,0 +1,75 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseStore, StoreGetter } from '../../base_store'; +import { MAX_LENGTH_MODULE_NAME, MIN_LENGTH_MODULE_NAME } from '../constants'; + +export interface NFTAttributes { + module: string; + attributes: Buffer; +} + +export interface NFTStoreData { + owner: Buffer; + attributesArray: NFTAttributes[]; +} + +export const nftStoreSchema = { + $id: '/nft/store/nft', + type: 'object', + required: ['owner', 'attributesArray'], + properties: { + owner: { + dataType: 'bytes', + fieldNumber: 1, + }, + attributesArray: { + type: 'array', + fieldNumber: 2, + items: { + type: 'object', + required: ['module', 'attributes'], + properties: { + module: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + pattern: '^[a-zA-Z0-9]*$', + fieldNumber: 1, + }, + attributes: { + dataType: 'bytes', + fieldNumber: 2, + }, + }, + }, + }, + }, +}; + +export class NFTStore extends BaseStore { + public schema = nftStoreSchema; + + public async save(context: StoreGetter, nftID: Buffer, data: NFTStoreData): Promise { + const attributesArray = data.attributesArray.filter( + attribute => attribute.attributes.length > 0, + ); + attributesArray.sort((a, b) => a.module.localeCompare(b.module, 'en')); + + await this.set(context, nftID, { + ...data, + attributesArray, + }); + } +} diff --git a/framework/src/modules/nft/stores/supported_nfts.ts b/framework/src/modules/nft/stores/supported_nfts.ts new file mode 100644 index 00000000000..63668534d31 --- /dev/null +++ b/framework/src/modules/nft/stores/supported_nfts.ts @@ -0,0 +1,71 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseStore, ImmutableStoreGetter, StoreGetter } from '../../base_store'; +import { LENGTH_COLLECTION_ID, LENGTH_CHAIN_ID } from '../constants'; + +export interface SupportedNFTsStoreData { + supportedCollectionIDArray: { + collectionID: Buffer; + }[]; +} + +export const supportedNFTsStoreSchema = { + $id: '/nft/store/supportedNFTs', + type: 'object', + required: ['supportedCollectionIDArray'], + properties: { + supportedCollectionIDArray: { + type: 'array', + fieldNumber: 1, + items: { + type: 'object', + required: ['collectionID'], + properties: { + collectionID: { + dataType: 'bytes', + minLength: LENGTH_COLLECTION_ID, + maxLength: LENGTH_COLLECTION_ID, + fieldNumber: 1, + }, + }, + }, + }, + }, +}; + +export class SupportedNFTsStore extends BaseStore { + public schema = supportedNFTsStoreSchema; + + public async save( + context: StoreGetter, + chainID: Buffer, + data: SupportedNFTsStoreData, + ): Promise { + const supportedCollectionIDArray = data.supportedCollectionIDArray.sort((a, b) => + a.collectionID.compare(b.collectionID), + ); + + await this.set(context, chainID, { supportedCollectionIDArray }); + } + + public async getAll( + context: ImmutableStoreGetter, + ): Promise<{ key: Buffer; value: SupportedNFTsStoreData }[]> { + return this.iterate(context, { + gte: Buffer.alloc(LENGTH_CHAIN_ID, 0), + lte: Buffer.alloc(LENGTH_CHAIN_ID, 255), + }); + } +} diff --git a/framework/src/modules/nft/stores/user.ts b/framework/src/modules/nft/stores/user.ts new file mode 100644 index 00000000000..752b55abf21 --- /dev/null +++ b/framework/src/modules/nft/stores/user.ts @@ -0,0 +1,42 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { BaseStore } from '../../base_store'; +import { MAX_LENGTH_MODULE_NAME, MIN_LENGTH_MODULE_NAME } from '../constants'; + +export interface UserStoreData { + lockingModule: string; +} + +export const userStoreSchema = { + $id: '/nft/store/user', + type: 'object', + required: ['lockingModule'], + properties: { + lockingModule: { + dataType: 'string', + minLength: MIN_LENGTH_MODULE_NAME, + maxLength: MAX_LENGTH_MODULE_NAME, + pattern: '^[a-zA-Z0-9]*$', + fieldNumber: 1, + }, + }, +}; + +export class UserStore extends BaseStore { + public schema = userStoreSchema; + + public getKey(address: Buffer, nftID: Buffer): Buffer { + return Buffer.concat([address, nftID]); + } +} diff --git a/framework/src/modules/nft/types.ts b/framework/src/modules/nft/types.ts new file mode 100644 index 00000000000..080089ccc0a --- /dev/null +++ b/framework/src/modules/nft/types.ts @@ -0,0 +1,93 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { ImmutableMethodContext, MethodContext } from '../../state_machine'; +import { JSONObject } from '../../types'; +import { CCMsg } from '../interoperability'; + +export interface ModuleConfig { + ownChainID: Buffer; +} + +export interface InteroperabilityMethod { + send( + methodContext: MethodContext, + feeAddress: Buffer, + module: string, + crossChainCommand: string, + receivingChainID: Buffer, + fee: bigint, + parameters: Buffer, + timestamp?: number, + ): Promise; + error(methodContext: MethodContext, ccm: CCMsg, code: number): Promise; + terminateChain(methodContext: MethodContext, chainID: Buffer): Promise; + getMessageFeeTokenID(methodContext: ImmutableMethodContext, chainID: Buffer): Promise; +} + +export interface FeeMethod { + payFee(methodContext: MethodContext, amount: bigint): void; +} + +export interface TokenMethod { + getAvailableBalance( + methodContext: ImmutableMethodContext, + address: Buffer, + tokenID: Buffer, + ): Promise; +} + +export interface NFTMethod { + getChainID(nftID: Buffer): Buffer; + destroy(methodContext: MethodContext, address: Buffer, nftID: Buffer): Promise; + getNFT(methodContext: ImmutableMethodContext, nftID: Buffer): Promise; + isNFTEscrowed(nft: NFT): boolean; + isNFTLocked(nft: NFT): boolean; +} + +export interface NFTAttributes { + module: string; + attributes: Buffer; +} + +export interface NFT { + owner: Buffer; + attributesArray: NFTAttributes[]; + lockingModule?: string; +} + +export type NFTJSON = JSONObject; + +export interface NFTOutputEndpoint { + owner: string; + attributesArray: NFTAttributes[]; + lockingModule?: string; +} + +export interface GenesisNFTStore { + nftSubstore: { + nftID: Buffer; + owner: Buffer; + attributesArray: { + module: string; + attributes: Buffer; + }[]; + }[]; + supportedNFTsSubstore: { + chainID: Buffer; + supportedCollectionIDArray: { + collectionID: Buffer; + }[]; + }[]; +} diff --git a/framework/src/modules/poa/commands/register_authority.ts b/framework/src/modules/poa/commands/register_authority.ts new file mode 100644 index 00000000000..38f7c5e5c9a --- /dev/null +++ b/framework/src/modules/poa/commands/register_authority.ts @@ -0,0 +1,94 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCommand } from '../../base_command'; +import { registerAuthoritySchema } from '../schemas'; +import { + CommandExecuteContext, + CommandVerifyContext, + VerificationResult, + VerifyStatus, +} from '../../../state_machine'; +import { RegisterAuthorityParams, ValidatorsMethod, FeeMethod } from '../types'; +import { COMMAND_REGISTER_AUTHORITY, POA_VALIDATOR_NAME_REGEX } from '../constants'; +import { ValidatorStore, NameStore } from '../stores'; + +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0047.md#register-authority-command +export class RegisterAuthorityCommand extends BaseCommand { + public schema = registerAuthoritySchema; + private _validatorsMethod!: ValidatorsMethod; + private _feeMethod!: FeeMethod; + private _authorityRegistrationFee!: bigint; + + public get name(): string { + return COMMAND_REGISTER_AUTHORITY; + } + + public init(args: { authorityRegistrationFee: bigint }) { + this._authorityRegistrationFee = args.authorityRegistrationFee; + } + + public addDependencies(validatorsMethod: ValidatorsMethod, feeMethod: FeeMethod) { + this._validatorsMethod = validatorsMethod; + this._feeMethod = feeMethod; + } + + public async verify( + context: CommandVerifyContext, + ): Promise { + const { name } = context.params; + + if (!POA_VALIDATOR_NAME_REGEX.test(name)) { + throw new Error(`Name does not comply with format ${POA_VALIDATOR_NAME_REGEX.toString()}.`); + } + + const nameExists = await this.stores.get(NameStore).has(context, Buffer.from(name, 'utf-8')); + if (nameExists) { + throw new Error('Name already exists.'); + } + + const validatorExists = await this.stores + .get(ValidatorStore) + .has(context, context.transaction.senderAddress); + if (validatorExists) { + throw new Error('Validator already exists.'); + } + + return { + status: VerifyStatus.OK, + }; + } + + public async execute(context: CommandExecuteContext): Promise { + const { params } = context; + + this._feeMethod.payFee(context, this._authorityRegistrationFee); + + await this.stores.get(ValidatorStore).set(context, context.transaction.senderAddress, { + name: params.name, + }); + + await this.stores.get(NameStore).set(context, Buffer.from(params.name, 'utf-8'), { + address: context.transaction.senderAddress, + }); + + await this._validatorsMethod.registerValidatorKeys( + context, + context.transaction.senderAddress, + params.blsKey, + params.generatorKey, + params.proofOfPossession, + ); + } +} diff --git a/framework/src/modules/poa/commands/update_authority.ts b/framework/src/modules/poa/commands/update_authority.ts new file mode 100644 index 00000000000..e5a40414203 --- /dev/null +++ b/framework/src/modules/poa/commands/update_authority.ts @@ -0,0 +1,204 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { MAX_UINT64 } from '@liskhq/lisk-validator'; +import { bls } from '@liskhq/lisk-cryptography'; +import { codec } from '@liskhq/lisk-codec'; +import { objects as objectUtils } from '@liskhq/lisk-utils'; +import { BaseCommand } from '../../base_command'; +import { updateAuthoritySchema, validatorSignatureMessageSchema } from '../schemas'; +import { + COMMAND_UPDATE_AUTHORITY, + MESSAGE_TAG_POA, + EMPTY_BYTES, + UpdateAuthorityResult, + KEY_SNAPSHOT_0, + KEY_SNAPSHOT_2, +} from '../constants'; +import { + CommandExecuteContext, + CommandVerifyContext, + VerificationResult, + VerifyStatus, +} from '../../../state_machine'; +import { UpdateAuthorityParams, ValidatorsMethod } from '../types'; +import { ChainPropertiesStore, SnapshotStore, ValidatorStore } from '../stores'; +import { AuthorityUpdateEvent } from '../events/authority_update'; + +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0047.md#update-authority-command +export class UpdateAuthorityCommand extends BaseCommand { + public schema = updateAuthoritySchema; + private _validatorsMethod!: ValidatorsMethod; + + public get name(): string { + return COMMAND_UPDATE_AUTHORITY; + } + + public addDependencies(validatorsMethod: ValidatorsMethod) { + this._validatorsMethod = validatorsMethod; + } + + public async verify( + context: CommandVerifyContext, + ): Promise { + const { newValidators, threshold, validatorsUpdateNonce } = context.params; + const newValidatorsAddresses = newValidators.map(newValidator => newValidator.address); + + if (!objectUtils.isBufferArrayOrdered(newValidatorsAddresses)) { + return { + status: VerifyStatus.FAIL, + error: new Error('Addresses in newValidators are not lexicographically ordered.'), + }; + } + + if (!objectUtils.bufferArrayUniqueItems(newValidatorsAddresses)) { + return { + status: VerifyStatus.FAIL, + error: new Error('Addresses in newValidators are not unique.'), + }; + } + + const validatorStore = this.stores.get(ValidatorStore); + let totalWeight = BigInt(0); + for (const newValidator of newValidators) { + const validatorExists = await validatorStore.has(context, newValidator.address); + if (!validatorExists) { + return { + status: VerifyStatus.FAIL, + error: new Error( + `No validator found for given address ${newValidator.address.toString('hex')}.`, + ), + }; + } + + if (newValidator.weight === BigInt(0)) { + return { + status: VerifyStatus.FAIL, + error: new Error(`Validator weight cannot be zero.`), + }; + } + + totalWeight += newValidator.weight; + } + + if (totalWeight === BigInt(0)) { + return { + status: VerifyStatus.FAIL, + error: new Error(`Validators total weight cannot be zero.`), + }; + } + + if (totalWeight > MAX_UINT64) { + return { + status: VerifyStatus.FAIL, + error: new Error(`Validators total weight exceeds ${MAX_UINT64}.`), + }; + } + + const minThreshold = totalWeight / BigInt(3) + BigInt(1); + if (threshold < minThreshold || threshold > totalWeight) { + return { + status: VerifyStatus.FAIL, + error: new Error( + `Threshold must be between ${minThreshold} and ${totalWeight} (inclusive).`, + ), + }; + } + + const chainPropertiesStore = await this.stores + .get(ChainPropertiesStore) + .get(context, EMPTY_BYTES); + if (validatorsUpdateNonce !== chainPropertiesStore.validatorsUpdateNonce) { + return { + status: VerifyStatus.FAIL, + error: new Error( + `validatorsUpdateNonce must be equal to ${chainPropertiesStore.validatorsUpdateNonce}.`, + ), + }; + } + + return { + status: VerifyStatus.OK, + }; + } + + public async execute(context: CommandExecuteContext): Promise { + const { newValidators, threshold, validatorsUpdateNonce, aggregationBits, signature } = + context.params; + + // Verify weighted aggregated signature. + const message = codec.encode(validatorSignatureMessageSchema, { + newValidators, + threshold, + validatorsUpdateNonce, + }); + + const validatorsInfos = []; + const snapshotStore = this.stores.get(SnapshotStore); + const snapshot0 = await snapshotStore.get(context, KEY_SNAPSHOT_0); + for (const snapshotValidator of snapshot0.validators) { + const keys = await this._validatorsMethod.getValidatorKeys( + context, + snapshotValidator.address, + ); + validatorsInfos.push({ + key: keys.blsKey, + weight: snapshotValidator.weight, + }); + } + + validatorsInfos.sort((a, b) => a.key.compare(b.key)); + const verified = bls.verifyWeightedAggSig( + validatorsInfos.map(validatorInfo => validatorInfo.key), + aggregationBits, + signature, + MESSAGE_TAG_POA, + context.chainID, + message, + validatorsInfos.map(validatorInfo => validatorInfo.weight), + snapshot0.threshold, + ); + + const authorityUpdateEvent = this.events.get(AuthorityUpdateEvent); + if (!verified) { + authorityUpdateEvent.log( + context, + { + result: UpdateAuthorityResult.FAIL_INVALID_SIGNATURE, + }, + true, + ); + throw new Error('Invalid weighted aggregated signature.'); + } + await snapshotStore.set(context, KEY_SNAPSHOT_2, { + validators: newValidators, + threshold, + }); + + const chainPropertiesStore = this.stores.get(ChainPropertiesStore); + const chainProperties = await chainPropertiesStore.get(context, EMPTY_BYTES); + await chainPropertiesStore.set(context, EMPTY_BYTES, { + ...chainProperties, + validatorsUpdateNonce: chainProperties.validatorsUpdateNonce + 1, + }); + + authorityUpdateEvent.log( + context, + { + result: UpdateAuthorityResult.SUCCESS, + }, + false, + ); + } +} diff --git a/framework/src/modules/poa/commands/update_generator_key.ts b/framework/src/modules/poa/commands/update_generator_key.ts new file mode 100644 index 00000000000..4a94d28b776 --- /dev/null +++ b/framework/src/modules/poa/commands/update_generator_key.ts @@ -0,0 +1,64 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCommand } from '../../base_command'; +import { updateGeneratorKeySchema } from '../schemas'; +import { COMMAND_UPDATE_KEY } from '../constants'; +import { + CommandExecuteContext, + CommandVerifyContext, + VerificationResult, + VerifyStatus, +} from '../../../state_machine'; +import { UpdateGeneratorKeyParams, ValidatorsMethod } from '../types'; +import { ValidatorStore } from '../stores'; + +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0047.md#update-generator-key-command +export class UpdateGeneratorKeyCommand extends BaseCommand { + public schema = updateGeneratorKeySchema; + private _validatorsMethod!: ValidatorsMethod; + + public get name(): string { + return COMMAND_UPDATE_KEY; + } + + public addDependencies(validatorsMethod: ValidatorsMethod) { + this._validatorsMethod = validatorsMethod; + } + + public async verify( + context: CommandVerifyContext, + ): Promise { + const validatorExists = await this.stores + .get(ValidatorStore) + .has(context, context.transaction.senderAddress); + if (!validatorExists) { + throw new Error('Validator does not exist.'); + } + + return { + status: VerifyStatus.OK, + }; + } + + public async execute(context: CommandExecuteContext): Promise { + const { generatorKey } = context.params; + + await this._validatorsMethod.setValidatorGeneratorKey( + context, + context.transaction.senderAddress, + generatorKey, + ); + } +} diff --git a/framework/src/modules/poa/constants.ts b/framework/src/modules/poa/constants.ts new file mode 100644 index 00000000000..056a6110edf --- /dev/null +++ b/framework/src/modules/poa/constants.ts @@ -0,0 +1,48 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { utils } from '@liskhq/lisk-cryptography'; + +export enum UpdateAuthorityResult { + SUCCESS = 0, + FAIL_INVALID_SIGNATURE, +} + +export const MODULE_NAME_POA = 'poa'; +export const MAX_LENGTH_NAME = 20; +export const LENGTH_BLS_KEY = 48; +export const LENGTH_PROOF_OF_POSSESSION = 96; +export const LENGTH_GENERATOR_KEY = 32; +export const NUM_BYTES_ADDRESS = 20; +export const MAX_NUM_VALIDATORS = 199; +export const POA_VALIDATOR_NAME_REGEX = /^[a-z0-9!@$&_.]+$/; +export const MESSAGE_TAG_POA = 'LSK_POA_'; +export const AUTHORITY_REGISTRATION_FEE = BigInt(1000000000); // Determined by Operator +export const EMPTY_BYTES = Buffer.alloc(0); +export const COMMAND_REGISTER_AUTHORITY = 'registerAuthority'; +export const COMMAND_UPDATE_KEY = 'updateKey'; +export const COMMAND_UPDATE_AUTHORITY = 'updateAuthority'; +export const MAX_UINT64 = BigInt(2) ** BigInt(64) - BigInt(1); +export const defaultConfig = { + authorityRegistrationFee: AUTHORITY_REGISTRATION_FEE.toString(), +}; + +// Store key +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0047.md#uint32be-function +export const KEY_SNAPSHOT_0 = utils.intToBuffer(0, 4); +export const KEY_SNAPSHOT_1 = utils.intToBuffer(1, 4); +export const KEY_SNAPSHOT_2 = utils.intToBuffer(2, 4); +export const SUBSTORE_PREFIX_VALIDATOR_INDEX = 0; +export const SUBSTORE_PREFIX_CHAIN_INDEX = 1; +export const SUBSTORE_PREFIX_NAME_INDEX = 2; +export const SUBSTORE_PREFIX_SNAPSHOT_INDEX = 3; diff --git a/framework/src/modules/poa/endpoint.ts b/framework/src/modules/poa/endpoint.ts new file mode 100644 index 00000000000..e5f9212a39e --- /dev/null +++ b/framework/src/modules/poa/endpoint.ts @@ -0,0 +1,112 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { validator } from '@liskhq/lisk-validator'; +import { address as cryptoAddress } from '@liskhq/lisk-cryptography'; +import { NotFoundError } from '@liskhq/lisk-db'; +import { BaseEndpoint } from '../base_endpoint'; +import { ValidatorStore } from './stores/validator'; +import { ModuleEndpointContext } from '../../types'; +import { KEY_SNAPSHOT_0, NUM_BYTES_ADDRESS } from './constants'; +import { SnapshotStore } from './stores'; +import { Validator } from './types'; +import { getValidatorRequestSchema } from './schemas'; + +export class PoAEndpoint extends BaseEndpoint { + private _authorityRegistrationFee!: bigint; + + public init(authorityRegistrationFee: bigint) { + this._authorityRegistrationFee = authorityRegistrationFee; + } + + public async getValidator(context: ModuleEndpointContext): Promise { + const validatorSubStore = this.stores.get(ValidatorStore); + + validator.validate(getValidatorRequestSchema, context.params); + const address = context.params.address as string; + + cryptoAddress.validateLisk32Address(address); + + let validatorName: { name: string }; + try { + validatorName = await validatorSubStore.get( + context, + cryptoAddress.getAddressFromLisk32Address(address), + ); + } catch (error) { + if (!(error instanceof NotFoundError)) { + throw error; + } + + throw new Error(`Validator not found in snapshot for address ${address}`); + } + + const snapshotStore = this.stores.get(SnapshotStore); + const currentRoundSnapshot = await snapshotStore.get(context, KEY_SNAPSHOT_0); + const validatorInfo = currentRoundSnapshot.validators.find( + v => cryptoAddress.getLisk32AddressFromAddress(v.address) === address, + ); + if (!validatorInfo) { + throw new Error(`Validator not found in snapshot for address ${address}`); + } + + return { + ...validatorName, + address, + weight: validatorInfo.weight.toString(), + }; + } + + public async getAllValidators( + context: ModuleEndpointContext, + ): Promise<{ validators: Validator[] }> { + const validatorStore = this.stores.get(ValidatorStore); + const startBuf = Buffer.alloc(NUM_BYTES_ADDRESS); + const endBuf = Buffer.alloc(NUM_BYTES_ADDRESS, 255); + const validatorStoreData = await validatorStore.iterate(context, { + gte: startBuf, + lte: endBuf, + }); + + const snapshotStore = this.stores.get(SnapshotStore); + const currentRoundSnapshot = await snapshotStore.get(context, KEY_SNAPSHOT_0); + + const validatorsData: Validator[] = []; + for (const data of validatorStoreData) { + const address = cryptoAddress.getLisk32AddressFromAddress(data.key); + const { value } = data; + const activeValidator = currentRoundSnapshot.validators.find( + v => cryptoAddress.getLisk32AddressFromAddress(v.address) === address, + ); + + const v: Validator = { + name: value.name, + address, + weight: activeValidator ? activeValidator.weight.toString() : '0', + }; + validatorsData.push(v); + } + + // This is needed since response from this endpoint is returning data in unexpected sorting order on next execution + // which can result in potential test/build failure + validatorsData.sort((v1, v2) => v1.name.localeCompare(v2.name, 'en')); + return { validators: validatorsData }; + } + + public getRegistrationFee(): { fee: string } { + return { + fee: this._authorityRegistrationFee.toString(), + }; + } +} diff --git a/framework/src/modules/poa/events/authority_update.ts b/framework/src/modules/poa/events/authority_update.ts new file mode 100644 index 00000000000..31b2597ef20 --- /dev/null +++ b/framework/src/modules/poa/events/authority_update.ts @@ -0,0 +1,40 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseEvent, EventQueuer } from '../../base_event'; +import { UpdateAuthorityResult } from '../constants'; + +export interface AuthorityUpdateData { + result: UpdateAuthorityResult; +} + +export const authorityUpdateDataSchema = { + $id: '/poa/events/authorityUpdate', + type: 'object', + required: ['result'], + properties: { + result: { + dataType: 'uint32', + fieldNumber: 1, + }, + }, +}; + +export class AuthorityUpdateEvent extends BaseEvent { + public schema = authorityUpdateDataSchema; + + public log(ctx: EventQueuer, data: AuthorityUpdateData, noRevert: boolean): void { + this.add(ctx, data, [], noRevert); + } +} diff --git a/framework/src/modules/poa/events/index.ts b/framework/src/modules/poa/events/index.ts new file mode 100644 index 00000000000..206ba71de27 --- /dev/null +++ b/framework/src/modules/poa/events/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ diff --git a/framework/src/modules/poa/index.ts b/framework/src/modules/poa/index.ts new file mode 100644 index 00000000000..2d9f117ab7c --- /dev/null +++ b/framework/src/modules/poa/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export { PoAModule } from './module'; +export { PoAMethod } from './method'; diff --git a/framework/src/modules/poa/internal_method.ts b/framework/src/modules/poa/internal_method.ts new file mode 100644 index 00000000000..9885f7e1f2e --- /dev/null +++ b/framework/src/modules/poa/internal_method.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseMethod } from '../base_method'; + +export class PoAInternalMethod extends BaseMethod {} diff --git a/framework/src/modules/poa/method.ts b/framework/src/modules/poa/method.ts new file mode 100644 index 00000000000..b720957dd68 --- /dev/null +++ b/framework/src/modules/poa/method.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseMethod } from '../base_method'; + +export class PoAMethod extends BaseMethod {} diff --git a/framework/src/modules/poa/module.ts b/framework/src/modules/poa/module.ts new file mode 100644 index 00000000000..89349ad70d8 --- /dev/null +++ b/framework/src/modules/poa/module.ts @@ -0,0 +1,347 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec } from '@liskhq/lisk-codec'; +import { objects } from '@liskhq/lisk-utils'; +import { validator } from '@liskhq/lisk-validator'; +import { BaseModule, ModuleInitArgs, ModuleMetadata } from '../base_module'; +import { PoAMethod } from './method'; +import { PoAEndpoint } from './endpoint'; +import { AuthorityUpdateEvent } from './events/authority_update'; +import { ChainPropertiesStore, ValidatorStore, NameStore, SnapshotStore } from './stores'; +import { BlockAfterExecuteContext, GenesisBlockExecuteContext } from '../../state_machine'; +import { + MODULE_NAME_POA, + EMPTY_BYTES, + KEY_SNAPSHOT_0, + KEY_SNAPSHOT_1, + KEY_SNAPSHOT_2, + MAX_UINT64, + defaultConfig, + POA_VALIDATOR_NAME_REGEX, + SUBSTORE_PREFIX_VALIDATOR_INDEX, + SUBSTORE_PREFIX_CHAIN_INDEX, + SUBSTORE_PREFIX_NAME_INDEX, + SUBSTORE_PREFIX_SNAPSHOT_INDEX, +} from './constants'; +import { shuffleValidatorList } from '../utils'; +import { NextValidatorsSetter, MethodContext } from '../../state_machine/types'; +import { + configSchema, + genesisPoAStoreSchema, + getAllValidatorsResponseSchema, + getRegistrationFeeResponseSchema, + getValidatorRequestSchema, + getValidatorResponseSchema, +} from './schemas'; +import { + FeeMethod, + GenesisPoAStore, + ValidatorsMethod, + RandomMethod, + ModuleConfigJSON, + ModuleConfig, + ActiveValidator, +} from './types'; +import { RegisterAuthorityCommand } from './commands/register_authority'; +import { UpdateAuthorityCommand } from './commands/update_authority'; +import { UpdateGeneratorKeyCommand } from './commands/update_generator_key'; + +export class PoAModule extends BaseModule { + public method = new PoAMethod(this.stores, this.events); + public endpoint = new PoAEndpoint(this.stores, this.offchainStores); + private _randomMethod!: RandomMethod; + private _validatorsMethod!: ValidatorsMethod; + private _feeMethod!: FeeMethod; + private readonly _registerAuthorityCommand = new RegisterAuthorityCommand( + this.stores, + this.events, + ); + private readonly _updateAuthorityCommand = new UpdateAuthorityCommand(this.stores, this.events); + private readonly _updateGeneratorKeyCommand = new UpdateGeneratorKeyCommand( + this.stores, + this.events, + ); + private _moduleConfig!: ModuleConfig; + + public commands = [ + this._registerAuthorityCommand, + this._updateAuthorityCommand, + this._updateGeneratorKeyCommand, + ]; + + public constructor() { + super(); + this.events.register(AuthorityUpdateEvent, new AuthorityUpdateEvent(this.name)); + this.stores.register( + ValidatorStore, + new ValidatorStore(this.name, SUBSTORE_PREFIX_VALIDATOR_INDEX), + ); + this.stores.register( + ChainPropertiesStore, + new ChainPropertiesStore(this.name, SUBSTORE_PREFIX_CHAIN_INDEX), + ); + this.stores.register(NameStore, new NameStore(this.name, SUBSTORE_PREFIX_NAME_INDEX)); + this.stores.register( + SnapshotStore, + new SnapshotStore(this.name, SUBSTORE_PREFIX_SNAPSHOT_INDEX), + ); + } + + public get name() { + return MODULE_NAME_POA; + } + + public addDependencies( + validatorsMethod: ValidatorsMethod, + feeMethod: FeeMethod, + randomMethod: RandomMethod, + ) { + this._validatorsMethod = validatorsMethod; + this._feeMethod = feeMethod; + this._randomMethod = randomMethod; + + // Add dependencies to commands + this._registerAuthorityCommand.addDependencies(this._validatorsMethod, this._feeMethod); + this._updateAuthorityCommand.addDependencies(this._validatorsMethod); + this._updateGeneratorKeyCommand.addDependencies(this._validatorsMethod); + } + + public metadata(): ModuleMetadata { + return { + ...this.baseMetadata(), + endpoints: [ + { + name: this.endpoint.getValidator.name, + request: getValidatorRequestSchema, + response: getValidatorResponseSchema, + }, + { + name: this.endpoint.getAllValidators.name, + response: getAllValidatorsResponseSchema, + }, + { + name: this.endpoint.getRegistrationFee.name, + response: getRegistrationFeeResponseSchema, + }, + ], + assets: [], + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async init(args: ModuleInitArgs) { + const config = objects.mergeDeep({}, { ...defaultConfig }, args.moduleConfig); + validator.validate(configSchema, config); + + this._moduleConfig = { + ...config, + authorityRegistrationFee: BigInt(config.authorityRegistrationFee), + }; + this._registerAuthorityCommand.init(this._moduleConfig); + this.endpoint.init(this._moduleConfig.authorityRegistrationFee); + } + + // LIP: https://github.com/LiskHQ/lips/blob/main/proposals/lip-0047.md#after-transactions-execution + public async afterTransactionsExecute(context: BlockAfterExecuteContext): Promise { + const chainPropertiesStore = this.stores.get(ChainPropertiesStore); + const chainProperties = await chainPropertiesStore.get(context, EMPTY_BYTES); + + if (context.header.height === chainProperties.roundEndHeight) { + const snapshotStore = this.stores.get(SnapshotStore); + const snapshot0 = await snapshotStore.get(context, KEY_SNAPSHOT_0); + const previousLengthValidators = snapshot0.validators.length; + + const snapshot1 = await snapshotStore.get(context, KEY_SNAPSHOT_1); + // Update the chain information for the next round + + // snapshot0 = snapshot1 + await snapshotStore.set(context, KEY_SNAPSHOT_0, snapshot1); + const snapshot2 = await snapshotStore.get(context, KEY_SNAPSHOT_2); + // snapshot1 = snapshot2 + await snapshotStore.set(context, KEY_SNAPSHOT_1, snapshot2); + + // Reshuffle the list of validators and pass it to the Validators module + const roundStartHeight = chainProperties.roundEndHeight - previousLengthValidators + 1; + const randomSeed = await this._randomMethod.getRandomBytes( + context, + roundStartHeight, + previousLengthValidators, + ); + + const nextValidators = shuffleValidatorList( + randomSeed, + snapshot1.validators, + ); + + await this._validatorsMethod.setValidatorsParams( + context as MethodContext, + context as NextValidatorsSetter, + snapshot1.threshold, + snapshot1.threshold, + nextValidators.map(v => ({ + address: v.address, + bftWeight: v.weight, + })), + ); + + chainProperties.roundEndHeight += snapshot1.validators.length; + + await chainPropertiesStore.set(context, EMPTY_BYTES, chainProperties); + } + } + + public async initGenesisState(context: GenesisBlockExecuteContext): Promise { + const genesisBlockAssetBytes = context.assets.getAsset(MODULE_NAME_POA); + if (!genesisBlockAssetBytes) { + return; + } + const asset = codec.decode(genesisPoAStoreSchema, genesisBlockAssetBytes); + validator.validate(genesisPoAStoreSchema, asset); + + const { validators, snapshotSubstore } = asset; + + // Check that the name property of all entries in the validators array are pairwise distinct. + const validatorNames = validators.map(v => v.name); + if (validatorNames.length !== new Set(validatorNames).size) { + throw new Error('`name` property of all entries in the validators must be distinct.'); + } + + // Check that the address properties of all entries in the validators array are pairwise distinct. + const validatorAddresses = validators.map(v => v.address); + if (!objects.bufferArrayUniqueItems(validatorAddresses)) { + throw new Error('`address` property of all entries in validators must be distinct.'); + } + + if (!objects.isBufferArrayOrdered(validatorAddresses)) { + throw new Error('`validators` must be ordered lexicographically by address.'); + } + + for (const poaValidator of validators) { + if (!POA_VALIDATOR_NAME_REGEX.test(poaValidator.name)) { + throw new Error('`name` property is invalid. Must contain only characters a-z0-9!@$&_.'); + } + } + + const { activeValidators, threshold } = snapshotSubstore; + const activeValidatorAddresses = activeValidators.map(v => v.address); + const validatorAddressesString = validatorAddresses.map(a => a.toString('hex')); + let totalWeight = BigInt(0); + + // Check that the address properties of entries in the snapshotSubstore.activeValidators are pairwise distinct. + if (!objects.bufferArrayUniqueItems(activeValidatorAddresses)) { + throw new Error('`address` properties in `activeValidators` must be distinct.'); + } + + if (!objects.isBufferArrayOrdered(activeValidatorAddresses)) { + throw new Error('`activeValidators` must be ordered lexicographically by address property.'); + } + for (const activeValidator of activeValidators) { + // Check that for every element activeValidator in the snapshotSubstore.activeValidators array, there is an entry validator in the validators array with validator.address == activeValidator.address. + if (!validatorAddressesString.includes(activeValidator.address.toString('hex'))) { + throw new Error('`activeValidator` address is missing from validators array.'); + } + + // Check that the weight property of every entry in the snapshotSubstore.activeValidators array is a positive integer. + if (activeValidator.weight <= BigInt(0)) { + throw new Error('`activeValidators` weight must be positive integer.'); + } + + totalWeight += activeValidator.weight; + } + + if (totalWeight > MAX_UINT64) { + throw new Error('Total weight `activeValidators` exceeds maximum value.'); + } + + // Check that the value of snapshotSubstore.threshold is within range + if (threshold < totalWeight / BigInt(3) + BigInt(1) || threshold > totalWeight) { + throw new Error('`threshold` in snapshot substore is not within range.'); + } + + // Create an entry in the validator substore for each entry validator in the validators + // Create an entry in the name substore for each entry validator in the validators + const validatorStore = this.stores.get(ValidatorStore); + const nameStore = this.stores.get(NameStore); + + for (const currentValidator of validators) { + await validatorStore.set(context, currentValidator.address, { name: currentValidator.name }); + await nameStore.set(context, Buffer.from(currentValidator.name, 'utf-8'), { + address: currentValidator.address, + }); + } + + // Create three entries in the snapshot substore indicating a snapshot of the next rounds of validators + const snapshotStore = this.stores.get(SnapshotStore); + await snapshotStore.set(context, KEY_SNAPSHOT_0, { + ...snapshotSubstore, + validators: activeValidators, + }); + await snapshotStore.set(context, KEY_SNAPSHOT_1, { + ...snapshotSubstore, + validators: activeValidators, + }); + await snapshotStore.set(context, KEY_SNAPSHOT_2, { + ...snapshotSubstore, + validators: activeValidators, + }); + + // Create an entry in the chain properties substore + const { header } = context; + const chainPropertiesStore = this.stores.get(ChainPropertiesStore); + await chainPropertiesStore.set(context, EMPTY_BYTES, { + roundEndHeight: header.height, + validatorsUpdateNonce: 0, + }); + } + + public async finalizeGenesisState(context: GenesisBlockExecuteContext): Promise { + const genesisBlockAssetBytes = context.assets.getAsset(MODULE_NAME_POA); + if (!genesisBlockAssetBytes) { + return; + } + const asset = codec.decode(genesisPoAStoreSchema, genesisBlockAssetBytes); + const snapshotStore = this.stores.get(SnapshotStore); + const currentRoundSnapshot = await snapshotStore.get(context, KEY_SNAPSHOT_0); + const chainPropertiesStore = this.stores.get(ChainPropertiesStore); + const chainProperties = await chainPropertiesStore.get(context, EMPTY_BYTES); + + await chainPropertiesStore.set(context, EMPTY_BYTES, { + ...chainProperties, + roundEndHeight: chainProperties.roundEndHeight + currentRoundSnapshot.validators.length, + }); + + // Pass the required information to the Validators module. + const methodContext = context.getMethodContext(); + + // Pass the BLS keys and generator keys to the Validators module. + for (const v of asset.validators) { + await this._validatorsMethod.registerValidatorKeys( + methodContext, + v.address, + v.blsKey, + v.generatorKey, + v.proofOfPossession, + ); + } + + await this._validatorsMethod.setValidatorsParams( + methodContext, + context, + currentRoundSnapshot.threshold, + currentRoundSnapshot.threshold, + currentRoundSnapshot.validators.map(v => ({ address: v.address, bftWeight: v.weight })), + ); + } +} diff --git a/framework/src/modules/poa/schemas.ts b/framework/src/modules/poa/schemas.ts new file mode 100644 index 00000000000..d15bd761335 --- /dev/null +++ b/framework/src/modules/poa/schemas.ts @@ -0,0 +1,289 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { + LENGTH_BLS_KEY, + LENGTH_GENERATOR_KEY, + LENGTH_PROOF_OF_POSSESSION, + MAX_LENGTH_NAME, + NUM_BYTES_ADDRESS, + MAX_NUM_VALIDATORS, +} from './constants'; + +export const configSchema = { + $id: '/poa/config', + type: 'object', + properties: { + authorityRegistrationFee: { + type: 'string', + format: 'uint64', + }, + }, +}; + +const validator = { + type: 'object', + required: ['address', 'weight'], + properties: { + address: { + dataType: 'bytes', + minLength: NUM_BYTES_ADDRESS, + maxLength: NUM_BYTES_ADDRESS, + fieldNumber: 1, + }, + weight: { + dataType: 'uint64', + fieldNumber: 2, + }, + }, +}; + +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0047.md#register-authority-command +export const registerAuthoritySchema = { + $id: '/poa/command/registerAuthority', + type: 'object', + required: ['name', 'blsKey', 'proofOfPossession', 'generatorKey'], + properties: { + name: { + dataType: 'string', + minLength: 1, + maxLength: MAX_LENGTH_NAME, + fieldNumber: 1, + }, + blsKey: { + dataType: 'bytes', + minLength: LENGTH_BLS_KEY, + maxLength: LENGTH_BLS_KEY, + fieldNumber: 2, + }, + proofOfPossession: { + dataType: 'bytes', + minLength: LENGTH_PROOF_OF_POSSESSION, + maxLength: LENGTH_PROOF_OF_POSSESSION, + fieldNumber: 3, + }, + generatorKey: { + dataType: 'bytes', + minLength: LENGTH_GENERATOR_KEY, + maxLength: LENGTH_GENERATOR_KEY, + fieldNumber: 4, + }, + }, +}; + +export const updateGeneratorKeySchema = { + $id: '/poa/command/updateGeneratorKey', + type: 'object', + required: ['generatorKey'], + properties: { + generatorKey: { + dataType: 'bytes', + minLength: LENGTH_GENERATOR_KEY, + maxLength: LENGTH_GENERATOR_KEY, + fieldNumber: 1, + }, + }, +}; + +export const updateAuthoritySchema = { + $id: '/poa/command/updateAuthority', + type: 'object', + required: ['newValidators', 'threshold', 'validatorsUpdateNonce', 'signature', 'aggregationBits'], + properties: { + newValidators: { + type: 'array', + fieldNumber: 1, + items: validator, + minItems: 1, + maxItems: MAX_NUM_VALIDATORS, + }, + threshold: { + dataType: 'uint64', + fieldNumber: 2, + }, + validatorsUpdateNonce: { + dataType: 'uint32', + fieldNumber: 3, + }, + signature: { + dataType: 'bytes', + fieldNumber: 4, + }, + aggregationBits: { + dataType: 'bytes', + fieldNumber: 5, + }, + }, +}; + +export const validatorSignatureMessageSchema = { + $id: '/poa/command/validatorSignatureMessage', + type: 'object', + required: ['newValidators', 'threshold', 'validatorsUpdateNonce'], + properties: { + newValidators: { + type: 'array', + fieldNumber: 1, + items: validator, + }, + threshold: { + dataType: 'uint64', + fieldNumber: 2, + }, + validatorsUpdateNonce: { + dataType: 'uint32', + fieldNumber: 3, + }, + }, +}; + +// https://github.com/LiskHQ/lips/blob/main/proposals/lip-0047.md#genesis-poa-store-schema +export const genesisPoAStoreSchema = { + $id: '/poa/genesis/genesisPoAStoreSchema', + type: 'object', + required: ['validators', 'snapshotSubstore'], + properties: { + validators: { + type: 'array', + fieldNumber: 1, + items: { + type: 'object', + required: ['address', 'name', 'blsKey', 'proofOfPossession', 'generatorKey'], + properties: { + address: { + dataType: 'bytes', + minLength: NUM_BYTES_ADDRESS, + maxLength: NUM_BYTES_ADDRESS, + fieldNumber: 1, + }, + name: { + dataType: 'string', + minLength: 1, + maxLength: MAX_LENGTH_NAME, + fieldNumber: 2, + }, + blsKey: { + dataType: 'bytes', + minLength: LENGTH_BLS_KEY, + maxLength: LENGTH_BLS_KEY, + fieldNumber: 3, + }, + proofOfPossession: { + dataType: 'bytes', + minLength: LENGTH_PROOF_OF_POSSESSION, + maxLength: LENGTH_PROOF_OF_POSSESSION, + fieldNumber: 4, + }, + generatorKey: { + dataType: 'bytes', + minLength: LENGTH_GENERATOR_KEY, + maxLength: LENGTH_GENERATOR_KEY, + fieldNumber: 5, + }, + }, + }, + }, + snapshotSubstore: { + type: 'object', + fieldNumber: 2, + properties: { + activeValidators: { + type: 'array', + fieldNumber: 1, + items: { + type: 'object', + required: ['address', 'weight'], + properties: { + address: { + dataType: 'bytes', + minLength: NUM_BYTES_ADDRESS, + maxLength: NUM_BYTES_ADDRESS, + fieldNumber: 1, + }, + weight: { + dataType: 'uint64', + fieldNumber: 2, + }, + }, + }, + minItems: 1, + maxItems: MAX_NUM_VALIDATORS, + }, + threshold: { + dataType: 'uint64', + fieldNumber: 2, + }, + }, + required: ['activeValidators', 'threshold'], + }, + }, +}; + +const validatorJSONSchema = { + type: 'object', + required: ['address', 'name', 'weight'], + properties: { + address: { + type: 'string', + format: 'lisk32', + }, + name: { + type: 'string', + }, + weight: { + type: 'string', + format: 'uint64', + }, + }, +}; + +export const getValidatorRequestSchema = { + $id: 'modules/poa/endpoint/getValidatorRequest', + type: 'object', + required: ['address'], + properties: { + address: { + dataType: 'string', + format: 'lisk32', + }, + }, +}; + +export const getValidatorResponseSchema = { + $id: 'modules/poa/endpoint/getValidatorResponse', + ...validatorJSONSchema, +}; + +export const getAllValidatorsResponseSchema = { + $id: 'modules/poa/endpoint/getAllValidatorsResponse', + type: 'object', + required: ['validators'], + properties: { + validators: { + type: 'array', + items: validatorJSONSchema, + }, + }, +}; + +export const getRegistrationFeeResponseSchema = { + $id: 'modules/poa/endpoint/getRegistrationFeeResponse', + type: 'object', + required: ['fee'], + properties: { + fee: { + type: 'string', + }, + }, +}; diff --git a/framework/src/modules/poa/stores/chain_properties.ts b/framework/src/modules/poa/stores/chain_properties.ts new file mode 100644 index 00000000000..f1c95327184 --- /dev/null +++ b/framework/src/modules/poa/stores/chain_properties.ts @@ -0,0 +1,39 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { BaseStore } from '../../base_store'; + +export interface ChainProperties { + roundEndHeight: number; + validatorsUpdateNonce: number; +} + +export const chainPropertiesSchema = { + $id: '/poa/chainProperties', + type: 'object', + required: ['roundEndHeight', 'validatorsUpdateNonce'], + properties: { + roundEndHeight: { + dataType: 'uint32', + fieldNumber: 1, + }, + validatorsUpdateNonce: { + dataType: 'uint32', + fieldNumber: 2, + }, + }, +}; + +export class ChainPropertiesStore extends BaseStore { + public schema = chainPropertiesSchema; +} diff --git a/framework/src/modules/poa/stores/index.ts b/framework/src/modules/poa/stores/index.ts new file mode 100644 index 00000000000..5ea18ba0cc0 --- /dev/null +++ b/framework/src/modules/poa/stores/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export * from './chain_properties'; +export * from './name'; +export * from './snapshot'; +export * from './validator'; diff --git a/framework/src/modules/poa/stores/name.ts b/framework/src/modules/poa/stores/name.ts new file mode 100644 index 00000000000..0419c6cc490 --- /dev/null +++ b/framework/src/modules/poa/stores/name.ts @@ -0,0 +1,37 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { BaseStore } from '../../base_store'; +import { NUM_BYTES_ADDRESS } from '../constants'; + +export interface ValidatorAddress { + address: Buffer; +} + +export const validatorAddressSchema = { + $id: '/poa/validatorAddress', + type: 'object', + required: ['address'], + properties: { + address: { + dataType: 'bytes', + fieldNumber: 1, + minLength: NUM_BYTES_ADDRESS, + maxLength: NUM_BYTES_ADDRESS, + }, + }, +}; + +export class NameStore extends BaseStore { + public schema = validatorAddressSchema; +} diff --git a/framework/src/modules/poa/stores/snapshot.ts b/framework/src/modules/poa/stores/snapshot.ts new file mode 100644 index 00000000000..217ae7b4648 --- /dev/null +++ b/framework/src/modules/poa/stores/snapshot.ts @@ -0,0 +1,57 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { BaseStore } from '../../base_store'; +import { NUM_BYTES_ADDRESS } from '../constants'; +import { ActiveValidator } from '../types'; + +export interface SnapshotObject { + validators: ActiveValidator[]; + threshold: bigint; +} + +export const snapshotSchema = { + $id: '/poa/snapshot', + type: 'object', + required: ['validators', 'threshold'], + properties: { + validators: { + type: 'array', + fieldNumber: 1, + items: { + type: 'object', + required: ['address', 'weight'], + properties: { + address: { + dataType: 'bytes', + minLength: NUM_BYTES_ADDRESS, + maxLength: NUM_BYTES_ADDRESS, + fieldNumber: 1, + }, + weight: { + dataType: 'uint64', + fieldNumber: 2, + }, + }, + }, + }, + threshold: { + dataType: 'uint64', + fieldNumber: 2, + }, + }, +}; + +export class SnapshotStore extends BaseStore { + public schema = snapshotSchema; +} diff --git a/framework/src/modules/poa/stores/validator.ts b/framework/src/modules/poa/stores/validator.ts new file mode 100644 index 00000000000..d176dd9843d --- /dev/null +++ b/framework/src/modules/poa/stores/validator.ts @@ -0,0 +1,37 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { BaseStore } from '../../base_store'; +import { MAX_LENGTH_NAME } from '../constants'; + +export interface ValidatorName { + name: string; +} + +export const validatorNameSchema = { + $id: '/poa/validatorName', + type: 'object', + required: ['name'], + properties: { + name: { + dataType: 'string', + fieldNumber: 1, + minLength: 1, + maxLength: MAX_LENGTH_NAME, + }, + }, +}; + +export class ValidatorStore extends BaseStore { + public schema = validatorNameSchema; +} diff --git a/framework/src/modules/poa/types.ts b/framework/src/modules/poa/types.ts new file mode 100644 index 00000000000..439d7538b4c --- /dev/null +++ b/framework/src/modules/poa/types.ts @@ -0,0 +1,126 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { + ImmutableMethodContext, + MethodContext, + NextValidatorsSetter, +} from '../../state_machine/types'; +import { JSONObject } from '../../types'; + +export interface ModuleConfig { + authorityRegistrationFee: bigint; +} + +export type ModuleConfigJSON = JSONObject; +export interface RegisterAuthorityParams { + name: string; + blsKey: Buffer; + proofOfPossession: Buffer; + generatorKey: Buffer; +} + +export interface UpdateAuthorityParams { + newValidators: { + address: Buffer; + weight: bigint; + }[]; + threshold: bigint; + validatorsUpdateNonce: number; + signature: Buffer; + aggregationBits: Buffer; +} + +export interface ValidatorsMethod { + setValidatorGeneratorKey( + methodContext: MethodContext, + validatorAddress: Buffer, + generatorKey: Buffer, + ): Promise; + registerValidatorKeys( + methodContext: MethodContext, + validatorAddress: Buffer, + blsKey: Buffer, + generatorKey: Buffer, + proofOfPossession: Buffer, + ): Promise; + registerValidatorWithoutBLSKey( + methodContext: MethodContext, + validatorAddress: Buffer, + generatorKey: Buffer, + ): Promise; + getValidatorKeys(methodContext: ImmutableMethodContext, address: Buffer): Promise; + getGeneratorsBetweenTimestamps( + methodContext: ImmutableMethodContext, + startTimestamp: number, + endTimestamp: number, + ): Promise>; + setValidatorsParams( + methodContext: MethodContext, + validatorSetter: NextValidatorsSetter, + preCommitThreshold: bigint, + certificateThreshold: bigint, + validators: { address: Buffer; bftWeight: bigint }[], + ): Promise; +} + +export interface RandomMethod { + getRandomBytes( + methodContext: ImmutableMethodContext, + height: number, + numberOfSeeds: number, + ): Promise; +} + +export interface ValidatorKeys { + generatorKey: Buffer; + blsKey: Buffer; +} + +export interface FeeMethod { + payFee(methodContext: MethodContext, amount: bigint): void; +} + +interface PoAValidator { + address: Buffer; + name: string; + blsKey: Buffer; + proofOfPossession: Buffer; + generatorKey: Buffer; +} + +export interface ActiveValidator { + address: Buffer; + weight: bigint; +} + +export interface SnapshotSubstore { + activeValidators: ActiveValidator[]; + threshold: bigint; +} + +export interface GenesisPoAStore { + validators: PoAValidator[]; + snapshotSubstore: SnapshotSubstore; +} + +export interface UpdateGeneratorKeyParams { + generatorKey: Buffer; +} + +export interface Validator { + address: string; + name: string; + weight: string; +} diff --git a/framework/src/modules/poa/utils.ts b/framework/src/modules/poa/utils.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/framework/src/modules/pos/module.ts b/framework/src/modules/pos/module.ts index 3b07fe23858..3feb7bdd462 100644 --- a/framework/src/modules/pos/module.ts +++ b/framework/src/modules/pos/module.ts @@ -71,12 +71,11 @@ import { equalUnlocking, isUsername, selectStandbyValidators, - shuffleValidatorList, sortUnlocking, getModuleConfig, getValidatorWeight, - ValidatorWeight, isSharingCoefficientSorted, + ValidatorWeight, } from './utils'; import { ValidatorStore } from './stores/validator'; import { GenesisDataStore } from './stores/genesis'; @@ -94,6 +93,7 @@ import { CommissionChangeEvent } from './events/commission_change'; import { ClaimRewardsCommand } from './commands/claim_rewards'; import { getMainchainID } from '../interoperability/utils'; import { RewardsAssignedEvent } from './events/rewards_assigned'; +import { shuffleValidatorList } from '../utils'; export class PoSModule extends BaseModule { public method = new PoSMethod(this.stores, this.events); @@ -691,7 +691,7 @@ export class PoSModule extends BaseModule { } // Update the validators - const shuffledValidators = shuffleValidatorList(randomSeed1, validators); + const shuffledValidators = shuffleValidatorList(randomSeed1, validators); let aggregateBFTWeight = BigInt(0); const bftValidators: { address: Buffer; bftWeight: bigint }[] = []; for (const v of shuffledValidators) { diff --git a/framework/src/modules/pos/stores/eligible_validators.ts b/framework/src/modules/pos/stores/eligible_validators.ts index 64009ddd605..6066bd0a858 100644 --- a/framework/src/modules/pos/stores/eligible_validators.ts +++ b/framework/src/modules/pos/stores/eligible_validators.ts @@ -69,8 +69,8 @@ export class EligibleValidatorsStore extends BaseStore { } public splitKey(key: Buffer): [Buffer, bigint] { - const weightBytes = key.slice(0, 8); - const address = key.slice(8); + const weightBytes = key.subarray(0, 8); + const address = key.subarray(8); return [address, weightBytes.readBigUInt64BE()]; } diff --git a/framework/src/modules/pos/utils.ts b/framework/src/modules/pos/utils.ts index c91b08b57ec..b4ce7d2ce34 100644 --- a/framework/src/modules/pos/utils.ts +++ b/framework/src/modules/pos/utils.ts @@ -12,7 +12,7 @@ * Removal or modification of this copyright notice is prohibited. */ -import { utils, ed } from '@liskhq/lisk-cryptography'; +import { ed } from '@liskhq/lisk-cryptography'; import { math } from '@liskhq/lisk-utils'; import { ModuleConfig, @@ -99,31 +99,6 @@ export const pickStandByValidator = ( return -1; }; -export const shuffleValidatorList = ( - previousRoundSeed1: Buffer, - addresses: ValidatorWeight[], -): ValidatorWeight[] => { - const validatorList = [...addresses].map(validator => ({ - ...validator, - })) as { address: Buffer; roundHash: Buffer; weight: bigint }[]; - - for (const validator of validatorList) { - const seedSource = Buffer.concat([previousRoundSeed1, validator.address]); - validator.roundHash = utils.hash(seedSource); - } - - validatorList.sort((validator1, validator2) => { - const diff = validator1.roundHash.compare(validator2.roundHash); - if (diff !== 0) { - return diff; - } - - return validator1.address.compare(validator2.address); - }); - - return validatorList; -}; - export const selectStandbyValidators = ( validatorWeights: ValidatorWeight[], randomSeed1: Buffer, diff --git a/framework/src/modules/random/endpoint.ts b/framework/src/modules/random/endpoint.ts index 23a23b4fe2f..fa9f9c1d914 100644 --- a/framework/src/modules/random/endpoint.ts +++ b/framework/src/modules/random/endpoint.ts @@ -32,7 +32,7 @@ import { setHashOnionUsageRequest, } from './schemas'; import { ValidatorRevealsStore } from './stores/validator_reveals'; -import { getSeedRevealValidity } from './utils'; +import { isSeedValidInput } from './utils'; import { HashOnionStore } from './stores/hash_onion'; import { UsedHashOnionStoreObject, UsedHashOnionsStore } from './stores/used_hash_onions'; @@ -48,10 +48,11 @@ export class RandomEndpoint extends BaseEndpoint { const { validatorReveals } = await randomDataStore.get(ctx, EMPTY_KEY); return { - valid: getSeedRevealValidity( + valid: isSeedValidInput( cryptography.address.getAddressFromLisk32Address(generatorAddress), Buffer.from(seedReveal, 'hex'), validatorReveals, + false, ), }; } diff --git a/framework/src/modules/random/method.ts b/framework/src/modules/random/method.ts index 6b5cc7171d8..9e96f0d7945 100644 --- a/framework/src/modules/random/method.ts +++ b/framework/src/modules/random/method.ts @@ -20,7 +20,7 @@ import { EMPTY_KEY } from '../validators/constants'; import { blockHeaderAssetRandomModule } from './schemas'; import { ValidatorRevealsStore } from './stores/validator_reveals'; import { BlockHeaderAssetRandomModule } from './types'; -import { getSeedRevealValidity, getRandomSeed } from './utils'; +import { isSeedValidInput, getRandomSeed } from './utils'; export class RandomMethod extends BaseMethod { private readonly _moduleName: string; @@ -47,7 +47,7 @@ export class RandomMethod extends BaseMethod { asset, ); - return getSeedRevealValidity(generatorAddress, seedReveal, validatorReveals); + return isSeedValidInput(generatorAddress, seedReveal, validatorReveals, false); } public async getRandomBytes( diff --git a/framework/src/modules/random/utils.ts b/framework/src/modules/random/utils.ts index 6e17b41b2c7..b666fd5bc79 100644 --- a/framework/src/modules/random/utils.ts +++ b/framework/src/modules/random/utils.ts @@ -19,43 +19,23 @@ import { ValidatorSeedReveal } from './stores/validator_reveals'; export const isSeedValidInput = ( generatorAddress: Buffer, seedReveal: Buffer, - validatorsReveal: ValidatorSeedReveal[], + validatorReveals: ValidatorSeedReveal[], + previousSeedRequired = true, ) => { let lastSeed: ValidatorSeedReveal | undefined; // by construction, validatorsReveal is order by height asc. Therefore, looping from end will give highest value. - for (let i = validatorsReveal.length - 1; i >= 0; i -= 1) { - const validatorReveal = validatorsReveal[i]; + for (let i = validatorReveals.length - 1; i >= 0; i -= 1) { + const validatorReveal = validatorReveals[i]; if (validatorReveal.generatorAddress.equals(generatorAddress)) { lastSeed = validatorReveal; break; } } - // if the last seed is does not exist, seed reveal is invalid for use - if (!lastSeed) { - return false; - } - return lastSeed.seedReveal.equals(utils.hash(seedReveal).slice(0, SEED_LENGTH)); -}; -export const getSeedRevealValidity = ( - generatorAddress: Buffer, - seedReveal: Buffer, - validatorsReveal: ValidatorSeedReveal[], -) => { - let lastSeed: ValidatorSeedReveal | undefined; - let maxheight = 0; - for (const validatorReveal of validatorsReveal) { - if ( - validatorReveal.generatorAddress.equals(generatorAddress) && - validatorReveal.height > maxheight - ) { - maxheight = validatorReveal.height; - - lastSeed = validatorReveal; - } + if (!lastSeed) { + return !previousSeedRequired; } - - return !lastSeed || lastSeed.seedReveal.equals(utils.hash(seedReveal).slice(0, SEED_LENGTH)); + return lastSeed.seedReveal.equals(utils.hash(seedReveal).subarray(0, SEED_LENGTH)); }; export const getRandomSeed = ( @@ -69,19 +49,16 @@ export const getRandomSeed = ( if (height < 0 || numberOfSeeds < 0) { throw new Error('Height or number of seeds cannot be negative.'); } - if (numberOfSeeds > 1000) { - throw new Error('Number of seeds cannot be greater than 1000.'); - } - const initRandomBuffer = utils.intToBuffer(height + numberOfSeeds, 4); - let randomSeed = utils.hash(initRandomBuffer).slice(0, SEED_LENGTH); + const initRandomBuffer = utils.intToBuffer(height + numberOfSeeds, 4); + const currentSeeds = [utils.hash(initRandomBuffer).subarray(0, 16)]; let isInFuture = true; - const currentSeeds = []; + for (const validatorReveal of validatorsReveal) { if (validatorReveal.height >= height) { isInFuture = false; - if (validatorReveal.height < height + numberOfSeeds) { - currentSeeds.push(validatorReveal); + if (validatorReveal.height < height + numberOfSeeds && validatorReveal.valid) { + currentSeeds.push(validatorReveal.seedReveal); } } } @@ -90,28 +67,28 @@ export const getRandomSeed = ( throw new Error('Height is in the future.'); } - for (const seedObject of currentSeeds) { - if (seedObject.valid) { - randomSeed = bitwiseXOR([randomSeed, seedObject.seedReveal]); - } - } - - return randomSeed; + return bitwiseXOR(currentSeeds); }; export const bitwiseXOR = (bufferArray: Buffer[]): Buffer => { + if (bufferArray.length === 0) { + throw new Error('bitwiseXOR requires at least one buffer for the input.'); + } + if (bufferArray.length === 1) { return bufferArray[0]; } - const bufferSizes = new Set(bufferArray.map(buffer => buffer.length)); - if (bufferSizes.size > 1) { - throw new Error('All input for XOR should be same size'); + const size = bufferArray[0].length; + for (let i = 1; i < bufferArray.length; i += 1) { + if (bufferArray[i].length !== size) { + throw new Error('All input for XOR should be same size'); + } } - const outputSize = [...bufferSizes][0]; - const result = Buffer.alloc(outputSize, 0); - for (let i = 0; i < outputSize; i += 1) { + const result = Buffer.alloc(size); + + for (let i = 0; i < size; i += 1) { // eslint-disable-next-line no-bitwise result[i] = bufferArray.map(b => b[i]).reduce((a, b) => a ^ b, 0); } diff --git a/framework/src/modules/token/cc_commands/cc_transfer.ts b/framework/src/modules/token/cc_commands/cc_transfer.ts index 204f797b13a..1ec92a87a37 100644 --- a/framework/src/modules/token/cc_commands/cc_transfer.ts +++ b/framework/src/modules/token/cc_commands/cc_transfer.ts @@ -12,8 +12,6 @@ * Removal or modification of this copyright notice is prohibited. */ import { codec } from '@liskhq/lisk-codec'; -import { validator } from '@liskhq/lisk-validator'; -// import { NotFoundError } from '@liskhq/lisk-db'; import { BaseCCCommand } from '../../interoperability/base_cc_command'; import { CrossChainMessageContext } from '../../interoperability/types'; import { TokenMethod } from '../method'; @@ -84,7 +82,6 @@ export class CrossChainTransferCommand extends BaseCCCommand { crossChainTransferMessageParams, ccm.params, ); - validator.validate(crossChainTransferMessageParams, params); const { tokenID, amount, senderAddress } = params; recipientAddress = params.recipientAddress; const [tokenChainID] = splitTokenID(tokenID); diff --git a/framework/src/modules/token/cc_method.ts b/framework/src/modules/token/cc_method.ts index b05487cc7a7..ae48543c884 100644 --- a/framework/src/modules/token/cc_method.ts +++ b/framework/src/modules/token/cc_method.ts @@ -180,7 +180,7 @@ export class TokenInteroperableMethod extends BaseCCMethod { public async recover(ctx: RecoverContext): Promise { const methodContext = ctx.getMethodContext(); const userStore = this.stores.get(UserStore); - const address = ctx.storeKey.slice(0, ADDRESS_LENGTH); + const address = ctx.storeKey.subarray(0, ADDRESS_LENGTH); let account: UserStoreData; if ( @@ -215,8 +215,8 @@ export class TokenInteroperableMethod extends BaseCCMethod { throw new Error('Invalid arguments.'); } - const chainID = ctx.storeKey.slice(ADDRESS_LENGTH, ADDRESS_LENGTH + CHAIN_ID_LENGTH); - const tokenID = ctx.storeKey.slice(ADDRESS_LENGTH, ADDRESS_LENGTH + TOKEN_ID_LENGTH); + const chainID = ctx.storeKey.subarray(ADDRESS_LENGTH, ADDRESS_LENGTH + CHAIN_ID_LENGTH); + const tokenID = ctx.storeKey.subarray(ADDRESS_LENGTH, ADDRESS_LENGTH + TOKEN_ID_LENGTH); const totalAmount = account.availableBalance + account.lockedBalances.reduce((prev, curr) => prev + curr.amount, BigInt(0)); diff --git a/framework/src/modules/token/endpoint.ts b/framework/src/modules/token/endpoint.ts index 55bf862dc1e..f7a4cd2ca27 100644 --- a/framework/src/modules/token/endpoint.ts +++ b/framework/src/modules/token/endpoint.ts @@ -52,7 +52,7 @@ export class TokenEndpoint extends BaseEndpoint { return { balances: userData.map(({ key, value: user }) => ({ - tokenID: key.slice(20).toString('hex'), + tokenID: key.subarray(20).toString('hex'), availableBalance: user.availableBalance.toString(), lockedBalances: user.lockedBalances.map(b => ({ amount: b.amount.toString(), @@ -120,7 +120,7 @@ export class TokenEndpoint extends BaseEndpoint { // main chain token const mainchainTokenID = Buffer.concat([ - context.chainID.slice(0, 1), + context.chainID.subarray(0, 1), Buffer.alloc(TOKEN_ID_LENGTH - 1, 0), ]); supportedTokens.push(mainchainTokenID.toString('hex')); @@ -168,8 +168,8 @@ export class TokenEndpoint extends BaseEndpoint { }); return { escrowedAmounts: escrowData.map(({ key, value: escrow }) => { - const escrowChainID = key.slice(0, CHAIN_ID_LENGTH); - const tokenID = key.slice(CHAIN_ID_LENGTH); + const escrowChainID = key.subarray(0, CHAIN_ID_LENGTH); + const tokenID = key.subarray(CHAIN_ID_LENGTH); return { escrowChainID: escrowChainID.toString('hex'), amount: escrow.amount.toString(), diff --git a/framework/src/modules/token/method.ts b/framework/src/modules/token/method.ts index a9fbf5cada1..a0f2c099f17 100644 --- a/framework/src/modules/token/method.ts +++ b/framework/src/modules/token/method.ts @@ -79,7 +79,7 @@ export class TokenMethod extends BaseMethod { } public getTokenIDLSK(): Buffer { - const networkID = this._config.ownChainID.slice(0, 1); + const networkID = this._config.ownChainID.subarray(0, 1); // 3 bytes for remaining chainID bytes return Buffer.concat([networkID, Buffer.alloc(3 + LOCAL_ID_LENGTH, 0)]); } diff --git a/framework/src/modules/token/module.ts b/framework/src/modules/token/module.ts index c6e46815a6a..2912177ae3d 100644 --- a/framework/src/modules/token/module.ts +++ b/framework/src/modules/token/module.ts @@ -393,7 +393,7 @@ export class TokenModule extends BaseInteroperableModule { ); } for (const tokenID of supportedTokenIDsData.supportedTokenIDs) { - if (!tokenID.slice(0, CHAIN_ID_LENGTH).equals(supportedTokenIDsData.chainID)) { + if (!tokenID.subarray(0, CHAIN_ID_LENGTH).equals(supportedTokenIDsData.chainID)) { throw new Error('supportedTokensSubstore tokenIDs must match the chainID.'); } } @@ -412,7 +412,7 @@ export class TokenModule extends BaseInteroperableModule { lte: Buffer.alloc(ADDRESS_LENGTH + TOKEN_ID_LENGTH, 255), }); for (const { key, value: user } of allUsers) { - const tokenID = key.slice(ADDRESS_LENGTH); + const tokenID = key.subarray(ADDRESS_LENGTH); const [chainID] = splitTokenID(tokenID); if (chainID.equals(context.chainID)) { const existingSupply = computedSupply.get(tokenID) ?? BigInt(0); @@ -429,7 +429,7 @@ export class TokenModule extends BaseInteroperableModule { lte: Buffer.alloc(CHAIN_ID_LENGTH + TOKEN_ID_LENGTH, 255), }); for (const { key, value } of allEscrows) { - const tokenID = key.slice(CHAIN_ID_LENGTH); + const tokenID = key.subarray(CHAIN_ID_LENGTH); const existingSupply = computedSupply.get(tokenID) ?? BigInt(0); computedSupply.set(tokenID, existingSupply + value.amount); } diff --git a/framework/src/modules/token/stores/user.ts b/framework/src/modules/token/stores/user.ts index bc84a7960de..b1e2e37e888 100644 --- a/framework/src/modules/token/stores/user.ts +++ b/framework/src/modules/token/stores/user.ts @@ -12,8 +12,8 @@ * Removal or modification of this copyright notice is prohibited. */ import { NotFoundError } from '@liskhq/lisk-db'; -import { BaseStore, ImmutableStoreGetter, StoreGetter } from '../../base_store'; -import { MAX_MODULE_NAME_LENGTH, MIN_MODULE_NAME_LENGTH, TOKEN_ID_LENGTH } from '../constants'; +import { BaseStore, StoreGetter } from '../../base_store'; +import { MAX_MODULE_NAME_LENGTH, MIN_MODULE_NAME_LENGTH } from '../constants'; import { TokenID } from '../types'; export interface UserStoreData { @@ -53,15 +53,6 @@ export const userStoreSchema = { export class UserStore extends BaseStore { public schema = userStoreSchema; - // TODO: Remove this function when updating the methods - public async accountExist(context: ImmutableStoreGetter, address: Buffer): Promise { - const allUserData = await this.iterate(context, { - gte: Buffer.concat([address, Buffer.alloc(TOKEN_ID_LENGTH, 0)]), - lte: Buffer.concat([address, Buffer.alloc(TOKEN_ID_LENGTH, 255)]), - }); - return allUserData.length !== 0; - } - public async createDefaultAccount( context: StoreGetter, address: Buffer, diff --git a/framework/src/modules/token/utils.ts b/framework/src/modules/token/utils.ts index b784a44cff1..7116cf9f14f 100644 --- a/framework/src/modules/token/utils.ts +++ b/framework/src/modules/token/utils.ts @@ -19,8 +19,8 @@ export const splitTokenID = (tokenID: TokenID): [Buffer, Buffer] => { if (tokenID.length !== TOKEN_ID_LENGTH) { throw new Error(`Token ID must have length ${TOKEN_ID_LENGTH}`); } - const chainID = tokenID.slice(0, CHAIN_ID_LENGTH); - const localID = tokenID.slice(CHAIN_ID_LENGTH); + const chainID = tokenID.subarray(0, CHAIN_ID_LENGTH); + const localID = tokenID.subarray(CHAIN_ID_LENGTH); return [chainID, localID]; }; diff --git a/framework/src/modules/utils/index.ts b/framework/src/modules/utils/index.ts new file mode 100644 index 00000000000..945dcdf9f82 --- /dev/null +++ b/framework/src/modules/utils/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export { shuffleValidatorList } from './shuffleValidatorList'; diff --git a/framework/src/modules/utils/shuffleValidatorList.ts b/framework/src/modules/utils/shuffleValidatorList.ts new file mode 100644 index 00000000000..1cfea0ae9bf --- /dev/null +++ b/framework/src/modules/utils/shuffleValidatorList.ts @@ -0,0 +1,46 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils } from '@liskhq/lisk-cryptography'; + +export const shuffleValidatorList = < + T extends { + readonly address: Buffer; + weight: bigint; + }, +>( + roundSeed: Buffer, + addresses: T[], +): (T & { roundHash: Buffer })[] => { + const validatorList = [...addresses].map(validator => ({ + ...validator, + roundHash: Buffer.from([]), + })) as (T & { roundHash: Buffer })[]; + + for (const validator of validatorList) { + const seedSource = Buffer.concat([roundSeed, validator.address]); + validator.roundHash = utils.hash(seedSource); + } + + validatorList.sort((validator1, validator2) => { + const diff = validator1.roundHash.compare(validator2.roundHash); + if (diff !== 0) { + return diff; + } + + return validator1.address.compare(validator2.address); + }); + + return validatorList; +}; diff --git a/framework/src/modules/validators/method.ts b/framework/src/modules/validators/method.ts index dad2006d0a8..18180f445ce 100644 --- a/framework/src/modules/validators/method.ts +++ b/framework/src/modules/validators/method.ts @@ -360,7 +360,7 @@ export class ValidatorsMethod extends BaseMethod { throw new Error(`BLS public key must be ${BLS_PUBLIC_KEY_LENGTH} bytes long.`); } if (args.proofOfPossession && args.proofOfPossession.length !== BLS_POP_LENGTH) { - throw new Error(`Proof of possesion must be ${BLS_POP_LENGTH} bytes long.`); + throw new Error(`Proof of Possession must be ${BLS_POP_LENGTH} bytes long.`); } if (args.generatorKey && args.generatorKey.length !== ED25519_PUBLIC_KEY_LENGTH) { throw new Error(`Generator key must be ${ED25519_PUBLIC_KEY_LENGTH} bytes long.`); diff --git a/framework/src/state_machine/event_queue.ts b/framework/src/state_machine/event_queue.ts index cdf49d54121..2e3408307c6 100644 --- a/framework/src/state_machine/event_queue.ts +++ b/framework/src/state_machine/event_queue.ts @@ -43,9 +43,7 @@ export class EventQueue { `Max size of event data is ${EVENT_MAX_EVENT_SIZE_BYTES} but received ${data.length}`, ); } - if (!allTopics.length) { - throw new Error('Topics must have at least one element.'); - } + if (allTopics.length > EVENT_MAX_TOPICS_PER_EVENT) { throw new Error( `Max topics per event is ${EVENT_MAX_TOPICS_PER_EVENT} but received ${allTopics.length}`, diff --git a/framework/src/state_machine/generator_context.ts b/framework/src/state_machine/generator_context.ts index 8f0fa12fb22..b1cfc103191 100644 --- a/framework/src/state_machine/generator_context.ts +++ b/framework/src/state_machine/generator_context.ts @@ -68,7 +68,7 @@ export class GenerationContext { this._stateStore.getStore(moduleID, storePrefix), stateStore: this._stateStore, getOffchainStore: (moduleID: Buffer, subStorePrefix: Buffer) => - this._generatorStore.getStore(moduleID, subStorePrefix.readUInt16BE(0)), + this._generatorStore.getStore(moduleID, subStorePrefix), header: this._header, assets: this._assets, chainID: this._chainID, diff --git a/framework/src/state_machine/prefixed_state_read_writer.ts b/framework/src/state_machine/prefixed_state_read_writer.ts index 04ace52cd94..e58b1716f8c 100644 --- a/framework/src/state_machine/prefixed_state_read_writer.ts +++ b/framework/src/state_machine/prefixed_state_read_writer.ts @@ -95,7 +95,7 @@ export class PrefixedStateReadWriter { }; const result = await this._readWriter.range(optionsWithKey); return result.map(kv => ({ - key: kv.key.slice(this._prefix.length), + key: kv.key.subarray(this._prefix.length), value: kv.value, })); } @@ -111,7 +111,7 @@ export class PrefixedStateReadWriter { }; const result = await this._readWriter.range(optionsWithKey); return result.map(kv => ({ - key: kv.key.slice(this._prefix.length), + key: kv.key.subarray(this._prefix.length), value: codec.decode(schema, kv.value), })); } diff --git a/framework/test/unit/engine/bft/bft_votes.spec.ts b/framework/test/unit/engine/bft/bft_votes.spec.ts index 2d3f905d831..7d2b14d0ef2 100644 --- a/framework/test/unit/engine/bft/bft_votes.spec.ts +++ b/framework/test/unit/engine/bft/bft_votes.spec.ts @@ -39,7 +39,7 @@ describe('BFT votes', () => { beforeEach(() => { accounts = [utils.getRandomBytes(20), utils.getRandomBytes(20), utils.getRandomBytes(20)]; bftVotes = { - maxHeightPrevoted: 103, + maxHeightPrevoted: 149, maxHeightPrecommitted: 56, maxHeightCertified: 5, blockBFTInfos: [ @@ -237,7 +237,60 @@ describe('BFT votes', () => { expect(paramsCache.getParameters).not.toHaveBeenCalled(); }); - it('should not stake on blocks if generator is not in the validators', async () => { + it('should vote on blocks with more than 1 BFT weight when validator holds more BFT weight', async () => { + const stateStore = new StateStore(new InMemoryDatabase()); + const paramsStore = stateStore.getStore(MODULE_STORE_PREFIX_BFT, STORE_PREFIX_BFT_PARAMETERS); + await paramsStore.setWithSchema( + utils.intToBuffer(101, 4), + { + prevoteThreshold: BigInt(68), + precommitThreshold: BigInt(68), + certificateThreshold: BigInt(68), + validators: [ + { + address: accounts[0], + bftWeight: BigInt(40), + blsKey: utils.getRandomBytes(48), + generatorKey: utils.getRandomBytes(32), + }, + { + address: accounts[1], + bftWeight: BigInt(0), + blsKey: utils.getRandomBytes(48), + generatorKey: utils.getRandomBytes(32), + }, + { + address: accounts[2], + bftWeight: BigInt(20), + blsKey: utils.getRandomBytes(48), + generatorKey: utils.getRandomBytes(32), + }, + ], + validatorsHash: utils.getRandomBytes(32), + }, + bftParametersSchema, + ); + paramsCache = new BFTParametersCache(paramsStore); + insertBlockBFTInfo( + bftVotes, + createFakeBlockHeader({ + height: 152, + maxHeightGenerated: 151, + generatorAddress: accounts[0], + }), + 5, + ); + await updatePrevotesPrecommits(bftVotes, paramsCache); + + expect(bftVotes.blockBFTInfos[0].prevoteWeight).toEqual(BigInt(40)); + expect(bftVotes.blockBFTInfos[3].precommitWeight).toEqual(BigInt(104)); + expect(bftVotes.blockBFTInfos[4].precommitWeight).toEqual(BigInt(107)); + // accounts[0] already voted on blockBFTInfos[1], so after this it should not get affected + expect(bftVotes.blockBFTInfos[1].prevoteWeight).toEqual(BigInt(65)); + expect(bftVotes.blockBFTInfos[2].prevoteWeight).toEqual(BigInt(65)); + }); + + it('should not vote on blocks if generator is not in the validators', async () => { jest.spyOn(paramsCache, 'getParameters'); insertBlockBFTInfo( bftVotes, @@ -323,7 +376,7 @@ describe('BFT votes', () => { bftVotes, createFakeBlockHeader({ height: 152, - maxHeightGenerated: 0, + maxHeightGenerated: 152, generatorAddress: accounts[2], }), 5, @@ -337,13 +390,13 @@ describe('BFT votes', () => { describe('updateMaxHeightPrevoted', () => { let paramsCache: BFTParametersCache; - it('should store maximum height where prevote exceeds threshold', async () => { + it('should store maxHeightPrevoted where prevote exceeds threshold', async () => { const stateStore = new StateStore(new InMemoryDatabase()); const paramsStore = stateStore.getStore(MODULE_STORE_PREFIX_BFT, STORE_PREFIX_BFT_PARAMETERS); await paramsStore.setWithSchema( utils.intToBuffer(101, 4), { - prevoteThreshold: BigInt(68), + prevoteThreshold: BigInt(65), precommitThreshold: BigInt(68), certificateThreshold: BigInt(68), validators: [ @@ -372,7 +425,7 @@ describe('BFT votes', () => { ); paramsCache = new BFTParametersCache(paramsStore); await expect(updateMaxHeightPrevoted(bftVotes, paramsCache)).toResolve(); - expect(bftVotes.maxHeightPrevoted).toBe(149); + expect(bftVotes.maxHeightPrevoted).toBe(151); }); it('should not update maxHeightPrevoted if no block info exceeds threshold', async () => { @@ -410,14 +463,14 @@ describe('BFT votes', () => { ); paramsCache = new BFTParametersCache(paramsStore); await expect(updateMaxHeightPrevoted(bftVotes, paramsCache)).toResolve(); - expect(bftVotes.maxHeightPrevoted).toBe(103); + expect(bftVotes.maxHeightPrevoted).toBe(149); }); }); describe('updateMaxHeightPrecommitted', () => { let paramsCache: BFTParametersCache; - it('should store maximum height where prevote exceeds threshold', async () => { + it('should store maxHeightPrecommitted where prevote exceeds threshold', async () => { const stateStore = new StateStore(new InMemoryDatabase()); const paramsStore = stateStore.getStore(MODULE_STORE_PREFIX_BFT, STORE_PREFIX_BFT_PARAMETERS); await paramsStore.setWithSchema( @@ -455,14 +508,14 @@ describe('BFT votes', () => { expect(bftVotes.maxHeightPrecommitted).toBe(148); }); - it('should not update maxHeightPrevoted if no block info exceeds threshold', async () => { + it('should not update maxHeightPrecommitted if no block info exceeds threshold', async () => { const stateStore = new StateStore(new InMemoryDatabase()); const paramsStore = stateStore.getStore(MODULE_STORE_PREFIX_BFT, STORE_PREFIX_BFT_PARAMETERS); await paramsStore.setWithSchema( utils.intToBuffer(101, 4), { prevoteThreshold: BigInt(68), - precommitThreshold: BigInt(103), + precommitThreshold: BigInt(69), certificateThreshold: BigInt(68), validators: [ { @@ -519,6 +572,7 @@ describe('BFT votes', () => { aggregateCommit: { aggregationBits: Buffer.alloc(0), certificateSignature: Buffer.alloc(0), + // this should never happen, because in the validation this height is required to be the same as bftVotes.maxheightCertified. height: 10, }, }), diff --git a/framework/test/unit/engine/bft/method.spec.ts b/framework/test/unit/engine/bft/method.spec.ts index 2a947325618..f67ac46b7f2 100644 --- a/framework/test/unit/engine/bft/method.spec.ts +++ b/framework/test/unit/engine/bft/method.spec.ts @@ -93,7 +93,7 @@ describe('BFT Method', () => { await votesStore.setWithSchema( EMPTY_KEY, { - maxHeightPrevoted: 10, + maxHeightPrevoted: 0, maxHeightPrecommitted: 0, maxHeightCertified: 0, blockBFTInfos: [ @@ -236,10 +236,12 @@ describe('BFT Method', () => { it('should return BFT parameters if it exists for the lower height', async () => { await expect(bftMethod.getBFTParameters(stateStore, 25)).resolves.toEqual(params20); + await expect(bftMethod.getBFTParameters(stateStore, 29)).resolves.toEqual(params20); }); it('should return BFT parameters if it exists for the height', async () => { await expect(bftMethod.getBFTParameters(stateStore, 20)).resolves.toEqual(params20); + await expect(bftMethod.getBFTParameters(stateStore, 30)).resolves.toEqual(params30); }); it('should throw if the BFT parameter does not exist for the height or lower', async () => { @@ -674,6 +676,7 @@ describe('BFT Method', () => { }); it('should return the next height strictly higher than the input where BFT parameter exists', async () => { + await expect(bftMethod.getNextHeightBFTParameters(stateStore, 19)).resolves.toBe(20); await expect(bftMethod.getNextHeightBFTParameters(stateStore, 20)).resolves.toBe(30); }); @@ -1049,4 +1052,59 @@ describe('BFT Method', () => { }); }); }); + + describe('getGeneratorAtTimestamp', () => { + const validators = new Array(103).fill(0).map(() => ({ + address: utils.getRandomBytes(20), + bftWeight: BigInt(1), + generatorKey: utils.getRandomBytes(32), + blsKey: utils.getRandomBytes(48), + })); + + beforeEach(async () => { + const bftParamsStore = stateStore.getStore( + MODULE_STORE_PREFIX_BFT, + STORE_PREFIX_BFT_PARAMETERS, + ); + await bftParamsStore.setWithSchema( + utils.intToBuffer(20, 4), + { + prevoteThreshold: BigInt(68), + precommitThreshold: BigInt(68), + certificateThreshold: BigInt(68), + validators, + validatorsHash: utils.getRandomBytes(32), + }, + bftParametersSchema, + ); + }); + + it('should return a validator in round robin', async () => { + for (let i = 0; i < 103; i += 1) { + // timestamp is computed to cover all possible modulo of 103 + await expect( + bftMethod.getGeneratorAtTimestamp(stateStore, 20, (103 * 1000000 + i) * 10), + ).resolves.toEqual(validators[i]); + } + }); + }); + + describe('getSlotNumber', () => { + it.each([ + { + input: 1683057470, + expected: 168305747, + }, + { + input: 1683057475, + expected: 168305747, + }, + { + input: 1683057479, + expected: 168305747, + }, + ])('should return expected value', ({ input, expected }) => { + expect(bftMethod.getSlotNumber(input)).toBe(expected); + }); + }); }); diff --git a/framework/test/unit/engine/bft/utils.spec.ts b/framework/test/unit/engine/bft/utils.spec.ts index 124f0f3cdb6..474fcdd866b 100644 --- a/framework/test/unit/engine/bft/utils.spec.ts +++ b/framework/test/unit/engine/bft/utils.spec.ts @@ -38,11 +38,13 @@ describe('bft utils', () => { const header1 = createFakeBlockHeader({ height: 10999, maxHeightPrevoted: 1099, + maxHeightGenerated: 0, generatorAddress, }); const header2 = createFakeBlockHeader({ height: 10999, maxHeightPrevoted: 1099, + maxHeightGenerated: 0, generatorAddress, }); @@ -53,11 +55,13 @@ describe('bft utils', () => { const header1 = createFakeBlockHeader({ height: 10999, maxHeightPrevoted: 1099, + maxHeightGenerated: 0, generatorAddress, }); const header2 = createFakeBlockHeader({ height: 11999, maxHeightPrevoted: 1099, + maxHeightGenerated: 0, generatorAddress, }); @@ -68,10 +72,13 @@ describe('bft utils', () => { const header1 = createFakeBlockHeader({ generatorAddress, height: 120, + maxHeightPrevoted: 0, + maxHeightGenerated: 0, }); const header2 = createFakeBlockHeader({ generatorAddress, height: 123, + maxHeightPrevoted: 0, maxHeightGenerated: 98, }); @@ -83,11 +90,13 @@ describe('bft utils', () => { generatorAddress, height: 133, maxHeightPrevoted: 101, + maxHeightGenerated: 0, }); const header2 = createFakeBlockHeader({ generatorAddress, height: 123, maxHeightPrevoted: 98, + maxHeightGenerated: 0, }); expect(areDistinctHeadersContradicting(header1, header2)).toBeTrue(); diff --git a/framework/test/unit/engine/consensus/consensus.spec.ts b/framework/test/unit/engine/consensus/consensus.spec.ts index 19adf9d4d19..2ca4d8b2331 100644 --- a/framework/test/unit/engine/consensus/consensus.spec.ts +++ b/framework/test/unit/engine/consensus/consensus.spec.ts @@ -220,6 +220,13 @@ describe('consensus', () => { } as never); await expect(initConsensus()).rejects.toThrow('Genesis block validators hash is invalid'); }); + + it('should fail initialization if ABI.commit fails', async () => { + // Arrange + (chain.genesisBlockExist as jest.Mock).mockResolvedValue(false); + jest.spyOn(consensus['_abi'], 'commit').mockRejectedValue(new Error('fail to commit')); + await expect(initConsensus()).rejects.toThrow('fail to commit'); + }); }); describe('certifySingleCommit', () => { @@ -703,6 +710,14 @@ describe('consensus', () => { expect(savingEvents).toHaveLength(3); savingEvents.forEach((e: Event, i: number) => expect(e.toObject().index).toEqual(i)); }); + + it('should reject when ABI.commit fails and it should not store the block', async () => { + jest.spyOn(chain, 'saveBlock'); + jest.spyOn(consensus['_abi'], 'commit').mockRejectedValue(new Error('fail to commit')); + + await expect(consensus['_executeValidated'](block)).rejects.toThrow('fail to commit'); + expect(chain.saveBlock).not.toHaveBeenCalled(); + }); }); describe('block verification', () => { @@ -728,9 +743,12 @@ describe('consensus', () => { it('should throw error when block timestamp is from future', () => { const invalidBlock = { ...block }; - jest.spyOn(bft.method, 'getSlotNumber').mockReturnValue(Math.floor(Date.now() / 10)); - - (invalidBlock.header as any).timestamp = Math.floor((Date.now() + 10000) / 1000); + jest + .spyOn(bft.method, 'getSlotNumber') + // return blockSlotNumber in the future + .mockReturnValueOnce(Math.floor(Date.now() / 1000 / 10) + 10000) + // return blockSlotNumber for the currrent value + .mockReturnValueOnce(Math.floor(Date.now() / 1000 / 10)); expect(() => consensus['_verifyTimestamp'](invalidBlock as any)).toThrow( `Invalid timestamp ${ @@ -866,6 +884,20 @@ describe('consensus', () => { ); }); + it('should throw error if the header impliesMaxPrevotes is not the same as the computed value', async () => { + when(consensus['_bft'].method.getBFTHeights as never) + .calledWith(stateStore) + .mockResolvedValue({ maxHeightPrevoted: block.header.maxHeightPrevoted } as never); + + when(consensus['_bft'].method.impliesMaximalPrevotes as never) + .calledWith(stateStore, block.header) + .mockResolvedValue(false as never); + + await expect(consensus['_verifyBFTProperties'](stateStore, block as any)).rejects.toThrow( + 'Invalid imply max prevote', + ); + }); + it('should be success if maxHeightPrevoted is valid and header is not contradicting', async () => { when(consensus['_bft'].method.getBFTHeights as never) .calledWith(stateStore) @@ -875,6 +907,10 @@ describe('consensus', () => { .calledWith(stateStore, block.header) .mockResolvedValue(false as never); + when(consensus['_bft'].method.impliesMaximalPrevotes as never) + .calledWith(stateStore, block.header) + .mockResolvedValue(true as never); + await expect( consensus['_verifyBFTProperties'](stateStore, block as any), ).resolves.toBeUndefined(); diff --git a/framework/test/unit/engine/legacy/codec.spec.ts b/framework/test/unit/engine/legacy/codec.spec.ts index 555bdb9ff8d..63b927d3f3d 100644 --- a/framework/test/unit/engine/legacy/codec.spec.ts +++ b/framework/test/unit/engine/legacy/codec.spec.ts @@ -36,7 +36,7 @@ describe('Legacy codec', () => { }); it('should fail to decode invalid block', () => { - expect(() => decodeBlock(encodedBlock.slice(2))).toThrow(); + expect(() => decodeBlock(encodedBlock.subarray(2))).toThrow(); }); }); diff --git a/framework/test/unit/modules/auth/fixtures.json b/framework/test/unit/modules/auth/fixtures.json deleted file mode 120000 index b7aa1791298..00000000000 --- a/framework/test/unit/modules/auth/fixtures.json +++ /dev/null @@ -1 +0,0 @@ -../../../../../protocol-specs/generator_outputs/multisignature_registration_transaction/multisignature_registration_transaction.json \ No newline at end of file diff --git a/framework/test/unit/modules/auth/module.spec.ts b/framework/test/unit/modules/auth/module.spec.ts index 1fa1322173f..01491d54fe6 100644 --- a/framework/test/unit/modules/auth/module.spec.ts +++ b/framework/test/unit/modules/auth/module.spec.ts @@ -11,14 +11,11 @@ * * Removal or modification of this copyright notice is prohibited. */ -import { Mnemonic } from '@liskhq/lisk-passphrase'; import { codec } from '@liskhq/lisk-codec'; -import { utils, ed, address as cryptoAddress, legacy } from '@liskhq/lisk-cryptography'; -import { Transaction, transactionSchema, TAG_TRANSACTION, BlockAssets } from '@liskhq/lisk-chain'; -import { objects as ObjectUtils } from '@liskhq/lisk-utils'; +import { utils, address as cryptoAddress } from '@liskhq/lisk-cryptography'; +import { Transaction, BlockAssets, EMPTY_BUFFER } from '@liskhq/lisk-chain'; import { when } from 'jest-when'; import { AuthModule } from '../../../../src/modules/auth'; -import * as fixtures from './fixtures.json'; import * as testing from '../../../../src/testing'; import { genesisAuthStoreSchema } from '../../../../src/modules/auth/schemas'; import { TransactionExecuteContext, VerifyStatus } from '../../../../src/state_machine'; @@ -30,78 +27,38 @@ import { authAccountSchema, AuthAccountStore, } from '../../../../src/modules/auth/stores/auth_account'; -import { ADDRESS_LENGTH, defaultConfig } from '../../../../src/modules/auth/constants'; -import { GenesisConfig } from '../../../../src'; +import { + chainID, + unsignedRegisterMultisigTx, + multisigAddress, + keyPairs, + multisigParams, +} from './multisig_fixture'; +import { ADDRESS_LENGTH } from '../../../../src/modules/auth/constants'; describe('AuthModule', () => { - let decodedMultiSignature: any; - let validTestTransaction: any; + let authAccountStoreMock: jest.Mock; + let storeMock: jest.Mock; let stateStore: any; let authModule: AuthModule; - let decodedBaseTransaction: any; - let passphrase: any; - let passphraseDerivedKeys: any; - let senderAccount: any; - - const { cloneDeep } = ObjectUtils; - const subStoreMock = jest.fn(); - const storeMock = jest.fn().mockReturnValue({ getWithSchema: subStoreMock }); - const defaultTestCase = fixtures.testCases[0]; - const chainID = Buffer.from(defaultTestCase.input.chainID, 'hex'); + const registerMultisigTx = new Transaction(unsignedRegisterMultisigTx); beforeEach(async () => { - authModule = new AuthModule(); - await authModule.init({ genesisConfig: {} as GenesisConfig, moduleConfig: defaultConfig }); - const buffer = Buffer.from(defaultTestCase.output.transaction, 'hex'); - const id = utils.hash(buffer); - decodedBaseTransaction = codec.decode(transactionSchema, buffer); - - decodedMultiSignature = { - ...decodedBaseTransaction, - id, - }; - - validTestTransaction = new Transaction(decodedMultiSignature); - - stateStore = { - getStore: storeMock, - }; - - senderAccount = { - address: Buffer.from(defaultTestCase.input.account.address, 'hex'), - }; - - when(subStoreMock) - .calledWith(senderAccount.address, authAccountSchema) - .mockReturnValue({ - mandatoryKeys: [], - optionalKeys: [], - nonce: BigInt(1), - numberOfSignatures: 0, - }); + authAccountStoreMock = jest.fn(); + storeMock = jest.fn().mockReturnValue({ getWithSchema: authAccountStoreMock }); - passphrase = Mnemonic.generateMnemonic(); - passphraseDerivedKeys = legacy.getPrivateAndPublicKeyFromPassphrase(passphrase); - const address = cryptoAddress.getAddressFromPublicKey(passphraseDerivedKeys.publicKey); - - when(subStoreMock) - .calledWith(address, authAccountSchema) - .mockReturnValue({ - mandatoryKeys: [], - optionalKeys: [], - nonce: BigInt(0), - numberOfSignatures: 0, - }); + authModule = new AuthModule(); + await authModule.init({ genesisConfig: {}, moduleConfig: {} } as never); + stateStore = { getStore: storeMock }; }); describe('initGenesisState', () => { - const address = utils.getRandomBytes(ADDRESS_LENGTH); const publicKey = utils.getRandomBytes(32); const validAsset = { authDataSubstore: [ { - address, + address: utils.getRandomBytes(ADDRESS_LENGTH), authAccount: { numberOfSignatures: 0, mandatoryKeys: [], @@ -296,7 +253,6 @@ describe('AuthModule', () => { describe.each(invalidTestData)('%p', (_, data) => { it('should throw error when asset is invalid', async () => { - // eslint-disable-next-line @typescript-eslint/ban-types const assetBytes = codec.encode(genesisAuthStoreSchema, data as object); const context = createGenesisBlockContext({ stateStore, @@ -310,60 +266,49 @@ describe('AuthModule', () => { describe('verifyTransaction', () => { describe('Invalid nonce errors', () => { - it('should return FAIL status with error when trx nonce is lower than account nonce', async () => { - // Arrange - const accountNonce = BigInt(2); + const accountNonce = BigInt(2); - when(subStoreMock).calledWith(senderAccount.address, authAccountSchema).mockReturnValue({ + beforeEach(() => { + when(authAccountStoreMock).calledWith(multisigAddress, authAccountSchema).mockReturnValue({ mandatoryKeys: [], optionalKeys: [], nonce: accountNonce, numberOfSignatures: 0, }); + }); + it('should return FAIL status with error when trx nonce is lower than account nonce', async () => { const context = testing .createTransactionContext({ stateStore, - transaction: validTestTransaction, + transaction: registerMultisigTx, chainID, }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( + await expect(authModule.verifyTransaction(context)).rejects.toThrow( new InvalidNonceError( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Transaction with id:${validTestTransaction.id.toString( + `Transaction with id:${registerMultisigTx.id.toString( 'hex', )} nonce is lower than account nonce.`, - validTestTransaction.nonce, + registerMultisigTx.nonce, accountNonce, ), ); }); it('should return PENDING status with no error when trx nonce is higher than account nonce', async () => { - // Arrange const transaction = new Transaction({ module: 'token', command: 'transfer', - nonce: BigInt('2'), - fee: BigInt('100000000'), - senderPublicKey: passphraseDerivedKeys.publicKey, + nonce: BigInt(4), + fee: BigInt(100_000_000), + senderPublicKey: keyPairs[0].publicKey, params: utils.getRandomBytes(100), signatures: [], }); - validTestTransaction = new Transaction(decodedMultiSignature); - - const signature = ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getBytes(), - passphraseDerivedKeys.privateKey, - ); - - transaction.signatures.push(signature); + transaction.sign(chainID, keyPairs[0].privateKey); const context = testing .createTransactionContext({ @@ -373,52 +318,67 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).resolves.toEqual({ + await expect(authModule.verifyTransaction(context)).resolves.toEqual({ status: VerifyStatus.PENDING, }); }); }); describe('Multi-signature registration transaction', () => { - it('should not throw for valid transaction', async () => { - // Arrange + registerMultisigTx.sign(chainID, keyPairs[0].privateKey); + + it('should not throw for a valid transaction', async () => { + when(authAccountStoreMock) + .calledWith(multisigAddress, authAccountSchema) + .mockReturnValue({ + mandatoryKeys: [], + optionalKeys: [], + nonce: BigInt(0), + numberOfSignatures: 0, + }); + const context = testing .createTransactionContext({ stateStore, - transaction: validTestTransaction, + transaction: registerMultisigTx, chainID, }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).resolves.toEqual({ + await expect(authModule.verifyTransaction(context)).resolves.toEqual({ status: VerifyStatus.OK, }); }); }); - describe('Transaction from single signatures account', () => { - it('should not throw for valid transaction', async () => { - // Arrange - const transaction = new Transaction({ + describe('Transaction from a single signature account', () => { + let transaction: Transaction; + + const singleSigAddress = cryptoAddress.getAddressFromPublicKey(keyPairs[1].publicKey); + + beforeEach(() => { + transaction = new Transaction({ module: 'token', command: 'transfer', - nonce: BigInt('0'), - fee: BigInt('100000000'), - senderPublicKey: passphraseDerivedKeys.publicKey, + nonce: BigInt(0), + fee: BigInt(100_000_000), + senderPublicKey: keyPairs[1].publicKey, params: utils.getRandomBytes(100), signatures: [], }); - const signature = ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getBytes(), - passphraseDerivedKeys.privateKey, - ); + when(authAccountStoreMock) + .calledWith(singleSigAddress, authAccountSchema) + .mockReturnValue({ + mandatoryKeys: [], + optionalKeys: [], + nonce: BigInt(0), + numberOfSignatures: 0, + }); + }); - transaction.signatures.push(signature); + it('should not throw for a valid transaction', async () => { + transaction.sign(chainID, keyPairs[1].privateKey); const context = testing .createTransactionContext({ @@ -428,23 +388,13 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).resolves.toEqual({ + await expect(authModule.verifyTransaction(context)).resolves.toEqual({ status: VerifyStatus.OK, }); }); - it('should throw if signature is missing', async () => { - // Arrange - const transaction = new Transaction({ - module: 'token', - command: 'transfer', - nonce: BigInt('0'), - fee: BigInt('100000000'), - senderPublicKey: passphraseDerivedKeys.publicKey, - params: utils.getRandomBytes(100), - signatures: [], - }); + it('should throw if signature is invalid', async () => { + transaction.signatures.push(utils.getRandomBytes(64)); const context = testing .createTransactionContext({ @@ -454,35 +404,28 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error( - 'Transactions from a single signature account should have exactly one signature. Found 0 signatures.', - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + 'Failed to validate signature', ); }); - it('should throw error if account is not multi signature and more than one signature present', async () => { - // Arrange - const transaction = new Transaction({ - module: 'token', - command: 'transfer', - nonce: BigInt('0'), - fee: BigInt('100000000'), - senderPublicKey: passphraseDerivedKeys.publicKey, - params: utils.getRandomBytes(100), - signatures: [], - }); + it('should throw if signature is missing', async () => { + const context = testing + .createTransactionContext({ + stateStore, + transaction, + chainID, + }) + .createTransactionVerifyContext(); - const signature = ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getBytes(), - passphraseDerivedKeys.privateKey, + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + 'Transactions from a single signature account should have exactly one signature. Found 0 signatures.', ); + }); - transaction.signatures.push(signature); - transaction.signatures.push(signature); + it('should throw error if account is not multi signature and more than one signature present', async () => { + transaction.sign(chainID, keyPairs[1].privateKey); + transaction.signatures.push(utils.getRandomBytes(64)); const context = testing .createTransactionContext({ @@ -492,119 +435,55 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error( - 'Transactions from a single signature account should have exactly one signature. Found 2 signatures.', - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + 'Transactions from a single signature account should have exactly one signature. Found 2 signatures.', ); }); }); - describe('Transaction from multi-signatures account', () => { - interface memberFixture { - passphrase: string; - keys?: { - privateKey: Buffer; - publicKey: Buffer; - }; - address?: Buffer; - } - - interface membersFixture { - [key: string]: memberFixture; - } - - const members: membersFixture = { - mainAccount: { - passphrase: 'order trip this crop race amused climb rather taxi morning holiday team', - }, - mandatoryA: { - passphrase: - 'clock cradle permit opinion hobby excite athlete weird soap mesh valley belt', - }, - mandatoryB: { - passphrase: - 'team dignity frost rookie gesture gaze piano daring fruit patrol chalk hidden', - }, - optionalA: { - passphrase: - 'welcome hello ostrich country drive car river jaguar warfare color tell risk', - }, - optionalB: { - passphrase: 'beef volcano emotion say lab reject small repeat reveal napkin bunker make', - }, - }; - - for (const aMember of Object.values(members)) { - aMember.keys = { - ...legacy.getPrivateAndPublicKeyFromPassphrase(aMember.passphrase), - }; - aMember.address = cryptoAddress.getAddressFromPublicKey(aMember.keys.publicKey); - } + describe('Transaction from a multi-signature account', () => { + let transaction: Transaction; - const multisigAccount = { - address: members.mainAccount.address, - numberOfSignatures: 3, - mandatoryKeys: [members.mandatoryA.keys?.publicKey, members.mandatoryB.keys?.publicKey], - optionalKeys: [members.optionalA.keys?.publicKey, members.optionalB.keys?.publicKey], + const privateKeys = { + mandatory: [keyPairs[0].privateKey, keyPairs[1].privateKey], + optional: [keyPairs[2].privateKey, keyPairs[3].privateKey], }; - let transaction: Transaction; - beforeEach(() => { - when(subStoreMock) - .calledWith(multisigAccount.address, authAccountSchema) + when(authAccountStoreMock) + .calledWith(multisigAddress, authAccountSchema) .mockResolvedValue({ numberOfSignatures: 3, - mandatoryKeys: [members.mandatoryA.keys?.publicKey, members.mandatoryB.keys?.publicKey], - optionalKeys: [members.optionalA.keys?.publicKey, members.optionalB.keys?.publicKey], + mandatoryKeys: multisigParams.mandatoryKeys, + optionalKeys: multisigParams.optionalKeys, nonce: BigInt(0), }); transaction = new Transaction({ module: 'token', command: 'transfer', - nonce: BigInt('0'), - fee: BigInt('100000000'), - senderPublicKey: (members as any).mainAccount.keys.publicKey, + nonce: BigInt(0), + fee: BigInt(100_000_000), + senderPublicKey: multisigParams.mandatoryKeys[0], params: utils.getRandomBytes(100), signatures: [], }); }); - it('should not throw for valid transaction', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalA.keys.privateKey, - ), - ); + it('should verify a valid transaction from a 1-of-2 multisig account with 0 mandatory signers', async () => { + when(authAccountStoreMock) + .calledWith(multisigAddress, authAccountSchema) + .mockResolvedValue({ + numberOfSignatures: 1, + mandatoryKeys: [], + optionalKeys: multisigParams.optionalKeys, + nonce: BigInt(0), + }); - transaction.signatures.push(Buffer.from('')); + transaction.sign(chainID, privateKeys.optional[0]); + transaction.signatures.push(EMPTY_BUFFER); - const context = testing + let context = testing .createTransactionContext({ stateStore, transaction, @@ -612,39 +491,16 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).resolves.toEqual({ + await expect(authModule.verifyTransaction(context)).resolves.toEqual({ status: VerifyStatus.OK, }); - }); - - it('should not throw for multisignature account with only optional', async () => { - // Arrange - const optionalOnlyMultisigAccount = cloneDeep(multisigAccount); - optionalOnlyMultisigAccount.mandatoryKeys = []; - optionalOnlyMultisigAccount.numberOfSignatures = 1; - when(subStoreMock) - .calledWith(optionalOnlyMultisigAccount.address, authAccountSchema) - .mockResolvedValue({ - numberOfSignatures: 1, - mandatoryKeys: [], - optionalKeys: [members.optionalA.keys?.publicKey, members.optionalB.keys?.publicKey], - nonce: BigInt(0), - }); + // now do the same, but with the other optional signature present + transaction.signatures.splice(0, 2); + transaction.signatures.push(EMPTY_BUFFER); + transaction.sign(chainID, privateKeys.optional[1]); - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalA.keys.privateKey, - ), - ); - - transaction.signatures.push(Buffer.from('')); - - const context = testing + context = testing .createTransactionContext({ stateStore, transaction, @@ -652,42 +508,17 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).resolves.toEqual({ + await expect(authModule.verifyTransaction(context)).resolves.toEqual({ status: VerifyStatus.OK, }); }); - it('should not throw for valid transaction when first optional is present', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); + it('should verify a valid transaction from 3-of-4 multisig account with 2 mandatory signers, when the first optional signature is present', async () => { + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.sign(chainID, privateKeys.optional[0]); + transaction.signatures.push(EMPTY_BUFFER); - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalA.keys.privateKey, - ), - ); - - transaction.signatures.push(Buffer.from('')); const context = testing .createTransactionContext({ stateStore, @@ -696,42 +527,16 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).resolves.toEqual({ + await expect(authModule.verifyTransaction(context)).resolves.toEqual({ status: VerifyStatus.OK, }); }); - it('should not throw for valid transaction when second optional is present', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push(Buffer.from('')); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalB.keys.privateKey, - ), - ); + it('should verify a valid transaction from 3-of-4 multisig account with 2 mandatory signers, when the second optional signature is present', async () => { + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.signatures.push(EMPTY_BUFFER); + transaction.sign(chainID, privateKeys.optional[1]); const context = testing .createTransactionContext({ @@ -741,40 +546,16 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).resolves.toEqual({ + await expect(authModule.verifyTransaction(context)).resolves.toEqual({ status: VerifyStatus.OK, }); }); - it('should throw for transaction where non optional absent signature is not empty buffer', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); + it('should throw when an optional absent signature is not replaced by an empty buffer', async () => { + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.sign(chainID, privateKeys.optional[1]); - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalB.keys.privateKey, - ), - ); const context = testing .createTransactionContext({ stateStore, @@ -783,53 +564,18 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error( - `Transaction signatures does not match required number of signatures: '3' for transaction with id '${transaction.id.toString( - 'hex', - )}'`, - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + `Transaction signatures does not match required number of signatures: '3' for transaction with id '${transaction.id.toString( + 'hex', + )}'`, ); }); - it('should throw error if number of provided signatures is bigger than numberOfSignatures in account asset', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalB.keys.privateKey, - ), - ); + it('should throw when a transaction from 3-of-4 multisig account has 4 signatures', async () => { + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.sign(chainID, privateKeys.optional[0]); + transaction.sign(chainID, privateKeys.optional[1]); const context = testing .createTransactionContext({ @@ -839,37 +585,18 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error( - `Transaction signatures does not match required number of signatures: '3' for transaction with id '${transaction.id.toString( - 'hex', - )}'`, - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + `Transaction signatures does not match required number of signatures: '3' for transaction with id '${transaction.id.toString( + 'hex', + )}'`, ); }); - it('should throw error if number of provided signatures is smaller than numberOfSignatures in account asset', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push(Buffer.from('')); + it('should throw when a transaction from 3-of-4 multisig account with 2 mandatory signers has only 2 mandatory signatures', async () => { + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.signatures.push(EMPTY_BUFFER); + transaction.signatures.push(EMPTY_BUFFER); const context = testing .createTransactionContext({ @@ -879,46 +606,18 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error( - `Transaction signatures does not match required number of signatures: '3' for transaction with id '${transaction.id.toString( - 'hex', - )}'`, - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + `Transaction signatures does not match required number of signatures: '3' for transaction with id '${transaction.id.toString( + 'hex', + )}'`, ); }); - it('should throw for transaction with valid numberOfSignatures but missing mandatory key signature', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); - - transaction.signatures.push(Buffer.from('')); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalB.keys.privateKey, - ), - ); + it('should throw when a transaction from 3-of-4 multisig account with 2 mandatory signers has only 2 optional signatures', async () => { + transaction.signatures.push(EMPTY_BUFFER); + transaction.signatures.push(EMPTY_BUFFER); + transaction.sign(chainID, privateKeys.optional[0]); + transaction.sign(chainID, privateKeys.optional[1]); const context = testing .createTransactionContext({ @@ -928,42 +627,18 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error('Missing signature for a mandatory key.'), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + `Transaction signatures does not match required number of signatures: '3' for transaction with id '${transaction.id.toString( + 'hex', + )}'`, ); }); - it('should throw error if any of the mandatory signatures is not valid', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalA.keys.privateKey, - ), - ); - - transaction.signatures.push(Buffer.from('')); + it('should throw if a mandatory signature is absent', async () => { + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.signatures.push(EMPTY_BUFFER); + transaction.sign(chainID, privateKeys.optional[0]); + transaction.sign(chainID, privateKeys.optional[1]); const context = testing .createTransactionContext({ @@ -973,45 +648,16 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).resolves.toEqual({ - status: VerifyStatus.OK, - }); - }); - - it('should throw error if any of the optional signatures is not valid', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push(Buffer.from('')); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalB.keys.privateKey, - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + 'Missing signature for a mandatory key.', ); + }); - // We change the first byte of the 2nd optional signature - transaction.signatures[3][0] = 10; + it('should throw if a mandatory signature is invalid', async () => { + transaction.signatures.push(utils.getRandomBytes(64)); + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.signatures.push(EMPTY_BUFFER); + transaction.sign(chainID, privateKeys.optional[1]); const context = testing .createTransactionContext({ @@ -1021,46 +667,35 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error( - `Failed to validate signature '${transaction.signatures[3].toString( - 'hex', - )}' for transaction with id '${transaction.id.toString('hex')}'`, - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + 'Failed to validate signature', ); }); - it('should throw error if mandatory signatures are not in order', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); + it('should throw if an optional signature is invalid', async () => { + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.signatures.push(utils.getRandomBytes(64)); + transaction.signatures.push(EMPTY_BUFFER); - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, + const context = testing + .createTransactionContext({ + stateStore, + transaction, chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); + }) + .createTransactionVerifyContext(); - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalA.keys.privateKey, - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + 'Failed to validate signature', ); + }); - transaction.signatures.push(Buffer.from('')); + it('should throw if mandatory signatures are not in order', async () => { + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.signatures.push(EMPTY_BUFFER); + transaction.sign(chainID, privateKeys.optional[1]); const context = testing .createTransactionContext({ @@ -1070,46 +705,18 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error( - `Failed to validate signature '${transaction.signatures[0].toString( - 'hex', - )}' for transaction with id '${transaction.id.toString('hex')}'`, - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + `Failed to validate signature '${transaction.signatures[0].toString( + 'hex', + )}' for transaction with id '${transaction.id.toString('hex')}'`, ); }); - it('should throw error if optional signatures are not in order', async () => { - // Arrange - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryA.keys.privateKey, - ), - ); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).mandatoryB.keys.privateKey, - ), - ); - - transaction.signatures.push(Buffer.from('')); - - transaction.signatures.push( - ed.signDataWithPrivateKey( - TAG_TRANSACTION, - chainID, - transaction.getSigningBytes(), - (members as any).optionalA.keys.privateKey, - ), - ); + it('should throw if optional signatures are not in order', async () => { + transaction.sign(chainID, privateKeys.mandatory[0]); + transaction.sign(chainID, privateKeys.mandatory[1]); + transaction.signatures.push(EMPTY_BUFFER); + transaction.sign(chainID, privateKeys.optional[0]); const context = testing .createTransactionContext({ @@ -1119,13 +726,10 @@ describe('AuthModule', () => { }) .createTransactionVerifyContext(); - // Act & Assert - return expect(authModule.verifyTransaction(context)).rejects.toThrow( - new Error( - `Failed to validate signature '${transaction.signatures[3].toString( - 'hex', - )}' for transaction with id '${transaction.id.toString('hex')}'`, - ), + await expect(authModule.verifyTransaction(context)).rejects.toThrow( + `Failed to validate signature '${transaction.signatures[3].toString( + 'hex', + )}' for transaction with id '${transaction.id.toString('hex')}'`, ); }); }); @@ -1142,21 +746,20 @@ describe('AuthModule', () => { context = testing .createTransactionContext({ stateStore, - transaction: validTestTransaction, + transaction: registerMultisigTx, chainID, }) .createTransactionExecuteContext(); }); it('should increment account nonce after a transaction', async () => { - const address = cryptoAddress.getAddressFromPublicKey(validTestTransaction.senderPublicKey); const authAccountBeforeTransaction = { - nonce: BigInt(validTestTransaction.nonce), + nonce: BigInt(registerMultisigTx.nonce), numberOfSignatures: 4, - mandatoryKeys: [utils.getRandomBytes(64), utils.getRandomBytes(64)], - optionalKeys: [utils.getRandomBytes(64), utils.getRandomBytes(64)], + mandatoryKeys: multisigParams.mandatoryKeys, + optionalKeys: multisigParams.optionalKeys, }; - await authAccountStore.set(context, address, authAccountBeforeTransaction); + await authAccountStore.set(context, multisigAddress, authAccountBeforeTransaction); await authModule.beforeCommandExecute(context); diff --git a/framework/test/unit/modules/auth/multisig_fixture.ts b/framework/test/unit/modules/auth/multisig_fixture.ts new file mode 100644 index 00000000000..e1fd2fc7d4c --- /dev/null +++ b/framework/test/unit/modules/auth/multisig_fixture.ts @@ -0,0 +1,91 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { ed, address } from '@liskhq/lisk-cryptography'; +import { codec } from '@liskhq/lisk-codec'; +import { Transaction } from '@liskhq/lisk-chain'; +import { + registerMultisignatureParamsSchema, + multisigRegMsgSchema, +} from '../../../../src/modules/auth/schemas'; +import { RegisterMultisignatureParams } from '../../../../src/modules/auth/types'; +import { MESSAGE_TAG_MULTISIG_REG } from '../../../../src/modules/auth/constants'; + +const keyPairsString = [ + { + privateKey: + '2475a8233503caade9542f2dd6c8c725f10bc03e3f809210b768f0a2320f06d50904c986211330582ef5e41ed9a2e7d6730bb7bdc59459a0caaaba55be4ec128', + publicKey: '0904c986211330582ef5e41ed9a2e7d6730bb7bdc59459a0caaaba55be4ec128', + }, + { + privateKey: + '985bc97b4b2aa91d590dde455c19c70818d97c56c7cfff790a1e0b71e3d15962557f1b9647fd2aefa357fed8bead72d1b02e5151b57d3c32d4d3f808c0705026', + publicKey: '557f1b9647fd2aefa357fed8bead72d1b02e5151b57d3c32d4d3f808c0705026', + }, + { + privateKey: + 'd0b159fe5a7cc3d5f4b39a97621b514bc55b0a0f1aca8adeed2dd1899d93f103a3f96c50d0446220ef2f98240898515cbba8155730679ca35326d98dcfb680f0', + publicKey: 'a3f96c50d0446220ef2f98240898515cbba8155730679ca35326d98dcfb680f0', + }, + { + privateKey: + '03e7852c6f1c6fe5cd0c5f7e3a36e499a1e0207e867f74f5b5bc42bfcc888bc8b8d2422aa7ebf1f85031f0bac2403be1fb24e0196d3bbed33987d4769eb37411', + publicKey: 'b8d2422aa7ebf1f85031f0bac2403be1fb24e0196d3bbed33987d4769eb37411', + }, +]; + +export const keyPairs = keyPairsString.map(keyPair => ({ + privateKey: Buffer.from(keyPair.privateKey, 'hex'), + publicKey: Buffer.from(keyPair.publicKey, 'hex'), +})); + +export const chainID = Buffer.from('04000000', 'hex'); + +export const multisigParams = { + numberOfSignatures: 4, + mandatoryKeys: [keyPairs[0].publicKey, keyPairs[1].publicKey], + optionalKeys: [keyPairs[2].publicKey, keyPairs[3].publicKey], +}; + +export const multisigAddress = address.getAddressFromPublicKey(multisigParams.mandatoryKeys[0]); +const decodedMessage = { + address: multisigAddress, + nonce: BigInt(0), + ...multisigParams, +}; +const encodedMessage = codec.encode(multisigRegMsgSchema, decodedMessage); +const signatures: Buffer[] = []; +for (const keyPair of keyPairs) { + signatures.push( + ed.signData(MESSAGE_TAG_MULTISIG_REG, chainID, encodedMessage, keyPair.privateKey), + ); +} + +export const decodedParams: RegisterMultisignatureParams = { + numberOfSignatures: multisigParams.numberOfSignatures, + mandatoryKeys: multisigParams.mandatoryKeys, + optionalKeys: multisigParams.optionalKeys, + signatures, +}; +const encodedParams = codec.encode(registerMultisignatureParamsSchema, decodedParams); + +export const unsignedRegisterMultisigTx = new Transaction({ + module: 'auth', + command: 'registerMultisignature', + fee: BigInt('100000000'), + params: encodedParams, + nonce: BigInt(0), + senderPublicKey: keyPairs[0].publicKey, + signatures: [], +}); diff --git a/framework/test/unit/modules/auth/register_multisignature.spec.ts b/framework/test/unit/modules/auth/register_multisignature.spec.ts index 6098405ac77..c1e52ab1f53 100644 --- a/framework/test/unit/modules/auth/register_multisignature.spec.ts +++ b/framework/test/unit/modules/auth/register_multisignature.spec.ts @@ -16,7 +16,6 @@ import { Transaction } from '@liskhq/lisk-chain'; import { codec } from '@liskhq/lisk-codec'; import { utils } from '@liskhq/lisk-cryptography'; import { validator } from '@liskhq/lisk-validator'; -import * as fixtures from './fixtures.json'; import * as testing from '../../../../src/testing'; import { RegisterMultisignatureCommand } from '../../../../src/modules/auth/commands/register_multisignature'; import { registerMultisignatureParamsSchema } from '../../../../src/modules/auth/schemas'; @@ -25,32 +24,33 @@ import { VerifyStatus } from '../../../../src/state_machine'; import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; import { InMemoryPrefixedStateDB } from '../../../../src/testing/in_memory_prefixed_state'; import { AuthModule } from '../../../../src/modules/auth'; -import { AuthAccountStore } from '../../../../src/modules/auth/stores/auth_account'; +import { AuthAccount, AuthAccountStore } from '../../../../src/modules/auth/stores/auth_account'; import { InvalidSignatureEvent } from '../../../../src/modules/auth/events/invalid_signature'; import { MultisignatureRegistrationEvent } from '../../../../src/modules/auth/events/multisignature_registration'; +import { chainID, decodedParams, keyPairs, unsignedRegisterMultisigTx } from './multisig_fixture'; describe('Register Multisignature command', () => { let registerMultisignatureCommand: RegisterMultisignatureCommand; let stateStore: PrefixedStateReadWriter; - let authStore: AuthAccountStore; - let transaction: Transaction; - let decodedParams: RegisterMultisignatureParams; + let authAccountStore: AuthAccountStore; const authModule = new AuthModule(); - const defaultTestCase = fixtures.testCases[0]; - const chainID = Buffer.from(defaultTestCase.input.chainID, 'hex'); + + const defaultAuthAccount: AuthAccount = { + numberOfSignatures: 0, + mandatoryKeys: [], + optionalKeys: [], + nonce: BigInt(0), + }; + + const transaction = new Transaction(unsignedRegisterMultisigTx); + transaction.sign(chainID, keyPairs[0].privateKey); beforeEach(() => { registerMultisignatureCommand = new RegisterMultisignatureCommand( authModule.stores, authModule.events, ); - const buffer = Buffer.from(defaultTestCase.output.transaction, 'hex'); - transaction = Transaction.fromBytes(buffer); - decodedParams = codec.decode( - registerMultisignatureParamsSchema, - transaction.params, - ); }); describe('verify schema', () => { it('should return error if params has numberOfSignatures > 64', () => { @@ -165,7 +165,7 @@ describe('Register Multisignature command', () => { }); describe('verify', () => { - it('should return status OK for valid params', async () => { + it('should return status OK for valid params: 2 mandatory, 2 optional and all 4 required signatures present', async () => { const context = testing .createTransactionContext({ transaction, @@ -174,17 +174,91 @@ describe('Register Multisignature command', () => { .createCommandVerifyContext( registerMultisignatureParamsSchema, ); + + const result = await registerMultisignatureCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.OK); + }); + + it('should return status OK for valid params: 2 mandatory keys, 0 optional keys and both 2 required signatures present', async () => { + const params = codec.encode(registerMultisignatureParamsSchema, { + ...decodedParams, + optionalKeys: [], + numberOfSignatures: 2, + signatures: [decodedParams.signatures[0], decodedParams.signatures[1]], + }); + + const context = testing + .createTransactionContext({ + transaction: new Transaction({ ...transaction.toObject(), params }), + chainID, + }) + .createCommandVerifyContext( + registerMultisignatureParamsSchema, + ); + + const result = await registerMultisignatureCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.OK); + }); + + it('should return status OK for valid params: 0 mandatory keys, 2 optional keys and both 2 required signatures present', async () => { + const params = codec.encode(registerMultisignatureParamsSchema, { + ...decodedParams, + mandatoryKeys: [], + numberOfSignatures: 2, + signatures: [decodedParams.signatures[2], decodedParams.signatures[3]], + }); + + const context = testing + .createTransactionContext({ + transaction: new Transaction({ ...transaction.toObject(), params }), + chainID, + }) + .createCommandVerifyContext( + registerMultisignatureParamsSchema, + ); + + const result = await registerMultisignatureCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.OK); + }); + + it('should return status OK when the total number of mandatory and optional keys is 64', async () => { + const mandatoryKeys = [...Array(20).keys()].map(() => utils.getRandomBytes(32)); + const optionalKeys = [...Array(44).keys()].map(() => utils.getRandomBytes(32)); + + mandatoryKeys.sort((a, b) => a.compare(b)); + optionalKeys.sort((a, b) => a.compare(b)); + + const params = codec.encode(registerMultisignatureParamsSchema, { + mandatoryKeys, + optionalKeys, + numberOfSignatures: 64, + signatures: [...Array(64).keys()].map(() => utils.getRandomBytes(64)), + }); + + const context = testing + .createTransactionContext({ + transaction: new Transaction({ ...transaction.toObject(), params }), + chainID, + }) + .createCommandVerifyContext( + registerMultisignatureParamsSchema, + ); + const result = await registerMultisignatureCommand.verify(context); expect(result.status).toBe(VerifyStatus.OK); }); it('should return error when there are duplicated mandatory keys', async () => { + const publicKey = utils.getRandomBytes(32); const params = codec.encode(registerMultisignatureParamsSchema, { ...decodedParams, - mandatoryKeys: [decodedParams.mandatoryKeys[0], decodedParams.mandatoryKeys[0]], - signatures: [utils.getRandomBytes(64)], + mandatoryKeys: [publicKey, publicKey], }); + const context = testing .createTransactionContext({ transaction: new Transaction({ ...transaction.toObject(), params }), @@ -195,15 +269,17 @@ describe('Register Multisignature command', () => { ); const result = await registerMultisignatureCommand.verify(context); + expect(result.error?.message).toBe('MandatoryKeys contains duplicate public keys.'); }); it('should return error when there are duplicated optional keys', async () => { + const publicKey = utils.getRandomBytes(32); const params = codec.encode(registerMultisignatureParamsSchema, { ...decodedParams, - optionalKeys: [decodedParams.optionalKeys[0], decodedParams.optionalKeys[0]], - signatures: [utils.getRandomBytes(64)], + optionalKeys: [publicKey, publicKey], }); + const context = testing .createTransactionContext({ transaction: new Transaction({ ...transaction.toObject(), params }), @@ -214,6 +290,7 @@ describe('Register Multisignature command', () => { ); const result = await registerMultisignatureCommand.verify(context); + expect(result.error?.message).toBe('OptionalKeys contains duplicate public keys.'); }); @@ -221,8 +298,8 @@ describe('Register Multisignature command', () => { const params = codec.encode(registerMultisignatureParamsSchema, { ...decodedParams, numberOfSignatures: 5, - signatures: [utils.getRandomBytes(64)], }); + const context = testing .createTransactionContext({ transaction: new Transaction({ ...transaction.toObject(), params }), @@ -231,7 +308,9 @@ describe('Register Multisignature command', () => { .createCommandVerifyContext( registerMultisignatureParamsSchema, ); + const result = await registerMultisignatureCommand.verify(context); + expect(result.error?.message).toBe( 'The numberOfSignatures is bigger than the count of Mandatory and Optional keys.', ); @@ -243,6 +322,7 @@ describe('Register Multisignature command', () => { numberOfSignatures: 1, signatures: [utils.getRandomBytes(64)], }); + const context = testing .createTransactionContext({ transaction: new Transaction({ ...transaction.toObject(), params }), @@ -251,7 +331,9 @@ describe('Register Multisignature command', () => { .createCommandVerifyContext( registerMultisignatureParamsSchema, ); + const result = await registerMultisignatureCommand.verify(context); + expect(result.error?.message).toBe( 'The numberOfSignatures needs to be equal or bigger than the number of Mandatory keys.', ); @@ -259,16 +341,10 @@ describe('Register Multisignature command', () => { it('should return error when mandatory and optional key sets are not disjointed', async () => { const params = codec.encode(registerMultisignatureParamsSchema, { - numberOfSignatures: 2, - mandatoryKeys: [ - Buffer.from('48e041ae61a32777c899c1f1b0a9588bdfe939030613277a39556518cc66d371', 'hex'), - Buffer.from('483077a8b23208f2fd85dacec0fbb0b590befea0a1fcd76a5b43f33063aaa180', 'hex'), - ], - optionalKeys: [ - Buffer.from('483077a8b23208f2fd85dacec0fbb0b590befea0a1fcd76a5b43f33063aaa180', 'hex'), - ], - signatures: [utils.getRandomBytes(64)], + ...decodedParams, + optionalKeys: [keyPairs[0].publicKey, keyPairs[2].publicKey], }); + const context = testing .createTransactionContext({ transaction: new Transaction({ ...transaction.toObject(), params }), @@ -279,6 +355,7 @@ describe('Register Multisignature command', () => { ); const result = await registerMultisignatureCommand.verify(context); + expect(result.error?.message).toBe( 'Invalid combination of Mandatory and Optional keys. Repeated keys across Mandatory and Optional were found.', ); @@ -287,9 +364,9 @@ describe('Register Multisignature command', () => { it('should return error when mandatory keys set is not sorted', async () => { const params = codec.encode(registerMultisignatureParamsSchema, { ...decodedParams, - numberOfSignatures: 2, - mandatoryKeys: [decodedParams.mandatoryKeys[1], decodedParams.mandatoryKeys[0]], + mandatoryKeys: [keyPairs[1].publicKey, keyPairs[0].publicKey], }); + const context = testing .createTransactionContext({ transaction: new Transaction({ ...transaction.toObject(), params }), @@ -300,15 +377,16 @@ describe('Register Multisignature command', () => { ); const result = await registerMultisignatureCommand.verify(context); + expect(result.error?.message).toBe('Mandatory keys should be sorted lexicographically.'); }); it('should return error when optional keys set is not sorted', async () => { const params = codec.encode(registerMultisignatureParamsSchema, { ...decodedParams, - numberOfSignatures: 2, - optionalKeys: [decodedParams.optionalKeys[1], decodedParams.optionalKeys[0]], + optionalKeys: [keyPairs[3].publicKey, keyPairs[2].publicKey], }); + const context = testing .createTransactionContext({ transaction: new Transaction({ ...transaction.toObject(), params }), @@ -319,6 +397,7 @@ describe('Register Multisignature command', () => { ); const result = await registerMultisignatureCommand.verify(context); + expect(result.error?.message).toBe('Optional keys should be sorted lexicographically.'); }); }); @@ -328,23 +407,10 @@ describe('Register Multisignature command', () => { beforeEach(() => { stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); - authStore = authModule.stores.get(AuthAccountStore); + authAccountStore = authModule.stores.get(AuthAccountStore); }); - it('should not throw when registering for first time', async () => { - await authStore.set( - { - getStore: (storePrefix: Buffer, substorePrefix: Buffer) => - stateStore.getStore(storePrefix, substorePrefix), - }, - transaction.senderAddress, - { - optionalKeys: [], - mandatoryKeys: [], - numberOfSignatures: 0, - nonce: BigInt(0), - }, - ); + it('should not throw when registering for the first time and signatures are valid', async () => { const context = testing .createTransactionContext({ stateStore, @@ -355,16 +421,18 @@ describe('Register Multisignature command', () => { registerMultisignatureParamsSchema, ); - context.eventQueue = eventQueueMock; + await authAccountStore.set(context, transaction.senderAddress, defaultAuthAccount); + context.eventQueue = eventQueueMock; jest.spyOn(authModule.events.get(MultisignatureRegistrationEvent), 'log'); await expect(registerMultisignatureCommand.execute(context)).resolves.toBeUndefined(); - const updatedStore = authModule.stores.get(AuthAccountStore); - const updatedData = await updatedStore.get(context, transaction.senderAddress); - expect(updatedData.numberOfSignatures).toBe(decodedParams.numberOfSignatures); - expect(updatedData.mandatoryKeys).toEqual(decodedParams.mandatoryKeys); - expect(updatedData.optionalKeys).toEqual(decodedParams.optionalKeys); + + const authAccount = await authAccountStore.get(context, transaction.senderAddress); + + expect(authAccount.numberOfSignatures).toBe(decodedParams.numberOfSignatures); + expect(authAccount.mandatoryKeys).toEqual(decodedParams.mandatoryKeys); + expect(authAccount.optionalKeys).toEqual(decodedParams.optionalKeys); expect(authModule.events.get(MultisignatureRegistrationEvent).log).toHaveBeenCalledWith( expect.anything(), transaction.senderAddress, @@ -376,23 +444,13 @@ describe('Register Multisignature command', () => { ); }); - it('should throw when incorrect signature', async () => { - const buffer = Buffer.from(defaultTestCase.output.transaction, 'hex'); - const multiSignatureTx = Transaction.fromBytes(buffer); - const multiSignatureTxDecodedParams = codec.decode( - registerMultisignatureParamsSchema, - multiSignatureTx.params, - ); + it('should throw when the signature is incorrect', async () => { const invalidSignature = utils.getRandomBytes(64); - multiSignatureTxDecodedParams.signatures[0] = invalidSignature; + decodedParams.signatures[0] = invalidSignature; - const paramsBytes = codec.encode( - registerMultisignatureParamsSchema, - multiSignatureTxDecodedParams, - ); const invalidTransaction = new Transaction({ - ...multiSignatureTx.toObject(), - params: paramsBytes, + ...transaction.toObject(), + params: codec.encode(registerMultisignatureParamsSchema, decodedParams), }); const context = testing @@ -405,16 +463,10 @@ describe('Register Multisignature command', () => { registerMultisignatureParamsSchema, ); - await authStore.set(context, transaction.senderAddress, { - optionalKeys: [], - mandatoryKeys: [], - numberOfSignatures: 0, - nonce: BigInt(0), - }); - + await authAccountStore.set(context, transaction.senderAddress, defaultAuthAccount); context.eventQueue = eventQueueMock; - jest.spyOn(authModule.events.get(InvalidSignatureEvent), 'error'); + await expect(registerMultisignatureCommand.execute(context)).rejects.toThrow( `Invalid signature for public key ${context.params.mandatoryKeys[0].toString('hex')}.`, ); diff --git a/framework/test/unit/modules/auth/utils.spec.ts b/framework/test/unit/modules/auth/utils.spec.ts new file mode 100644 index 00000000000..3f5d5e00ddd --- /dev/null +++ b/framework/test/unit/modules/auth/utils.spec.ts @@ -0,0 +1,52 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils, ed } from '@liskhq/lisk-cryptography'; +import { Transaction, TAG_TRANSACTION } from '@liskhq/lisk-chain'; +import { verifySignature } from '../../../../src/modules/auth/utils'; + +describe('utils', () => { + describe('verifySignature', () => { + const chainID = Buffer.from('04000000', 'hex'); + + it('should verify a valid transaction signature', async () => { + const privateKey = await ed.getPrivateKeyFromPhraseAndPath('hello lisk', "m/44'/134'/0'"); + const publicKey = ed.getPublicKeyFromPrivateKey(privateKey); + + const transaction = new Transaction({ + module: 'token', + command: 'transfer', + nonce: BigInt('0'), + fee: BigInt('100000000'), + senderPublicKey: publicKey, + params: utils.getRandomBytes(100), + signatures: [], + }); + + const transactionSigningBytes = transaction.getSigningBytes(); + const signature = ed.signDataWithPrivateKey( + TAG_TRANSACTION, + chainID, + transactionSigningBytes, + privateKey, + ); + + transaction.signatures.push(signature); + + expect(() => + verifySignature(chainID, publicKey, signature, transactionSigningBytes, transaction.id), + ).not.toThrow(); + }); + }); +}); diff --git a/framework/test/unit/modules/base_store.spec.ts b/framework/test/unit/modules/base_store.spec.ts index 7e909aef002..a32b8c5a6e4 100644 --- a/framework/test/unit/modules/base_store.spec.ts +++ b/framework/test/unit/modules/base_store.spec.ts @@ -85,6 +85,10 @@ describe('BaseStore', () => { await expect(store.has(context, key)).resolves.toBeTrue(); }); + + it('should return false when key does not exist', async () => { + await expect(store.has(context, key)).resolves.toBeFalse(); + }); }); describe('set', () => { diff --git a/framework/test/unit/modules/dynamic_reward/module.spec.ts b/framework/test/unit/modules/dynamic_reward/module.spec.ts index 3260ad2ccaa..001b7971325 100644 --- a/framework/test/unit/modules/dynamic_reward/module.spec.ts +++ b/framework/test/unit/modules/dynamic_reward/module.spec.ts @@ -57,6 +57,21 @@ describe('DynamicRewardModule', () => { let randomMethod: RandomMethod; let validatorsMethod: ValidatorsMethod; let posMethod: PoSMethod; + let generatorAddress: Buffer; + let standbyValidatorAddress: Buffer; + let stateStore: PrefixedStateReadWriter; + let blockHeader: BlockHeader; + + const activeValidator = 4; + const minimumReward = + (BigInt(defaultConfig.brackets[0]) * + BigInt(defaultConfig.factorMinimumRewardActiveValidators)) / + DECIMAL_PERCENT_FACTOR; + const totalRewardActiveValidator = BigInt(defaultConfig.brackets[0]) * BigInt(activeValidator); + const stakeRewardActiveValidators = + totalRewardActiveValidator - minimumReward * BigInt(activeValidator); + // generatorAddress has 20% of total weight, bftWeightSum/bftWeight = BigInt(5) + const defaultReward = minimumReward + stakeRewardActiveValidators / BigInt(5); beforeEach(async () => { rewardModule = new DynamicRewardModule(); @@ -124,9 +139,7 @@ describe('DynamicRewardModule', () => { }); describe('initGenesisState', () => { - let blockHeader: BlockHeader; let blockExecuteContext: GenesisBlockExecuteContext; - let stateStore: PrefixedStateReadWriter; beforeEach(() => { stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); @@ -149,23 +162,12 @@ describe('DynamicRewardModule', () => { describe('beforeTransactionsExecute', () => { let blockExecuteContext: BlockExecuteContext; - let generatorAddress: Buffer; - let standbyValidatorAddress: Buffer; - let stateStore: PrefixedStateReadWriter; - - const activeValidator = 4; - const minimumReward = - (BigInt(defaultConfig.brackets[0]) * - BigInt(defaultConfig.factorMinimumRewardActiveValidators)) / - DECIMAL_PERCENT_FACTOR; - const totalRewardActiveValidator = BigInt(defaultConfig.brackets[0]) * BigInt(activeValidator); - const ratioReward = totalRewardActiveValidator - minimumReward * BigInt(activeValidator); beforeEach(async () => { generatorAddress = utils.getRandomBytes(20); standbyValidatorAddress = utils.getRandomBytes(20); stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); - const blockHeader = createBlockHeaderWithDefaults({ + blockHeader = createBlockHeaderWithDefaults({ height: defaultConfig.offset, generatorAddress, }); @@ -221,9 +223,8 @@ describe('DynamicRewardModule', () => { await rewardModule.beforeTransactionsExecute(blockExecuteContext); - // generatorAddress has 20% of total weight expect(blockExecuteContext.contextStore.get(CONTEXT_STORE_KEY_DYNAMIC_BLOCK_REWARD)).toEqual( - minimumReward + ratioReward / BigInt(5), // 20 / 100 for generator address + defaultReward, ); expect( blockExecuteContext.contextStore.get(CONTEXT_STORE_KEY_DYNAMIC_BLOCK_REDUCTION), @@ -241,7 +242,7 @@ describe('DynamicRewardModule', () => { generatorMap, ); - const blockHeader = createBlockHeaderWithDefaults({ + blockHeader = createBlockHeaderWithDefaults({ height: defaultConfig.offset, generatorAddress: standbyValidatorAddress, }); @@ -259,24 +260,64 @@ describe('DynamicRewardModule', () => { blockExecuteContext.contextStore.get(CONTEXT_STORE_KEY_DYNAMIC_BLOCK_REDUCTION), ).toEqual(REWARD_NO_REDUCTION); }); + + it('should store zero reward with seed reveal reduction when seed reveal is invalid', async () => { + // Round not finished + const generatorMap = new Array(1).fill(0).reduce(prev => { + // eslint-disable-next-line no-param-reassign + prev[utils.getRandomBytes(20).toString('binary')] = 1; + return prev; + }, {}); + (validatorsMethod.getGeneratorsBetweenTimestamps as jest.Mock).mockResolvedValue( + generatorMap, + ); + (randomMethod.isSeedRevealValid as jest.Mock).mockResolvedValue(false); + + await rewardModule.beforeTransactionsExecute(blockExecuteContext); + + expect(blockExecuteContext.contextStore.get(CONTEXT_STORE_KEY_DYNAMIC_BLOCK_REWARD)).toEqual( + BigInt(0), + ); + expect( + blockExecuteContext.contextStore.get(CONTEXT_STORE_KEY_DYNAMIC_BLOCK_REDUCTION), + ).toEqual(REWARD_REDUCTION_SEED_REVEAL); + }); + + it('should return quarter deducted reward when header does not imply max prevotes', async () => { + // Round not finished + const generatorMap = new Array(1).fill(0).reduce(prev => { + // eslint-disable-next-line no-param-reassign + prev[utils.getRandomBytes(20).toString('binary')] = 1; + return prev; + }, {}); + (validatorsMethod.getGeneratorsBetweenTimestamps as jest.Mock).mockResolvedValue( + generatorMap, + ); + blockHeader = createBlockHeaderWithDefaults({ + height: defaultConfig.offset, + impliesMaxPrevotes: false, + generatorAddress, + }); + blockExecuteContext = createBlockContext({ + stateStore, + header: blockHeader, + }).getBlockAfterExecuteContext(); + + await rewardModule.beforeTransactionsExecute(blockExecuteContext); + + expect(blockExecuteContext.contextStore.get(CONTEXT_STORE_KEY_DYNAMIC_BLOCK_REWARD)).toEqual( + defaultReward / BigInt(4), + ); + expect( + blockExecuteContext.contextStore.get(CONTEXT_STORE_KEY_DYNAMIC_BLOCK_REDUCTION), + ).toEqual(REWARD_REDUCTION_MAX_PREVOTES); + }); }); describe('afterTransactionsExecute', () => { let blockExecuteContext: BlockAfterExecuteContext; - let stateStore: PrefixedStateReadWriter; - let generatorAddress: Buffer; - let standbyValidatorAddress: Buffer; let contextStore: Map; - const activeValidator = 4; - const minimumReward = - (BigInt(defaultConfig.brackets[0]) * - BigInt(defaultConfig.factorMinimumRewardActiveValidators)) / - DECIMAL_PERCENT_FACTOR; - const totalRewardActiveValidator = BigInt(defaultConfig.brackets[0]) * BigInt(activeValidator); - const ratioReward = totalRewardActiveValidator - minimumReward * BigInt(activeValidator); - const defaultReward = minimumReward + ratioReward / BigInt(5); - beforeEach(async () => { jest.spyOn(rewardModule.events.get(RewardMintedEvent), 'log'); jest.spyOn(tokenMethod, 'userSubstoreExists'); @@ -284,7 +325,7 @@ describe('DynamicRewardModule', () => { standbyValidatorAddress = utils.getRandomBytes(20); contextStore = new Map(); stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); - const blockHeader = createBlockHeaderWithDefaults({ + blockHeader = createBlockHeaderWithDefaults({ height: defaultConfig.offset, generatorAddress, }); @@ -322,50 +363,6 @@ describe('DynamicRewardModule', () => { .mockResolvedValue(true as never); }); - it('should return zero reward with seed reveal reduction when seed reveal is invalid', async () => { - (randomMethod.isSeedRevealValid as jest.Mock).mockResolvedValue(false); - await rewardModule.beforeTransactionsExecute(blockExecuteContext); - await rewardModule.afterTransactionsExecute(blockExecuteContext); - - expect(rewardModule.events.get(RewardMintedEvent).log).toHaveBeenCalledWith( - expect.anything(), - blockExecuteContext.header.generatorAddress, - { amount: BigInt(0), reduction: REWARD_REDUCTION_SEED_REVEAL }, - ); - }); - - it('should return quarter deducted reward when header does not imply max prevotes', async () => { - const blockHeader = createBlockHeaderWithDefaults({ - height: defaultConfig.offset, - impliesMaxPrevotes: false, - generatorAddress, - }); - blockExecuteContext = createBlockContext({ - stateStore, - contextStore, - header: blockHeader, - }).getBlockAfterExecuteContext(); - when(tokenMethod.userSubstoreExists) - .calledWith( - expect.anything(), - blockExecuteContext.header.generatorAddress, - rewardModule['_moduleConfig'].tokenID, - ) - .mockResolvedValue(true as never); - - await rewardModule.beforeTransactionsExecute(blockExecuteContext); - await rewardModule.afterTransactionsExecute(blockExecuteContext); - - expect(rewardModule.events.get(RewardMintedEvent).log).toHaveBeenCalledWith( - expect.anything(), - blockExecuteContext.header.generatorAddress, - { - amount: defaultReward / BigInt(4), - reduction: REWARD_REDUCTION_MAX_PREVOTES, - }, - ); - }); - it('should return full reward when header and assets are valid', async () => { await rewardModule.beforeTransactionsExecute(blockExecuteContext); await rewardModule.afterTransactionsExecute(blockExecuteContext); @@ -377,7 +374,7 @@ describe('DynamicRewardModule', () => { ); }); - it('should mint the token and update shared reward when reward is non zero and user account of geenrator exists for the token id', async () => { + it('should mint the token and update shared reward when reward is non zero and user account of generator exists for the token id', async () => { await rewardModule.beforeTransactionsExecute(blockExecuteContext); await rewardModule.afterTransactionsExecute(blockExecuteContext); @@ -401,7 +398,7 @@ describe('DynamicRewardModule', () => { ); }); - it('should not mint or update shared reward and return zero reward with no account reduction when reward is non zero and user account of geenrator does not exist for the token id', async () => { + it('should not mint or update shared reward and return zero reward with no account reduction when reward is non zero and user account of generator does not exist for the token id', async () => { when(tokenMethod.userSubstoreExists) .calledWith( expect.anything(), @@ -437,9 +434,9 @@ describe('DynamicRewardModule', () => { expect(posMethod.updateSharedRewards).not.toHaveBeenCalled(); }); - it('should store timestamp when end of round', async () => { + it('should store timestamp when it is end of round', async () => { const timestamp = 123456789; - const blockHeader = createBlockHeaderWithDefaults({ + blockHeader = createBlockHeaderWithDefaults({ height: defaultConfig.offset, timestamp, generatorAddress, @@ -461,5 +458,30 @@ describe('DynamicRewardModule', () => { expect(updatedTimestamp).toEqual(timestamp); }); + + it('should store timestamp when it is not end of round', async () => { + const timestamp = 123456789; + blockHeader = createBlockHeaderWithDefaults({ + height: defaultConfig.offset, + timestamp, + generatorAddress, + }); + blockExecuteContext = createBlockContext({ + stateStore, + contextStore, + header: blockHeader, + }).getBlockAfterExecuteContext(); + + (posMethod.isEndOfRound as jest.Mock).mockResolvedValue(false); + + await rewardModule.beforeTransactionsExecute(blockExecuteContext); + await rewardModule.afterTransactionsExecute(blockExecuteContext); + + const { timestamp: updatedTimestamp } = await rewardModule.stores + .get(EndOfRoundTimestampStore) + .get(blockExecuteContext, EMPTY_BYTES); + + expect(updatedTimestamp).not.toEqual(timestamp); + }); }); }); diff --git a/framework/test/unit/modules/interoperability/base_cross_chain_update_command.spec.ts b/framework/test/unit/modules/interoperability/base_cross_chain_update_command.spec.ts index 5017a38fc8a..51d49194290 100644 --- a/framework/test/unit/modules/interoperability/base_cross_chain_update_command.spec.ts +++ b/framework/test/unit/modules/interoperability/base_cross_chain_update_command.spec.ts @@ -94,6 +94,7 @@ describe('BaseCrossChainUpdateCommand', () => { senderPublicKey, signatures: [], }; + const minReturnFeePerByte = BigInt(10000000); const certificate = codec.encode(certificateSchema, { blockID: utils.getRandomBytes(32), @@ -322,6 +323,19 @@ describe('BaseCrossChainUpdateCommand', () => { .set(stateStore, params.sendingChainID, chainAccount); }); + it('should reject when ccu params validation fails', async () => { + const nonBufferSendingChainID = 2; + verifyContext = { + ...verifyContext, + params: { ...params, sendingChainID: nonBufferSendingChainID } as any, + }; + + // 2nd param `isMainchain` could be false + await expect(command['verifyCommon'](verifyContext, false)).rejects.toThrow( + `Property '.sendingChainID' should pass "dataType" keyword validation`, + ); + }); + it('should call validator.validate with crossChainUpdateTransactionParams schema', async () => { jest.spyOn(validator, 'validate'); @@ -1571,6 +1585,7 @@ describe('BaseCrossChainUpdateCommand', () => { describe('bounce', () => { const ccmStatus = CCMStatusCode.MODULE_NOT_SUPPORTED; const ccmProcessedEventCode = CCMProcessedCode.MODULE_NOT_SUPPORTED; + const ccmSize = 100; let stateStore: PrefixedStateReadWriter; beforeEach(async () => { @@ -1592,7 +1607,7 @@ describe('BaseCrossChainUpdateCommand', () => { }); await expect( - command['bounce'](context, 100, ccmStatus, ccmProcessedEventCode), + command['bounce'](context, ccmSize, ccmStatus, ccmProcessedEventCode), ).resolves.toBeUndefined(); expect(context.eventQueue.getEvents()).toHaveLength(1); @@ -1609,17 +1624,18 @@ describe('BaseCrossChainUpdateCommand', () => { }); it('should log event when ccm.fee is less than min fee', async () => { + const minFee = minReturnFeePerByte * BigInt(ccmSize); context = createCrossChainMessageContext({ ccm: { ...defaultCCM, status: CCMStatusCode.OK, - fee: BigInt(1), + fee: minFee - BigInt(1), }, stateStore, }); await expect( - command['bounce'](context, 100, ccmStatus, ccmProcessedEventCode), + command['bounce'](context, ccmSize, ccmStatus, ccmProcessedEventCode), ).resolves.toBeUndefined(); expect(context.eventQueue.getEvents()).toHaveLength(1); @@ -1649,7 +1665,7 @@ describe('BaseCrossChainUpdateCommand', () => { }); await expect( - command['bounce'](context, 100, ccmStatus, ccmProcessedEventCode), + command['bounce'](context, ccmSize, ccmStatus, ccmProcessedEventCode), ).resolves.toBeUndefined(); expect(internalMethod.addToOutbox).toHaveBeenCalledWith( @@ -1685,7 +1701,7 @@ describe('BaseCrossChainUpdateCommand', () => { }); await expect( - command['bounce'](context, 100, ccmStatus, ccmProcessedEventCode), + command['bounce'](context, ccmSize, ccmStatus, ccmProcessedEventCode), ).resolves.toBeUndefined(); expect(internalMethod.addToOutbox).toHaveBeenCalledWith( @@ -1715,7 +1731,7 @@ describe('BaseCrossChainUpdateCommand', () => { }); await expect( - command['bounce'](context, 100, ccmStatus, ccmProcessedEventCode), + command['bounce'](context, ccmSize, ccmStatus, ccmProcessedEventCode), ).resolves.toBeUndefined(); expect(internalMethod.addToOutbox).toHaveBeenCalledWith( @@ -1742,7 +1758,7 @@ describe('BaseCrossChainUpdateCommand', () => { }); await expect( - command['bounce'](context, 100, ccmStatus, ccmProcessedEventCode), + command['bounce'](context, ccmSize, ccmStatus, ccmProcessedEventCode), ).resolves.toBeUndefined(); expect(context.eventQueue.getEvents()).toHaveLength(2); diff --git a/framework/test/unit/modules/interoperability/base_interoperability_module.spec.ts b/framework/test/unit/modules/interoperability/base_interoperability_module.spec.ts index f84794382e7..863f80f6d73 100644 --- a/framework/test/unit/modules/interoperability/base_interoperability_module.spec.ts +++ b/framework/test/unit/modules/interoperability/base_interoperability_module.spec.ts @@ -12,6 +12,7 @@ import { lastCertificate, terminatedOutboxAccount, terminatedStateAccount, + mainchainID, } from './interopFixtures'; import { ActiveValidator, @@ -344,76 +345,74 @@ must NOT have more than ${MAX_NUM_VALIDATORS} items`, ); }); - describe('activeValidators.certificateThreshold', () => { - it(`should throw error if 'totalWeight / BigInt(3) + BigInt(1) > certificateThreshold'`, async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - chainInfos: [ - { - ...chainInfo, - chainValidators: { - activeValidators: [ - { - blsKey: Buffer.from( - '901550cf1fde7dde29218ee82c5196754efea99813af079bb2809a7fad8a053f93726d1e61ccf427118dcc27b0c07d9a', - 'hex', - ), - bftWeight: BigInt(100), - }, - { - // utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH).toString('hex') - blsKey: Buffer.from( - 'c1d3c7919a4ea7e3b5d5b0068513c2cd7fe047a632e13d9238a51fcd6a4afd7ee16906978992a702bccf1f0149fa5d39', - 'hex', - ), - bftWeight: BigInt(200), - }, - ], - // totalWeight / BigInt(3) + BigInt(1) = (100 + 200)/3 + 1 = 101 - // totalWeight / BigInt(3) + BigInt(1) > certificateThreshold - certificateThreshold: BigInt(10), // 101 > 10 - }, + it(`should throw error if 'totalWeight / BigInt(3) + BigInt(1) > certificateThreshold'`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + activeValidators: [ + { + blsKey: Buffer.from( + '901550cf1fde7dde29218ee82c5196754efea99813af079bb2809a7fad8a053f93726d1e61ccf427118dcc27b0c07d9a', + 'hex', + ), + bftWeight: BigInt(100), + }, + { + // utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH).toString('hex') + blsKey: Buffer.from( + 'c1d3c7919a4ea7e3b5d5b0068513c2cd7fe047a632e13d9238a51fcd6a4afd7ee16906978992a702bccf1f0149fa5d39', + 'hex', + ), + bftWeight: BigInt(200), + }, + ], + // totalWeight / BigInt(3) + BigInt(1) = (100 + 200)/3 + 1 = 101 + // totalWeight / BigInt(3) + BigInt(1) > certificateThreshold + certificateThreshold: BigInt(10), // 101 > 10 }, - ], - }, - params, - ); + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `Invalid certificateThreshold input.`, - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `Invalid certificateThreshold input.`, + ); + }); - it(`should throw error if certificateThreshold > totalWeight`, async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - chainInfos: [ - { - ...chainInfo, - chainValidators: { - activeValidators: [ - { - blsKey: Buffer.from( - '901550cf1fde7dde29218ee82c5196754efea99813af079bb2809a7fad8a053f93726d1e61ccf427118dcc27b0c07d9a', - 'hex', - ), - bftWeight: BigInt(10), - }, - ], - certificateThreshold: BigInt(20), - }, + it(`should throw error if certificateThreshold > totalWeight`, async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainValidators: { + activeValidators: [ + { + blsKey: Buffer.from( + '901550cf1fde7dde29218ee82c5196754efea99813af079bb2809a7fad8a053f93726d1e61ccf427118dcc27b0c07d9a', + 'hex', + ), + bftWeight: BigInt(10), + }, + ], + certificateThreshold: BigInt(20), }, - ], - }, - params, - ); + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `Invalid certificateThreshold input.`, - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `Invalid certificateThreshold input.`, + ); }); it(`should throw error if invalid validatorsHash provided`, async () => { @@ -445,6 +444,50 @@ must NOT have more than ${MAX_NUM_VALIDATORS} items`, }); }); + describe('_verifyChainID', () => { + it('should throw error if chainInfo.chainID equals getMainchainID()', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainID: mainchainID, + }, + ], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `chainInfo.chainID must not be equal to ${mainchainID.toString('hex')}.`, + ); + }); + + it('should throw error if chainInfo.chainID[0] !== getMainchainID()[0]', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainID: Buffer.from([1, 0, 0, 0]), + }, + ], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `chainInfo.chainID[0] must be equal to ${mainchainID[0]}.`, + ); + }); + + it('should not throw error when chainID !== mainchainID & chainInfo.chainId[0] == getMainchainID()[0]', async () => { + await expect( + interopMod.initGenesisState(contextWithValidValidatorsHash), + ).resolves.toBeUndefined(); + }); + }); + describe('_verifyTerminatedStateAccountsIDs', () => { certificateThreshold = BigInt(10); const validChainInfos = [ diff --git a/framework/test/unit/modules/interoperability/base_state_recovery.spec.ts b/framework/test/unit/modules/interoperability/base_state_recovery.spec.ts index 069aa5407cb..c6e6d6607e1 100644 --- a/framework/test/unit/modules/interoperability/base_state_recovery.spec.ts +++ b/framework/test/unit/modules/interoperability/base_state_recovery.spec.ts @@ -171,7 +171,9 @@ describe('RecoverStateCommand', () => { const result = await stateRecoveryCommand.verify(commandVerifyContext); expect(result.status).toBe(VerifyStatus.FAIL); - expect(result.error?.message).toInclude('Module is not recoverable.'); + expect(result.error?.message).toInclude( + "Module is not recoverable, as it doesn't have a recover method.", + ); }); it('should return error if recovered store keys are not pairwise distinct', async () => { @@ -180,7 +182,7 @@ describe('RecoverStateCommand', () => { const result = await stateRecoveryCommand.verify(commandVerifyContext); expect(result.status).toBe(VerifyStatus.FAIL); - expect(result.error?.message).toInclude('Recovered store keys are not pairwise distinct.'); + expect(result.error?.message).toInclude('Recoverable store keys are not pairwise distinct.'); }); }); @@ -200,6 +202,12 @@ describe('RecoverStateCommand', () => { expect(invalidSMTVerificationEvent.error).toHaveBeenCalled(); }); + it(`should not throw error if recovery is available for "${moduleName}"`, async () => { + await expect(stateRecoveryCommand.execute(commandExecuteContext)).resolves.not.toThrow( + `Recovery failed for module: ${moduleName}`, + ); + }); + it(`should throw error if recovery not available for "${moduleName}"`, async () => { interoperableCCMethods.delete(moduleName); diff --git a/framework/test/unit/modules/interoperability/internal_method.spec.ts b/framework/test/unit/modules/interoperability/internal_method.spec.ts index f34a442a23a..739c6b3060a 100644 --- a/framework/test/unit/modules/interoperability/internal_method.spec.ts +++ b/framework/test/unit/modules/interoperability/internal_method.spec.ts @@ -611,7 +611,6 @@ describe('Base interoperability internal method', () => { }, ]; - // TODO: I have no idea why `$title` is not working, fix this it.each(testCases)('$title', async ({ changedValues }) => { // Assign const isValueChanged = await interopMod.stores diff --git a/framework/test/unit/modules/interoperability/mainchain/commands/submit_mainchain_cross_chain_update.spec.ts b/framework/test/unit/modules/interoperability/mainchain/commands/submit_mainchain_cross_chain_update.spec.ts index fdf94a2052d..84c3aec1b5e 100644 --- a/framework/test/unit/modules/interoperability/mainchain/commands/submit_mainchain_cross_chain_update.spec.ts +++ b/framework/test/unit/modules/interoperability/mainchain/commands/submit_mainchain_cross_chain_update.spec.ts @@ -414,7 +414,7 @@ describe('SubmitMainchainCrossChainUpdateCommand', () => { }); }); - it('should verify verifyCommon is called', async () => { + it('should check if verifyCommon is called', async () => { jest.spyOn(mainchainCCUUpdateCommand, 'verifyCommon' as any); await expect(mainchainCCUUpdateCommand.verify(verifyContext)).resolves.toEqual({ @@ -424,6 +424,23 @@ describe('SubmitMainchainCrossChainUpdateCommand', () => { expect(mainchainCCUUpdateCommand['verifyCommon']).toHaveBeenCalled(); }); + it('should call isLive with 3 params', async () => { + jest.spyOn(mainchainCCUUpdateCommand['internalMethod'], 'isLive'); + + await expect( + mainchainCCUUpdateCommand.verify({ + ...verifyContext, + params: { ...params } as any, + }), + ).resolves.toEqual({ status: VerifyStatus.OK }); + + expect(mainchainCCUUpdateCommand['internalMethod'].isLive).toHaveBeenCalledWith( + verifyContext, + verifyContext.params.sendingChainID, + verifyContext.header.timestamp, + ); + }); + it(`should not verify liveness condition when sendingChainAccount.status == ${ChainStatus.REGISTERED} and inboxUpdate is empty`, async () => { await expect( mainchainCCUUpdateCommand.verify({ diff --git a/framework/test/unit/modules/interoperability/mainchain/commands/terminate_sidechain_for_liveness.spec.ts b/framework/test/unit/modules/interoperability/mainchain/commands/terminate_sidechain_for_liveness.spec.ts index 4acfc46560a..5dc0146673a 100644 --- a/framework/test/unit/modules/interoperability/mainchain/commands/terminate_sidechain_for_liveness.spec.ts +++ b/framework/test/unit/modules/interoperability/mainchain/commands/terminate_sidechain_for_liveness.spec.ts @@ -15,6 +15,7 @@ import { codec } from '@liskhq/lisk-codec'; import { Transaction } from '@liskhq/lisk-chain'; import { utils } from '@liskhq/lisk-cryptography'; +import { validator } from '@liskhq/lisk-validator'; import { CommandExecuteContext, MainchainInteroperabilityModule } from '../../../../../../src'; import { BaseCCCommand } from '../../../../../../src/modules/interoperability/base_cc_command'; import { BaseCCMethod } from '../../../../../../src/modules/interoperability/base_cc_method'; @@ -35,59 +36,84 @@ import { ChainStatus, } from '../../../../../../src/modules/interoperability/stores/chain_account'; import { TerminateSidechainForLivenessCommand } from '../../../../../../src/modules/interoperability'; +import { CHAIN_ID_LENGTH } from '../../../../../../src/modules/token/constants'; describe('TerminateSidechainForLivenessCommand', () => { const interopMod = new MainchainInteroperabilityModule(); + let livenessTerminationCommand: TerminateSidechainForLivenessCommand; + let commandVerifyContext: CommandVerifyContext; + let interoperableCCMethods: Map; + let ccCommands: Map; + let transaction: Transaction; + let transactionParams: TerminateSidechainForLivenessParams; + let encodedTransactionParams: Buffer; + + beforeEach(() => { + interoperableCCMethods = new Map(); + ccCommands = new Map(); + transactionParams = { + chainID: utils.intToBuffer(3, 4), + }; + encodedTransactionParams = codec.encode( + terminateSidechainForLivenessParamsSchema, + transactionParams, + ); + transaction = new Transaction({ + module: MODULE_NAME_INTEROPERABILITY, + command: COMMAND_NAME_LIVENESS_TERMINATION, + fee: BigInt(100000000), + nonce: BigInt(0), + params: encodedTransactionParams, + senderPublicKey: utils.getRandomBytes(32), + signatures: [], + }); + livenessTerminationCommand = new TerminateSidechainForLivenessCommand( + interopMod.stores, + interopMod.events, + interoperableCCMethods, + ccCommands, + interopMod['internalMethod'], + ); + }); + + describe('verifySchema', () => { + it(`should throw error when chainID is not bytes`, () => { + expect(() => + validator.validate(livenessTerminationCommand.schema, { + chainID: 123, + }), + ).toThrow('Property \'.chainID\' should pass "dataType" keyword validation'); + }); + it(`should throw error when chainID has length less than ${CHAIN_ID_LENGTH}`, () => { + expect(() => + validator.validate(livenessTerminationCommand.schema, { + chainID: Buffer.alloc(CHAIN_ID_LENGTH - 1), + }), + ).toThrow("Property '.chainID' minLength not satisfied"); + }); + it(`should throw error when chainID has length greater than ${CHAIN_ID_LENGTH}`, () => { + expect(() => + validator.validate(livenessTerminationCommand.schema, { + chainID: Buffer.alloc(CHAIN_ID_LENGTH + 1), + }), + ).toThrow("Property '.chainID' maxLength exceeded"); + }); + }); describe('verify', () => { - let livenessTerminationCommand: TerminateSidechainForLivenessCommand; - let commandVerifyContext: CommandVerifyContext; - let interoperableCCMethods: Map; - let ccCommands: Map; - let transaction: Transaction; - let transactionParams: TerminateSidechainForLivenessParams; - let encodedTransactionParams: Buffer; let chainAccount: ChainAccount; beforeEach(async () => { - interoperableCCMethods = new Map(); - ccCommands = new Map(); - - livenessTerminationCommand = new TerminateSidechainForLivenessCommand( - interopMod.stores, - interopMod.events, - interoperableCCMethods, - ccCommands, - interopMod['internalMethod'], - ); - - transactionParams = { - chainID: utils.intToBuffer(3, 4), - }; chainAccount = { lastCertificate: { height: 10, stateRoot: utils.getRandomBytes(32), - timestamp: Date.now(), + timestamp: Math.floor(Date.now() / 1000), validatorsHash: utils.getRandomBytes(32), }, name: 'staleSidechain', status: ChainStatus.ACTIVE, }; - encodedTransactionParams = codec.encode( - terminateSidechainForLivenessParamsSchema, - transactionParams, - ); - - transaction = new Transaction({ - module: MODULE_NAME_INTEROPERABILITY, - command: COMMAND_NAME_LIVENESS_TERMINATION, - fee: BigInt(100000000), - nonce: BigInt(0), - params: encodedTransactionParams, - senderPublicKey: utils.getRandomBytes(32), - signatures: [], - }); commandVerifyContext = createTransactionContext({ transaction, }).createCommandVerifyContext( @@ -139,45 +165,10 @@ describe('TerminateSidechainForLivenessCommand', () => { }); describe('execute', () => { - let livenessTerminationCommand: TerminateSidechainForLivenessCommand; let commandExecuteContext: CommandExecuteContext; - let interoperableCCMethods: Map; - let ccCommands: Map; - let transaction: Transaction; - let transactionParams: TerminateSidechainForLivenessParams; - let encodedTransactionParams: Buffer; let transactionContext: TransactionContext; beforeEach(() => { - interoperableCCMethods = new Map(); - ccCommands = new Map(); - livenessTerminationCommand = new TerminateSidechainForLivenessCommand( - interopMod.stores, - interopMod.events, - interoperableCCMethods, - ccCommands, - interopMod['internalMethod'], - ); - - transactionParams = { - chainID: utils.intToBuffer(3, 4), - }; - - encodedTransactionParams = codec.encode( - terminateSidechainForLivenessParamsSchema, - transactionParams, - ); - - transaction = new Transaction({ - module: MODULE_NAME_INTEROPERABILITY, - command: COMMAND_NAME_LIVENESS_TERMINATION, - fee: BigInt(100000000), - nonce: BigInt(0), - params: encodedTransactionParams, - senderPublicKey: utils.getRandomBytes(32), - signatures: [], - }); - transactionContext = createTransactionContext({ transaction, }); @@ -192,7 +183,7 @@ describe('TerminateSidechainForLivenessCommand', () => { it('should successfully terminate chain', async () => { await livenessTerminationCommand.execute(commandExecuteContext); expect(interopMod['internalMethod'].terminateChainInternal).toHaveBeenCalledWith( - expect.anything(), + commandExecuteContext, transactionParams.chainID, ); }); diff --git a/framework/test/unit/modules/interoperability/mainchain/module.spec.ts b/framework/test/unit/modules/interoperability/mainchain/module.spec.ts index a75e2eb4a5c..84b6d333a21 100644 --- a/framework/test/unit/modules/interoperability/mainchain/module.spec.ts +++ b/framework/test/unit/modules/interoperability/mainchain/module.spec.ts @@ -13,14 +13,23 @@ */ import { utils } from '@liskhq/lisk-cryptography'; +import { codec } from '@liskhq/lisk-codec'; import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; import { HASH_LENGTH, CHAIN_NAME_MAINCHAIN, EMPTY_HASH, + MODULE_NAME_INTEROPERABILITY, } from '../../../../../src/modules/interoperability/constants'; -import { ChainStatus, MainchainInteroperabilityModule } from '../../../../../src'; -import { ChainInfo } from '../../../../../src/modules/interoperability/types'; +import { + ChainStatus, + MainchainInteroperabilityModule, + genesisInteroperabilitySchema, +} from '../../../../../src'; +import { + ChainInfo, + GenesisInteroperability, +} from '../../../../../src/modules/interoperability/types'; import { InMemoryPrefixedStateDB, createGenesisBlockContext, @@ -39,13 +48,13 @@ import { lastCertificate, terminatedStateAccount, terminatedOutboxAccount, - mainchainID, createInitGenesisStateContext, contextWithValidValidatorsHash, getStoreMock, } from '../interopFixtures'; import { RegisteredNamesStore } from '../../../../../src/modules/interoperability/stores/registered_names'; import { InvalidNameError } from '../../../../../src/modules/interoperability/errors'; +import { BaseInteroperabilityModule } from '../../../../../src/modules/interoperability/base_interoperability_module'; describe('initGenesisState', () => { const chainID = Buffer.from([0, 0, 0, 0]); @@ -108,8 +117,21 @@ describe('initGenesisState', () => { ); }); - describe('when chainInfos is empty', () => { - it('should throw error if ownChainNonce !== 0', async () => { + it(`should call _verifyChainInfos from initGenesisState`, async () => { + jest.spyOn(interopMod, '_verifyChainInfos' as any); + + await expect( + interopMod.initGenesisState(contextWithValidValidatorsHash), + ).resolves.toBeUndefined(); + expect(interopMod['_verifyChainInfos']).toHaveBeenCalledTimes(1); + }); + + describe('_verifyChainInfos', () => { + beforeEach(() => { + certificateThreshold = BigInt(10); + }); + + it('should throw error when chainInfos is empty & ownChainNonce !== 0', async () => { const context = createInitGenesisStateContext( { ...genesisInteroperability, @@ -122,14 +144,8 @@ describe('initGenesisState', () => { 'ownChainNonce must be 0 if chainInfos is empty.', ); }); - }); - - describe('when chainInfos is not empty', () => { - beforeEach(() => { - certificateThreshold = BigInt(10); - }); - it('should throw error if ownChainNonce <= 0', async () => { + it('should throw error when chainInfos is not empty & ownChainNonce <= 0', async () => { const context = createInitGenesisStateContext( { ...genesisInteroperability, @@ -177,45 +193,51 @@ describe('initGenesisState', () => { ); }); - describe('chainInfo.chainID', () => { - it('should throw error if chainInfo.chainID equals getMainchainID()', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - chainInfos: [ - { - ...chainInfo, - chainID: mainchainID, - }, - ], - }, - params, - ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `chainID must not be equal to ${mainchainID.toString('hex')}.`, - ); - }); + it("should throw error if not 'the entries chainData.name must be pairwise distinct' ", async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainID: Buffer.from([0, 0, 0, 1]), + }, + { + ...chainInfo, + chainID: Buffer.from([0, 0, 0, 2]), + }, + ], + }, + params, + ); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'chainData.name must be pairwise distinct.', + ); + }); - it('should throw error if chainInfo.chainID[0] !== getMainchainID()[0]', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - chainInfos: [ - { - ...chainInfo, - chainID: Buffer.from([1, 0, 0, 0]), - }, - ], - }, - params, - ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `chainID[0] must be equal to ${mainchainID[0]}.`, - ); - }); + it('should check that _verifyChainID is called from _verifyChainInfos', async () => { + jest.spyOn(interopMod, '_verifyChainID' as any); + + await expect( + interopMod.initGenesisState(contextWithValidValidatorsHash), + ).resolves.toBeUndefined(); + + // must be true to pass this test + expect(interopMod['_verifyChainID']).toHaveBeenCalled(); + }); + + it('should check that _verifyChainData is called from _verifyChainInfos', async () => { + jest.spyOn(interopMod, '_verifyChainData' as any); + + await expect( + interopMod.initGenesisState(contextWithValidValidatorsHash), + ).resolves.toBeUndefined(); + + // must be true to pass this test + expect(interopMod['_verifyChainData']).toHaveBeenCalled(); }); - describe('chainInfo.chainData', () => { + describe('_verifyChainData', () => { it(`should throw error if not 'chainData.lastCertificate.timestamp < g.header.timestamp'`, async () => { const context = createInitGenesisStateContext( { @@ -334,379 +356,600 @@ describe('initGenesisState', () => { }); }); - describe('terminatedStateAccounts', () => { - it('should not throw error if length of terminatedStateAccounts is zero', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: [ - { - ...chainInfo, - chainData: { - ...chainData, - lastCertificate: { - ...lastCertificate, - validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), - }, - }, - chainValidators: { - activeValidators, - certificateThreshold, - }, - }, - ], - terminatedStateAccounts: [], - }, - params, - ); + it('should check that _verifyChannelData is called from _verifyChainInfos', async () => { + jest.spyOn(interopMod, '_verifyChannelData' as any); - await expect(interopMod.initGenesisState(context)).resolves.not.toThrow(); - }); + await expect( + interopMod.initGenesisState(contextWithValidValidatorsHash), + ).resolves.toBeUndefined(); + }); + }); - it('should call _verifyTerminatedStateAccountsIDs', async () => { - jest.spyOn(interopMod, '_verifyTerminatedStateAccountsIDs' as any); + it('should check that _verifyChainValidators is called from _verifyChainInfos', async () => { + jest.spyOn(interopMod, '_verifyChainValidators' as any); - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - chainInfos: [ - { - ...chainInfo, - chainData: { - ...chainData, - status: ChainStatus.TERMINATED, - lastCertificate: { - ...lastCertificate, - validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), - }, - }, - chainValidators: { - activeValidators, - certificateThreshold, + await expect( + interopMod.initGenesisState(contextWithValidValidatorsHash), + ).resolves.toBeUndefined(); + + // must be true to pass this test + expect(interopMod['_verifyChainValidators']).toHaveBeenCalled(); + }); + + it(`should call _verifyTerminatedStateAccounts from initGenesisState`, async () => { + jest.spyOn(interopMod, '_verifyTerminatedStateAccounts' as any); + + await interopMod.initGenesisState(contextWithValidValidatorsHash); + expect(interopMod['_verifyTerminatedStateAccounts']).toHaveBeenCalledTimes(1); + }); + + describe('_verifyTerminatedStateAccounts', () => { + it('should not throw error if length of terminatedStateAccounts is zero', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, }, - ], - terminatedStateAccounts: [ - { - chainID: chainInfo.chainID, - terminatedStateAccount, + chainValidators: { + activeValidators, + certificateThreshold, }, - ], - }, - params, - ); + }, + ], + terminatedStateAccounts: [], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).resolves.toBeUndefined(); - expect(interopMod['_verifyTerminatedStateAccountsIDs']).toHaveBeenCalledTimes(1); - }); + await expect(interopMod.initGenesisState(context)).resolves.not.toThrow(); + }); - it('should throw error if chainInfo.chainID exists in terminatedStateAccounts & chainInfo.chainData.status is ACTIVE', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - chainInfos: [ - { - ...chainInfo, - chainData: { - ...chainData, - status: ChainStatus.ACTIVE, - lastCertificate: { - ...lastCertificate, - validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), - }, - }, - chainValidators: { - activeValidators, - certificateThreshold, + it('should call _verifyTerminatedStateAccountsIDs', async () => { + jest.spyOn(interopMod, '_verifyTerminatedStateAccountsIDs' as any); + + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.TERMINATED, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, }, - ], - terminatedStateAccounts: [ - { - chainID: chainInfo.chainID, - terminatedStateAccount, + chainValidators: { + activeValidators, + certificateThreshold, }, - ], - }, - params, - ); + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state`, - ); - }); + await expect(interopMod.initGenesisState(context)).resolves.toBeUndefined(); + expect(interopMod['_verifyTerminatedStateAccountsIDs']).toHaveBeenCalledTimes(1); + }); - it('should throw error if chainInfo.chainID exists in terminatedStateAccounts & chainInfo.chainData.status is REGISTERED', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: [ - { - ...chainInfo, - chainData: { - ...chainData, - lastCertificate: { - ...lastCertificate, - validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), - }, - }, - chainValidators: { - activeValidators, - certificateThreshold, + it('_verifyChainID the same number of times as size of terminatedStateAccounts + size of chainInfo', async () => { + jest.spyOn(interopMod, '_verifyChainID' as any); + + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.TERMINATED, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, }, - ], - terminatedStateAccounts: [ - { - chainID: chainInfo.chainID, - terminatedStateAccount, + chainValidators: { + activeValidators, + certificateThreshold, }, - ], - }, - params, - ); + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state`, - ); - }); + await expect(interopMod.initGenesisState(context)).resolves.toBeUndefined(); + expect(interopMod['_verifyChainID']).toHaveBeenCalledTimes(2); + }); - it('should throw error if chainID in terminatedStateAccounts does not exist in chainInfo', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: [ - { - ...chainInfo, - chainData: { - ...chainData, - lastCertificate: { - ...lastCertificate, - validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), - }, - }, - chainValidators: { - activeValidators, - certificateThreshold, + it('should throw error if chainInfo.chainID exists in terminatedStateAccounts & chainInfo.chainData.status is ACTIVE', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.ACTIVE, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, }, - ], - terminatedStateAccounts: [ - { - chainID: Buffer.from([0, 0, 0, 2]), - terminatedStateAccount, + chainValidators: { + activeValidators, + certificateThreshold, }, - ], - }, - params, - ); - - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state', - ); - }); - - it('should throw error if some stateAccount in terminatedStateAccounts have stateRoot not equal to chainData.lastCertificate.stateRoot', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: validChainInfos, - terminatedStateAccounts: [ - { - chainID: Buffer.from([0, 0, 0, 1]), - terminatedStateAccount: { - ...terminatedStateAccount, - stateRoot: Buffer.from(utils.getRandomBytes(HASH_LENGTH)), - }, - }, - ], - }, - params, - ); + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - "stateAccount.stateRoot doesn't match chainInfo.chainData.lastCertificate.stateRoot.", - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state`, + ); + }); - it('should throw error if some stateAccount in terminatedStateAccounts have mainchainStateRoot not equal to EMPTY_HASH', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: validChainInfos, - terminatedStateAccounts: [ - { - chainID: Buffer.from([0, 0, 0, 1]), - terminatedStateAccount: { - ...terminatedStateAccount, - mainchainStateRoot: Buffer.from(utils.getRandomBytes(HASH_LENGTH)), + it('should throw error if chainInfo.chainID exists in terminatedStateAccounts & chainInfo.chainData.status is REGISTERED', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, }, - ], - }, - params, - ); + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `stateAccount.mainchainStateRoot is not equal to ${EMPTY_HASH.toString('hex')}.`, - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state`, + ); + }); - it('should throw error if some stateAccount in terminatedStateAccounts is not initialized', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: validChainInfos, - terminatedStateAccounts: [ - { - chainID: Buffer.from([0, 0, 0, 1]), - terminatedStateAccount: { - ...terminatedStateAccount, - initialized: false, + it('should throw error if chainID in terminatedStateAccounts does not exist in chainInfo', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, }, - ], - }, - params, - ); + chainValidators: { + activeValidators, + certificateThreshold, + }, + }, + ], + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedStateAccount, + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'stateAccount is not initialized.', - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state', + ); }); - describe('terminatedOutboxAccounts', () => { - it('should throw error if terminatedOutboxAccounts do not hold unique chainID', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: validChainInfos, - terminatedOutboxAccounts: [ - { - chainID: chainInfo.chainID, - terminatedOutboxAccount, + it('should throw error if some stateAccount in terminatedStateAccounts have stateRoot not equal to chainData.lastCertificate.stateRoot', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: Buffer.from(utils.getRandomBytes(HASH_LENGTH)), }, - { - chainID: chainInfo.chainID, - terminatedOutboxAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + "stateAccount.stateRoot doesn't match chainInfo.chainData.lastCertificate.stateRoot.", + ); + }); + + it('should throw error if some stateAccount in terminatedStateAccounts have mainchainStateRoot not equal to EMPTY_HASH', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount: { + ...terminatedStateAccount, + mainchainStateRoot: Buffer.from(utils.getRandomBytes(HASH_LENGTH)), }, - ], - terminatedStateAccounts: [ - { - chainID: chainInfo.chainID, - terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.mainchainStateRoot is not equal to ${EMPTY_HASH.toString('hex')}.`, + ); + }); + + it('should throw error if some stateAccount in terminatedStateAccounts is not initialized', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount: { + ...terminatedStateAccount, + initialized: false, }, - ], - }, - params, - ); + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'terminatedOutboxAccounts do not hold unique chainID', - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'stateAccount is not initialized.', + ); + }); - it('should throw error if terminatedOutboxAccounts is not ordered lexicographically by chainID', async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: [ - { - ...validChainInfos[0], - chainData: { - ...validChainInfos[0].chainData, - name: 'dummy1', + it("should not throw error if length of terminatedStateAccounts is zero while there doesn't exist some chain in chainData with status TERMINATED", async () => { + certificateThreshold = BigInt(10); + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.ACTIVE, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), }, - chainID: Buffer.from([0, 0, 0, 1]), }, - { - ...validChainInfos[0], - chainData: { - ...validChainInfos[0].chainData, - name: 'dummy2', - }, - chainID: Buffer.from([0, 0, 0, 2]), + chainValidators: { + activeValidators, + certificateThreshold, }, - ], - terminatedOutboxAccounts: [ - { - chainID: Buffer.from([0, 0, 0, 2]), - terminatedOutboxAccount, + }, + ], + terminatedStateAccounts: [], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).resolves.not.toThrow(); + }); + + it('should throw if there is an entry in terminateStateAccounts for a chainID that is ACTIVE in chainInfos', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, + status: ChainStatus.ACTIVE, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, }, - { - chainID: Buffer.from([0, 0, 0, 1]), - terminatedOutboxAccount, + chainValidators: { + activeValidators, + certificateThreshold, }, - ], - terminatedStateAccounts: [ - { - chainID: Buffer.from([0, 0, 0, 1]), - terminatedStateAccount, + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state', + ); + }); + + it('should throw error if chainInfo.chainID exists in terminatedStateAccounts & chainInfo.chainData.status !== CHAIN_STATUS_TERMINATED', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...chainInfo, + chainData: { + ...chainData, // status: ChainStatus.REGISTERED, + lastCertificate: { + ...lastCertificate, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, }, - { - chainID: Buffer.from([0, 0, 0, 2]), - terminatedStateAccount, + chainValidators: { + activeValidators, + certificateThreshold, }, - ], - }, - params, - ); + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - 'terminatedOutboxAccounts must be ordered lexicographically by chainID.', - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'For each terminatedStateAccount there should be a corresponding chainInfo at TERMINATED state', + ); + }); + }); - it("should throw error if terminatedOutboxAccounts don't have a corresponding entry (with chainID == outboxAccount.chainID) in terminatedStateAccounts", async () => { - const context = createInitGenesisStateContext( - { - ...genesisInteroperability, - // this is needed to verify `validatorsHash` related tests (above) - chainInfos: validChainInfos, - terminatedStateAccounts: [ - { - chainID: Buffer.from([0, 0, 0, 1]), - terminatedStateAccount, + it(`should call _verifyTerminatedOutboxAccounts from initGenesisState `, async () => { + jest.spyOn(interopMod, '_verifyTerminatedOutboxAccounts' as any); + + await interopMod.initGenesisState(contextWithValidValidatorsHash); + expect(interopMod['_verifyTerminatedOutboxAccounts']).toHaveBeenCalledTimes(1); + }); + + describe('_verifyTerminatedOutboxAccounts', () => { + it('should throw error if terminatedOutboxAccounts do not hold unique chainID', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedOutboxAccounts: [ + { + chainID: chainInfo.chainID, + terminatedOutboxAccount, + }, + { + chainID: chainInfo.chainID, + terminatedOutboxAccount, + }, + ], + terminatedStateAccounts: [ + { + chainID: chainInfo.chainID, + terminatedStateAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'terminatedOutboxAccounts do not hold unique chainID', + ); + }); + + it('should throw error if terminatedOutboxAccounts is not ordered lexicographically by chainID', async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: [ + { + ...validChainInfos[0], + chainData: { + ...validChainInfos[0].chainData, + name: 'dummy1', }, - ], - terminatedOutboxAccounts: [ - { - chainID: Buffer.from([0, 0, 0, 2]), - terminatedOutboxAccount, + chainID: Buffer.from([0, 0, 0, 1]), + }, + { + ...validChainInfos[0], + chainData: { + ...validChainInfos[0].chainData, + name: 'dummy2', }, - ], - }, - params, - ); + chainID: Buffer.from([0, 0, 0, 2]), + }, + ], + terminatedOutboxAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedOutboxAccount, + }, + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedOutboxAccount, + }, + ], + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount, + }, + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedStateAccount, + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `Each entry outboxAccount in terminatedOutboxAccounts must have a corresponding entry in terminatedStateAccount. outboxAccount with chainID: ${Buffer.from( - [0, 0, 0, 2], - ).toString('hex')} does not exist in terminatedStateAccounts`, - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + 'terminatedOutboxAccounts must be ordered lexicographically by chainID.', + ); }); + + it("should throw error if terminatedOutboxAccounts don't have a corresponding entry (with chainID == outboxAccount.chainID) in terminatedStateAccounts", async () => { + const context = createInitGenesisStateContext( + { + ...genesisInteroperability, + // this is needed to verify `validatorsHash` related tests (above) + chainInfos: validChainInfos, + terminatedStateAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 1]), + terminatedStateAccount, + }, + ], + terminatedOutboxAccounts: [ + { + chainID: Buffer.from([0, 0, 0, 2]), + terminatedOutboxAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `outboxAccount with chainID: ${Buffer.from([0, 0, 0, 2]).toString( + 'hex', + )} must have a corresponding entry in terminatedStateAccounts.`, + ); + }); + }); + + it(`should call processGenesisState from initGenesisState`, async () => { + jest.spyOn(interopMod, 'processGenesisState' as any); + + await expect( + interopMod.initGenesisState(contextWithValidValidatorsHash), + ).resolves.not.toThrow(); + + expect(interopMod['processGenesisState']).toHaveBeenCalledTimes(1); }); describe('processGenesisState', () => { - it('should check that processGenesisState method has been called', async () => { - jest.spyOn(interopMod, 'processGenesisState'); + let registeredNamesStore: RegisteredNamesStore; + + beforeEach(() => { + registeredNamesStore = interopMod.stores.get(RegisteredNamesStore); + }); + + it('should check that super.processGenesisState has been called', async () => { + const spyInstance = jest.spyOn(BaseInteroperabilityModule.prototype, 'processGenesisState'); + await interopMod.initGenesisState(contextWithValidValidatorsHash); + expect(spyInstance).toHaveBeenCalledTimes(1); + }); + + it('should check that all entries are created in registered names substore', async () => { + jest.spyOn(registeredNamesStore, 'set'); await expect( interopMod.initGenesisState(contextWithValidValidatorsHash), ).resolves.not.toThrow(); - expect(interopMod.processGenesisState).toHaveBeenCalled(); - expect(registeredNamesStoreMock.set).toHaveBeenCalledTimes(2); + // let's go with dynamic fixtures, so that if chainInfos length will change inside contextWithValidValidatorsHash, + // we wouldn't have to refactor this part of tests + const genesisInteroperabilityLocal = codec.decode( + genesisInteroperabilitySchema, + contextWithValidValidatorsHash.assets.getAsset(MODULE_NAME_INTEROPERABILITY) as Buffer, // not undefined at this point + ); + + expect(registeredNamesStore.set).toHaveBeenCalledTimes( + 1 + genesisInteroperabilityLocal.chainInfos.length, + ); + + for (const chainInfoLocal of genesisInteroperabilityLocal.chainInfos) { + expect(registeredNamesStore.set).toHaveBeenCalledWith( + contextWithValidValidatorsHash, + Buffer.from(chainInfoLocal.chainData.name, 'ascii'), + { + chainID: chainInfo.chainID, + }, + ); + } + + expect(registeredNamesStore.set).toHaveBeenCalledWith( + contextWithValidValidatorsHash, + Buffer.from(CHAIN_NAME_MAINCHAIN, 'ascii'), + { + chainID: contextWithValidValidatorsHash.chainID, + }, + ); }); }); }); diff --git a/framework/test/unit/modules/interoperability/sidechain/commands/initialize_state_recovery.spec.ts b/framework/test/unit/modules/interoperability/sidechain/commands/initialize_state_recovery.spec.ts index ec3ef4bce05..ee8b5d20264 100644 --- a/framework/test/unit/modules/interoperability/sidechain/commands/initialize_state_recovery.spec.ts +++ b/framework/test/unit/modules/interoperability/sidechain/commands/initialize_state_recovery.spec.ts @@ -55,22 +55,17 @@ import { InvalidSMTVerificationEvent } from '../../../../../../src/modules/inter describe('Sidechain InitializeStateRecoveryCommand', () => { const interopMod = new SidechainInteroperabilityModule(); type StoreMock = Mocked; - const chainAccountStoreMock = { + const getSetHas = () => ({ get: jest.fn(), set: jest.fn(), has: jest.fn(), + }); + const chainAccountStoreMock = { + ...getSetHas(), key: Buffer.from('chainAccount', 'hex'), }; - const ownChainAccountStoreMock = { - get: jest.fn(), - set: jest.fn(), - has: jest.fn(), - }; - const terminatedStateAccountMock = { - get: jest.fn(), - set: jest.fn(), - has: jest.fn(), - }; + const ownChainAccountStoreMock = getSetHas(); + const terminatedStateAccountMock = getSetHas(); let stateRecoveryInitCommand: InitializeStateRecoveryCommand; let commandExecuteContext: CommandExecuteContext; let transaction: Transaction; @@ -85,10 +80,17 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { let commandVerifyContext: CommandVerifyContext; let stateStore: PrefixedStateReadWriter; let mainchainAccount: ChainAccount; + let ownChainAccount: OwnChainAccount; beforeEach(async () => { stateRecoveryInitCommand = interopMod['_stateRecoveryInitCommand']; + ownChainAccount = { + name: 'sidechain', + chainID: utils.intToBuffer(2, 4), + nonce: BigInt('0'), + }; + sidechainChainAccount = { name: 'sidechain1', lastCertificate: { @@ -179,7 +181,6 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { }); describe('verify', () => { - let ownChainAccount: OwnChainAccount; beforeEach(() => { mainchainAccount = { name: 'mainchain', @@ -191,17 +192,9 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { }, status: ChainStatus.ACTIVE, }; - ownChainAccount = { - name: 'sidechain', - chainID: utils.intToBuffer(2, 4), - nonce: BigInt('0'), - }; terminatedStateAccountMock.has.mockResolvedValue(true); ownChainAccountStoreMock.get.mockResolvedValue(ownChainAccount); chainAccountStoreMock.get.mockResolvedValue(mainchainAccount); - interopStoreMock = { - createTerminatedStateAccount: jest.fn(), - }; commandVerifyContext = transactionContext.createCommandVerifyContext( stateRecoveryInitParamsSchema, ); @@ -212,7 +205,15 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { expect(result.status).toBe(VerifyStatus.OK); }); - it('should return error if chain id is same as mainchain id or own chain account id', async () => { + it('should return error if chain id is same as mainchain id', async () => { + commandVerifyContext.params.chainID = getMainchainID(ownChainAccount.chainID); + + await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow( + 'Chain ID is not valid.', + ); + }); + + it('should return error if chain id is same as own chain account id', async () => { commandVerifyContext.params.chainID = ownChainAccount.chainID; await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow( @@ -220,6 +221,25 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { ); }); + it("should not return error if terminated state account doesn't exist", async () => { + await terminatedStateSubstore.del(createStoreGetter(stateStore), transactionParams.chainID); + + await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).resolves.not.toThrow( + 'Sidechain is already terminated.', + ); + }); + + it('should not return error if terminated state account exists but not initialized', async () => { + await terminatedStateSubstore.set(createStoreGetter(stateStore), transactionParams.chainID, { + ...terminatedStateAccount, + initialized: false, + }); + + await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).resolves.not.toThrow( + 'Sidechain is already terminated.', + ); + }); + it('should return error if terminated state account exists and is initialized', async () => { await terminatedStateSubstore.set(createStoreGetter(stateStore), transactionParams.chainID, { ...terminatedStateAccount, @@ -268,7 +288,7 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { ); }); - it('should return error if the sidechain is active on the mainchain and does not violate the liveness requirement', async () => { + it('should return error if the sidechain has ChainStatus.REGISTERED status', async () => { await terminatedStateSubstore.set(createStoreGetter(stateStore), transactionParams.chainID, { ...terminatedStateAccount, initialized: false, @@ -285,7 +305,7 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { timestamp: 100, validatorsHash: utils.getRandomBytes(32), }, - status: ChainStatus.ACTIVE, + status: ChainStatus.REGISTERED, }; sidechainChainAccountEncoded = codec.encode(chainDataSchema, sidechainChainAccount); transactionParams = { @@ -313,11 +333,11 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { ); await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow( - 'Sidechain is still active and obeys the liveness requirement.', + 'Sidechain has status registered.', ); }); - it('should return error if the sidechain has ChainStatus.REGISTERED status', async () => { + it('should return error if the sidechain is active on the mainchain and does not violate the liveness requirement', async () => { await terminatedStateSubstore.set(createStoreGetter(stateStore), transactionParams.chainID, { ...terminatedStateAccount, initialized: false, @@ -334,7 +354,7 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { timestamp: 100, validatorsHash: utils.getRandomBytes(32), }, - status: ChainStatus.REGISTERED, + status: ChainStatus.ACTIVE, }; sidechainChainAccountEncoded = codec.encode(chainDataSchema, sidechainChainAccount); transactionParams = { @@ -362,7 +382,62 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { ); await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).rejects.toThrow( - 'Sidechain has status registered.', + 'Sidechain is still active and obeys the liveness requirement.', + ); + }); + + it('should not return error if the sidechain is active on the mainchain and does violate the liveness requirement', async () => { + await terminatedStateSubstore.set(createStoreGetter(stateStore), transactionParams.chainID, { + ...terminatedStateAccount, + initialized: false, + }); + const mainchainID = getMainchainID(transactionParams.chainID); + when(chainAccountStoreMock.get) + .calledWith(expect.anything(), mainchainID) + .mockResolvedValue({ + ...mainchainAccount, + lastCertificate: { + ...mainchainAccount.lastCertificate, + timestamp: LIVENESS_LIMIT + 50, + }, + } as ChainAccount); + sidechainChainAccount = { + name: 'sidechain1', + lastCertificate: { + height: 10, + stateRoot: utils.getRandomBytes(32), + timestamp: 10, + validatorsHash: utils.getRandomBytes(32), + }, + status: ChainStatus.ACTIVE, + }; + sidechainChainAccountEncoded = codec.encode(chainDataSchema, sidechainChainAccount); + transactionParams = { + chainID: utils.intToBuffer(3, 4), + bitmap: Buffer.alloc(0), + siblingHashes: [], + sidechainAccount: sidechainChainAccountEncoded, + }; + encodedTransactionParams = codec.encode(stateRecoveryInitParamsSchema, transactionParams); + transaction = new Transaction({ + module: MODULE_NAME_INTEROPERABILITY, + command: COMMAND_NAME_STATE_RECOVERY_INIT, + fee: BigInt(100000000), + nonce: BigInt(0), + params: encodedTransactionParams, + senderPublicKey: utils.getRandomBytes(32), + signatures: [], + }); + transactionContext = createTransactionContext({ + transaction, + stateStore, + }); + commandVerifyContext = transactionContext.createCommandVerifyContext( + stateRecoveryInitParamsSchema, + ); + + await expect(stateRecoveryInitCommand.verify(commandVerifyContext)).resolves.not.toThrow( + 'Sidechain is still active and obeys the liveness requirement.', ); }); }); @@ -391,8 +466,9 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { jest.spyOn(SparseMerkleTree.prototype, 'verify').mockResolvedValue(false); jest.spyOn(invalidSMTVerificationEvent, 'error'); + const msg = `given chainID: ${commandExecuteContext.params.chainID.toString('hex')}.`; await expect(stateRecoveryInitCommand.execute(commandExecuteContext)).rejects.toThrow( - 'State recovery initialization proof of inclusion is not valid', + `State recovery initialization proof of inclusion is not valid for ${msg}.`, ); expect(interopStoreMock.createTerminatedStateAccount).not.toHaveBeenCalled(); expect(invalidSMTVerificationEvent.error).toHaveBeenCalled(); @@ -413,15 +489,24 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { transactionParams.chainID, ); + const msg = `mainchainID: ${mainchainID.toString('hex')}`; await expect(stateRecoveryInitCommand.execute(commandExecuteContext)).rejects.toThrow( - 'State recovery initialization proof of inclusion is not valid', + `State recovery initialization proof of inclusion is not valid for ${msg}.`, ); expect(interopStoreMock.createTerminatedStateAccount).not.toHaveBeenCalled(); expect(invalidSMTVerificationEvent.error).toHaveBeenCalled(); }); it('should create a terminated state account when there is none', async () => { - // Arrange & Assign & Act + const mainchainID = getMainchainID(commandExecuteContext.chainID); + + when(chainAccountStoreMock.get) + .calledWith(expect.anything(), mainchainID) + .mockResolvedValue(mainchainAccount); + + jest.spyOn(terminatedStateSubstore, 'has').mockResolvedValue(false); + jest.spyOn(stateRecoveryInitCommand['internalMethod'], 'createTerminatedStateAccount'); + await stateRecoveryInitCommand.execute(commandExecuteContext); const accountFromStore = await terminatedStateSubstore.get( @@ -429,29 +514,44 @@ describe('Sidechain InitializeStateRecoveryCommand', () => { transactionParams.chainID, ); - // Assert expect(accountFromStore).toEqual({ ...terminatedStateAccount, initialized: true }); - expect(interopStoreMock.createTerminatedStateAccount).not.toHaveBeenCalled(); + expect( + stateRecoveryInitCommand['internalMethod'].createTerminatedStateAccount, + ).toHaveBeenCalledTimes(1); }); it('should update the terminated state account when there is one', async () => { - // Arrange & Assign & Act - when(terminatedStateAccountMock.has) - .calledWith(expect.anything(), transactionParams.chainID) - .mockResolvedValue(false); - const terminatedStateStore = interopMod.stores.get(TerminatedStateStore); - terminatedStateStore.get = terminatedStateAccountMock.get; - terminatedStateAccountMock.get.mockResolvedValue(terminatedStateAccount); + + jest.spyOn(stateRecoveryInitCommand['internalMethod'], 'createTerminatedStateAccount'); + jest.spyOn(terminatedStateStore, 'get').mockResolvedValue(terminatedStateAccount); + jest.spyOn(terminatedStateStore, 'set'); + await stateRecoveryInitCommand.execute(commandExecuteContext); + const deserializedSidechainAccount = codec.decode( + chainDataSchema, + commandExecuteContext.params.sidechainAccount, + ); + expect(terminatedStateStore.set).toHaveBeenCalledWith( + commandExecuteContext, + commandExecuteContext.params.chainID, + { + stateRoot: deserializedSidechainAccount.lastCertificate.stateRoot, + mainchainStateRoot: EMPTY_HASH, + initialized: true, + }, + ); + const accountFromStore = await terminatedStateSubstore.get( commandExecuteContext, transactionParams.chainID, ); - - // Assert expect(accountFromStore).toEqual(terminatedStateAccount); + + expect( + stateRecoveryInitCommand['internalMethod'].createTerminatedStateAccount, + ).not.toHaveBeenCalled(); }); }); }); diff --git a/framework/test/unit/modules/interoperability/sidechain/commands/submit_sidechain_cross_chain_update.spec.ts b/framework/test/unit/modules/interoperability/sidechain/commands/submit_sidechain_cross_chain_update.spec.ts index 7f7f60057aa..44fecc3766c 100644 --- a/framework/test/unit/modules/interoperability/sidechain/commands/submit_sidechain_cross_chain_update.spec.ts +++ b/framework/test/unit/modules/interoperability/sidechain/commands/submit_sidechain_cross_chain_update.spec.ts @@ -304,7 +304,7 @@ describe('SubmitSidechainCrossChainUpdateCommand', () => { jest.spyOn(sidechainCCUUpdateCommand['internalMethod'], 'isLive').mockResolvedValue(true); }); - it('should verify verifyCommon is called', async () => { + it('should check if verifyCommon is called', async () => { jest.spyOn(sidechainCCUUpdateCommand, 'verifyCommon' as any); await expect(sidechainCCUUpdateCommand.verify(verifyContext)).resolves.toEqual({ @@ -314,15 +314,6 @@ describe('SubmitSidechainCrossChainUpdateCommand', () => { expect(sidechainCCUUpdateCommand['verifyCommon']).toHaveBeenCalled(); }); - it('should reject when ccu params validation fails', async () => { - await expect( - sidechainCCUUpdateCommand.verify({ - ...verifyContext, - params: { ...params, sendingChainID: 2 } as any, - }), - ).rejects.toThrow('.sendingChainID'); - }); - it('should call isLive with only 2 params', async () => { jest.spyOn(sidechainCCUUpdateCommand['internalMethod'], 'isLive'); @@ -333,15 +324,16 @@ describe('SubmitSidechainCrossChainUpdateCommand', () => { }), ).resolves.toEqual({ status: VerifyStatus.OK }); - expect(sidechainCCUUpdateCommand['internalMethod'].isLive).toHaveBeenCalledWith( + expect(sidechainCCUUpdateCommand['internalMethod'].isLive).not.toHaveBeenCalledWith( verifyContext, verifyContext.params.sendingChainID, + verifyContext.header.timestamp, ); - expect(sidechainCCUUpdateCommand['internalMethod'].isLive).not.toHaveBeenCalledWith( + // should be tested later, otherwise, it can pass even if above fails + expect(sidechainCCUUpdateCommand['internalMethod'].isLive).toHaveBeenCalledWith( verifyContext, verifyContext.params.sendingChainID, - verifyContext.header.timestamp, ); }); }); diff --git a/framework/test/unit/modules/interoperability/sidechain/module.spec.ts b/framework/test/unit/modules/interoperability/sidechain/module.spec.ts index 7d82ac2029a..1207637c682 100644 --- a/framework/test/unit/modules/interoperability/sidechain/module.spec.ts +++ b/framework/test/unit/modules/interoperability/sidechain/module.spec.ts @@ -44,6 +44,7 @@ import { MIN_CHAIN_NAME_LENGTH, } from '../../../../../src/modules/interoperability/constants'; import { InvalidNameError } from '../../../../../src/modules/interoperability/errors'; +import { BaseInteroperabilityModule } from '../../../../../src/modules/interoperability/base_interoperability_module'; describe('initGenesisState', () => { const chainID = Buffer.from([1, 2, 3, 4]); @@ -51,6 +52,52 @@ describe('initGenesisState', () => { let stateStore: PrefixedStateReadWriter; let interopMod: SidechainInteroperabilityModule; + const activeValidators = [ + { + ...activeValidator, + bftWeight: BigInt(300), + }, + ]; + + const defaultData = { + ...genesisInteroperability, + ownChainName: 'dummy', + chainInfos: [ + { + ...chainInfo, + chainID: getMainchainID(chainID), + chainData: { + ...chainData, + name: CHAIN_NAME_MAINCHAIN, + }, + }, + ], + }; + + const certificateThreshold = BigInt(150); + const chainInfosDefault = [ + { + ...defaultData.chainInfos[0], + chainData: { + ...defaultData.chainInfos[0].chainData, + lastCertificate: { + ...lastCertificate, + timestamp: Date.now() / 10000, + validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), + }, + }, + channelData: { + ...defaultData.chainInfos[0].channelData, + messageFeeTokenID: getTokenIDLSK(chainID), + }, + chainValidators: { + ...chainValidators, + activeValidators, + certificateThreshold, + }, + }, + ]; + beforeEach(() => { stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); interopMod = new SidechainInteroperabilityModule(); @@ -60,6 +107,26 @@ describe('initGenesisState', () => { }; }); + it('should check that _verifyChainInfos is called from initGenesisState', async () => { + jest.spyOn(interopMod, '_verifyChainInfos' as any); + + const genesisInteropWithEmptyChainInfos = { + ...genesisInteroperability, + chainInfos: [], + }; + + const context = createInitGenesisStateContext( + { + ...genesisInteropWithEmptyChainInfos, + ownChainName: 'xyz', + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow(); + expect(interopMod['_verifyChainInfos']).toHaveBeenCalledTimes(1); + }); + describe('_verifyChainInfos', () => { describe('when chainInfos is empty', () => { const genesisInteropWithEmptyChainInfos = { @@ -97,7 +164,7 @@ describe('initGenesisState', () => { ); }); - it('should throw error terminatedStateAccounts is not empty', async () => { + it('should throw error when terminatedStateAccounts is not empty', async () => { const context = createInitGenesisStateContext( { ...genesisInteropWithEmptyChainInfos, @@ -117,69 +184,31 @@ describe('initGenesisState', () => { `terminatedStateAccounts must be empty, ${ifChainInfosIsEmpty}.`, ); }); - }); - describe('when chainInfos is not empty', () => { - const defaultData = { - ...genesisInteroperability, - ownChainName: 'dummy', - chainInfos: [ + + it('should throw error when terminatedOutboxAccounts is not empty', async () => { + const context = createInitGenesisStateContext( { - ...chainInfo, - chainID: getMainchainID(chainID), - chainData: { - ...chainData, - name: CHAIN_NAME_MAINCHAIN, - }, + ...genesisInteropWithEmptyChainInfos, + ownChainName: '', + ownChainNonce: BigInt(0), + terminatedOutboxAccounts: [ + { + chainID, + terminatedOutboxAccount, + }, + ], }, - ], - }; - - const activeValidators = [ - { - ...activeValidator, - bftWeight: BigInt(300), - }, - ]; + params, + ); - const certificateThreshold = BigInt(150); - const chainInfosDefault = [ - { - ...defaultData.chainInfos[0], - chainData: { - ...defaultData.chainInfos[0].chainData, - lastCertificate: { - ...lastCertificate, - timestamp: Date.now() / 10000, - validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), - }, - }, - channelData: { - ...defaultData.chainInfos[0].channelData, - messageFeeTokenID: getTokenIDLSK(chainID), - }, - chainValidators: { - ...chainValidators, - activeValidators, - certificateThreshold, - }, - }, - ]; + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `terminatedOutboxAccounts must be empty, ${ifChainInfosIsEmpty}.`, + ); + }); + }); + describe('when chainInfos is not empty', () => { describe('ownChainName', () => { - it(`should throw error if doesn't contain chars from ${validNameChars}`, async () => { - const context = createInitGenesisStateContext( - { - ...defaultData, - ownChainName: 'a%b', - }, - params, - ); - - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - new InvalidNameError('ownChainName').message, - ); - }); - it(`should throw error if doesn't have length between ${MIN_CHAIN_NAME_LENGTH} and ${MAX_CHAIN_NAME_LENGTH}`, async () => { const context1 = createInitGenesisStateContext( { @@ -189,7 +218,7 @@ describe('initGenesisState', () => { params, ); await expect(interopMod.initGenesisState(context1)).rejects.toThrow( - `ownChainName.length must be between ${MIN_CHAIN_NAME_LENGTH} and ${MAX_CHAIN_NAME_LENGTH}`, + `ownChainName.length must be inclusively between ${MIN_CHAIN_NAME_LENGTH} and ${MAX_CHAIN_NAME_LENGTH}.`, ); const context2 = createInitGenesisStateContext( @@ -206,6 +235,20 @@ describe('initGenesisState', () => { ); }); + it(`should throw error if doesn't contain chars from ${validNameChars}`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + ownChainName: 'a%b', + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + new InvalidNameError('ownChainName').message, + ); + }); + it(`should throw error if === ${CHAIN_NAME_MAINCHAIN}`, async () => { const context = createInitGenesisStateContext( { @@ -370,213 +413,214 @@ describe('initGenesisState', () => { }); }); - describe('_verifyTerminatedStateAccounts', () => { - const chainIDNotEqualToOwnChainID = Buffer.from([1, 3, 5, 7]); + it('should call _verifyChannelData & _verifyChainValidators', async () => { + jest.spyOn(interopMod, '_verifyChannelData' as any); + jest.spyOn(interopMod, '_verifyChainValidators' as any); - it(`should throw error if stateAccount.chainID is equal to getMainchainID()`, async () => { - const chainIDDefault = getMainchainID(chainID); - const context = createInitGenesisStateContext( - { - ...defaultData, - chainInfos: [ - { - ...defaultData.chainInfos[0], - chainData: { - ...defaultData.chainInfos[0].chainData, - lastCertificate: { - ...lastCertificate, - timestamp: Date.now() / 10000, - validatorsHash: computeValidatorsHash(activeValidators, certificateThreshold), - }, - }, - channelData: { - ...defaultData.chainInfos[0].channelData, - messageFeeTokenID: getTokenIDLSK(chainID), - }, - chainValidators: { - ...chainValidators, - activeValidators, - certificateThreshold, - }, - }, - ], - terminatedStateAccounts: [ - { - chainID: chainIDDefault, - terminatedStateAccount, - }, - ], - }, - { - ...params, - header: { - timestamp: Date.now(), - } as any, - }, - ); + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + }, + { + ...params, + header: { + timestamp: chainInfosDefault[0].chainData.lastCertificate.timestamp + 1000, + } as any, + }, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `stateAccount.chainID must not be equal to ${chainIDDefault.toString('hex')}.`, - ); - }); + await interopMod.initGenesisState(context); + expect(interopMod['_verifyChannelData']).toHaveBeenCalledTimes(1); + expect(interopMod['_verifyChainValidators']).toHaveBeenCalledTimes(1); + }); + }); + }); - it(`should throw error if not stateAccount.chainId[0] == getMainchainID()[0]`, async () => { - const mainchainID = getMainchainID(params.chainID as Buffer); - const context = createInitGenesisStateContext( + it('should check that _verifyTerminatedStateAccounts is called from initGenesisState', async () => { + jest.spyOn(interopMod, '_verifyTerminatedStateAccounts' as any); + + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID: Buffer.from([1, 1, 2, 3]), + terminatedStateAccount, + }, + ], + }, + params, + ); + + await interopMod.initGenesisState(context); + expect(interopMod['_verifyTerminatedStateAccounts']).toHaveBeenCalledTimes(1); + }); + + describe('_verifyTerminatedStateAccounts', () => { + const chainIDNotEqualToOwnChainID = Buffer.from([1, 3, 5, 7]); + + it('should call _verifyTerminatedStateAccounts', async () => { + jest.spyOn(interopMod, '_verifyTerminatedStateAccounts' as any); + + // const chainIDDefault = getMainchainID(chainID); + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ { - ...defaultData, - chainInfos: chainInfosDefault, - terminatedStateAccounts: [ - { - chainID: Buffer.from([0, 1, 2, 3]), - terminatedStateAccount, - }, - ], + chainID: Buffer.from([1, 1, 2, 3]), + terminatedStateAccount, }, - params, - ); + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `stateAccount.chainID[0] must be equal to ${mainchainID[0]}.`, - ); - }); + await interopMod.initGenesisState(context); + expect(interopMod['_verifyTerminatedStateAccounts']).toHaveBeenCalledTimes(1); + }); - it(`should throw error if stateAccount.chainID is equal to OWN_CHAIN_ID`, async () => { - const context = createInitGenesisStateContext( + it('_verifyChainID the same number of times as size of terminatedStateAccounts', async () => { + jest.spyOn(interopMod, '_verifyChainID' as any); + + // const chainIDDefault = getMainchainID(chainID); + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ { - ...defaultData, - chainInfos: chainInfosDefault, - terminatedStateAccounts: [ - { - chainID: params.chainID as Buffer, - terminatedStateAccount, - }, - ], + chainID: Buffer.from([1, 1, 2, 3]), + terminatedStateAccount, }, - params, - ); + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `stateAccount.chainID must not be equal to OWN_CHAIN_ID.`, - ); - }); + await interopMod.initGenesisState(context); + expect(interopMod['_verifyChainID']).toHaveBeenCalledTimes(1); + }); - it(`should throw error if stateAccount.stateRoot equals EMPTY_HASH, if initialised is true`, async () => { - const context = createInitGenesisStateContext( + it(`should throw error if stateAccount.chainID is equal to OWN_CHAIN_ID`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ { - ...defaultData, - chainInfos: chainInfosDefault, - terminatedStateAccounts: [ - { - chainID: chainIDNotEqualToOwnChainID, - terminatedStateAccount: { - ...terminatedStateAccount, - stateRoot: EMPTY_HASH, - initialized: true, - }, - }, - ], + chainID: params.chainID as Buffer, + terminatedStateAccount, }, - params, - ); + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `stateAccount.stateRoot mst be not equal to "${EMPTY_HASH.toString( - 'hex', - )}", if initialized is true.`, - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.chainID must not be equal to OWN_CHAIN_ID.`, + ); + }); - it(`should throw error if stateAccount.mainchainStateRoot is not equal to EMPTY_HASH, if initialised is true`, async () => { - const context = createInitGenesisStateContext( - { - ...defaultData, - chainInfos: chainInfosDefault, - terminatedStateAccounts: [ - { - chainID: chainIDNotEqualToOwnChainID, - terminatedStateAccount: { - ...terminatedStateAccount, - stateRoot: utils.getRandomBytes(HASH_LENGTH), - mainchainStateRoot: utils.getRandomBytes(HASH_LENGTH), - initialized: true, - }, + describe('when initialised is true', () => { + it(`should throw error if stateAccount.stateRoot equals EMPTY_HASH`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID: chainIDNotEqualToOwnChainID, + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: EMPTY_HASH, + initialized: true, }, - ], - }, - params, - ); + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `terminatedStateAccount.mainchainStateRoot must be equal to "${EMPTY_HASH.toString( - 'hex', - )}", if initialized is true`, - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.stateRoot must not be equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is true.`, + ); + }); - it(`should throw error if stateAccount.stateRoot is not equal to EMPTY_HASH, if initialised is false`, async () => { - const context = createInitGenesisStateContext( - { - ...defaultData, - chainInfos: chainInfosDefault, - terminatedStateAccounts: [ - { - chainID: chainIDNotEqualToOwnChainID, - terminatedStateAccount: { - ...terminatedStateAccount, - stateRoot: utils.getRandomBytes(HASH_LENGTH), - initialized: false, - }, + it(`should throw error if stateAccount.mainchainStateRoot is not equal to EMPTY_HASH`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID: chainIDNotEqualToOwnChainID, + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: utils.getRandomBytes(HASH_LENGTH), + mainchainStateRoot: utils.getRandomBytes(HASH_LENGTH), + initialized: true, }, - ], - }, - params, - ); + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `stateAccount.stateRoot mst be equal to "${EMPTY_HASH.toString( - 'hex', - )}", if initialized is false.`, - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `terminatedStateAccount.mainchainStateRoot must be equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is true`, + ); + }); + }); - it(`should throw error if stateAccount.mainchainStateRoot is equal to EMPTY_HASH, if initialised is false`, async () => { - const context = createInitGenesisStateContext( - { - ...defaultData, - chainInfos: chainInfosDefault, - terminatedStateAccounts: [ - { - chainID: chainIDNotEqualToOwnChainID, - terminatedStateAccount: { - ...terminatedStateAccount, - stateRoot: EMPTY_HASH, - mainchainStateRoot: EMPTY_HASH, - initialized: false, - }, + describe('when initialised is false', () => { + it(`should throw error if stateAccount.stateRoot is not equal to EMPTY_HASH`, async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedStateAccounts: [ + { + chainID: chainIDNotEqualToOwnChainID, + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: utils.getRandomBytes(HASH_LENGTH), + initialized: false, }, - ], - }, - params, - ); + }, + ], + }, + params, + ); - await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `terminatedStateAccount.mainchainStateRoot must be not equal to "${EMPTY_HASH.toString( - 'hex', - )}", if initialized is false.`, - ); - }); + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `stateAccount.stateRoot mst be equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is false.`, + ); }); - it('should throw error if terminatedOutboxAccounts is not empty', async () => { + it(`should throw error if stateAccount.mainchainStateRoot is equal to EMPTY_HASH`, async () => { const context = createInitGenesisStateContext( { ...defaultData, chainInfos: chainInfosDefault, - terminatedOutboxAccounts: [ + terminatedStateAccounts: [ { - chainID, - terminatedOutboxAccount, + chainID: chainIDNotEqualToOwnChainID, + terminatedStateAccount: { + ...terminatedStateAccount, + stateRoot: EMPTY_HASH, + mainchainStateRoot: EMPTY_HASH, + initialized: false, + }, }, ], }, @@ -584,9 +628,46 @@ describe('initGenesisState', () => { ); await expect(interopMod.initGenesisState(context)).rejects.toThrow( - `terminatedOutboxAccounts must be empty.`, + `terminatedStateAccount.mainchainStateRoot must not be equal to "${EMPTY_HASH.toString( + 'hex', + )}", if initialized is false.`, ); }); }); }); + + it('should throw error if terminatedOutboxAccounts is not empty', async () => { + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedOutboxAccounts: [ + { + chainID, + terminatedOutboxAccount, + }, + ], + }, + params, + ); + + await expect(interopMod.initGenesisState(context)).rejects.toThrow( + `terminatedOutboxAccounts must be empty.`, + ); + }); + + it('should check that super.processGenesisState has been called from initGenesisState', async () => { + const spyInstance = jest.spyOn(BaseInteroperabilityModule.prototype, 'processGenesisState'); + + const context = createInitGenesisStateContext( + { + ...defaultData, + chainInfos: chainInfosDefault, + terminatedOutboxAccounts: [], + }, + params, + ); + await interopMod.initGenesisState(context); + expect(spyInstance).toHaveBeenCalledTimes(1); + }); }); diff --git a/framework/test/unit/modules/nft/cc_comands/cc_transfer.spec.ts b/framework/test/unit/modules/nft/cc_comands/cc_transfer.spec.ts new file mode 100644 index 00000000000..c422e16e890 --- /dev/null +++ b/framework/test/unit/modules/nft/cc_comands/cc_transfer.spec.ts @@ -0,0 +1,787 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec } from '@liskhq/lisk-codec'; +import { utils } from '@liskhq/lisk-cryptography'; +import { NFTModule } from '../../../../../src/modules/nft/module'; +import { InMemoryPrefixedStateDB } from '../../../../../src/testing'; +import { + ALL_SUPPORTED_NFTS_KEY, + CCM_STATUS_CODE_OK, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + FEE_CREATE_NFT, + LENGTH_CHAIN_ID, + LENGTH_NFT_ID, + NftEventResult, +} from '../../../../../src/modules/nft/constants'; +import { NFTStore } from '../../../../../src/modules/nft/stores/nft'; +import { CCMsg, CrossChainMessageContext, ccuParamsSchema } from '../../../../../src'; +import { InternalMethod } from '../../../../../src/modules/nft/internal_method'; +import { NFTMethod } from '../../../../../src/modules/nft/method'; +import { EventQueue, MethodContext, createMethodContext } from '../../../../../src/state_machine'; +import { CrossChainTransferCommand } from '../../../../../src/modules/nft/cc_commands/cc_transfer'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { crossChainNFTTransferMessageParamsSchema } from '../../../../../src/modules/nft/schemas'; +import { + CCM_STATUS_OK, + CCM_STATUS_PROTOCOL_VIOLATION, +} from '../../../../../src/modules/token/constants'; +import { fakeLogger } from '../../../../utils/mocks/logger'; +import { CcmTransferEvent } from '../../../../../src/modules/nft/events/ccm_transfer'; +import { EscrowStore } from '../../../../../src/modules/nft/stores/escrow'; +import { UserStore } from '../../../../../src/modules/nft/stores/user'; +import { SupportedNFTsStore } from '../../../../../src/modules/nft/stores/supported_nfts'; +import { CCMStatusCode } from '../../../../../src/modules/interoperability/constants'; + +describe('CrossChain Transfer Command', () => { + const module = new NFTModule(); + const method = new NFTMethod(module.stores, module.events); + const internalMethod = new InternalMethod(module.stores, module.events); + const feeMethod = { payFee: jest.fn() }; + const tokenMethod = { + getAvailableBalance: jest.fn(), + }; + const checkEventResult = ( + eventQueue: EventQueue, + length: number, + EventClass: any, + index: number, + expectedResult: any, + result: any = 0, + ) => { + expect(eventQueue.getEvents()).toHaveLength(length); + expect(eventQueue.getEvents()[index].toObject().name).toEqual(new EventClass('nft').name); + + const eventData = codec.decode>( + new EventClass('nft').schema, + eventQueue.getEvents()[index].toObject().data, + ); + + expect(eventData).toEqual({ ...expectedResult, result }); + }; + const defaultAddress = utils.getRandomBytes(20); + const sendingChainID = Buffer.from([1, 1, 1, 1]); + const receivingChainID = Buffer.from([0, 0, 0, 1]); + const senderAddress = utils.getRandomBytes(20); + const recipientAddress = utils.getRandomBytes(20); + const attributesArray = [{ module: 'pos', attributes: Buffer.alloc(5) }]; + const getStore = (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix); + const getMethodContext = () => methodContext; + const eventQueue = new EventQueue(0); + const contextStore = new Map(); + const nftID = Buffer.alloc(LENGTH_NFT_ID, 1); + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const ownChainID = Buffer.alloc(LENGTH_CHAIN_ID, 1); + const config = { + ownChainID, + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + const interopMethod = { + send: jest.fn(), + error: jest.fn(), + terminateChain: jest.fn(), + getMessageFeeTokenID: jest.fn(), + }; + const defaultHeader = { + height: 0, + timestamp: 0, + }; + const defaultEncodedCCUParams = codec.encode(ccuParamsSchema, { + activeValidatorsUpdate: { + blsKeysUpdate: [], + bftWeightsUpdate: [], + bftWeightsUpdateBitmap: Buffer.alloc(0), + }, + certificate: Buffer.alloc(1), + certificateThreshold: BigInt(1), + inboxUpdate: { + crossChainMessages: [], + messageWitnessHashes: [], + outboxRootWitness: { + bitmap: Buffer.alloc(1), + siblingHashes: [], + }, + }, + sendingChainID: Buffer.from('04000001', 'hex'), + }); + const defaultTransaction = { + senderAddress: defaultAddress, + fee: BigInt(0), + params: defaultEncodedCCUParams, + }; + let params: Buffer; + let ccm: CCMsg; + let command: CrossChainTransferCommand; + let methodContext: MethodContext; + let stateStore: PrefixedStateReadWriter; + let context: CrossChainMessageContext; + let nftStore: NFTStore; + let escrowStore: EscrowStore; + let userStore: UserStore; + + beforeEach(async () => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + method.addDependencies(internalMethod, feeMethod); + method.init(config); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(config); + command = new CrossChainTransferCommand(module.stores, module.events); + command.init({ method, internalMethod, feeMethod }); + methodContext = createMethodContext({ + stateStore, + eventQueue: new EventQueue(0), + contextStore: new Map(), + }); + nftStore = module.stores.get(NFTStore); + await nftStore.save(methodContext, nftID, { + owner: sendingChainID, + attributesArray: [], + }); + params = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID, + senderAddress, + recipientAddress, + attributesArray, + data: '', + }); + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCM_STATUS_OK, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: ownChainID, + }; + }); + + describe('verify', () => { + it('should resolve if verification succeeds', async () => { + await expect(command.verify(context)).resolves.toBeUndefined(); + }); + + it('throw for invalid ccm status', async () => { + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: 72, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue, + getStore, + logger: fakeLogger, + chainID, + }; + + await expect(command.verify(context)).rejects.toThrow('Invalid CCM error code'); + }); + + it('throw if nft chain id is equal to neither own chain id or sending chain id', async () => { + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(newConfig); + params = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID: Buffer.alloc(LENGTH_NFT_ID, 1), + senderAddress, + recipientAddress, + attributesArray, + data: '', + }); + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + receivingChainID, + fee: BigInt(30000), + status: CCM_STATUS_OK, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue, + getStore, + logger: fakeLogger, + chainID: newConfig.ownChainID, + }; + + await expect(command.verify(context)).rejects.toThrow( + 'NFT is not native to either the sending chain or the receiving chain', + ); + }); + + it('should throw if nft chain id equals own chain id but no entry exists in nft substore for the nft id', async () => { + await nftStore.del(methodContext, nftID); + + await expect(command.verify(context)).rejects.toThrow( + 'Non-existent entry in the NFT substore', + ); + }); + + it('should throw if nft chain id equals own chain id but the owner of nft is different from the sending chain', async () => { + await nftStore.del(methodContext, nftID); + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray: [], + }); + + await expect(command.verify(context)).rejects.toThrow('NFT has not been properly escrowed'); + }); + + it('throw if nft chain id is not equal to own chain id and ccm status code is CCMStatusCode.MODULE_NOT_SUPPORTED', async () => { + const newCcm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCMStatusCode.MODULE_NOT_SUPPORTED, + params, + }; + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(newConfig); + context = { + ccm: newCcm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: newConfig.ownChainID, + }; + await nftStore.del(methodContext, nftID); + + await expect(command.verify(context)).rejects.toThrow( + 'Module or cross-chain command not supported', + ); + }); + + it('throw if nft chain id is not equal to own chain id and ccm status code is CCMStatusCode.CROSS_CHAIN_COMMAND_NOT_SUPPORTED', async () => { + const newCcm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCMStatusCode.CROSS_CHAIN_COMMAND_NOT_SUPPORTED, + params, + }; + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(newConfig); + context = { + ccm: newCcm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: newConfig.ownChainID, + }; + await nftStore.del(methodContext, nftID); + + await expect(command.verify(context)).rejects.toThrow( + 'Module or cross-chain command not supported', + ); + }); + + it('throw if nft chain id is not equal to own chain id and entry already exists in nft substore for the nft id', async () => { + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(newConfig); + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: newConfig.ownChainID, + }; + + await expect(command.verify(context)).rejects.toThrow('NFT substore entry already exists'); + }); + + it('should not throw if nft chain id is not equal to own chain id and no entry exists in nft substore for the nft id', async () => { + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(newConfig); + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: newConfig.ownChainID, + }; + await nftStore.del(methodContext, nftID); + + await expect(command.verify(context)).resolves.toBeUndefined(); + }); + }); + + describe('execute', () => { + beforeEach(async () => { + userStore = module.stores.get(UserStore); + escrowStore = module.stores.get(EscrowStore); + await escrowStore.set(methodContext, escrowStore.getKey(sendingChainID, nftID), {}); + }); + + it('should throw if validation fails', async () => { + params = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID: Buffer.alloc(LENGTH_NFT_ID, 1), + senderAddress: utils.getRandomBytes(32), + recipientAddress, + attributesArray, + data: '', + }); + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCM_STATUS_OK, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue, + getStore, + logger: fakeLogger, + chainID, + }; + + await expect(command.execute(context)).rejects.toThrow( + `Property '.senderAddress' address length invalid`, + ); + }); + + it('should throw if fail to decode the CCM', async () => { + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCM_STATUS_OK, + params: Buffer.from(''), + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue, + getStore, + logger: fakeLogger, + chainID, + }; + + await expect(command.execute(context)).rejects.toThrow( + 'Message does not contain a property for fieldNumber: 1.', + ); + }); + + it('should set appropriate values to stores and emit appropriate successful ccm transfer event for nft chain id equals own chain id and ccm status code ok', async () => { + await expect(command.execute(context)).resolves.toBeUndefined(); + const nftStoreData = await nftStore.get(methodContext, nftID); + const userAccountExists = await userStore.has( + methodContext, + userStore.getKey(recipientAddress, nftID), + ); + const escrowAccountExists = await escrowStore.has( + methodContext, + escrowStore.getKey(sendingChainID, nftID), + ); + expect(nftStoreData.owner).toStrictEqual(recipientAddress); + expect(nftStoreData.attributesArray).toEqual([]); + expect(userAccountExists).toBe(true); + expect(escrowAccountExists).toBe(false); + checkEventResult(context.eventQueue, 1, CcmTransferEvent, 0, { + senderAddress, + recipientAddress, + nftID, + receivingChainID: ccm.receivingChainID, + sendingChainID: ccm.sendingChainID, + }); + }); + + it('should set appropriate values to stores and emit appropriate successful ccm transfer event for nft chain id equals own chain id but not ccm status code ok', async () => { + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCM_STATUS_PROTOCOL_VIOLATION, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: ownChainID, + }; + + await expect(command.execute(context)).resolves.toBeUndefined(); + const nftStoreData = await nftStore.get(methodContext, nftID); + const userAccountExistsForRecipient = await userStore.has( + methodContext, + userStore.getKey(recipientAddress, nftID), + ); + const userAccountExistsForSender = await userStore.has( + methodContext, + userStore.getKey(senderAddress, nftID), + ); + const escrowAccountExists = await escrowStore.has( + methodContext, + escrowStore.getKey(sendingChainID, nftID), + ); + expect(nftStoreData.owner).toStrictEqual(senderAddress); + expect(nftStoreData.attributesArray).toEqual([]); + expect(userAccountExistsForRecipient).toBe(false); + expect(userAccountExistsForSender).toBe(true); + expect(escrowAccountExists).toBe(false); + checkEventResult(context.eventQueue, 1, CcmTransferEvent, 0, { + senderAddress, + recipientAddress: senderAddress, + nftID, + receivingChainID: ccm.receivingChainID, + sendingChainID: ccm.sendingChainID, + }); + }); + + it('should reject and emit unsuccessful ccm transfer event if nft chain id does not equal own chain id and nft is not supported', async () => { + const newNftID = utils.getRandomBytes(LENGTH_NFT_ID); + await nftStore.save(methodContext, newNftID, { + owner: sendingChainID, + attributesArray: [], + }); + params = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID: newNftID, + senderAddress, + recipientAddress, + attributesArray, + data: '', + }); + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCM_STATUS_OK, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID, + }; + + await expect(command.execute(context)).rejects.toThrow('Non-supported NFT'); + checkEventResult( + context.eventQueue, + 1, + CcmTransferEvent, + 0, + { + senderAddress, + recipientAddress, + nftID: newNftID, + receivingChainID: ccm.receivingChainID, + sendingChainID: ccm.sendingChainID, + }, + NftEventResult.RESULT_NFT_NOT_SUPPORTED, + ); + }); + + it('should set appropriate values to stores and emit appropriate successful ccm transfer event if nft chain id does not equal own chain id but nft is supported and ccm status code ok', async () => { + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(newConfig); + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: newConfig.ownChainID, + }; + const supportedNFTsStore = module.stores.get(SupportedNFTsStore); + await supportedNFTsStore.set(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + jest.spyOn(feeMethod, 'payFee'); + + await expect(command.execute(context)).resolves.toBeUndefined(); + const nftStoreData = await nftStore.get(methodContext, nftID); + const userAccountExists = await userStore.has( + methodContext, + userStore.getKey(recipientAddress, nftID), + ); + expect(feeMethod.payFee).toHaveBeenCalledWith(methodContext, BigInt(FEE_CREATE_NFT)); + expect(nftStoreData.owner).toStrictEqual(recipientAddress); + expect(nftStoreData.attributesArray).toEqual(attributesArray); + expect(userAccountExists).toBe(true); + checkEventResult(context.eventQueue, 1, CcmTransferEvent, 0, { + senderAddress, + recipientAddress, + nftID, + receivingChainID: ccm.receivingChainID, + sendingChainID: ccm.sendingChainID, + }); + }); + + it('should set appropriate values to stores and emit appropriate successful ccm transfer event if nft chain id does not equal own chain id but nft is supported and not ccm status code ok', async () => { + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(newConfig); + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCM_STATUS_PROTOCOL_VIOLATION, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID, + }; + const supportedNFTsStore = module.stores.get(SupportedNFTsStore); + await supportedNFTsStore.set(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + jest.spyOn(feeMethod, 'payFee'); + + await expect(command.execute(context)).resolves.toBeUndefined(); + const nftStoreData = await nftStore.get(methodContext, nftID); + const userAccountExistsForRecipient = await userStore.has( + methodContext, + userStore.getKey(recipientAddress, nftID), + ); + const userAccountExistsForSender = await userStore.has( + methodContext, + userStore.getKey(senderAddress, nftID), + ); + expect(feeMethod.payFee).toHaveBeenCalledWith(methodContext, BigInt(FEE_CREATE_NFT)); + expect(nftStoreData.owner).toStrictEqual(senderAddress); + expect(nftStoreData.attributesArray).toEqual(attributesArray); + expect(userAccountExistsForRecipient).toBe(false); + expect(userAccountExistsForSender).toBe(true); + checkEventResult(context.eventQueue, 1, CcmTransferEvent, 0, { + senderAddress, + recipientAddress: senderAddress, + nftID, + receivingChainID: ccm.receivingChainID, + sendingChainID: ccm.sendingChainID, + }); + }); + + it('should throw if duplicate module attributes are found when a foreign NFT is received - status === CCM_STATUS_CODE_OK', async () => { + params = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID, + senderAddress, + recipientAddress, + attributesArray: [ + { module: 'module1', attributes: Buffer.alloc(5) }, + { module: 'module1', attributes: Buffer.alloc(5) }, + ], + data: '', + }); + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCM_STATUS_CODE_OK, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID, + }; + + await expect(command.execute(context)).rejects.toThrow('Invalid attributes array provided'); + }); + }); + + it('should throw if duplicate module attributes are found when a foreign NFT is bounced - status !== CCM_STATUS_CODE_OK', async () => { + params = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID, + senderAddress, + recipientAddress, + attributesArray: [ + { module: 'module1', attributes: Buffer.alloc(5) }, + { module: 'module1', attributes: Buffer.alloc(5) }, + ], + data: '', + }); + ccm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: 12345, + params, + }; + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID, + }; + + await expect(command.execute(context)).rejects.toThrow('Invalid attributes array provided'); + }); +}); diff --git a/framework/test/unit/modules/nft/commands/transfer.spec.ts b/framework/test/unit/modules/nft/commands/transfer.spec.ts new file mode 100644 index 00000000000..12ea04e93e8 --- /dev/null +++ b/framework/test/unit/modules/nft/commands/transfer.spec.ts @@ -0,0 +1,255 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { Transaction } from '@liskhq/lisk-chain'; +import { codec } from '@liskhq/lisk-codec'; +import { utils, address } from '@liskhq/lisk-cryptography'; +import { NFTModule } from '../../../../../src/modules/nft/module'; +import { TransferCommand, TransferParams } from '../../../../../src/modules/nft/commands/transfer'; +import { createTransactionContext } from '../../../../../src/testing'; +import { transferParamsSchema } from '../../../../../src/modules/nft/schemas'; +import { + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + LENGTH_NFT_ID, + NFT_NOT_LOCKED, +} from '../../../../../src/modules/nft/constants'; +import { NFTAttributes, NFTStore } from '../../../../../src/modules/nft/stores/nft'; +import { createStoreGetter } from '../../../../../src/testing/utils'; +import { NFTMethod, VerifyStatus } from '../../../../../src'; +import { InternalMethod } from '../../../../../src/modules/nft/internal_method'; +import { UserStore } from '../../../../../src/modules/nft/stores/user'; +import { EventQueue } from '../../../../../src/state_machine'; +import { TransferEvent } from '../../../../../src/modules/nft/events/transfer'; +import { InteroperabilityMethod, TokenMethod } from '../../../../../src/modules/nft/types'; + +describe('Transfer command', () => { + const module = new NFTModule(); + const nftMethod = new NFTMethod(module.stores, module.events); + let interoperabilityMethod!: InteroperabilityMethod; + let tokenMethod!: TokenMethod; + const internalMethod = new InternalMethod(module.stores, module.events); + internalMethod.addDependencies(nftMethod, interoperabilityMethod, tokenMethod); + + let command: TransferCommand; + + const validParams: TransferParams = { + nftID: Buffer.alloc(LENGTH_NFT_ID, 1), + recipientAddress: utils.getRandomBytes(20), + data: '', + }; + + const checkEventResult = ( + eventQueue: EventQueue, + length: number, + EventClass: any, + index: number, + expectedResult: any, + result: any = 0, + ) => { + expect(eventQueue.getEvents()).toHaveLength(length); + expect(eventQueue.getEvents()[index].toObject().name).toEqual(new EventClass('nft').name); + + const eventData = codec.decode>( + new EventClass('nft').schema, + eventQueue.getEvents()[index].toObject().data, + ); + + expect(eventData).toEqual({ ...expectedResult, result }); + }; + + const createTransactionContextWithOverridingParams = ( + params: Record, + txParams: Record = {}, + ) => + createTransactionContext({ + transaction: new Transaction({ + module: module.name, + command: 'transfer', + fee: BigInt(5000000), + nonce: BigInt(0), + senderPublicKey, + params: codec.encode(transferParamsSchema, { + ...validParams, + ...params, + }), + signatures: [utils.getRandomBytes(64)], + ...txParams, + }), + }); + + const nftStore = module.stores.get(NFTStore); + const userStore = module.stores.get(UserStore); + + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const senderPublicKey = utils.getRandomBytes(32); + const owner = address.getAddressFromPublicKey(senderPublicKey); + + beforeEach(() => { + command = new TransferCommand(module.stores, module.events); + command.init({ internalMethod }); + }); + + describe('verify', () => { + it('should fail if nftID does not exist', async () => { + const nftIDNotExistingContext = createTransactionContextWithOverridingParams({ + nftID: Buffer.alloc(LENGTH_NFT_ID, 0), + }); + + const nftIDNotExistingVerification = await command.verify( + nftIDNotExistingContext.createCommandVerifyContext(transferParamsSchema), + ); + expect(nftIDNotExistingVerification.status).toBe(VerifyStatus.FAIL); + expect(nftIDNotExistingVerification.error?.message).toBe('NFT does not exist'); + }); + + it('should fail if NFT is escrowed to another chain', async () => { + const nftEscrowedContext = createTransactionContextWithOverridingParams({ + nftID, + }); + + await nftStore.save(createStoreGetter(nftEscrowedContext.stateStore), nftID, { + owner: chainID, + attributesArray: [], + }); + + const nftEscrowedVerification = await command.verify( + nftEscrowedContext.createCommandVerifyContext(transferParamsSchema), + ); + expect(nftEscrowedVerification.status).toBe(VerifyStatus.FAIL); + expect(nftEscrowedVerification.error?.message).toBe('NFT is escrowed to another chain'); + }); + + it('should fail if owner of the NFT is not the sender', async () => { + const nftIncorrectOwnerContext = createTransactionContextWithOverridingParams({ + nftID, + }); + const newOwner = utils.getRandomBytes(LENGTH_ADDRESS); + + await nftStore.save(createStoreGetter(nftIncorrectOwnerContext.stateStore), nftID, { + owner: newOwner, + attributesArray: [], + }); + await userStore.set( + createStoreGetter(nftIncorrectOwnerContext.stateStore), + userStore.getKey(newOwner, nftID), + { + lockingModule: 'token', + }, + ); + + const nftIncorrectOwnerVerification = await command.verify( + nftIncorrectOwnerContext.createCommandVerifyContext(transferParamsSchema), + ); + expect(nftIncorrectOwnerVerification.status).toBe(VerifyStatus.FAIL); + expect(nftIncorrectOwnerVerification.error?.message).toBe( + 'Transfer not initiated by the NFT owner', + ); + }); + + it('should fail if NFT exists and is locked by its owner', async () => { + const lockedNFTContext = createTransactionContextWithOverridingParams( + { nftID }, + { senderPublicKey }, + ); + + await nftStore.save(createStoreGetter(lockedNFTContext.stateStore), nftID, { + owner, + attributesArray: [], + }); + + await userStore.set( + createStoreGetter(lockedNFTContext.stateStore), + userStore.getKey(owner, nftID), + { + lockingModule: 'token', + }, + ); + + const lockedNFTVerification = await command.verify( + lockedNFTContext.createCommandVerifyContext(transferParamsSchema), + ); + expect(lockedNFTVerification.status).toBe(VerifyStatus.FAIL); + expect(lockedNFTVerification.error?.message).toBe('Locked NFTs cannot be transferred'); + }); + + it('should verify if unlocked NFT exists and its owner is performing the transfer', async () => { + const validContext = createTransactionContextWithOverridingParams( + { nftID }, + { senderPublicKey }, + ); + + await nftStore.save(createStoreGetter(validContext.stateStore), nftID, { + owner, + attributesArray: [], + }); + + await userStore.set( + createStoreGetter(validContext.stateStore), + userStore.getKey(owner, nftID), + { + lockingModule: NFT_NOT_LOCKED, + }, + ); + + await expect( + command.verify(validContext.createCommandVerifyContext(transferParamsSchema)), + ).resolves.toEqual({ status: VerifyStatus.OK }); + }); + }); + + describe('execute', () => { + it('should transfer NFT and emit Transfer event', async () => { + const senderAddress = owner; + const recipientAddress = utils.getRandomBytes(LENGTH_ADDRESS); + const attributesArray: NFTAttributes[] = []; + + const validContext = createTransactionContextWithOverridingParams( + { nftID, recipientAddress }, + { senderPublicKey }, + ); + + await nftStore.save(createStoreGetter(validContext.stateStore), nftID, { + owner: senderAddress, + attributesArray, + }); + + await userStore.set( + createStoreGetter(validContext.stateStore), + userStore.getKey(senderAddress, nftID), + { + lockingModule: NFT_NOT_LOCKED, + }, + ); + + await expect( + command.execute(validContext.createCommandExecuteContext(transferParamsSchema)), + ).resolves.toBeUndefined(); + + await expect( + nftStore.get(createStoreGetter(validContext.stateStore), nftID), + ).resolves.toEqual({ + owner: recipientAddress, + attributesArray, + }); + + checkEventResult(validContext.eventQueue, 1, TransferEvent, 0, { + senderAddress, + recipientAddress, + nftID, + }); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/commands/transfer_cross_chain.spec.ts b/framework/test/unit/modules/nft/commands/transfer_cross_chain.spec.ts new file mode 100644 index 00000000000..1256d05a18e --- /dev/null +++ b/framework/test/unit/modules/nft/commands/transfer_cross_chain.spec.ts @@ -0,0 +1,380 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { Transaction } from '@liskhq/lisk-chain'; +import { codec } from '@liskhq/lisk-codec'; +import { address, utils } from '@liskhq/lisk-cryptography'; +import { NFTModule } from '../../../../../src/modules/nft/module'; +import { + TransferCrossChainCommand, + TransferCrossChainParams, +} from '../../../../../src/modules/nft/commands/transfer_cross_chain'; +import { crossChainTransferParamsSchema } from '../../../../../src/modules/nft/schemas'; +import { + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + LENGTH_NFT_ID, + LENGTH_TOKEN_ID, + NFT_NOT_LOCKED, +} from '../../../../../src/modules/nft/constants'; +import { InMemoryPrefixedStateDB } from '../../../../../src/testing/in_memory_prefixed_state'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { EventQueue, VerifyStatus, createMethodContext } from '../../../../../src/state_machine'; +import { TokenMethod } from '../../../../../src'; +import { MethodContext } from '../../../../../src/state_machine/method_context'; +import { NFTStore } from '../../../../../src/modules/nft/stores/nft'; +import { UserStore } from '../../../../../src/modules/nft/stores/user'; +import * as Token from '../../../../../src/modules/token/stores/user'; +import { NFTMethod } from '../../../../../src/modules/nft/method'; +import { InteroperabilityMethod } from '../../../../../src/modules/nft/types'; +import { createTransactionContext } from '../../../../../src/testing'; +import { InternalMethod } from '../../../../../src/modules/nft/internal_method'; +import { + TransferCrossChainEvent, + TransferCrossChainEventData, +} from '../../../../../src/modules/nft/events/transfer_cross_chain'; + +describe('TransferCrossChainComand', () => { + const module = new NFTModule(); + module.stores.register( + Token.UserStore, + new Token.UserStore(module.name, module.stores.keys.length + 1), + ); + + const command = new TransferCrossChainCommand(module.stores, module.events); + const nftMethod = new NFTMethod(module.stores, module.events); + const tokenMethod = new TokenMethod(module.stores, module.events, module.name); + const internalMethod = new InternalMethod(module.stores, module.events); + let interoperabilityMethod!: InteroperabilityMethod; + + const senderPublicKey = utils.getRandomBytes(32); + const owner = address.getAddressFromPublicKey(senderPublicKey); + const ownChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const receivingChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const messageFeeTokenID = utils.getRandomBytes(LENGTH_TOKEN_ID); + const availableBalance = BigInt(1000000); + + const nftStore = module.stores.get(NFTStore); + const userStore = module.stores.get(UserStore); + const tokenUserStore = module.stores.get(Token.UserStore); + + let stateStore!: PrefixedStateReadWriter; + let methodContext!: MethodContext; + + let existingNFT: { nftID: any; owner: any }; + let lockedExistingNFT: { nftID: any; owner: any }; + let escrowedNFT: { nftID: any; owner: any }; + + const validParams: TransferCrossChainParams = { + nftID: Buffer.alloc(LENGTH_NFT_ID), + receivingChainID, + recipientAddress: utils.getRandomBytes(LENGTH_ADDRESS), + data: '', + messageFee: BigInt(100000), + includeAttributes: false, + }; + + const checkEventResult = ( + eventQueue: EventQueue, + length: number, + EventClass: any, + index: number, + expectedResult: EventDataType, + result: any = 0, + ) => { + expect(eventQueue.getEvents()).toHaveLength(length); + expect(eventQueue.getEvents()[index].toObject().name).toEqual(new EventClass('nft').name); + + const eventData = codec.decode>( + new EventClass('nft').schema, + eventQueue.getEvents()[index].toObject().data, + ); + + expect(eventData).toEqual({ ...expectedResult, result }); + }; + + const createTransactionContextWithOverridingParams = ( + params: Record, + txParams: Record = {}, + ) => + createTransactionContext({ + chainID: ownChainID, + stateStore, + transaction: new Transaction({ + module: module.name, + command: 'transfer', + fee: BigInt(5000000), + nonce: BigInt(0), + senderPublicKey, + params: codec.encode(crossChainTransferParamsSchema, { + ...validParams, + ...params, + }), + signatures: [utils.getRandomBytes(64)], + ...txParams, + }), + }); + + beforeEach(async () => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + + methodContext = createMethodContext({ + stateStore, + eventQueue: new EventQueue(0), + contextStore: new Map(), + }); + + interoperabilityMethod = { + send: jest.fn().mockResolvedValue(Promise.resolve()), + error: jest.fn().mockResolvedValue(Promise.resolve()), + terminateChain: jest.fn().mockResolvedValue(Promise.resolve()), + getMessageFeeTokenID: jest.fn().mockResolvedValue(Promise.resolve(messageFeeTokenID)), + }; + + internalMethod.init({ + ownChainID, + }); + + internalMethod.addDependencies(nftMethod, interoperabilityMethod, tokenMethod); + + command.init({ nftMethod, tokenMethod, interoperabilityMethod, internalMethod }); + + existingNFT = { + owner, + nftID: Buffer.concat([ownChainID, utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID)]), + }; + + lockedExistingNFT = { + owner, + nftID: Buffer.concat([ownChainID, utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID)]), + }; + + escrowedNFT = { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + nftID: utils.getRandomBytes(LENGTH_NFT_ID), + }; + + await nftStore.save(methodContext, existingNFT.nftID, { + owner: existingNFT.owner, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(existingNFT.owner, existingNFT.nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await module.stores.get(NFTStore).save(methodContext, lockedExistingNFT.nftID, { + owner: lockedExistingNFT.owner, + attributesArray: [], + }); + + await userStore.set( + methodContext, + userStore.getKey(lockedExistingNFT.owner, lockedExistingNFT.nftID), + { + lockingModule: 'token', + }, + ); + + await module.stores.get(NFTStore).save(methodContext, escrowedNFT.nftID, { + owner: escrowedNFT.owner, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(escrowedNFT.owner, escrowedNFT.nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await tokenUserStore.set(methodContext, tokenUserStore.getKey(owner, messageFeeTokenID), { + availableBalance, + lockedBalances: [], + }); + }); + + describe('verify', () => { + it('should fail if receiving chain id is same as the own chain id', async () => { + const receivingChainIDContext = createTransactionContextWithOverridingParams({ + receivingChainID: ownChainID, + nftID: existingNFT.nftID, + }); + const receivingChainIDVerification = await command.verify( + receivingChainIDContext.createCommandVerifyContext(crossChainTransferParamsSchema), + ); + expect(receivingChainIDVerification.status).toBe(VerifyStatus.FAIL); + expect(receivingChainIDVerification.error?.message).toBe( + 'Receiving chain cannot be the sending chain', + ); + }); + + it('should fail if NFT does not exist', async () => { + const nftIDNotExistingContext = createTransactionContextWithOverridingParams({ + nftID: utils.getRandomBytes(LENGTH_NFT_ID), + }); + + const nftIDNotExistingVerification = await command.verify( + nftIDNotExistingContext.createCommandVerifyContext(crossChainTransferParamsSchema), + ); + expect(nftIDNotExistingVerification.status).toBe(VerifyStatus.FAIL); + expect(nftIDNotExistingVerification.error?.message).toBe('NFT does not exist'); + }); + + it('should fail if NFT is escrowed', async () => { + const nftEscrowedContext = createTransactionContextWithOverridingParams({ + nftID: escrowedNFT.nftID, + receivingChainID: escrowedNFT.nftID.subarray(0, LENGTH_CHAIN_ID), + }); + + const nftEscrowedVerification = await command.verify( + nftEscrowedContext.createCommandVerifyContext(crossChainTransferParamsSchema), + ); + expect(nftEscrowedVerification.status).toBe(VerifyStatus.FAIL); + expect(nftEscrowedVerification.error?.message).toBe('NFT is escrowed to another chain'); + }); + + it('should fail if NFT is not native neither to the sending nor to the receiving chain', async () => { + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + const context = createTransactionContextWithOverridingParams({ nftID }); + + await nftStore.save(methodContext, nftID, { + owner, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(owner, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + const receivingChainIDVerification = await command.verify( + context.createCommandVerifyContext(crossChainTransferParamsSchema), + ); + expect(receivingChainIDVerification.status).toBe(VerifyStatus.FAIL); + expect(receivingChainIDVerification.error?.message).toBe( + 'NFT must be native to either the sending or the receiving chain', + ); + }); + + it('should fail if the owner of the NFT is not the sender', async () => { + const ownerNotSenderContext = createTransactionContextWithOverridingParams({ + nftID: existingNFT.nftID, + }); + + const nft = await nftStore.get(methodContext, existingNFT.nftID); + const newOwner = utils.getRandomBytes(LENGTH_ADDRESS); + nft.owner = newOwner; + await nftStore.save(methodContext, existingNFT.nftID, nft); + await userStore.set(methodContext, userStore.getKey(newOwner, existingNFT.nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + const receivingChainIDVerification = await command.verify( + ownerNotSenderContext.createCommandVerifyContext(crossChainTransferParamsSchema), + ); + expect(receivingChainIDVerification.status).toBe(VerifyStatus.FAIL); + expect(receivingChainIDVerification.error?.message).toBe( + 'Transfer not initiated by the NFT owner', + ); + }); + + it('should fail if NFT is locked', async () => { + const nftLockedContext = createTransactionContextWithOverridingParams({ + nftID: lockedExistingNFT.nftID, + }); + + const receivingChainIDVerification = await command.verify( + nftLockedContext.createCommandVerifyContext(crossChainTransferParamsSchema), + ); + expect(receivingChainIDVerification.status).toBe(VerifyStatus.FAIL); + expect(receivingChainIDVerification.error?.message).toBe('Locked NFTs cannot be transferred'); + }); + + it('should fail if senders has insufficient balance of value messageFee and token messageFeeTokenID', async () => { + const insufficientMessageFeeBalanceContext = createTransactionContextWithOverridingParams({ + messageFeeTokenID, + messageFee: availableBalance + BigInt(1), + nftID: existingNFT.nftID, + }); + + const receivingChainIDVerification = await command.verify( + insufficientMessageFeeBalanceContext.createCommandVerifyContext( + crossChainTransferParamsSchema, + ), + ); + expect(receivingChainIDVerification.status).toBe(VerifyStatus.FAIL); + expect(receivingChainIDVerification.error?.message).toBe( + 'Insufficient balance for the message fee', + ); + }); + + it('should pass verification when NFT is native', async () => { + const context = createTransactionContextWithOverridingParams({ + nftID: existingNFT.nftID, + }); + + await expect( + command.verify(context.createCommandVerifyContext(crossChainTransferParamsSchema)), + ).resolves.toEqual({ status: VerifyStatus.OK }); + }); + + it('should pass verification when NFT is native to receiving chain', async () => { + const nftID = Buffer.concat([ + receivingChainID, + utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID), + ]); + + await nftStore.save(methodContext, nftID, { + owner, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(owner, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + const context = createTransactionContextWithOverridingParams({ + nftID, + }); + + await expect( + command.verify(context.createCommandVerifyContext(crossChainTransferParamsSchema)), + ).resolves.toEqual({ status: VerifyStatus.OK }); + }); + }); + + describe('execute', () => { + it('should transfer NFT and emit TransferCrossChainEvent', async () => { + const context = createTransactionContextWithOverridingParams({ + nftID: existingNFT.nftID, + }); + + await expect( + command.execute(context.createCommandExecuteContext(crossChainTransferParamsSchema)), + ).resolves.toBeUndefined(); + + checkEventResult( + context.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress: owner, + recipientAddress: validParams.recipientAddress, + receivingChainID: validParams.receivingChainID, + nftID: existingNFT.nftID, + includeAttributes: validParams.includeAttributes, + }, + ); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/endpoint.spec.ts b/framework/test/unit/modules/nft/endpoint.spec.ts new file mode 100644 index 00000000000..6bf141958fa --- /dev/null +++ b/framework/test/unit/modules/nft/endpoint.spec.ts @@ -0,0 +1,877 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { validator } from '@liskhq/lisk-validator'; +import { address, utils } from '@liskhq/lisk-cryptography'; +import { NFTEndpoint } from '../../../../src/modules/nft/endpoint'; +import { NFTMethod } from '../../../../src/modules/nft/method'; +import { NFTModule } from '../../../../src/modules/nft/module'; +import { MethodContext } from '../../../../src/state_machine'; +import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; +import { + InMemoryPrefixedStateDB, + createTransientMethodContext, + createTransientModuleEndpointContext, +} from '../../../../src/testing'; +import { NFTStore } from '../../../../src/modules/nft/stores/nft'; +import { UserStore } from '../../../../src/modules/nft/stores/user'; +import { + ALL_SUPPORTED_NFTS_KEY, + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + LENGTH_COLLECTION_ID, + LENGTH_NFT_ID, + NFT_NOT_LOCKED, +} from '../../../../src/modules/nft/constants'; +import { NFT } from '../../../../src/modules/nft/types'; +import { JSONObject } from '../../../../src'; +import { SupportedNFTsStore } from '../../../../src/modules/nft/stores/supported_nfts'; +import { + isCollectionIDSupportedResponseSchema, + getEscrowedNFTIDsResponseSchema, + getNFTResponseSchema, + getNFTsResponseSchema, + hasNFTResponseSchema, + isNFTSupportedResponseSchema, +} from '../../../../src/modules/nft/schemas'; +import { EscrowStore } from '../../../../src/modules/nft/stores/escrow'; + +type NFTofOwner = Omit & { id: Buffer }; + +describe('NFTEndpoint', () => { + const module = new NFTModule(); + const method = new NFTMethod(module.stores, module.events); + const endpoint = new NFTEndpoint(module.stores, module.events); + const ownChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + method.init({ ownChainID }); + + endpoint.addDependencies(method); + + const nftStore = module.stores.get(NFTStore); + const userStore = module.stores.get(UserStore); + const escrowStore = module.stores.get(EscrowStore); + const supportedNFTsStore = module.stores.get(SupportedNFTsStore); + + let stateStore: PrefixedStateReadWriter; + let methodContext: MethodContext; + + const owner = utils.getRandomBytes(LENGTH_ADDRESS); + const ownerAddress = address.getLisk32AddressFromAddress(owner); + const escrowChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + const nfts: NFTofOwner[] = [ + { + id: utils.getRandomBytes(LENGTH_NFT_ID), + attributesArray: [ + { + module: 'pos', + attributes: Buffer.alloc(10, 0), + }, + ], + lockingModule: NFT_NOT_LOCKED, + }, + { + id: utils.getRandomBytes(LENGTH_NFT_ID), + attributesArray: [], + lockingModule: 'pos', + }, + ]; + + beforeEach(() => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + methodContext = createTransientMethodContext({ stateStore }); + }); + + describe('getNFTs', () => { + beforeEach(async () => { + for (const nft of nfts) { + await nftStore.save(methodContext, nft.id, { + owner, + attributesArray: nft.attributesArray, + }); + + await userStore.set(methodContext, userStore.getKey(owner, nft.id), { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + lockingModule: nft.lockingModule!, + }); + } + + await nftStore.save(methodContext, utils.getRandomBytes(LENGTH_NFT_ID), { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + }); + + it('should fail if address does not have valid length', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + address: 'incorrect', + }, + }); + + await expect(endpoint.getNFTs(context)).rejects.toThrow( + `'.address' must match format "lisk32"`, + ); + }); + + it('should return empty NFTs collection if owner has no NFTs', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + address: address.getLisk32AddressFromAddress(utils.getRandomBytes(LENGTH_ADDRESS)), + }, + }); + + await expect(endpoint.getNFTs(context)).resolves.toEqual({ nfts: [] }); + + validator.validate(getNFTsResponseSchema, { nfts: [] }); + }); + + it('should return NFTs for the provided owner lexicograhpically per id', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + address: ownerAddress, + }, + }); + + const expectedNFTs = { + nfts: nfts + .sort((a, b) => a.id.compare(b.id)) + .map(nft => ({ + id: nft.id.toString('hex'), + attributesArray: nft.attributesArray.map(attribute => ({ + module: attribute.module, + attributes: attribute.attributes.toString('hex'), + })), + lockingModule: nft.lockingModule, + })), + }; + + await expect(endpoint.getNFTs(context)).resolves.toEqual(expectedNFTs); + + validator.validate(getNFTsResponseSchema, expectedNFTs); + }); + + it('should return NFT details for escrowed NFT', async () => { + await escrowStore.set(methodContext, escrowChainID, {}); + + await nftStore.save(methodContext, nfts[0].id, { + owner: escrowChainID, + attributesArray: [], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + id: nfts[0].id.toString('hex'), + }, + }); + + const expectedNFT: JSONObject = { + owner: escrowChainID.toString('hex'), + attributesArray: [], + }; + + await expect(endpoint.getNFT(context)).resolves.toEqual(expectedNFT); + + validator.validate(getNFTResponseSchema, expectedNFT); + }); + }); + + describe('hasNFT', () => { + it('should fail if address is not valid', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + address: 'incorrect', + id: utils.getRandomBytes(LENGTH_NFT_ID).toString('hex'), + }, + }); + + await expect(endpoint.hasNFT(context)).rejects.toThrow( + `'.address' must match format "lisk32"`, + ); + }); + + it('should fail if id does not have valid length', async () => { + const minLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + address: ownerAddress, + id: utils.getRandomBytes(LENGTH_NFT_ID - 1).toString('hex'), + }, + }); + + const maxLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + address: ownerAddress, + id: utils.getRandomBytes(LENGTH_NFT_ID + 1).toString('hex'), + }, + }); + + await expect(endpoint.hasNFT(minLengthContext)).rejects.toThrow( + `'.id' must NOT have fewer than 32 characters`, + ); + + await expect(endpoint.hasNFT(maxLengthContext)).rejects.toThrow( + `'.id' must NOT have more than 32 characters`, + ); + }); + + it('should return false if provided NFT does not exist', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + address: ownerAddress, + id: utils.getRandomBytes(LENGTH_NFT_ID).toString('hex'), + }, + }); + + await expect(endpoint.hasNFT(context)).resolves.toEqual({ hasNFT: false }); + + validator.validate(hasNFTResponseSchema, { hasNFT: false }); + }); + + it('should return false if provided NFT is not owned by the provided address', async () => { + await nftStore.save(methodContext, nfts[0].id, { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + address: ownerAddress, + id: nfts[0].id.toString('hex'), + }, + }); + + await expect(endpoint.hasNFT(context)).resolves.toEqual({ hasNFT: false }); + + validator.validate(hasNFTResponseSchema, { hasNFT: false }); + }); + + it('should return true if provided is owned by the provided address', async () => { + await nftStore.save(methodContext, nfts[0].id, { + owner, + attributesArray: [], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + address: ownerAddress, + id: nfts[0].id.toString('hex'), + }, + }); + + await expect(endpoint.hasNFT(context)).resolves.toEqual({ hasNFT: true }); + + validator.validate(hasNFTResponseSchema, { hasNFT: true }); + }); + }); + + describe('getNFT', () => { + it('should fail if id does not have valid length', async () => { + const minLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + id: utils.getRandomBytes(LENGTH_NFT_ID - 1).toString('hex'), + }, + }); + + const maxLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + id: utils.getRandomBytes(LENGTH_NFT_ID + 1).toString('hex'), + }, + }); + + await expect(endpoint.getNFT(minLengthContext)).rejects.toThrow( + `'.id' must NOT have fewer than 32 characters`, + ); + + await expect(endpoint.getNFT(maxLengthContext)).rejects.toThrow( + `'.id' must NOT have more than 32 characters`, + ); + }); + + it('should fail if NFT does not exist', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + id: nfts[0].id.toString('hex'), + }, + }); + + await expect(endpoint.getNFT(context)).rejects.toThrow('NFT substore entry does not exist'); + }); + + it('should return NFT details', async () => { + const attributesArray = [ + { + module: 'pos', + attributes: utils.getRandomBytes(10), + }, + ]; + await nftStore.save(methodContext, nfts[0].id, { + owner, + attributesArray, + }); + + await userStore.set(methodContext, userStore.getKey(owner, nfts[0].id), { + lockingModule: NFT_NOT_LOCKED, + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + id: nfts[0].id.toString('hex'), + }, + }); + + const expectedNFT: JSONObject = { + owner: owner.toString('hex'), + attributesArray: attributesArray.map(attribute => ({ + module: attribute.module, + attributes: attribute.attributes.toString('hex'), + })), + lockingModule: NFT_NOT_LOCKED, + }; + + await expect(endpoint.getNFT(context)).resolves.toEqual(expectedNFT); + + validator.validate(getNFTResponseSchema, expectedNFT); + }); + }); + + describe('getSupportedCollectionIDs', () => { + it('should return a supportedCollectionIDs array as [*] when ALL_SUPPORTED_NFTS_KEY exists in SupportedNFTsStore', async () => { + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + chainID: ownChainID, + }); + + await expect(endpoint.getSupportedCollectionIDs(context)).resolves.toEqual({ + supportedCollectionIDs: ['*'], + }); + }); + + it("should return a supportedCollectionIDs array with ownChainID + 8 [*]s and chainID + 8 [*]s for all chain id's stored in the supportedNFT store when supportedCollectionIDArray is empty", async () => { + const chainID1 = Buffer.from('00000001', 'hex'); + const chainID2 = Buffer.from('00000002', 'hex'); + + await supportedNFTsStore.save(methodContext, chainID1, { + supportedCollectionIDArray: [], + }); + await supportedNFTsStore.save(methodContext, chainID2, { + supportedCollectionIDArray: [], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + chainID: ownChainID, + }); + + await expect(endpoint.getSupportedCollectionIDs(context)).resolves.toEqual({ + supportedCollectionIDs: [ + `${ownChainID.toString('hex')}********`, + `${chainID1.toString('hex')}********`, + `${chainID2.toString('hex')}********`, + ], + }); + }); + + it("should return a supportedCollectionIDs array with ownChainID + 8 [*]s and for all chain id's stored in the supportedNFT store when supportedCollectionIDArray is not empty", async () => { + const chainID1 = Buffer.from('00000001', 'hex'); + const chainID2 = Buffer.from('00000002', 'hex'); + const collectionID1 = Buffer.from('00000001', 'hex'); + const collectionID2 = Buffer.from('00000002', 'hex'); + const collectionID3 = Buffer.from('00000003', 'hex'); + + await supportedNFTsStore.save(methodContext, chainID1, { + supportedCollectionIDArray: [ + { + collectionID: collectionID1, + }, + { + collectionID: collectionID2, + }, + ], + }); + await supportedNFTsStore.save(methodContext, chainID2, { + supportedCollectionIDArray: [ + { + collectionID: collectionID3, + }, + ], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + chainID: ownChainID, + }); + + await expect(endpoint.getSupportedCollectionIDs(context)).resolves.toEqual({ + supportedCollectionIDs: [ + `${ownChainID.toString('hex')}********`, + Buffer.concat([chainID1, collectionID1]).toString('hex'), + Buffer.concat([chainID1, collectionID2]).toString('hex'), + Buffer.concat([chainID2, collectionID3]).toString('hex'), + ], + }); + }); + + it('should return supportedCollectionIDs array with ownChainID + 8(*)s when there are no entries in the supportedNftStore', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + chainID: ownChainID, + }); + + await expect(endpoint.getSupportedCollectionIDs(context)).resolves.toEqual({ + supportedCollectionIDs: [`${ownChainID.toString('hex')}********`], + }); + }); + }); + + describe('isCollectionIDSupported', () => { + it('should fail if provided chainID has invalid length', async () => { + const minLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID - 1).toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID).toString('hex'), + }, + }); + + const maxLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID + 1).toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID).toString('hex'), + }, + }); + + await expect(endpoint.isCollectionIDSupported(minLengthContext)).rejects.toThrow( + `'.chainID' must NOT have fewer than 8 characters`, + ); + + await expect(endpoint.isCollectionIDSupported(maxLengthContext)).rejects.toThrow( + `'.chainID' must NOT have more than 8 characters`, + ); + }); + + it('should fail if provided collectionID has invalid length', async () => { + const minLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID).toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID - 1).toString('hex'), + }, + }); + + const maxLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID).toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID + 1).toString('hex'), + }, + }); + + await expect(endpoint.isCollectionIDSupported(minLengthContext)).rejects.toThrow( + `'.collectionID' must NOT have fewer than 8 characters`, + ); + + await expect(endpoint.isCollectionIDSupported(maxLengthContext)).rejects.toThrow( + `'.collectionID' must NOT have more than 8 characters`, + ); + }); + + it('should return false if NFT is not supported', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID).toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID).toString('hex'), + }, + }); + + await expect(endpoint.isCollectionIDSupported(context)).resolves.toEqual({ + isCollectionIDSupported: false, + }); + }); + + it('should return false if provided chainID does not exist', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID).toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID).toString('hex'), + }, + }); + + await expect(endpoint.isCollectionIDSupported(context)).resolves.toEqual({ + isCollectionIDSupported: false, + }); + + validator.validate(isCollectionIDSupportedResponseSchema, { isCollectionIDSupported: false }); + }); + + it('should return false if provided collectionID does not exist for the provided chainID', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID), + }, + ], + }); + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: chainID.toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID).toString('hex'), + }, + }); + + await expect(endpoint.isCollectionIDSupported(context)).resolves.toEqual({ + isCollectionIDSupported: false, + }); + }); + + it('should return true if provided collectionID exists for the provided chainID', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const collectionID = utils.getRandomBytes(LENGTH_COLLECTION_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID, + }, + ], + }); + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: chainID.toString('hex'), + collectionID: collectionID.toString('hex'), + }, + }); + + await expect(endpoint.isCollectionIDSupported(context)).resolves.toEqual({ + isCollectionIDSupported: true, + }); + + validator.validate(isCollectionIDSupportedResponseSchema, { isCollectionIDSupported: true }); + }); + }); + + describe('getEscrowedNFTIDs', () => { + it('should fail if provided chainID has invalid length', async () => { + const minLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID - 1).toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID).toString('hex'), + }, + }); + + const maxLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID + 1).toString('hex'), + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID).toString('hex'), + }, + }); + + await expect(endpoint.getEscrowedNFTIDs(minLengthContext)).rejects.toThrow( + `'.chainID' must NOT have fewer than 8 characters`, + ); + + await expect(endpoint.getEscrowedNFTIDs(maxLengthContext)).rejects.toThrow( + `'.chainID' must NOT have more than 8 characters`, + ); + }); + + it('should return empty list if provided chain has no NFTs escrowed to it', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID).toString('hex'), + }, + }); + + await expect(endpoint.getEscrowedNFTIDs(context)).resolves.toEqual({ escrowedNFTIDs: [] }); + + validator.validate(getEscrowedNFTIDsResponseSchema, { escrowedNFTIDs: [] }); + }); + + it('should return list of escrowed NFTs for the chainID', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const nftIDs = [Buffer.alloc(LENGTH_NFT_ID, 0), Buffer.alloc(LENGTH_NFT_ID, 255)]; + + for (const nftID of nftIDs) { + await nftStore.save(methodContext, nftID, { + owner: chainID, + attributesArray: [], + }); + } + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + chainID: chainID.toString('hex'), + }, + }); + + const expectedNFTIDs = { escrowedNFTIDs: nftIDs.map(nftID => nftID.toString('hex')) }; + + await expect(endpoint.getEscrowedNFTIDs(context)).resolves.toEqual(expectedNFTIDs); + + validator.validate(getEscrowedNFTIDsResponseSchema, expectedNFTIDs); + }); + }); + + describe('isNFTSupported', () => { + it('should fail if nftID does not have valid length', async () => { + const minLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + nftID: utils.getRandomBytes(LENGTH_NFT_ID - 1).toString('hex'), + }, + }); + + const maxLengthContext = createTransientModuleEndpointContext({ + stateStore, + params: { + nftID: utils.getRandomBytes(LENGTH_NFT_ID + 1).toString('hex'), + }, + }); + + await expect(endpoint.isNFTSupported(minLengthContext)).rejects.toThrow( + `'.nftID' must NOT have fewer than 32 characters`, + ); + + await expect(endpoint.isNFTSupported(maxLengthContext)).rejects.toThrow( + `'.nftID' must NOT have more than 32 characters`, + ); + }); + + it('should return false if NFT does not exist', async () => { + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + nftID: utils.getRandomBytes(LENGTH_NFT_ID).toString('hex'), + }, + }); + + await expect(endpoint.isNFTSupported(context)).resolves.toEqual({ isNFTSupported: false }); + + validator.validate(isNFTSupportedResponseSchema, { isNFTSupported: false }); + }); + + it('should return true if chainID of NFT is equal to ownChainID', async () => { + const nftID = Buffer.concat([ownChainID, Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID)]); + + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + nftID: nftID.toString('hex'), + }, + }); + + await expect(endpoint.isNFTSupported(context)).resolves.toEqual({ isNFTSupported: true }); + + validator.validate(isNFTSupportedResponseSchema, { isNFTSupported: true }); + }); + + it('should return true if all NFTs are supported', async () => { + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + nftID: nftID.toString('hex'), + }, + }); + + await expect(endpoint.isNFTSupported(context)).resolves.toEqual({ isNFTSupported: true }); + }); + + it('should return true if all collections of the chain are supported', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const nftID = Buffer.concat([chainID, Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID)]); + + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + nftID: nftID.toString('hex'), + }, + }); + + await expect(endpoint.isNFTSupported(context)).resolves.toEqual({ isNFTSupported: true }); + }); + + it('should return true if collection of the chain is supported', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const collectionID = utils.getRandomBytes(LENGTH_COLLECTION_ID); + const nftID = Buffer.concat([ + chainID, + collectionID, + Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID), + ]); + + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID, + }, + ], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + nftID: nftID.toString('hex'), + }, + }); + + await expect(endpoint.isNFTSupported(context)).resolves.toEqual({ isNFTSupported: true }); + }); + + it('should return false if collection of the chain is not supported', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const collectionID = utils.getRandomBytes(LENGTH_COLLECTION_ID); + const nftID = Buffer.concat([ + chainID, + collectionID, + Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID), + ]); + + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID), + }, + ], + }); + + const context = createTransientModuleEndpointContext({ + stateStore, + params: { + nftID: nftID.toString('hex'), + }, + }); + + await expect(endpoint.isNFTSupported(context)).resolves.toEqual({ isNFTSupported: false }); + }); + }); + + describe('getSupportedNFTs', () => { + it('should return * when all nft`s are supported globally', async () => { + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + const moduleEndpointContext = createTransientModuleEndpointContext({ stateStore }); + + await expect(endpoint.getSupportedNFTs(moduleEndpointContext)).resolves.toEqual({ + supportedNFTs: ['*'], + }); + }); + + it('should return the list of supported nft`s when all the nft`s from a chain are supported', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + const moduleEndpointContext = createTransientModuleEndpointContext({ + stateStore, + chainID, + }); + + await expect(endpoint.getSupportedNFTs(moduleEndpointContext)).resolves.toEqual({ + supportedNFTs: [`${chainID.toString('hex')}********`], + }); + }); + + it('should return the list of supported nft`s when not all the nft`s from a chain are supported', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const supportedCollections = [ + { + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID), + }, + { + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID), + }, + ]; + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: supportedCollections, + }); + + const moduleEndpointContext = createTransientModuleEndpointContext({ + stateStore, + chainID, + }); + + await expect(endpoint.getSupportedNFTs(moduleEndpointContext)).resolves.toEqual({ + supportedNFTs: [ + chainID.toString('hex') + supportedCollections[0].collectionID.toString('hex'), + chainID.toString('hex') + supportedCollections[1].collectionID.toString('hex'), + ], + }); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/init_genesis_state_fixtures.ts b/framework/test/unit/modules/nft/init_genesis_state_fixtures.ts new file mode 100644 index 00000000000..3c7a8bb338e --- /dev/null +++ b/framework/test/unit/modules/nft/init_genesis_state_fixtures.ts @@ -0,0 +1,199 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils } from '@liskhq/lisk-cryptography'; +import { GenesisNFTStore } from '../../../../src/modules/nft/types'; +import { + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + LENGTH_COLLECTION_ID, + LENGTH_NFT_ID, +} from '../../../../src/modules/nft/constants'; + +const nftID1 = utils.getRandomBytes(LENGTH_NFT_ID); +const nftID2 = utils.getRandomBytes(LENGTH_NFT_ID); +const nftID3 = utils.getRandomBytes(LENGTH_NFT_ID); +const owner = utils.getRandomBytes(LENGTH_ADDRESS); +const escrowedChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + +export const validData: GenesisNFTStore = { + nftSubstore: [ + { + nftID: nftID1, + owner, + attributesArray: [ + { + module: 'pos', + attributes: utils.getRandomBytes(10), + }, + { + module: 'token', + attributes: utils.getRandomBytes(10), + }, + ], + }, + { + nftID: nftID2, + owner, + attributesArray: [ + { + module: 'pos', + attributes: utils.getRandomBytes(10), + }, + { + module: 'token', + attributes: utils.getRandomBytes(10), + }, + ], + }, + { + nftID: nftID3, + owner: escrowedChainID, + attributesArray: [], + }, + ], + supportedNFTsSubstore: [ + { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + supportedCollectionIDArray: [], + }, + ], +}; + +export const validGenesisAssets = [['Valid genesis asset', validData]]; + +export const invalidSchemaNFTSubstoreGenesisAssets = [ + [ + 'Invalid nftID - minimum length not satisfied', + { + ...validData, + nftSubstore: [ + { + nftID: utils.getRandomBytes(LENGTH_NFT_ID - 1), + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }, + ], + }, + `nftID' minLength not satisfied`, + ], + [ + 'Invalid nftID - maximum length exceeded', + { + ...validData, + nftSubstore: [ + { + nftID: utils.getRandomBytes(LENGTH_NFT_ID + 1), + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }, + ], + }, + `nftID' maxLength exceeded`, + ], + [ + 'Invalid attributesArray.module - minimum length not satisfied', + { + ...validData, + nftSubstore: [ + { + nftID: utils.getRandomBytes(LENGTH_NFT_ID), + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [ + { + module: '', + attributes: utils.getRandomBytes(10), + }, + ], + }, + ], + }, + `module' must NOT have fewer than 1 characters`, + ], + [ + 'Invalid attributesArray.module - maximum length exceeded', + { + ...validData, + nftSubstore: [ + { + nftID: utils.getRandomBytes(LENGTH_NFT_ID), + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [ + { + module: '1'.repeat(33), + attributes: utils.getRandomBytes(10), + }, + ], + }, + ], + }, + `module' must NOT have more than 32 characters`, + ], + [ + 'Invalid attributesArray.module - must match pattern "^[a-zA-Z0-9]*$"', + { + ...validData, + nftSubstore: [ + { + nftID: utils.getRandomBytes(LENGTH_NFT_ID), + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [ + { + module: '#$a1!', + attributes: utils.getRandomBytes(10), + }, + ], + }, + ], + }, + 'must match pattern "^[a-zA-Z0-9]*$"', + ], +]; + +export const invalidSchemaSupportedNFTsSubstoreGenesisAssets = [ + [ + 'Invalid collectionID - minimum length not satisfied', + { + ...validData, + supportedNFTsSubstore: [ + { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + supportedCollectionIDArray: [ + { + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID - 1), + }, + ], + }, + ], + }, + `collectionID' minLength not satisfied`, + ], + [ + 'Invalid collectionID - maximum length exceeded', + { + ...validData, + supportedNFTsSubstore: [ + { + chainID: utils.getRandomBytes(LENGTH_COLLECTION_ID), + supportedCollectionIDArray: [ + { + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID + 1), + }, + ], + }, + ], + }, + `collectionID' maxLength exceeded`, + ], +]; diff --git a/framework/test/unit/modules/nft/internal_method.spec.ts b/framework/test/unit/modules/nft/internal_method.spec.ts new file mode 100644 index 00000000000..89456d8d661 --- /dev/null +++ b/framework/test/unit/modules/nft/internal_method.spec.ts @@ -0,0 +1,575 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils } from '@liskhq/lisk-cryptography'; +import { codec } from '@liskhq/lisk-codec'; +import { NFTModule } from '../../../../src/modules/nft/module'; +import { InternalMethod } from '../../../../src/modules/nft/internal_method'; +import { EventQueue, createMethodContext } from '../../../../src/state_machine'; +import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; +import { InMemoryPrefixedStateDB } from '../../../../src/testing/in_memory_prefixed_state'; +import { + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + LENGTH_NFT_ID, + MODULE_NAME_NFT, + NFT_NOT_LOCKED, + LENGTH_TOKEN_ID, +} from '../../../../src/modules/nft/constants'; +import { NFTStore } from '../../../../src/modules/nft/stores/nft'; +import { MethodContext } from '../../../../src/state_machine/method_context'; +import { TransferEvent, TransferEventData } from '../../../../src/modules/nft/events/transfer'; +import { UserStore } from '../../../../src/modules/nft/stores/user'; +import { EscrowStore } from '../../../../src/modules/nft/stores/escrow'; +import { NFTMethod } from '../../../../src/modules/nft/method'; +import { + InteroperabilityMethod, + NFTAttributes, + TokenMethod, +} from '../../../../src/modules/nft/types'; +import { + TransferCrossChainEvent, + TransferCrossChainEventData, +} from '../../../../src/modules/nft/events/transfer_cross_chain'; +import { DestroyEvent, DestroyEventData } from '../../../../src/modules/nft/events/destroy'; +import { crossChainNFTTransferMessageParamsSchema } from '../../../../src/modules/nft/schemas'; + +describe('InternalMethod', () => { + const module = new NFTModule(); + const internalMethod = new InternalMethod(module.stores, module.events); + const method = new NFTMethod(module.stores, module.events); + let interoperabilityMethod!: InteroperabilityMethod; + let tokenMethod!: TokenMethod; + internalMethod.addDependencies(method, interoperabilityMethod, tokenMethod); + + const ownChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + internalMethod.init({ ownChainID }); + + let methodContext!: MethodContext; + + const checkEventResult = ( + eventQueue: EventQueue, + length: number, + EventClass: any, + index: number, + expectedResult: EventDataType, + result: any = 0, + ) => { + expect(eventQueue.getEvents()).toHaveLength(length); + expect(eventQueue.getEvents()[index].toObject().name).toEqual(new EventClass('nft').name); + + const eventData = codec.decode>( + new EventClass('nft').schema, + eventQueue.getEvents()[index].toObject().data, + ); + + expect(eventData).toEqual({ ...expectedResult, result }); + }; + + const userStore = module.stores.get(UserStore); + const nftStore = module.stores.get(NFTStore); + const escrowStore = module.stores.get(EscrowStore); + + const address = utils.getRandomBytes(LENGTH_ADDRESS); + const senderAddress = utils.getRandomBytes(LENGTH_ADDRESS); + const recipientAddress = utils.getRandomBytes(LENGTH_ADDRESS); + let nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + beforeEach(() => { + methodContext = createMethodContext({ + stateStore: new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()), + eventQueue: new EventQueue(0), + contextStore: new Map(), + }); + }); + + describe('createEscrowEntry', () => { + it('should create an entry in EscrowStore', async () => { + const receivingChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await internalMethod.createEscrowEntry(methodContext, receivingChainID, nftID); + + await expect( + escrowStore.get(methodContext, escrowStore.getKey(receivingChainID, nftID)), + ).resolves.toEqual({}); + }); + }); + + describe('hasDuplicateModuleNames', () => { + it('should return false when the attributes array is empty', () => { + const attributesArray: NFTAttributes[] = []; + + expect(internalMethod.hasDuplicateModuleNames(attributesArray)).toBeFalse(); + }); + + it('should return false when all module names are unique', () => { + const attributesArray: NFTAttributes[] = [ + { module: 'module1', attributes: Buffer.from('attributes1') }, + { module: 'module2', attributes: Buffer.from('attributes2') }, + { module: 'module3', attributes: Buffer.from('attributes3') }, + ]; + + const result = internalMethod.hasDuplicateModuleNames(attributesArray); + + expect(result).toBeFalse(); + }); + + it('should return true when there are duplicate module names', () => { + const attributesArray: NFTAttributes[] = [ + { module: 'module1', attributes: Buffer.from('attributes1') }, + { module: 'module1', attributes: Buffer.from('attributes2') }, + { module: 'module3', attributes: Buffer.from('attributes3') }, + ]; + + const result = internalMethod.hasDuplicateModuleNames(attributesArray); + + expect(result).toBeTrue(); + }); + }); + + describe('createNFTEntry', () => { + it('should throw for duplicate module names in attributes array', async () => { + const attributesArray = [ + { + module: 'module1', + attributes: Buffer.alloc(8, 1), + }, + { + module: 'module1', + attributes: Buffer.alloc(8, 2), + }, + ]; + + await expect( + internalMethod.createNFTEntry(methodContext, address, nftID, attributesArray), + ).rejects.toThrow('Invalid attributes array provided'); + }); + + it('should create an entry in NFStore with attributes sorted by module if there is no duplicate module name', async () => { + const unsortedAttributesArray = [ + { + module: 'module1', + attributes: Buffer.alloc(8, 1), + }, + { + module: 'module2', + attributes: Buffer.alloc(8, 1), + }, + ]; + + const sortedAttributesArray = [...unsortedAttributesArray].sort((a, b) => + a.module.localeCompare(b.module, 'en'), + ); + + await internalMethod.createNFTEntry(methodContext, address, nftID, unsortedAttributesArray); + + await expect(nftStore.get(methodContext, nftID)).resolves.toEqual({ + owner: address, + attributesArray: sortedAttributesArray, + }); + }); + }); + + describe('createUserEntry', () => { + it('should create an entry for an unlocked NFT in UserStore', async () => { + await expect( + internalMethod.createUserEntry(methodContext, address, nftID), + ).resolves.toBeUndefined(); + + await expect(userStore.get(methodContext, userStore.getKey(address, nftID))).resolves.toEqual( + { + lockingModule: NFT_NOT_LOCKED, + }, + ); + }); + }); + + describe('transfer', () => { + it('should transfer NFT from sender to recipient and emit Transfer event', async () => { + await module.stores.get(NFTStore).save(methodContext, nftID, { + owner: senderAddress, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(senderAddress, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await internalMethod.transfer(methodContext, recipientAddress, nftID); + + await expect(module.stores.get(NFTStore).get(methodContext, nftID)).resolves.toEqual({ + owner: recipientAddress, + attributesArray: [], + }); + + await expect( + userStore.has(methodContext, userStore.getKey(senderAddress, nftID)), + ).resolves.toBeFalse(); + + await expect( + userStore.get(methodContext, userStore.getKey(recipientAddress, nftID)), + ).resolves.toEqual({ + lockingModule: NFT_NOT_LOCKED, + }); + + checkEventResult(methodContext.eventQueue, 1, TransferEvent, 0, { + senderAddress, + recipientAddress, + nftID, + }); + }); + + it('should fail if NFT does not exist', async () => { + await expect(internalMethod.transfer(methodContext, recipientAddress, nftID)).rejects.toThrow( + 'does not exist', + ); + }); + }); + + describe('transferCrossChain', () => { + let receivingChainID: Buffer; + const messageFee = BigInt(1000); + const data = ''; + const timestamp = Math.floor(Date.now() / 1000); + + beforeEach(() => { + receivingChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + interoperabilityMethod = { + send: jest.fn().mockResolvedValue(Promise.resolve()), + error: jest.fn().mockResolvedValue(Promise.resolve()), + terminateChain: jest.fn().mockRejectedValue(Promise.resolve()), + getMessageFeeTokenID: jest + .fn() + .mockResolvedValue(Promise.resolve(utils.getRandomBytes(LENGTH_TOKEN_ID))), + }; + + internalMethod.addDependencies(method, interoperabilityMethod, tokenMethod); + }); + + describe('if attributes are not included ccm contains empty attributes', () => { + const includeAttributes = false; + + it('should transfer the ownership of the NFT to the receiving chain and escrow it for a native NFT', async () => { + const chainID = ownChainID; + nftID = Buffer.concat([chainID, utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID)]); + + const ccmParameters = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID, + senderAddress, + recipientAddress, + attributesArray: [], + data, + }); + + await nftStore.save(methodContext, nftID, { + owner: senderAddress, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(senderAddress, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await expect( + internalMethod.transferCrossChain( + methodContext, + senderAddress, + recipientAddress, + nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + timestamp, + ), + ).resolves.toBeUndefined(); + + await expect(nftStore.get(methodContext, nftID)).resolves.toEqual({ + owner: receivingChainID, + attributesArray: [], + }); + + await expect( + userStore.has(methodContext, userStore.getKey(senderAddress, nftID)), + ).resolves.toBeFalse(); + + await expect( + escrowStore.get(methodContext, escrowStore.getKey(receivingChainID, nftID)), + ).resolves.toEqual({}); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress, + recipientAddress, + nftID, + receivingChainID, + includeAttributes, + }, + ); + + expect(internalMethod['_interoperabilityMethod'].send).toHaveBeenCalledOnce(); + expect(internalMethod['_interoperabilityMethod'].send).toHaveBeenNthCalledWith( + 1, + expect.anything(), + senderAddress, + MODULE_NAME_NFT, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + receivingChainID, + messageFee, + ccmParameters, + timestamp, + ); + }); + + it('should destroy NFT if the chain ID of the NFT is the same as receiving chain', async () => { + nftID = Buffer.concat([ + receivingChainID, + utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID), + ]); + + const ccmParameters = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID, + senderAddress, + recipientAddress, + attributesArray: [], + data, + }); + + await nftStore.save(methodContext, nftID, { + owner: senderAddress, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(senderAddress, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await expect( + internalMethod.transferCrossChain( + methodContext, + senderAddress, + recipientAddress, + nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + timestamp, + ), + ).resolves.toBeUndefined(); + + checkEventResult(methodContext.eventQueue, 2, DestroyEvent, 0, { + address: senderAddress, + nftID, + }); + + checkEventResult( + methodContext.eventQueue, + 2, + TransferCrossChainEvent, + 1, + { + senderAddress, + recipientAddress, + nftID, + receivingChainID, + includeAttributes, + }, + ); + + expect(internalMethod['_interoperabilityMethod'].send).toHaveBeenCalledOnce(); + expect(internalMethod['_interoperabilityMethod'].send).toHaveBeenNthCalledWith( + 1, + expect.anything(), + senderAddress, + MODULE_NAME_NFT, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + receivingChainID, + messageFee, + ccmParameters, + timestamp, + ); + }); + }); + + describe('if attributes are included ccm contains attributes of the NFT', () => { + const includeAttributes = true; + + it('should transfer the ownership of the NFT to the receiving chain and escrow it for a native NFT', async () => { + const chainID = ownChainID; + nftID = Buffer.concat([chainID, utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID)]); + + const attributesArray = [ + { + module: 'pos', + attributes: utils.getRandomBytes(20), + }, + ]; + + const ccmParameters = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID, + senderAddress, + recipientAddress, + attributesArray, + data, + }); + + await nftStore.save(methodContext, nftID, { + owner: senderAddress, + attributesArray, + }); + + await userStore.set(methodContext, userStore.getKey(senderAddress, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await expect( + internalMethod.transferCrossChain( + methodContext, + senderAddress, + recipientAddress, + nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + timestamp, + ), + ).resolves.toBeUndefined(); + + await expect(nftStore.get(methodContext, nftID)).resolves.toEqual({ + owner: receivingChainID, + attributesArray, + }); + + await expect( + userStore.has(methodContext, userStore.getKey(senderAddress, nftID)), + ).resolves.toBeFalse(); + + await expect( + escrowStore.get(methodContext, escrowStore.getKey(receivingChainID, nftID)), + ).resolves.toEqual({}); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress, + recipientAddress, + nftID, + receivingChainID, + includeAttributes, + }, + ); + + expect(internalMethod['_interoperabilityMethod'].send).toHaveBeenCalledOnce(); + expect(internalMethod['_interoperabilityMethod'].send).toHaveBeenNthCalledWith( + 1, + expect.anything(), + senderAddress, + MODULE_NAME_NFT, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + receivingChainID, + messageFee, + ccmParameters, + timestamp, + ); + }); + + it('should destroy NFT if the chain ID of the NFT is the same as receiving chain', async () => { + nftID = Buffer.concat([ + receivingChainID, + utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID), + ]); + + const attributesArray = [ + { + module: 'pos', + attributes: utils.getRandomBytes(20), + }, + ]; + + const ccmParameters = codec.encode(crossChainNFTTransferMessageParamsSchema, { + nftID, + senderAddress, + recipientAddress, + attributesArray, + data, + }); + + await nftStore.save(methodContext, nftID, { + owner: senderAddress, + attributesArray, + }); + + await userStore.set(methodContext, userStore.getKey(senderAddress, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await expect( + internalMethod.transferCrossChain( + methodContext, + senderAddress, + recipientAddress, + nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + timestamp, + ), + ).resolves.toBeUndefined(); + + checkEventResult(methodContext.eventQueue, 2, DestroyEvent, 0, { + address: senderAddress, + nftID, + }); + + checkEventResult( + methodContext.eventQueue, + 2, + TransferCrossChainEvent, + 1, + { + senderAddress, + recipientAddress, + nftID, + receivingChainID, + includeAttributes, + }, + ); + + expect(internalMethod['_interoperabilityMethod'].send).toHaveBeenCalledOnce(); + expect(internalMethod['_interoperabilityMethod'].send).toHaveBeenNthCalledWith( + 1, + expect.anything(), + senderAddress, + MODULE_NAME_NFT, + CROSS_CHAIN_COMMAND_NAME_TRANSFER, + receivingChainID, + messageFee, + ccmParameters, + timestamp, + ); + }); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/method.spec.ts b/framework/test/unit/modules/nft/method.spec.ts new file mode 100644 index 00000000000..7fd740f9e83 --- /dev/null +++ b/framework/test/unit/modules/nft/method.spec.ts @@ -0,0 +1,1902 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec } from '@liskhq/lisk-codec'; +import { utils } from '@liskhq/lisk-cryptography'; +import { when } from 'jest-when'; +import { NFTMethod } from '../../../../src/modules/nft/method'; +import { NFTModule } from '../../../../src/modules/nft/module'; +import { EventQueue } from '../../../../src/state_machine'; +import { MethodContext, createMethodContext } from '../../../../src/state_machine/method_context'; +import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; +import { InMemoryPrefixedStateDB } from '../../../../src/testing/in_memory_prefixed_state'; +import { + ALL_SUPPORTED_NFTS_KEY, + FEE_CREATE_NFT, + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + LENGTH_COLLECTION_ID, + LENGTH_NFT_ID, + LENGTH_TOKEN_ID, + NFT_NOT_LOCKED, + NftEventResult, +} from '../../../../src/modules/nft/constants'; +import { NFTStore, nftStoreSchema } from '../../../../src/modules/nft/stores/nft'; +import { UserStore } from '../../../../src/modules/nft/stores/user'; +import { DestroyEvent, DestroyEventData } from '../../../../src/modules/nft/events/destroy'; +import { SupportedNFTsStore } from '../../../../src/modules/nft/stores/supported_nfts'; +import { CreateEvent } from '../../../../src/modules/nft/events/create'; +import { LockEvent, LockEventData } from '../../../../src/modules/nft/events/lock'; +import { InternalMethod } from '../../../../src/modules/nft/internal_method'; +import { TransferEvent, TransferEventData } from '../../../../src/modules/nft/events/transfer'; +import { + TransferCrossChainEvent, + TransferCrossChainEventData, +} from '../../../../src/modules/nft/events/transfer_cross_chain'; +import { AllNFTsSupportedEvent } from '../../../../src/modules/nft/events/all_nfts_supported'; +import { AllNFTsSupportRemovedEvent } from '../../../../src/modules/nft/events/all_nfts_support_removed'; +import { + AllNFTsFromChainSupportedEvent, + AllNFTsFromChainSupportedEventData, +} from '../../../../src/modules/nft/events/all_nfts_from_chain_suported'; +import { + AllNFTsFromCollectionSupportRemovedEvent, + AllNFTsFromCollectionSupportRemovedEventData, +} from '../../../../src/modules/nft/events/all_nfts_from_collection_support_removed'; +import { + AllNFTsFromCollectionSupportedEvent, + AllNFTsFromCollectionSupportedEventData, +} from '../../../../src/modules/nft/events/all_nfts_from_collection_suppported'; +import { + AllNFTsFromChainSupportRemovedEvent, + AllNFTsFromChainSupportRemovedEventData, +} from '../../../../src/modules/nft/events/all_nfts_from_chain_support_removed'; +import { RecoverEvent, RecoverEventData } from '../../../../src/modules/nft/events/recover'; +import { + SetAttributesEvent, + SetAttributesEventData, +} from '../../../../src/modules/nft/events/set_attributes'; +import { EscrowStore } from '../../../../src/modules/nft/stores/escrow'; +import { UnlockEvent, UnlockEventData } from '../../../../src/modules/nft/events/unlock'; + +describe('NFTMethod', () => { + const module = new NFTModule(); + const method = new NFTMethod(module.stores, module.events); + const internalMethod = new InternalMethod(module.stores, module.events); + const messageFeeTokenID = utils.getRandomBytes(LENGTH_TOKEN_ID); + const interopMethod = { + send: jest.fn(), + error: jest.fn(), + terminateChain: jest.fn(), + getMessageFeeTokenID: jest.fn().mockResolvedValue(Promise.resolve(messageFeeTokenID)), + }; + const feeMethod = { payFee: jest.fn() }; + const tokenMethod = { + getAvailableBalance: jest.fn(), + }; + const config = { + ownChainID: Buffer.alloc(LENGTH_CHAIN_ID, 1), + escrowAccountInitializationFee: BigInt(50_000_000), + userAccountInitializationFee: BigInt(50_000_000), + }; + + let methodContext!: MethodContext; + + const lockingModule = 'token'; + const nftStore = module.stores.get(NFTStore); + const userStore = module.stores.get(UserStore); + const supportedNFTsStore = module.stores.get(SupportedNFTsStore); + + const firstIndex = Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID, 0); + firstIndex.writeBigUInt64BE(BigInt(0)); + const nftID = Buffer.concat([ + config.ownChainID, + utils.getRandomBytes(LENGTH_CHAIN_ID), + firstIndex, + ]); + + const checkEventResult = ( + eventQueue: EventQueue, + length: number, + EventClass: any, + index: number, + expectedResult: EventDataType, + result: any = 0, + ) => { + const events = eventQueue.getEvents(); + expect(events).toHaveLength(length); + expect(events[index].toObject().name).toEqual(new EventClass('nft').name); + + const eventData = codec.decode>( + new EventClass('nft').schema, + events[index].toObject().data, + ); + + if (result !== null) { + expect(eventData).toEqual({ ...expectedResult, result }); + } + }; + + let existingNFT: { nftID: any; owner: any }; + let existingNativeNFT: { nftID: any; owner: any }; + let lockedExistingNFT: { nftID: any; owner: any; lockingModule: string }; + let escrowedNFT: { nftID: any; owner: any }; + + beforeEach(async () => { + method.addDependencies(internalMethod, feeMethod); + method.init(config); + internalMethod.addDependencies(method, interopMethod, tokenMethod); + internalMethod.init(config); + + methodContext = createMethodContext({ + stateStore: new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()), + eventQueue: new EventQueue(0), + contextStore: new Map(), + }); + + existingNFT = { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + nftID: utils.getRandomBytes(LENGTH_NFT_ID), + }; + + existingNativeNFT = { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + nftID: Buffer.concat([config.ownChainID, Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID)]), + }; + + lockedExistingNFT = { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + nftID: utils.getRandomBytes(LENGTH_NFT_ID), + lockingModule: 'token', + }; + + escrowedNFT = { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + nftID: utils.getRandomBytes(LENGTH_NFT_ID), + }; + + await nftStore.save(methodContext, existingNFT.nftID, { + owner: existingNFT.owner, + attributesArray: [], + }); + + await nftStore.save(methodContext, existingNativeNFT.nftID, { + owner: existingNativeNFT.owner, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(existingNFT.owner, existingNFT.nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await nftStore.save(methodContext, lockedExistingNFT.nftID, { + owner: lockedExistingNFT.owner, + attributesArray: [], + }); + + await userStore.set( + methodContext, + userStore.getKey(lockedExistingNFT.owner, lockedExistingNFT.nftID), + { + lockingModule: lockedExistingNFT.lockingModule, + }, + ); + + await nftStore.save(methodContext, escrowedNFT.nftID, { + owner: escrowedNFT.owner, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(escrowedNFT.owner, escrowedNFT.nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + }); + + describe('getChainID', () => { + it('should throw if nftID has invalid length', () => { + expect(() => { + method.getChainID(utils.getRandomBytes(LENGTH_NFT_ID - 1)); + }).toThrow(`NFT ID must have length ${LENGTH_NFT_ID}`); + }); + + it('should return the first bytes of length LENGTH_CHAIN_ID from provided nftID', () => { + expect(method.getChainID(nftID)).toEqual(nftID.subarray(0, LENGTH_CHAIN_ID)); + }); + }); + + describe('isNFTEscrowed', () => { + it('should return true if nft owner is a chain', () => { + expect(method.isNFTEscrowed({ ...escrowedNFT, attributesArray: [] })).toBeTrue(); + }); + + it('should return false if nft owner is not a chain', () => { + expect(method.isNFTEscrowed({ ...existingNFT, attributesArray: [] })).toBeFalse(); + }); + }); + + describe('getNFT', () => { + it('should fail if NFT does not exist', async () => { + await expect( + method.getNFT(methodContext, utils.getRandomBytes(LENGTH_NFT_ID)), + ).rejects.toThrow('NFT substore entry does not exist'); + }); + + it('should fail if NFT exist but the corresponding entry in the user store does not exist', async () => { + await userStore.del(methodContext, userStore.getKey(existingNFT.owner, existingNFT.nftID)); + await expect(method.getNFT(methodContext, existingNFT.nftID)).rejects.toThrow( + 'User substore entry does not exist', + ); + }); + + it('should return NFT details if NFT and corresponding user store entry exist', async () => { + await expect(method.getNFT(methodContext, existingNFT.nftID)).resolves.toStrictEqual({ + owner: existingNFT.owner, + attributesArray: [], + lockingModule: NFT_NOT_LOCKED, + }); + }); + }); + + describe('isNFTLocked', () => { + it('should return true if nft is locked', () => { + expect(method.isNFTLocked({ ...lockedExistingNFT, attributesArray: [] })).toBeTrue(); + }); + + it('should return false if nft does not have locking module property', () => { + expect(method.isNFTLocked({ ...existingNFT, attributesArray: [] })).toBeFalse(); + }); + + it('should return false if nft is locked by module NFT_NOT_LOCKED', () => { + expect( + method.isNFTLocked({ ...existingNFT, lockingModule: NFT_NOT_LOCKED, attributesArray: [] }), + ).toBeFalse(); + }); + }); + + describe('destroy', () => { + it('should fail and emit Destroy event if NFT does not exist', async () => { + const address = utils.getRandomBytes(LENGTH_ADDRESS); + + await expect(method.destroy(methodContext, address, nftID)).rejects.toThrow( + 'NFT does not exist', + ); + + checkEventResult( + methodContext.eventQueue, + 1, + DestroyEvent, + 0, + { + address, + nftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + }); + + it('should fail and emit Destroy event if NFT is not owned by the provided address', async () => { + const notOwner = utils.getRandomBytes(LENGTH_ADDRESS); + + await expect(method.destroy(methodContext, notOwner, existingNFT.nftID)).rejects.toThrow( + 'Not initiated by the NFT owner', + ); + + checkEventResult( + methodContext.eventQueue, + 1, + DestroyEvent, + 0, + { + address: notOwner, + nftID: existingNFT.nftID, + }, + NftEventResult.RESULT_INITIATED_BY_NONOWNER, + ); + }); + + it('should fail and emit Destroy event if NFT is escrowed', async () => { + await expect( + method.destroy(methodContext, escrowedNFT.owner, escrowedNFT.nftID), + ).rejects.toThrow('NFT is escrowed to another chain'); + + checkEventResult( + methodContext.eventQueue, + 1, + DestroyEvent, + 0, + { + address: escrowedNFT.owner, + nftID: escrowedNFT.nftID, + }, + NftEventResult.RESULT_NFT_ESCROWED, + ); + }); + + it('should fail and emit Destroy event if NFT is locked', async () => { + await expect( + method.destroy(methodContext, lockedExistingNFT.owner, lockedExistingNFT.nftID), + ).rejects.toThrow('Locked NFTs cannot be destroyed'); + + checkEventResult( + methodContext.eventQueue, + 1, + DestroyEvent, + 0, + { + address: lockedExistingNFT.owner, + nftID: lockedExistingNFT.nftID, + }, + NftEventResult.RESULT_NFT_LOCKED, + ); + }); + + it('should delete NFTStore and UserStore entry and emit Destroy event', async () => { + await expect( + method.destroy(methodContext, existingNFT.owner, existingNFT.nftID), + ).resolves.toBeUndefined(); + + await expect(nftStore.has(methodContext, existingNFT.nftID)).resolves.toBeFalse(); + await expect( + userStore.has(methodContext, Buffer.concat([existingNFT.owner, escrowedNFT.nftID])), + ).resolves.toBeFalse(); + + checkEventResult(methodContext.eventQueue, 1, DestroyEvent, 0, { + address: existingNFT.owner, + nftID: existingNFT.nftID, + }); + }); + }); + + describe('getCollectionID', () => { + it('should return the first bytes of length LENGTH_CHAIN_ID from provided nftID', async () => { + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray: [], + }); + const expectedValue = nftID.subarray(LENGTH_CHAIN_ID, LENGTH_CHAIN_ID + LENGTH_COLLECTION_ID); + const receivedValue = method.getCollectionID(nftID); + expect(receivedValue).toEqual(expectedValue); + }); + }); + + describe('isNFTSupported', () => { + it('should return true if nft chain id equals own chain id', async () => { + const isSupported = await method.isNFTSupported(methodContext, existingNativeNFT.nftID); + expect(isSupported).toBe(true); + }); + + it('should return true if nft chain id does not equal own chain id but all nft keys are supported', async () => { + await supportedNFTsStore.set(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + const isSupported = await method.isNFTSupported(methodContext, nftID); + expect(isSupported).toBe(true); + }); + + it('should return true if nft chain id does not equal own chain id but nft chain id is supported and corresponding supported collection id array is empty', async () => { + await supportedNFTsStore.set(methodContext, nftID.subarray(0, LENGTH_CHAIN_ID), { + supportedCollectionIDArray: [], + }); + + const isSupported = await method.isNFTSupported(methodContext, nftID); + expect(isSupported).toBe(true); + }); + + it('should return true if nft chain id does not equal own chain id but nft chain id is supported and corresponding supported collection id array includes collection id for nft id', async () => { + await supportedNFTsStore.set(methodContext, nftID.subarray(0, LENGTH_CHAIN_ID), { + supportedCollectionIDArray: [ + { collectionID: nftID.subarray(LENGTH_CHAIN_ID, LENGTH_CHAIN_ID + LENGTH_COLLECTION_ID) }, + { collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID) }, + ], + }); + + const isSupported = await method.isNFTSupported(methodContext, nftID); + expect(isSupported).toBe(true); + }); + + it('should return false if nft chain id does not equal own chain id and nft chain id is supported but corresponding supported collection id array does not include collection id for nft id', async () => { + const foreignNFT = utils.getRandomBytes(LENGTH_NFT_ID); + await nftStore.save(methodContext, foreignNFT, { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + + await supportedNFTsStore.set(methodContext, foreignNFT.subarray(0, LENGTH_CHAIN_ID), { + supportedCollectionIDArray: [ + { collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID) }, + { collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID) }, + ], + }); + + const isSupported = await method.isNFTSupported(methodContext, foreignNFT); + expect(isSupported).toBe(false); + }); + }); + + describe('getNextAvailableIndex', () => { + const attributesArray = [ + { module: 'customMod1', attributes: Buffer.alloc(5) }, + { module: 'customMod2', attributes: Buffer.alloc(2) }, + ]; + const collectionID = nftID.subarray(LENGTH_CHAIN_ID, LENGTH_CHAIN_ID + LENGTH_COLLECTION_ID); + + beforeEach(async () => { + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray, + }); + }); + + it('should return index count 0 if there is no entry in nft substore', async () => { + await nftStore.del(methodContext, nftID); + const returnedIndex = await method.getNextAvailableIndex(methodContext, collectionID); + expect(returnedIndex).toBe(BigInt(0)); + }); + + it('should return index count 0 if entry exists in the nft substore for the nft id and no key matches the given collection id', async () => { + const returnedIndex = await method.getNextAvailableIndex( + methodContext, + utils.getRandomBytes(LENGTH_COLLECTION_ID), + ); + expect(returnedIndex).toBe(BigInt(0)); + }); + + it('should return existing highest index incremented by 1 within the given collection id', async () => { + const highestIndex = Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID, 0); + highestIndex.writeBigUInt64BE(BigInt(419)); + const nftIDHighestIndex = Buffer.concat([config.ownChainID, collectionID, highestIndex]); + await nftStore.save(methodContext, nftIDHighestIndex, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray, + }); + + const returnedIndex = await method.getNextAvailableIndex(methodContext, collectionID); + expect(returnedIndex).toBe(BigInt(420)); + }); + + it('should throw if indexes within a collection are consumed', async () => { + const largestIndex = Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID, 0); + largestIndex.writeBigUInt64BE(BigInt(BigInt(2 ** 64) - BigInt(1))); + const nftIDHighestIndex = Buffer.concat([config.ownChainID, collectionID, largestIndex]); + await nftStore.save(methodContext, nftIDHighestIndex, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray, + }); + + await expect(method.getNextAvailableIndex(methodContext, collectionID)).rejects.toThrow( + 'No more available indexes', + ); + }); + }); + + describe('create', () => { + const attributesArray1 = [{ module: 'customMod3', attributes: Buffer.alloc(7) }]; + const attributesArray2 = [{ module: 'customMod3', attributes: Buffer.alloc(9) }]; + const collectionID = nftID.subarray(LENGTH_CHAIN_ID, LENGTH_CHAIN_ID + LENGTH_COLLECTION_ID); + const address = utils.getRandomBytes(LENGTH_ADDRESS); + + beforeEach(() => { + method.addDependencies(internalMethod, feeMethod); + jest.spyOn(feeMethod, 'payFee'); + }); + + it('should throw for duplicate module names in attributes array', async () => { + const attributesArray = [ + { + module: 'token', + attributes: Buffer.alloc(8, 1), + }, + { + module: 'token', + attributes: Buffer.alloc(8, 2), + }, + ]; + + await expect( + method.create(methodContext, address, collectionID, attributesArray), + ).rejects.toThrow('Invalid attributes array provided'); + }); + + it('should set data to stores with correct key and emit successfull create event when there is no entry in the nft substore', async () => { + const indexBytes = Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID); + indexBytes.writeBigInt64BE(BigInt(0)); + + const expectedKey = Buffer.concat([config.ownChainID, collectionID, indexBytes]); + + await method.create(methodContext, address, collectionID, attributesArray2); + const nftStoreData = await nftStore.get(methodContext, expectedKey); + const userStoreData = await userStore.get( + methodContext, + userStore.getKey(address, expectedKey), + ); + expect(feeMethod.payFee).toHaveBeenCalledWith(methodContext, BigInt(FEE_CREATE_NFT)); + expect(nftStoreData.owner).toStrictEqual(address); + expect(nftStoreData.attributesArray).toEqual(attributesArray2); + expect(userStoreData.lockingModule).toEqual(NFT_NOT_LOCKED); + + checkEventResult(methodContext.eventQueue, 1, CreateEvent, 0, { + address, + nftID: expectedKey, + }); + }); + + it('should set data to stores with correct key and emit successfull create event when there is some entry in the nft substore', async () => { + const indexBytes = Buffer.alloc(LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID); + indexBytes.writeBigUint64BE(BigInt(911)); + const newKey = Buffer.concat([config.ownChainID, collectionID, indexBytes]); + await nftStore.save(methodContext, newKey, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray: attributesArray1, + }); + + const expectedIndexBytes = Buffer.alloc( + LENGTH_NFT_ID - LENGTH_CHAIN_ID - LENGTH_COLLECTION_ID, + ); + expectedIndexBytes.writeBigUint64BE(BigInt(912)); + const expectedKey = Buffer.concat([config.ownChainID, collectionID, expectedIndexBytes]); + + await method.create(methodContext, address, collectionID, attributesArray2); + const nftStoreData = await nftStore.get(methodContext, expectedKey); + const userStoreData = await userStore.get( + methodContext, + userStore.getKey(address, expectedKey), + ); + expect(feeMethod.payFee).toHaveBeenCalledWith(methodContext, BigInt(FEE_CREATE_NFT)); + expect(nftStoreData.owner).toStrictEqual(address); + expect(nftStoreData.attributesArray).toEqual(attributesArray2); + expect(userStoreData.lockingModule).toEqual(NFT_NOT_LOCKED); + + checkEventResult(methodContext.eventQueue, 1, CreateEvent, 0, { + address, + nftID: expectedKey, + }); + }); + }); + + describe('lock', () => { + it('should throw if provided locking module is "nft"', async () => { + await expect(method.lock(methodContext, NFT_NOT_LOCKED, existingNFT.nftID)).rejects.toThrow( + 'Cannot be locked by NFT module', + ); + }); + + it('should throw and log LockEvent if NFT does not exist', async () => { + await expect(method.lock(methodContext, lockingModule, nftID)).rejects.toThrow( + 'NFT does not exist', + ); + + checkEventResult( + methodContext.eventQueue, + 1, + LockEvent, + 0, + { + module: lockingModule, + nftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + }); + + it('should throw and log LockEvent if NFT is escrowed', async () => { + await expect(method.lock(methodContext, lockingModule, escrowedNFT.nftID)).rejects.toThrow( + 'NFT is escrowed to another chain', + ); + + checkEventResult( + methodContext.eventQueue, + 1, + LockEvent, + 0, + { + module: lockingModule, + nftID: escrowedNFT.nftID, + }, + NftEventResult.RESULT_NFT_ESCROWED, + ); + }); + + it('should throw and log LockEvent if NFT is locked', async () => { + await expect( + method.lock(methodContext, lockingModule, lockedExistingNFT.nftID), + ).rejects.toThrow('NFT is already locked'); + + checkEventResult( + methodContext.eventQueue, + 1, + LockEvent, + 0, + { + module: lockingModule, + nftID: lockedExistingNFT.nftID, + }, + NftEventResult.RESULT_NFT_LOCKED, + ); + }); + + it('should update the locking module and log LockEvent', async () => { + const expectedLockingModule = 'lockingModule'; + await expect( + method.lock(methodContext, expectedLockingModule, existingNFT.nftID), + ).resolves.toBeUndefined(); + + checkEventResult( + methodContext.eventQueue, + 1, + LockEvent, + 0, + { + module: expectedLockingModule, + nftID: existingNFT.nftID, + }, + NftEventResult.RESULT_SUCCESSFUL, + ); + + const { lockingModule: actualLockingModule } = await userStore.get( + methodContext, + userStore.getKey(existingNFT.owner, existingNFT.nftID), + ); + + expect(actualLockingModule).toEqual(expectedLockingModule); + }); + }); + + describe('unlock', () => { + it('should throw and log LockEvent if NFT does not exist', async () => { + await expect(method.unlock(methodContext, module.name, nftID)).rejects.toThrow( + 'NFT does not exist', + ); + + checkEventResult( + methodContext.eventQueue, + 1, + UnlockEvent, + 0, + { + module: module.name, + nftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + }); + + it('should throw if NFT is escrowed', async () => { + await expect(method.unlock(methodContext, module.name, escrowedNFT.nftID)).rejects.toThrow( + 'NFT is escrowed to another chain', + ); + }); + + it('should throw and log LockEvent if NFT is not locked', async () => { + await expect(method.unlock(methodContext, module.name, existingNFT.nftID)).rejects.toThrow( + 'NFT is not locked', + ); + + checkEventResult( + methodContext.eventQueue, + 1, + UnlockEvent, + 0, + { + module: module.name, + nftID: existingNFT.nftID, + }, + NftEventResult.RESULT_NFT_NOT_LOCKED, + ); + }); + + it('should throw and log UnlockEvent if unlocking module is not the locking module', async () => { + await expect( + method.unlock(methodContext, module.name, lockedExistingNFT.nftID), + ).rejects.toThrow('Unlocking NFT via module that did not lock it'); + + checkEventResult( + methodContext.eventQueue, + 1, + UnlockEvent, + 0, + { + module: module.name, + nftID: lockedExistingNFT.nftID, + }, + NftEventResult.RESULT_UNAUTHORIZED_UNLOCK, + ); + }); + + it('should unlock and log UnlockEvent', async () => { + await expect( + method.unlock(methodContext, lockedExistingNFT.lockingModule, lockedExistingNFT.nftID), + ).resolves.toBeUndefined(); + + checkEventResult( + methodContext.eventQueue, + 1, + UnlockEvent, + 0, + { + module: lockedExistingNFT.lockingModule, + nftID: lockedExistingNFT.nftID, + }, + NftEventResult.RESULT_SUCCESSFUL, + ); + + const { lockingModule: expectedLockingModule } = await userStore.get( + methodContext, + userStore.getKey(lockedExistingNFT.owner, lockedExistingNFT.nftID), + ); + + expect(expectedLockingModule).toEqual(NFT_NOT_LOCKED); + }); + }); + + describe('transfer', () => { + const senderAddress = utils.getRandomBytes(LENGTH_ADDRESS); + const recipientAddress = utils.getRandomBytes(LENGTH_ADDRESS); + + it('should throw and emit error transfer event if nft does not exist', async () => { + await expect( + method.transfer(methodContext, senderAddress, recipientAddress, nftID), + ).rejects.toThrow('NFT does not exist'); + checkEventResult( + methodContext.eventQueue, + 1, + TransferEvent, + 0, + { + senderAddress, + recipientAddress, + nftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + }); + + it('should throw and emit error transfer event if nft is escrowed', async () => { + await expect( + method.transfer(methodContext, senderAddress, recipientAddress, escrowedNFT.nftID), + ).rejects.toThrow('NFT is escrowed to another chain'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferEvent, + 0, + { + senderAddress, + recipientAddress, + nftID: escrowedNFT.nftID, + }, + NftEventResult.RESULT_NFT_ESCROWED, + ); + }); + + it('should throw and emit error transfer event if transfer is not initiated by the nft owner', async () => { + await expect( + method.transfer(methodContext, senderAddress, recipientAddress, existingNFT.nftID), + ).rejects.toThrow('Transfer not initiated by the NFT owner'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferEvent, + 0, + { + senderAddress, + recipientAddress, + nftID: existingNFT.nftID, + }, + NftEventResult.RESULT_INITIATED_BY_NONOWNER, + ); + }); + + it('should throw and emit error transfer event if nft is locked', async () => { + await expect( + method.transfer( + methodContext, + lockedExistingNFT.owner, + recipientAddress, + lockedExistingNFT.nftID, + ), + ).rejects.toThrow('Locked NFTs cannot be transferred'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferEvent, + 0, + { + senderAddress: lockedExistingNFT.owner, + recipientAddress, + nftID: lockedExistingNFT.nftID, + }, + NftEventResult.RESULT_NFT_LOCKED, + ); + }); + + it('should resolve if all params are valid', async () => { + jest.spyOn(internalMethod, 'transfer'); + + await expect( + method.transfer(methodContext, existingNFT.owner, recipientAddress, existingNFT.nftID), + ).resolves.toBeUndefined(); + expect(internalMethod['transfer']).toHaveBeenCalledWith( + methodContext, + recipientAddress, + existingNFT.nftID, + ); + }); + }); + + describe('transferCrossChain', () => { + const senderAddress = utils.getRandomBytes(LENGTH_ADDRESS); + const recipientAddress = utils.getRandomBytes(LENGTH_ADDRESS); + const messageFee = BigInt(1000); + const data = ''; + const includeAttributes = false; + let receivingChainID: Buffer; + + beforeEach(() => { + receivingChainID = existingNFT.nftID.slice(0, LENGTH_CHAIN_ID); + }); + + it('should throw and emit error transfer cross chain event if receiving chain id is same as the own chain id', async () => { + config.ownChainID = receivingChainID; + method.init(config); + internalMethod.init(config); + await expect( + method.transferCrossChain( + methodContext, + existingNFT.owner, + recipientAddress, + existingNFT.nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ), + ).rejects.toThrow('Receiving chain cannot be the sending chain'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress: existingNFT.owner, + recipientAddress, + receivingChainID, + nftID: existingNFT.nftID, + includeAttributes, + }, + NftEventResult.INVALID_RECEIVING_CHAIN, + ); + }); + + it('should throw and emit error transfer cross chain event if nft does not exist', async () => { + config.ownChainID = Buffer.alloc(LENGTH_CHAIN_ID, 1); + method.init(config); + internalMethod.init(config); + const nonExistingNFTID = utils.getRandomBytes(LENGTH_NFT_ID); + receivingChainID = nonExistingNFTID.subarray(0, LENGTH_CHAIN_ID); + await expect( + method.transferCrossChain( + methodContext, + senderAddress, + recipientAddress, + nonExistingNFTID, + receivingChainID, + messageFee, + data, + includeAttributes, + ), + ).rejects.toThrow('NFT does not exist'); + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress, + recipientAddress, + receivingChainID, + nftID: nonExistingNFTID, + includeAttributes, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + }); + + it('should throw and emit error transfer cross chain event if nft is escrowed', async () => { + receivingChainID = escrowedNFT.nftID.slice(0, LENGTH_CHAIN_ID); + await expect( + method.transferCrossChain( + methodContext, + senderAddress, + recipientAddress, + escrowedNFT.nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ), + ).rejects.toThrow('NFT is escrowed to another chain'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress, + recipientAddress, + receivingChainID, + nftID: escrowedNFT.nftID, + includeAttributes, + }, + NftEventResult.RESULT_NFT_ESCROWED, + ); + }); + + it('should throw and emit error transfer cross chain event if nft chain id is equal to neither own chain id or receiving chain id', async () => { + const randomAddress = utils.getRandomBytes(LENGTH_ADDRESS); + const randomNftID = utils.getRandomBytes(LENGTH_NFT_ID); + + await nftStore.save(methodContext, randomNftID, { + owner: randomAddress, + attributesArray: [], + }); + + await userStore.set(methodContext, userStore.getKey(randomAddress, randomNftID), { + lockingModule: NFT_NOT_LOCKED, + }); + + await expect( + method.transferCrossChain( + methodContext, + randomAddress, + recipientAddress, + randomNftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ), + ).rejects.toThrow('NFT must be native to either the sending or the receiving chain'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress: randomAddress, + recipientAddress, + receivingChainID, + nftID: randomNftID, + includeAttributes, + }, + NftEventResult.RESULT_NFT_NOT_NATIVE, + ); + }); + + it('should throw and emit error transfer cross chain event if transfer is not initiated by the nft owner', async () => { + await expect( + method.transferCrossChain( + methodContext, + senderAddress, + recipientAddress, + existingNFT.nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ), + ).rejects.toThrow('Transfer not initiated by the NFT owner'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress, + recipientAddress, + receivingChainID, + nftID: existingNFT.nftID, + includeAttributes, + }, + NftEventResult.RESULT_INITIATED_BY_NONOWNER, + ); + }); + + it('should throw and emit error transfer cross chain event if nft is locked', async () => { + receivingChainID = lockedExistingNFT.nftID.slice(0, LENGTH_CHAIN_ID); + await expect( + method.transferCrossChain( + methodContext, + lockedExistingNFT.owner, + recipientAddress, + lockedExistingNFT.nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ), + ).rejects.toThrow('Locked NFTs cannot be transferred'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress: lockedExistingNFT.owner, + recipientAddress, + receivingChainID, + nftID: lockedExistingNFT.nftID, + includeAttributes, + }, + NftEventResult.RESULT_NFT_LOCKED, + ); + }); + + it('should throw and emit error transfer cross chain event if balance is less than message fee', async () => { + when(tokenMethod.getAvailableBalance) + .calledWith(methodContext, existingNFT.owner, messageFeeTokenID) + .mockResolvedValue(messageFee - BigInt(10)); + + await expect( + method.transferCrossChain( + methodContext, + existingNFT.owner, + recipientAddress, + existingNFT.nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ), + ).rejects.toThrow('Insufficient balance for the message fee'); + + checkEventResult( + methodContext.eventQueue, + 1, + TransferCrossChainEvent, + 0, + { + senderAddress: existingNFT.owner, + recipientAddress, + receivingChainID, + nftID: existingNFT.nftID, + includeAttributes, + }, + NftEventResult.RESULT_INSUFFICIENT_BALANCE, + ); + }); + + it('should resolve if all params are valid', async () => { + jest.spyOn(internalMethod, 'transferCrossChain'); + when(tokenMethod.getAvailableBalance) + .calledWith(methodContext, existingNFT.owner, messageFeeTokenID) + .mockResolvedValue(messageFee + BigInt(10)); + + await expect( + method.transferCrossChain( + methodContext, + existingNFT.owner, + recipientAddress, + existingNFT.nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ), + ).resolves.toBeUndefined(); + expect(internalMethod['transferCrossChain']).toHaveBeenCalledWith( + methodContext, + existingNFT.owner, + recipientAddress, + existingNFT.nftID, + receivingChainID, + messageFee, + data, + includeAttributes, + ); + }); + }); + + describe('supportAllNFTs', () => { + it('should remove all existing entries, add ALL_SUPPORTED_NFTS_KEY entry and log AllNFTsSupportedEvent', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + await expect(method.supportAllNFTs(methodContext)).resolves.toBeUndefined(); + await expect( + supportedNFTsStore.has(methodContext, ALL_SUPPORTED_NFTS_KEY), + ).resolves.toBeTrue(); + + await expect(supportedNFTsStore.has(methodContext, chainID)).resolves.toBeFalse(); + + checkEventResult(methodContext.eventQueue, 1, AllNFTsSupportedEvent, 0, {}, null); + }); + + it('should not update SupportedNFTsStore if ALL_SUPPORTED_NFTS_KEY entry already exists', async () => { + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + await expect(method.supportAllNFTs(methodContext)).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + }); + + describe('removeSupportAllNFTs', () => { + it('should remove all existing entries and log AllNFTsSupportRemovedEvent', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + await expect(supportedNFTsStore.has(methodContext, chainID)).resolves.toBeTrue(); + await expect(method.removeSupportAllNFTs(methodContext)).resolves.toBeUndefined(); + await expect( + supportedNFTsStore.has(methodContext, ALL_SUPPORTED_NFTS_KEY), + ).resolves.toBeFalse(); + + await expect(supportedNFTsStore.has(methodContext, chainID)).resolves.toBeFalse(); + + checkEventResult(methodContext.eventQueue, 1, AllNFTsSupportRemovedEvent, 0, {}, null); + }); + + it('should remove all existing entries even if the ALL_SUPPORTED_NFTS_KEY entry exists', async () => { + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + await expect(method.removeSupportAllNFTs(methodContext)).resolves.toBeUndefined(); + await expect( + supportedNFTsStore.has(methodContext, ALL_SUPPORTED_NFTS_KEY), + ).resolves.toBeFalse(); + + checkEventResult(methodContext.eventQueue, 1, AllNFTsSupportRemovedEvent, 0, {}, null); + expect(methodContext.eventQueue.getEvents()).toHaveLength(1); + }); + }); + + describe('supportAllNFTsFromChain', () => { + it('should not update SupportedNFTsStore if provided chainID is equal to ownChainID', async () => { + await expect( + method.supportAllNFTsFromChain(methodContext, config.ownChainID), + ).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should not update SupportedNFTsStore if ALL_SUPPORTED_NFTS_KEY entry exists', async () => { + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + await expect( + method.supportAllNFTsFromChain(methodContext, utils.getRandomBytes(LENGTH_CHAIN_ID)), + ).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should not update SupportedNFTStore if all collections of provided chainID are already supported', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + await expect(method.supportAllNFTsFromChain(methodContext, chainID)).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should update SupportedNFTStore if provided chainID does not exist', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await expect(method.supportAllNFTsFromChain(methodContext, chainID)).resolves.toBeUndefined(); + + await expect(supportedNFTsStore.get(methodContext, chainID)).resolves.toEqual({ + supportedCollectionIDArray: [], + }); + + checkEventResult( + methodContext.eventQueue, + 1, + AllNFTsFromChainSupportedEvent, + 0, + { + chainID, + }, + null, + ); + }); + + it('should update SupportedNFTStore if provided chainID has supported collections', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID), + }, + ], + }); + + await expect(method.supportAllNFTsFromChain(methodContext, chainID)).resolves.toBeUndefined(); + + await expect(supportedNFTsStore.get(methodContext, chainID)).resolves.toEqual({ + supportedCollectionIDArray: [], + }); + + checkEventResult( + methodContext.eventQueue, + 1, + AllNFTsFromChainSupportedEvent, + 0, + { + chainID, + }, + null, + ); + }); + }); + + describe('removeSupportAllNFTsFromChain', () => { + it('should throw if provided chainID is equal to ownChainID', async () => { + await expect( + method.removeSupportAllNFTsFromChain(methodContext, config.ownChainID), + ).rejects.toThrow('Support for native NFTs cannot be removed'); + }); + + it('should throw if all NFTs are supported', async () => { + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + await expect( + method.removeSupportAllNFTsFromChain(methodContext, utils.getRandomBytes(LENGTH_CHAIN_ID)), + ).rejects.toThrow('All NFTs from all chains are supported'); + }); + + it('should not update Supported NFTs store if provided chain does not exist', async () => { + await expect( + method.removeSupportAllNFTsFromChain(methodContext, utils.getRandomBytes(LENGTH_CHAIN_ID)), + ).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should remove support for the provided chain and log AllNFTsFromChainSupportedEvent event', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + await expect( + method.removeSupportAllNFTsFromChain(methodContext, chainID), + ).resolves.toBeUndefined(); + + checkEventResult( + methodContext.eventQueue, + 1, + AllNFTsFromChainSupportRemovedEvent, + 0, + { + chainID, + }, + null, + ); + + await expect(supportedNFTsStore.has(methodContext, chainID)).resolves.toBeFalse(); + }); + }); + + describe('supportAllNFTsFromCollection', () => { + it('should not update SupportedNFTsStore if provided chainID is equal to ownChainID', async () => { + await expect( + method.supportAllNFTsFromCollection( + methodContext, + config.ownChainID, + utils.getRandomBytes(LENGTH_COLLECTION_ID), + ), + ).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should not update SupportedNFTsStore if all NFTs are supported', async () => { + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + await expect( + method.supportAllNFTsFromCollection( + methodContext, + utils.getRandomBytes(LENGTH_CHAIN_ID), + utils.getRandomBytes(LENGTH_COLLECTION_ID), + ), + ).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should not update SupportedNFTsStore if all collections of the provided chain are supported', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + await expect( + method.supportAllNFTsFromCollection( + methodContext, + chainID, + utils.getRandomBytes(LENGTH_COLLECTION_ID), + ), + ).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should not update SupportedNFTsStore if the provided collection is already supported for the provided chain', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const collectionID = utils.getRandomBytes(LENGTH_COLLECTION_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID, + }, + ], + }); + + await expect( + method.supportAllNFTsFromCollection(methodContext, chainID, collectionID), + ).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should add the collection to supported collections of the already supported chain lexicographically', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const collectionID = Buffer.alloc(LENGTH_COLLECTION_ID, 0); + const alreadySupportedCollection = Buffer.alloc(LENGTH_COLLECTION_ID, 1); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID: alreadySupportedCollection, + }, + ], + }); + + await expect( + method.supportAllNFTsFromCollection(methodContext, chainID, collectionID), + ).resolves.toBeUndefined(); + + const expectedSupportedCollectionIDArray = [ + { + collectionID, + }, + { + collectionID: alreadySupportedCollection, + }, + ]; + + await expect(supportedNFTsStore.get(methodContext, chainID)).resolves.toEqual({ + supportedCollectionIDArray: expectedSupportedCollectionIDArray, + }); + + checkEventResult( + methodContext.eventQueue, + 1, + AllNFTsFromCollectionSupportedEvent, + 0, + { + chainID, + collectionID, + }, + null, + ); + }); + + it('should support the provided collection for the provided chain', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const collectionID = utils.getRandomBytes(LENGTH_COLLECTION_ID); + + await expect( + method.supportAllNFTsFromCollection(methodContext, chainID, collectionID), + ).resolves.toBeUndefined(); + + await expect(supportedNFTsStore.get(methodContext, chainID)).resolves.toEqual({ + supportedCollectionIDArray: [{ collectionID }], + }); + + checkEventResult( + methodContext.eventQueue, + 1, + AllNFTsFromCollectionSupportedEvent, + 0, + { + chainID, + collectionID, + }, + null, + ); + }); + }); + + describe('removeSupportAllNFTsFromCollection', () => { + it('should not update SupportedNFTsStore if provided chainID is equal to ownChainID', async () => { + await expect( + method.removeSupportAllNFTsFromCollection( + methodContext, + config.ownChainID, + utils.getRandomBytes(LENGTH_CHAIN_ID), + ), + ).rejects.toThrow('Invalid operation. Support for native NFTs cannot be removed'); + }); + + it('should throw if all NFTs are supported', async () => { + await supportedNFTsStore.save(methodContext, ALL_SUPPORTED_NFTS_KEY, { + supportedCollectionIDArray: [], + }); + + await expect( + method.removeSupportAllNFTsFromCollection( + methodContext, + utils.getRandomBytes(LENGTH_CHAIN_ID), + utils.getRandomBytes(LENGTH_COLLECTION_ID), + ), + ).rejects.toThrow('All NFTs from all chains are supported'); + }); + + it('should throw if all NFTs for the specified chain are supported', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [], + }); + + await expect( + method.removeSupportAllNFTsFromCollection( + methodContext, + chainID, + utils.getRandomBytes(LENGTH_COLLECTION_ID), + ), + ).rejects.toThrow('All NFTs from the specified chain are supported'); + }); + + it('should not update SupportedNFTsStore if collection is not already supported', async () => { + await expect( + method.removeSupportAllNFTsFromCollection( + methodContext, + utils.getRandomBytes(LENGTH_CHAIN_ID), + utils.getRandomBytes(LENGTH_COLLECTION_ID), + ), + ).resolves.toBeUndefined(); + + expect(methodContext.eventQueue.getEvents()).toHaveLength(0); + }); + + it('should remove the support for provided collection and save the remaning supported collections lexicographically', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const collectionID = Buffer.alloc(LENGTH_CHAIN_ID, 5); + + const supportedCollectionIDArray = [ + { + collectionID: Buffer.alloc(LENGTH_CHAIN_ID, 3), + }, + { + collectionID, + }, + { + collectionID: Buffer.alloc(LENGTH_CHAIN_ID, 7), + }, + ]; + + const expectedSupportedCollectionIDArray = [ + { + collectionID: Buffer.alloc(LENGTH_CHAIN_ID, 3), + }, + { + collectionID: Buffer.alloc(LENGTH_CHAIN_ID, 7), + }, + ]; + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray, + }); + + await expect( + method.removeSupportAllNFTsFromCollection(methodContext, chainID, collectionID), + ).resolves.toBeUndefined(); + + await expect(supportedNFTsStore.get(methodContext, chainID)).resolves.toEqual({ + supportedCollectionIDArray: expectedSupportedCollectionIDArray, + }); + + checkEventResult( + methodContext.eventQueue, + 1, + AllNFTsFromCollectionSupportRemovedEvent, + 0, + { + collectionID, + chainID, + }, + null, + ); + }); + + it('should remove the entry for provided collection if the only supported collection is removed', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const collectionID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + await supportedNFTsStore.save(methodContext, chainID, { + supportedCollectionIDArray: [ + { + collectionID, + }, + ], + }); + + await expect( + method.removeSupportAllNFTsFromCollection(methodContext, chainID, collectionID), + ).resolves.toBeUndefined(); + + await expect(supportedNFTsStore.has(methodContext, chainID)).resolves.toBeFalse(); + + checkEventResult( + methodContext.eventQueue, + 1, + AllNFTsFromCollectionSupportRemovedEvent, + 0, + { + collectionID, + chainID, + }, + null, + ); + }); + }); + + describe('recover', () => { + const terminatedChainID = Buffer.alloc(LENGTH_CHAIN_ID, 8); + const substorePrefix = Buffer.from('0000', 'hex'); + const newNftID = Buffer.alloc(LENGTH_NFT_ID, 1); + const nft = codec.encode(nftStoreSchema, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray: [], + }); + + it('should throw and emit error recover event if substore prefix is not valid', async () => { + await expect( + method.recover(methodContext, terminatedChainID, Buffer.alloc(2, 2), nftID, nft), + ).rejects.toThrow('Invalid inputs'); + + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID, + }, + NftEventResult.RESULT_RECOVER_FAIL_INVALID_INPUTS, + ); + }); + + it('should throw and emit error recover event if NFT ID length is not valid', async () => { + const invalidNftID = utils.getRandomBytes(LENGTH_NFT_ID + 1); + + await expect( + method.recover(methodContext, terminatedChainID, substorePrefix, invalidNftID, nft), + ).rejects.toThrow('Invalid inputs'); + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID: invalidNftID, + }, + NftEventResult.RESULT_RECOVER_FAIL_INVALID_INPUTS, + ); + }); + + it('should throw and emit error recover event if NFT is not valid', async () => { + await expect( + method.recover( + methodContext, + terminatedChainID, + substorePrefix, + nftID, + Buffer.from('asfas'), + ), + ).rejects.toThrow('Invalid inputs'); + + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID, + }, + NftEventResult.RESULT_RECOVER_FAIL_INVALID_INPUTS, + ); + }); + + it('should throw and emit error recover event if module name length in attributes array is not valid', async () => { + const newStoreValue = codec.encode(nftStoreSchema, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray: [ + { module: 'customMod1', attributes: Buffer.alloc(5) }, + { module: '', attributes: Buffer.alloc(2) }, + ], + }); + + await expect( + method.recover(methodContext, terminatedChainID, substorePrefix, nftID, newStoreValue), + ).rejects.toThrow('Invalid inputs'); + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID, + }, + NftEventResult.RESULT_RECOVER_FAIL_INVALID_INPUTS, + ); + }); + + it('should throw and emit error recover event if nft chain id is not same as own chain id', async () => { + // ensure that random NFT is on a different chain than ownChainID + const randomNftID = Buffer.concat([ + Buffer.alloc(LENGTH_CHAIN_ID, 9), + utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID), + ]); + await nftStore.save(methodContext, randomNftID, { + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }); + + await expect( + method.recover(methodContext, terminatedChainID, substorePrefix, randomNftID, nft), + ).rejects.toThrow('Recovery called by a foreign chain'); + + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID: randomNftID, + }, + NftEventResult.RESULT_INITIATED_BY_NONNATIVE_CHAIN, + ); + }); + + it('should throw and emit error recover event if nft does not exist', async () => { + const unknownNftID = Buffer.concat([ + config.ownChainID, + utils.getRandomBytes(LENGTH_NFT_ID - LENGTH_CHAIN_ID), + ]); + + await expect( + method.recover(methodContext, terminatedChainID, substorePrefix, unknownNftID, nft), + ).rejects.toThrow('NFT substore entry does not exist'); + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID: unknownNftID, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + }); + + it('should throw and emit error recover event if nft is not escrowed to terminated chain', async () => { + await nftStore.save(methodContext, newNftID, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray: [], + }); + + await expect( + method.recover(methodContext, terminatedChainID, substorePrefix, newNftID, nft), + ).rejects.toThrow('NFT was not escrowed to terminated chain'); + + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID: newNftID, + }, + NftEventResult.RESULT_NFT_NOT_ESCROWED, + ); + }); + + it('should throw and emit error recover event if NFT owner length is invalid', async () => { + await nftStore.save(methodContext, newNftID, { + owner: terminatedChainID, + attributesArray: [], + }); + + await expect( + method.recover(methodContext, terminatedChainID, substorePrefix, newNftID, nft), + ).rejects.toThrow('Invalid account information'); + + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID: newNftID, + }, + NftEventResult.RESULT_INVALID_ACCOUNT, + ); + }); + + it('should set appropriate values to stores and resolve with emitting success recover event if params are valid', async () => { + const nftOwner = utils.getRandomBytes(LENGTH_ADDRESS); + const newNft = codec.encode(nftStoreSchema, { + owner: nftOwner, + attributesArray: [], + }); + await nftStore.save(methodContext, newNftID, { + owner: terminatedChainID, + attributesArray: [], + }); + jest.spyOn(internalMethod, 'createUserEntry'); + + await expect( + method.recover(methodContext, terminatedChainID, substorePrefix, newNftID, newNft), + ).resolves.toBeUndefined(); + + checkEventResult( + methodContext.eventQueue, + 1, + RecoverEvent, + 0, + { + terminatedChainID, + nftID: newNftID, + }, + NftEventResult.RESULT_SUCCESSFUL, + ); + const retrievedNft = await nftStore.get(methodContext, newNftID); + const escrowStore = module.stores.get(EscrowStore); + const escrowAccountExists = await escrowStore.has( + methodContext, + escrowStore.getKey(terminatedChainID, newNftID), + ); + expect(retrievedNft.owner).toStrictEqual(nftOwner); + expect(retrievedNft.attributesArray).toEqual([]); + expect(internalMethod['createUserEntry']).toHaveBeenCalledWith( + methodContext, + nftOwner, + newNftID, + ); + expect(escrowAccountExists).toBe(false); + }); + }); + + describe('setAttributes', () => { + it('should throw and log SetAttributesEvent if NFT does not exist', async () => { + const attributes = Buffer.alloc(9); + + await expect( + method.setAttributes(methodContext, module.name, nftID, attributes), + ).rejects.toThrow('NFT substore entry does not exist'); + + checkEventResult( + methodContext.eventQueue, + 1, + SetAttributesEvent, + 0, + { + nftID, + attributes, + }, + NftEventResult.RESULT_NFT_DOES_NOT_EXIST, + ); + }); + + it('should set attributes if NFT exists and no entry exists for the given module', async () => { + const attributes = Buffer.alloc(7); + + await expect( + method.setAttributes(methodContext, module.name, existingNFT.nftID, attributes), + ).resolves.toBeUndefined(); + + checkEventResult( + methodContext.eventQueue, + 1, + SetAttributesEvent, + 0, + { + nftID: existingNFT.nftID, + attributes, + }, + NftEventResult.RESULT_SUCCESSFUL, + ); + const storedNFT = await method.getNFT(methodContext, existingNFT.nftID); + const storedAttributes = storedNFT.attributesArray.find(a => a.module === module.name); + expect(storedAttributes?.attributes).toStrictEqual(attributes); + }); + + it('should update attributes if NFT exists and an entry already exists for the given module', async () => { + const newAttributes = Buffer.alloc(12); + const attributesArray1 = [ + { module: 'customMod1', attributes: Buffer.alloc(5) }, + { module: 'customMod2', attributes: Buffer.alloc(2) }, + ]; + await nftStore.save(methodContext, nftID, { + owner: utils.getRandomBytes(LENGTH_CHAIN_ID), + attributesArray: attributesArray1, + }); + + await expect( + method.setAttributes( + methodContext, + attributesArray1[0].module, + existingNFT.nftID, + newAttributes, + ), + ).resolves.toBeUndefined(); + + checkEventResult( + methodContext.eventQueue, + 1, + SetAttributesEvent, + 0, + { + nftID: existingNFT.nftID, + attributes: newAttributes, + }, + NftEventResult.RESULT_SUCCESSFUL, + ); + const storedNFT = await method.getNFT(methodContext, existingNFT.nftID); + const storedAttributes = storedNFT.attributesArray.find( + a => a.module === attributesArray1[0].module, + ); + expect(storedAttributes?.attributes).toStrictEqual(newAttributes); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/module.spec.ts b/framework/test/unit/modules/nft/module.spec.ts new file mode 100644 index 00000000000..bf791397b69 --- /dev/null +++ b/framework/test/unit/modules/nft/module.spec.ts @@ -0,0 +1,482 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils } from '@liskhq/lisk-cryptography'; +import { codec } from '@liskhq/lisk-codec'; +import { BlockAssets } from '@liskhq/lisk-chain'; +import { NFTModule } from '../../../../src/modules/nft/module'; +import { createGenesisBlockContext } from '../../../../src/testing'; +import { + invalidSchemaNFTSubstoreGenesisAssets, + invalidSchemaSupportedNFTsSubstoreGenesisAssets, + validData, +} from './init_genesis_state_fixtures'; +import { genesisNFTStoreSchema } from '../../../../src/modules/nft/schemas'; +import { + ALL_SUPPORTED_NFTS_KEY, + LENGTH_ADDRESS, + LENGTH_CHAIN_ID, + LENGTH_COLLECTION_ID, + LENGTH_NFT_ID, + MODULE_NAME_NFT, + NFT_NOT_LOCKED, +} from '../../../../src/modules/nft/constants'; +import { NFTStore } from '../../../../src/modules/nft/stores/nft'; +import { SupportedNFTsStore } from '../../../../src/modules/nft/stores/supported_nfts'; +import { UserStore } from '../../../../src/modules/nft/stores/user'; +import { EscrowStore } from '../../../../src/modules/nft/stores/escrow'; + +describe('nft module', () => { + const module = new NFTModule(); + + const nftStore = module.stores.get(NFTStore); + const userStore = module.stores.get(UserStore); + const escrowStore = module.stores.get(EscrowStore); + const supportedNFTsSubstore = module.stores.get(SupportedNFTsStore); + + const createGenesisBlockContextFromGenesisAssets = (genesisAssets: object) => { + const encodedAsset = codec.encode(genesisNFTStoreSchema, genesisAssets); + + const context = createGenesisBlockContext({ + assets: new BlockAssets([{ module: module.name, data: encodedAsset }]), + }).createInitGenesisStateContext(); + + return context; + }; + + it('should have the name "nft"', () => { + expect(module.name).toBe(MODULE_NAME_NFT); + }); + + describe('initGenesisState', () => { + describe('validate nftSubstore schema', () => { + it.each(invalidSchemaNFTSubstoreGenesisAssets)('%s', async (_, input, err) => { + if (typeof input === 'string') { + return; + } + + const encodedAsset = codec.encode(genesisNFTStoreSchema, input); + + const context = createGenesisBlockContext({ + assets: new BlockAssets([{ module: module.name, data: encodedAsset }]), + }).createInitGenesisStateContext(); + + await expect(module.initGenesisState(context)).rejects.toThrow(err as string); + }); + }); + + describe('validate supportedNFTsSubstore schema', () => { + it.each(invalidSchemaSupportedNFTsSubstoreGenesisAssets)('%s', async (_, input, err) => { + if (typeof input === 'string') { + return; + } + + const encodedAsset = codec.encode(genesisNFTStoreSchema, input); + + const context = createGenesisBlockContext({ + assets: new BlockAssets([{ module: module.name, data: encodedAsset }]), + }).createInitGenesisStateContext(); + + await expect(module.initGenesisState(context)).rejects.toThrow(err as string); + }); + }); + + it('should throw if owner of the NFT is not a valid address', async () => { + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + const genesisAssets = { + ...validData, + nftSubstore: [ + { + nftID, + owner: utils.getRandomBytes(LENGTH_ADDRESS - 1), + attributesArray: [], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).rejects.toThrow( + `nftID ${nftID.toString('hex')} has invalid owner`, + ); + }); + + it('should throw if owner of the NFT is not a valid chain', async () => { + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + const genesisAssets = { + ...validData, + nftSubstore: [ + { + nftID, + owner: utils.getRandomBytes(LENGTH_CHAIN_ID + 1), + attributesArray: [], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).rejects.toThrow( + `nftID ${nftID.toString('hex')} has invalid owner`, + ); + }); + + it('should throw if nftID is duplicated', async () => { + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + const genesisAssets = { + ...validData, + nftSubstore: [ + { + nftID, + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }, + { + nftID, + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).rejects.toThrow( + `nftID ${nftID.toString('hex')} duplicated`, + ); + }); + + it('should throw if NFT has duplicate attribute for a module', async () => { + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + const moduleName = 'pos'; + + const genesisAssets = { + ...validData, + nftSubstore: [ + { + nftID, + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [ + { + module: moduleName, + attributes: Buffer.alloc(10), + }, + { + module: moduleName, + attributes: Buffer.alloc(0), + }, + ], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).rejects.toThrow( + `nftID ${nftID.toString('hex')} has a duplicate attribute for pos module`, + ); + }); + + it('should throw if all NFTs are supported and SupportedNFTsSubstore contains more than one entry', async () => { + const genesisAssets = { + ...validData, + supportedNFTsSubstore: [ + { + chainID: Buffer.alloc(0), + supportedCollectionIDArray: [], + }, + { + chainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + supportedCollectionIDArray: [], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).rejects.toThrow( + 'SupportedNFTsSubstore should contain only one entry if all NFTs are supported', + ); + }); + + it('should throw if all NFTs are supported and supportedCollectionIDArray is not empty', async () => { + const genesisAssets = { + ...validData, + supportedNFTsSubstore: [ + { + chainID: ALL_SUPPORTED_NFTS_KEY, + supportedCollectionIDArray: [ + { + collectionID: utils.getRandomBytes(LENGTH_COLLECTION_ID), + }, + ], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).rejects.toThrow( + 'supportedCollectionIDArray must be empty if all NFTs are supported', + ); + }); + + it('should throw if supported chain is duplicated', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + const genesisAssets = { + ...validData, + supportedNFTsSubstore: [ + { + chainID, + supportedCollectionIDArray: [], + }, + { + chainID, + supportedCollectionIDArray: [], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).rejects.toThrow( + `chainID ${chainID.toString('hex')} duplicated`, + ); + }); + + it('should create NFTs, their corresponding user or escrow entries and supported chains', async () => { + const context = createGenesisBlockContextFromGenesisAssets(validData); + + await expect(module.initGenesisState(context)).resolves.toBeUndefined(); + + for (const nft of validData.nftSubstore) { + const { nftID, owner, attributesArray } = nft; + + await expect(nftStore.get(context.getMethodContext(), nftID)).resolves.toEqual({ + owner, + attributesArray, + }); + + if (owner.length === LENGTH_CHAIN_ID) { + await expect( + escrowStore.get(context.getMethodContext(), escrowStore.getKey(owner, nftID)), + ).resolves.toEqual({}); + } else { + await expect( + userStore.get(context.getMethodContext(), userStore.getKey(owner, nftID)), + ).resolves.toEqual({ + lockingModule: NFT_NOT_LOCKED, + }); + } + } + + for (const supportedChain of validData.supportedNFTsSubstore) { + const { chainID, supportedCollectionIDArray } = supportedChain; + + await expect( + supportedNFTsSubstore.get(context.getMethodContext(), chainID), + ).resolves.toEqual({ supportedCollectionIDArray }); + } + }); + + it('should create entries for all NFTs lexicographically', async () => { + const nftID1 = Buffer.alloc(LENGTH_NFT_ID, 1); + const nftID2 = Buffer.alloc(LENGTH_NFT_ID, 0); + + const genesisAssets = { + ...validData, + nftSubstore: [ + { + nftID: nftID1, + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }, + { + nftID: nftID2, + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).resolves.toBeUndefined(); + + const allNFTs = await nftStore.iterate(context.getMethodContext(), { + gte: Buffer.alloc(LENGTH_NFT_ID, 0), + lte: Buffer.alloc(LENGTH_NFT_ID, 255), + }); + + const expectedKeys = [nftID2, nftID1]; + + expect(expectedKeys).toEqual(allNFTs.map(nft => nft.key)); + }); + + it('should create entry for an NFT with attributesArray sorted lexicographically on module', async () => { + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + const genesisAssets = { + ...validData, + nftSubstore: [ + { + nftID, + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [ + { + module: 'token', + attributes: utils.getRandomBytes(10), + }, + { + module: 'pos', + attributes: utils.getRandomBytes(10), + }, + ], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).resolves.toBeUndefined(); + + const nft = await nftStore.get(context.getMethodContext(), nftID); + + expect(nft.attributesArray.map(attribute => attribute.module)).toEqual(['pos', 'token']); + }); + + it('should remove entries in attributes array with empty attributes', async () => { + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + const genesisAssets = { + ...validData, + nftSubstore: [ + { + nftID, + owner: utils.getRandomBytes(LENGTH_ADDRESS), + attributesArray: [ + { + module: 'token', + attributes: Buffer.alloc(0), + }, + ], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).resolves.toBeUndefined(); + + const nft = await nftStore.get(context.getMethodContext(), nftID); + + expect(nft.attributesArray).toHaveLength(0); + }); + + it('should create an entry for ALL_SUPPORTED_NFTS_KEY with empty supportedCollectionIDArray if all NFTs are supported', async () => { + const genesisAssets = { + ...validData, + supportedNFTsSubstore: [ + { + chainID: ALL_SUPPORTED_NFTS_KEY, + supportedCollectionIDArray: [], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).resolves.toBeUndefined(); + + const supportedNFTs = await supportedNFTsSubstore.get( + context.getMethodContext(), + ALL_SUPPORTED_NFTS_KEY, + ); + + expect(supportedNFTs.supportedCollectionIDArray).toHaveLength(0); + }); + + it('should create entries for supported chains lexicographically', async () => { + const chainID1 = Buffer.alloc(LENGTH_CHAIN_ID, 1); + const chainID2 = Buffer.alloc(LENGTH_CHAIN_ID, 0); + + const genesisAssets = { + ...validData, + supportedNFTsSubstore: [ + { + chainID: chainID1, + supportedCollectionIDArray: [], + }, + { + chainID: chainID2, + supportedCollectionIDArray: [], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).resolves.toBeUndefined(); + + const allSupportedNFTs = await supportedNFTsSubstore.getAll(context.getMethodContext()); + + const expectedKeys = [chainID2, chainID1]; + + expect(expectedKeys).toEqual(allSupportedNFTs.map(supportedNFTs => supportedNFTs.key)); + }); + + it('should create an entry for supported chains with supportedCollectionIDArray sorted lexicographically', async () => { + const chainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + + const collectionID1 = Buffer.alloc(LENGTH_COLLECTION_ID, 1); + const collectionID2 = Buffer.alloc(LENGTH_COLLECTION_ID, 0); + + const genesisAssets = { + ...validData, + supportedNFTsSubstore: [ + { + chainID, + supportedCollectionIDArray: [ + { + collectionID: collectionID1, + }, + { + collectionID: collectionID2, + }, + ], + }, + ], + }; + + const context = createGenesisBlockContextFromGenesisAssets(genesisAssets); + + await expect(module.initGenesisState(context)).resolves.toBeUndefined(); + + const supportedNFT = await supportedNFTsSubstore.get(context.getMethodContext(), chainID); + + expect(supportedNFT.supportedCollectionIDArray).toEqual([ + { + collectionID: collectionID2, + }, + { + collectionID: collectionID1, + }, + ]); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/stores/escrow.spec.ts b/framework/test/unit/modules/nft/stores/escrow.spec.ts new file mode 100644 index 00000000000..89d27e973af --- /dev/null +++ b/framework/test/unit/modules/nft/stores/escrow.spec.ts @@ -0,0 +1,36 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils } from '@liskhq/lisk-cryptography'; +import { EscrowStore } from '../../../../../src/modules/nft/stores/escrow'; +import { LENGTH_CHAIN_ID, LENGTH_NFT_ID } from '../../../../../src/modules/nft/constants'; + +describe('EscrowStore', () => { + let store: EscrowStore; + + beforeEach(() => { + store = new EscrowStore('NFT', 5); + }); + + describe('getKey', () => { + it('should concatenate the provided receivingChainID and nftID', () => { + const receivingChainID = utils.getRandomBytes(LENGTH_CHAIN_ID); + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + expect(store.getKey(receivingChainID, nftID)).toEqual( + Buffer.concat([receivingChainID, nftID]), + ); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/stores/nft.spec.ts b/framework/test/unit/modules/nft/stores/nft.spec.ts new file mode 100644 index 00000000000..7142bd787e1 --- /dev/null +++ b/framework/test/unit/modules/nft/stores/nft.spec.ts @@ -0,0 +1,106 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { NFTStore } from '../../../../../src/modules/nft/stores/nft'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { InMemoryPrefixedStateDB } from '../../../../../src/testing'; +import { createStoreGetter } from '../../../../../src/testing/utils'; +import { LENGTH_NFT_ID } from '../../../../../src/modules/nft/constants'; +import { StoreGetter } from '../../../../../src'; + +describe('NFTStore', () => { + let store: NFTStore; + let context: StoreGetter; + + beforeEach(() => { + store = new NFTStore('NFT', 5); + + const db = new InMemoryPrefixedStateDB(); + const stateStore = new PrefixedStateReadWriter(db); + + context = createStoreGetter(stateStore); + }); + + describe('save', () => { + it('should order NFTs of an owner by module', async () => { + const nftID = Buffer.alloc(LENGTH_NFT_ID, 0); + const owner = Buffer.alloc(8, 1); + + const unsortedAttributesArray = [ + { + module: 'token', + attributes: Buffer.alloc(8, 1), + }, + { + module: 'pos', + attributes: Buffer.alloc(8, 1), + }, + ]; + + const sortedAttributesArray = [ + { + module: 'pos', + attributes: Buffer.alloc(8, 1), + }, + { + module: 'token', + attributes: Buffer.alloc(8, 1), + }, + ]; + + await store.save(context, nftID, { + owner, + attributesArray: unsortedAttributesArray, + }); + + await expect(store.get(context, nftID)).resolves.toEqual({ + owner, + attributesArray: sortedAttributesArray, + }); + }); + + it('should remove modules with no attributes array', async () => { + const nftID = Buffer.alloc(LENGTH_NFT_ID, 0); + const owner = Buffer.alloc(8, 1); + + const attributesArray = [ + { + module: 'nft', + attributes: Buffer.alloc(0), + }, + { + module: 'pos', + attributes: Buffer.alloc(8, 1), + }, + ]; + + const filteredAttributesArray = [ + { + module: 'pos', + attributes: Buffer.alloc(8, 1), + }, + ]; + + await store.save(context, nftID, { + owner, + attributesArray, + }); + + await expect(store.get(context, nftID)).resolves.toEqual({ + owner, + attributesArray: filteredAttributesArray, + }); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/stores/supported_nfts.spec.ts b/framework/test/unit/modules/nft/stores/supported_nfts.spec.ts new file mode 100644 index 00000000000..054ad566eaa --- /dev/null +++ b/framework/test/unit/modules/nft/stores/supported_nfts.spec.ts @@ -0,0 +1,86 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils } from '@liskhq/lisk-cryptography'; +import { SupportedNFTsStore } from '../../../../../src/modules/nft/stores/supported_nfts'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { InMemoryPrefixedStateDB } from '../../../../../src/testing'; +import { createStoreGetter } from '../../../../../src/testing/utils'; +import { LENGTH_CHAIN_ID, LENGTH_COLLECTION_ID } from '../../../../../src/modules/nft/constants'; +import { CHAIN_ID_LENGTH, StoreGetter } from '../../../../../src'; + +describe('NFTStore', () => { + let store: SupportedNFTsStore; + let context: StoreGetter; + + beforeEach(() => { + store = new SupportedNFTsStore('NFT', 5); + + const db = new InMemoryPrefixedStateDB(); + const stateStore = new PrefixedStateReadWriter(db); + + context = createStoreGetter(stateStore); + }); + + describe('save', () => { + it('should order supported NFT collection of a chain', async () => { + const chainID = Buffer.alloc(CHAIN_ID_LENGTH, 0); + + const unsortedSupportedCollections = [ + { + collectionID: Buffer.alloc(LENGTH_COLLECTION_ID, 1), + }, + { + collectionID: Buffer.alloc(LENGTH_COLLECTION_ID, 0), + }, + { + collectionID: Buffer.from([0, 1, 1, 0]), + }, + ]; + + const sortedSupportedCollections = unsortedSupportedCollections.sort((a, b) => + a.collectionID.compare(b.collectionID), + ); + + const data = { + supportedCollectionIDArray: unsortedSupportedCollections, + }; + await store.save(context, chainID, data); + + await expect(store.get(context, chainID)).resolves.toEqual({ + supportedCollectionIDArray: sortedSupportedCollections, + }); + }); + }); + + describe('getAll', () => { + it('should retrieve all NFTs with key between 0 and maximum value for Buffer of length LENGTH_CHAIN_ID', async () => { + await store.save(context, Buffer.alloc(LENGTH_CHAIN_ID, 0), { + supportedCollectionIDArray: [], + }); + + await store.save(context, Buffer.alloc(LENGTH_CHAIN_ID, 1), { + supportedCollectionIDArray: [], + }); + + await store.save(context, utils.getRandomBytes(LENGTH_CHAIN_ID), { + supportedCollectionIDArray: [], + }); + + const allSupportedNFTs = await store.getAll(context); + + expect([...allSupportedNFTs.keys()]).toHaveLength(3); + }); + }); +}); diff --git a/framework/test/unit/modules/nft/stores/user.spec.ts b/framework/test/unit/modules/nft/stores/user.spec.ts new file mode 100644 index 00000000000..e16fcc3f52a --- /dev/null +++ b/framework/test/unit/modules/nft/stores/user.spec.ts @@ -0,0 +1,34 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils } from '@liskhq/lisk-cryptography'; +import { UserStore } from '../../../../../src/modules/nft/stores/user'; +import { LENGTH_ADDRESS, LENGTH_NFT_ID } from '../../../../../src/modules/nft/constants'; + +describe('UserStore', () => { + let store: UserStore; + + beforeEach(() => { + store = new UserStore('NFT', 5); + }); + + describe('getKey', () => { + it('should concatenate the provided address and nftID', () => { + const address = utils.getRandomBytes(LENGTH_ADDRESS); + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); + + expect(store.getKey(address, nftID)).toEqual(Buffer.concat([address, nftID])); + }); + }); +}); diff --git a/framework/test/unit/modules/poa/commands/register_authority.spec.ts b/framework/test/unit/modules/poa/commands/register_authority.spec.ts new file mode 100644 index 00000000000..5e5eff50e41 --- /dev/null +++ b/framework/test/unit/modules/poa/commands/register_authority.spec.ts @@ -0,0 +1,277 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { validator } from '@liskhq/lisk-validator'; +import { address, utils } from '@liskhq/lisk-cryptography'; +import { TransactionAttrs } from '@liskhq/lisk-chain'; +import { codec } from '@liskhq/lisk-codec'; +import * as testing from '../../../../../src/testing'; +import { + CommandExecuteContext, + CommandVerifyContext, + Transaction, + VerifyStatus, + PoAModule, +} from '../../../../../src'; +import { RegisterAuthorityCommand } from '../../../../../src/modules/poa/commands/register_authority'; +import { + COMMAND_REGISTER_AUTHORITY, + AUTHORITY_REGISTRATION_FEE, + LENGTH_BLS_KEY, + LENGTH_PROOF_OF_POSSESSION, + LENGTH_GENERATOR_KEY, + MODULE_NAME_POA, + POA_VALIDATOR_NAME_REGEX, + MAX_LENGTH_NAME, +} from '../../../../../src/modules/poa/constants'; + +import { registerAuthoritySchema } from '../../../../../src/modules/poa/schemas'; +import { RegisterAuthorityParams, ValidatorsMethod } from '../../../../../src/modules/poa/types'; + +import { createStoreGetter } from '../../../../../src/testing/utils'; +import { NameStore, ValidatorStore } from '../../../../../src/modules/poa/stores'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { InMemoryPrefixedStateDB } from '../../../../../src/testing'; +import { ED25519_PUBLIC_KEY_LENGTH } from '../../../../../src/modules/validators/constants'; + +describe('RegisterAuthority', () => { + const poaModule = new PoAModule(); + let registerAuthorityCommand: RegisterAuthorityCommand; + let mockValidatorsMethod: ValidatorsMethod; + let mockFeeMethod: any; + let stateStore: PrefixedStateReadWriter; + let validatorStore: ValidatorStore; + let nameStore: NameStore; + + const registerAuthorityTransactionParams = { + name: 'max', + blsKey: utils.getRandomBytes(LENGTH_BLS_KEY), + proofOfPossession: utils.getRandomBytes(LENGTH_PROOF_OF_POSSESSION), + generatorKey: utils.getRandomBytes(LENGTH_GENERATOR_KEY), + }; + + const publicKey = utils.getRandomBytes(ED25519_PUBLIC_KEY_LENGTH); + const chainID = Buffer.from([0, 0, 0, 1]); + + const buildTransaction = (transaction: Partial): Transaction => { + return new Transaction({ + module: transaction.module ?? MODULE_NAME_POA, + command: transaction.command ?? COMMAND_REGISTER_AUTHORITY, + senderPublicKey: transaction.senderPublicKey ?? publicKey, + nonce: transaction.nonce ?? BigInt(0), + fee: transaction.fee ?? AUTHORITY_REGISTRATION_FEE, + params: + transaction.params ?? + codec.encode(registerAuthoritySchema, registerAuthorityTransactionParams), + signatures: transaction.signatures ?? [publicKey], + }); + }; + + beforeEach(async () => { + registerAuthorityCommand = new RegisterAuthorityCommand(poaModule.stores, poaModule.events); + mockValidatorsMethod = { + setValidatorGeneratorKey: jest.fn(), + registerValidatorKeys: jest.fn(), + registerValidatorWithoutBLSKey: jest.fn(), + getValidatorKeys: jest.fn(), + getGeneratorsBetweenTimestamps: jest.fn(), + setValidatorsParams: jest.fn(), + }; + mockFeeMethod = { + payFee: jest.fn(), + }; + (poaModule as any)['_registerAuthorityCommand'] = registerAuthorityCommand; + await poaModule.init({ + genesisConfig: {} as any, + moduleConfig: { authorityRegistrationFee: AUTHORITY_REGISTRATION_FEE.toString() }, + }); + registerAuthorityCommand.addDependencies(mockValidatorsMethod, mockFeeMethod); + + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + validatorStore = poaModule.stores.get(ValidatorStore); + nameStore = poaModule.stores.get(NameStore); + }); + + describe('verifySchema', () => { + it(`should throw error when name is longer than ${MAX_LENGTH_NAME}`, () => { + expect(() => + validator.validate(registerAuthorityCommand.schema, { + ...registerAuthorityTransactionParams, + name: 'aaaaaaaaaaaaaaaaaaaaaaa', + }), + ).toThrow(`Property '.name' must NOT have more than 20 characters`); + }); + + it(`should throw error when bls key shorter than ${LENGTH_BLS_KEY}`, () => { + expect(() => + validator.validate(registerAuthorityCommand.schema, { + ...registerAuthorityTransactionParams, + blsKey: utils.getRandomBytes(LENGTH_BLS_KEY - 1), + }), + ).toThrow(`Property '.blsKey' minLength not satisfied`); + }); + + it(`should throw error when bls key longer than ${LENGTH_BLS_KEY}`, () => { + expect(() => + validator.validate(registerAuthorityCommand.schema, { + ...registerAuthorityTransactionParams, + blsKey: utils.getRandomBytes(LENGTH_BLS_KEY + 1), + }), + ).toThrow(`Property '.blsKey' maxLength exceeded`); + }); + + it(`should throw error when proof of possession shorter than ${LENGTH_PROOF_OF_POSSESSION}`, () => { + expect(() => + validator.validate(registerAuthorityCommand.schema, { + ...registerAuthorityTransactionParams, + proofOfPossession: utils.getRandomBytes(LENGTH_PROOF_OF_POSSESSION - 1), + }), + ).toThrow(`Property '.proofOfPossession' minLength not satisfied`); + }); + + it(`should throw error when proof of possession longer than ${LENGTH_PROOF_OF_POSSESSION}`, () => { + expect(() => + validator.validate(registerAuthorityCommand.schema, { + ...registerAuthorityTransactionParams, + proofOfPossession: utils.getRandomBytes(LENGTH_PROOF_OF_POSSESSION + 1), + }), + ).toThrow(`Property '.proofOfPossession' maxLength exceeded`); + }); + + it(`should throw error when generator key shorter than ${LENGTH_GENERATOR_KEY}`, () => { + expect(() => + validator.validate(registerAuthorityCommand.schema, { + ...registerAuthorityTransactionParams, + generatorKey: utils.getRandomBytes(LENGTH_GENERATOR_KEY - 1), + }), + ).toThrow(`Property '.generatorKey' minLength not satisfied`); + }); + + it(`should throw error when generator key longer than ${LENGTH_GENERATOR_KEY}`, () => { + expect(() => + validator.validate(registerAuthorityCommand.schema, { + ...registerAuthorityTransactionParams, + generatorKey: utils.getRandomBytes(LENGTH_GENERATOR_KEY + 1), + }), + ).toThrow(`Property '.generatorKey' maxLength exceeded`); + }); + }); + + describe('verify', () => { + let context: CommandVerifyContext; + beforeEach(() => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({}), + chainID, + }) + .createCommandVerifyContext(registerAuthoritySchema); + }); + + it('should return error when name does not comply regex', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: codec.encode(registerAuthoritySchema, { + ...registerAuthorityTransactionParams, + name: '###', + }), + }), + chainID, + }) + .createCommandVerifyContext(registerAuthoritySchema); + + await expect(registerAuthorityCommand.verify(context)).rejects.toThrow( + `Name does not comply with format ${POA_VALIDATOR_NAME_REGEX.toString()}.`, + ); + }); + + it('should return error when name already exist', async () => { + await nameStore.set( + createStoreGetter(stateStore), + Buffer.from(registerAuthorityTransactionParams.name), + { + address: address.getAddressFromPublicKey(context.transaction.senderPublicKey), + }, + ); + + await expect(registerAuthorityCommand.verify(context)).rejects.toThrow( + 'Name already exists.', + ); + }); + + it('should return error when senderAddress already exist', async () => { + await validatorStore.set( + createStoreGetter(stateStore), + address.getAddressFromPublicKey(publicKey), + { + name: registerAuthorityTransactionParams.name, + }, + ); + + await expect(registerAuthorityCommand.verify(context)).rejects.toThrow( + 'Validator already exists.', + ); + }); + + it('should return OK when transaction is valid', async () => { + const result = await registerAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.OK); + }); + }); + + describe('execute', () => { + let context: CommandExecuteContext; + beforeEach(() => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({}), + chainID, + }) + .createCommandExecuteContext(registerAuthoritySchema); + }); + + it('should call registerValidatorKeys', async () => { + await registerAuthorityCommand.execute(context); + + expect(mockFeeMethod.payFee).toHaveBeenCalledWith( + expect.anything(), + AUTHORITY_REGISTRATION_FEE, + ); + await expect( + validatorStore.has( + createStoreGetter(stateStore), + address.getAddressFromPublicKey(publicKey), + ), + ).resolves.toBe(true); + await expect( + nameStore.has( + createStoreGetter(stateStore), + Buffer.from(registerAuthorityTransactionParams.name), + ), + ).resolves.toBe(true); + expect(mockValidatorsMethod.registerValidatorKeys).toHaveBeenCalledWith( + expect.anything(), + address.getAddressFromPublicKey(publicKey), + context.params.blsKey, + context.params.generatorKey, + context.params.proofOfPossession, + ); + }); + }); +}); diff --git a/framework/test/unit/modules/poa/commands/update_authority.spec.ts b/framework/test/unit/modules/poa/commands/update_authority.spec.ts new file mode 100644 index 00000000000..36a57ff7bd9 --- /dev/null +++ b/framework/test/unit/modules/poa/commands/update_authority.spec.ts @@ -0,0 +1,475 @@ +import { bls, utils } from '@liskhq/lisk-cryptography'; +import { codec } from '@liskhq/lisk-codec'; +import { TransactionAttrs } from '@liskhq/lisk-chain'; +import { MAX_UINT64, validator } from '@liskhq/lisk-validator'; +import { + CommandExecuteContext, + CommandVerifyContext, + MAX_NUM_VALIDATORS, + PoAModule, + Transaction, + VerifyStatus, +} from '../../../../../src'; +import { UpdateAuthorityCommand } from '../../../../../src/modules/poa/commands/update_authority'; +import { UpdateAuthorityParams, ValidatorsMethod } from '../../../../../src/modules/poa/types'; +import { + AUTHORITY_REGISTRATION_FEE, + COMMAND_UPDATE_AUTHORITY, + EMPTY_BYTES, + KEY_SNAPSHOT_0, + KEY_SNAPSHOT_2, + MODULE_NAME_POA, + UpdateAuthorityResult, +} from '../../../../../src/modules/poa/constants'; +import { updateAuthoritySchema } from '../../../../../src/modules/poa/schemas'; +import * as testing from '../../../../../src/testing'; +import { InMemoryPrefixedStateDB } from '../../../../../src/testing'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { + ChainPropertiesStore, + SnapshotStore, + ValidatorStore, +} from '../../../../../src/modules/poa/stores'; +import { createStoreGetter } from '../../../../../src/testing/utils'; +import { AuthorityUpdateEvent } from '../../../../../src/modules/poa/events/authority_update'; +import { EventQueue } from '../../../../../src/state_machine'; +import { ED25519_PUBLIC_KEY_LENGTH } from '../../../../../src/modules/validators/constants'; + +describe('UpdateAuthority', () => { + const poaModule = new PoAModule(); + let updateAuthorityCommand: UpdateAuthorityCommand; + let mockValidatorsMethod: ValidatorsMethod; + let stateStore: PrefixedStateReadWriter; + let validatorStore: ValidatorStore; + let chainPropertiesStore: ChainPropertiesStore; + let snapshotStore: SnapshotStore; + + const address0 = Buffer.from('0000000000000000000000000000000000000000', 'hex'); + const address1 = Buffer.from('0000000000000000000000000000000000000001', 'hex'); + const address2 = Buffer.from('0000000000000000000000000000000000000002', 'hex'); + + const updateAuthorityValidatorParams: UpdateAuthorityParams = { + newValidators: [ + { + address: address0, + weight: BigInt(40), + }, + { + address: address1, + weight: BigInt(40), + }, + ], + threshold: BigInt(68), + validatorsUpdateNonce: 0, + signature: utils.getRandomBytes(64), + aggregationBits: Buffer.from([0]), + }; + + const buildUpdateAuthorityValidatorParams = (params: Partial): Buffer => + codec.encode(updateAuthoritySchema, { + newValidators: params.newValidators ?? updateAuthorityValidatorParams.newValidators, + threshold: params.threshold ?? updateAuthorityValidatorParams.threshold, + validatorsUpdateNonce: + params.validatorsUpdateNonce ?? updateAuthorityValidatorParams.validatorsUpdateNonce, + signature: params.signature ?? updateAuthorityValidatorParams.signature, + aggregationBits: params.aggregationBits ?? updateAuthorityValidatorParams.aggregationBits, + }); + + const publicKey = utils.getRandomBytes(ED25519_PUBLIC_KEY_LENGTH); + const chainID = Buffer.from([0, 0, 0, 1]); + + const buildTransaction = (transaction: Partial): Transaction => { + return new Transaction({ + module: transaction.module ?? MODULE_NAME_POA, + command: transaction.command ?? COMMAND_UPDATE_AUTHORITY, + senderPublicKey: transaction.senderPublicKey ?? publicKey, + nonce: transaction.nonce ?? BigInt(0), + fee: transaction.fee ?? AUTHORITY_REGISTRATION_FEE, + params: + transaction.params ?? codec.encode(updateAuthoritySchema, updateAuthorityValidatorParams), + signatures: transaction.signatures ?? [publicKey], + }); + }; + + beforeEach(async () => { + updateAuthorityCommand = new UpdateAuthorityCommand(poaModule.stores, poaModule.events); + mockValidatorsMethod = { + setValidatorGeneratorKey: jest.fn(), + registerValidatorKeys: jest.fn(), + registerValidatorWithoutBLSKey: jest.fn(), + getValidatorKeys: jest.fn(), + getGeneratorsBetweenTimestamps: jest.fn(), + setValidatorsParams: jest.fn(), + }; + updateAuthorityCommand.addDependencies(mockValidatorsMethod); + + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + validatorStore = poaModule.stores.get(ValidatorStore); + chainPropertiesStore = poaModule.stores.get(ChainPropertiesStore); + snapshotStore = poaModule.stores.get(SnapshotStore); + + await validatorStore.set(createStoreGetter(stateStore), address0, { + name: 'validator0', + }); + await validatorStore.set(createStoreGetter(stateStore), address1, { + name: 'validator1', + }); + await chainPropertiesStore.set(createStoreGetter(stateStore), EMPTY_BYTES, { + roundEndHeight: 0, + validatorsUpdateNonce: 0, + }); + }); + + describe('verifySchema', () => { + it('should throw error when length of newValidators is less than 1', () => { + expect(() => + validator.validate(updateAuthorityCommand.schema, { + ...updateAuthorityValidatorParams, + newValidators: [], + }), + ).toThrow('must NOT have fewer than 1 items'); + }); + + it('should throw error when length of newValidators is greater than MAX_NUM_VALIDATORS', () => { + expect(() => + validator.validate(updateAuthorityCommand.schema, { + ...updateAuthorityValidatorParams, + newValidators: Array.from(Array(MAX_NUM_VALIDATORS + 1).keys()).map(_ => ({ + address: utils.getRandomBytes(20), + weight: BigInt(1), + })), + }), + ).toThrow(`must NOT have more than ${MAX_NUM_VALIDATORS} items`); + }); + }); + + describe('verify', () => { + let context: CommandVerifyContext; + beforeEach(() => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({}), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + }); + + it('should return error when newValidators are not lexicographically ordered', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + newValidators: [ + { + address: address1, + weight: BigInt(1), + }, + { + address: address0, + weight: BigInt(1), + }, + ], + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude( + `Addresses in newValidators are not lexicographically ordered.`, + ); + }); + + it('should return error when addresses are in newValidators are not unique', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + newValidators: [ + { + address: address0, + weight: BigInt(1), + }, + { + address: address1, + weight: BigInt(1), + }, + { + address: address1, + weight: BigInt(1), + }, + ], + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude(`Addresses in newValidators are not unique.`); + }); + + it('should return error when validator is not in ValidatorStore', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + newValidators: [ + ...updateAuthorityValidatorParams.newValidators, + { + address: address2, + weight: BigInt(2), + }, + ], + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude( + `No validator found for given address ${address2.toString('hex')}.`, + ); + }); + + it('should return error when validator weight is zero', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + newValidators: [ + { + address: address0, + weight: BigInt(0), + }, + { + address: address1, + weight: BigInt(1), + }, + ], + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude(`Validator weight cannot be zero.`); + }); + + it('should return error when totalWeight is zero', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + newValidators: [ + { + address: address0, + weight: BigInt(0), + }, + { + address: address1, + weight: BigInt(0), + }, + ], + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude(`Validator weight cannot be zero.`); + }); + + it('should return error when totalWeight is greater than MAX_UINT64', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + newValidators: [ + { + address: address0, + weight: BigInt(MAX_UINT64), + }, + { + address: address1, + weight: BigInt(1), + }, + ], + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude(`Validators total weight exceeds ${MAX_UINT64}`); + }); + + it('should return error when trsParams.threshold is less than (totalWeight / 3) + 1 ', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + threshold: BigInt(20), + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + + const totalWeight = updateAuthorityValidatorParams.newValidators.reduce( + (acc, v) => acc + v.weight, + BigInt(0), + ); + const minThreshold = totalWeight / BigInt(3) + BigInt(1); + + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude( + `Threshold must be between ${minThreshold} and ${totalWeight} (inclusive).`, + ); + }); + + it('should return error when trsParams.threshold is greater than totalWeight', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + threshold: BigInt(81), + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + + const totalWeight = updateAuthorityValidatorParams.newValidators.reduce( + (acc, v) => acc + v.weight, + BigInt(0), + ); + const minThreshold = totalWeight / BigInt(3) + BigInt(1); + + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude( + `Threshold must be between ${minThreshold} and ${totalWeight}`, + ); + }); + + it('should return error when trsParams.validatorsUpdateNonce does not equal to chainProperties.validatorsUpdateNonce', async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({ + params: buildUpdateAuthorityValidatorParams({ + validatorsUpdateNonce: 1, + }), + }), + chainID, + }) + .createCommandVerifyContext(updateAuthoritySchema); + + const chainProperties = await chainPropertiesStore.get(context, EMPTY_BYTES); + + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.FAIL); + expect(result.error?.message).toInclude( + `validatorsUpdateNonce must be equal to ${chainProperties.validatorsUpdateNonce}.`, + ); + }); + + it('should return OK when transaction is valid', async () => { + const result = await updateAuthorityCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.OK); + }); + }); + + describe('execute', () => { + let context: CommandExecuteContext; + + const checkEventResult = ( + eventQueue: EventQueue, + BaseEvent: any, + expectedResult: UpdateAuthorityResult, + length = 1, + index = 0, + ) => { + expect(eventQueue.getEvents()).toHaveLength(length); + expect(eventQueue.getEvents()[index].toObject().name).toEqual(new BaseEvent('token').name); + expect( + codec.decode>( + new BaseEvent('token').schema, + eventQueue.getEvents()[index].toObject().data, + ).result, + ).toEqual(expectedResult); + }; + beforeEach(async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({}), + chainID, + }) + .createCommandExecuteContext(updateAuthoritySchema); + + await snapshotStore.set(createStoreGetter(stateStore), KEY_SNAPSHOT_0, { + validators: [], + threshold: BigInt(0), + }); + }); + + it('should emit event and throw error when verifyWeightedAggSig failed', async () => { + jest.spyOn(bls, 'verifyWeightedAggSig').mockReturnValue(false); + + await expect(updateAuthorityCommand.execute(context)).rejects.toThrow( + 'Invalid weighted aggregated signature.', + ); + + checkEventResult( + context.eventQueue, + AuthorityUpdateEvent, + UpdateAuthorityResult.FAIL_INVALID_SIGNATURE, + ); + }); + + it('should increase stores (snapshotStore2 & chainProperties) and emit event when verifyWeightedAggSig is true', async () => { + jest.spyOn(bls, 'verifyWeightedAggSig').mockReturnValue(true); + + await updateAuthorityCommand.execute(context); + + expect(await snapshotStore.get(context, KEY_SNAPSHOT_2)).toStrictEqual({ + validators: updateAuthorityValidatorParams.newValidators, + threshold: updateAuthorityValidatorParams.threshold, + }); + expect(await chainPropertiesStore.get(context, EMPTY_BYTES)).toStrictEqual({ + roundEndHeight: 0, + validatorsUpdateNonce: 1, + }); + + checkEventResult(context.eventQueue, AuthorityUpdateEvent, UpdateAuthorityResult.SUCCESS); + }); + }); +}); diff --git a/framework/test/unit/modules/poa/commands/update_generator_key.spec.ts b/framework/test/unit/modules/poa/commands/update_generator_key.spec.ts new file mode 100644 index 00000000000..a435a7c2d47 --- /dev/null +++ b/framework/test/unit/modules/poa/commands/update_generator_key.spec.ts @@ -0,0 +1,163 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { validator } from '@liskhq/lisk-validator'; +import { codec } from '@liskhq/lisk-codec'; +import { TransactionAttrs } from '@liskhq/lisk-chain'; +import { utils, address } from '@liskhq/lisk-cryptography'; + +import { + PoAModule, + Transaction, + CommandVerifyContext, + CommandExecuteContext, + VerifyStatus, +} from '../../../../../src'; +import { UpdateGeneratorKeyParams, ValidatorsMethod } from '../../../../../src/modules/poa/types'; +import { ValidatorStore } from '../../../../../src/modules/poa/stores'; +import { + AUTHORITY_REGISTRATION_FEE, + COMMAND_UPDATE_KEY, + LENGTH_GENERATOR_KEY, + MODULE_NAME_POA, +} from '../../../../../src/modules/poa/constants'; +import { updateGeneratorKeySchema } from '../../../../../src/modules/poa/schemas'; +import { PrefixedStateReadWriter } from '../../../../../src/state_machine/prefixed_state_read_writer'; +import { InMemoryPrefixedStateDB } from '../../../../../src/testing'; +import * as testing from '../../../../../src/testing'; +import { UpdateGeneratorKeyCommand } from '../../../../../src/modules/poa/commands/update_generator_key'; +import { createStoreGetter } from '../../../../../src/testing/utils'; + +describe('UpdateGeneratorKey', () => { + const poaModule = new PoAModule(); + let updateGeneratorKeyCommand: UpdateGeneratorKeyCommand; + let stateStore: PrefixedStateReadWriter; + let mockValidatorsMethod: ValidatorsMethod; + let validatorStore: ValidatorStore; + + const publicKey = utils.getRandomBytes(32); + const chainID = Buffer.from([0, 0, 0, 1]); + + const updateGeneratorKeyParams: UpdateGeneratorKeyParams = { + generatorKey: utils.getRandomBytes(LENGTH_GENERATOR_KEY), + }; + + const buildTransaction = (transaction: Partial): Transaction => { + return new Transaction({ + module: transaction.module ?? MODULE_NAME_POA, + command: transaction.command ?? COMMAND_UPDATE_KEY, + senderPublicKey: transaction.senderPublicKey ?? publicKey, + nonce: transaction.nonce ?? BigInt(0), + fee: transaction.fee ?? AUTHORITY_REGISTRATION_FEE, + params: + transaction.params ?? codec.encode(updateGeneratorKeySchema, updateGeneratorKeyParams), + signatures: transaction.signatures ?? [publicKey], + }); + }; + + beforeEach(async () => { + updateGeneratorKeyCommand = new UpdateGeneratorKeyCommand(poaModule.stores, poaModule.events); + mockValidatorsMethod = { + setValidatorGeneratorKey: jest.fn(), + registerValidatorKeys: jest.fn(), + registerValidatorWithoutBLSKey: jest.fn(), + getValidatorKeys: jest.fn(), + getGeneratorsBetweenTimestamps: jest.fn(), + setValidatorsParams: jest.fn(), + }; + updateGeneratorKeyCommand.addDependencies(mockValidatorsMethod); + + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + validatorStore = poaModule.stores.get(ValidatorStore); + + await validatorStore.set( + createStoreGetter(stateStore), + address.getAddressFromPublicKey(publicKey), + { + name: 'validator', + }, + ); + }); + + describe('verifySchema', () => { + it(`should throw error when generator key shorter than ${LENGTH_GENERATOR_KEY}`, () => { + expect(() => + validator.validate(updateGeneratorKeyCommand.schema, { + generatorKey: utils.getRandomBytes(LENGTH_GENERATOR_KEY - 1), + }), + ).toThrow(`Property '.generatorKey' minLength not satisfied`); + }); + + it(`should throw error when generator key longer than ${LENGTH_GENERATOR_KEY}`, () => { + expect(() => + validator.validate(updateGeneratorKeyCommand.schema, { + generatorKey: utils.getRandomBytes(LENGTH_GENERATOR_KEY + 1), + }), + ).toThrow(`Property '.generatorKey' maxLength exceeded`); + }); + }); + + describe('verify', () => { + let context: CommandVerifyContext; + beforeEach(() => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({}), + chainID, + }) + .createCommandVerifyContext(updateGeneratorKeySchema); + }); + + it('should return error when validator not exist', async () => { + await validatorStore.del( + createStoreGetter(stateStore), + address.getAddressFromPublicKey(publicKey), + ); + + await expect(updateGeneratorKeyCommand.verify(context)).rejects.toThrow( + 'Validator does not exist.', + ); + }); + + it('should return OK when transaction is valid', async () => { + const result = await updateGeneratorKeyCommand.verify(context); + + expect(result.status).toBe(VerifyStatus.OK); + }); + }); + + describe('execute', () => { + let context: CommandExecuteContext; + beforeEach(async () => { + context = testing + .createTransactionContext({ + stateStore, + transaction: buildTransaction({}), + chainID, + }) + .createCommandExecuteContext(updateGeneratorKeySchema); + }); + + it('should call setValidatorGeneratorKey', async () => { + await updateGeneratorKeyCommand.execute(context); + + expect(mockValidatorsMethod.setValidatorGeneratorKey).toHaveBeenCalledWith( + expect.anything(), + address.getAddressFromPublicKey(publicKey), + context.params.generatorKey, + ); + }); + }); +}); diff --git a/framework/test/unit/modules/poa/endpoint.spec.ts b/framework/test/unit/modules/poa/endpoint.spec.ts new file mode 100644 index 00000000000..01af8feba6a --- /dev/null +++ b/framework/test/unit/modules/poa/endpoint.spec.ts @@ -0,0 +1,198 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { address as cryptoAddress, utils } from '@liskhq/lisk-cryptography'; +import { PoAModule } from '../../../../src'; +import { PoAEndpoint } from '../../../../src/modules/poa/endpoint'; +import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; +import { SnapshotStore, ValidatorStore } from '../../../../src/modules/poa/stores'; +import { + InMemoryPrefixedStateDB, + createTransientModuleEndpointContext, +} from '../../../../src/testing'; +import { createStoreGetter } from '../../../../src/testing/utils'; +import { AUTHORITY_REGISTRATION_FEE, KEY_SNAPSHOT_0 } from '../../../../src/modules/poa/constants'; + +describe('PoAModuleEndpoint', () => { + const poa = new PoAModule(); + + let poaEndpoint: PoAEndpoint; + let stateStore: PrefixedStateReadWriter; + let validatorStore: ValidatorStore; + let snapshotStore: SnapshotStore; + + const address1 = utils.getRandomBytes(20); + const address2 = utils.getRandomBytes(20); + const address3 = utils.getRandomBytes(20); + + const validatorData = { + name: 'validator1', + address: cryptoAddress.getLisk32AddressFromAddress(address1), + weight: BigInt(1), + }; + + const snapshot = { + threshold: BigInt(2), + validators: [ + { + address: address1, + weight: BigInt(1), + }, + { + address: address2, + weight: BigInt(2), + }, + ], + }; + + beforeEach(() => { + poaEndpoint = new PoAEndpoint(poa.stores, poa.offchainStores); + poaEndpoint.init(AUTHORITY_REGISTRATION_FEE); + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + validatorStore = poa.stores.get(ValidatorStore); + snapshotStore = poa.stores.get(SnapshotStore); + }); + + describe('getValidator', () => { + beforeEach(async () => { + await validatorStore.set(createStoreGetter(stateStore), address1, { + name: validatorData.name, + }); + await snapshotStore.set(createStoreGetter(stateStore), KEY_SNAPSHOT_0, snapshot); + }); + + it('should return correct validator data corresponding to the input address', async () => { + const validatorDataReturned = await poaEndpoint.getValidator( + createTransientModuleEndpointContext({ + stateStore, + params: { + address: cryptoAddress.getLisk32AddressFromAddress(address1), + }, + }), + ); + + const validatorDataJSON = { + ...validatorData, + weight: validatorData.weight.toString(), + }; + + expect(validatorDataReturned).toStrictEqual(validatorDataJSON); + }); + + it('should return valid JSON output', async () => { + const validatorDataReturned = await poaEndpoint.getValidator( + createTransientModuleEndpointContext({ + stateStore, + params: { + address: cryptoAddress.getLisk32AddressFromAddress(address1), + }, + }), + ); + + expect(validatorDataReturned.weight).toBeString(); + }); + + it('should throw error if input address for validator not found', async () => { + await expect( + poaEndpoint.getValidator( + createTransientModuleEndpointContext({ + stateStore, + params: { address: cryptoAddress.getLisk32AddressFromAddress(address3) }, + }), + ), + ).rejects.toThrow( + `Validator not found in snapshot for address ${cryptoAddress.getLisk32AddressFromAddress( + address3, + )}`, + ); + }); + }); + + describe('getAllValidators', () => { + const address1Str = cryptoAddress.getLisk32AddressFromAddress(address1); + const address2Str = cryptoAddress.getLisk32AddressFromAddress(address2); + + const addresses = [address1Str, address2Str]; + + it('should return correct data for all validators', async () => { + await validatorStore.set(createStoreGetter(stateStore), address1, { + name: validatorData.name, + }); + await validatorStore.set(createStoreGetter(stateStore), address2, { name: 'validator2' }); + await snapshotStore.set(createStoreGetter(stateStore), KEY_SNAPSHOT_0, snapshot); + + const { validators } = await poaEndpoint.getAllValidators( + createTransientModuleEndpointContext({ stateStore }), + ); + + expect(addresses).toContain(validators[0].address); + expect(addresses).toContain(validators[1].address); + }); + + it('should return valid JSON output', async () => { + await validatorStore.set(createStoreGetter(stateStore), address1, { + name: validatorData.name, + }); + await validatorStore.set(createStoreGetter(stateStore), address2, { name: 'validator2' }); + await snapshotStore.set(createStoreGetter(stateStore), KEY_SNAPSHOT_0, snapshot); + + const { validators } = await poaEndpoint.getAllValidators( + createTransientModuleEndpointContext({ stateStore }), + ); + + // Here we are checking against name sorted values from endpoint + expect(validators[0].weight).toBe(snapshot.validators[0].weight.toString()); + expect(validators[1].weight).toBe(snapshot.validators[1].weight.toString()); + }); + + it('should return json with empty weight for non active validator', async () => { + await validatorStore.set(createStoreGetter(stateStore), address1, { name: 'validator1' }); + await validatorStore.set(createStoreGetter(stateStore), address2, { name: 'validator2' }); + const currentSnapshot = { + threshold: BigInt(2), + validators: [ + { + address: address1, + weight: BigInt(1), + }, + ], + }; + await snapshotStore.set(createStoreGetter(stateStore), KEY_SNAPSHOT_0, currentSnapshot); + + const { validators } = await poaEndpoint.getAllValidators( + createTransientModuleEndpointContext({ stateStore }), + ); + + // Checking against name-sorted values + expect(validators[0].weight).toBe(currentSnapshot.validators[0].weight.toString()); + expect(validators[1].weight).toBe('0'); + }); + }); + + describe('getRegistrationFee', () => { + it('should return the default registration fee', () => { + const response = poaEndpoint.getRegistrationFee(); + + expect(response).toEqual({ fee: AUTHORITY_REGISTRATION_FEE.toString() }); + }); + + it('should return the configured registration fee', () => { + const authorityRegistrationFee = BigInt(200000); + poaEndpoint.init(authorityRegistrationFee); + const response = poaEndpoint.getRegistrationFee(); + + expect(response).toEqual({ fee: authorityRegistrationFee.toString() }); + }); + }); +}); diff --git a/framework/test/unit/modules/poa/genesis_block_test_data.ts b/framework/test/unit/modules/poa/genesis_block_test_data.ts new file mode 100644 index 00000000000..0cbcff4bd7b --- /dev/null +++ b/framework/test/unit/modules/poa/genesis_block_test_data.ts @@ -0,0 +1,197 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { bls, address as cryptoAddress, legacy, utils } from '@liskhq/lisk-cryptography'; +import { Mnemonic } from '@liskhq/lisk-passphrase'; + +export const validators = new Array(103).fill(0).map((_, i) => { + const passphrase = Mnemonic.generateMnemonic(); + const keys = legacy.getPrivateAndPublicKeyFromPassphrase(passphrase); + const address = cryptoAddress.getAddressFromPublicKey(keys.publicKey); + const blsPrivateKey = bls.generatePrivateKey(Buffer.from(passphrase, 'utf-8')); + const blsPublicKey = bls.getPublicKeyFromPrivateKey(blsPrivateKey); + const blsPoP = bls.popProve(blsPrivateKey); + return { + address, + name: `genesis_${i}`, + blsKey: blsPublicKey, + proofOfPossession: blsPoP, + generatorKey: keys.publicKey, + }; +}); +validators.sort((a, b) => a.address.compare(b.address)); + +const activeValidators = validators + .slice(0, validators.length - 2) + .map(v => ({ address: v.address, weight: BigInt(1) })); +const threshold = BigInt(35); + +export const validAsset = { + validators, + snapshotSubstore: { + activeValidators, + threshold, + }, +}; + +export const invalidAssets: any[] = [ + [ + 'Invalid validator name length', + { + validators: [ + { + ...validators[0], + name: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + }, + ...validators.slice(1, validators.length), + ], + snapshotSubstore: { + activeValidators, + threshold, + }, + }, + ], + [ + 'Invalid validator name character', + { + validators: [ + { + ...validators[0], + name: '@@@__++', + }, + ...validators.slice(1, validators.length), + ], + snapshotSubstore: { + activeValidators, + threshold, + }, + }, + '`name` property is invalid. Must contain only characters a-z0-9!@$&_.', + ], + [ + 'Not unique validator name', + { + validators: [ + { + ...validators[0], + name: validators[1].name, + }, + ...validators.slice(1, validators.length), + ], + snapshotSubstore: { + activeValidators, + threshold, + }, + }, + '`name` property of all entries in the validators must be distinct.', + ], + [ + 'Not unique validator address', + { + validators: [ + { + ...validators[0], + address: validators[1].address, + }, + ...validators.slice(1, validators.length), + ], + snapshotSubstore: { + activeValidators, + threshold, + }, + }, + '`address` property of all entries in validators must be distinct.', + ], + [ + 'validator address is not ordered', + { + validators: validators.slice(0).sort((a, b) => b.address.compare(a.address)), + snapshotSubstore: { + activeValidators, + threshold, + }, + }, + '`validators` must be ordered lexicographically by address.', + ], + [ + 'active validator address is not unique', + { + validators, + snapshotSubstore: { + activeValidators: [ + { + ...activeValidators[0], + address: activeValidators[1].address, + }, + ...activeValidators.slice(1, activeValidators.length), + ], + threshold, + }, + }, + '`address` properties in `activeValidators` must be distinct.', + ], + [ + 'active validator address is not ordered', + { + validators, + snapshotSubstore: { + activeValidators: activeValidators.slice(0).sort((a, b) => b.address.compare(a.address)), + threshold, + }, + }, + '`activeValidators` must be ordered lexicographically by address property.', + ], + [ + 'active validator address is missing from validators array', + { + validators, + snapshotSubstore: { + activeValidators: [ + { ...activeValidators[0], address: utils.getRandomBytes(20) }, + ...activeValidators.slice(1, activeValidators.length), + ].sort((a, b) => a.address.compare(b.address)), + threshold, + }, + }, + '`activeValidator` address is missing from validators array.', + ], + [ + 'active validator weight must be positive integer', + { + validators, + snapshotSubstore: { + activeValidators: [ + { ...activeValidators[0], weight: BigInt(0) }, + ...activeValidators.slice(1, activeValidators.length), + ], + threshold, + }, + }, + '`activeValidators` weight must be positive integer.', + ], + [ + 'active validators total weight must be within range', + { + validators, + snapshotSubstore: { + activeValidators: [ + { ...activeValidators[0], weight: BigInt(1000000000000000) }, + ...activeValidators.slice(1, activeValidators.length), + ], + threshold, + }, + }, + '`threshold` in snapshot substore is not within range.', + ], +]; diff --git a/framework/test/unit/modules/poa/module.spec.ts b/framework/test/unit/modules/poa/module.spec.ts new file mode 100644 index 00000000000..491dfd64d09 --- /dev/null +++ b/framework/test/unit/modules/poa/module.spec.ts @@ -0,0 +1,507 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec } from '@liskhq/lisk-codec'; +import { utils } from '@liskhq/lisk-cryptography'; +import { BlockAssets } from '@liskhq/lisk-chain'; +import { PrefixedStateReadWriter } from '../../../../src/state_machine/prefixed_state_read_writer'; +import { + InMemoryPrefixedStateDB, + createBlockContext, + createGenesisBlockContext, + createTransientMethodContext, +} from '../../../../src/testing'; +import { invalidAssets, validAsset } from './genesis_block_test_data'; +import { PoAModule } from '../../../../src/modules/poa/module'; +import { genesisPoAStoreSchema } from '../../../../src/modules/poa/schemas'; +import { + AUTHORITY_REGISTRATION_FEE, + EMPTY_BYTES, + KEY_SNAPSHOT_0, + KEY_SNAPSHOT_1, + KEY_SNAPSHOT_2, + LENGTH_BLS_KEY, + LENGTH_GENERATOR_KEY, +} from '../../../../src/modules/poa/constants'; +import { + ActiveValidator, + FeeMethod, + ModuleConfigJSON, + RandomMethod, + ValidatorsMethod, +} from '../../../../src/modules/poa/types'; +import { createFakeBlockHeader } from '../../../fixtures'; +import { + BlockAfterExecuteContext, + GenesisBlockContext, + GenesisBlockExecuteContext, + MethodContext, +} from '../../../../src/state_machine'; +import { + ValidatorStore, + SnapshotStore, + NameStore, + ChainPropertiesStore, + SnapshotObject, + ChainProperties, +} from '../../../../src/modules/poa/stores'; +import { shuffleValidatorList } from '../../../../src/modules/utils'; + +describe('PoA module', () => { + let poaModule: PoAModule; + let randomMethod: RandomMethod; + let validatorMethod: ValidatorsMethod; + let feeMethod: FeeMethod; + + beforeEach(() => { + poaModule = new PoAModule(); + randomMethod = { + getRandomBytes: jest.fn(), + }; + validatorMethod = { + setValidatorGeneratorKey: jest.fn(), + registerValidatorKeys: jest.fn().mockResolvedValue(true), + registerValidatorWithoutBLSKey: jest.fn().mockResolvedValue(true), + getValidatorKeys: jest.fn().mockResolvedValue({ + blsKey: utils.getRandomBytes(LENGTH_BLS_KEY), + generatorKey: utils.getRandomBytes(LENGTH_GENERATOR_KEY), + }), + getGeneratorsBetweenTimestamps: jest.fn(), + setValidatorsParams: jest.fn(), + }; + feeMethod = { + payFee: jest.fn(), + }; + }); + describe('constructor', () => {}); + + describe('init', () => { + let genesisConfig: any; + let moduleConfigJSON: ModuleConfigJSON; + + beforeEach(() => { + genesisConfig = {}; + moduleConfigJSON = { + authorityRegistrationFee: AUTHORITY_REGISTRATION_FEE.toString(), + }; + }); + it('should assign authorityRegistrationFee from config if given', async () => { + jest.spyOn(poaModule['_registerAuthorityCommand'], 'init'); + jest.spyOn(poaModule.endpoint, 'init'); + await poaModule.init({ + genesisConfig, + moduleConfig: { + ...moduleConfigJSON, + authorityRegistrationFee: '20000', + }, + }); + + expect(poaModule['_moduleConfig'].authorityRegistrationFee).toEqual(BigInt('20000')); + expect(poaModule['_registerAuthorityCommand'].init).toHaveBeenCalledWith( + poaModule['_moduleConfig'], + ); + expect(poaModule.endpoint.init).toHaveBeenCalledWith( + poaModule['_moduleConfig'].authorityRegistrationFee, + ); + }); + + it('should assign default value for authorityRegistrationFee when not given in config', async () => { + jest.spyOn(poaModule['_registerAuthorityCommand'], 'init'); + jest.spyOn(poaModule.endpoint, 'init'); + await poaModule.init({ + genesisConfig, + moduleConfig: { ...moduleConfigJSON }, + }); + + expect(poaModule['_moduleConfig'].authorityRegistrationFee).toEqual( + AUTHORITY_REGISTRATION_FEE, + ); + expect(poaModule['_registerAuthorityCommand'].init).toHaveBeenCalledWith( + poaModule['_moduleConfig'], + ); + expect(poaModule.endpoint.init).toHaveBeenCalledWith( + poaModule['_moduleConfig'].authorityRegistrationFee, + ); + }); + }); + + describe('addDependencies', () => { + it('should add all the dependencies', () => { + jest.spyOn(poaModule['_registerAuthorityCommand'], 'addDependencies'); + jest.spyOn(poaModule['_updateAuthorityCommand'], 'addDependencies'); + jest.spyOn(poaModule['_updateGeneratorKeyCommand'], 'addDependencies'); + poaModule.addDependencies(validatorMethod, feeMethod, randomMethod); + + expect(poaModule['_validatorsMethod']).toBeDefined(); + expect(poaModule['_feeMethod']).toBeDefined(); + expect(poaModule['_randomMethod']).toBeDefined(); + + // Check command dependencies + expect(poaModule['_registerAuthorityCommand'].addDependencies).toHaveBeenCalledWith( + poaModule['_validatorsMethod'], + poaModule['_feeMethod'], + ); + expect(poaModule['_updateAuthorityCommand'].addDependencies).toHaveBeenCalledWith( + poaModule['_validatorsMethod'], + ); + expect(poaModule['_updateGeneratorKeyCommand'].addDependencies).toHaveBeenCalledWith( + poaModule['_validatorsMethod'], + ); + }); + }); + + describe('afterTransactionsExecute', () => { + const genesisData = { + height: 0, + initRounds: 3, + initValidators: [], + }; + const bootstrapRounds = genesisData.initRounds; + let stateStore: PrefixedStateReadWriter; + let context: BlockAfterExecuteContext; + let currentTimestamp: number; + let height: number; + let snapshot0: SnapshotObject; + let snapshot1: SnapshotObject; + let snapshot2: SnapshotObject; + let chainPropertiesStore: ChainPropertiesStore; + let snapshotStore: SnapshotStore; + let methodContext: MethodContext; + let randomSeed: Buffer; + let chainProperties: ChainProperties; + + beforeEach(async () => { + poaModule = new PoAModule(); + poaModule.addDependencies(validatorMethod, feeMethod, randomMethod); + height = 103 * (bootstrapRounds + 1); + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + currentTimestamp = Math.floor(Date.now() / 1000); + + context = createBlockContext({ + stateStore, + header: createFakeBlockHeader({ + height, + timestamp: currentTimestamp, + }), + }).getBlockAfterExecuteContext(); + methodContext = createTransientMethodContext({ stateStore }); + chainProperties = { + roundEndHeight: height - 1, + validatorsUpdateNonce: 4, + }; + chainPropertiesStore = poaModule.stores.get(ChainPropertiesStore); + await chainPropertiesStore.set(methodContext, EMPTY_BYTES, chainProperties); + snapshot0 = { + threshold: BigInt(4), + validators: [ + { + address: Buffer.from('4162070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4262070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4362070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4462070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4562070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + ], + }; + + snapshot1 = { + threshold: BigInt(4), + validators: [ + { + address: Buffer.from('4162070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4862070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4362070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4762070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4562070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + ], + }; + + snapshot2 = { + threshold: BigInt(4), + validators: [ + { + address: Buffer.from('4262070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4862070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4362070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4762070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + { + address: Buffer.from('4562070a641cf689f765d43ad792e1970e6bb863', 'binary'), + weight: BigInt(1), + }, + ], + }; + + snapshotStore = poaModule.stores.get(SnapshotStore); + await snapshotStore.set(methodContext, KEY_SNAPSHOT_0, snapshot0); + await snapshotStore.set(methodContext, KEY_SNAPSHOT_1, snapshot1); + await snapshotStore.set(methodContext, KEY_SNAPSHOT_2, snapshot2); + randomSeed = utils.getRandomBytes(20); + jest.spyOn(snapshotStore, 'set'); + jest.spyOn(randomMethod, 'getRandomBytes').mockResolvedValue(randomSeed); + jest.spyOn(validatorMethod, 'setValidatorsParams').mockResolvedValue(); + }); + it('should not do anything when context.header.height !== chainProperties.roundEndHeight', async () => { + await poaModule.afterTransactionsExecute(context); + expect(poaModule.stores.get(SnapshotStore).set).not.toHaveBeenCalled(); + expect(randomMethod.getRandomBytes).not.toHaveBeenCalled(); + expect(validatorMethod.setValidatorsParams).not.toHaveBeenCalled(); + }); + + it('should set snapshots and call validatorsMethod.setValidatorsParams when context.header.height === chainProperties.roundEndHeight', async () => { + chainProperties = { + ...chainProperties, + roundEndHeight: height, + }; + await chainPropertiesStore.set(methodContext, EMPTY_BYTES, chainProperties); + const roundStartHeight = height - snapshot0.validators.length + 1; + const validators = []; + for (const validator of snapshot1.validators) { + validators.push(validator); + } + const nextValidators = shuffleValidatorList(randomSeed, validators); + await poaModule.afterTransactionsExecute(context); + expect(poaModule.stores.get(SnapshotStore).set).toHaveBeenCalledWith( + context, + KEY_SNAPSHOT_0, + snapshot1, + ); + expect(poaModule.stores.get(SnapshotStore).set).toHaveBeenCalledWith( + context, + KEY_SNAPSHOT_1, + snapshot2, + ); + expect(randomMethod.getRandomBytes).toHaveBeenCalledWith( + context, + roundStartHeight, + snapshot0.validators.length, + ); + expect(validatorMethod.setValidatorsParams).toHaveBeenCalledWith( + context, + context, + snapshot1.threshold, + snapshot1.threshold, + nextValidators.map(v => ({ + address: v.address, + bftWeight: v.weight, + })), + ); + await expect(chainPropertiesStore.get(context, EMPTY_BYTES)).resolves.toEqual({ + ...chainProperties, + roundEndHeight: chainProperties.roundEndHeight + snapshot1.validators.length, + }); + }); + }); + + describe('initGenesisState', () => { + let stateStore: PrefixedStateReadWriter; + let poa: PoAModule; + + beforeEach(() => { + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + poa = new PoAModule(); + poa.addDependencies(validatorMethod, feeMethod, randomMethod); + }); + + it('should not throw error if asset does not exist', async () => { + const context = createGenesisBlockContext({ + stateStore, + }).createInitGenesisStateContext(); + jest.spyOn(context, 'getStore'); + + await expect(poa.initGenesisState(context)).toResolve(); + expect(context.getStore).not.toHaveBeenCalled(); + }); + + describe.each(invalidAssets)('%p', (_, data, errString) => { + it('should throw error when asset is invalid', async () => { + const assetBytes = codec.encode(genesisPoAStoreSchema, data as object); + const context = createGenesisBlockContext({ + stateStore, + header: createFakeBlockHeader({ height: 12345 }), + assets: new BlockAssets([{ module: poa.name, data: assetBytes }]), + }).createInitGenesisStateContext(); + + await expect(poa.initGenesisState(context)).rejects.toThrow(errString as string); + }); + }); + + describe('when the genesis asset is valid', () => { + let genesisContext: GenesisBlockContext; + let context: GenesisBlockExecuteContext; + + beforeEach(() => { + const assetBytes = codec.encode(genesisPoAStoreSchema, validAsset); + genesisContext = createGenesisBlockContext({ + stateStore, + assets: new BlockAssets([{ module: poa.name, data: assetBytes }]), + }); + context = genesisContext.createInitGenesisStateContext(); + }); + + it('should store all the validators', async () => { + await expect(poa.initGenesisState(context)).resolves.toBeUndefined(); + const nameStore = poa.stores.get(NameStore); + const allNames = await nameStore.iterate(context, { + gte: Buffer.from([0]), + lte: Buffer.from([255]), + }); + expect(allNames).toHaveLength(validAsset.validators.length); + const validatorStore = poa.stores.get(ValidatorStore); + const allValidators = await validatorStore.iterate(context, { + gte: Buffer.alloc(20, 0), + lte: Buffer.alloc(20, 255), + }); + expect(allValidators).toHaveLength(validAsset.validators.length); + }); + + it('should store snapshot current round', async () => { + await expect(poa.initGenesisState(context)).toResolve(); + const snapshotStore = poa.stores.get(SnapshotStore); + await expect(snapshotStore.get(context, KEY_SNAPSHOT_0)).resolves.toEqual({ + validators: validAsset.snapshotSubstore.activeValidators, + threshold: validAsset.snapshotSubstore.threshold, + }); + }); + + it('should store snapshot current round + 1', async () => { + await expect(poa.initGenesisState(context)).toResolve(); + const snapshotStore = poa.stores.get(SnapshotStore); + await expect(snapshotStore.get(context, KEY_SNAPSHOT_1)).resolves.toEqual({ + validators: validAsset.snapshotSubstore.activeValidators, + threshold: validAsset.snapshotSubstore.threshold, + }); + }); + + it('should store snapshot current round + 2', async () => { + await expect(poa.initGenesisState(context)).toResolve(); + const snapshotStore = poa.stores.get(SnapshotStore); + await expect(snapshotStore.get(context, KEY_SNAPSHOT_2)).resolves.toEqual({ + validators: validAsset.snapshotSubstore.activeValidators, + threshold: validAsset.snapshotSubstore.threshold, + }); + }); + + it('should store chain properties', async () => { + await expect(poa.initGenesisState(context)).toResolve(); + const chainPropertiesStore = poa.stores.get(ChainPropertiesStore); + await expect(chainPropertiesStore.get(context, EMPTY_BYTES)).resolves.toEqual({ + roundEndHeight: context.header.height, + validatorsUpdateNonce: 0, + }); + }); + }); + }); + + describe('finalizeGenesisState', () => { + let genesisContext: GenesisBlockContext; + let context: GenesisBlockExecuteContext; + let snapshotStore: SnapshotStore; + let chainPropertiesStore: ChainPropertiesStore; + let stateStore: PrefixedStateReadWriter; + let poa: PoAModule; + + beforeEach(async () => { + poa = new PoAModule(); + poa.addDependencies(validatorMethod, feeMethod, randomMethod); + stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); + const assetBytes = codec.encode(genesisPoAStoreSchema, validAsset); + genesisContext = createGenesisBlockContext({ + stateStore, + assets: new BlockAssets([{ module: poa.name, data: assetBytes }]), + }); + context = genesisContext.createInitGenesisStateContext(); + snapshotStore = poa.stores.get(SnapshotStore); + await snapshotStore.set(context, KEY_SNAPSHOT_0, { + ...validAsset.snapshotSubstore, + validators: validAsset.snapshotSubstore.activeValidators, + }); + chainPropertiesStore = poa.stores.get(ChainPropertiesStore); + await chainPropertiesStore.set(context, EMPTY_BYTES, { + roundEndHeight: context.header.height, + validatorsUpdateNonce: 0, + }); + }); + + it('should store updated chain properties', async () => { + await expect(poa.finalizeGenesisState(context)).toResolve(); + poa.stores.get(ChainPropertiesStore); + await expect(chainPropertiesStore.get(context, EMPTY_BYTES)).resolves.toEqual({ + roundEndHeight: context.header.height + validAsset.snapshotSubstore.activeValidators.length, + validatorsUpdateNonce: 0, + }); + }); + + it('should register all active validators as BFT validators', async () => { + await expect(poa.finalizeGenesisState(context)).toResolve(); + expect(poa['_validatorsMethod'].setValidatorsParams).toHaveBeenCalledWith( + expect.any(Object), + expect.any(Object), + BigInt(validAsset.snapshotSubstore.threshold), + BigInt(validAsset.snapshotSubstore.threshold), + validAsset.snapshotSubstore.activeValidators.map(d => ({ + address: d.address, + bftWeight: d.weight, + })), + ); + }); + + it('should fail if registerValidatorKeys return false', async () => { + (poa['_validatorsMethod'].registerValidatorKeys as jest.Mock).mockRejectedValue( + new Error('Invalid validator key found in poa genesis asset validators.'), + ); + + await expect(poa.finalizeGenesisState(context)).rejects.toThrow( + 'Invalid validator key found in poa genesis asset validators.', + ); + }); + }); +}); diff --git a/framework/test/unit/modules/pos/commands/validator_registration.spec.ts b/framework/test/unit/modules/pos/commands/register_validator.spec.ts similarity index 99% rename from framework/test/unit/modules/pos/commands/validator_registration.spec.ts rename to framework/test/unit/modules/pos/commands/register_validator.spec.ts index 4fc8df99a59..f5bd045833c 100644 --- a/framework/test/unit/modules/pos/commands/validator_registration.spec.ts +++ b/framework/test/unit/modules/pos/commands/register_validator.spec.ts @@ -86,7 +86,6 @@ describe('Validator registration command', () => { 'hex', ); - // TODO: move this function to utils and import from all other tests using it const checkEventResult = ( eventQueue: EventQueue, EventClass: any, diff --git a/framework/test/unit/modules/pos/commands/report_misbehavior.spec.ts b/framework/test/unit/modules/pos/commands/report_misbehavior.spec.ts index 24d373020ce..d720ad32674 100644 --- a/framework/test/unit/modules/pos/commands/report_misbehavior.spec.ts +++ b/framework/test/unit/modules/pos/commands/report_misbehavior.spec.ts @@ -535,6 +535,33 @@ describe('ReportMisbehaviorCommand', () => { await expect(pomCommand.verify(context)).not.toReject(); }); + + it('should return error when generator of header1 is not a validator', async () => { + const randomAddress = utils.getRandomBytes(20); + + transactionParamsDecoded = { + header1: codec.encode(blockHeaderSchema, { + ...header1, + generatorAddress: randomAddress, + }), + header2: codec.encode(blockHeaderSchema, { + ...header2, + generatorAddress: randomAddress, + }), + }; + transactionParams = codec.encode(pomCommand.schema, transactionParamsDecoded); + transaction.params = transactionParams; + context = testing + .createTransactionContext({ + stateStore, + transaction, + }) + .createCommandExecuteContext(pomCommand.schema); + + await expect(pomCommand.verify(context)).rejects.toThrow( + `Specified key 7160f8688000${randomAddress.toString('hex')} does not exist`, + ); + }); }); describe('execute', () => { diff --git a/framework/test/unit/modules/pos/commands/stake.spec.ts b/framework/test/unit/modules/pos/commands/stake.spec.ts index 4aacc1ffc42..120a9179937 100644 --- a/framework/test/unit/modules/pos/commands/stake.spec.ts +++ b/framework/test/unit/modules/pos/commands/stake.spec.ts @@ -29,6 +29,7 @@ import { ValidatorAccount, ValidatorStore } from '../../../../../src/modules/pos import { EligibleValidatorsStore } from '../../../../../src/modules/pos/stores/eligible_validators'; import { StakerStore } from '../../../../../src/modules/pos/stores/staker'; import { + StakerData, ModuleConfigJSON, StakeObject, StakeTransactionParams, @@ -72,18 +73,18 @@ describe('StakeCommand', () => { const validator1StakeAmount = liskToBeddows(90); const validator2StakeAmount = liskToBeddows(50); - let validatorInfo1: ValidatorAccount; - let validatorInfo2: ValidatorAccount; - let validatorInfo3: ValidatorAccount; + let defaultValidator: ValidatorAccount; + let validator1: ValidatorAccount; + let validator2: ValidatorAccount; + let validator3: ValidatorAccount; let stakerStore: StakerStore; let validatorStore: ValidatorStore; let context: any; let transaction: any; let command: StakeCommand; - let transactionParams: Buffer; let transactionParamsDecoded: any; let stateStore: PrefixedStateReadWriter; - let lockFn: any; + let tokenLockMock: jest.Mock; let tokenMethod: any; let internalMethod: InternalMethod; let mockAssignStakeRewards: jest.SpyInstance< @@ -97,9 +98,9 @@ describe('StakeCommand', () => { >; beforeEach(async () => { - lockFn = jest.fn(); + tokenLockMock = jest.fn(); tokenMethod = { - lock: lockFn, + lock: tokenLockMock, unlock: jest.fn(), getAvailableBalance: jest.fn(), burn: jest.fn(), @@ -134,11 +135,11 @@ describe('StakeCommand', () => { stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); - validatorInfo1 = { + defaultValidator = { consecutiveMissedBlocks: 0, isBanned: false, lastGeneratedHeight: 5, - name: 'someValidator1', + name: 'defaultValidator', reportMisbehaviorHeights: [], selfStake: BigInt(0), totalStake: BigInt(0), @@ -147,43 +148,37 @@ describe('StakeCommand', () => { sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }; - validatorInfo2 = { - consecutiveMissedBlocks: 0, - isBanned: false, - lastGeneratedHeight: 5, + validator1 = { + ...defaultValidator, + name: 'someValidator1', + }; + + validator2 = { + ...defaultValidator, name: 'someValidator2', - reportMisbehaviorHeights: [], - selfStake: BigInt(0), - totalStake: BigInt(0), - commission: 0, - lastCommissionIncreaseHeight: 0, - sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }; - validatorInfo3 = { - consecutiveMissedBlocks: 0, - isBanned: false, - lastGeneratedHeight: 5, + validator3 = { + ...defaultValidator, name: 'someValidator3', - reportMisbehaviorHeights: [], - selfStake: BigInt(0), - totalStake: BigInt(0), - commission: 0, - lastCommissionIncreaseHeight: 0, - sharingCoefficients: [{ tokenID: Buffer.alloc(8), coefficient: Buffer.alloc(24) }], }; - validatorStore = pos.stores.get(ValidatorStore); - - await validatorStore.set(createStoreGetter(stateStore), validatorAddress1, validatorInfo1); - await validatorStore.set(createStoreGetter(stateStore), validatorAddress2, validatorInfo2); - stakerStore = pos.stores.get(StakerStore); validatorStore = pos.stores.get(ValidatorStore); - await validatorStore.set(createStoreGetter(stateStore), validatorAddress1, validatorInfo1); - await validatorStore.set(createStoreGetter(stateStore), validatorAddress2, validatorInfo2); - await validatorStore.set(createStoreGetter(stateStore), validatorAddress3, validatorInfo3); + await validatorStore.set(createStoreGetter(stateStore), validatorAddress1, validator1); + await validatorStore.set(createStoreGetter(stateStore), validatorAddress2, validator2); + await validatorStore.set(createStoreGetter(stateStore), validatorAddress3, validator3); + + transaction = new Transaction({ + module: 'pos', + command: 'stake', + fee: BigInt(1500000), + nonce: BigInt(0), + params: Buffer.alloc(0), + senderPublicKey, + signatures: [], + }); }); describe('constructor', () => { @@ -273,81 +268,57 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes contains valid contents', () => { it('should not throw errors with valid upstake case', async () => { - // Arrange transactionParamsDecoded = { stakes: [{ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(20) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty('status', VerifyStatus.OK); }); it('should not throw errors with valid downstake cast', async () => { - // Arrange transactionParamsDecoded = { stakes: [{ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(-20) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty('status', VerifyStatus.OK); }); it('should not throw errors with valid mixed stakes case', async () => { - // Arrange transactionParamsDecoded = { stakes: [ { validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(20) }, { validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(-20) }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty('status', VerifyStatus.OK); }); }); describe('when transaction.params.stakes contains more than 10 positive stakes', () => { it('should throw error', async () => { - // Arrange transactionParamsDecoded = { stakes: Array(11) .fill(0) .map(() => ({ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(10) })), }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty( 'error.message', 'Upstake can only be casted up to 10.', @@ -357,7 +328,6 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes contains more than 10 negative stakes', () => { it('should throw error', async () => { - // Arrange transactionParamsDecoded = { stakes: Array(11) .fill(0) @@ -366,16 +336,11 @@ describe('StakeCommand', () => { amount: liskToBeddows(-10), })), }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty( 'error.message', 'Downstake can only be casted up to 10.', @@ -385,23 +350,15 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes includes duplicate validators within positive amount', () => { it('should throw error', async () => { - // Arrange const validatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { - stakes: Array(2) - .fill(0) - .map(() => ({ validatorAddress, amount: liskToBeddows(10) })), + stakes: Array(2).fill({ validatorAddress, amount: liskToBeddows(10) }), }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty( 'error.message', 'Validator address must be unique.', @@ -411,7 +368,6 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes includes duplicate validators within positive and negative amount', () => { it('should throw error', async () => { - // Arrange const validatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { stakes: [ @@ -419,16 +375,11 @@ describe('StakeCommand', () => { { validatorAddress, amount: liskToBeddows(-10) }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty( 'error.message', 'Validator address must be unique.', @@ -438,21 +389,15 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes includes zero amount', () => { it('should throw error', async () => { - // Arrange const validatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { stakes: [{ validatorAddress, amount: liskToBeddows(0) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty( 'error.message', 'Amount cannot be 0.', @@ -462,21 +407,15 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes includes positive amount which is not multiple of 10 * 10^8', () => { it('should throw an error', async () => { - // Arrange const validatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { stakes: [{ validatorAddress, amount: BigInt(20) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty( 'error.message', 'Amount should be multiple of 10 * 10^8.', @@ -486,21 +425,15 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes includes negative amount which is not multiple of 10 * 10^8', () => { it('should throw error', async () => { - // Arrange const validatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { stakes: [{ validatorAddress, amount: BigInt(-20) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, }).createCommandVerifyContext(command.schema); - // Assert await expect(command.verify(context)).resolves.toHaveProperty( 'error.message', 'Amount should be multiple of 10 * 10^8.', @@ -510,34 +443,17 @@ describe('StakeCommand', () => { }); describe('execute', () => { - beforeEach(() => { - transaction = new Transaction({ - module: 'pos', - command: 'stake', - fee: BigInt(1500000), - nonce: BigInt(0), - params: transactionParams, - senderPublicKey, - signatures: [], - }); - }); describe('when transaction.params.stakes contain positive amount', () => { it('should emit ValidatorStakedEvent with STAKE_SUCCESSFUL result', async () => { - // Arrange transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress1, amount: liskToBeddows(10) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, }).createCommandExecuteContext(command.schema); - // Assert await expect(command.execute(context)).resolves.toBeUndefined(); checkEventResult( @@ -555,39 +471,28 @@ describe('StakeCommand', () => { }); it('should throw error if stake amount is more than balance', async () => { - // Arrange transactionParamsDecoded = { stakes: [{ validatorAddress: utils.getRandomBytes(20), amount: liskToBeddows(100) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, }).createCommandExecuteContext(command.schema); - lockFn.mockRejectedValue(new Error('Not enough balance to lock')); + tokenLockMock.mockRejectedValue(new Error('Not enough balance to lock')); - // Assert await expect(command.execute(context)).rejects.toThrow(); }); it('should make account to have correct balance', async () => { - // Arrange transactionParamsDecoded = { stakes: [ { validatorAddress: validatorAddress1, amount: validator1StakeAmount }, { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -595,16 +500,15 @@ describe('StakeCommand', () => { await command.execute(context); - // Assert - expect(lockFn).toHaveBeenCalledTimes(2); - expect(lockFn).toHaveBeenCalledWith( + expect(tokenLockMock).toHaveBeenCalledTimes(2); + expect(tokenLockMock).toHaveBeenCalledWith( expect.anything(), senderAddress, MODULE_NAME_POS, posTokenID, validator1StakeAmount, ); - expect(lockFn).toHaveBeenCalledWith( + expect(tokenLockMock).toHaveBeenCalledWith( expect.anything(), senderAddress, MODULE_NAME_POS, @@ -614,20 +518,10 @@ describe('StakeCommand', () => { }); it('should not change pendingUnlocks', async () => { - // Arrange - stakerStore = pos.stores.get(StakerStore); - validatorStore = pos.stores.get(ValidatorStore); - - await validatorStore.set(createStoreGetter(stateStore), validatorAddress1, validatorInfo1); - transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress1, amount: validator1StakeAmount }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -640,29 +534,17 @@ describe('StakeCommand', () => { senderAddress, ); - // Assert expect(pendingUnlocks).toHaveLength(0); }); - it('should order stakerData.sentStakes', async () => { - // Arrange - stakerStore = pos.stores.get(StakerStore); - validatorStore = pos.stores.get(ValidatorStore); - - await validatorStore.set(createStoreGetter(stateStore), validatorAddress1, validatorInfo1); - await validatorStore.set(createStoreGetter(stateStore), validatorAddress2, validatorInfo2); - + it('should order stakerData.stakes', async () => { transactionParamsDecoded = { stakes: [ { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, { validatorAddress: validatorAddress1, amount: validator1StakeAmount }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -672,31 +554,20 @@ describe('StakeCommand', () => { const { stakes } = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - const sentStakesCopy = stakes.slice(0); - sentStakesCopy.sort((a: any, b: any) => a.validatorAddress.compare(b.validatorAddress)); + const stakesCopy = stakes.slice(0); + stakesCopy.sort((a: any, b: any) => a.validatorAddress.compare(b.validatorAddress)); - // Assert - expect(stakes).toStrictEqual(sentStakesCopy); + expect(stakes).toStrictEqual(stakesCopy); }); - it('should make upstaked validator account to have correct totalStakeReceived', async () => { - // Arrange - validatorStore = pos.stores.get(ValidatorStore); - - await validatorStore.set(createStoreGetter(stateStore), validatorAddress1, validatorInfo1); - await validatorStore.set(createStoreGetter(stateStore), validatorAddress2, validatorInfo2); - + it('should correctly update validator totalStake when a staker is upstaking for the first time', async () => { transactionParamsDecoded = { stakes: [ { validatorAddress: validatorAddress1, amount: validator1StakeAmount }, { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -704,40 +575,77 @@ describe('StakeCommand', () => { await command.execute(context); - const { totalStake: totalStakeReceived1 } = await validatorStore.get( + const { totalStake: totalStake1 } = await validatorStore.get( createStoreGetter(stateStore), validatorAddress1, ); - const { totalStake: totalStakeReceived2 } = await validatorStore.get( + const { totalStake: totalStake2 } = await validatorStore.get( createStoreGetter(stateStore), validatorAddress2, ); - // Assert - expect(totalStakeReceived1).toBe(validator1StakeAmount); - expect(totalStakeReceived2).toBe(validator2StakeAmount); + expect(totalStake1).toBe(validator1StakeAmount); + expect(totalStake2).toBe(validator2StakeAmount); }); - it('should update stake object when it exists before and create if it does not exist', async () => { - // Arrange - stakerStore = pos.stores.get(StakerStore); - validatorStore = pos.stores.get(ValidatorStore); + it("should increase staker's stakes.amount and validator's totalStake when an existing staker further increases their stake", async () => { + const previousStakeAmount = liskToBeddows(120); + const newStakeAmount = liskToBeddows(88); + + const validatorAccount: ValidatorAccount = { + ...validator1, + totalStake: previousStakeAmount, + selfStake: liskToBeddows(50), + }; + const stakerData: StakerData = { + stakes: [ + { + validatorAddress: validatorAddress1, + amount: previousStakeAmount, + sharingCoefficients: validatorAccount.sharingCoefficients, + }, + ], + pendingUnlocks: [], + }; + + await stakerStore.set(createStoreGetter(stateStore), senderAddress, stakerData); + await validatorStore.set( + createStoreGetter(stateStore), + validatorAddress1, + validatorAccount, + ); - await validatorStore.set(createStoreGetter(stateStore), validatorAddress1, validatorInfo1); transactionParamsDecoded = { - stakes: [{ validatorAddress: validatorAddress1, amount: validator1StakeAmount }], + stakes: [{ validatorAddress: validatorAddress1, amount: newStakeAmount }], }; + transaction.params = codec.encode(command.schema, transactionParamsDecoded); + context = createTransactionContext({ + transaction, + stateStore, + }).createCommandExecuteContext(command.schema); + + await command.execute(context); - transactionParams = codec.encode(command.schema, transactionParamsDecoded); + const { totalStake } = await validatorStore.get( + createStoreGetter(stateStore), + validatorAddress1, + ); + const { stakes } = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - transaction.params = transactionParams; + expect(totalStake).toBe(previousStakeAmount + newStakeAmount); + expect(stakes[0].amount).toBe(previousStakeAmount + newStakeAmount); + }); + it('should create a new entry in staker store, when a new staker upstakes', async () => { + transactionParamsDecoded = { + stakes: [{ validatorAddress: validatorAddress1, amount: validator1StakeAmount }], + }; + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, }).createCommandExecuteContext(command.schema); - // Assert await expect( stakerStore.get(createStoreGetter(stateStore), senderAddress), ).rejects.toThrow(); @@ -752,7 +660,7 @@ describe('StakeCommand', () => { }); }); - describe('when transaction.params.stakes contain negative amount which makes stakerStore.sentStakes to be 0 entries', () => { + describe('when transaction.params.stakes contain negative amount which decreases StakerData.stakes[x].amount to 0', () => { beforeEach(async () => { transactionParamsDecoded = { stakes: [ @@ -760,11 +668,7 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -781,11 +685,7 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount * BigInt(-1) }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -794,7 +694,7 @@ describe('StakeCommand', () => { } as any, }).createCommandExecuteContext(command.schema); - lockFn.mockClear(); + tokenLockMock.mockClear(); }); it('should emit ValidatorStakedEvent with STAKE_SUCCESSFUL result', async () => { @@ -817,25 +717,18 @@ describe('StakeCommand', () => { }); it('should not change account balance', async () => { - // Act await command.execute(context); - // Assert - expect(lockFn).toHaveBeenCalledTimes(0); + expect(tokenLockMock).toHaveBeenCalledTimes(0); }); it('should remove stake which has zero amount', async () => { - // Arrange transactionParamsDecoded = { stakes: [ { validatorAddress: validatorAddress1, amount: validator1StakeAmount * BigInt(-1) }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -845,23 +738,17 @@ describe('StakeCommand', () => { const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - // Assert expect(stakerData.stakes).toHaveLength(1); expect(stakerData.stakes[0].validatorAddress).not.toEqual(validatorAddress1); }); it('should update stake which has non-zero amount', async () => { - // Arrange const downStakeAmount = liskToBeddows(10); transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress1, amount: downStakeAmount * BigInt(-1) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -871,9 +758,8 @@ describe('StakeCommand', () => { const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - // Assert expect( - stakerData.stakes.find((v: any) => v.validatorAddress.equals(validatorAddress1)), + stakerData.stakes.find(val => val.validatorAddress.equals(validatorAddress1)), ).toEqual({ validatorAddress: validatorAddress1, amount: validator1StakeAmount - downStakeAmount, @@ -882,12 +768,10 @@ describe('StakeCommand', () => { }); it('should make account to have correct unlocking', async () => { - // Arrange await command.execute(context); const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - // Assert expect(stakerData.pendingUnlocks).toHaveLength(2); expect(stakerData.pendingUnlocks).toEqual( [ @@ -906,20 +790,17 @@ describe('StakeCommand', () => { }); it('should order stakerData.pendingUnlocks', async () => { - // Arrange await command.execute(context); const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - // Assert expect(stakerData.pendingUnlocks).toHaveLength(2); expect(stakerData.pendingUnlocks.map((d: any) => d.validatorAddress)).toEqual( [validatorAddress1, validatorAddress2].sort((a, b) => a.compare(b)), ); }); - it('should make downstaked validator account to have correct totalStakeReceived', async () => { - // Arrange + it('should make downstaked validator account to have correct totalStake', async () => { await command.execute(context); const validatorData1 = await validatorStore.get( @@ -931,29 +812,22 @@ describe('StakeCommand', () => { validatorAddress2, ); - // Assert expect(validatorData1.totalStake).toEqual(BigInt(0)); expect(validatorData2.totalStake).toEqual(BigInt(0)); }); it('should throw error and emit ValidatorStakedEvent with STAKE_FAILED_INVALID_UNSTAKE_PARAMETERS result when downstaked validator is not already upstaked', async () => { - // Arrange const downStakeAmount = liskToBeddows(10); transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress3, amount: downStakeAmount * BigInt(-1) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, }).createCommandExecuteContext(command.schema); - // Assert await expect(command.execute(context)).rejects.toThrow( 'Cannot cast downstake to validator who is not upstaked.', ); @@ -983,11 +857,7 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1004,11 +874,7 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: negativeStakeValidator2 }, ].sort((a, b) => -1 * a.validatorAddress.compare(b.validatorAddress)), }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1017,7 +883,7 @@ describe('StakeCommand', () => { } as any, }).createCommandExecuteContext(command.schema); - lockFn.mockClear(); + tokenLockMock.mockClear(); }); it('should assign reward to staker for downstake and upstake for already staked validator', async () => { @@ -1026,7 +892,7 @@ describe('StakeCommand', () => { expect(mockAssignStakeRewards).toHaveBeenCalledTimes(2); }); - it('should assign sharingCoefficients of the validator to the corresponding sentStake of the staker for that validator', async () => { + it('should assign sharingCoefficients of the validator to the corresponding stake of the staker for that validator', async () => { const sharingCoefficients = [ { tokenID: Buffer.alloc(8), @@ -1038,15 +904,6 @@ describe('StakeCommand', () => { }, ]; - const validator1 = await validatorStore.get( - createStoreGetter(stateStore), - validatorAddress1, - ); - const validator2 = await validatorStore.get( - createStoreGetter(stateStore), - validatorAddress2, - ); - validator1.sharingCoefficients = sharingCoefficients; validator2.sharingCoefficients = sharingCoefficients; @@ -1058,11 +915,11 @@ describe('StakeCommand', () => { const { stakes } = await stakerStore.get(createStoreGetter(stateStore), senderAddress); expect( - stakes.find(sentStake => sentStake.validatorAddress.equals(validatorAddress1)) + stakes.find(stake => stake.validatorAddress.equals(validatorAddress1)) ?.sharingCoefficients, ).toEqual(sharingCoefficients); expect( - stakes.find(sentStake => sentStake.validatorAddress.equals(validatorAddress2)) + stakes.find(stake => stake.validatorAddress.equals(validatorAddress2)) ?.sharingCoefficients, ).toEqual(sharingCoefficients); }); @@ -1071,11 +928,7 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress: validatorAddress3, amount: positiveStakeValidator1 }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1091,19 +944,18 @@ describe('StakeCommand', () => { const validatorAddress = utils.getRandomBytes(20); const selfStake = BigInt(2) + BigInt(defaultConfig.minWeightStandby); - const validatorInfo = { - ...validatorInfo1, + const val = { + ...validator1, selfStake, totalStake: BigInt(1) + BigInt(100) * BigInt(defaultConfig.minWeightStandby), }; const expectedWeight = BigInt(10) * selfStake; - await validatorStore.set(createStoreGetter(stateStore), validatorAddress, validatorInfo); + await validatorStore.set(createStoreGetter(stateStore), validatorAddress, val); + transactionParamsDecoded = { stakes: [{ validatorAddress, amount: positiveStakeValidator1 }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - transaction.params = transactionParams; + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1126,9 +978,7 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress, amount: BigInt(-2) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - transaction.params = transactionParams; + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1148,12 +998,10 @@ describe('StakeCommand', () => { }); it('should make staker to have correct balance', async () => { - // Arrange await command.execute(context); - // Assert - expect(lockFn).toHaveBeenCalledTimes(1); - expect(lockFn).toHaveBeenCalledWith( + expect(tokenLockMock).toHaveBeenCalledTimes(1); + expect(tokenLockMock).toHaveBeenCalledWith( expect.anything(), senderAddress, MODULE_NAME_POS, @@ -1163,11 +1011,9 @@ describe('StakeCommand', () => { }); it('should make staker to have correct unlocking', async () => { - // Arrange await command.execute(context); const stakerData = await stakerStore.get(createStoreGetter(stateStore), senderAddress); - // Assert expect(stakerData.pendingUnlocks).toHaveLength(1); expect(stakerData.pendingUnlocks).toEqual([ { @@ -1178,21 +1024,20 @@ describe('StakeCommand', () => { ]); }); - it('should make upstaked validator account to have correct totalStakeReceived', async () => { - // Arrange + it('should make upstaked validator account to have correct totalStake', async () => { await command.execute(context); - const validatorData1 = await validatorStore.get( + const updatedValidator1 = await validatorStore.get( createStoreGetter(stateStore), validatorAddress1, ); - // Assert - expect(validatorData1.totalStake).toEqual(validator1StakeAmount + positiveStakeValidator1); + expect(updatedValidator1.totalStake).toEqual( + validator1StakeAmount + positiveStakeValidator1, + ); }); - it('should make downstaked validator account to have correct totalStakeReceived', async () => { - // Arrange + it('should make downstaked validator account to have correct totalStake', async () => { await command.execute(context); const validatorData2 = await validatorStore.get( @@ -1200,7 +1045,6 @@ describe('StakeCommand', () => { validatorAddress2, ); - // Assert expect(validatorData2.totalStake).toEqual(validator2StakeAmount + negativeStakeValidator2); }); }); @@ -1213,11 +1057,7 @@ describe('StakeCommand', () => { { validatorAddress: validatorAddress2, amount: validator2StakeAmount }, ].sort((a, b) => -1 * a.validatorAddress.compare(b.validatorAddress)), }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1226,23 +1066,18 @@ describe('StakeCommand', () => { } as any, }).createCommandExecuteContext(command.schema); - lockFn.mockClear(); + tokenLockMock.mockClear(); }); describe('when transaction.params.stakes contain validator address which is not registered', () => { it('should throw error and emit ValidatorStakedEevnt with STAKE_FAILED_NON_REGISTERED_VALIDATOR failure', async () => { - // Arrange const nonExistingValidatorAddress = utils.getRandomBytes(20); transactionParamsDecoded = { ...transactionParamsDecoded, stakes: [{ validatorAddress: nonExistingValidatorAddress, amount: liskToBeddows(76) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1251,7 +1086,6 @@ describe('StakeCommand', () => { } as any, }).createCommandExecuteContext(command.schema); - // Assert await expect(command.execute(context)).rejects.toThrow( 'Invalid stake: no registered validator with the specified address', ); @@ -1271,9 +1105,8 @@ describe('StakeCommand', () => { }); }); - describe('when transaction.params.stakes positive amount makes stakerData.sentStakes entries more than 10', () => { + describe('when transaction.params.stakes positive amount makes StakerData.stakes array contain more than 10 elements', () => { it('should throw error and emit ValidatorStakedEvent with STAKE_FAILED_TOO_MANY_SENT_STAKES failure', async () => { - // Arrange const stakes = []; for (let i = 0; i < 12; i += 1) { @@ -1304,17 +1137,12 @@ describe('StakeCommand', () => { } transactionParamsDecoded = { stakes }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, }).createCommandExecuteContext(command.schema); - // Assert await expect(command.execute(context)).rejects.toThrow('Sender can only stake upto 10.'); checkEventResult( @@ -1332,9 +1160,8 @@ describe('StakeCommand', () => { }); }); - describe('when transaction.params.stakes negative amount decrease stakerData.sentStakes entries yet positive amount makes account exceeds more than 10', () => { + describe('when transaction.params.stakes negative amount decrease StakerData.stakes array entries, yet positive amount makes account exceeds more than 10', () => { it('should throw error and emit ValidatorStakedEvent with STAKE_FAILED_TOO_MANY_SENT_STAKES failure', async () => { - // Arrange const initialValidatorAmount = 8; const stakerData = await stakerStore.getOrDefault( createStoreGetter(stateStore), @@ -1419,17 +1246,12 @@ describe('StakeCommand', () => { // now we added 2 negative stakes and 3 new positive stakes // which will make total positive stakes to grow over 10 transactionParamsDecoded = { stakes }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, }).createCommandExecuteContext(command.schema); - // Assert await expect(command.execute(context)).rejects.toThrow('Sender can only stake upto 10.'); checkEventResult( @@ -1449,7 +1271,6 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes has negative amount and makes stakerData.pendingUnlocks more than 20 entries', () => { it('should throw error and emit ValidatorStakedEvent with STAKE_FAILED_TOO_MANY_PENDING_UNLOCKS failure', async () => { - // Arrange const initialValidatorAmountForUnlocks = 19; const stakerData = await stakerStore.getOrDefault( createStoreGetter(stateStore), @@ -1536,17 +1357,12 @@ describe('StakeCommand', () => { // now we added 2 negative stakes // which will make total unlocking to grow over 20 transactionParamsDecoded = { stakes }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, }).createCommandExecuteContext(command.schema); - // Assert await expect(command.execute(context)).rejects.toThrow( `Pending unlocks cannot exceed ${defaultConfig.maxNumberPendingUnlocks}.`, ); @@ -1570,7 +1386,6 @@ describe('StakeCommand', () => { describe('when transaction.params.stakes negative amount exceeds the previously staked amount', () => { it('should throw error and emit ValidatorStakedEvent with STAKE_FAILED_INVALID_UNSTAKE_PARAMETERS', async () => { - // Arrange const stakerData = await stakerStore.getOrDefault( createStoreGetter(stateStore), senderAddress, @@ -1590,17 +1405,12 @@ describe('StakeCommand', () => { }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, }).createCommandExecuteContext(command.schema); - // Assert await expect(command.execute(context)).rejects.toThrow( 'The unstake amount exceeds the staked amount for this validator.', ); @@ -1631,7 +1441,7 @@ describe('StakeCommand', () => { selfStake = BigInt(20); const validatorInfo = { - ...validatorInfo1, + ...validator1, totalStake, selfStake, }; @@ -1640,11 +1450,7 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress: senderAddress, amount: senderStakeAmountPositive }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1653,11 +1459,10 @@ describe('StakeCommand', () => { } as any, }).createCommandExecuteContext(command.schema); - lockFn.mockClear(); + tokenLockMock.mockClear(); }); - it('should update stakes and totalStakeReceived', async () => { - // Act & Assign + it('should update stakes and totalStake', async () => { await command.execute(context); const validatorData = await validatorStore.get( @@ -1668,10 +1473,10 @@ describe('StakeCommand', () => { createStoreGetter(stateStore), senderAddress, ); - // Assert + expect(validatorData.totalStake).toEqual(totalStake + senderStakeAmountPositive); expect(stakerData.stakes).toHaveLength(1); - expect(lockFn).toHaveBeenCalledWith( + expect(tokenLockMock).toHaveBeenCalledWith( expect.anything(), senderAddress, MODULE_NAME_POS, @@ -1680,21 +1485,19 @@ describe('StakeCommand', () => { ); }); - it('should change validatorData.selfStake and totalStakeReceived with positive stake', async () => { - // Act & Assign + it('should change validatorData.selfStake and totalStake with positive stake', async () => { await command.execute(context); const validatorData = await validatorStore.get( createStoreGetter(stateStore), senderAddress, ); - // Assert + expect(validatorData.totalStake).toEqual(totalStake + senderStakeAmountPositive); expect(validatorData.selfStake).toEqual(selfStake + senderStakeAmountPositive); }); - it('should change validatorData.selfStake, totalStakeReceived and unlocking with negative stake', async () => { - // Act & Assign + it('should change validatorData.selfStake, totalStake and unlocking with negative stake', async () => { await command.execute(context); transactionParamsDecoded = { @@ -1702,11 +1505,7 @@ describe('StakeCommand', () => { { validatorAddress: senderAddress, amount: senderStakeAmountNegative * BigInt(-1) }, ], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1726,7 +1525,6 @@ describe('StakeCommand', () => { senderAddress, ); - // Assert expect(validatorData.totalStake).toEqual( totalStake + senderStakeAmountPositive - senderStakeAmountNegative, ); @@ -1782,11 +1580,7 @@ describe('StakeCommand', () => { transactionParamsDecoded = { stakes: [{ validatorAddress, amount: senderStakeAmountPositive }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1795,34 +1589,28 @@ describe('StakeCommand', () => { } as any, }).createCommandExecuteContext(command.schema); - lockFn.mockClear(); + tokenLockMock.mockClear(); }); - it('should not change validatorData.selfStake but should update totalStakeReceived with positive stake', async () => { - // Act & Assign + it('should not change validatorData.selfStake but should update totalStake with positive stake', async () => { await command.execute(context); const validatorData = await validatorStore.get( createStoreGetter(stateStore), validatorAddress, ); - // Assert + expect(validatorData.totalStake).toEqual(senderStakeAmountPositive + validatorSelfStake); expect(validatorData.selfStake).toEqual(validatorSelfStake); }); - it('should not change validatorData.selfStake but should change totalStakeReceived and unlocking with negative stake', async () => { - // Act & Assign + it('should not change validatorData.selfStake but should change totalStake and unlocking with negative stake', async () => { await command.execute(context); transactionParamsDecoded = { stakes: [{ validatorAddress, amount: senderStakeAmountNegative * BigInt(-1) }], }; - - transactionParams = codec.encode(command.schema, transactionParamsDecoded); - - transaction.params = transactionParams; - + transaction.params = codec.encode(command.schema, transactionParamsDecoded); context = createTransactionContext({ transaction, stateStore, @@ -1842,7 +1630,6 @@ describe('StakeCommand', () => { senderAddress, ); - // Assert expect(validatorData.totalStake).toEqual( senderStakeAmountPositive - senderStakeAmountNegative + validatorSelfStake, ); diff --git a/framework/test/unit/modules/pos/method.spec.ts b/framework/test/unit/modules/pos/method.spec.ts index 17649aa86b2..d21079efc14 100644 --- a/framework/test/unit/modules/pos/method.spec.ts +++ b/framework/test/unit/modules/pos/method.spec.ts @@ -51,6 +51,7 @@ describe('PoSMethod', () => { const tokenMethod: any = { lock: jest.fn() }; const address = utils.getRandomBytes(20); + const invalidAddress = Buffer.alloc(0); const stakerData = { stakes: [ { @@ -150,6 +151,14 @@ describe('PoSMethod', () => { expect(stakerDataReturned).toStrictEqual(stakerData); }); }); + + describe('when input address is invalid', () => { + it('should throw error', async () => { + await expect(posMethod.getStaker(methodContext, invalidAddress)).rejects.toThrow( + invalidAddress.toString('hex'), + ); + }); + }); }); describe('getValidator', () => { @@ -161,6 +170,14 @@ describe('PoSMethod', () => { expect(validatorDataReturned).toStrictEqual(validatorData); }); }); + + describe('when input address is invalid', () => { + it('should throw error', async () => { + await expect(posMethod.getValidator(methodContext, invalidAddress)).rejects.toThrow( + invalidAddress.toString('hex'), + ); + }); + }); }); describe('updateSharedRewards', () => { @@ -357,7 +374,7 @@ describe('PoSMethod', () => { const eligibleValidators = await pos.stores .get(EligibleValidatorsStore) .getAll(methodContext); - expect(eligibleValidators.find(v => v.key.slice(8).equals(address))).toBeDefined(); + expect(eligibleValidators.find(v => v.key.subarray(8).equals(address))).toBeDefined(); }); it('should reject changing status if the validator does not exist', async () => { diff --git a/framework/test/unit/modules/pos/utils.spec.ts b/framework/test/unit/modules/pos/utils.spec.ts index 1a0e942d49c..a4913ebb8af 100644 --- a/framework/test/unit/modules/pos/utils.spec.ts +++ b/framework/test/unit/modules/pos/utils.spec.ts @@ -11,7 +11,7 @@ * * Removal or modification of this copyright notice is prohibited. */ -import { address as cryptoAddress } from '@liskhq/lisk-cryptography'; + import { math } from '@liskhq/lisk-utils'; import { defaultConfig, TOKEN_ID_LENGTH } from '../../../../src/modules/pos/constants'; import { @@ -19,43 +19,11 @@ import { ModuleConfigJSON, StakeSharingCoefficient, } from '../../../../src/modules/pos/types'; -import { - calculateStakeRewards, - getModuleConfig, - shuffleValidatorList, -} from '../../../../src/modules/pos/utils'; -import * as validatorShufflingScenario from '../../../fixtures/pos_validator_shuffling/uniformly_shuffled_validator_list.json'; +import { calculateStakeRewards, getModuleConfig } from '../../../../src/modules/pos/utils'; const { q96 } = math; describe('utils', () => { - describe('shuffleValidatorList', () => { - const { previousRoundSeed1 } = validatorShufflingScenario.testCases.input; - const addressList = [...validatorShufflingScenario.testCases.input.validatorList].map( - address => ({ - address: Buffer.from(address, 'hex'), - weight: BigInt(1), - }), - ); - it('should return a list of uniformly shuffled list of validators', () => { - const shuffledValidatorList = shuffleValidatorList( - Buffer.from(previousRoundSeed1, 'hex'), - addressList, - ); - - expect(shuffledValidatorList).toHaveLength(addressList.length); - shuffledValidatorList.forEach(validator => - expect( - addressList.map(a => cryptoAddress.getLisk32AddressFromAddress(a.address)), - ).toContain(cryptoAddress.getLisk32AddressFromAddress(validator.address)), - ); - - expect(shuffledValidatorList.map(b => b.address.toString('hex'))).toEqual( - validatorShufflingScenario.testCases.output.validatorList, - ); - }); - }); - describe('getModuleConfig', () => { it('converts ModuleConfigJSON to ModuleConfig', () => { const expected: ModuleConfig = { diff --git a/framework/test/unit/modules/random/bitwise_xor_fixtures.ts b/framework/test/unit/modules/random/bitwise_xor_fixtures.ts new file mode 100644 index 00000000000..67cb2bafac7 --- /dev/null +++ b/framework/test/unit/modules/random/bitwise_xor_fixtures.ts @@ -0,0 +1,52 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export const bitwiseXORFixtures = [ + { + input: [Buffer.from([0, 0, 0, 0]), Buffer.from([0, 0, 0, 0])], + output: Buffer.from([0, 0, 0, 0]), + }, + { + input: [Buffer.from([1, 1, 1, 1]), Buffer.from([1, 1, 1, 1])], + output: Buffer.from([0, 0, 0, 0]), + }, + { + input: [Buffer.from([0, 1, 0, 0]), Buffer.from([0, 0, 1, 0])], + output: Buffer.from([0, 1, 1, 0]), + }, + { + input: [Buffer.from([0, 0, 1, 1]), Buffer.from([1, 1, 0, 0])], + output: Buffer.from([1, 1, 1, 1]), + }, + { + input: [ + Buffer.from([0, 0, 1, 1]), + Buffer.from([1, 1, 0, 0]), + Buffer.from([1, 1, 1, 0]), + Buffer.from([1, 0, 0, 0]), + ], + output: Buffer.from([1, 0, 0, 1]), + }, + { + input: [ + Buffer.from([1, 0, 1, 1]), + Buffer.from([0, 1, 1, 0]), + Buffer.from([1, 1, 1, 0]), + Buffer.from([0, 0, 0, 0]), + Buffer.from([1, 1, 1, 0]), + Buffer.from([1, 1, 0, 1]), + ], + output: Buffer.from([0, 0, 0, 0]), + }, +]; diff --git a/framework/test/unit/modules/random/method.spec.ts b/framework/test/unit/modules/random/method.spec.ts index a9e22fe6259..6b53c83726f 100644 --- a/framework/test/unit/modules/random/method.spec.ts +++ b/framework/test/unit/modules/random/method.spec.ts @@ -31,7 +31,7 @@ import { } from '../../../../src/modules/random/stores/validator_reveals'; const strippedHashOfIntegerBuffer = (num: number) => - cryptography.utils.hash(cryptography.utils.intToBuffer(num, 4)).slice(0, SEED_LENGTH); + cryptography.utils.hash(cryptography.utils.intToBuffer(num, 4)).subarray(0, SEED_LENGTH); describe('RandomModuleMethod', () => { let randomMethod: RandomMethod; @@ -291,15 +291,6 @@ describe('RandomModuleMethod', () => { ); }); - it('should throw error when numberOfSeeds is greater than 1000', async () => { - const height = 11; - const numberOfSeeds = 1001; - - await expect(randomMethod.getRandomBytes(context, height, numberOfSeeds)).rejects.toThrow( - 'Number of seeds cannot be greater than 1000.', - ); - }); - it('should throw error when height is non integer input', async () => { const height = 5.1; const numberOfSeeds = 2; @@ -329,10 +320,7 @@ describe('RandomModuleMethod', () => { Buffer.from(genesisValidators.validators[0].hashOnion.hashes[2], 'hex'), ]; // Do XOR of randomSeed with hashes of seed reveal with height >= randomStoreValidator.height >= height + numberOfSeeds - const xorExpected = bitwiseXOR([ - bitwiseXOR([randomSeed, hashesExpected[0]]), - hashesExpected[1], - ]); + const xorExpected = bitwiseXOR([randomSeed, ...hashesExpected]); expect(xorExpected).toHaveLength(16); await expect(randomMethod.getRandomBytes(context, height, numberOfSeeds)).resolves.toEqual( @@ -352,10 +340,7 @@ describe('RandomModuleMethod', () => { Buffer.from(genesisValidators.validators[1].hashOnion.hashes[1], 'hex'), ]; // Do XOR of randomSeed with hashes of seed reveal with height >= randomStoreValidator.height >= height + numberOfSeeds - const xorExpected = bitwiseXOR([ - bitwiseXOR([bitwiseXOR([randomSeed, hashesExpected[0]]), hashesExpected[1]]), - hashesExpected[2], - ]); + const xorExpected = bitwiseXOR([randomSeed, ...hashesExpected]); await expect(randomMethod.getRandomBytes(context, height, numberOfSeeds)).resolves.toEqual( xorExpected, @@ -394,7 +379,7 @@ describe('RandomModuleMethod', () => { Buffer.from(genesisValidators.validators[0].hashOnion.hashes[1], 'hex'), ]; // Do XOR of randomSeed with hashes of seed reveal with height >= randomStoreValidator.height >= height + numberOfSeeds - const xorExpected = bitwiseXOR([randomSeed, hashesExpected[0]]); + const xorExpected = bitwiseXOR([randomSeed, ...hashesExpected]); await expect(randomMethod.getRandomBytes(context, height, numberOfSeeds)).resolves.toEqual( xorExpected, diff --git a/framework/test/unit/modules/random/module.spec.ts b/framework/test/unit/modules/random/module.spec.ts index cc12119f634..c91ff170430 100644 --- a/framework/test/unit/modules/random/module.spec.ts +++ b/framework/test/unit/modules/random/module.spec.ts @@ -756,7 +756,7 @@ describe('RandomModule', () => { const seedHash = (seed: Buffer, times: number) => { let res = seed; for (let i = 0; i < times; i += 1) { - res = utils.hash(res).slice(0, 16); + res = utils.hash(res).subarray(0, 16); } return res; }; diff --git a/framework/test/unit/modules/random/utils.spec.ts b/framework/test/unit/modules/random/utils.spec.ts new file mode 100644 index 00000000000..f284c324a8a --- /dev/null +++ b/framework/test/unit/modules/random/utils.spec.ts @@ -0,0 +1,105 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { utils } from '@liskhq/lisk-cryptography'; +import { bitwiseXOR, isSeedValidInput } from '../../../../src/modules/random/utils'; +import { bitwiseXORFixtures } from './bitwise_xor_fixtures'; +import { ValidatorSeedReveal } from '../../../../src/modules/random/stores/validator_reveals'; +import { SEED_LENGTH, ADDRESS_LENGTH } from '../../../../src/modules/random/constants'; + +describe('Random module utils', () => { + describe('bitwiseXOR', () => { + it('should throw if an empty array is provided as an argument', () => { + expect(() => bitwiseXOR([])).toThrow( + 'bitwiseXOR requires at least one buffer for the input.', + ); + }); + + it('should return the first element if there are no other elements', () => { + const buffer = Buffer.from([0, 1, 1, 1]); + const input = [buffer]; + + expect(bitwiseXOR(input)).toEqual(buffer); + }); + + it.each(bitwiseXORFixtures)('should return correct XOR value', ({ input, output }) => { + expect(bitwiseXOR(input)).toEqual(output); + }); + + it('should throw if input elements have different length', () => { + const input = [Buffer.from([0, 1, 1, 1]), Buffer.from([0, 0, 0, 1, 0])]; + + expect(() => bitwiseXOR(input)).toThrow('All input for XOR should be same size'); + }); + }); + + describe('isSeedValidInput', () => { + const generatorAddress = utils.getRandomBytes(ADDRESS_LENGTH); + const seed = utils.getRandomBytes(SEED_LENGTH); + const previousSeed = utils.hash(seed).subarray(0, SEED_LENGTH); + let validatorSeedReveals: ValidatorSeedReveal[]; + + beforeEach(() => { + let height = 100; + validatorSeedReveals = Array(103) + .fill(0) + .map(() => { + height += 1; + return { + generatorAddress: utils.getRandomBytes(ADDRESS_LENGTH), + seedReveal: utils.getRandomBytes(SEED_LENGTH), + height, + valid: true, + }; + }); + }); + + it('should return true when a matching seed is provided corresponding to the highest seed from the generator', () => { + validatorSeedReveals[88].generatorAddress = generatorAddress; + validatorSeedReveals[88].seedReveal = previousSeed; + + expect(isSeedValidInput(generatorAddress, seed, validatorSeedReveals)).toBe(true); + }); + + it('should return false when a matching seed is provided, but not corresponding to the highest seed from the generator', () => { + validatorSeedReveals[88].generatorAddress = generatorAddress; + validatorSeedReveals[88].seedReveal = previousSeed; + + validatorSeedReveals[99].generatorAddress = generatorAddress; + + expect(isSeedValidInput(generatorAddress, seed, validatorSeedReveals)).toBe(false); + }); + + it('should return false when previous seed exists, but the provided seed does not match', () => { + validatorSeedReveals[88].generatorAddress = generatorAddress; + + expect(isSeedValidInput(generatorAddress, seed, validatorSeedReveals)).toBe(false); + }); + + it('should return false when previous seed is missing and previous seed is required', () => { + expect(isSeedValidInput(generatorAddress, seed, validatorSeedReveals)).toBe(false); + }); + + it('should return true for any provided seed when previous seed is missing, but it is not required', () => { + expect( + isSeedValidInput( + generatorAddress, + utils.getRandomBytes(SEED_LENGTH), + validatorSeedReveals, + false, + ), + ).toBe(true); + }); + }); +}); diff --git a/framework/test/unit/modules/token/cc_commands/cc_transfer.spec.ts b/framework/test/unit/modules/token/cc_commands/cc_transfer.spec.ts index c279e06988b..6b6c660f018 100644 --- a/framework/test/unit/modules/token/cc_commands/cc_transfer.spec.ts +++ b/framework/test/unit/modules/token/cc_commands/cc_transfer.spec.ts @@ -88,6 +88,60 @@ describe('CrossChain Transfer Command', () => { let escrowStore: EscrowStore; let userStore: UserStore; + const createTransactionContextWithOverridingCCMAndParams = ( + { params, ccm }: { params?: Record; ccm?: Record } = { + params: {}, + ccm: {}, + }, + ) => { + const validParams = { + tokenID: defaultTokenID, + amount: defaultAmount, + senderAddress: defaultAddress, + recipientAddress: defaultAddress, + data: 'ddd', + }; + + const finalCCM = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: tokenModule.name, + nonce: BigInt(1), + sendingChainID: Buffer.from([3, 0, 0, 0]), + receivingChainID: Buffer.from([0, 0, 0, 1]), + fee: BigInt(3000), + status: CCM_STATUS_OK, + params: codec.encode(crossChainTransferMessageParams, { + ...validParams, + ...params, + }), + ...ccm, + }; + + const context = { + ccm: finalCCM, + feeAddress: defaultAddress, + transaction: { + senderAddress: defaultAddress, + fee: BigInt(0), + params: defaultEncodedCCUParams, + }, + header: { + height: 0, + timestamp: 0, + }, + stateStore, + contextStore: new Map(), + getMethodContext: () => methodContext, + eventQueue: new EventQueue(0), + ccmSize: BigInt(30), + getStore: (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix), + logger: fakeLogger, + chainID: ownChainID, + }; + + return context; + }; + beforeEach(async () => { method = new TokenMethod(tokenModule.stores, tokenModule.events, tokenModule.name); command = new CrossChainTransferCommand(tokenModule.stores, tokenModule.events); @@ -132,7 +186,7 @@ describe('CrossChain Transfer Command', () => { escrowStore = tokenModule.stores.get(EscrowStore); await escrowStore.set( methodContext, - Buffer.concat([defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), defaultTokenID]), + Buffer.concat([defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultTokenID]), { amount: defaultEscrowAmount }, ); await escrowStore.set( @@ -305,55 +359,6 @@ describe('CrossChain Transfer Command', () => { }); describe('execute', () => { - it('should throw if validation fails', async () => { - // Arrange - const params = codec.encode(crossChainTransferMessageParams, { - tokenID: Buffer.from([0, 0, 0, 1]), - amount: defaultAmount, - senderAddress: defaultAddress, - recipientAddress: defaultAddress, - data: 'ddd', - }); - - const ccm = { - crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, - module: tokenModule.name, - nonce: BigInt(1), - sendingChainID: Buffer.from([3, 0, 0, 0]), - receivingChainID: Buffer.from([0, 0, 0, 1]), - fee: BigInt(30000), - status: CCM_STATUS_OK, - params, - }; - - const ctx = { - ccm, - feeAddress: defaultAddress, - transaction: { - senderAddress: defaultAddress, - fee: BigInt(0), - params: defaultEncodedCCUParams, - }, - header: { - height: 0, - timestamp: 0, - }, - stateStore, - contextStore: new Map(), - getMethodContext: () => methodContext, - eventQueue: new EventQueue(0), - ccmSize: BigInt(30), - getStore: (moduleID: Buffer, prefix: Buffer) => stateStore.getStore(moduleID, prefix), - logger: fakeLogger, - chainID: ownChainID, - }; - - // Act & Assert - await expect(command.execute(ctx)).rejects.toThrow( - `Property '.tokenID' minLength not satisfied`, - ); - }); - it('should throw if fail to decode the CCM', async () => { // Arrange const ccm = { @@ -501,9 +506,9 @@ describe('CrossChain Transfer Command', () => { // Act & Assert await expect(command.execute(ctx)).resolves.toBeUndefined(); - await expect( - method.userSubstoreExists(methodContext, defaultAddress, defaultTokenID), - ).resolves.toBe(true); + const key = userStore.getKey(defaultAddress, defaultTokenID); + const userData = await userStore.get(methodContext, key); + expect(userData.availableBalance).toEqual(defaultAccount.availableBalance + defaultAmount); }); it("should initialize account when recipient user store doesn't exist", async () => { @@ -691,7 +696,7 @@ describe('CrossChain Transfer Command', () => { }); }); - it('should throw when the fee to initialize an account is insufficient', async () => { + it('should throw when escrow account has insufficient balance', async () => { // Arrange const params = codec.encode(crossChainTransferMessageParams, { tokenID: defaultTokenID, @@ -737,5 +742,15 @@ describe('CrossChain Transfer Command', () => { // Act && Assert await expect(command.execute(ctx)).rejects.toThrow('Insufficient balance in escrow account.'); }); + + it('should throw if escrow account does not exist', async () => { + const escrowAccountNotExistingContext = createTransactionContextWithOverridingCCMAndParams({ + params: { tokenID: Buffer.from([0, 0, 0, 1, 0, 2, 3, 8]) }, + }); + + await expect(command.execute(escrowAccountNotExistingContext)).rejects.toThrow( + 'does not exist', + ); + }); }); }); diff --git a/framework/test/unit/modules/token/cc_method.spec.ts b/framework/test/unit/modules/token/cc_method.spec.ts index 788a87144eb..90ad6491dd0 100644 --- a/framework/test/unit/modules/token/cc_method.spec.ts +++ b/framework/test/unit/modules/token/cc_method.spec.ts @@ -146,7 +146,7 @@ describe('TokenInteroperableMethod', () => { escrowStore = tokenModule.stores.get(EscrowStore); await escrowStore.set( methodContext, - Buffer.concat([defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), defaultTokenID]), + Buffer.concat([defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultTokenID]), { amount: defaultEscrowAmount }, ); await escrowStore.set( diff --git a/framework/test/unit/modules/token/commands/transfer.spec.ts b/framework/test/unit/modules/token/commands/transfer.spec.ts index f68e24ca39d..7dbedfee1a9 100644 --- a/framework/test/unit/modules/token/commands/transfer.spec.ts +++ b/framework/test/unit/modules/token/commands/transfer.spec.ts @@ -183,7 +183,7 @@ describe('Transfer command', () => { ).rejects.toThrow(`balance ${availableBalance} is not sufficient for ${amount}`); }); - it('should pass if balance for the provided tokenID is sufficient', async () => { + it('should pass if parameters are valid and balance for the provided tokenID is sufficient', async () => { const amount = BigInt(100000000); jest.spyOn(command['_method'], 'getAvailableBalance').mockResolvedValue(amount); diff --git a/framework/test/unit/modules/token/endpoint.spec.ts b/framework/test/unit/modules/token/endpoint.spec.ts index 738b7b55927..01e0ef89543 100644 --- a/framework/test/unit/modules/token/endpoint.spec.ts +++ b/framework/test/unit/modules/token/endpoint.spec.ts @@ -316,6 +316,24 @@ describe('token endpoint', () => { }); describe('isSupported', () => { + it('should return true for a native token', async () => { + const moduleEndpointContext = createTransientModuleEndpointContext({ + stateStore, + params: { tokenID: nativeTokenID.toString('hex') }, + }); + + expect(await endpoint.isSupported(moduleEndpointContext)).toEqual({ supported: true }); + }); + + it('should return true for LSK', async () => { + const moduleEndpointContext = createTransientModuleEndpointContext({ + stateStore, + params: { tokenID: mainChainTokenID.toString('hex') }, + }); + + expect(await endpoint.isSupported(moduleEndpointContext)).toEqual({ supported: true }); + }); + it('should return true for a supported token', async () => { await supportedTokensStore.set(methodContext, foreignChainID, { supportedTokenIDs: supportedForeignChainTokenIDs, diff --git a/framework/test/unit/modules/token/init_genesis_state_fixture.ts b/framework/test/unit/modules/token/init_genesis_state_fixture.ts index 1c508242771..1309b6aebff 100644 --- a/framework/test/unit/modules/token/init_genesis_state_fixture.ts +++ b/framework/test/unit/modules/token/init_genesis_state_fixture.ts @@ -87,6 +87,22 @@ export const validGenesisAssets = [ ]; export const invalidGenesisAssets = [ + [ + 'minimum token id length not satisfied', + { + ...validData, + userSubstore: [ + { + address: Buffer.alloc(20, 0), + tokenID: Buffer.from([9, 0, 0]), + availableBalance: oneUnit, + lockedBalances: [{ module: 'pos', amount: oneUnit }], + }, + ...validData.userSubstore.slice(1), + ], + }, + "tokenID' minLength not satisfied", + ], [ 'Invalid address length', { @@ -104,7 +120,7 @@ export const invalidGenesisAssets = [ ".address' address length invalid", ], [ - 'Invalid token id length', + 'maximum token id length', { ...validData, userSubstore: [ @@ -120,28 +136,28 @@ export const invalidGenesisAssets = [ "tokenID' maxLength exceeded", ], [ - 'Overflow uint64 for available balance', + 'Unsorted userstore by address', { ...validData, userSubstore: [ { - address: Buffer.alloc(20, 0), - tokenID: Buffer.from([9, 0, 0, 0, 0, 0, 0, 0, 0]), - availableBalance: BigInt('1000000000000000000000000000'), + address: Buffer.alloc(20, 9), + tokenID: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), + availableBalance: BigInt('1000'), lockedBalances: [{ module: 'pos', amount: oneUnit }], }, ...validData.userSubstore.slice(1), ], }, - 'Value out of range of uint64', + 'UserSubstore must be sorted by address and tokenID', ], [ - 'Unsorted userstore by address', + 'Unsorted tokens in userstore by address', { ...validData, userSubstore: [ { - address: Buffer.alloc(20, 9), + address: Buffer.alloc(20, 1), tokenID: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), availableBalance: BigInt('1000'), lockedBalances: [{ module: 'pos', amount: oneUnit }], @@ -227,6 +243,66 @@ export const invalidGenesisAssets = [ }, 'SupplySubstore must be sorted by tokenID', ], + [ + 'escrowChainID minimum length not satisified for escrowSubstore', + { + ...validData, + escrowSubstore: [ + ...validData.escrowSubstore, + { + escrowChainID: Buffer.from([0, 0, 0]), + tokenID: Buffer.from([0, 0, 0, 0, 0, 0, 1, 0]), + amount: oneUnit, + }, + ], + }, + ".escrowChainID' minLength not satisfied", + ], + [ + 'escrowChainID maximum length not exceeded for escrowSubstore', + { + ...validData, + escrowSubstore: [ + ...validData.escrowSubstore, + { + escrowChainID: Buffer.from([0, 0, 0, 0, 0]), + tokenID: Buffer.from([0, 0, 0, 0, 0, 0, 1, 0]), + amount: oneUnit, + }, + ], + }, + ".escrowChainID' maxLength exceeded", + ], + [ + 'tokenID minimum length not satisfied for escrowSubstore', + { + ...validData, + escrowSubstore: [ + ...validData.escrowSubstore, + { + escrowChainID: Buffer.from([0, 0, 0, 0, 0]), + tokenID: Buffer.from([0, 0, 0, 0, 0]), + amount: oneUnit, + }, + ], + }, + ".tokenID' minLength not satisfied", + ], + [ + 'tokenID maximum length exceeded for escrowSubstore', + { + ...validData, + escrowSubstore: [ + ...validData.escrowSubstore, + { + escrowChainID: Buffer.from([0, 0, 0, 0, 0]), + tokenID: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + amount: oneUnit, + }, + ], + }, + ".tokenID' maxLength exceeded", + ], [ 'Duplicate escrow store', { @@ -404,7 +480,7 @@ export const invalidGenesisAssets = [ { chainID: Buffer.from([0, 0, 0, 4]), supportedTokenIDs: [ - Buffer.from([0, 0, 0, 4, 0, 0, 0, 0]), + Buffer.from([0, 0, 0, 4, 0, 0, 0, 1]), Buffer.from([0, 0, 0, 4, 0, 0, 0, 0]), ], }, diff --git a/framework/test/unit/modules/token/method.spec.ts b/framework/test/unit/modules/token/method.spec.ts index 6512b4a7aff..4b154c4a786 100644 --- a/framework/test/unit/modules/token/method.spec.ts +++ b/framework/test/unit/modules/token/method.spec.ts @@ -131,7 +131,7 @@ describe('token module', () => { const escrowStore = tokenModule.stores.get(EscrowStore); await escrowStore.set( methodContext, - Buffer.concat([defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), defaultTokenID]), + Buffer.concat([defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultTokenID]), { amount: defaultEscrowAmount }, ); }); @@ -202,7 +202,7 @@ describe('token module', () => { await expect( method.getEscrowedAmount( methodContext, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultForeignTokenID, ), ).rejects.toThrow('Only native token can have escrow amount'); @@ -218,7 +218,7 @@ describe('token module', () => { await expect( method.getEscrowedAmount( methodContext, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), Buffer.from([0, 0, 0, 1, 0, 0, 0, 1]), ), ).resolves.toBe(BigInt(0)); @@ -228,7 +228,7 @@ describe('token module', () => { await expect( method.getEscrowedAmount( methodContext, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultTokenID, ), ).resolves.toEqual(defaultEscrowAmount); @@ -239,22 +239,36 @@ describe('token module', () => { const tokenID = Buffer.concat([ownChainID, Buffer.alloc(4, 255)]); it('should reject if token is not native', async () => { - await expect( - method.initializeToken(methodContext, Buffer.from([2, 0, 0, 0, 0, 0, 0, 0])), - ).rejects.toThrow('Only native token can be initialized'); + try { + await method.initializeToken(methodContext, Buffer.from([2, 0, 0, 0, 0, 0, 0, 0])); + } catch (e: any) { + expect(e.message).toBe('Only native token can be initialized.'); + checkEventResult( + methodContext.eventQueue, + InitializeTokenEvent, + TokenEventResult.TOKEN_ID_NOT_NATIVE, + ); + } }); it('should reject if there is no available local ID', async () => { - const supplyStore = tokenModule.stores.get(SupplyStore); - await supplyStore.set(methodContext, tokenID, { - totalSupply: defaultTotalSupply, - }); - await expect(method.initializeToken(methodContext, tokenID)).rejects.toThrow( - 'The specified token ID is not available', - ); - }); - - it('log initialize token event', async () => { + try { + const supplyStore = tokenModule.stores.get(SupplyStore); + await supplyStore.set(methodContext, tokenID, { + totalSupply: defaultTotalSupply, + }); + await method.initializeToken(methodContext, tokenID); + } catch (e: any) { + expect(e.message).toBe('The specified token ID is not available.'); + checkEventResult( + methodContext.eventQueue, + InitializeTokenEvent, + TokenEventResult.TOKEN_ID_NOT_AVAILABLE, + ); + } + }); + + it('logs initialize token event', async () => { await method.initializeToken(methodContext, tokenID); expect(methodContext.eventQueue.getEvents()).toHaveLength(1); checkEventResult(methodContext.eventQueue, InitializeTokenEvent, TokenEventResult.SUCCESSFUL); @@ -293,7 +307,7 @@ describe('token module', () => { ); }); - it('should reject if supply exceed max balance', async () => { + it('should reject if supply exceeds maximum range allowed', async () => { await expect( method.mint( methodContext, @@ -497,7 +511,7 @@ describe('token module', () => { await expect( method.initializeEscrowAccount( methodContext, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultTokenID, ), ).resolves.toBeUndefined(); @@ -664,7 +678,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), defaultTokenID, BigInt('100'), @@ -730,7 +744,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, newAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), defaultTokenID, BigInt('100'), @@ -769,7 +783,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, newAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), defaultTokenID, BigInt('100'), @@ -798,7 +812,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, newAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), defaultTokenID, BigInt('100'), @@ -833,7 +847,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), unknownToken, BigInt(100000), @@ -873,7 +887,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), defaultTokenID, BigInt('100000'), @@ -887,7 +901,7 @@ describe('token module', () => { const escrowStore = tokenModule.stores.get(EscrowStore); const { amount } = await escrowStore.get( methodContext, - escrowStore.getKey(defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), defaultTokenID), + escrowStore.getKey(defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultTokenID), ); expect(amount).toEqual(defaultEscrowAmount + BigInt('100000')); checkEventResult( @@ -903,7 +917,7 @@ describe('token module', () => { await method.transferCrossChain( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), recipient, defaultTokenID, BigInt('100000'), @@ -916,7 +930,7 @@ describe('token module', () => { defaultAddress, 'token', CROSS_CHAIN_COMMAND_NAME_TRANSFER, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), BigInt('10000'), codec.encode(crossChainTransferMessageParams, { tokenID: defaultTokenID, @@ -938,7 +952,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), defaultTokenID, BigInt(0), @@ -953,7 +967,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), defaultTokenID, BigInt(-1), @@ -968,7 +982,7 @@ describe('token module', () => { method.transferCrossChain( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), utils.getRandomBytes(20), defaultTokenID, BigInt('100000'), @@ -1179,7 +1193,7 @@ describe('token module', () => { method.payMessageFee( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), BigInt(-1), ), ).rejects.toThrow('Invalid Message Fee'); @@ -1190,7 +1204,7 @@ describe('token module', () => { method.payMessageFee( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultAccount.availableBalance + BigInt(1), ), ).rejects.toThrow('does not have sufficient balance'); @@ -1201,14 +1215,14 @@ describe('token module', () => { method.payMessageFee( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), BigInt(100), ), ).resolves.toBeUndefined(); const escrowStore = tokenModule.stores.get(EscrowStore); const { amount } = await escrowStore.get( methodContext, - escrowStore.getKey(defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), defaultTokenID), + escrowStore.getKey(defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), defaultTokenID), ); expect(amount).toEqual(defaultEscrowAmount + BigInt('100')); }); @@ -1218,7 +1232,7 @@ describe('token module', () => { method.payMessageFee( methodContext, defaultAddress, - defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH), + defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH), BigInt(100), ), ).resolves.toBeUndefined(); @@ -1230,8 +1244,10 @@ describe('token module', () => { describe('supportAllTokens', () => { it('should call support all token', async () => { + const supportAllSpy = jest.spyOn(tokenModule.stores.get(SupportedTokensStore), 'supportAll'); await expect(method.supportAllTokens(methodContext)).resolves.toBeUndefined(); + expect(supportAllSpy).toHaveBeenCalledOnce(); expect(methodContext.eventQueue.getEvents()).toHaveLength(1); expect(methodContext.eventQueue.getEvents()[0].toObject().name).toEqual( new AllTokensSupportedEvent('token').name, @@ -1241,9 +1257,13 @@ describe('token module', () => { describe('removeAllTokensSupport', () => { it('should call remove support all token', async () => { - await tokenModule.stores.get(SupportedTokensStore).supportAll(methodContext); + const supportedTokensStore = tokenModule.stores.get(SupportedTokensStore); + + await supportedTokensStore.supportAll(methodContext); + const removeAllSpy = jest.spyOn(supportedTokensStore, 'removeAll'); await expect(method.removeAllTokensSupport(methodContext)).resolves.toBeUndefined(); + expect(removeAllSpy).toHaveBeenCalledOnce(); expect(methodContext.eventQueue.getEvents()).toHaveLength(1); expect(methodContext.eventQueue.getEvents()[0].toObject().name).toEqual( new AllTokensSupportRemovedEvent('token').name, @@ -1371,10 +1391,15 @@ describe('token module', () => { describe('supportTokenID', () => { it('should call support token', async () => { + const supportTokenSpy = jest.spyOn( + tokenModule.stores.get(SupportedTokensStore), + 'supportToken', + ); await expect( method.supportTokenID(methodContext, Buffer.from([1, 2, 3, 4, 0, 0, 0, 0])), ).resolves.toBeUndefined(); + expect(supportTokenSpy).toHaveBeenCalledOnce(); expect(methodContext.eventQueue.getEvents()).toHaveLength(1); expect(methodContext.eventQueue.getEvents()[0].toObject().name).toEqual( new TokenIDSupportedEvent('token').name, @@ -1481,7 +1506,7 @@ describe('token module', () => { }); describe('escrowSubstoreExists', () => { - const escrowChainID = defaultForeignTokenID.slice(0, CHAIN_ID_LENGTH); + const escrowChainID = defaultForeignTokenID.subarray(0, CHAIN_ID_LENGTH); it('should return false if escrow subStore does not exist for the given chain id and token id', async () => { const escrowStore = tokenModule.stores.get(EscrowStore); @@ -1497,4 +1522,14 @@ describe('token module', () => { ).resolves.toBeTrue(); }); }); + + describe('isTokenIDAvailable', () => { + it('should return true if provided tokenID exists in SupplyStore', async () => { + await expect(method.isTokenSupported(methodContext, defaultTokenID)).resolves.toBeTrue(); + }); + + it('should return false if provided tokenID does not exist in SupplyStore', async () => { + await expect(method.isTokenSupported(methodContext, Buffer.alloc(8, 1))).resolves.toBeFalse(); + }); + }); }); diff --git a/framework/test/unit/modules/token/stores/supported_tokens.spec.ts b/framework/test/unit/modules/token/stores/supported_tokens.spec.ts index 383dd63361a..908476ee0b8 100644 --- a/framework/test/unit/modules/token/stores/supported_tokens.spec.ts +++ b/framework/test/unit/modules/token/stores/supported_tokens.spec.ts @@ -104,6 +104,12 @@ describe('SupportedTokensStore', () => { store.isSupported(context, Buffer.from([1, 1, 1, 1, 0, 0, 0, 0])), ).resolves.toBeTrue(); }); + + it('should return true if all tokens are supported', async () => { + await store.supportAll(context); + + await expect(store.isSupported(context, Buffer.alloc(8))).resolves.toBeTrue(); + }); }); describe('supportAll', () => { @@ -160,6 +166,21 @@ describe('SupportedTokensStore', () => { await expect( store.isSupported(context, Buffer.from([2, 0, 0, 0, 0, 0, 0, 0])), ).resolves.toBeTrue(); + + await expect(store.get(context, Buffer.from([2, 0, 0, 0]))).resolves.toEqual({ + supportedTokenIDs: [], + }); + }); + + it('should update data with empty list if tokens are supported', async () => { + const chainID = Buffer.from([2, 0, 0, 0]); + const tokenID = Buffer.concat([chainID, Buffer.from([1, 1, 1, 1])]); + + await store.set(context, chainID, { supportedTokenIDs: [tokenID] }); + + await store.supportChain(context, chainID); + + await expect(store.get(context, chainID)).resolves.toEqual({ supportedTokenIDs: [] }); }); }); @@ -270,16 +291,18 @@ describe('SupportedTokensStore', () => { describe('removeSupportForToken', () => { it('should reject if chain is native', async () => { - await store.set(context, Buffer.from([1, 1, 1, 1]), { - supportedTokenIDs: [Buffer.from([1, 1, 1, 1, 0, 0, 0, 0])], - }); - await expect( store.removeSupportForToken(context, Buffer.concat([ownChainID, Buffer.alloc(4)])), ).rejects.toThrow('Cannot remove support for LSK or native token.'); }); - it('should not do anything if all tokens are supported', async () => { + it('should reject if token is LSK', async () => { + await expect( + store.removeSupportForToken(context, Buffer.from([1, 0, 0, 0, 0, 0, 0, 0])), + ).rejects.toThrow('Cannot remove support for LSK or native token.'); + }); + + it('should reject if all tokens are supported', async () => { await store.set(context, ALL_SUPPORTED_TOKENS_KEY, { supportedTokenIDs: [] }); const tokenID = Buffer.from([2, 0, 0, 0, 1, 0, 0, 0]); await expect(store.removeSupportForToken(context, tokenID)).rejects.toThrow( @@ -289,7 +312,7 @@ describe('SupportedTokensStore', () => { await expect(store.allSupported(context)).resolves.toBeTrue(); }); - it('should remove data if only the tokenID removed is supported', async () => { + it('should remove chain from supported tokens if the only supported tokenID is removed', async () => { const tokenID = Buffer.from([1, 1, 1, 1, 1, 0, 0, 0]); await store.set(context, Buffer.from([1, 1, 1, 1]), { supportedTokenIDs: [tokenID], @@ -299,7 +322,7 @@ describe('SupportedTokensStore', () => { await expect(store.has(context, Buffer.from([1, 1, 1, 1]))).resolves.toBeFalse(); }); - it('should remove data if the tokenID and keep other supported tokens', async () => { + it('should remove supported tokenID and keep other supported tokens', async () => { const tokenID = Buffer.from([1, 1, 1, 1, 1, 0, 0, 0]); await store.set(context, Buffer.from([1, 1, 1, 1]), { supportedTokenIDs: [ @@ -319,10 +342,37 @@ describe('SupportedTokensStore', () => { }); }); - it('should return undefined if support does not exist', async () => { + it('should not modify supported tokens store if a token that is not supported is the input', async () => { + const chainID = Buffer.from([1, 1, 1, 1]); + const notSupportedToken = Buffer.concat([chainID, Buffer.from([1, 0, 0, 0])]); + + const supportedTokensStoreState = { + supportedTokenIDs: [ + Buffer.concat([chainID, Buffer.from([0, 0, 1, 1])]), + Buffer.concat([chainID, Buffer.from([1, 0, 1, 1])]), + ], + }; + + await store.set(context, chainID, supportedTokensStoreState); + + await store.removeSupportForToken(context, notSupportedToken); + + await expect(store.get(context, chainID)).resolves.toEqual(supportedTokensStoreState); + }); + + it('should not modify store if support does not exist', async () => { await expect( store.removeSupportForToken(context, Buffer.from([1, 1, 1, 1, 1, 0, 0, 0])), ).resolves.toBeUndefined(); + + const chainID = Buffer.from([1, 1, 1, 1]); + const tokenID = Buffer.from([1, 0, 0, 0]); + + await expect( + store.removeSupportForToken(context, Buffer.concat([chainID, tokenID])), + ).resolves.toBeUndefined(); + + await expect(store.has(context, chainID)).resolves.toBeFalse(); }); it('should reject if the supported tokens array length is 0', async () => { @@ -334,15 +384,5 @@ describe('SupportedTokensStore', () => { store.removeSupportForToken(context, Buffer.from([1, 1, 1, 1, 1, 0, 0, 0])), ).rejects.toThrow('All tokens from the specified chain are supported.'); }); - - it('should remove token from supported tokens if a token with value tokenID exists', async () => { - const tokenID = Buffer.from([1, 1, 1, 1, 1, 0, 0, 0]); - await store.set(context, Buffer.from([1, 1, 1, 1]), { - supportedTokenIDs: [tokenID], - }); - - await expect(store.removeSupportForToken(context, tokenID)).resolves.toBeUndefined(); - await expect(store.has(context, Buffer.from([1, 1, 1, 1]))).resolves.toBeFalse(); - }); }); }); diff --git a/framework/test/unit/modules/utils/shuffleValidatorList.spec.ts b/framework/test/unit/modules/utils/shuffleValidatorList.spec.ts new file mode 100644 index 00000000000..dbe2760bb7d --- /dev/null +++ b/framework/test/unit/modules/utils/shuffleValidatorList.spec.ts @@ -0,0 +1,44 @@ +/* + * Copyright © 2023 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { address as cryptoAddress } from '@liskhq/lisk-cryptography'; +import * as validatorShufflingScenario from '../../../fixtures/pos_validator_shuffling/uniformly_shuffled_validator_list.json'; +import { shuffleValidatorList } from '../../../../src/modules/utils'; + +describe('shuffleValidatorList', () => { + const { previousRoundSeed1 } = validatorShufflingScenario.testCases.input; + const addressList = [...validatorShufflingScenario.testCases.input.validatorList].map( + address => ({ + address: Buffer.from(address, 'hex'), + weight: BigInt(1), + }), + ); + it('should return a list of uniformly shuffled list of validators', () => { + const shuffledValidatorList = shuffleValidatorList( + Buffer.from(previousRoundSeed1, 'hex'), + addressList, + ); + + expect(shuffledValidatorList).toHaveLength(addressList.length); + shuffledValidatorList.forEach(validator => + expect(addressList.map(a => cryptoAddress.getLisk32AddressFromAddress(a.address))).toContain( + cryptoAddress.getLisk32AddressFromAddress(validator.address), + ), + ); + + expect(shuffledValidatorList.map(b => b.address.toString('hex'))).toEqual( + validatorShufflingScenario.testCases.output.validatorList, + ); + }); +}); diff --git a/framework/test/unit/modules/validators/endpoint.spec.ts b/framework/test/unit/modules/validators/endpoint.spec.ts index 8b933bb67d9..b61339acf42 100644 --- a/framework/test/unit/modules/validators/endpoint.spec.ts +++ b/framework/test/unit/modules/validators/endpoint.spec.ts @@ -35,10 +35,12 @@ describe('ValidatorsModuleEndpoint', () => { const validatorAddress = utils.getRandomBytes(ADDRESS_LENGTH); const blsKey = utils.getRandomBytes(BLS_PUBLIC_KEY_LENGTH); const generatorKey = utils.getRandomBytes(ED25519_PUBLIC_KEY_LENGTH); + const validBLSKey = + 'b301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81'; const validProof = '88bb31b27eae23038e14f9d9d1b628a39f5881b5278c3c6f0249f81ba0deb1f68aa5f8847854d6554051aa810fdf1cdb02df4af7a5647b1aa4afb60ec6d446ee17af24a8a50876ffdaf9bf475038ec5f8ebeda1c1c6a3220293e23b13a9a5d26'; - beforeAll(() => { + beforeEach(() => { validatorsModule = new ValidatorsModule(); stateStore = new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()); }); @@ -49,14 +51,16 @@ describe('ValidatorsModuleEndpoint', () => { const context = createTransientModuleEndpointContext({ stateStore, params: { - proofOfPossession: proof.toString('hex'), - blsKey: blsKey.toString('hex'), + proofOfPossession: validProof, + blsKey: validBLSKey, }, }); - await validatorsModule.stores.get(BLSKeyStore).set(createStoreGetter(stateStore), blsKey, { - address: utils.getRandomBytes(ADDRESS_LENGTH), - }); + await validatorsModule.stores + .get(BLSKeyStore) + .set(createStoreGetter(stateStore), Buffer.from(validBLSKey, 'hex'), { + address: utils.getRandomBytes(ADDRESS_LENGTH), + }); await expect(validatorsModule.endpoint.validateBLSKey(context)).resolves.toStrictEqual({ valid: false, @@ -81,8 +85,7 @@ describe('ValidatorsModuleEndpoint', () => { stateStore, params: { proofOfPossession: validProof, - blsKey: - 'b301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81', + blsKey: validBLSKey, }, }); await expect(validatorsModule.endpoint.validateBLSKey(context)).resolves.toStrictEqual({ @@ -91,7 +94,7 @@ describe('ValidatorsModuleEndpoint', () => { }); it('should resolve with false when proof of possession is invalid but bls key has a valid length', async () => { - const validBLSKey = + const anotherValidBLSKey = 'a491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a'; const invalidProof = 'b803eb0ed93ea10224a73b6b9c725796be9f5fefd215ef7a5b97234cc956cf6870db6127b7e4d824ec62276078e787db05584ce1adbf076bc0808ca0f15b73d59060254b25393d95dfc7abe3cda566842aaedf50bbb062aae1bbb6ef3b1fffff'; @@ -99,7 +102,7 @@ describe('ValidatorsModuleEndpoint', () => { stateStore, params: { proofOfPossession: invalidProof, - blsKey: validBLSKey, + blsKey: anotherValidBLSKey, }, }); await expect(validatorsModule.endpoint.validateBLSKey(context)).resolves.toStrictEqual({ diff --git a/framework/test/unit/modules/validators/method.spec.ts b/framework/test/unit/modules/validators/method.spec.ts index 888076ffd2c..d533e835e44 100644 --- a/framework/test/unit/modules/validators/method.spec.ts +++ b/framework/test/unit/modules/validators/method.spec.ts @@ -13,7 +13,7 @@ */ import { codec } from '@liskhq/lisk-codec'; -import { utils } from '@liskhq/lisk-cryptography'; +import { utils, address as addressUtils } from '@liskhq/lisk-cryptography'; import { ValidatorsMethod, ValidatorsModule } from '../../../../src/modules/validators'; import { MODULE_NAME_VALIDATORS, @@ -151,6 +151,8 @@ describe('ValidatorsModuleMethod', () => { [address], false, ); + await expect(validatorsSubStore.has(methodContext, address)).resolves.toBe(true); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(true); }); it('should not be able to create new validator account if validator address already exists, bls key is not registered and proof of possession is valid', async () => { @@ -180,6 +182,7 @@ describe('ValidatorsModuleMethod', () => { [address], true, ); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(false); }); it('should not be able to create new validator account if validator address does not exist, bls key is already registered and proof of possession is valid', async () => { @@ -208,6 +211,7 @@ describe('ValidatorsModuleMethod', () => { [address], true, ); + await expect(validatorsSubStore.has(methodContext, address)).resolves.toBe(false); }); it('should not be able to create new validator account if validator address does not exist, bls key is not registered and proof of possession is invalid', async () => { @@ -233,6 +237,8 @@ describe('ValidatorsModuleMethod', () => { [address], true, ); + await expect(validatorsSubStore.has(methodContext, address)).resolves.toBe(false); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(false); }); it('should not be able to register validator keys if validator address does not exist, bls key is not registered and proof of possession is valid but validatorAddress is shorter than 20 bytes', async () => { @@ -245,8 +251,8 @@ describe('ValidatorsModuleMethod', () => { proofOfPossession, ), ).rejects.toThrow(`Validator address must be ${ADDRESS_LENGTH} bytes long.`); - await expect(validatorsSubStore.get(methodContext, invalidAddressShort)).rejects.toThrow(); - await expect(blsKeysSubStore.get(methodContext, blsKey)).rejects.toThrow(); + await expect(validatorsSubStore.has(methodContext, invalidAddressShort)).resolves.toBe(false); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(false); }); it('should not be able to register validator keys if validator address does not exist, bls key is not registered and proof of possession is valid but validatorAddress is longer than 20 bytes', async () => { @@ -259,8 +265,8 @@ describe('ValidatorsModuleMethod', () => { proofOfPossession, ), ).rejects.toThrow(`Validator address must be ${ADDRESS_LENGTH} bytes long.`); - await expect(validatorsSubStore.get(methodContext, invalidAddressLong)).rejects.toThrow(); - await expect(blsKeysSubStore.get(methodContext, blsKey)).rejects.toThrow(); + await expect(validatorsSubStore.has(methodContext, invalidAddressLong)).resolves.toBe(false); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(false); }); it('should not be able to register validator keys if validator address does not exist, bls key is not registered and proof of possession is valid but generator key is shorter than 32 bytes', async () => { @@ -273,8 +279,8 @@ describe('ValidatorsModuleMethod', () => { proofOfPossession, ), ).rejects.toThrow(); - await expect(validatorsSubStore.get(methodContext, address)).rejects.toThrow(); - await expect(blsKeysSubStore.get(methodContext, blsKey)).rejects.toThrow(); + await expect(validatorsSubStore.has(methodContext, address)).resolves.toBe(false); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(false); }); it('should not be able to register validator keys if validator address does not exist, bls key is not registered and proof of possession is valid but generator key is longer than 32 bytes', async () => { @@ -287,8 +293,8 @@ describe('ValidatorsModuleMethod', () => { proofOfPossession, ), ).rejects.toThrow(); - await expect(validatorsSubStore.get(methodContext, address)).rejects.toThrow(); - await expect(blsKeysSubStore.get(methodContext, blsKey)).rejects.toThrow(); + await expect(validatorsSubStore.has(methodContext, address)).resolves.toBe(false); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(false); }); it('should not be able to register validator keys if validator address does not exist, bls key is not registered and proof of possession is valid but bls key is shorter than 48 bytes', async () => { @@ -301,8 +307,8 @@ describe('ValidatorsModuleMethod', () => { proofOfPossession, ), ).rejects.toThrow(); - await expect(validatorsSubStore.get(methodContext, address)).rejects.toThrow(); - await expect(blsKeysSubStore.get(methodContext, invalidBlsKeyShort)).rejects.toThrow(); + await expect(validatorsSubStore.has(methodContext, address)).resolves.toBe(false); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(false); }); it('should not be able to register validator keys if validator address does not exist, bls key is not registered and proof of possession is valid but bls key is longer than 48 bytes', async () => { @@ -315,8 +321,8 @@ describe('ValidatorsModuleMethod', () => { proofOfPossession, ), ).rejects.toThrow(); - await expect(validatorsSubStore.get(methodContext, address)).rejects.toThrow(); - await expect(blsKeysSubStore.get(methodContext, invalidBlsKeyLong)).rejects.toThrow(); + await expect(validatorsSubStore.has(methodContext, address)).resolves.toBe(false); + await expect(blsKeysSubStore.has(methodContext, blsKey)).resolves.toBe(false); }); }); @@ -533,8 +539,9 @@ describe('ValidatorsModuleMethod', () => { }); it('should be able to correctly set generator key for validator if address exists', async () => { + const anotherGeneratorKey = utils.getRandomBytes(ED25519_PUBLIC_KEY_LENGTH); const generatorEventData = codec.encode(generatorKeyRegDataSchema, { - generatorKey, + generatorKey: anotherGeneratorKey, result: KeyRegResult.SUCCESS, }); const validatorAccount = { @@ -546,12 +553,12 @@ describe('ValidatorsModuleMethod', () => { const isSet = await validatorsModule.method.setValidatorGeneratorKey( methodContext, address, - generatorKey, + anotherGeneratorKey, ); const setValidatorAccount = await validatorsSubStore.get(methodContext, address); expect(isSet).toBe(true); - expect(setValidatorAccount.generatorKey.equals(generatorKey)).toBe(true); + expect(setValidatorAccount.generatorKey.equals(anotherGeneratorKey)).toBe(true); expect(methodContext.eventQueue.add).toHaveBeenCalledWith( MODULE_NAME_VALIDATORS, validatorsModule.events.get(GeneratorKeyRegistrationEvent).name, @@ -673,14 +680,14 @@ describe('ValidatorsModuleMethod', () => { ).resolves.toBeObject(); }); - it('should be able to return generators with at least one generator assigned more than one slot if input timestamps are valid and difference between input timestamps is greater than one round', async () => { + it('should be able to return generators with at least one generator assigned more than one slot if input timestamps are valid and difference between input timestamps is greater than or equal to one round plus two blocks', async () => { const validatorsPerRound = 101; const timePerRound = validatorsPerRound * blockTime; const result = await validatorsModule.method.getGeneratorsBetweenTimestamps( methodContext, 0, - timePerRound + 2 * blockTime + 1, + timePerRound + 2 * blockTime, ); let genWithCountGreaterThanOne = 0; for (const generatorAddress of Object.keys(result)) { @@ -692,14 +699,14 @@ describe('ValidatorsModuleMethod', () => { expect(genWithCountGreaterThanOne).toBeGreaterThan(0); }); - it('should be able to return with all generators assigned at least 2 slots and at least one generator assigned more than 2 slots if input timestamps are valid and difference between input timestamps is greater than 2 rounds', async () => { + it('should be able to return with all generators assigned at least 2 slots and at least one generator assigned more than 2 slots if input timestamps are valid and difference between timestamps is larger or equal to length of two rounds plus two block slots', async () => { const validatorsPerRound = 101; const timePerRound = validatorsPerRound * blockTime; const result = await validatorsModule.method.getGeneratorsBetweenTimestamps( methodContext, 0, - timePerRound * 2 + 2 * blockTime + 1, + timePerRound * 2 + 2 * blockTime, ); let genWithCountGreaterThanOne = 0; @@ -762,11 +769,37 @@ describe('ValidatorsModuleMethod', () => { ).resolves.toEqual({}); }); - it('should return empty result when startSlotNumber equals endSlotNumber but in the same block slot', async () => { + it('should return empty result when startTimestamp equals endTimestamp', async () => { await expect( validatorsModule.method.getGeneratorsBetweenTimestamps(methodContext, 2, 2), ).resolves.toEqual({}); }); + + it('should return 3 generators from indicies 100, 0 and 1 of generator list, all having assigned 1 slot', async () => { + const { validators } = await validatorsParamsSubStore.get(methodContext, EMPTY_KEY); + const generatorAddressesInStore = validators.map(validator => + validator.address.toString('binary'), + ); + const expectedGenerators = [ + generatorAddressesInStore[100], + generatorAddressesInStore[0], + generatorAddressesInStore[1], + ]; + + const result = await validatorsModule.method.getGeneratorsBetweenTimestamps( + methodContext, + 99 * blockTime, + 103 * blockTime, + ); + + const actualGenerators = Object.keys(result); + + for (const generatorAddress of actualGenerators) { + expect(result[generatorAddress]).toBe(1); + } + + expect(expectedGenerators).toEqual(actualGenerators); + }); }); describe('getValidatorKeys', () => { @@ -854,6 +887,7 @@ describe('ValidatorsModuleMethod', () => { expect(isSet).toBe(true); expect(setValidatorAccount.generatorKey.equals(generatorKey)).toBe(true); + expect(setValidatorAccount.blsKey.equals(INVALID_BLS_KEY)).toBe(true); expect(methodContext.eventQueue.add).toHaveBeenCalledWith( MODULE_NAME_VALIDATORS, validatorsModule.events.get(GeneratorKeyRegistrationEvent).name, @@ -974,4 +1008,134 @@ describe('ValidatorsModuleMethod', () => { ); }); }); + + describe('setValidatorsParams', () => { + it('should update ValidatorsParamsStore with the provided validators, preCommitThreshold, certificateThreshold and call setNextValidators', async () => { + const validatorSetter = { + setNextValidators: jest.fn().mockReturnValue(undefined), + }; + + const validators = [ + { + generatorKey: Buffer.from( + '91fdf7f2a3eb93e493f736a4f9fce0e1df082836bf6d06e739bb3b0e1690fada', + 'hex', + ), + blsKey: Buffer.from( + 'a84b3fc0a53fcb07c6057442cf11b37ef0a3d3216fc8e245f9cbf43c13193515f0de3ab9ef4f6b0e04ecdb4df212d96a', + 'hex', + ), + address: addressUtils.getAddressFromLisk32Address( + 'lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk', + ), + bftWeight: BigInt(54), + }, + { + generatorKey: Buffer.from( + 'b53ef930d84d3ce5b4947c2502da06bcbc0fb2c71ee96f3b3a35340516712c71', + 'hex', + ), + blsKey: Buffer.from( + '8d4151757d14b1a30f7088f0bb1505bfd94a471872d565de563dbce32f696cb77afcc026170c343d0329ad554df564f6', + 'hex', + ), + address: addressUtils.getAddressFromLisk32Address( + 'lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6', + ), + bftWeight: BigInt(33), + }, + ]; + + for (const validator of validators) { + await validatorsSubStore.set(methodContext, validator.address, { + generatorKey: validator.generatorKey, + blsKey: validator.blsKey, + }); + } + + const preCommitThreshold = BigInt(100); + const certificateThreshold = BigInt(200); + + await validatorsMethod.setValidatorsParams( + methodContext, + validatorSetter, + preCommitThreshold, + certificateThreshold, + validators, + ); + + const expectedValidatorParams = { + certificateThreshold, + preCommitThreshold, + validators, + }; + + const validatorParams = await validatorsParamsSubStore.get(methodContext, EMPTY_KEY); + + expect(validatorParams).toEqual(expectedValidatorParams); + + expect(validatorSetter.setNextValidators).toHaveBeenNthCalledWith( + 1, + preCommitThreshold, + certificateThreshold, + validators, + ); + }); + + it('should throw if provided validator does not exist in Validator', async () => { + const validatorSetter = { + setNextValidators: jest.fn().mockReturnValue(undefined), + }; + + const validators = [ + { + generatorKey: Buffer.from( + '91fdf7f2a3eb93e493f736a4f9fce0e1df082836bf6d06e739bb3b0e1690fada', + 'hex', + ), + blsKey: Buffer.from( + 'a84b3fc0a53fcb07c6057442cf11b37ef0a3d3216fc8e245f9cbf43c13193515f0de3ab9ef4f6b0e04ecdb4df212d96a', + 'hex', + ), + address: addressUtils.getAddressFromLisk32Address( + 'lsk8kpswabbcjrnfp89demrfvryx9sgjsma87pusk', + ), + bftWeight: BigInt(54), + }, + { + generatorKey: Buffer.from( + 'b53ef930d84d3ce5b4947c2502da06bcbc0fb2c71ee96f3b3a35340516712c71', + 'hex', + ), + blsKey: Buffer.from( + '8d4151757d14b1a30f7088f0bb1505bfd94a471872d565de563dbce32f696cb77afcc026170c343d0329ad554df564f6', + 'hex', + ), + address: addressUtils.getAddressFromLisk32Address( + 'lskkjm548jqdrgzqrozpkew9z82kqfvtpmvavj7d6', + ), + bftWeight: BigInt(33), + }, + ]; + + const preCommitThreshold = BigInt(100); + const certificateThreshold = BigInt(200); + + await expect( + validatorsMethod.setValidatorsParams( + methodContext, + validatorSetter, + preCommitThreshold, + certificateThreshold, + validators, + ), + ).rejects.toThrow('does not exist'); + + const validatorParamsExits = await validatorsParamsSubStore.has(methodContext, EMPTY_KEY); + + expect(validatorParamsExits).toBe(false); + + expect(validatorSetter.setNextValidators).not.toHaveBeenCalled(); + }); + }); }); diff --git a/framework/test/unit/state_machine/event_queue.spec.ts b/framework/test/unit/state_machine/event_queue.spec.ts index 0f83bcc61a4..7608c5faf9d 100644 --- a/framework/test/unit/state_machine/event_queue.spec.ts +++ b/framework/test/unit/state_machine/event_queue.spec.ts @@ -70,17 +70,6 @@ describe('EventQueue', () => { ).toThrow('Max size of event data is'); }); - it('should throw error if topics is empty', () => { - expect(() => - eventQueue.add( - 'token', - 'Token Event Name', - utils.getRandomBytes(EVENT_MAX_EVENT_SIZE_BYTES), - [], - ), - ).toThrow('Topics must have at least one element'); - }); - it('should throw error if topics length exceeds maxumum allowed', () => { expect(() => eventQueue.add( diff --git a/framework/test/unit/state_machine/state_machine.spec.ts b/framework/test/unit/state_machine/state_machine.spec.ts index 5fd74ebae5d..236322eb399 100644 --- a/framework/test/unit/state_machine/state_machine.spec.ts +++ b/framework/test/unit/state_machine/state_machine.spec.ts @@ -186,6 +186,7 @@ describe('state_machine', () => { getMethodContext: expect.any(Function), getStore: expect.any(Function), }); + expect(mod.commands[0].execute).toHaveBeenCalledTimes(1); expect(mod.afterCommandExecute).toHaveBeenCalledTimes(1); }); @@ -322,6 +323,23 @@ describe('state_machine', () => { // expect(systemMod.verifyAssets).toHaveBeenCalledTimes(1); expect(mod.verifyAssets).toHaveBeenCalledTimes(1); }); + + it('should fail if module is not registered', async () => { + await expect( + stateMachine.verifyAssets( + new BlockContext({ + eventQueue, + logger, + stateStore, + contextStore, + header, + assets: new BlockAssets([{ module: 'unknown', data: Buffer.alloc(30) }]), + chainID, + transactions: [transaction], + }), + ), + ).rejects.toThrow('Module unknown is not registered'); + }); }); describe('beforeExecuteBlock', () => { diff --git a/protocol-specs/generators/address_generation/index.js b/protocol-specs/generators/address_generation/index.js index e562c22a008..267bfbea6aa 100644 --- a/protocol-specs/generators/address_generation/index.js +++ b/protocol-specs/generators/address_generation/index.js @@ -35,7 +35,7 @@ const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; const getBinaryAddress = publicKey => { const publicKeyBuffer = Buffer.from(publicKey, 'hex'); - return utils.hash(publicKeyBuffer).slice(0, 20); + return utils.hash(publicKeyBuffer).subarray(0, 20); }; const polymod = uint5Array => { diff --git a/protocol-specs/generators/pos_generator_selection/sample_generator.js b/protocol-specs/generators/pos_generator_selection/sample_generator.js index 5b1b19e2486..56761039f8e 100644 --- a/protocol-specs/generators/pos_generator_selection/sample_generator.js +++ b/protocol-specs/generators/pos_generator_selection/sample_generator.js @@ -26,7 +26,7 @@ const generateValidators = (num, fixedNum) => { for (let i = 0; i < num; i += 1) { const passphrase = Mnemonic.generateMnemonic(); const { publicKey } = ed.getKeys(passphrase); - const address = utils.hash(Buffer.from(publicKey, 'hex')).slice(0, 20); + const address = utils.hash(Buffer.from(publicKey, 'hex')).subarray(0, 20); const buf = crypto.randomBytes(8); const randomNumber = buf.readBigUInt64BE() / BigInt(10) ** BigInt(8); const validatorWeight = fixedValue diff --git a/protocol-specs/generators/pos_random_seed_generation/index.js b/protocol-specs/generators/pos_random_seed_generation/index.js index e7324eec595..7513708cfcb 100644 --- a/protocol-specs/generators/pos_random_seed_generation/index.js +++ b/protocol-specs/generators/pos_random_seed_generation/index.js @@ -35,7 +35,7 @@ const strippedHash = data => { throw new Error('Hash input is not a valid type'); } - return utils.hash(data).slice(0, 16); + return utils.hash(data).subarray(0, 16); }; const bitwiseXOR = bufferArray => { diff --git a/protocol-specs/generators/pos_validator_shuffling/sample_generator.js b/protocol-specs/generators/pos_validator_shuffling/sample_generator.js index 788276feb9d..da3875048c2 100644 --- a/protocol-specs/generators/pos_validator_shuffling/sample_generator.js +++ b/protocol-specs/generators/pos_validator_shuffling/sample_generator.js @@ -25,7 +25,7 @@ const generateValidators = num => { for (let i = 0; i < num; i += 1) { const passphrase = Mnemonic.generateMnemonic(); const { publicKey } = ed.getKeys(passphrase); - const address = utils.hash(Buffer.from(publicKey, 'hex')).slice(0, 20); + const address = utils.hash(Buffer.from(publicKey, 'hex')).subarray(0, 20); validatorList.push({ address, diff --git a/protocol-specs/package.json b/protocol-specs/package.json index 88e7a031fbb..dfe85b3a5e4 100644 --- a/protocol-specs/package.json +++ b/protocol-specs/package.json @@ -19,11 +19,11 @@ }, "dependencies": { "@liskhq/bignum": "1.3.1", - "@liskhq/lisk-codec": "0.4.0-rc.2", - "@liskhq/lisk-cryptography": "4.0.0-rc.2", - "@liskhq/lisk-passphrase": "4.0.0-rc.0", - "@liskhq/lisk-validator": "0.8.0-rc.2", - "protobufjs": "6.11.3" + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-passphrase": "4.1.0-rc.0", + "@liskhq/lisk-validator": "^0.9.0-rc.0", + "protobufjs": "7.2.4" }, "devDependencies": { "eslint": "8.28.0", diff --git a/sdk/package.json b/sdk/package.json index 2057c0c9a9d..b1967a04919 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "lisk-sdk", - "version": "6.0.0-rc.5", + "version": "6.1.0-rc.0", "description": "Official SDK for the Lisk blockchain application platform", "author": "Lisk Foundation , lightcurve GmbH ", "license": "Apache-2.0", @@ -29,19 +29,19 @@ "build": "tsc" }, "dependencies": { - "@liskhq/lisk-api-client": "^6.0.0-rc.4", - "@liskhq/lisk-chain": "^0.5.0-rc.4", - "@liskhq/lisk-codec": "^0.4.0-rc.2", - "@liskhq/lisk-cryptography": "^4.0.0-rc.2", - "@liskhq/lisk-db": "0.3.10", - "@liskhq/lisk-p2p": "^0.9.0-rc.2", - "@liskhq/lisk-passphrase": "^4.0.0-rc.0", - "@liskhq/lisk-transaction-pool": "^0.7.0-rc.2", - "@liskhq/lisk-transactions": "^6.0.0-rc.2", - "@liskhq/lisk-tree": "^0.4.0-rc.2", + "@liskhq/lisk-api-client": "^6.1.0-rc.0", + "@liskhq/lisk-chain": "^0.6.0-rc.0", + "@liskhq/lisk-codec": "^0.5.0-rc.0", + "@liskhq/lisk-cryptography": "^4.1.0-rc.0", + "@liskhq/lisk-db": "0.3.7", + "@liskhq/lisk-p2p": "^0.10.0-rc.0", + "@liskhq/lisk-passphrase": "^4.1.0-rc.0", + "@liskhq/lisk-transaction-pool": "^0.8.0-rc.0", + "@liskhq/lisk-transactions": "^6.1.0-rc.0", + "@liskhq/lisk-tree": "^0.5.0-rc.0", "@liskhq/lisk-utils": "^0.4.0-rc.0", - "@liskhq/lisk-validator": "^0.8.0-rc.2", - "lisk-framework": "^0.11.0-rc.5" + "@liskhq/lisk-validator": "^0.9.0-rc.0", + "lisk-framework": "^0.12.0-rc.0" }, "devDependencies": { "eslint": "8.28.0", diff --git a/test/package.json b/test/package.json index f9c2d1dd0e6..0e79ca8437e 100644 --- a/test/package.json +++ b/test/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "debug": "4.3.4", - "lisk-sdk": "^6.0.0-alpha.0" + "lisk-sdk": "^6.1.0-rc.0" }, "devDependencies": { "@types/jest": "29.2.3", diff --git a/yarn.lock b/yarn.lock index 703727986a0..6be1f9fee56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1855,16 +1855,45 @@ dependencies: "@types/node" "11.11.2" -"@liskhq/lisk-db@0.3.10": - version "0.3.10" - resolved "https://registry.yarnpkg.com/@liskhq/lisk-db/-/lisk-db-0.3.10.tgz#af0a96925a3a76a6b05bef0a31dacc2b2357bfb0" - integrity sha512-1vG6qCCbSw016peckgcl8vxwJMke0kUi9a4OCUmp+OgzcroNmc/EHR/psOVaBqW3Jrmcg7BNpY4d3vGsFy1tsg== +"@liskhq/lisk-cryptography@^4.0.0-rc.2": + version "4.0.0-rc.2" + resolved "https://registry.yarnpkg.com/@liskhq/lisk-cryptography/-/lisk-cryptography-4.0.0-rc.2.tgz#042191f8c02834388157469015b73318df097964" + integrity sha512-kxHQ1fGiOC5WaqRRKaJJIOI6EqljyYZuDl7zg+Ihwz36ZDTRtGNLpUCA8+1GPYBwRghp2QobRibUBZh649Hh5A== + dependencies: + "@liskhq/lisk-passphrase" "^4.0.0-rc.0" + buffer-reverse "1.0.1" + hash-wasm "4.9.0" + tweetnacl "1.0.3" + +"@liskhq/lisk-db@0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@liskhq/lisk-db/-/lisk-db-0.3.7.tgz#9dce3d0c37f248f9221b26f0d57c3306d7d072ef" + integrity sha512-n0/i3lea8ApTaSX99mhEJr3IZgG5l65M8Bmoo3D4ytbzyTySY9q7ITqj0y3kEZd997g4IUJl0x6Slq2RgFeucA== dependencies: "@mapbox/node-pre-gyp" "^1.0.9" "@types/node" "^16 || ^18" cargo-cp-artifact "^0.1" shelljs "^0.8.5" +"@liskhq/lisk-passphrase@^4.0.0-rc.0": + version "4.0.0-rc.0" + resolved "https://npm.lisk.com/@liskhq/lisk-passphrase/-/lisk-passphrase-4.0.0-rc.0.tgz#78fe583229c96d76258906375e34ff84a413be05" + integrity sha512-m87nhvUpOlSLr5NRV2M4INtg0IjjFF7Bte96Iq6X1dhzOjlmPg/QUQa7MFUzQu3NEWWHnpwON8QQK1FUE6ixYw== + dependencies: + bip39 "3.0.3" + +"@liskhq/lisk-validator@^0.8.0-rc.0": + version "0.8.0-rc.2" + resolved "https://registry.yarnpkg.com/@liskhq/lisk-validator/-/lisk-validator-0.8.0-rc.2.tgz#4c81e83027c5c832f7a2915ce0bdc79614387707" + integrity sha512-b4V0z2TlOxEosAKpxgR/lgobvpw5Lc2/ZAuBxDuwlvuXh5WSoz+VaPMf2Ry7N9TQwXtvHx3MO8chraJN+F5bEg== + dependencies: + "@liskhq/lisk-cryptography" "^4.0.0-rc.2" + ajv "8.1.0" + ajv-formats "2.1.1" + debug "4.3.4" + semver "7.5.2" + validator "13.7.0" + "@lmdb/lmdb-darwin-arm64@2.5.2": version "2.5.2" resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.5.2.tgz#bc66fa43286b5c082e8fee0eacc17995806b6fbe" @@ -3709,11 +3738,6 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== -"@types/long@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" - integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== - "@types/mem-fs-editor@*": version "7.0.0" resolved "https://registry.yarnpkg.com/@types/mem-fs-editor/-/mem-fs-editor-7.0.0.tgz#e6576e0f66e20055481b2cdbf193457f1a2c4e65" @@ -3754,10 +3778,10 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== -"@types/node@*", "@types/node@>=13.7.0": - version "17.0.38" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" - integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^16 || ^18": + version "18.16.16" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.16.tgz#3b64862856c7874ccf7439e6bab872d245c86d8e" + integrity sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g== "@types/node@11.11.2": version "11.11.2" @@ -3774,11 +3798,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014" integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw== -"@types/node@^16 || ^18": - version "18.16.16" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.16.tgz#3b64862856c7874ccf7439e6bab872d245c86d8e" - integrity sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g== - "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -11267,10 +11286,10 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" -long@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" @@ -13332,10 +13351,10 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= -protobufjs@6.11.3: - version "6.11.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" - integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== +protobufjs@7.2.4: + version "7.2.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.4.tgz#3fc1ec0cdc89dd91aef9ba6037ba07408485c3ae" + integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -13347,9 +13366,8 @@ protobufjs@6.11.3: "@protobufjs/path" "^1.1.2" "@protobufjs/pool" "^1.1.0" "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" "@types/node" ">=13.7.0" - long "^4.0.0" + long "^5.0.0" protocols@^2.0.0, protocols@^2.0.1: version "2.0.1" @@ -16208,9 +16226,9 @@ widest-line@^3.1.0: string-width "^4.0.0" word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wordwrap@^1.0.0: version "1.0.0"