diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 101 |
1 files changed, 99 insertions, 2 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index 310c0d4..be6c80d 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -776,6 +776,29 @@ static void PublishPendingPayouts(const VersionedBlob& v) | |||
| 776 | gPayoutLedgerPublishedSeq, "pending payout ledger"); | 776 | gPayoutLedgerPublishedSeq, "pending payout ledger"); |
| 777 | } | 777 | } |
| 778 | 778 | ||
| 779 | // Parse a persisted block-hash field, refusing to guess. SetHex() reads | ||
| 780 | // whatever hex prefix it finds and stops at the first character it does not | ||
| 781 | // recognise, so a corrupt value does not fail to parse -- it parses as a | ||
| 782 | // DIFFERENT hash. Both shapes of that are wrong in a dangerous direction: a | ||
| 783 | // value with a hex-looking prefix ("deadbeef...") becomes a hash no block | ||
| 784 | // ever had, which PayoutBlockStillOnChain reads as an orphaned block and | ||
| 785 | // FlushMaturePayouts then VOIDS the debt; a value with no hex prefix at all | ||
| 786 | // becomes 0, which is grandfathered past the orphan check on the strength | ||
| 787 | // of garbage. A field that is not the 64-digit hex string ToString() wrote | ||
| 788 | // is corruption, and corruption is a load failure like any other parse | ||
| 789 | // error -- the file is quarantined, not read around. | ||
| 790 | static uint256 ParsePayoutBlockHash(const std::string& str) | ||
| 791 | { | ||
| 792 | if (str.size() != 64 || | ||
| 793 | str.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) | ||
| 794 | throw std::runtime_error( | ||
| 795 | strprintf("blockHash \"%s\" is not a 64-digit hex string", | ||
| 796 | str.c_str())); | ||
| 797 | uint256 h; | ||
| 798 | h.SetHex(str); | ||
| 799 | return h; | ||
| 800 | } | ||
| 801 | |||
| 779 | static void LoadPendingPayouts() | 802 | static void LoadPendingPayouts() |
| 780 | { | 803 | { |
| 781 | std::string path = PayoutFilePath(); | 804 | std::string path = PayoutFilePath(); |
| @@ -797,7 +820,8 @@ static void LoadPendingPayouts() | |||
| 797 | // field. Leave blockHash zero and they are grandfathered past the | 820 | // field. Leave blockHash zero and they are grandfathered past the |
| 798 | // orphan check rather than voided on a guess. | 821 | // orphan check rather than voided on a guess. |
| 799 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) | 822 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) |
| 800 | pp.blockHash.SetHex(obj["blockHash"].get<std::string>()); | 823 | pp.blockHash = |
| 824 | ParsePayoutBlockHash(obj["blockHash"].get<std::string>()); | ||
| 801 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) | 825 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) |
| 802 | pp.blockHeight = obj["blockHeight"].get<int>(); | 826 | pp.blockHeight = obj["blockHeight"].get<int>(); |
| 803 | // Upstream's tolerance for an entry with no usable height, kept | 827 | // Upstream's tolerance for an entry with no usable height, kept |
| @@ -976,7 +1000,8 @@ static void LoadPaying() | |||
| 976 | r.amount = obj["amount"].get<int64>(); | 1000 | r.amount = obj["amount"].get<int64>(); |
| 977 | r.matureAtHeight = obj["matureAtHeight"].get<int>(); | 1001 | r.matureAtHeight = obj["matureAtHeight"].get<int>(); |
| 978 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) | 1002 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) |
| 979 | r.blockHash.SetHex(obj["blockHash"].get<std::string>()); | 1003 | r.blockHash = |
| 1004 | ParsePayoutBlockHash(obj["blockHash"].get<std::string>()); | ||
| 980 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) | 1005 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) |
| 981 | r.blockHeight = obj["blockHeight"].get<int>(); | 1006 | r.blockHeight = obj["blockHeight"].get<int>(); |
| 982 | if (obj.contains("retries") && obj["retries"].is_number()) | 1007 | if (obj.contains("retries") && obj["retries"].is_number()) |
| @@ -4681,6 +4706,78 @@ int RunPayoutLedgerSelfTest() | |||
| 4681 | strPoolRoundsFile = savedRoundsFile; | 4706 | strPoolRoundsFile = savedRoundsFile; |
| 4682 | } | 4707 | } |
| 4683 | 4708 | ||
| 4709 | // -- 16. A malformed block hash is corruption, not a hash ---------------- | ||
| 4710 | // | ||
| 4711 | // SetHex() parses whatever hex prefix it finds and stops, so a corrupt | ||
| 4712 | // blockHash did not fail to load -- it loaded as a DIFFERENT hash. | ||
| 4713 | // "deadbeef..." became 0xdeadbeef, a hash no block ever had, which the | ||
| 4714 | // orphan check reads as an orphaned block and the next flush VOIDS the | ||
| 4715 | // debt; a value with no hex prefix became 0, which is grandfathered past | ||
| 4716 | // the orphan check instead. Either way a corrupt file silently changed | ||
| 4717 | // what the pool owed. It is a load failure now, handled like every other | ||
| 4718 | // parse error: quarantine, and never rewrite. | ||
| 4719 | { | ||
| 4720 | PayoutTestResetState(); | ||
| 4721 | remove(ledgerPath.c_str()); | ||
| 4722 | remove(payingPath.c_str()); | ||
| 4723 | |||
| 4724 | const std::string badLedger = strprintf( | ||
| 4725 | "[{\"amounts\":{\"%s\":100000000}," | ||
| 4726 | "\"blockHash\":\"deadbeef oops, not a hash\"," | ||
| 4727 | "\"blockHeight\":10,\"matureAtHeight\":10}]", addrA.c_str()); | ||
| 4728 | check(PayoutTestWriteFile(ledgerPath, badLedger), | ||
| 4729 | "wrote a ledger whose blockHash is not a 64-digit hex string"); | ||
| 4730 | LoadPendingPayouts(); | ||
| 4731 | { | ||
| 4732 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 4733 | check(gPendingPayouts.empty(), | ||
| 4734 | "no entry is loaded with a guessed hash"); | ||
| 4735 | } | ||
| 4736 | std::string gone; | ||
| 4737 | check(!PayoutTestReadFile(ledgerPath, gone), | ||
| 4738 | "the ledger with the corrupt hash is quarantined"); | ||
| 4739 | |||
| 4740 | const std::string badPaying = strprintf( | ||
| 4741 | "[{\"address\":\"%s\",\"amount\":100000000," | ||
| 4742 | "\"blockHash\":\"00000000cafe\",\"blockHeight\":10," | ||
| 4743 | "\"matureAtHeight\":10}]", addrA.c_str()); | ||
| 4744 | check(PayoutTestWriteFile(payingPath, badPaying), | ||
| 4745 | "wrote an in-flight list whose blockHash is truncated"); | ||
| 4746 | LoadPaying(); | ||
| 4747 | { | ||
| 4748 | std::lock_guard<std::mutex> lk(gPayoutExecMutex); | ||
| 4749 | check(gPaying.empty(), | ||
| 4750 | "no in-flight record is loaded with a guessed hash"); | ||
| 4751 | } | ||
| 4752 | check(!PayoutTestReadFile(payingPath, gone), | ||
| 4753 | "the in-flight list with the corrupt hash is quarantined"); | ||
| 4754 | |||
| 4755 | // The two spellings every real file uses still load: the 64-digit | ||
| 4756 | // hash of a keyed round, and the 64 zeros this build writes for a | ||
| 4757 | // grandfathered entry. | ||
| 4758 | PayoutTestResetState(); | ||
| 4759 | remove(ledgerPath.c_str()); | ||
| 4760 | const std::string goodLedger = strprintf( | ||
| 4761 | "[{\"amounts\":{\"%s\":100000000}," | ||
| 4762 | "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000cd\"," | ||
| 4763 | "\"blockHeight\":10,\"matureAtHeight\":10}," | ||
| 4764 | "{\"amounts\":{\"%s\":200000000}," | ||
| 4765 | "\"blockHash\":\"%s\"," | ||
| 4766 | "\"blockHeight\":20,\"matureAtHeight\":20}]", | ||
| 4767 | addrA.c_str(), addrB.c_str(), uint256(0).ToString().c_str()); | ||
| 4768 | check(PayoutTestWriteFile(ledgerPath, goodLedger), | ||
| 4769 | "wrote a ledger in both real hash spellings"); | ||
| 4770 | LoadPendingPayouts(); | ||
| 4771 | { | ||
| 4772 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 4773 | check(gPendingPayouts.size() == 2 && | ||
| 4774 | gPendingPayouts[0].blockHash != 0 && | ||
| 4775 | gPendingPayouts[1].blockHash == 0, | ||
| 4776 | "a real hash and a serialised zero hash both still load"); | ||
| 4777 | } | ||
| 4778 | remove(ledgerPath.c_str()); | ||
| 4779 | } | ||
| 4780 | |||
| 4684 | // -- clean up ------------------------------------------------------------ | 4781 | // -- clean up ------------------------------------------------------------ |
| 4685 | PayoutTestResetState(); | 4782 | PayoutTestResetState(); |
| 4686 | CRITICAL_BLOCK(cs_mapWallet) | 4783 | CRITICAL_BLOCK(cs_mapWallet) |