From 49d9ed5db0a356bd4004c0e2c0174dd9a38ea5ad Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 12 Nov 2026 15:08:19 +0700 Subject: [PATCH 1/2] bench: measure the DKG commit step at llmq_400_85 size Builds and verifies a premature commitment for 400 members and threshold 340. The cold variant first decodes every verification vector from its database encoding, as GetVerifiedContributions does on a cache miss. --- src/bench/bls_dkg.cpp | 71 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/bench/bls_dkg.cpp b/src/bench/bls_dkg.cpp index 0e3719643e1e..955cf4221deb 100644 --- a/src/bench/bls_dkg.cpp +++ b/src/bench/bls_dkg.cpp @@ -5,7 +5,10 @@ #include #include +#include +#include #include +#include #include #include @@ -59,7 +62,12 @@ class DKG } public: - explicit DKG(int quorumSize) + explicit DKG(int quorumSize) : + DKG(quorumSize, quorumSize / 2 + 1) + { + } + + DKG(int quorumSize, int threshold) { members.reserve(quorumSize); ids.reserve(quorumSize); @@ -73,7 +81,7 @@ class DKG blsWorker.Start(llmq::DEFAULT_WORKER_COUNT); for (auto& member : members) { - blsWorker.GenerateContributions(quorumSize / 2 + 1, ids, member.vvec, member.skShares); + blsWorker.GenerateContributions(threshold, ids, member.vvec, member.skShares); } } ~DKG() @@ -90,6 +98,48 @@ class DKG }); } + // The work of ActiveDKGSession::SendCommitment. A cold run decodes every verification vector from its + // database encoding first, as CDKGSessionManager::GetVerifiedContributions does on a cache miss. + void Bench_BuildCommitment(benchmark::Bench& bench, bool cold) + { + ReceiveVvecs(); + ReceiveShares(0); + CDataStream db(SER_DISK, CLIENT_VERSION); + for (const auto& vvec : receivedVvecs) { + db << *vvec; + } + + bench.epochs(3).run([&] { + auto vvecs = receivedVvecs; + if (cold) { + CDataStream s{db}; + for (auto& vvec : vvecs) { + vvec = std::make_shared>(); + s >> *vvec; + } + } + const auto vvec = blsWorker.BuildQuorumVerificationVector(vvecs); + const auto skShare = blsWorker.AggregateSecretKeys(receivedSkShares); + const auto sig = skShare.Sign(::SerializeHash(*vvec), false); + assert(sig.IsValid()); + }); + } + + // The per-sender work of CDKGSession::ReceiveMessage(CDKGPrematureCommitment) once the vvec is built + void Bench_VerifyCommitment(benchmark::Bench& bench) + { + ReceiveVvecs(); + ReceiveShares(0); + const uint256 hash = ::SerializeHash(*quorumVvec); + const auto sig = blsWorker.AggregateSecretKeys(receivedSkShares).Sign(hash, false); + + bench.run([&] { + const bool valid = ::SerializeHash(*quorumVvec) == hash && + sig.VerifyInsecure(CBLSWorker::BuildPubKeyShare(quorumVvec, ids[0]), hash, false); + assert(valid); + }); + } + void Bench_VerifyContributionShares(benchmark::Bench& bench, int invalidCount, bool aggregated, uint32_t epoch_iters) { ReceiveVvecs(); @@ -190,6 +240,23 @@ static void BLSDKG_InitDKG(benchmark::Bench& bench, uint32_t epoch_iters, int qu } \ BENCHMARK(BLSDKG_VerifyContributionShares_##name##_##quorumSize, benchmark::PriorityLevel::HIGH) +// llmq_400_85 +static void BLSDKG_BuildCommitment_cold_400(benchmark::Bench& bench) +{ + DKG(bench.output() ? 400 : 2, bench.output() ? 340 : 2).Bench_BuildCommitment(bench, /*cold=*/true); +} +static void BLSDKG_BuildCommitment_cached_400(benchmark::Bench& bench) +{ + DKG(bench.output() ? 400 : 2, bench.output() ? 340 : 2).Bench_BuildCommitment(bench, /*cold=*/false); +} +static void BLSDKG_VerifyCommitment_400(benchmark::Bench& bench) +{ + DKG(bench.output() ? 400 : 2, bench.output() ? 340 : 2).Bench_VerifyCommitment(bench); +} +BENCHMARK(BLSDKG_BuildCommitment_cold_400, benchmark::PriorityLevel::HIGH) +BENCHMARK(BLSDKG_BuildCommitment_cached_400, benchmark::PriorityLevel::HIGH) +BENCHMARK(BLSDKG_VerifyCommitment_400, benchmark::PriorityLevel::HIGH) + BENCH_GenerateContributions(simple, 50, 50); BENCH_GenerateContributions(simple, 100, 5); From 898351d69be6ea13e4b8b34e3e9aa7b05083524c Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 12 Nov 2026 15:08:19 +0700 Subject: [PATCH 2/2] perf(llmq): keep verified DKG contributions cached for the session At commit time GetVerifiedContributions re-read and decoded every member's verification vector from LevelDB, although the session still held them in memory. Fill the cache when a verified contribution is written and keep an entry while its vector is still referenced. The commit step for 400 members and threshold 340 goes from 5.0 s to 0.2 s. --- src/llmq/dkgsessionmgr.cpp | 17 +++++++++++++++-- src/llmq/dkgsessionmgr.h | 6 ++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/llmq/dkgsessionmgr.cpp b/src/llmq/dkgsessionmgr.cpp index 77f81eab13fa..b53026a0ca3d 100644 --- a/src/llmq/dkgsessionmgr.cpp +++ b/src/llmq/dkgsessionmgr.cpp @@ -128,11 +128,22 @@ void CDKGSessionManager::WriteVerifiedVvecContribution(Consensus::LLMQType llmqT s << CBLSPublicKeyVersionWrapper(pubkey, false); } db->Write(std::make_tuple(DB_VVEC, llmqType, pQuorumBaseBlockIndex->GetBlockHash(), proTxHash), Span{s}); + + // Reading the vvec back at commit time costs a point decompression and subgroup check per coefficient + LOCK(contributionsCacheCs); + auto& entry = contributionsCache[{llmqType, pQuorumBaseBlockIndex->GetBlockHash(), proTxHash}]; + entry.entryTime = SteadyClock::now(); + entry.vvec = vvec; } void CDKGSessionManager::WriteVerifiedSkContribution(Consensus::LLMQType llmqType, const CBlockIndex* pQuorumBaseBlockIndex, const uint256& proTxHash, const CBLSSecretKey& skContribution) { db->Write(std::make_tuple(DB_SKCONTRIB, llmqType, pQuorumBaseBlockIndex->GetBlockHash(), proTxHash), skContribution); + + LOCK(contributionsCacheCs); + if (auto it = contributionsCache.find({llmqType, pQuorumBaseBlockIndex->GetBlockHash(), proTxHash}); it != contributionsCache.end()) { + it->second.skContribution = skContribution; + } } void CDKGSessionManager::WriteEncryptedContributions(Consensus::LLMQType llmqType, const CBlockIndex* pQuorumBaseBlockIndex, const uint256& proTxHash, const CBLSIESMultiRecipientObjects& contributions) @@ -226,8 +237,10 @@ void CDKGSessionManager::CleanupCache() const { LOCK(contributionsCacheCs); const auto curTime = SteadyClock::now(); - std::erase_if(contributionsCache, - [&](const auto& entry) { return curTime - entry.second.entryTime > MAX_CONTRIBUTION_CACHE_TIME; }); + // A vvec still referenced elsewhere (by its DKG session) costs nothing to keep and is expensive to reload + std::erase_if(contributionsCache, [&](const auto& entry) { + return entry.second.vvec.use_count() == 1 && curTime - entry.second.entryTime > MAX_CONTRIBUTION_CACHE_TIME; + }); } void CDKGSessionManager::CleanupOldContributions() const diff --git a/src/llmq/dkgsessionmgr.h b/src/llmq/dkgsessionmgr.h index cf1cdfa52d1e..8e19cd1f1aad 100644 --- a/src/llmq/dkgsessionmgr.h +++ b/src/llmq/dkgsessionmgr.h @@ -140,8 +140,10 @@ class CDKGSessionManager bool GetPrematureCommitment(const uint256& hash, CDKGPrematureCommitment& ret) const; // Contributions are written while in the DKG - void WriteVerifiedVvecContribution(Consensus::LLMQType llmqType, const CBlockIndex* pQuorumBaseBlockIndex, const uint256& proTxHash, const BLSVerificationVectorPtr& vvec); - void WriteVerifiedSkContribution(Consensus::LLMQType llmqType, const CBlockIndex* pQuorumBaseBlockIndex, const uint256& proTxHash, const CBLSSecretKey& skContribution); + void WriteVerifiedVvecContribution(Consensus::LLMQType llmqType, const CBlockIndex* pQuorumBaseBlockIndex, const uint256& proTxHash, const BLSVerificationVectorPtr& vvec) + EXCLUSIVE_LOCKS_REQUIRED(!contributionsCacheCs); + void WriteVerifiedSkContribution(Consensus::LLMQType llmqType, const CBlockIndex* pQuorumBaseBlockIndex, const uint256& proTxHash, const CBLSSecretKey& skContribution) + EXCLUSIVE_LOCKS_REQUIRED(!contributionsCacheCs); bool GetVerifiedContributions(Consensus::LLMQType llmqType, const CBlockIndex* pQuorumBaseBlockIndex, const std::vector& validMembers, std::vector& memberIndexesRet, std::vector& vvecsRet,