feat(app): flag transactions above the newest checkpoint as not anchored
Blocks mined after the newest hardcoded checkpoint cannot be chained to it, so only the Merkle proof against the server's header backs them. The history list and the transaction detail now show a 'not anchored' label for those transactions. Informational only: spending is not blocked. See ADR 0041.
This commit is contained in:
1 parent
24368a07b5
commit
305f22ed0e
13 files changed
+119
-7
No files matched your search
@@ -396,6 +396,40 @@ public sealed class MainWindowViewModelTests : VmTestBase
|
||||
Assert.True(_server.CallCount("blockchain.transaction.get_merkle") >= 1); // Merkle proof was demanded
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task HistoryRows_AboveTheNewestCheckpoint_AreFlaggedAsUnanchored() => UiSession.Run(async () =>
|
||||
{
|
||||
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
WireFundedChain(account.GetReceiveAddress(0), 2_500_000); // confirmed at height 100
|
||||
// The fake serves the same header for every height: use its hash as a checkpoint at 50.
|
||||
string headerHash;
|
||||
await using (var c = await ElectrumClient.ConnectAsync(_server.Host, _server.Port, useSsl: false))
|
||||
headerHash = BlockHeaderInfo.Parse(await c.GetBlockHeaderAsync(50)).Hash.ToString();
|
||||
MainWindowViewModel.ProfileOverride = ChainProfiles.Mainnet with
|
||||
{
|
||||
BootstrapServers = [],
|
||||
Checkpoints = [new Checkpoint(50, headerHash, 0x1d00ffff)],
|
||||
};
|
||||
var vm = new MainWindowViewModel();
|
||||
RestoreFlow(vm, Mnemonic, null, "unanch");
|
||||
await Eventually(() => vm.IsConnected && !vm.IsSyncing && vm.History.Count == 1);
|
||||
|
||||
Assert.True(vm.History[0].Verified); // the Merkle proof checks out
|
||||
Assert.True(vm.History[0].Unanchored); // but height 100 is above the checkpoint at 50
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task HistoryRows_CoveredByACheckpoint_AreNotFlagged() => UiSession.Run(async () =>
|
||||
{
|
||||
var account = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet);
|
||||
WireFundedChain(account.GetReceiveAddress(0), 2_500_000);
|
||||
var vm = new MainWindowViewModel(); // default test profile: no checkpoints
|
||||
RestoreFlow(vm, Mnemonic, null, "anch");
|
||||
await Eventually(() => vm.IsConnected && !vm.IsSyncing && vm.History.Count == 1);
|
||||
|
||||
Assert.False(vm.History[0].Unanchored);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task FeePresets_ComeFromTheServer_AndTheNormalEstimateIsPreselected() => UiSession.Run(async () =>
|
||||
{
|
||||
|
||||
@@ -116,4 +116,23 @@ public class ChainProfileTests
|
||||
data[3] = (byte)header;
|
||||
return Encoders.Base58Check.EncodeData(data);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, false)] // unconfirmed
|
||||
[InlineData(1, false)]
|
||||
[InlineData(100, false)] // at the newest checkpoint: still anchored
|
||||
[InlineData(101, true)]
|
||||
[InlineData(5_000_000, true)]
|
||||
public void IsAboveNewestCheckpoint_flags_only_heights_the_anchoring_cannot_cover(int height, bool expected)
|
||||
{
|
||||
var profile = ChainProfiles.Mainnet with
|
||||
{
|
||||
Checkpoints = [new Checkpoint(50, "00", 0), new Checkpoint(100, "00", 0)],
|
||||
};
|
||||
Assert.Equal(expected, profile.IsAboveNewestCheckpoint(height));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_profile_without_checkpoints_never_reports_an_unanchored_height() =>
|
||||
Assert.False((ChainProfiles.Mainnet with { Checkpoints = [] }).IsAboveNewestCheckpoint(1_000_000));
|
||||
}
|
||||
Reference in new issue
Block a user