Skip to content
Draft
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
55 changes: 35 additions & 20 deletions tools/SendBlobs/BlobSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public async Task SendRandomBlobs(
IReleaseSpec spec)
{
List<(Signer, ulong)> signers = [];
IBlobProofsManager proofs = IBlobProofsManager.For(spec.BlobProofVersion);

if (waitForInclusion)
{
Expand Down Expand Up @@ -96,12 +97,12 @@ public async Task SendRandomBlobs(

int signerIndex = -1;

ulong excessBlobs = (ulong)blobTxCounts.Sum(btxc => btxc.blobCount) / 2;
ulong excessBlobs = EstimateTotalBlobs(blobTxCounts, spec) / 2;

foreach ((int txCount, int blobCount, string @break) txs in blobTxCounts)
{
int txCount = txs.txCount;
int blobCount = txs.blobCount;
int blobCount = ResolveBlobCount(txs.blobCount, txs.@break, spec);
string @break = txs.@break;

while (txCount > 0)
Expand All @@ -115,14 +116,6 @@ public async Task SendRandomBlobs(
Signer signer = signers[signerIndex].Item1;
ulong nonce = signers[signerIndex].Item2;

switch (@break)
{
case "1": blobCount = 0; break;
case "2": blobCount = (int)spec.MaxBlobCount + 1; break;
case "14": blobCount = 100; break;
case "15": blobCount = 1000; break;
}

byte[][] blobs = new byte[blobCount][];

for (int blobIndex = 0; blobIndex < blobCount; blobIndex++)
Expand All @@ -141,8 +134,6 @@ public async Task SendRandomBlobs(
}
}

IBlobProofsManager proofs = IBlobProofsManager.For(spec.BlobProofVersion);

ShardBlobNetworkWrapper blobsContainer = proofs.AllocateWrapper(blobs);
proofs.ComputeProofsAndCommitments(blobsContainer);

Expand Down Expand Up @@ -185,8 +176,8 @@ public async Task SendRandomBlobs(
if (result is not null)
signers[signerIndex] = new(signer, nonce + 1);

if (waitForInclusion)
await WaitForBlobInclusion(_rpcClient, result, blockResult.Number);
if (waitForInclusion && result is Hash256 includedHash)
await WaitForBlobInclusion(_rpcClient, includedHash, blockResult.Number);
}
}
}
Expand Down Expand Up @@ -266,8 +257,8 @@ public async Task SendData(

Hash256? hash = await SendTransaction(chainId, nonce, maxGasPrice, maxPriorityFeePerGas, maxFeePerBlobGas, receiver, blobHashes, blobsContainer, signer);

if (waitForInclusion)
await WaitForBlobInclusion(_rpcClient, hash, blockResult.Number);
if (waitForInclusion && hash is Hash256 includedHash)
await WaitForBlobInclusion(_rpcClient, includedHash, blockResult.Number);
}

private async Task<(UInt256 maxGasPrice, UInt256 maxPriorityFeePerGas, UInt256 maxFeePerBlobGas)> GetGasPrices
Expand Down Expand Up @@ -346,7 +337,7 @@ public async Task SendData(
return result is not null ? tx.CalculateHash() : null;
}

private async static Task WaitForBlobInclusion(IJsonRpcClient rpcClient, Hash256? txHash, UInt256 lastBlockNumber)
private async static Task WaitForBlobInclusion(IJsonRpcClient rpcClient, Hash256 txHash, UInt256 lastBlockNumber)
{
Console.WriteLine("Waiting for blob transaction to be included in a block");
int waitInMs = 2000;
Expand All @@ -360,10 +351,8 @@ private async static Task WaitForBlobInclusion(IJsonRpcClient rpcClient, Hash256
{
lastBlockNumber = blockResult.Number + 1;

if (txHash is not null && blockResult.Transactions.Contains(txHash))
if (blockResult.Transactions.Contains(txHash))
{
string? receipt = await rpcClient.Post<string>("eth_getTransactionByHash", txHash.ToString(), true);

Console.WriteLine($"Found blob transaction in block {blockResult.Number}");
return;
}
Expand All @@ -377,4 +366,30 @@ private async static Task WaitForBlobInclusion(IJsonRpcClient rpcClient, Hash256
if (retryCount == 0) break;
}
}

private static ulong EstimateTotalBlobs((int count, int blobCount, string @break)[] blobTxCounts, IReleaseSpec spec)
{
ulong total = 0;
foreach ((int count, int blobCount, string @break) option in blobTxCounts)
{
int resolvedBlobCount = ResolveBlobCount(option.blobCount, option.@break, spec);
if (resolvedBlobCount <= 0 || option.count <= 0)
{
continue;
}

total += (ulong)option.count * (ulong)resolvedBlobCount;
}

return total;
}

private static int ResolveBlobCount(int requestedCount, string breakCode, IReleaseSpec spec) => breakCode switch
{
"1" => 0,
"2" => (int)spec.MaxBlobCount + 1,
"14" => 100,
"15" => 1000,
_ => requestedCount
};
}
4 changes: 2 additions & 2 deletions tools/SendBlobs/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited
# SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
# SPDX-License-Identifier: LGPL-3.0-only

FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
Expand All @@ -21,7 +21,7 @@ RUN arch=$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH") && \
-p:DebugType=None \
-p:DebugSymbols=false

FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-noble
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble

WORKDIR /nethermind

Expand Down
114 changes: 56 additions & 58 deletions tools/SendBlobs/FundsDistributor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Consensus;
Expand Down Expand Up @@ -30,13 +30,7 @@ public FundsDistributor(IJsonRpcClient rpcClient, ulong chainId, string? keyFile
/// <summary>
/// Distribute the available funds from an address to a number of newly generated private keys.
/// </summary>
/// <param name="distributeFrom"></param>
/// <param name="keysToMake"></param>
/// <param name="maxFee"></param>
/// <param name="maxPriorityFee"></param>
/// <returns><see cref="IEnumerable{string}"/> containing all the executed tx hashes.</returns>
/// <exception cref="AccountException"></exception>
public async Task<IEnumerable<string>> DitributeFunds(Signer distributeFrom, uint keysToMake, UInt256 maxFee, UInt256 maxPriorityFee)
public async Task<IEnumerable<string>> DistributeFunds(Signer distributeFrom, uint keysToMake, UInt256 maxFee, UInt256 maxPriorityFee)
{
if (keysToMake == 0)
throw new ArgumentException("keysToMake must be greater than zero.", nameof(keysToMake));
Expand All @@ -48,25 +42,16 @@ public async Task<IEnumerable<string>> DitributeFunds(Signer distributeFrom, uin
if (nonceString is null)
throw new AccountException($"Unable to get nonce for {distributeFrom.Address}");

string? gasPriceRes = await _rpcClient.Post<string>("eth_gasPrice") ?? "1";
UInt256 gasPrice = HexConvert.ToUInt256(gasPriceRes);

string? maxPriorityFeePerGasRes;
UInt256 maxPriorityFeePerGas = maxPriorityFee;
if (maxPriorityFee == 0)
{
maxPriorityFeePerGasRes = await _rpcClient.Post<string>("eth_maxPriorityFeePerGas") ?? "1";
maxPriorityFeePerGas = HexConvert.ToUInt256(maxPriorityFeePerGasRes);
}

UInt256 balance = new UInt256(Bytes.FromHexString(balanceString));

if (balance == 0)
throw new AccountException($"Balance on provided signer {distributeFrom.Address} is 0.");

ulong nonce = HexConvert.ToUInt64(nonceString);

UInt256 approxGasFee = (gasPrice + maxPriorityFeePerGas) * GasCostOf.Transaction;
(UInt256 feePerGasEstimate, UInt256 priorityFeeEstimate) = await ResolveFeeSettingsAsync(maxFee, maxPriorityFee);

UInt256 approxGasFee = feePerGasEstimate * GasCostOf.Transaction;

//Leave 10% of the balance as buffer in case of gas spikes
UInt256 balanceMinusBuffer = (balance * 900) / 1000;
Expand All @@ -80,9 +65,7 @@ public async Task<IEnumerable<string>> DitributeFunds(Signer distributeFrom, uin
UInt256 perKeyToSend = balanceToDistribute / keysToMake;

using PrivateKeyGenerator generator = new();
IEnumerable<PrivateKey> privateKeys = Enumerable.Range(1, (int)keysToMake).Select(i => generator.Generate());

List<string> txHash = new List<string>();
List<string> txHashes = new();

TxDecoder txDecoder = TxDecoder.Instance;
StreamWriter? keyWriter = null;
Expand All @@ -94,25 +77,25 @@ public async Task<IEnumerable<string>> DitributeFunds(Signer distributeFrom, uin
keyWriter = File.AppendText(_keyFilePath);
}

bool refreshFeesPerTransaction = maxFee == 0 || maxPriorityFee == 0;

using (keyWriter)
{
foreach (PrivateKey key in privateKeys)
for (uint i = 0; i < keysToMake; i++)
{
if (maxFee == 0)
{
gasPriceRes = await _rpcClient.Post<string>("eth_gasPrice") ?? "1";
gasPrice = HexConvert.ToUInt256(gasPriceRes);

maxPriorityFeePerGasRes = await _rpcClient.Post<string>("eth_maxPriorityFeePerGas") ?? "1";
maxPriorityFeePerGas = HexConvert.ToUInt256(maxPriorityFeePerGasRes);
}

Transaction tx = CreateTx(_chainId,
key.Address,
maxFee != 0 ? maxFee : gasPrice + maxPriorityFeePerGas,
nonce,
maxPriorityFeePerGas,
perKeyToSend);
PrivateKey key = generator.Generate();

(UInt256 txMaxFeePerGas, UInt256 txPriorityFeePerGas) = refreshFeesPerTransaction
? await ResolveFeeSettingsAsync(maxFee, maxPriorityFee)
: (feePerGasEstimate, priorityFeeEstimate);

Transaction tx = CreateTx(
_chainId,
key.Address,
txMaxFeePerGas,
nonce,
txPriorityFeePerGas,
perKeyToSend);

await distributeFrom.Sign(tx);

Expand All @@ -121,7 +104,7 @@ public async Task<IEnumerable<string>> DitributeFunds(Signer distributeFrom, uin

string? result = await _rpcClient.Post<string>("eth_sendRawTransaction", $"0x{txRlp}");
if (result is not null)
txHash.Add(result);
txHashes.Add(result);

if (keyWriter is not null)
keyWriter.WriteLine(key.ToString());
Expand All @@ -130,7 +113,7 @@ public async Task<IEnumerable<string>> DitributeFunds(Signer distributeFrom, uin
}
}

return txHash;
return txHashes;
}

/// <summary>
Expand All @@ -144,7 +127,10 @@ public async Task<IEnumerable<string>> ReclaimFunds(Address beneficiary, UInt256
{
IEnumerable<Signer> privateSigners = _keyFilePath is null
? []
: File.ReadAllLines(_keyFilePath).Select(k => new Signer(_chainId, new PrivateKey(k), _logManager));
: File.ReadAllLines(_keyFilePath)
.Select(static k => k.Trim())
.Where(static k => !string.IsNullOrWhiteSpace(k))
.Select(k => new Signer(_chainId, new PrivateKey(k), _logManager));

ILogger log = _logManager.GetClassLogger();
List<string> txHashes = new List<string>();
Expand All @@ -163,17 +149,9 @@ public async Task<IEnumerable<string>> ReclaimFunds(Address beneficiary, UInt256

ulong nonce = HexConvert.ToUInt64(nonceString);

string? gasPriceRes = await _rpcClient.Post<string>("eth_gasPrice") ?? "1";
UInt256 gasPrice = HexConvert.ToUInt256(gasPriceRes);
(UInt256 resolvedMaxFeePerGas, UInt256 resolvedPriorityFeePerGas) = await ResolveFeeSettingsAsync(maxFee, maxPriorityFee);

UInt256 maxPriorityFeePerGas = maxPriorityFee;
if (maxPriorityFee == 0)
{
string? maxPriorityFeePerGasRes = await _rpcClient.Post<string>("eth_maxPriorityFeePerGas") ?? "1";
maxPriorityFeePerGas = HexConvert.ToUInt256(maxPriorityFeePerGasRes);
}

UInt256 approxGasFee = (gasPrice + maxPriorityFeePerGas) * GasCostOf.Transaction;
UInt256 approxGasFee = resolvedMaxFeePerGas * GasCostOf.Transaction;

if (balance < approxGasFee)
{
Expand All @@ -183,12 +161,13 @@ public async Task<IEnumerable<string>> ReclaimFunds(Address beneficiary, UInt256

UInt256 toSend = balance - approxGasFee;

Transaction tx = CreateTx(_chainId,
beneficiary,
maxFee != 0 ? maxFee : gasPrice + maxPriorityFeePerGas,
nonce,
maxPriorityFeePerGas,
toSend);
Transaction tx = CreateTx(
_chainId,
beneficiary,
resolvedMaxFeePerGas,
nonce,
resolvedPriorityFeePerGas,
toSend);
await signer.Sign(tx);

string txRlp = Convert.ToHexStringLower(txDecoder
Expand All @@ -215,4 +194,23 @@ private static Transaction CreateTx(ulong chainId, Address beneficiary, UInt256
To = beneficiary,
};
}

private async Task<(UInt256 maxFeePerGas, UInt256 maxPriorityFeePerGas)> ResolveFeeSettingsAsync(UInt256 maxFeeOverride, UInt256 maxPriorityFeeOverride)
{
UInt256 resolvedPriority = maxPriorityFeeOverride;
if (resolvedPriority == 0)
{
string? rpcResult = await _rpcClient.Post<string>("eth_maxPriorityFeePerGas") ?? "0x1";
resolvedPriority = HexConvert.ToUInt256(rpcResult);
}

UInt256 resolvedMaxFee = maxFeeOverride;
if (resolvedMaxFee == 0)
{
string? rpcResult = await _rpcClient.Post<string>("eth_gasPrice") ?? "0x1";
resolvedMaxFee = HexConvert.ToUInt256(rpcResult);
}

return (resolvedMaxFee, resolvedPriority);
}
}
45 changes: 40 additions & 5 deletions tools/SendBlobs/HexConvert.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Globalization;
using Nethermind.Core.Extensions;
using Nethermind.Int256;

namespace SendBlobs;
internal static class HexConvert
{
public static UInt256 ToUInt256(string s)
public static UInt256 ToUInt256(string value)
{
return (UInt256)ToUInt64(s);
ArgumentException.ThrowIfNullOrWhiteSpace(value);
value = value.Trim();

if (IsHex(value))
{
return new UInt256(Bytes.FromHexString(value));
}

return UInt256.Parse(value);
}

public static ulong ToUInt64(string s)
public static ulong ToUInt64(string value)
{
return Convert.ToUInt64(s, s.StartsWith("0x") ? 16 : 10);
ArgumentException.ThrowIfNullOrWhiteSpace(value);
value = value.Trim();

if (!IsHex(value))
{
return ulong.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
}

string hexBody = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? value[2..]
: value;

if (hexBody.Length <= 16)
{
return Convert.ToUInt64(hexBody, 16);
}

UInt256 parsed = new(Bytes.FromHexString(value));
if (parsed > ulong.MaxValue)
{
throw new OverflowException($"Value '{value}' exceeds UInt64 range.");
}

return (ulong)parsed;
}

private static bool IsHex(string value) => value.StartsWith("0x", StringComparison.OrdinalIgnoreCase);
}
Loading