Skip to content

Commit b4d001a

Browse files
authored
instantsend: deterministic lock using the same msg hash as islock (#4381)
When receiving an islock, propagate it as islock. When creating/receiving and isdlock, propagate it as isdlock to peers which support it and as islock to peers which don't. Functional tests to cover both islock and isdlock scenarios.
1 parent 7b94386 commit b4d001a

File tree

12 files changed

+156
-35
lines changed

12 files changed

+156
-35
lines changed

src/llmq/quorums_instantsend.cpp

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ static const std::string DB_ARCHIVED_BY_HASH = "is_a2";
3939
static const std::string DB_VERSION = "is_v";
4040

4141
const int CInstantSendDb::CURRENT_VERSION;
42+
const uint8_t CInstantSendLock::islock_version;
43+
const uint8_t CInstantSendLock::isdlock_version;
4244

4345
CInstantSendManager* quorumInstantSendManager;
4446

@@ -325,10 +327,14 @@ CInstantSendLockPtr CInstantSendDb::GetInstantSendLockByHash(const uint256& hash
325327
return ret;
326328
}
327329

328-
ret = std::make_shared<CInstantSendLock>();
330+
ret = std::make_shared<CInstantSendLock>(CInstantSendLock::isdlock_version);
329331
bool exists = db->Read(std::make_tuple(DB_ISLOCK_BY_HASH, hash), *ret);
330332
if (!exists) {
331-
ret = nullptr;
333+
ret = std::make_shared<CInstantSendLock>();
334+
exists = db->Read(std::make_tuple(DB_ISLOCK_BY_HASH, hash), *ret);
335+
if (!exists) {
336+
ret = nullptr;
337+
}
332338
}
333339
islockCache.insert(hash, ret);
334340
return ret;
@@ -686,7 +692,7 @@ void CInstantSendManager::HandleNewInputLockRecoveredSig(const CRecoveredSig& re
686692

687693
void CInstantSendManager::TrySignInstantSendLock(const CTransaction& tx)
688694
{
689-
auto llmqType = Params().GetConsensus().llmqTypeInstantSend;
695+
const auto llmqType = Params().GetConsensus().llmqTypeInstantSend;
690696

691697
for (auto& in : tx.vin) {
692698
auto id = ::SerializeHash(std::make_pair(INPUTLOCK_REQUESTID_PREFIX, in.prevout));
@@ -698,12 +704,20 @@ void CInstantSendManager::TrySignInstantSendLock(const CTransaction& tx)
698704
LogPrint(BCLog::INSTANTSEND, "CInstantSendManager::%s -- txid=%s: got all recovered sigs, creating CInstantSendLock\n", __func__,
699705
tx.GetHash().ToString());
700706

701-
CInstantSendLock islock;
707+
CInstantSendLock islock(CInstantSendLock::isdlock_version);
702708
islock.txid = tx.GetHash();
703709
for (auto& in : tx.vin) {
704710
islock.inputs.emplace_back(in.prevout);
705711
}
706712

713+
// compute cycle hash
714+
{
715+
LOCK(cs_main);
716+
const auto dkgInterval = GetLLMQParams(llmqType).dkgInterval;
717+
const auto quorumHeight = chainActive.Height() - (chainActive.Height() % dkgInterval);
718+
islock.cycleHash = chainActive[quorumHeight]->GetBlockHash();
719+
}
720+
707721
auto id = islock.GetRequestId();
708722

709723
if (quorumSigningManager->HasRecoveredSigForId(llmqType, id)) {
@@ -760,8 +774,9 @@ void CInstantSendManager::ProcessMessage(CNode* pfrom, const std::string& strCom
760774
return;
761775
}
762776

763-
if (strCommand == NetMsgType::ISLOCK) {
764-
auto islock = std::make_shared<CInstantSendLock>();
777+
if (strCommand == NetMsgType::ISLOCK || strCommand == NetMsgType::ISDLOCK) {
778+
const auto islock_version = strCommand == NetMsgType::ISLOCK ? CInstantSendLock::islock_version : CInstantSendLock::isdlock_version;
779+
const auto islock = std::make_shared<CInstantSendLock>(islock_version);
765780
vRecv >> *islock;
766781
ProcessMessageInstantSendLock(pfrom, islock);
767782
}
@@ -775,14 +790,29 @@ void CInstantSendManager::ProcessMessageInstantSendLock(const CNode* pfrom, cons
775790

776791
{
777792
LOCK(cs_main);
778-
EraseObjectRequest(pfrom->GetId(), CInv(MSG_ISLOCK, hash));
793+
EraseObjectRequest(pfrom->GetId(), CInv(islock->IsDeterministic() ? MSG_ISDLOCK : MSG_ISLOCK, hash));
779794
}
780795

781796
if (!PreVerifyInstantSendLock(*islock)) {
782797
LOCK(cs_main);
783798
Misbehaving(pfrom->GetId(), 100);
784799
return;
785800
}
801+
if (islock->IsDeterministic()) {
802+
const auto blockIndex = WITH_LOCK(cs_main, return LookupBlockIndex(islock->cycleHash));
803+
if (blockIndex == nullptr) {
804+
// Maybe we don't have the block yet or maybe some peer spams invalid values for cycleHash
805+
WITH_LOCK(cs_main, Misbehaving(pfrom->GetId(), 1));
806+
return;
807+
}
808+
809+
const auto llmqType = Params().GetConsensus().llmqTypeInstantSend;
810+
const auto dkgInterval = GetLLMQParams(llmqType).dkgInterval;
811+
if (blockIndex->nHeight % dkgInterval != 0) {
812+
WITH_LOCK(cs_main, Misbehaving(pfrom->GetId(), 100));
813+
return;
814+
}
815+
}
786816

787817
LOCK(cs);
788818
if (pendingInstantSendLocks.count(hash) || db.KnownInstantSendLock(hash)) {
@@ -901,7 +931,23 @@ std::unordered_set<uint256> CInstantSendManager::ProcessPendingInstantSendLocks(
901931
continue;
902932
}
903933

904-
auto quorum = llmq::CSigningManager::SelectQuorumForSigning(llmqType, id, -1, signOffset);
934+
int nSignHeight{-1};
935+
if (islock->IsDeterministic()) {
936+
LOCK(cs_main);
937+
938+
const auto blockIndex = LookupBlockIndex(islock->cycleHash);
939+
if (blockIndex == nullptr) {
940+
batchVerifier.badSources.emplace(nodeId);
941+
continue;
942+
}
943+
944+
const auto dkgInterval = GetLLMQParams(Params().GetConsensus().llmqTypeInstantSend).dkgInterval;
945+
if (blockIndex->nHeight + dkgInterval < chainActive.Height()) {
946+
nSignHeight = blockIndex->nHeight + dkgInterval - 1;
947+
}
948+
}
949+
950+
auto quorum = llmq::CSigningManager::SelectQuorumForSigning(llmqType, id, nSignHeight, signOffset);
905951
if (!quorum) {
906952
// should not happen, but if one fails to select, all others will also fail to select
907953
return {};
@@ -1028,13 +1074,14 @@ void CInstantSendManager::ProcessInstantSendLock(NodeId from, const uint256& has
10281074
TruncateRecoveredSigsForInputs(*islock);
10291075
}
10301076

1031-
CInv inv(MSG_ISLOCK, hash);
1077+
const auto is_det = islock->IsDeterministic();
1078+
CInv inv(is_det ? MSG_ISDLOCK : MSG_ISLOCK, hash);
10321079
if (tx != nullptr) {
1033-
g_connman->RelayInvFiltered(inv, *tx, LLMQS_PROTO_VERSION);
1080+
g_connman->RelayInvFiltered(inv, *tx, is_det ? ISDLOCK_PROTO_VERSION : LLMQS_PROTO_VERSION);
10341081
} else {
10351082
// we don't have the TX yet, so we only filter based on txid. Later when that TX arrives, we will re-announce
10361083
// with the TX taken into account.
1037-
g_connman->RelayInvFiltered(inv, islock->txid, LLMQS_PROTO_VERSION);
1084+
g_connman->RelayInvFiltered(inv, islock->txid, is_det ? ISDLOCK_PROTO_VERSION : LLMQS_PROTO_VERSION);
10381085
}
10391086

10401087
ResolveBlockConflicts(hash, *islock);
@@ -1068,8 +1115,8 @@ void CInstantSendManager::TransactionAddedToMempool(const CTransactionRef& tx)
10681115
}
10691116
// In case the islock was received before the TX, filtered announcement might have missed this islock because
10701117
// we were unable to check for filter matches deep inside the TX. Now we have the TX, so we should retry.
1071-
CInv inv(MSG_ISLOCK, ::SerializeHash(*islock));
1072-
g_connman->RelayInvFiltered(inv, *tx, LLMQS_PROTO_VERSION);
1118+
CInv inv(islock->IsDeterministic() ? MSG_ISDLOCK : MSG_ISLOCK, ::SerializeHash(*islock));
1119+
g_connman->RelayInvFiltered(inv, *tx, islock->IsDeterministic() ? ISDLOCK_PROTO_VERSION : LLMQS_PROTO_VERSION);
10731120
// If the islock was received before the TX, we know we were not able to send
10741121
// the notification at that time, we need to do it now.
10751122
LogPrint(BCLog::INSTANTSEND, "CInstantSendManager::%s -- notify about an earlier received lock for tx %s\n", __func__, tx->GetHash().ToString());

src/llmq/quorums_instantsend.h

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,37 @@
2020
namespace llmq
2121
{
2222

23-
class CInstantSendLock
23+
struct CInstantSendLock
2424
{
25-
public:
25+
// This is the old format of instant send lock, it must be 0
26+
static const uint8_t islock_version = 0;
27+
// This is the new format of instant send deterministic lock, this should be incremented for new isdlock versions
28+
static const uint8_t isdlock_version = 1;
29+
30+
uint8_t nVersion;
2631
std::vector<COutPoint> inputs;
2732
uint256 txid;
33+
uint256 cycleHash;
2834
CBLSLazySignature sig;
2935

30-
public:
36+
CInstantSendLock() : CInstantSendLock(islock_version) {}
37+
explicit CInstantSendLock(const uint8_t desiredVersion) : nVersion(desiredVersion) {}
38+
3139
SERIALIZE_METHODS(CInstantSendLock, obj)
3240
{
33-
READWRITE(obj.inputs, obj.txid, obj.sig);
41+
if (s.GetVersion() >= ISDLOCK_PROTO_VERSION && obj.IsDeterministic()) {
42+
READWRITE(obj.nVersion);
43+
}
44+
READWRITE(obj.inputs);
45+
READWRITE(obj.txid);
46+
if (s.GetVersion() >= ISDLOCK_PROTO_VERSION && obj.IsDeterministic()) {
47+
READWRITE(obj.cycleHash);
48+
}
49+
READWRITE(obj.sig);
3450
}
3551

3652
uint256 GetRequestId() const;
53+
bool IsDeterministic() const { return nVersion != islock_version; }
3754
};
3855

3956
typedef std::shared_ptr<CInstantSendLock> CInstantSendLockPtr;

src/net_processing.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ std::chrono::microseconds GetObjectInterval(int invType)
792792
case MSG_CLSIG:
793793
return std::chrono::seconds{5};
794794
case MSG_ISLOCK:
795+
case MSG_ISDLOCK:
795796
return std::chrono::seconds{10};
796797
default:
797798
return GETDATA_TX_INTERVAL;
@@ -1438,6 +1439,7 @@ bool static AlreadyHave(const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
14381439
case MSG_CLSIG:
14391440
return llmq::chainLocksHandler->AlreadyHave(inv);
14401441
case MSG_ISLOCK:
1442+
case MSG_ISDLOCK:
14411443
return llmq::quorumInstantSendManager->AlreadyHave(inv);
14421444
}
14431445

@@ -1772,10 +1774,11 @@ void static ProcessGetData(CNode* pfrom, const CChainParams& chainparams, CConnm
17721774
}
17731775
}
17741776

1775-
if (!push && (inv.type == MSG_ISLOCK)) {
1777+
if (!push && (inv.type == MSG_ISLOCK || inv.type == MSG_ISDLOCK)) {
17761778
llmq::CInstantSendLock o;
17771779
if (llmq::quorumInstantSendManager->GetInstantSendLockByHash(inv.hash, o)) {
1778-
connman->PushMessage(pfrom, msgMaker.Make(NetMsgType::ISLOCK, o));
1780+
const auto msg_type = inv.type == MSG_ISLOCK ? NetMsgType::ISLOCK : NetMsgType::ISDLOCK;
1781+
connman->PushMessage(pfrom, msgMaker.Make(msg_type, o));
17791782
push = true;
17801783
}
17811784
}
@@ -4604,9 +4607,11 @@ bool PeerLogicValidation::SendMessages(CNode* pto)
46044607
int nInvType = CCoinJoin::GetDSTX(hash) ? MSG_DSTX : MSG_TX;
46054608
queueAndMaybePushInv(CInv(nInvType, hash));
46064609

4607-
uint256 islockHash;
4608-
if (!llmq::quorumInstantSendManager->GetInstantSendLockHashByTxid(hash, islockHash)) continue;
4609-
queueAndMaybePushInv(CInv(MSG_ISLOCK, islockHash));
4610+
const auto islock = llmq::quorumInstantSendManager->GetInstantSendLockByTxid(hash);
4611+
if (islock == nullptr) continue;
4612+
if (pto->nVersion < LLMQS_PROTO_VERSION) continue;
4613+
if (pto->nVersion < ISDLOCK_PROTO_VERSION && islock->IsDeterministic()) continue;
4614+
queueAndMaybePushInv(CInv(islock->IsDeterministic() ? MSG_ISDLOCK : MSG_ISLOCK, ::SerializeHash(*islock)));
46104615
}
46114616

46124617
// Send an inv for the best ChainLock we have

src/protocol.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const char* QGETDATA = "qgetdata";
8080
const char* QDATA = "qdata";
8181
const char *CLSIG="clsig";
8282
const char *ISLOCK="islock";
83+
const char *ISDLOCK="isdlock";
8384
const char *MNAUTH="mnauth";
8485
}; // namespace NetMsgType
8586

@@ -157,6 +158,7 @@ const static std::string allNetMessageTypes[] = {
157158
NetMsgType::QDATA,
158159
NetMsgType::CLSIG,
159160
NetMsgType::ISLOCK,
161+
NetMsgType::ISDLOCK,
160162
NetMsgType::MNAUTH,
161163
};
162164
const static std::vector<std::string> allNetMessageTypesVec(allNetMessageTypes, allNetMessageTypes+ARRAYLEN(allNetMessageTypes));
@@ -289,6 +291,7 @@ const char* CInv::GetCommandInternal() const
289291
case MSG_QUORUM_RECOVERED_SIG: return NetMsgType::QSIGREC;
290292
case MSG_CLSIG: return NetMsgType::CLSIG;
291293
case MSG_ISLOCK: return NetMsgType::ISLOCK;
294+
case MSG_ISDLOCK: return NetMsgType::ISDLOCK;
292295
default:
293296
return nullptr;
294297
}

src/protocol.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ extern const char* QGETDATA;
291291
extern const char* QDATA;
292292
extern const char *CLSIG;
293293
extern const char *ISLOCK;
294+
extern const char *ISDLOCK;
294295
extern const char *MNAUTH;
295296
};
296297

@@ -459,6 +460,7 @@ enum GetDataMsg {
459460
MSG_QUORUM_RECOVERED_SIG = 28,
460461
MSG_CLSIG = 29,
461462
MSG_ISLOCK = 30,
463+
MSG_ISDLOCK = 31,
462464
};
463465

464466
/** inv message data */

src/version.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313

14-
static const int PROTOCOL_VERSION = 70219;
14+
static const int PROTOCOL_VERSION = 70220;
1515

1616
//! initial proto version, to be increased after version/verack negotiation
1717
static const int INIT_PROTO_VERSION = 209;
@@ -48,6 +48,9 @@ static const int MNAUTH_NODE_VER_VERSION = 70218;
4848
//! introduction of QGETDATA/QDATA messages
4949
static const int LLMQ_DATA_MESSAGES_VERSION = 70219;
5050

51+
//! introduction of instant send deterministic lock (ISDLOCK)
52+
static const int ISDLOCK_PROTO_VERSION = 70220;
53+
5154
// Make sure that none of the values above collide with `ADDRV2_FORMAT`.
5255

5356
#endif // BITCOIN_VERSION_H

test/functional/feature_llmq_is_cl_conflicts.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ def send_clsig(self, clsig):
3535
inv = msg_inv([CInv(29, hash)])
3636
self.send_message(inv)
3737

38-
def send_islock(self, islock):
38+
def send_islock(self, islock, deterministic=False):
3939
hash = uint256_from_str(hash256(islock.serialize()))
4040
self.islocks[hash] = islock
4141

42-
inv = msg_inv([CInv(30, hash)])
42+
inv = msg_inv([CInv(31 if deterministic else 30, hash)])
4343
self.send_message(inv)
4444

4545
def on_getdata(self, message):
@@ -72,7 +72,8 @@ def run_test(self):
7272
self.test_chainlock_overrides_islock(False)
7373
self.test_chainlock_overrides_islock(True, False)
7474
self.test_chainlock_overrides_islock(True, True)
75-
self.test_chainlock_overrides_islock_overrides_nonchainlock()
75+
self.test_chainlock_overrides_islock_overrides_nonchainlock(False)
76+
self.test_chainlock_overrides_islock_overrides_nonchainlock(True)
7677

7778
def test_chainlock_overrides_islock(self, test_block_conflict, mine_confllicting=False):
7879
if not test_block_conflict:
@@ -191,7 +192,7 @@ def test_chainlock_overrides_islock(self, test_block_conflict, mine_confllicting
191192
assert rawtx['instantlock']
192193
assert not rawtx['instantlock_internal']
193194

194-
def test_chainlock_overrides_islock_overrides_nonchainlock(self):
195+
def test_chainlock_overrides_islock_overrides_nonchainlock(self, deterministic):
195196
# create two raw TXs, they will conflict with each other
196197
rawtx1 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100)['hex']
197198
rawtx2 = self.create_raw_tx(self.nodes[0], self.nodes[0], 1, 1, 100)['hex']
@@ -200,7 +201,7 @@ def test_chainlock_overrides_islock_overrides_nonchainlock(self):
200201
rawtx2_txid = encode(hash256(hex_str_to_bytes(rawtx2))[::-1], 'hex_codec').decode('ascii')
201202

202203
# Create an ISLOCK but don't broadcast it yet
203-
islock = self.create_islock(rawtx2)
204+
islock = self.create_islock(rawtx2, deterministic)
204205

205206
# Disable ChainLocks to avoid accidental locking
206207
self.nodes[0].spork("SPORK_19_CHAINLOCKS_ENABLED", 4070908800)
@@ -229,7 +230,7 @@ def test_chainlock_overrides_islock_overrides_nonchainlock(self):
229230

230231
# Send the ISLOCK, which should result in the last 2 blocks to be invalidated, even though the nodes don't know
231232
# the locked transaction yet
232-
self.test_node.send_islock(islock)
233+
self.test_node.send_islock(islock, deterministic)
233234
time.sleep(5)
234235

235236
assert self.nodes[0].getbestblockhash() == good_tip

test/functional/interface_zmq_dash.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
hash256,
3232
msg_clsig,
3333
msg_inv,
34-
msg_islock,
34+
msg_isdlock,
3535
msg_tx,
3636
ser_string,
3737
uint256_from_str,
@@ -266,7 +266,7 @@ def test_instantsend_publishers(self):
266266
zmq_tx_lock_tx.deserialize(zmq_tx_lock_sig_stream)
267267
assert zmq_tx_lock_tx.is_valid()
268268
assert_equal(zmq_tx_lock_tx.hash, rpc_raw_tx_1['txid'])
269-
zmq_tx_lock = msg_islock()
269+
zmq_tx_lock = msg_isdlock()
270270
zmq_tx_lock.deserialize(zmq_tx_lock_sig_stream)
271271
assert_equal(uint256_to_string(zmq_tx_lock.txid), rpc_raw_tx_1['txid'])
272272
# Try to send the second transaction. This must throw an RPC error because it conflicts with rpc_raw_tx_1

test/functional/rpc_verifyislock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def run_test(self):
8282
break
8383
assert selected_hash == oldest_quorum_hash
8484
# Create the ISLOCK, then mine a quorum to move the signing quorum out of the active set
85-
islock = self.create_islock(rawtx)
85+
islock = self.create_islock(rawtx, True)
8686
# Mine one block to trigger the "signHeight + dkgInterval" verification for the ISLOCK
8787
self.mine_quorum()
8888
# Verify the ISLOCK for a transaction that is not yet known by the node

0 commit comments

Comments
 (0)