diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8c0977f08823..d1448691fdb6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -237,6 +237,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL node/minisketchwrapper.cpp node/peerman_args.cpp node/psbt.cpp + node/quantum.cpp node/timeoffsets.cpp node/transaction.cpp node/txdownloadman_impl.cpp diff --git a/src/common/args.cpp b/src/common/args.cpp index cfd36e1f6a5a..a26c96b1ce0d 100644 --- a/src/common/args.cpp +++ b/src/common/args.cpp @@ -848,6 +848,7 @@ const std::vector TEST_OPTIONS_DOC{ "addrman (use deterministic addrman)", "reindex_after_failure_noninteractive_yes (When asked for a reindex after failure interactively, simulate as-if answered with 'yes')", "bip94 (enforce BIP94 consensus rules)", + "fakenums (use a NUMS point with a known discrete log for the quantum tripwire, so a theft and proof can be simulated)", }; bool HasTestOption(const ArgsManager& args, const std::string& test_option) diff --git a/src/init.cpp b/src/init.cpp index 0e72443cc338..5dbcbe96cad4 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -65,6 +65,7 @@ #include #include #include +#include #include #include #include @@ -1461,6 +1462,17 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) LogInfo("Using at most %i automatic connections (%i file descriptors available)", nMaxConnections, available_fds); + // The quantum tripwire normally verifies proofs against the real BIP-341 + // NUMS point (unknown discrete log). On regtest, -test=fakenums switches to a + // NUMS point whose discrete log is known, so a theft -- and a verifying proof + // -- can be simulated end to end. + if (HasTestOption(args, "fakenums")) { + node::GetQuantumProofStore().UseFakeNumsPoint(); + LogInfo("Quantum tripwire using fake NUMS point (known discrete log) for testing"); + } + // Persist the tripwire's activation point here, so it survives a restart. + node::GetQuantumProofStore().SetActivationFilePath(args.GetDataDirNet() / "quantum_tripwire.dat"); + // Warn about relative -datadir path. if (args.IsArgSet("-datadir") && !args.GetPathArg("-datadir").is_absolute()) { LogWarning("Relative datadir option '%s' specified, which will be interpreted relative to the " @@ -1684,6 +1696,11 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) validation_signals.RegisterValidationInterface(fee_estimator); } + // Watch connected blocks for a published quantum-tripwire proof, so the + // "quantum" deployment can activate instantly. Unregistered by + // UnregisterAllValidationInterfaces() at shutdown. + validation_signals.RegisterValidationInterface(&node::GetQuantumProofStore()); + for (const std::string& socket_addr : args.GetArgs("-bind")) { std::string host_out; uint16_t port_out{0}; @@ -1913,6 +1930,14 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) ChainstateManager& chainman = *Assert(node.chainman); auto& kernel_notifications{*Assert(node.notifications)}; + // Restore the quantum tripwire's activation from its persisted file (if any). + // This must run after the headers are loaded from disk (done above, so the + // activation block can be looked up) but before the network is started and + // new headers are accepted (which could reorg the chain mid-check), to avoid + // a race. Blocks connected later, e.g. during reindex/IBD, are handled + // incrementally by the registered validation interface. + node::GetQuantumProofStore().LoadActivationFromFile(chainman); + assert(!node.peerman); node.peerman = PeerManager::make(*node.connman, *node.addrman, node.banman.get(), chainman, diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 61b8a4ecd852..deddc5100fd9 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -539,6 +540,7 @@ class PeerManagerImpl final : public PeerManager std::vector GetPrivateBroadcastInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); std::vector AbortPrivateBroadcast(const uint256& id) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); void SendPings() override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); + void RelayQuantumProof(std::span proof) override { FloodQuantumProof(proof, /*exclude_peer=*/std::nullopt); } void InitiateTxBroadcastToAll(const Txid& txid, const Wtxid& wtxid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); void InitiateTxBroadcastPrivate(const CTransactionRef& tx) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex); void SetBestBlock(int height, std::chrono::seconds time) override @@ -720,6 +722,16 @@ class PeerManagerImpl final : public PeerManager /** Send a message to a peer */ void PushMessage(CNode& node, CSerializedNetMsg&& msg) const { m_connman.PushMessage(&node, std::move(msg)); } + //! Send a quantum-tripwire proof to every connected peer except `exclude_peer`. + void FloodQuantumProof(std::span proof, std::optional exclude_peer) + { + const std::vector data(proof.begin(), proof.end()); + m_connman.ForEachNode([&](CNode* pnode) { + if (!exclude_peer || pnode->GetId() != *exclude_peer) { + MakeAndPushMessage(*pnode, NetMsgType::QPROOF, data); + } + }); + } template void MakeAndPushMessage(CNode& node, std::string msg_type, Args&&... args) const { @@ -5101,6 +5113,26 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string return; } + if (msg_type == NetMsgType::QPROOF) { + // Relayed "aRsm" ECDL-break (quantum tripwire) proof. + std::vector proof; + vRecv >> proof; + if (proof.size() != node::QUANTUM_PROOF_SIZE) { + LogDebug(BCLog::NET, "qproof of invalid size %u from peer=%d\n", proof.size(), pfrom.GetId()); + return; + } + // Verify and store. Add() returns false for an invalid (fake) or already + // known proof, in which case we neither store nor relay it: over p2p we + // only ever accept and forward the first valid proof. + if (!node::GetQuantumProofStore().Add(proof)) { + LogDebug(BCLog::NET, "ignored invalid or duplicate qproof from peer=%d\n", pfrom.GetId()); + return; + } + LogDebug(BCLog::NET, "stored and relaying new qproof from peer=%d\n", pfrom.GetId()); + FloodQuantumProof(proof, /*exclude_peer=*/pfrom.GetId()); + return; + } + // Ignore unknown message types for extensibility LogDebug(BCLog::NET, "Unknown message type \"%s\" from peer=%d", SanitizeString(msg_type), pfrom.GetId()); return; diff --git a/src/net_processing.h b/src/net_processing.h index 630656e2af6d..9acd9645e5be 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -153,6 +154,9 @@ class PeerManager : public CValidationInterface, public NetEventsInterface /** Send ping message to all peers */ virtual void SendPings() = 0; + /** Relay a verified "aRsm" ECDL-break (quantum tripwire) proof to all peers. */ + virtual void RelayQuantumProof(std::span proof) = 0; + /** Set the height of the best block and its time (seconds since epoch). */ virtual void SetBestBlock(int height, std::chrono::seconds time) = 0; diff --git a/src/node/miner.cpp b/src/node/miner.cpp index 32a21440c4e5..959e3f75b535 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -183,6 +184,34 @@ std::unique_ptr BlockAssembler::CreateNewBlock() coinbaseTx.vout[0].nValue = block_reward; coinbase_tx.block_reward_remaining = block_reward; + // Quantum tripwire: until the "quantum" deployment activates, publish the + // single stored "aRsm" ECDL-break proof as a required OP_RETURN output in the + // coinbase. The proof is QUANTUM_PROOF_SIZE bytes, carried in a standard + // null-data output with zero value (so block_reward_remaining is unaffected), + // and added as a required_output that IPC mining clients must include, + // alongside the witness commitment appended below. + // + // The proof cannot fit in the 80-byte block header, so the coinbase is the + // only place to carry it. Using a coinbase OP_RETURN as a signalling channel + // mirrors a draft for coinbase-based version-bit signalling: + // https://gnusha.org/pi/bitcoindev/D52CEDCC-C7EF-429E-802F-F28DC9241FF0@sprovoost.nl/ + // In that thread Antoine Poinsot argued against using the coinbase for + // signalling -- chiefly that, unlike version bits in the header, it forces + // nodes to read full blocks to learn the activation state. We sidestep that + // by persisting the activation point (block hash + proof) to a small file + // (see node::QuantumProofStore), so determining activation after the one-off + // signal needs no block re-reads: a header lookup suffices, even on pruned + // nodes. We publish the proof in exactly one block: once that block connects, + // the deployment activates from the next block and we stop adding it + // (IsActiveAtHeight() below). + if (auto& quantum{GetQuantumProofStore()}; !quantum.IsActiveAtHeight(nHeight)) { + if (const auto proof{quantum.GetProof()}) { + CTxOut proof_out{0, CScript() << OP_RETURN << *proof}; + coinbaseTx.vout.push_back(proof_out); + coinbase_tx.required_outputs.push_back(proof_out); + } + } + // Start the coinbase scriptSig with the block height as required by BIP34. // Mining clients are expected to append extra data to this prefix, so // increasing its length would reduce the space they can use and may break diff --git a/src/node/quantum.cpp b/src/node/quantum.cpp new file mode 100644 index 000000000000..aa505a8f8f34 --- /dev/null +++ b/src/node/quantum.cpp @@ -0,0 +1,272 @@ +// Copyright (c) 2026 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include