Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 147 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ More usage examples:
- [ ] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
- [ ] NIP-11: Relay Information Document
- [ ] NIP-12: Generic Tag Queries
- [ ] NIP-13: Proof of Work
- [ ] NIP-14: Subject tag in text events
- [x] NIP-13: Proof of Work
- [x] NIP-14: Subject tag in text events
- [x] NIP-15: End of Stored Events Notice
- [x] NIP-19: bech32-encoded entities
- [x] NIP-20: Command Results
Expand All @@ -133,6 +133,151 @@ More usage examples:

**Pull Requests are welcome!**

#### Proof of Work (NIP-13)

Proof of Work in Nostr allows clients to demonstrate computational effort spent on creating an event. This can be used for spam reduction by requiring events to have a minimum difficulty level.

##### Basic Usage

```csharp
// Create an event
var ev = new NostrEvent
{
Kind = NostrKind.ShortTextNote,
CreatedAt = DateTime.UtcNow,
Content = "This message includes proof of work to demonstrate NIP-13"
};

// Mine the event with difficulty 16 (leading zero bits)
var minedEvent = await ev.GeneratePow(16);

// Once mined, sign it with your private key
var key = NostrPrivateKey.FromBech32("nsec1xxx");
var signedMinedEvent = minedEvent.Sign(key);

// Check the actual difficulty achieved
int difficulty = signedMinedEvent.GetDifficulty();
Console.WriteLine($"Event ID: {signedMinedEvent.Id}");
Console.WriteLine($"Difficulty achieved: {difficulty} bits");

// Send to relay
client.Send(new NostrEventRequest(signedMinedEvent));
```

##### Validating Proof of Work

When receiving an event, you can verify if it meets a minimum difficulty requirement:

```csharp
// Check if an event has a valid PoW with a difficulty of at least 15 bits
int minDifficulty = 15;
bool isValid = receivedEvent.HasValidPow(minDifficulty);
if (isValid)
{
Console.WriteLine($"Event has valid PoW with difficulty: {receivedEvent.GetDifficulty()} bits");
}
else
{
Console.WriteLine("Event does not have sufficient proof of work");
}
```

##### Advanced: Using Delegate-based Mining for Progress Updates

For longer mining operations, you may want to receive progress updates and have more control over the mining process. The library provides a delegate-based approach for this:

```csharp
// Define delegate handlers for progress and completion
void OnProgress(string currentNonce, int difficulty, long attemptsCount)
{
Console.WriteLine($"Mining in progress - Current nonce: {currentNonce}, " +
$"Current difficulty: {difficulty}, Attempts: {attemptsCount}");
}

void OnComplete(bool success, string nonce, int difficulty, long totalAttempts, long elapsedMs)
{
if (success)
{
Console.WriteLine($"Mining successful! Found nonce: {nonce}");
Console.WriteLine($"Difficulty achieved: {difficulty} bits");
Console.WriteLine($"Total attempts: {totalAttempts:N0} in {elapsedMs}ms");
Console.WriteLine($"Hash rate: {totalAttempts * 1000 / elapsedMs} hashes/second");
}
else
{
Console.WriteLine($"Mining was cancelled or failed after {totalAttempts:N0} attempts");
}
}

// Create and configure a PoW calculator
var calculator = new PowCalculator();
calculator.OnProgress += OnProgress; // Subscribe to progress updates
calculator.OnCompletion += OnComplete; // Subscribe to completion notification

try
{
// Start PoW calculation with target difficulty and specified nonce size (in bytes)
string eventId = myEvent.ComputeId(); // First compute the event ID
await calculator.StartCalculation(eventId, 20, 4); // Target 20 bits, 4-byte nonce

// The mining happens on a background thread, your UI remains responsive

// If you need to cancel the calculation:
// calculator.CancelCalculation();
}
catch (Exception ex)
{
Console.WriteLine($"Error in PoW calculation: {ex.Message}");
}
finally
{
// Important: Unsubscribe when done to prevent memory leaks
calculator.OnProgress -= OnProgress;
calculator.OnCompletion -= OnComplete;
}
```

##### Manual Mining with Low-level API

For more control over the mining process, you can use the lower-level `EventMiner` class directly:

```csharp
// Create a new event
var originalEvent = new NostrEvent
{
Kind = NostrKind.ShortTextNote,
CreatedAt = DateTime.UtcNow,
Content = "Mining with low-level API"
};

// Set up cancellation (optional, for timeout)
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));

try
{
// Mine the event directly using the EventMiner
var minedEvent = await EventMiner.MineEventAsync(originalEvent, 16, cts.Token);

Console.WriteLine($"Mining successful! Event ID: {minedEvent.Id}");
Console.WriteLine($"Difficulty: {minedEvent.GetDifficulty()} bits");

// Find the nonce used
var nonceTag = minedEvent.Tags?.FindFirstTag("nonce");
if (nonceTag != null)
{
Console.WriteLine($"Nonce: {nonceTag.AdditionalData[0]}");
Console.WriteLine($"Target difficulty: {nonceTag.AdditionalData[1]}");
}

// Send the event to a relay
client.Send(new NostrEventRequest(minedEvent));
}
catch (OperationCanceledException)
{
Console.WriteLine("Mining was cancelled or timed out");
}
```

### Reconnecting

A built-in reconnection invokes after 1 minute (default) of not receiving any messages from the server.
Expand Down
8 changes: 8 additions & 0 deletions apps/nostr-debug/NostrDebug.Web/Components/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
</span>
</FluentAnchor>
</li>
<li>
<FluentAnchor Href="delegate-pow" Appearance=@SetAppearance("delegate-pow")>
<FluentIcon Slot="start" Name="@FluentIcons.CodeText" Size="@IconSize.Size20" Color="Color.Accent" />
<span class="nav-item-text">
Delegate PoW
</span>
</FluentAnchor>
</li>
</ul>

</div>
Expand Down
178 changes: 178 additions & 0 deletions apps/nostr-debug/NostrDebug.Web/Pages/Delegate.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
@page "/delegate-pow"
@using System.Diagnostics
@using NostrDebug.Web.Pow
@implements IDisposable

<PageHeader Title="Delegated Proof of Work" Subtitle="Using delegates for PoW calculation"></PageHeader>

<div class="conversion-group">
<p>
This page demonstrates the use of delegates for reporting progress and completion of Proof of Work calculations.
The <code>PowCalculator</code> class uses delegates to report progress and completion of calculations.
</p>

<FluentTextField Style="width: 100%" Placeholder="hex" @bind-Value="@_eventId" class="m-b-1">
<FluentIcon Name="@FluentIcons.Document" Slot="start" Size="@IconSize.Size16" Color="Color.Neutral" />
<strong>Event ID</strong>
</FluentTextField>

<FluentNumberField Style="width: 100%" Min="0" Max="256" Placeholder="Target difficulty" @bind-Value="@_targetDifficulty" class="m-b-1">
<FluentIcon Name="@FluentIcons.NumberSymbol" Slot="start" Size="@IconSize.Size16" Color="Color.Neutral" />
<strong>Target Difficulty</strong>
</FluentNumberField>

<FluentNumberField Min="1" Max="32" @bind-Value="@_nonceSize" class="m-b-1">
<strong>Nonce Size</strong>
</FluentNumberField>

<Stack Orientation="Orientation.Horizontal" HorizontalGap="10" class="m-b-1">
<FluentButton Appearance="Appearance.Accent" @onclick="StartCalculation" Disabled="@_isCalculating">
<FluentIcon Name="@FluentIcons.CalculatorMultiple" Slot="start" Size="@IconSize.Size16" Color="Color.Fill" />
Start PoW Calculation
</FluentButton>

<FluentButton Appearance="Appearance.Neutral" @onclick="CancelCalculation" Disabled="@(!_isCalculating)">
<FluentIcon Name="@FluentIcons.Stop" Slot="start" Size="@IconSize.Size16" Color="Color.Error" />
Cancel
</FluentButton>
</Stack>

@if (_isCalculating)
{
<div class="pow-progress m-b-1">
<Stack Orientation="Orientation.Horizontal" VerticalAlignment="StackVerticalAlignment.Center">
<FluentProgressRing />
<span>Calculating PoW... Current nonce: @_currentNonce, Difficulty: @_currentDifficulty, Attempts: @_attempts</span>
</Stack>
</div>
}

@if (!string.IsNullOrEmpty(_resultNonce))
{
<div class="pow-valid m-t-1 m-b-1">
<Stack Orientation="Orientation.Vertical" VerticalGap="5">
<Stack Orientation="Orientation.Horizontal" HorizontalGap="10" class="m-b-1">
<FluentIcon Name="@FluentIcons.CheckmarkCircle" Slot="start" Size="@IconSize.Size20" Color="Color.Success"/>
<strong>PoW Successfully Generated</strong>
</Stack>
<div><strong>Nonce:</strong> @_resultNonce</div>
<div><strong>Difficulty:</strong> @_resultDifficulty</div>
<div><strong>Total Attempts:</strong> @_totalAttempts.ToString("N0")</div>
<div><strong>Time Taken:</strong> @_elapsedTime ms (@(_elapsedTime / 1000.0) seconds)</div>
<div><strong>Speed:</strong> @((int)(_totalAttempts / (_elapsedTime / 1000.0))) hashes/second</div>
</Stack>
</div>
}

@if (_calculationFailed)
{
<div class="pow-invalid m-t-1 m-b-1">
<Stack Orientation="Orientation.Horizontal" HorizontalGap="10" class="m-b-1">
<FluentIcon Name="@FluentIcons.ErrorCircle" Slot="start" Size="@IconSize.Size20" Color="Color.Error"/>
<strong>PoW Calculation Failed or Cancelled</strong>
</Stack>
</div>
}
</div>

@code {
private string _eventId = "";
private int _targetDifficulty = 16;
private int _nonceSize = 4;

private bool _isCalculating;
private string _currentNonce = "";
private int _currentDifficulty;
private long _attempts;

private string _resultNonce = "";
private int _resultDifficulty;
private long _totalAttempts;
private long _elapsedTime;
private bool _calculationFailed;

// Create an instance of the PowCalculator
private PowCalculator _calculator = new PowCalculator();

protected override void OnInitialized()
{
// Subscribe to the events using delegate methods
_calculator.OnProgress += ProgressHandler;
_calculator.OnCompletion += CompletionHandler;
}

public void Dispose()
{
// Important: Unsubscribe from events when component is disposed
_calculator.OnProgress -= ProgressHandler;
_calculator.OnCompletion -= CompletionHandler;
}

private async Task StartCalculation()
{
if (string.IsNullOrWhiteSpace(_eventId))
{
return;
}

_isCalculating = true;
_resultNonce = "";
_calculationFailed = false;
_currentNonce = "";
_currentDifficulty = 0;
_attempts = 0;

StateHasChanged();

try
{
await _calculator.StartCalculation(_eventId, _targetDifficulty, _nonceSize);
}
catch (Exception ex)
{
Console.WriteLine($"Error starting calculation: {ex.Message}");
_isCalculating = false;
_calculationFailed = true;
StateHasChanged();
}
}

private void CancelCalculation()
{
_calculator.CancelCalculation();
}

// Handler for progress updates - this is called via the delegate
private void ProgressHandler(string currentNonce, int difficulty, long attemptsCount)
{
_currentNonce = currentNonce;
_currentDifficulty = difficulty;
_attempts = attemptsCount;

// Ensure UI updates
InvokeAsync(StateHasChanged);
}

// Handler for completion notification - this is called via the delegate
private void CompletionHandler(bool success, string nonce, int difficulty, long totalAttempts, long elapsedMs)
{
_isCalculating = false;

if (success)
{
_resultNonce = nonce;
_resultDifficulty = difficulty;
_totalAttempts = totalAttempts;
_elapsedTime = elapsedMs;
}
else
{
_calculationFailed = true;
_totalAttempts = totalAttempts;
_elapsedTime = elapsedMs;
}

// Ensure UI updates
InvokeAsync(StateHasChanged);
}
}
Loading
Loading