From 6bee96325eeacc4a64a0e7080499b3a34153e41d Mon Sep 17 00:00:00 2001 From: feie9454 Date: Fri, 24 Apr 2026 17:13:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=9E=E9=AA=8C7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BSVTest.sln | 25 +++ bsv-blockchain.sln | 2 - src/Bsv.MessageTool/Program.cs | 247 ++++++++++++++--------------- src/Bsv.WifTool/Bsv.WifTool.csproj | 14 -- src/Bsv.WifTool/Program.cs | 43 ----- 5 files changed, 141 insertions(+), 190 deletions(-) create mode 100644 BSVTest.sln delete mode 100644 src/Bsv.WifTool/Bsv.WifTool.csproj delete mode 100644 src/Bsv.WifTool/Program.cs diff --git a/BSVTest.sln b/BSVTest.sln new file mode 100644 index 0000000..0f82151 --- /dev/null +++ b/BSVTest.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BSVTest", "BSVTest\BSVTest.csproj", "{DD9417B3-C8B3-49BB-A76B-26D5D7FADB40}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DD9417B3-C8B3-49BB-A76B-26D5D7FADB40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD9417B3-C8B3-49BB-A76B-26D5D7FADB40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD9417B3-C8B3-49BB-A76B-26D5D7FADB40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD9417B3-C8B3-49BB-A76B-26D5D7FADB40}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C72CCBE2-C9F3-479D-A189-210BE09F2D54} + EndGlobalSection +EndGlobal diff --git a/bsv-blockchain.sln b/bsv-blockchain.sln index c4b300f..67c13fb 100644 --- a/bsv-blockchain.sln +++ b/bsv-blockchain.sln @@ -6,8 +6,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A83F02BA-518 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bsv.BlockStats", "src\Bsv.BlockStats\Bsv.BlockStats.csproj", "{F40A60EB-646F-4182-ABBF-D0350B89E1D8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bsv.WifTool", "src\Bsv.WifTool\Bsv.WifTool.csproj", "{527B4760-7362-49A1-96A1-216A1695B49C}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bsv.MessageTool", "src\Bsv.MessageTool\Bsv.MessageTool.csproj", "{DB2B26EB-9FD9-4B48-87CE-9AFEBB2CB683}" EndProject Global diff --git a/src/Bsv.MessageTool/Program.cs b/src/Bsv.MessageTool/Program.cs index d66245c..127241b 100644 --- a/src/Bsv.MessageTool/Program.cs +++ b/src/Bsv.MessageTool/Program.cs @@ -1,157 +1,142 @@ -using System.Security.Cryptography; -using System.Text.RegularExpressions; +using System.Net.Http.Json; +using System.Security.Cryptography; using System.Text; +using System.Text.Json.Serialization; +using BitcoinBlockchain.Data; using NBitcoin; -const string cipherTextBase64 = "QklFMQMdos3oztNAUqW++4+GjfblOT2MSNZEd18K++IqbAckRCq7/RjlzUJbgonrHbZabwdDSyTkrpUM2X8K0aip90/7K0fNvbILk7kgfiQBxMErAwnJzcJXLtKI/Yb3M4Kpj3BSWLhFpYkpx8lMmBsg8lHHnD+JIkf8johOj60UzL0CxeFS5NvohFRmzdesox2Ge8CoNn8A+szcsrENmxvDWGsQx0C/D1ya6GobKYZpWnG+KA0yui7KpfQCM6jP7JvnaLc="; -const string recipientWif = "cRLzVXAinDb2Fvn3hTn5R3q57wbigm5SvFe4WbiDmzrMqGYwPfD3"; -const string candidateAPublicKey = "029b9415580dc6fb0bc298923470ce2f2289cfefb6a06e14f1620e877c164a65b7"; -const string candidateCPrivateKey = "cUsnFska4Ksz8eqCA31JBVYCPPe453jQ8FXHfACaL23sZcBarXKS"; -const string candidateDPublicKey = "03f25e285ae095e927ef3477c05d67ce8d4d31e3a151459c9a76f124248a3738b5"; +const string privateKeyStr = "cNNfJQD8k2awDySowmH631JEJ9AVeueVkwLs9ihDkELhJhpMJVSt"; +const string opReturnData = "jHquEky7h7KGkUkaccZt1R4"; +const string whatsOnChainBaseUrl = "https://api.whatsonchain.com/v1/bsv/test"; -var recipientSecret = ResolveSecret(recipientWif); -var envelope = Bie1Envelope.Parse(cipherTextBase64); -var plainText = envelope.Decrypt(recipientSecret.PrivateKey); -var sender = InferSender(envelope.SenderPublicKey, plainText, recipientSecret.PrivateKey.PubKey); +var secret = ResolveSecret(privateKeyStr); +var address = secret.PrivateKey.PubKey.GetAddress(ScriptPubKeyType.Legacy, secret.Network).ToString(); +var expectedScriptBytes = new byte[] { 0x6a }.Concat(Encoding.ASCII.GetBytes(opReturnData)).ToArray(); +var expectedScript = new ByteArray(expectedScriptBytes); +var cipherBytes = NBitcoin.DataEncoders.Encoders.Base58.DecodeData(opReturnData); -Console.WriteLine($"密文解密后是 {plainText}"); -Console.WriteLine($"内嵌发送方公钥是 {envelope.SenderPublicKey.ToHex()}"); -Console.WriteLine($"这条消息来自 {sender}"); +using var httpClient = new HttpClient(); +httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("BSVTest/1.0"); -static string InferSender(PubKey embeddedSenderPublicKey, string plainText, PubKey recipientPublicKey) +var history = await httpClient.GetFromJsonAsync>($"{whatsOnChainBaseUrl}/address/{address}/history") + ?? throw new InvalidOperationException("无法读取地址历史。"); + +MatchedTransaction? firstMatch = null; + +foreach (var historyItem in history.OrderBy(item => item.Height).ThenBy(item => item.TxHash, StringComparer.Ordinal)) { - var candidates = new Dictionary(StringComparer.Ordinal) - { - ["A"] = new PubKey(Convert.FromHexString(candidateAPublicKey)), - ["B"] = recipientPublicKey, - ["C"] = ResolveSecret(candidateCPrivateKey).PrivateKey.PubKey, - ["D"] = new PubKey(Convert.FromHexString(candidateDPublicKey)) - }; + var transaction = await httpClient.GetFromJsonAsync($"{whatsOnChainBaseUrl}/tx/hash/{historyItem.TxHash}") + ?? throw new InvalidOperationException($"无法读取交易详情: {historyItem.TxHash}"); - var signedMessage = ParseSignedMessage(plainText); - if (signedMessage is not null) - { - foreach (var candidate in candidates) - { - if (candidate.Value.VerifyMessage(signedMessage.Message, signedMessage.Signature)) - { - return candidate.Key; - } - } - } + foreach (var output in transaction.Vout) + { + if (string.IsNullOrWhiteSpace(output.ScriptPubKey.Hex)) + { + continue; + } - foreach (var candidate in candidates) - { - if (candidate.Value.ToHex().Equals(embeddedSenderPublicKey.ToHex(), StringComparison.OrdinalIgnoreCase)) - { - return candidate.Key; - } - } + var actualScript = new ByteArray(Convert.FromHexString(output.ScriptPubKey.Hex)); + if (actualScript == expectedScript) + { + firstMatch = new MatchedTransaction( + transaction.TxId, + historyItem.Height, + transaction.BlockTime, + output.N, + output.ScriptPubKey.Hex, + output.ScriptPubKey.OpReturn?.Parts ?? Array.Empty()); + break; + } + } - foreach (var candidate in candidates.Keys) - { - if (plainText.Contains($"来自{candidate}", StringComparison.Ordinal) - || plainText.Contains($"我是{candidate}", StringComparison.Ordinal) - || plainText.Contains($"from {candidate}", StringComparison.OrdinalIgnoreCase) - || plainText.Contains($"sender:{candidate}", StringComparison.OrdinalIgnoreCase)) - { - return candidate; - } - } - - return "无法仅凭当前信息自动判定"; + if (firstMatch is not null) + { + break; + } } -static SignedMessage? ParseSignedMessage(string plainText) +if (firstMatch is null) { - var match = Regex.Match(plainText, "^Msg: \"(?.*)\", signature: \"(?.*)\"$", RegexOptions.CultureInvariant); - if (!match.Success) - { - return null; - } - - return new SignedMessage(match.Groups["message"].Value, match.Groups["signature"].Value); + throw new InvalidOperationException("没有在该私钥相关地址的历史交易中找到目标 OP_RETURN。"); } +var plainText = DecryptWithBitcoinSVCryptorAes(cipherBytes, privateKeyStr); + +Console.WriteLine($"私钥网络: {secret.Network}"); +Console.WriteLine($"相关地址: {address}"); +Console.WriteLine($"目标脚本(hex): {expectedScript}"); +Console.WriteLine($"第一个命中交易: {firstMatch.TxId}"); +Console.WriteLine($"命中区块高度: {firstMatch.Height}"); +Console.WriteLine($"命中区块时间: {firstMatch.BlockTime}"); +Console.WriteLine($"命中输出序号: {firstMatch.OutputIndex}"); +Console.WriteLine($"链上脚本(hex): {firstMatch.ScriptHex}"); +Console.WriteLine($"Whatsonchain 解析出的 OP_RETURN parts: {string.Join(" | ", firstMatch.OpReturnParts)}"); +Console.WriteLine($"base58 解码后长度: {cipherBytes.Length}"); +Console.WriteLine($"base58 解码后 hex: {Convert.ToHexString(cipherBytes)}"); +Console.WriteLine($"AES_class.AesDecrypt 明文: {plainText}"); + static BitcoinSecret ResolveSecret(string wif) { - foreach (var network in new[] { Network.Main, Network.TestNet }) - { - try - { - return new BitcoinSecret(wif, network); - } - catch (FormatException) - { - } - } + foreach (var network in new[] { Network.Main, Network.TestNet }) + { + try + { + return new BitcoinSecret(wif, network); + } + catch (FormatException) + { + } + } - throw new FormatException($"无法识别 WIF 私钥对应的网络: {wif}"); + throw new FormatException($"无法识别 WIF 私钥对应的网络: {wif}"); } -sealed class Bie1Envelope +static string DecryptWithBitcoinSVCryptorAes(byte[] cipherText, string privateKeyStr) { - private Bie1Envelope(byte[] rawBytes, PubKey senderPublicKey, byte[] cipherText, byte[] mac) - { - RawBytes = rawBytes; - SenderPublicKey = senderPublicKey; - CipherText = cipherText; - Mac = mac; - } + var privateKeyBytes = Encoding.ASCII.GetBytes(privateKeyStr); + using var aes = Aes.Create(); + using var sha256 = SHA256.Create(); - public byte[] RawBytes { get; } + aes.Key = sha256.ComputeHash(privateKeyBytes, 0, 16); + var ivSource = sha256.ComputeHash(privateKeyBytes, 16, 16); + var iv = new byte[16]; + Array.Copy(ivSource, iv, iv.Length); + aes.IV = iv; - public PubKey SenderPublicKey { get; } + using var memoryStream = new MemoryStream(); + using (var cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Write)) + { + cryptoStream.Write(cipherText, 0, cipherText.Length); + cryptoStream.Close(); + } - public byte[] CipherText { get; } - - public byte[] Mac { get; } - - public static Bie1Envelope Parse(string base64) - { - var rawBytes = Convert.FromBase64String(base64); - if (rawBytes.Length <= 69) - { - throw new FormatException("BIE1 密文长度无效。"); - } - - var magic = Encoding.ASCII.GetString(rawBytes, 0, 4); - if (!string.Equals(magic, "BIE1", StringComparison.Ordinal)) - { - throw new FormatException("密文不是 BIE1 格式。"); - } - - var senderPublicKeyBytes = rawBytes[4..37]; - var cipherText = rawBytes[37..^32]; - var mac = rawBytes[^32..]; - - return new Bie1Envelope(rawBytes, new PubKey(senderPublicKeyBytes), cipherText, mac); - } - - public string Decrypt(Key recipientPrivateKey) - { - var sharedPublicKey = SenderPublicKey.GetSharedPubkey(recipientPrivateKey); - var derived = SHA512.HashData(sharedPublicKey.ToBytes()); - var iv = derived[..16]; - var encryptionKey = derived[16..32]; - var macKey = derived[32..64]; - var expectedMac = HMACSHA256.HashData(macKey, RawBytes[..^32]); - - if (!CryptographicOperations.FixedTimeEquals(expectedMac, Mac)) - { - throw new CryptographicException("BIE1 HMAC 校验失败。"); - } - - using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.PKCS7; - aes.Key = encryptionKey; - aes.IV = iv; - - using var decryptor = aes.CreateDecryptor(); - var plainBytes = decryptor.TransformFinalBlock(CipherText, 0, CipherText.Length); - return Encoding.UTF8.GetString(plainBytes); - } + return Encoding.UTF8.GetString(memoryStream.ToArray()); } -sealed record SignedMessage(string Message, string Signature); +sealed record AddressHistoryItem( + [property: JsonPropertyName("tx_hash")] string TxHash, + [property: JsonPropertyName("height")] long Height); + +sealed record TransactionResponse( + [property: JsonPropertyName("txid")] string TxId, + [property: JsonPropertyName("blocktime")] long? BlockTime, + [property: JsonPropertyName("vout")] IReadOnlyList Vout); + +sealed record TransactionOutputResponse( + [property: JsonPropertyName("n")] int N, + [property: JsonPropertyName("scriptPubKey")] ScriptPubKeyResponse ScriptPubKey); + +sealed record ScriptPubKeyResponse( + [property: JsonPropertyName("hex")] string Hex, + [property: JsonPropertyName("opReturn")] OpReturnResponse? OpReturn); + +sealed record OpReturnResponse( + [property: JsonPropertyName("parts")] IReadOnlyList? Parts); + +sealed record MatchedTransaction( + string TxId, + long Height, + long? BlockTime, + int OutputIndex, + string ScriptHex, + IReadOnlyList OpReturnParts); diff --git a/src/Bsv.WifTool/Bsv.WifTool.csproj b/src/Bsv.WifTool/Bsv.WifTool.csproj deleted file mode 100644 index 92ffa2e..0000000 --- a/src/Bsv.WifTool/Bsv.WifTool.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - - - - - diff --git a/src/Bsv.WifTool/Program.cs b/src/Bsv.WifTool/Program.cs deleted file mode 100644 index 8032bbb..0000000 --- a/src/Bsv.WifTool/Program.cs +++ /dev/null @@ -1,43 +0,0 @@ -using NBitcoin; - -const string defaultWif = "Kwa6hgpEfzwcsvcTCetNjfkPtcCgVt9suVo4zjtBp7At17GAQZUi"; - -var wif = args.FirstOrDefault() ?? defaultWif; -var report = Create(wif); - -Console.WriteLine($"WIF私钥:{wif}"); -Console.WriteLine($"其网络是 {report.NetworkName}"); -Console.WriteLine($"对应的public key 是 {report.PublicKey}"); -Console.WriteLine($"对应的public key hash 是 {report.PublicKeyHash}"); -Console.WriteLine($"对应的地址是 {report.Address}"); - -static WifDetails Create(string wif) -{ - foreach (var candidate in new[] - { - new NetworkCandidate(Network.Main, "Main"), - new NetworkCandidate(Network.TestNet, "TestNet") - }) - { - try - { - var secret = new BitcoinSecret(wif, candidate.Network); - var pubKey = secret.PrivateKey.PubKey; - - return new WifDetails( - candidate.NetworkName, - pubKey.ToHex(), - pubKey.Hash.ToString(), - pubKey.GetAddress(ScriptPubKeyType.Legacy, candidate.Network).ToString()); - } - catch (FormatException) - { - } - } - - throw new FormatException($"无法识别 WIF 私钥对应的网络: {wif}"); -} - -sealed record NetworkCandidate(Network Network, string NetworkName); - -sealed record WifDetails(string NetworkName, string PublicKey, string PublicKeyHash, string Address);