Compare commits
4 Commits
be818a50a8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| feb765663c | |||
| 322ce8f305 | |||
| 077835a30b | |||
| 663460d62e |
+102
@@ -5,6 +5,108 @@ Technical changelog for PalladiumWallet. Format loosely follows
|
||||
by subsystem rather than strictly by date, since `0.9.0` is the first
|
||||
release and covers the full history from the initial commit.
|
||||
|
||||
## [1.0.0] — 2026-07-09
|
||||
|
||||
First stable release. Closes the last open security gap from 0.9.x (header
|
||||
trust was not actually anchored to any checkpoint), fixes several crash
|
||||
paths found by a new fuzzing harness, and adds OP_RETURN/coinbase-tag
|
||||
decoding to the transaction detail view.
|
||||
|
||||
### Security
|
||||
|
||||
- `WalletSynchronizer.AnchorToCheckpointAsync`: header trust is now actually
|
||||
anchored to `ChainProfiles.Mainnet.Checkpoints` (24 real `[height, hash,
|
||||
bits]` checkpoints pulled from a fully-synced node, every 20,000 blocks
|
||||
plus one near tip). Previously the checkpoint array was empty and the
|
||||
methods meant to enforce it (`MatchesCheckpoint`/`IsValidChild`) were
|
||||
never called — on this LWMA chain, where PoW can't be recomputed
|
||||
client-side, a malicious or eclipsing server could hand back any
|
||||
internally-consistent header for a Merkle proof with nothing tying it to
|
||||
the real chain. For every header used in a Merkle proof, the intervening
|
||||
headers are now downloaded back to the nearest checkpoint and verified as
|
||||
an unbroken prev-hash chain terminating in that checkpoint's exact hash
|
||||
(memoized per sync session). Testnet/Regtest remain unanchored (no node
|
||||
available to source checkpoints from); a missing checkpoint is a no-op,
|
||||
not a failure.
|
||||
- New fuzzing harness (`tests/PalladiumWallet.Fuzz`, SharpFuzz-based): one
|
||||
target per untrusted-input parser (header, Merkle proof, peer list,
|
||||
wallet file, mnemonic/key/address/amount), each encoding that parser's
|
||||
documented error contract. Found and fixed:
|
||||
- `Bip39.TryParse` threw `NotSupportedException` on text resembling no
|
||||
wordlist instead of failing gracefully.
|
||||
- `ElectrumApi.ParsePeers` threw on any `server.peers.subscribe` response
|
||||
shape other than the expected `[ip, hostname, [features...]]`, and on a
|
||||
JSON string containing invalid UTF-8.
|
||||
- `EncryptedFile.Decrypt` let a tampered/corrupted wallet file escape as
|
||||
raw `JsonException`/`FormatException`/`ArgumentNullException` instead of
|
||||
the documented `WrongPasswordException`/`InvalidDataException`
|
||||
contract; worse, the PBKDF2 iteration count was read from the
|
||||
(attacker-controlled) container with no upper bound — a tampered file
|
||||
demanding e.g. 2³¹ iterations would hang the wallet on open. Iteration
|
||||
count is now clamped to 10,000,000.
|
||||
- `CertificatePinStore.Load`: a corrupted pin file blocked every SSL
|
||||
connection until manually deleted; now falls back to first-contact
|
||||
TOFU like `ServerRegistry` already did.
|
||||
- The seed corpus (incl. a regression input per fixed finding) replays
|
||||
inside `dotnet test` via `FuzzCorpusTests`, so a fix can't silently
|
||||
regress.
|
||||
- `SECURITY.md` corrected: PBKDF2 parameters (600,000 iterations / 16-byte
|
||||
salt, not the pre-hardening 100,000 / 32-byte), a stale file reference,
|
||||
and disclosure of the AI-assisted testing methodology used (adversarial
|
||||
fake-server simulation, property-based fuzzing, targeted security
|
||||
review) as a complement to, not a replacement for, independent review.
|
||||
|
||||
### Added
|
||||
|
||||
- Transaction detail view now decodes OP_RETURN output payloads (UTF-8, or
|
||||
hex if the bytes aren't valid text — multiple OP_RETURN outputs in one tx
|
||||
are each decoded independently) and coinbase scriptSig pool tags (e.g.
|
||||
`/slush/`, extracted as printable-ASCII runs amid the binary BIP34
|
||||
height/extranonce). Both were previously dropped entirely — discarded
|
||||
once no destination address could be derived from the script.
|
||||
- Help overlay: "User guide" button next to "Report a bug", linking to
|
||||
`USERGUIDE.md` on GitHub.
|
||||
- `USERGUIDE.md`: full end-user reference for GUI and CLI (wizard flows,
|
||||
script types, fees, gap limit, TOFU cert pinning, CLI commands) with the
|
||||
exact numbers the software enforces.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Send/Donate/Sync/Wizard view models wrote status/error strings directly
|
||||
in Italian regardless of the active language; routed through `Loc` with
|
||||
the missing keys added. `CertificatePinMismatchException` no longer
|
||||
bakes an Italian message into `.Message` — it exposes `Host`/`Port` for
|
||||
the UI to translate.
|
||||
- CLI (`src/Cli/Program.cs`) printed all output in Italian regardless of
|
||||
the code/docs-are-English-only policy; translated every user-facing
|
||||
string and comment.
|
||||
|
||||
### Testing
|
||||
|
||||
- Test suite expanded from 307 to 392 tests, closing coverage gaps in:
|
||||
checkpoint-anchoring (including the memoization and non-generic-retry
|
||||
branches), the PoW-checked branch of `BlockHeaderInfo.IsValidChild`
|
||||
(never run since every profile sets `SkipPowValidation`), all 8 BIP-39
|
||||
wordlist languages plus the empty-input guard, SLIP-132 rejection of
|
||||
malformed/corrupted keys, `TransactionFactory`'s standardness-policy
|
||||
rejection, `ImportedKeyAccount`'s fund-safety fallbacks,
|
||||
`WalletLoader`'s defensive branches for corrupted files,
|
||||
`PalladiumNetworks.For`/`INetworkSet`, `AppPaths`' full data-root
|
||||
precedence chain (via new internal override seams), `UpdateChecker`
|
||||
end-to-end via a stub-transport seam, and `ElectrumClient`'s
|
||||
multi-segment response dispatch.
|
||||
- OP_RETURN/coinbase-tag decoding covered: UTF-8 text, binary fallback to
|
||||
hex, multiple OP_RETURN outputs in one tx, pool-tag extraction, and the
|
||||
absence of false positives on standard outputs/inputs.
|
||||
|
||||
### Documentation
|
||||
|
||||
- `AGENTS.md`/`CLAUDE.md` re-synced (had drifted) and reformatted from
|
||||
dense prose to scannable bullet lists; a stale `SECURITY.md` file
|
||||
reference fixed.
|
||||
- `README.md` test-coverage section and `SECURITY.md` updated to describe
|
||||
the checkpoint anchoring and fuzzing guarantees actually enforced now.
|
||||
|
||||
## [0.9.1] — 2026-07-02
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -205,6 +205,7 @@ build_android() {
|
||||
-t:SignAndroidPackage \
|
||||
-p:AndroidSdkDirectory=\${ANDROID_HOME} \
|
||||
-p:ApplicationVersion=${VERSION_CODE} \
|
||||
-p:AndroidKeyStore=true \
|
||||
-p:AndroidSigningKeyStore=/keystore/release.keystore \
|
||||
-p:AndroidSigningKeyAlias=palladiumwallet \
|
||||
-p:AndroidSigningStorePass=\${ANDROID_KS_PASS} \
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ApplicationId>io.github.davide3011.palladiumwallet</ApplicationId>
|
||||
<!-- ApplicationVersion = versionCode (intero), ApplicationDisplayVersion = versionName -->
|
||||
<ApplicationVersion>2</ApplicationVersion>
|
||||
<ApplicationDisplayVersion>0.9.1</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>3</ApplicationVersion>
|
||||
<ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
|
||||
<AndroidPackageFormat>apk</AndroidPackageFormat>
|
||||
<!-- Includi le assembly .NET DENTRO l'apk: senza, in Debug si usa il Fast
|
||||
Deployment (assembly spinte via adb da `dotnet run`) e un apk installato
|
||||
|
||||
@@ -63,6 +63,7 @@ public sealed class Loc
|
||||
["help.tab.info"] = ["Info", "Info", "Info", "Info", "Info", "Info"],
|
||||
["help.tab.donate"] = ["Dona", "Donate", "Donar", "Faire un don", "Doar", "Spenden"],
|
||||
["help.bug.report"] = ["Segnala un bug", "Report a bug", "Informar un error", "Signaler un bug", "Reportar um bug", "Fehler melden"],
|
||||
["help.user.guide"] = ["Guida utente", "User guide", "Guía del usuario", "Guide utilisateur", "Guia do usuário", "Benutzerhandbuch"],
|
||||
["update.title"] = ["Aggiornamento disponibile", "Update available", "Actualización disponible", "Mise à jour disponible", "Atualização disponível", "Update verfügbar"],
|
||||
["update.message"] = ["È disponibile una nuova versione:", "A new version is available:", "Hay una nueva versión disponible:", "Une nouvelle version est disponible :", "Uma nova versão está disponível:", "Eine neue Version ist verfügbar:"],
|
||||
["update.download"] = ["Scarica", "Download", "Descargar", "Télécharger", "Baixar", "Herunterladen"],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<!-- Versione dell'applicazione: unico punto da modificare. Compare nel
|
||||
titolo della finestra ed è incisa nei binari pubblicati. -->
|
||||
<Version>0.9.1</Version>
|
||||
<Version>1.0.0</Version>
|
||||
<Nullable>enable</Nullable>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -64,13 +64,17 @@ public sealed class TransactionDetailsViewModel
|
||||
RbfText = loc[d.RbfSignaled ? "tx.yes" : "tx.no"];
|
||||
Inputs = new ObservableCollection<TxIoRow>(d.Inputs.Select((i, n) => new TxIoRow(
|
||||
i.IsCoinbase ? loc["tx.coinbase"] : $"{i.PrevTxid}:{i.PrevIndex}",
|
||||
i.IsCoinbase ? loc["tx.coinbase.newcoins"] : i.Address ?? "—",
|
||||
i.IsCoinbase
|
||||
? i.CoinbaseTag is { Length: > 0 } tag
|
||||
? $"{loc["tx.coinbase.newcoins"]} — {tag}"
|
||||
: loc["tx.coinbase.newcoins"]
|
||||
: i.Address ?? "—",
|
||||
i.AmountSats is { } a ? CoinAmount.FormatIn(a, unit) : "—",
|
||||
i.IsMine)));
|
||||
|
||||
Outputs = new ObservableCollection<TxIoRow>(d.Outputs.Select(o => new TxIoRow(
|
||||
$"#{o.Index}",
|
||||
o.Address ?? $"({o.ScriptType})",
|
||||
o.Address ?? (o.OpReturnText is { Length: > 0 } msg ? $"OP_RETURN: {msg}" : $"({o.ScriptType})"),
|
||||
CoinAmount.FormatIn(o.AmountSats, unit),
|
||||
o.IsMine)));
|
||||
}
|
||||
|
||||
@@ -1563,9 +1563,14 @@
|
||||
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Loc[help.info]}" TextWrapping="Wrap"/>
|
||||
<Button Content="{Binding Loc[help.bug.report]}"
|
||||
Click="OnOpenBugReportClick"
|
||||
HorizontalAlignment="Left"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="{Binding Loc[help.bug.report]}"
|
||||
Click="OnOpenBugReportClick"
|
||||
HorizontalAlignment="Left"/>
|
||||
<Button Content="{Binding Loc[help.user.guide]}"
|
||||
Click="OnOpenUserGuideClick"
|
||||
HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<Border BorderBrush="{DynamicResource BorderSubtleBrush}"
|
||||
BorderThickness="0,1,0,0" Margin="0,4,0,0"/>
|
||||
<StackPanel Spacing="4">
|
||||
|
||||
@@ -144,6 +144,14 @@ public partial class MainView : UserControl
|
||||
await launcher.LaunchUriAsync(new Uri(issueUrl));
|
||||
}
|
||||
|
||||
private async void OnOpenUserGuideClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
const string userGuideUrl = "https://github.com/davide3011/PalladiumWallet/blob/main/USERGUIDE.md";
|
||||
var launcher = TopLevel.GetTopLevel(this)?.Launcher;
|
||||
if (launcher is not null)
|
||||
await launcher.LaunchUriAsync(new Uri(userGuideUrl));
|
||||
}
|
||||
|
||||
private async void OnOpenReleasePageClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel vm || string.IsNullOrEmpty(vm.UpdateReleaseUrl)) return;
|
||||
|
||||
@@ -48,8 +48,9 @@ public static class ChainProfiles
|
||||
new ServerEndpoint("89.117.149.130", 50001, 50002),
|
||||
],
|
||||
// Real mainnet [height, hash, bits], pulled from a fully-synced palladiumd via
|
||||
// RPC (getblockhash/getblockheader), spaced every 20,000 blocks (~660h) plus one
|
||||
// recent block. Anchors WalletSynchronizer's header-chain verification (§7.3):
|
||||
// RPC (getblockhash/getblockheader) or verified against a trusted indexing server,
|
||||
// spaced every 20,000 blocks (~660h) plus recent ones added at each release to keep
|
||||
// the header-anchoring walk short. Anchors WalletSynchronizer's header-chain verification (§7.3):
|
||||
// bounds how far back a forged header chain must be walked to be caught, since
|
||||
// this LWMA chain cannot be PoW-validated locally (SkipPowValidation).
|
||||
Checkpoints =
|
||||
@@ -78,6 +79,7 @@ public static class ChainProfiles
|
||||
new Checkpoint(440000, "00000000000001b09d7da81403a9b383a734305a8783cb3a0dbe009edea26a95", 0x1a0216c4),
|
||||
new Checkpoint(460000, "00000000000000ecc7413f638bfe7be80a36bacab858ce9a814f194d9df526d5", 0x1a07dd8f),
|
||||
new Checkpoint(468800, "000000000000052c61652eed72b441d8c1f1926710a8d691d101be4961dba105", 0x1a1838ee),
|
||||
new Checkpoint(475124, "00000000000009e66da1e1a430fd1932aa75bf513053df088764545d941f13ca", 0x1a1a98cb),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -5,12 +5,18 @@ using PalladiumWallet.Core.Spv;
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>An input of a transaction, with the spent output resolved from the server.</summary>
|
||||
/// <param name="CoinbaseTag">
|
||||
/// Printable ASCII runs (e.g. pool tags like "/slush/") extracted from the coinbase
|
||||
/// scriptSig; null for non-coinbase inputs or if no printable text was found.
|
||||
/// </param>
|
||||
public sealed record TxInputInfo(
|
||||
string PrevTxid, uint PrevIndex, long? AmountSats, string? Address, bool IsMine, bool IsCoinbase);
|
||||
string PrevTxid, uint PrevIndex, long? AmountSats, string? Address, bool IsMine, bool IsCoinbase,
|
||||
string? CoinbaseTag = null);
|
||||
|
||||
/// <summary>An output of a transaction.</summary>
|
||||
/// <param name="OpReturnText">Decoded OP_RETURN payload (UTF-8, or hex if not valid text); null otherwise.</param>
|
||||
public sealed record TxOutputInfo(
|
||||
uint Index, long AmountSats, string? Address, string ScriptType, bool IsMine);
|
||||
uint Index, long AmountSats, string? Address, string ScriptType, string? OpReturnText, bool IsMine);
|
||||
|
||||
/// <summary>
|
||||
/// Complete transaction data assembled by querying the server: the raw transaction
|
||||
@@ -105,8 +111,9 @@ public static class TransactionInspector
|
||||
{
|
||||
var o = tx.Outputs[i];
|
||||
var addr = AddrOf(o.ScriptPubKey);
|
||||
var opReturn = addr is null ? OpReturnTextOf(o.ScriptPubKey) : null;
|
||||
outputs.Add(new TxOutputInfo(
|
||||
(uint)i, o.Value.Satoshi, addr, ScriptType(o.ScriptPubKey),
|
||||
(uint)i, o.Value.Satoshi, addr, ScriptType(o.ScriptPubKey), opReturn,
|
||||
addr is not null && ownedAddresses.Contains(addr)));
|
||||
}
|
||||
|
||||
@@ -134,7 +141,7 @@ public static class TransactionInspector
|
||||
{
|
||||
if (tx.IsCoinBase)
|
||||
{
|
||||
inputs.Add(new TxInputInfo("", inp.PrevOut.N, null, null, false, true));
|
||||
inputs.Add(new TxInputInfo("", inp.PrevOut.N, null, null, false, true, CoinbaseTagOf(inp.ScriptSig.ToBytes())));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -189,4 +196,48 @@ public static class TransactionInspector
|
||||
}
|
||||
catch { return "—"; }
|
||||
}
|
||||
|
||||
/// <summary>Decodes an OP_RETURN output's pushed data as UTF-8 text, falling back to hex if it isn't valid text.</summary>
|
||||
private static string? OpReturnTextOf(Script script)
|
||||
{
|
||||
if (!script.IsUnspendable) return null;
|
||||
try
|
||||
{
|
||||
var data = script.ToOps().Skip(1)
|
||||
.Where(op => op.PushData is { Length: > 0 })
|
||||
.SelectMany(op => op.PushData)
|
||||
.ToArray();
|
||||
return data.Length == 0 ? null : DecodeUtf8OrHex(data);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static string DecodeUtf8OrHex(byte[] data)
|
||||
{
|
||||
var text = System.Text.Encoding.UTF8.GetString(data);
|
||||
var looksLikeText = !text.Contains('�') && text.All(c => !char.IsControl(c) || c is '\n' or '\r' or '\t');
|
||||
return looksLikeText ? text : "0x" + Convert.ToHexString(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts printable-ASCII runs (≥4 chars) from a coinbase scriptSig, e.g. pool
|
||||
/// tags like "/slush/" embedded among the binary BIP34 height and extranonce.
|
||||
/// </summary>
|
||||
private static string? CoinbaseTagOf(byte[] scriptSig)
|
||||
{
|
||||
var runs = new List<string>();
|
||||
var run = new System.Text.StringBuilder();
|
||||
void Flush()
|
||||
{
|
||||
if (run.Length >= 4) runs.Add(run.ToString());
|
||||
run.Clear();
|
||||
}
|
||||
foreach (var b in scriptSig)
|
||||
{
|
||||
if (b is >= 0x20 and <= 0x7E) run.Append((char)b);
|
||||
else Flush();
|
||||
}
|
||||
Flush();
|
||||
return runs.Count == 0 ? null : string.Join(" ", runs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,139 @@ public class TransactionInspectorTests
|
||||
Assert.Equal(0, server.CallCount("blockchain.block.header"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Un_output_OP_RETURN_con_testo_UTF8_viene_decodificato()
|
||||
{
|
||||
var account = Account();
|
||||
var tx = Net.CreateTransaction();
|
||||
tx.Inputs.Add(new TxIn(new OutPoint(uint256.One, 0)));
|
||||
tx.Outputs.Add(Money.Satoshis(1_000_000), account.GetReceiveAddress(0));
|
||||
tx.Outputs.Add(Money.Zero, TxNullDataTemplate.Instance.GenerateScriptPubKey("hello palladium"u8.ToArray()));
|
||||
|
||||
var (server, client) = await StartAsync(tx);
|
||||
await using var _ = server; await using var __ = client;
|
||||
|
||||
var details = await TransactionInspector.FetchAsync(
|
||||
client, Net, tx.GetHash().ToString(), tipHeight: 200, height: 101,
|
||||
Owned(account), netSats: 1_000_000, verified: true);
|
||||
|
||||
var opReturn = Assert.Single(details.Outputs, o => o.Index == 1);
|
||||
Assert.Null(opReturn.Address);
|
||||
Assert.Equal("hello palladium", opReturn.OpReturnText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Un_output_OP_RETURN_con_dati_binari_ricade_su_hex()
|
||||
{
|
||||
var account = Account();
|
||||
byte[] data = [0x00, 0x01, 0xFF, 0xFE, 0x02];
|
||||
var tx = Net.CreateTransaction();
|
||||
tx.Inputs.Add(new TxIn(new OutPoint(uint256.One, 0)));
|
||||
tx.Outputs.Add(Money.Satoshis(1_000_000), account.GetReceiveAddress(0));
|
||||
tx.Outputs.Add(Money.Zero, TxNullDataTemplate.Instance.GenerateScriptPubKey(data));
|
||||
|
||||
var (server, client) = await StartAsync(tx);
|
||||
await using var _ = server; await using var __ = client;
|
||||
|
||||
var details = await TransactionInspector.FetchAsync(
|
||||
client, Net, tx.GetHash().ToString(), tipHeight: 200, height: 101,
|
||||
Owned(account), netSats: 1_000_000, verified: true);
|
||||
|
||||
var opReturn = Assert.Single(details.Outputs, o => o.Index == 1);
|
||||
Assert.Equal("0x" + Convert.ToHexString(data), opReturn.OpReturnText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Piu_output_OP_RETURN_nella_stessa_tx_sono_decodificati_singolarmente()
|
||||
{
|
||||
var account = Account();
|
||||
var tx = Net.CreateTransaction();
|
||||
tx.Inputs.Add(new TxIn(new OutPoint(uint256.One, 0)));
|
||||
tx.Outputs.Add(Money.Satoshis(1_000_000), account.GetReceiveAddress(0));
|
||||
tx.Outputs.Add(Money.Zero, TxNullDataTemplate.Instance.GenerateScriptPubKey("first"u8.ToArray()));
|
||||
tx.Outputs.Add(Money.Zero, TxNullDataTemplate.Instance.GenerateScriptPubKey("second"u8.ToArray()));
|
||||
|
||||
var (server, client) = await StartAsync(tx);
|
||||
await using var _ = server; await using var __ = client;
|
||||
|
||||
var details = await TransactionInspector.FetchAsync(
|
||||
client, Net, tx.GetHash().ToString(), tipHeight: 200, height: 101,
|
||||
Owned(account), netSats: 1_000_000, verified: true);
|
||||
|
||||
Assert.Equal("first", details.Outputs.Single(o => o.Index == 1).OpReturnText);
|
||||
Assert.Equal("second", details.Outputs.Single(o => o.Index == 2).OpReturnText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Un_output_normale_non_ha_testo_OP_RETURN()
|
||||
{
|
||||
var (funding, spend, _, account) = SpendPair();
|
||||
var (server, client) = await StartAsync(funding, spend);
|
||||
await using var _ = server; await using var __ = client;
|
||||
|
||||
var details = await TransactionInspector.FetchAsync(
|
||||
client, Net, spend.GetHash().ToString(), tipHeight: 200, height: 101,
|
||||
Owned(account), netSats: -610_000, verified: true);
|
||||
|
||||
Assert.All(details.Outputs, o => Assert.Null(o.OpReturnText));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Il_tag_pool_nello_scriptSig_della_coinbase_viene_estratto()
|
||||
{
|
||||
var account = Account();
|
||||
var coinbase = Net.CreateTransaction();
|
||||
// BIP34 height push (binary) followed by a pool tag (printable ASCII).
|
||||
var scriptSig = new Script(Op.GetPushOp(190), Op.GetPushOp("/slush/"u8.ToArray()));
|
||||
coinbase.Inputs.Add(new TxIn(new OutPoint(uint256.Zero, 0xffffffff), scriptSig));
|
||||
coinbase.Outputs.Add(Money.Satoshis(5_000_000_000), account.GetReceiveAddress(0));
|
||||
|
||||
var (server, client) = await StartAsync(coinbase);
|
||||
await using var _ = server; await using var __ = client;
|
||||
|
||||
var details = await TransactionInspector.FetchAsync(
|
||||
client, Net, coinbase.GetHash().ToString(), tipHeight: 200, height: 190,
|
||||
Owned(account), netSats: 5_000_000_000, verified: true);
|
||||
|
||||
var input = Assert.Single(details.Inputs);
|
||||
Assert.True(input.IsCoinbase);
|
||||
Assert.Contains("/slush/", input.CoinbaseTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Uno_scriptSig_coinbase_senza_testo_stampabile_non_produce_tag()
|
||||
{
|
||||
var account = Account();
|
||||
var coinbase = Net.CreateTransaction();
|
||||
var scriptSig = new Script(Op.GetPushOp([0x00, 0x01, 0x02]));
|
||||
coinbase.Inputs.Add(new TxIn(new OutPoint(uint256.Zero, 0xffffffff), scriptSig));
|
||||
coinbase.Outputs.Add(Money.Satoshis(5_000_000_000), account.GetReceiveAddress(0));
|
||||
|
||||
var (server, client) = await StartAsync(coinbase);
|
||||
await using var _ = server; await using var __ = client;
|
||||
|
||||
var details = await TransactionInspector.FetchAsync(
|
||||
client, Net, coinbase.GetHash().ToString(), tipHeight: 200, height: 190,
|
||||
Owned(account), netSats: 5_000_000_000, verified: true);
|
||||
|
||||
var input = Assert.Single(details.Inputs);
|
||||
Assert.Null(input.CoinbaseTag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Un_input_normale_non_ha_tag_coinbase()
|
||||
{
|
||||
var (funding, spend, _, account) = SpendPair();
|
||||
var (server, client) = await StartAsync(funding, spend);
|
||||
await using var _ = server; await using var __ = client;
|
||||
|
||||
var details = await TransactionInspector.FetchAsync(
|
||||
client, Net, spend.GetHash().ToString(), tipHeight: 200, height: 101,
|
||||
Owned(account), netSats: -610_000, verified: true);
|
||||
|
||||
Assert.All(details.Inputs, i => Assert.Null(i.CoinbaseTag));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task La_cache_delle_tx_evita_le_richieste_al_server()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user