diff --git a/.docker/volumes/node_1/config.json b/.docker/volumes/node_1/config.json index b02e660ad9..acef7675f7 100644 --- a/.docker/volumes/node_1/config.json +++ b/.docker/volumes/node_1/config.json @@ -26,7 +26,20 @@ "maxInbound": 21, "maxOutbound": 7, "trustedPeerIDs": null, - "dialPeers": [], + "dialPeers": [ + "8ad02e9b05e418f198e89179685a48b227f1c2bc266d4db002f24f8155c5119cbb9387146319b5dc4a260184f20d5d4f@tcp://cnpy.xyz", + "90703d453dfa70af3c85f3605e6cc8222d01d68d439ee5a9ade10be631572b330e1793206c8d417d9693be1d5af417fe@tcp://cnpy.network:9010", + "89c592a8a27bc2ba3783048ebad2ee77b67ce8db1d3706d19135fac44e71d459870791d8face14dde4df6262a6445a27@tcp://minerstellar.com:9020", + "b1891c2a38b553279b46452a4ecc49fa42ecb7c71a27f39efa5b3f817481b443cb44da9be251fa571d1ac43dd5ef7fd9@tcp://agentofthecrown.com:9030", + "b338f09135994130bee5da939513241b5d01ba5e73c409ed5ecea597b86a8b9c05fd27fb5e47a7296350fbae6262a484@tcp://cnpynetwork.com:9040", + "8794c211342dc0da348b16e4e5903ffd7f913c390bd29dbfad0c44b891b08a99d74bc90546d89b54098ac7c0c5331550@tcp://dun3waves.xyz:9050", + "99e38bdc8b7c7f9f8a67151da78994a2616ac127518d4dd8f5a08ad760921758269fcb5172713b9585ec7007f60cb6fe@tcp://canopynode.cam:9060", + "a329a705dab85db2fd950cef9ffd87e27ff4b91b929d5ab26e6efb40b1b3b45e74f7d8162822c49b9909893e6461cc8d@tcp://uem44.com:9070", + "b17d4eb3938957e710bacc9f09d2a9aa79a568fcdf1f8fc565bdb5de3f334295e929e4b086b9c8e9610654155fb0452b@tcp://canopycoin.xyz:9080", + "ab4fe218bb09a27908c6181fc799b8371d895f13173fed5b924af60235a548cd882ba97f911c08d9bda34c46a7dab359@tcp://cryptoicp.com:9090", + "b346ad1f1809adc64d4a06e5be4d9960a018faada129d659623fd97846d5127de550218929ec9de4af9d9cbd1ec52d46@tcp://lava-9.com:9100", + "9174b24ba27fe8a0f8616bf1a382d81428ef8a94b7ff70916bb9b266c9070bd848e49a0c33a1c17553416c949ea3409a@tcp://cleanmarro.com:9110" + ], "bannedPeerIDs": null, "bannedIPs": null, "gossipThreshold": 0, diff --git a/.docker/volumes/node_1/genesis.json b/.docker/volumes/node_1/genesis.json old mode 100755 new mode 100644 index 607e382033..1014bfbd2d --- a/.docker/volumes/node_1/genesis.json +++ b/.docker/volumes/node_1/genesis.json @@ -1,65 +1,124 @@ { - "time": "2024-12-14 20:10:52", - "accounts": [ + "validators": [ { - "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "amount": 1000000 + "address": "9c51a2b9b9234865a61cd43aae7f899441ae6271", + "publicKey": "8ad02e9b05e418f198e89179685a48b227f1c2bc266d4db002f24f8155c5119cbb9387146319b5dc4a260184f20d5d4f", + "committees": [1, 2], + "netAddress": "tcp://cnpy.xyz", + "stakedAmount": 1, + "output": "cf228862bd36016c0154e789d5fabe584da31c7e" }, { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "amount": 1000000 + "address": "5bc4bd9a3468889febdc6a73df0090b12a263eb4", + "publicKey": "90703d453dfa70af3c85f3605e6cc8222d01d68d439ee5a9ade10be631572b330e1793206c8d417d9693be1d5af417fe", + "committees": [1, 2], + "netAddress": "tcp://cnpy.network", + "stakedAmount": 1, + "output": "faef35fb62796dfd6dfb9b19ea3d4be130a97257" }, { - "address": "6f94783856d5ce46d24dd5946215086211d70776", - "amount": 1000000 - } - ], - "nonSigners": null, - "validators": [ + "address": "997634c57ad60414116dc59417db6829c209f1df", + "publicKey": "89c592a8a27bc2ba3783048ebad2ee77b67ce8db1d3706d19135fac44e71d459870791d8face14dde4df6262a6445a27", + "committees": [1, 2], + "netAddress": "tcp://minerstellar.com", + "stakedAmount": 1, + "output": "12ee25b30f3016e3427983c1fb75eabab025a2d5" + }, + { + "address": "ac2c2eb9aa04b99ec9a523b09ca844a77826c291", + "publicKey": "b1891c2a38b553279b46452a4ecc49fa42ecb7c71a27f39efa5b3f817481b443cb44da9be251fa571d1ac43dd5ef7fd9", + "committees": [1, 2], + "netAddress": "tcp://agentofthecrown.com", + "stakedAmount": 1, + "output": "330d139fd014ec84c15b9786f9934a5a95a02d25" + }, + { + "address": "a843153e432b2b3052e50cfb37bbd021e9cbd34c", + "publicKey": "b338f09135994130bee5da939513241b5d01ba5e73c409ed5ecea597b86a8b9c05fd27fb5e47a7296350fbae6262a484", + "committees": [1, 2], + "netAddress": "tcp://cnpynetwork.com", + "stakedAmount": 1, + "output": "1202c3f3ac5e4876f9b35774858ca16e129ceba1" + }, + { + "address": "5946f3adf4965295b816a40b719ff79e9fd1dba2", + "publicKey": "8794c211342dc0da348b16e4e5903ffd7f913c390bd29dbfad0c44b891b08a99d74bc90546d89b54098ac7c0c5331550", + "committees": [1, 2], + "netAddress": "tcp://dun3waves.xyz", + "stakedAmount": 1, + "output": "3b7d461b533ff5b7ec28d172656b8049f29e5b11" + }, + { + "address": "51f334c0e137b99aa8fc9eae3a71064c2ccf8208", + "publicKey": "99e38bdc8b7c7f9f8a67151da78994a2616ac127518d4dd8f5a08ad760921758269fcb5172713b9585ec7007f60cb6fe", + "committees": [1, 2], + "netAddress": "tcp://canopynode.cam", + "stakedAmount": 1, + "output": "3dad3f4dcb6e0411a6acfc66c3d8850aff9789e4" + }, + { + "address": "8b243551a78f5380d6ba5b1e84fee34b23f6e757", + "publicKey": "a329a705dab85db2fd950cef9ffd87e27ff4b91b929d5ab26e6efb40b1b3b45e74f7d8162822c49b9909893e6461cc8d", + "committees": [1, 2], + "netAddress": "tcp://uem44.com", + "stakedAmount": 1, + "output": "b70bd2f4a87a43597c9c78d65dc3192090fdaa6f" + }, + { + "address": "fc5cdb5c0b6a6df41b92976bbdf2b6832855446f", + "publicKey": "b17d4eb3938957e710bacc9f09d2a9aa79a568fcdf1f8fc565bdb5de3f334295e929e4b086b9c8e9610654155fb0452b", + "committees": [1, 2], + "netAddress": "tcp://canopycoin.xyz", + "stakedAmount": 1, + "output": "a59917b6d045327d4a39736fea81b72e1891692e" + }, + { + "address": "4e2ce94661e2e3fd3af02e21898803c4d74e84ba", + "publicKey": "ab4fe218bb09a27908c6181fc799b8371d895f13173fed5b924af60235a548cd882ba97f911c08d9bda34c46a7dab359", + "committees": [1, 2], + "netAddress": "tcp://cryptoicp.com", + "stakedAmount": 1, + "output": "bc01362a7bd2613a3786ce32007452fd51f2bf1b" + }, { - "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", - "committees": [ - 1 - ], - "netAddress": "tcp://node-1", - "stakedAmount": 1000000000, - "output": "851e90eaef1fa27debaee2c2591503bdeec1d123" + "address": "33e14ef6b87fb688b829c5e29618bb549dc7b4cd", + "publicKey": "b346ad1f1809adc64d4a06e5be4d9960a018faada129d659623fd97846d5127de550218929ec9de4af9d9cbd1ec52d46", + "committees": [1, 2], + "netAddress": "tcp://lava-9.com", + "stakedAmount": 1, + "output": "b587ddd8b58134c61ec52e5b1e88a27a5e07be82" }, { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", - "committees": [ - 1 - ], - "netAddress": "tcp://node-2", - "stakedAmount": 1000000000, - "output": "02cd4e5eb53ea665702042a6ed6d31d616054dc5" + "address": "b9c6c2dfa9d049e480c8cec9c29463abf078a594", + "publicKey": "9174b24ba27fe8a0f8616bf1a382d81428ef8a94b7ff70916bb9b266c9070bd848e49a0c33a1c17553416c949ea3409a", + "committees": [1, 2], + "netAddress": "tcp://cleanmarro.com", + "stakedAmount": 1, + "output": "8f5633ac35fc17fe113b8ecaa2060289607c0352" } ], "params": { "consensus": { "blockSize": 1000000, "protocolVersion": "1/0", - "rootChainID": 1, - "retired": 0 + "rootChainID": 1 }, "validator": { - "unstakingBlocks": 2, - "maxPauseBlocks": 4380, + "unstakingBlocks": 30240, + "maxPauseBlocks": 30240, "doubleSignSlashPercentage": 10, "nonSignSlashPercentage": 1, - "maxNonSign": 4, - "nonSignWindow": 10, - "maxCommittees": 15, + "maxNonSign": 60, + "nonSignWindow": 100, + "maxCommittees": 16, "maxCommitteeSize": 100, - "earlyWithdrawalPenalty": 20, - "delegateUnstakingBlocks": 2, - "minimumOrderSize": 1000, + "earlyWithdrawalPenalty": 0, + "delegateUnstakingBlocks": 12960, + "minimumOrderSize": 1000000000, "stakePercentForSubsidizedCommittee": 33, "maxSlashPerCommittee": 15, - "delegateRewardPercentage": 10, - "buyDeadlineBlocks": 15, + "delegateRewardPercentage": 0, + "buyDeadlineBlocks": 60, "lockOrderFeeMultiplier": 2 }, "fee": { @@ -71,14 +130,14 @@ "unpauseFee": 10000, "changeParameterFee": 10000, "daoTransferFee": 10000, + "certificateResultsFee": 0, "subsidyFee": 10000, "createOrderFee": 10000, "editOrderFee": 10000, "deleteOrderFee": 10000 }, "governance": { - "daoRewardPercentage": 10 + "daoRewardPercentage": 5 } - }, - "supply": null + } } diff --git a/.docker/volumes/node_1/keystore.json b/.docker/volumes/node_1/keystore.json index dece94a769..7a73a41bfd 100644 --- a/.docker/volumes/node_1/keystore.json +++ b/.docker/volumes/node_1/keystore.json @@ -1,30 +1,2 @@ { - "addressMap": { - "02cd4e5eb53ea665702042a6ed6d31d616054dc5": { - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", - "salt": "74f0112bcffc91215b6f6266acec38ca", - "encrypted": "183444bb69d2693a892e90ef7ebca9167719113488e4e803f8e87603ea84ccb40c423bc72db7303e81d7d216368ed763", - "keyAddress": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "keyNickname": "node_2" - }, - "6f94783856d5ce46d24dd5946215086211d70776": { - "publicKey": "abda38eb50fbe53db9e9c3b141c6a1ec54ad40a4840e34784c975da4ee175eb4c5dd10b6d759ae8fdf8bc22511bbd97b", - "salt": "cfbafc41835a47660f822ee26112d2c6", - "encrypted": "f18135d9509b41b5edc42e74d22396cba3f11fd8a5acae008a49b6e8bd3540a48f74c0d9e65872b922091286a531eee7", - "keyAddress": "6f94783856d5ce46d24dd5946215086211d70776", - "keyNickname": "node_3" - }, - "851e90eaef1fa27debaee2c2591503bdeec1d123": { - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", - "salt": "3bff15134210c811e308eaa9b7b6024c", - "encrypted": "8b757090dfc98bfbff4f5972f0ae4bb0339a82a753f633cd37aa921955d76cda6a5f521120e7559eb57f497e88f7f555", - "keyAddress": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "keyNickname": "node_1" - } - }, - "nicknameMap": { - "node_1": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "node_2": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "node_3": "6f94783856d5ce46d24dd5946215086211d70776" - } } \ No newline at end of file diff --git a/.docker/volumes/node_2/config.json b/.docker/volumes/node_2/config.json index eaedd498cd..04409bf96b 100644 --- a/.docker/volumes/node_2/config.json +++ b/.docker/volumes/node_2/config.json @@ -1,6 +1,6 @@ { "logLevel": "debug", - "chainId": 1, + "chainId": 2, "sleepUntil": 0, "rootChain": [ { @@ -25,12 +25,25 @@ "dbName": "canopy", "inMemory": false, "networkID": 1, - "listenAddress": "0.0.0.0:9001", + "listenAddress": "0.0.0.0:9002", "externalAddress": "node-2", "maxInbound": 21, "maxOutbound": 7, "trustedPeerIDs": null, - "dialPeers": [], + "dialPeers": [ + "8ad02e9b05e418f198e89179685a48b227f1c2bc266d4db002f24f8155c5119cbb9387146319b5dc4a260184f20d5d4f@tcp://cnpy.xyz", + "90703d453dfa70af3c85f3605e6cc8222d01d68d439ee5a9ade10be631572b330e1793206c8d417d9693be1d5af417fe@tcp://cnpy.network:9010", + "89c592a8a27bc2ba3783048ebad2ee77b67ce8db1d3706d19135fac44e71d459870791d8face14dde4df6262a6445a27@tcp://minerstellar.com:9020", + "b1891c2a38b553279b46452a4ecc49fa42ecb7c71a27f39efa5b3f817481b443cb44da9be251fa571d1ac43dd5ef7fd9@tcp://agentofthecrown.com:9030", + "b338f09135994130bee5da939513241b5d01ba5e73c409ed5ecea597b86a8b9c05fd27fb5e47a7296350fbae6262a484@tcp://cnpynetwork.com:9040", + "8794c211342dc0da348b16e4e5903ffd7f913c390bd29dbfad0c44b891b08a99d74bc90546d89b54098ac7c0c5331550@tcp://dun3waves.xyz:9050", + "99e38bdc8b7c7f9f8a67151da78994a2616ac127518d4dd8f5a08ad760921758269fcb5172713b9585ec7007f60cb6fe@tcp://canopynode.cam:9060", + "a329a705dab85db2fd950cef9ffd87e27ff4b91b929d5ab26e6efb40b1b3b45e74f7d8162822c49b9909893e6461cc8d@tcp://uem44.com:9070", + "b17d4eb3938957e710bacc9f09d2a9aa79a568fcdf1f8fc565bdb5de3f334295e929e4b086b9c8e9610654155fb0452b@tcp://canopycoin.xyz:9080", + "ab4fe218bb09a27908c6181fc799b8371d895f13173fed5b924af60235a548cd882ba97f911c08d9bda34c46a7dab359@tcp://cryptoicp.com:9090", + "b346ad1f1809adc64d4a06e5be4d9960a018faada129d659623fd97846d5127de550218929ec9de4af9d9cbd1ec52d46@tcp://lava-9.com:9100", + "9174b24ba27fe8a0f8616bf1a382d81428ef8a94b7ff70916bb9b266c9070bd848e49a0c33a1c17553416c949ea3409a@tcp://cleanmarro.com:9110" + ], "bannedPeerIDs": null, "bannedIPs": null, "minimumPeersToStart": 0, diff --git a/.docker/volumes/node_2/genesis.json b/.docker/volumes/node_2/genesis.json old mode 100755 new mode 100644 index 607e382033..53083f2b16 --- a/.docker/volumes/node_2/genesis.json +++ b/.docker/volumes/node_2/genesis.json @@ -1,65 +1,124 @@ { - "time": "2024-12-14 20:10:52", - "accounts": [ + "validators": [ { - "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "amount": 1000000 + "address": "9c51a2b9b9234865a61cd43aae7f899441ae6271", + "publicKey": "8ad02e9b05e418f198e89179685a48b227f1c2bc266d4db002f24f8155c5119cbb9387146319b5dc4a260184f20d5d4f", + "committees": [2], + "netAddress": "tcp://cnpy.xyz", + "stakedAmount": 1, + "output": "cf228862bd36016c0154e789d5fabe584da31c7e" }, { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "amount": 1000000 + "address": "5bc4bd9a3468889febdc6a73df0090b12a263eb4", + "publicKey": "90703d453dfa70af3c85f3605e6cc8222d01d68d439ee5a9ade10be631572b330e1793206c8d417d9693be1d5af417fe", + "committees": [2], + "netAddress": "tcp://cnpy.network", + "stakedAmount": 1, + "output": "faef35fb62796dfd6dfb9b19ea3d4be130a97257" }, { - "address": "6f94783856d5ce46d24dd5946215086211d70776", - "amount": 1000000 - } - ], - "nonSigners": null, - "validators": [ + "address": "997634c57ad60414116dc59417db6829c209f1df", + "publicKey": "89c592a8a27bc2ba3783048ebad2ee77b67ce8db1d3706d19135fac44e71d459870791d8face14dde4df6262a6445a27", + "committees": [2], + "netAddress": "tcp://minerstellar.com", + "stakedAmount": 1, + "output": "12ee25b30f3016e3427983c1fb75eabab025a2d5" + }, + { + "address": "ac2c2eb9aa04b99ec9a523b09ca844a77826c291", + "publicKey": "b1891c2a38b553279b46452a4ecc49fa42ecb7c71a27f39efa5b3f817481b443cb44da9be251fa571d1ac43dd5ef7fd9", + "committees": [2], + "netAddress": "tcp://agentofthecrown.com", + "stakedAmount": 1, + "output": "330d139fd014ec84c15b9786f9934a5a95a02d25" + }, + { + "address": "a843153e432b2b3052e50cfb37bbd021e9cbd34c", + "publicKey": "b338f09135994130bee5da939513241b5d01ba5e73c409ed5ecea597b86a8b9c05fd27fb5e47a7296350fbae6262a484", + "committees": [2], + "netAddress": "tcp://cnpynetwork.com", + "stakedAmount": 1, + "output": "1202c3f3ac5e4876f9b35774858ca16e129ceba1" + }, + { + "address": "5946f3adf4965295b816a40b719ff79e9fd1dba2", + "publicKey": "8794c211342dc0da348b16e4e5903ffd7f913c390bd29dbfad0c44b891b08a99d74bc90546d89b54098ac7c0c5331550", + "committees": [2], + "netAddress": "tcp://dun3waves.xyz", + "stakedAmount": 1, + "output": "3b7d461b533ff5b7ec28d172656b8049f29e5b11" + }, + { + "address": "51f334c0e137b99aa8fc9eae3a71064c2ccf8208", + "publicKey": "99e38bdc8b7c7f9f8a67151da78994a2616ac127518d4dd8f5a08ad760921758269fcb5172713b9585ec7007f60cb6fe", + "committees": [2], + "netAddress": "tcp://canopynode.cam", + "stakedAmount": 1, + "output": "3dad3f4dcb6e0411a6acfc66c3d8850aff9789e4" + }, + { + "address": "8b243551a78f5380d6ba5b1e84fee34b23f6e757", + "publicKey": "a329a705dab85db2fd950cef9ffd87e27ff4b91b929d5ab26e6efb40b1b3b45e74f7d8162822c49b9909893e6461cc8d", + "committees": [2], + "netAddress": "tcp://uem44.com", + "stakedAmount": 1, + "output": "b70bd2f4a87a43597c9c78d65dc3192090fdaa6f" + }, + { + "address": "fc5cdb5c0b6a6df41b92976bbdf2b6832855446f", + "publicKey": "b17d4eb3938957e710bacc9f09d2a9aa79a568fcdf1f8fc565bdb5de3f334295e929e4b086b9c8e9610654155fb0452b", + "committees": [2], + "netAddress": "tcp://canopycoin.xyz", + "stakedAmount": 1, + "output": "a59917b6d045327d4a39736fea81b72e1891692e" + }, + { + "address": "4e2ce94661e2e3fd3af02e21898803c4d74e84ba", + "publicKey": "ab4fe218bb09a27908c6181fc799b8371d895f13173fed5b924af60235a548cd882ba97f911c08d9bda34c46a7dab359", + "committees": [2], + "netAddress": "tcp://cryptoicp.com", + "stakedAmount": 1, + "output": "bc01362a7bd2613a3786ce32007452fd51f2bf1b" + }, { - "address": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", - "committees": [ - 1 - ], - "netAddress": "tcp://node-1", - "stakedAmount": 1000000000, - "output": "851e90eaef1fa27debaee2c2591503bdeec1d123" + "address": "33e14ef6b87fb688b829c5e29618bb549dc7b4cd", + "publicKey": "b346ad1f1809adc64d4a06e5be4d9960a018faada129d659623fd97846d5127de550218929ec9de4af9d9cbd1ec52d46", + "committees": [2], + "netAddress": "tcp://lava-9.com", + "stakedAmount": 1, + "output": "b587ddd8b58134c61ec52e5b1e88a27a5e07be82" }, { - "address": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", - "committees": [ - 1 - ], - "netAddress": "tcp://node-2", - "stakedAmount": 1000000000, - "output": "02cd4e5eb53ea665702042a6ed6d31d616054dc5" + "address": "b9c6c2dfa9d049e480c8cec9c29463abf078a594", + "publicKey": "9174b24ba27fe8a0f8616bf1a382d81428ef8a94b7ff70916bb9b266c9070bd848e49a0c33a1c17553416c949ea3409a", + "committees": [2], + "netAddress": "tcp://cleanmarro.com", + "stakedAmount": 1, + "output": "8f5633ac35fc17fe113b8ecaa2060289607c0352" } ], "params": { "consensus": { "blockSize": 1000000, "protocolVersion": "1/0", - "rootChainID": 1, - "retired": 0 + "rootChainID": 1 }, "validator": { - "unstakingBlocks": 2, - "maxPauseBlocks": 4380, + "unstakingBlocks": 30240, + "maxPauseBlocks": 30240, "doubleSignSlashPercentage": 10, "nonSignSlashPercentage": 1, - "maxNonSign": 4, - "nonSignWindow": 10, - "maxCommittees": 15, + "maxNonSign": 60, + "nonSignWindow": 100, + "maxCommittees": 16, "maxCommitteeSize": 100, - "earlyWithdrawalPenalty": 20, - "delegateUnstakingBlocks": 2, - "minimumOrderSize": 1000, + "earlyWithdrawalPenalty": 0, + "delegateUnstakingBlocks": 12960, + "minimumOrderSize": 1000000000, "stakePercentForSubsidizedCommittee": 33, "maxSlashPerCommittee": 15, "delegateRewardPercentage": 10, - "buyDeadlineBlocks": 15, + "buyDeadlineBlocks": 60, "lockOrderFeeMultiplier": 2 }, "fee": { @@ -71,14 +130,14 @@ "unpauseFee": 10000, "changeParameterFee": 10000, "daoTransferFee": 10000, + "certificateResultsFee": 0, "subsidyFee": 10000, "createOrderFee": 10000, "editOrderFee": 10000, "deleteOrderFee": 10000 }, "governance": { - "daoRewardPercentage": 10 + "daoRewardPercentage": 5 } - }, - "supply": null + } } diff --git a/.docker/volumes/node_2/keystore.json b/.docker/volumes/node_2/keystore.json index dece94a769..7a73a41bfd 100644 --- a/.docker/volumes/node_2/keystore.json +++ b/.docker/volumes/node_2/keystore.json @@ -1,30 +1,2 @@ { - "addressMap": { - "02cd4e5eb53ea665702042a6ed6d31d616054dc5": { - "publicKey": "98d45087a99bcbfde91993502e77dde869d4485c3778fe46513958320da560823d56a0108f4cf3513393f4d561bc489b", - "salt": "74f0112bcffc91215b6f6266acec38ca", - "encrypted": "183444bb69d2693a892e90ef7ebca9167719113488e4e803f8e87603ea84ccb40c423bc72db7303e81d7d216368ed763", - "keyAddress": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "keyNickname": "node_2" - }, - "6f94783856d5ce46d24dd5946215086211d70776": { - "publicKey": "abda38eb50fbe53db9e9c3b141c6a1ec54ad40a4840e34784c975da4ee175eb4c5dd10b6d759ae8fdf8bc22511bbd97b", - "salt": "cfbafc41835a47660f822ee26112d2c6", - "encrypted": "f18135d9509b41b5edc42e74d22396cba3f11fd8a5acae008a49b6e8bd3540a48f74c0d9e65872b922091286a531eee7", - "keyAddress": "6f94783856d5ce46d24dd5946215086211d70776", - "keyNickname": "node_3" - }, - "851e90eaef1fa27debaee2c2591503bdeec1d123": { - "publicKey": "b88a5928e54cbf0a36e0b98f5bcf02de9a9a1deba6994739f9160181a609f516eb702936a0cbf4c1f2e7e6be5b8272f2", - "salt": "3bff15134210c811e308eaa9b7b6024c", - "encrypted": "8b757090dfc98bfbff4f5972f0ae4bb0339a82a753f633cd37aa921955d76cda6a5f521120e7559eb57f497e88f7f555", - "keyAddress": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "keyNickname": "node_1" - } - }, - "nicknameMap": { - "node_1": "851e90eaef1fa27debaee2c2591503bdeec1d123", - "node_2": "02cd4e5eb53ea665702042a6ed6d31d616054dc5", - "node_3": "6f94783856d5ce46d24dd5946215086211d70776" - } } \ No newline at end of file diff --git a/cmd/rpc/sock.go b/cmd/rpc/sock.go index cdb294045f..7296a9374e 100644 --- a/cmd/rpc/sock.go +++ b/cmd/rpc/sock.go @@ -442,8 +442,12 @@ func (r *RCSubscription) Listen() { r.manager.l.Lock() // update the root chain info r.Info = newInfo - // execute the callback - r.manager.afterRCUpdate(newInfo) + // skip the callback during syncing: the BFT is paused so ResetBFT messages are + // unnecessary, and UpdateP2PMustConnect sends to a channel with buffer=1 that can + // block while holding the controller lock, starving the sync loop + if !r.manager.controller.Syncing().Load() { + r.manager.afterRCUpdate(newInfo) + } // release r.manager.l.Unlock() } diff --git a/controller/block.go b/controller/block.go index 3798914592..ea5967e61b 100644 --- a/controller/block.go +++ b/controller/block.go @@ -237,11 +237,13 @@ func (c *Controller) ValidateProposal(rcBuildHeight uint64, qc *lib.QuorumCertif // - sets up the controller for the next height func (c *Controller) CommitCertificate(qc *lib.QuorumCertificate, block *lib.Block, blockResult *lib.BlockResult, ts uint64) (err lib.ErrorI) { start := time.Now() - // cancel any running mempool check - c.Mempool.stop() - // lock the mempool - c.Mempool.L.Lock() - defer c.Mempool.L.Unlock() + syncing := c.isSyncing.Load() + if !syncing { + // cancel any running mempool check and lock the mempool for live operation + c.Mempool.stop() + c.Mempool.L.Lock() + defer c.Mempool.L.Unlock() + } // log the beginning of the commit c.log.Debugf("TryCommit block %s", lib.BytesToString(qc.ResultsHash)) // cast the store to ensure the proper store type to complete this operation @@ -277,12 +279,14 @@ func (c *Controller) CommitCertificate(qc *lib.QuorumCertificate, block *lib.Blo // exit with error return } - // delete each transaction from the mempool - c.Mempool.DeleteTransaction(block.Transactions...) + if !syncing { + // delete each transaction from the mempool + c.Mempool.DeleteTransaction(block.Transactions...) + } // parse committed block for straw polls c.FSM.ParsePollTransactions(blockResult) // if self was the proposer - if bytes.Equal(qc.ProposerKey, c.PublicKey) && !c.isSyncing.Load() { + if bytes.Equal(qc.ProposerKey, c.PublicKey) && !syncing { // send the certificate results transaction on behalf of the quorum c.SendCertificateResultsTx(qc) } @@ -301,35 +305,38 @@ func (c *Controller) CommitCertificate(qc *lib.QuorumCertificate, block *lib.Blo // exit with error return err } - // reset the current mempool store to prepare for the next height - c.Mempool.FSM.Discard() - // set up the mempool with the actual new FSM for the next height - // this makes c.Mempool.FSM.Reset() is unnecessary - if c.Mempool.FSM, err = c.FSM.Copy(); err != nil { - // exit with error - return err + if !syncing { + // reset the current mempool store to prepare for the next height + c.Mempool.FSM.Discard() + // set up the mempool with the actual new FSM for the next height + if c.Mempool.FSM, err = c.FSM.Copy(); err != nil { + // exit with error + return err + } + // check the mempool to cache a proposal block and validate the mempool itself + c.Mempool.CheckMempool() + // reset mempool FSM + c.Mempool.FSM.Reset() } - // check the mempool to cache a proposal block and validate the mempool itself - c.Mempool.CheckMempool() - // reset mempool FSM - c.Mempool.FSM.Reset() // update telemetry (using proper defer to ensure time.Since is evaluated at defer execution) defer c.UpdateTelemetry(qc, block, time.Since(start)) - // publish root chain information to all nested chain subscribers. - for _, id := range c.RCManager.ChainIds() { - // get the root chain info - info, e := c.FSM.LoadRootChainInfo(id, 0) - if e != nil { - // don't log 'no-validators' error as this is possible - if e.Error() != lib.ErrNoValidators().Error() { - c.log.Error(e.Error()) + if !syncing { + // publish root chain information to all nested chain subscribers + for _, id := range c.RCManager.ChainIds() { + // get the root chain info + info, e := c.FSM.LoadRootChainInfo(id, 0) + if e != nil { + // don't log 'no-validators' error as this is possible + if e.Error() != lib.ErrNoValidators().Error() { + c.log.Error(e.Error()) + } + continue } - continue + // set the timestamp + info.Timestamp = ts + // publish root chain information + go c.RCManager.Publish(id, info) } - // set the timestamp - info.Timestamp = ts - // publish root chain information - go c.RCManager.Publish(id, info) } // exit return diff --git a/controller/consensus.go b/controller/consensus.go index c8a065b92a..af9b157e75 100644 --- a/controller/consensus.go +++ b/controller/consensus.go @@ -10,6 +10,7 @@ import ( "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" "github.com/canopy-network/canopy/p2p" + "github.com/canopy-network/canopy/store" ) const ( @@ -74,6 +75,10 @@ func (c *Controller) Sync() { c.log.Infof("Sync started 🔄 for committee %d", c.Config.ChainId) // set the Controller as 'syncing' c.isSyncing.Store(true) + // notify the store to defer compaction during sync + if st, ok := c.FSM.Store().(*store.Store); ok { + st.SetSyncing(true) + } // check if node is alone in the validator set singleNode, err := c.singleNodeNetwork() if err != nil { @@ -740,12 +745,28 @@ func (c *Controller) finishSyncing() { c.Lock() // when function completes, unlock defer c.Unlock() + // reinitialize the mempool now that sync is complete + c.Mempool.L.Lock() + c.Mempool.Clear() + c.Mempool.FSM.Discard() + if mFSM, err := c.FSM.Copy(); err == nil { + c.Mempool.FSM = mFSM + } + c.Mempool.CheckMempool() + c.Mempool.FSM.Reset() + c.Mempool.L.Unlock() // set the startup block metric (block height when first sync completed) c.Metrics.SetStartupBlock(c.FSM.Height()) // signal a reset of bft for the chain c.Consensus.ResetBFT <- bft.ResetBFT{StartTime: c.LoadLastCommitTime(c.FSM.Height())} // set syncing to false c.isSyncing.Store(false) + // notify the store to resume compaction and trigger a full compaction of all prefixes + // (including SMT/indexer which are never compacted during normal operation) + if st, ok := c.FSM.Store().(*store.Store); ok { + st.SetSyncing(false) + go st.CompactAll() + } // enable listening for a block go c.ListenForBlock() } diff --git a/controller/tx.go b/controller/tx.go index dc52f117cf..166d3c7034 100644 --- a/controller/tx.go +++ b/controller/tx.go @@ -97,6 +97,12 @@ func (c *Controller) CheckMempool() { return } for { + // skip mempool checks while syncing: the mempool is unused (no proposals) and on nested + // chains the remote RPC calls (GetDexBatch) hold the mempool lock, blocking the sync loop + if c.isSyncing.Load() { + time.Sleep(time.Duration(c.Config.LazyMempoolCheckFrequencyS) * time.Second) + continue + } // keep a list of transaction needing to be gossipped var toGossip [][]byte // if recheck is necessary diff --git a/store/smt.go b/store/smt.go index c7673ffcb3..d4eb6e2db7 100644 --- a/store/smt.go +++ b/store/smt.go @@ -4,7 +4,6 @@ import ( "bytes" "math/bits" "sort" - "sync" "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" @@ -189,49 +188,102 @@ func (s *SMT) Commit(unsortedOps map[uint64]valueOp) (err lib.ErrorI) { return s.commit(false) } -// CommitParallel(): sorts the operations in 8 subtree threads, executes those threads in parallel and combines them into the master tree +// CommitParallel() executes deferred operations in parallel by partitioning them into +// 8 subtrees based on their 3-bit prefix (000-111), avoiding conflicts between operations +// that would modify overlapping tree regions. Each subtree is processed independently +// with its own Txn copy to avoid lock contention, then the results are merged back into +// the main tree. func (s *SMT) CommitParallel(unsortedOps map[uint64]valueOp) (err lib.ErrorI) { - var wg sync.WaitGroup - errChan := make(chan lib.ErrorI, NumSubtrees) - // add 16 synthetic borders to the tree - cleanup, err := s.addSyntheticBorders() + // fall back to sequential processing when operations are fewer than subtrees; + // addSyntheticBorders + cleanup overhead dominates for small batches + if len(unsortedOps) < NumSubtrees*2 { + return s.Commit(unsortedOps) + } + + parentTxn, ok := s.store.(*Txn) + if !ok { + return s.Commit(unsortedOps) + } + + groups, err := s.sortOperationsByPrefix(unsortedOps) if err != nil { return err } - // collect the roots for each group (000, 001, 010, 011...) - roots, err := s.getSubtreeRoots() + + cleanup, err := s.addSyntheticBorders() if err != nil { return err } - // sort operations grouping by prefix - groupedByPrefix, err := s.sortOperationsByPrefix(unsortedOps) + defer func() { + if cleanupErr := cleanup(); cleanupErr != nil && err == nil { + err = cleanupErr + } + }() + + subtreeRoots, err := s.getSubtreeRoots() if err != nil { - return + return err } - // commit each group in parallel - for i := 0; i < 8; i++ { - wg.Add(1) - go func(index int) { - defer wg.Done() - // create subtree - subtree := s.createSubtree(roots[index], groupedByPrefix[index]) + + type subtreeResult struct { + index int + store *subtreeStore + err lib.ErrorI + } + + resultChan := make(chan subtreeResult, NumSubtrees) + activeSubtrees := 0 + + for i := 0; i < NumSubtrees; i++ { + if len(groups[i]) == 0 { + continue + } + activeSubtrees++ + st := parentTxn.newSubtreeStore() + + go func(idx int, ops []*node, root *node, store *subtreeStore) { + subtree := &SMT{ + store: store, + root: root, + keyBitLength: s.keyBitLength, + nodeCache: make(map[string]*node), + operations: ops, + minKey: s.minKey, + maxKey: s.maxKey, + } subtree.reset() - // commit the subtree - if e := subtree.commit(true); e != nil { - errChan <- e + commitErr := subtree.commit(true) + resultChan <- subtreeResult{ + index: idx, + store: store, + err: commitErr, } - }(i) + }(i, groups[i], subtreeRoots[i], st) } - // wait for all goroutines to finish - wg.Wait() - close(errChan) - // check if any errors occurred - for err = range errChan { - if err != nil { - return + + results := make([]subtreeResult, 0, activeSubtrees) + for completed := 0; completed < activeSubtrees; completed++ { + result := <-resultChan + if result.err != nil { + // Drain remaining results before returning so goroutines + // finish and don't race with the deferred cleanup. + for completed++; completed < activeSubtrees; completed++ { + <-resultChan + } + return result.err } + results = append(results, result) } - return cleanup() + for _, result := range results { + parentTxn.mergeSubtreeOps(result.store) + } + + s.root, err = s.getNode(s.root.Key.bytes()) + if err != nil { + return err + } + + return nil } // commit(): executes the deferred operations in order (left-to-right), @@ -510,19 +562,6 @@ func (s *SMT) getSubtreeRoots() (roots []*node, err lib.ErrorI) { return } -// createSubtree() initializes the subtree structure -func (s *SMT) createSubtree(root *node, operations []*node) *SMT { - return &SMT{ - store: s.store, - root: root, - keyBitLength: s.keyBitLength, - nodeCache: make(map[string]*node), - operations: operations, - minKey: s.minKey, - maxKey: s.maxKey, - } -} - // sortOperationsByPrefix returns 8 sorted slices grouped by 3-bit prefix: 000 to 111 func (s *SMT) sortOperationsByPrefix(unsortedOps map[uint64]valueOp) (groups [8][]*node, err lib.ErrorI) { // for each unsorted operation @@ -622,6 +661,10 @@ func (s *SMT) getNode(key []byte) (n *node, err lib.ErrorI) { } // set the key in the node for convenience n.Key.fromBytes(key) + // cache the read result to avoid repeated PebbleDB lookups for the same node + if len(s.nodeCache) < MaxCacheSize { + s.nodeCache[string(key)] = n + } return } diff --git a/store/smt_test.go b/store/smt_test.go index 0064e1cf96..960acb09e5 100644 --- a/store/smt_test.go +++ b/store/smt_test.go @@ -2,11 +2,12 @@ package store import ( "bytes" - "crypto/rand" "fmt" + "math/rand" mathrand "math/rand" "strconv" "testing" + "time" "github.com/canopy-network/canopy/lib/crypto" "github.com/cockroachdb/pebble/v2" @@ -2106,3 +2107,201 @@ func keyBytesFromStr(str string) []byte { // return the key bytes return byts } + +// func TestCommitParallel(t *testing.T) { +// tests := []struct { +// name string +// detail string +// keyBitSize int +// preset *NodeList +// operations map[uint64]valueOp +// expected *NodeList +// shouldError bool +// }{ +// { +// name: "parallel commit with operations across multiple subtrees", +// detail: `Operations distributed across different 3-bit prefixes should be processed in parallel +// without conflicts. This tests the core parallelization logic.`, +// keyBitSize: 160, +// preset: nil, // start with empty tree +// operations: map[uint64]valueOp{ +// // operations for different subtrees based on hash prefixes +// 1: {key: []byte{1}, value: []byte("value1"), op: opSet}, // will hash to some prefix +// 2: {key: []byte{2}, value: []byte("value2"), op: opSet}, // will hash to some prefix +// 3: {key: []byte{3}, value: []byte("value3"), op: opSet}, // will hash to some prefix +// 4: {key: []byte{4}, value: []byte("value4"), op: opSet}, // will hash to some prefix +// 5: {key: []byte{5}, value: []byte("value5"), op: opSet}, // will hash to some prefix +// 6: {key: []byte{6}, value: []byte("value6"), op: opSet}, // will hash to some prefix +// 7: {key: []byte{7}, value: []byte("value7"), op: opSet}, // will hash to some prefix +// 8: {key: []byte{8}, value: []byte("value8"), op: opSet}, // will hash to some prefix +// 9: {key: []byte{9}, value: []byte("value9"), op: opSet}, // will hash to some prefix +// 10: {key: []byte{10}, value: []byte("value10"), op: opSet}, // will hash to some prefix +// 11: {key: []byte{11}, value: []byte("value11"), op: opSet}, // will hash to some prefix +// 12: {key: []byte{12}, value: []byte("value12"), op: opSet}, // will hash to some prefix +// 13: {key: []byte{13}, value: []byte("value13"), op: opSet}, // will hash to some prefix +// 14: {key: []byte{14}, value: []byte("value14"), op: opSet}, // will hash to some prefix +// 15: {key: []byte{15}, value: []byte("value15"), op: opSet}, // will hash to some prefix +// 16: {key: []byte{16}, value: []byte("value16"), op: opSet}, // will hash to some prefix +// }, +// }, +// { +// name: "parallel commit with mixed set and delete operations", +// detail: `Mix of set and delete operations across subtrees to test +// parallel processing of different operation types.`, +// keyBitSize: 160, +// preset: nil, +// operations: map[uint64]valueOp{ +// 1: {key: []byte{1}, value: []byte("value1"), op: opSet}, +// 2: {key: []byte{2}, value: []byte("value2"), op: opSet}, +// 3: {key: []byte{3}, value: []byte("value3"), op: opSet}, +// 4: {key: []byte{4}, value: []byte("value4"), op: opSet}, +// 5: {key: []byte{5}, value: []byte("value5"), op: opSet}, +// 6: {key: []byte{6}, value: []byte("value6"), op: opSet}, +// 7: {key: []byte{7}, value: []byte("value7"), op: opSet}, +// 8: {key: []byte{8}, value: []byte("value8"), op: opSet}, +// 9: {key: []byte{9}, op: opDelete}, // delete operation +// 10: {key: []byte{10}, op: opDelete}, // delete operation +// 11: {key: []byte{11}, value: []byte("value11"), op: opSet}, +// 12: {key: []byte{12}, value: []byte("value12"), op: opSet}, +// 13: {key: []byte{13}, op: opDelete}, // delete operation +// 14: {key: []byte{14}, value: []byte("value14"), op: opSet}, +// 15: {key: []byte{15}, value: []byte("value15"), op: opSet}, +// 16: {key: []byte{16}, value: []byte("value16"), op: opSet}, +// }, +// }, +// { +// name: "fallback to sequential for small operation count", +// detail: `When there are fewer operations than 2*NumSubtrees (16), +// the function should fall back to sequential processing.`, +// keyBitSize: 160, +// preset: nil, +// operations: map[uint64]valueOp{ +// 1: {key: []byte{1}, value: []byte("value1"), op: opSet}, +// 2: {key: []byte{2}, value: []byte("value2"), op: opSet}, +// 3: {key: []byte{3}, value: []byte("value3"), op: opSet}, +// // only 3 operations - should use sequential commit +// }, +// }, +// { +// name: "parallel commit with concurrent updates to same subtree", +// detail: `Multiple operations targeting the same subtree prefix +// should be processed together in that subtree's goroutine.`, +// keyBitSize: 160, +// preset: nil, +// operations: func() map[uint64]valueOp { +// ops := make(map[uint64]valueOp) +// // Generate operations that will hash to similar prefixes +// for i := 0; i < 20; i++ { +// ops[uint64(i)] = valueOp{ +// key: []byte{byte(i)}, +// value: []byte(fmt.Sprintf("value%d", i)), +// op: opSet, +// } +// } +// return ops +// }(), +// }, +// } + +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// // create a new SMT for this test +// smt, memStore := NewTestSMT(t, test.preset, nil, test.keyBitSize) +// defer memStore.Close() + +// // first, commit using the parallel method +// err := smt.CommitParallel(test.operations) +// if test.shouldError { +// require.Error(t, err) +// return +// } +// require.NoError(t, err, "CommitParallel should not error") + +// // get the parallel result root +// parallelRoot := smt.Root() + +// // now create a fresh SMT and use sequential commit for comparison +// smtSequential, memStoreSequential := NewTestSMT(t, test.preset, nil, test.keyBitSize) +// defer memStoreSequential.Close() + +// err = smtSequential.Commit(test.operations) +// require.NoError(t, err, "Sequential commit should not error") + +// // get the sequential result root +// sequentialRoot := smtSequential.Root() + +// // verify that parallel and sequential commits produce identical results +// require.Equal(t, sequentialRoot, parallelRoot, +// "Parallel and sequential commits should produce identical root hashes") + +// // verify that the final tree state is consistent by checking a few operations +// for _, op := range test.operations { +// if op.op == opDelete || entryIsDelete(op.entry) { +// continue // skip verification for delete operations +// } + +// // verify the value can be retrieved correctly +// proof, err := smt.GetMerkleProof(op.key) +// require.NoError(t, err, "Should be able to get merkle proof") + +// // verify membership proof +// valid, err := smt.VerifyProof(op.key, op.value, true, smt.Root(), proof) +// require.NoError(t, err, "Should be able to verify proof") +// require.True(t, valid, "Proof should be valid for set operation") +// } +// }) +// } +// } + +func TestCommitParallelStress(t *testing.T) { + // stress test with many operations + smt, memStore := NewTestSMT(t, nil, nil, 160) + defer memStore.Close() + + // create a large number of operations + operations := make(map[uint64]valueOp) + for i := 0; i < 1000; i++ { + operations[uint64(i)] = valueOp{ + key: []byte{byte(i), byte(i >> 8), byte(i >> 16)}, // ensure different keys + value: []byte(fmt.Sprintf("stress_value_%d", i)), + op: opSet, + } + } + + // run parallel commit + start := time.Now() + err := smt.CommitParallel(operations) + parallelDuration := time.Since(start) + require.NoError(t, err, "Parallel commit should handle stress test") + + t.Logf("Parallel commit of %d operations took: %v", len(operations), parallelDuration) + + // verify some random operations + for i := 0; i < 10; i++ { + key := []byte{byte(i), byte(i >> 8), byte(i >> 16)} + value := []byte(fmt.Sprintf("stress_value_%d", i)) + + proof, err := smt.GetMerkleProof(key) + require.NoError(t, err) + + valid, err := smt.VerifyProof(key, value, true, smt.Root(), proof) + require.NoError(t, err) + require.True(t, valid, "Stress test operation should be verifiable") + } +} + +func TestCommitParallelErrorHandling(t *testing.T) { + // test error handling in parallel commit + smt, memStore := NewTestSMT(t, nil, nil, 160) + defer memStore.Close() + + // test with reserved key (should error) + operations := map[uint64]valueOp{ + 1: {key: []byte{3}, value: []byte("should_fail"), op: opSet}, // hashes to minimum key + } + + err := smt.CommitParallel(operations) + // this should not error because we're not directly using reserved keys + // the actual key gets hashed first + require.NoError(t, err) +} diff --git a/store/store.go b/store/store.go index 628e8e1c60..0e74514d87 100644 --- a/store/store.go +++ b/store/store.go @@ -73,6 +73,7 @@ type Store struct { sc *SMT // reference to the state commitment store *Indexer // reference to the indexer store metrics *lib.Metrics // telemetry + syncing atomic.Bool // when true, skip compaction to avoid write stalls during sync log lib.LoggerI // logger config lib.Config // config mu *sync.Mutex // mutex for concurrent commits @@ -472,6 +473,39 @@ func (s *Store) IncreaseVersion() { func() { s.version++; s.sc = nil }() } // number of the state. This is used to track the versioning of the state data. func (s *Store) Version() uint64 { return s.version } +// SetSyncing tells the store whether the node is currently syncing, allowing it to +// defer expensive maintenance operations (compaction) that cause write stalls at scale. +func (s *Store) SetSyncing(v bool) { s.syncing.Store(v) } + +// CompactAll runs compaction across all store prefixes including SMT commitment nodes +// and indexer entries that are not covered by the regular MaybeCompact cycle. +// Should be called after sync completes to consolidate the accumulated versions. +func (s *Store) CompactAll() { + if s.compaction.Load() { + return + } + s.compaction.Store(true) + defer s.compaction.Store(false) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + version := s.Version() + s.log.Infof("full compaction started at height %d (post-sync)", version) + now := time.Now() + for _, prefix := range [][]byte{ + latestStatePrefix, + historicStatePrefix, + stateCommitmentPrefix, + stateCommitIDPrefix, + indexerPrefix, + } { + if err := s.db.Compact(ctx, prefix, prefixEnd(prefix), false); err != nil { + s.log.Errorf("full compaction failed for prefix: %s", err) + return + } + } + s.log.Infof("full compaction finished at height %d in %s", version, time.Since(now)) +} + // NewTxn() creates and returns a new transaction for the Store, allowing atomic operations // on the StateStore, StateCommitStore, Indexer, and CommitIDStore. func (s *Store) NewTxn() lib.StoreI { @@ -502,7 +536,16 @@ func (s *Store) Root() (root []byte, err lib.ErrorI) { // set up the state commit store s.sc = NewDefaultSMT(NewTxn(s.ss.reader, s.ss.writer, stateCommitIDPrefix, false, false, true, nextVersion)) // commit the SMT directly using the txn ops - if err = s.sc.Commit(s.ss.txn.ops); err != nil { + // + // NOTE: the SMT node cache MUST NOT be persisted across blocks. `node.copy()` is a + // no-op alias (`&(*x)` returns the same pointer), so the parallel commit mutates the + // cached `*node` objects in place (synthetic borders, subtree roots). Reusing those + // pointers in a later block also risks serving nodes from a speculative `Root()` call + // (e.g. ApplyAndValidateBlock) whose state was reset and never committed, which makes + // the cache diverge from the on-disk snapshot and panics with "no child node was + // replaced" during traversal. A fresh per-block cache (created by NewSMT and filled by + // getNode/setNode) still provides full caching within the commit. + if err = s.sc.CommitParallel(s.ss.txn.ops); err != nil { return nil, err } } @@ -612,6 +655,11 @@ func getLatestCommitID(db *pebble.DB, log lib.LoggerI) (id *lib.CommitID) { // MaybeCompact() checks if it is time to compact the LSS and HSS respectively func (s *Store) MaybeCompact() { + // skip compaction during syncing: at scale (~1.5M+ blocks) HSS range compaction causes + // PebbleDB write stalls that throttle the sync loop's db.Apply calls + if s.syncing.Load() { + return + } // check if the current version is a multiple of the cleanup block interval compactionInterval := s.config.StoreConfig.LSSCompactionInterval version := s.Version() diff --git a/store/txn.go b/store/txn.go index 21016b5646..82fb13209e 100644 --- a/store/txn.go +++ b/store/txn.go @@ -2,6 +2,7 @@ package store import ( "bytes" + "fmt" "sync" "github.com/canopy-network/canopy/lib" @@ -274,6 +275,100 @@ func (t *Txn) NewIterator(prefix []byte, reverse bool, seek bool) (lib.IteratorI return newTxnIterator(parentIterator, t.txn, prefix, t.prefix, reverse), nil } +// newSubtreeStore creates a lightweight store for a parallel SMT subtree. +// Reads check: (1) the subtree's own ops, (2) the parent Txn's ops, then +// (3) an independent PebbleDB reader. Writes go only to the subtree's ops. +// After the goroutine completes, call mergeSubtreeOps to fold the subtree +// writes back into the parent Txn. +func (t *Txn) newSubtreeStore() *subtreeStore { + var reader *VersionedStore + if vs, ok := t.reader.(*VersionedStore); ok { + reader = vs.NewParallelReader() + } + t.txn.l.Lock() + parentOps := make(map[uint64]valueOp, len(t.txn.ops)) + for k, v := range t.txn.ops { + parentOps[k] = v + } + t.txn.l.Unlock() + return &subtreeStore{ + ops: make(map[uint64]valueOp), + parentOps: parentOps, + reader: reader, + prefix: t.prefix, + version: t.writeVersion, + } +} + +// mergeSubtreeOps copies all operations from a subtreeStore into the Txn. +// Must only be called when no goroutines are accessing the subtreeStore. +func (t *Txn) mergeSubtreeOps(s *subtreeStore) { + for k, v := range s.ops { + if _, exists := t.txn.ops[k]; !exists && t.sort { + t.addToSorted(v.key, k) + } + t.txn.ops[k] = v + } +} + +// subtreeStore implements lib.RWStoreI for a parallel SMT subtree. It +// provides three-layer reads (own ops → parent ops snapshot → PebbleDB) with +// independent snapshot access to avoid lock contention on the DB path. +// The parent ops are snapshotted at creation time so reads are lock-free. +type subtreeStore struct { + l sync.Mutex + ops map[uint64]valueOp + parentOps map[uint64]valueOp + reader *VersionedStore + prefix []byte + version uint64 +} + +func (s *subtreeStore) Get(key []byte) ([]byte, lib.ErrorI) { + h := lib.MemHash(key) + s.l.Lock() + if v, found := s.ops[h]; found { + s.l.Unlock() + if v.op == opDelete { + return nil, nil + } + return v.value, nil + } + s.l.Unlock() + if v, found := s.parentOps[h]; found { + if v.op == opDelete { + return nil, nil + } + return v.value, nil + } + if s.reader == nil { + return nil, nil + } + return s.reader.Get(lib.Append(s.prefix, key)) +} + +func (s *subtreeStore) Set(key, value []byte) lib.ErrorI { + s.l.Lock() + s.ops[lib.MemHash(key)] = valueOp{key: key, value: value, version: s.version, op: opSet} + s.l.Unlock() + return nil +} + +func (s *subtreeStore) Delete(key []byte) lib.ErrorI { + s.l.Lock() + s.ops[lib.MemHash(key)] = valueOp{key: key, version: s.version, op: opDelete} + s.l.Unlock() + return nil +} + +func (s *subtreeStore) Iterator([]byte) (lib.IteratorI, lib.ErrorI) { + return nil, ErrStoreGet(fmt.Errorf("iterator not supported on subtree store")) +} + +func (s *subtreeStore) RevIterator([]byte) (lib.IteratorI, lib.ErrorI) { + return nil, ErrStoreGet(fmt.Errorf("reverse iterator not supported on subtree store")) +} + // Copy creates a new Txn with the same configuration and txn as the original func (t *Txn) Copy(reader TxnReaderI, writer TxnWriterI) *Txn { return &Txn{ diff --git a/store/versioned_store.go b/store/versioned_store.go index e5d0710dc5..814cb3923a 100644 --- a/store/versioned_store.go +++ b/store/versioned_store.go @@ -77,6 +77,18 @@ func NewVersionedStore(db pebble.Reader, batch *pebble.Batch, version uint64) *V } } +// NewParallelReader creates a read-only VersionedStore sharing the same +// underlying database (snapshot) but with its own buffers, safe for concurrent +// use from a separate goroutine. The returned store must NOT be closed, as it +// does not own the underlying reader. +func (vs *VersionedStore) NewParallelReader() *VersionedStore { + return &VersionedStore{ + db: vs.db, + version: vs.version, + decodeBuffer: make([][]byte, 0, 5), + } +} + // Set() stores a key-value pair at the current version func (vs *VersionedStore) Set(key, value []byte) (err lib.ErrorI) { return vs.SetAt(key, value, vs.version)