diff --git a/tools/SendBlobs/BlobSender.cs b/tools/SendBlobs/BlobSender.cs index aaad1d6ab34..e67a782e9c1 100644 --- a/tools/SendBlobs/BlobSender.cs +++ b/tools/SendBlobs/BlobSender.cs @@ -65,6 +65,7 @@ public async Task SendRandomBlobs( IReleaseSpec spec) { List<(Signer, ulong)> signers = []; + IBlobProofsManager proofs = IBlobProofsManager.For(spec.BlobProofVersion); if (waitForInclusion) { @@ -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) @@ -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++) @@ -141,8 +134,6 @@ public async Task SendRandomBlobs( } } - IBlobProofsManager proofs = IBlobProofsManager.For(spec.BlobProofVersion); - ShardBlobNetworkWrapper blobsContainer = proofs.AllocateWrapper(blobs); proofs.ComputeProofsAndCommitments(blobsContainer); @@ -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); } } } @@ -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 @@ -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; @@ -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("eth_getTransactionByHash", txHash.ToString(), true); - Console.WriteLine($"Found blob transaction in block {blockResult.Number}"); return; } @@ -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 + }; } diff --git a/tools/SendBlobs/Dockerfile b/tools/SendBlobs/Dockerfile index 0c087d90339..5df245bc3dd 100644 --- a/tools/SendBlobs/Dockerfile +++ b/tools/SendBlobs/Dockerfile @@ -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 @@ -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 diff --git a/tools/SendBlobs/FundsDistributor.cs b/tools/SendBlobs/FundsDistributor.cs index fe87fd76b3b..332354c91fc 100644 --- a/tools/SendBlobs/FundsDistributor.cs +++ b/tools/SendBlobs/FundsDistributor.cs @@ -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; @@ -30,13 +30,7 @@ public FundsDistributor(IJsonRpcClient rpcClient, ulong chainId, string? keyFile /// /// Distribute the available funds from an address to a number of newly generated private keys. /// - /// - /// - /// - /// - /// containing all the executed tx hashes. - /// - public async Task> DitributeFunds(Signer distributeFrom, uint keysToMake, UInt256 maxFee, UInt256 maxPriorityFee) + public async Task> DistributeFunds(Signer distributeFrom, uint keysToMake, UInt256 maxFee, UInt256 maxPriorityFee) { if (keysToMake == 0) throw new ArgumentException("keysToMake must be greater than zero.", nameof(keysToMake)); @@ -48,17 +42,6 @@ public async Task> DitributeFunds(Signer distributeFrom, uin if (nonceString is null) throw new AccountException($"Unable to get nonce for {distributeFrom.Address}"); - string? gasPriceRes = await _rpcClient.Post("eth_gasPrice") ?? "1"; - UInt256 gasPrice = HexConvert.ToUInt256(gasPriceRes); - - string? maxPriorityFeePerGasRes; - UInt256 maxPriorityFeePerGas = maxPriorityFee; - if (maxPriorityFee == 0) - { - maxPriorityFeePerGasRes = await _rpcClient.Post("eth_maxPriorityFeePerGas") ?? "1"; - maxPriorityFeePerGas = HexConvert.ToUInt256(maxPriorityFeePerGasRes); - } - UInt256 balance = new UInt256(Bytes.FromHexString(balanceString)); if (balance == 0) @@ -66,7 +49,9 @@ public async Task> DitributeFunds(Signer distributeFrom, uin 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; @@ -80,9 +65,7 @@ public async Task> DitributeFunds(Signer distributeFrom, uin UInt256 perKeyToSend = balanceToDistribute / keysToMake; using PrivateKeyGenerator generator = new(); - IEnumerable privateKeys = Enumerable.Range(1, (int)keysToMake).Select(i => generator.Generate()); - - List txHash = new List(); + List txHashes = new(); TxDecoder txDecoder = TxDecoder.Instance; StreamWriter? keyWriter = null; @@ -94,25 +77,25 @@ public async Task> 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("eth_gasPrice") ?? "1"; - gasPrice = HexConvert.ToUInt256(gasPriceRes); - - maxPriorityFeePerGasRes = await _rpcClient.Post("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); @@ -121,7 +104,7 @@ public async Task> DitributeFunds(Signer distributeFrom, uin string? result = await _rpcClient.Post("eth_sendRawTransaction", $"0x{txRlp}"); if (result is not null) - txHash.Add(result); + txHashes.Add(result); if (keyWriter is not null) keyWriter.WriteLine(key.ToString()); @@ -130,7 +113,7 @@ public async Task> DitributeFunds(Signer distributeFrom, uin } } - return txHash; + return txHashes; } /// @@ -144,7 +127,10 @@ public async Task> ReclaimFunds(Address beneficiary, UInt256 { IEnumerable 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 txHashes = new List(); @@ -163,17 +149,9 @@ public async Task> ReclaimFunds(Address beneficiary, UInt256 ulong nonce = HexConvert.ToUInt64(nonceString); - string? gasPriceRes = await _rpcClient.Post("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("eth_maxPriorityFeePerGas") ?? "1"; - maxPriorityFeePerGas = HexConvert.ToUInt256(maxPriorityFeePerGasRes); - } - - UInt256 approxGasFee = (gasPrice + maxPriorityFeePerGas) * GasCostOf.Transaction; + UInt256 approxGasFee = resolvedMaxFeePerGas * GasCostOf.Transaction; if (balance < approxGasFee) { @@ -183,12 +161,13 @@ public async Task> 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 @@ -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("eth_maxPriorityFeePerGas") ?? "0x1"; + resolvedPriority = HexConvert.ToUInt256(rpcResult); + } + + UInt256 resolvedMaxFee = maxFeeOverride; + if (resolvedMaxFee == 0) + { + string? rpcResult = await _rpcClient.Post("eth_gasPrice") ?? "0x1"; + resolvedMaxFee = HexConvert.ToUInt256(rpcResult); + } + + return (resolvedMaxFee, resolvedPriority); + } } diff --git a/tools/SendBlobs/HexConvert.cs b/tools/SendBlobs/HexConvert.cs index d68014b229c..30afd03f170 100644 --- a/tools/SendBlobs/HexConvert.cs +++ b/tools/SendBlobs/HexConvert.cs @@ -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); } diff --git a/tools/SendBlobs/SetupCli.cs b/tools/SendBlobs/SetupCli.cs index af627173441..98f1d9cb764 100644 --- a/tools/SendBlobs/SetupCli.cs +++ b/tools/SendBlobs/SetupCli.cs @@ -5,6 +5,7 @@ using Nethermind.Crypto; using Nethermind.Logging; using System.CommandLine; +using System.Globalization; using Nethermind.Core.Specs; using Nethermind.Specs.Forks; using Nethermind.JsonRpc.Client; @@ -60,7 +61,7 @@ public static void SetupExecute(RootCommand command) Description = "A multiplier to use for gas fees", HelpName = "value" }; - Option maxPriorityFeeGasOption = new("--maxpriorityfee") + Option maxPriorityFeeGasOption = new("--maxpriorityfee") { Description = "The maximum priority fee for each transaction", HelpName = "fee" @@ -87,7 +88,25 @@ public static void SetupExecute(RootCommand command) string? privateKeyValue = parseResult.GetValue(privateKeyOption); if (privateKeyFileValue is not null) - privateKeys = File.ReadAllLines(privateKeyFileValue).Select(k => new PrivateKey(k)).ToArray(); + { + if (!File.Exists(privateKeyFileValue)) + { + Console.WriteLine($"Key file '{privateKeyFileValue}' was not found."); + return Task.CompletedTask; + } + + privateKeys = File.ReadAllLines(privateKeyFileValue) + .Select(static k => k.Trim()) + .Where(static k => !string.IsNullOrWhiteSpace(k)) + .Select(static k => new PrivateKey(k)) + .ToArray(); + + if (privateKeys.Length == 0) + { + Console.WriteLine($"Key file '{privateKeyFileValue}' does not contain any private keys."); + return Task.CompletedTask; + } + } else if (privateKeyValue is not null) privateKeys = [new PrivateKey(privateKeyValue)]; else @@ -99,9 +118,20 @@ public static void SetupExecute(RootCommand command) string? fork = parseResult.GetValue(forkOption); IReleaseSpec spec = fork is null ? Prague.Instance : SpecNameParser.Parse(fork); + (int count, int blobCount, string @break)[] txOptions; + try + { + txOptions = ParseTxOptions(parseResult.GetValue(blobTxOption)); + } + catch (ArgumentException ex) + { + Console.WriteLine(ex.Message); + return Task.CompletedTask; + } + BlobSender sender = new(parseResult.GetValue(rpcUrlOption)!, SimpleConsoleLogManager.Instance); return sender.SendRandomBlobs( - ParseTxOptions(parseResult.GetValue(blobTxOption)), + txOptions, privateKeys, parseResult.GetValue(receiverOption)!, parseResult.GetValue(maxFeePerBlobGasOption) ?? parseResult.GetValue(maxFeePerDataGasOptionObsolete), @@ -117,37 +147,40 @@ private static (int count, int blobCount, string @break)[] ParseTxOptions(string if (string.IsNullOrWhiteSpace(options)) return Array.Empty<(int count, int blobCount, string @break)>(); - ReadOnlySpan chars = options.AsSpan(); - var result = new List<(int, int, string)>(); + List<(int count, int blobCount, string @break)> result = new(); - ReadOnlySpan nextComma; - int offSet = 0; - while (true) + string[] parts = options.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + foreach (string rawPart in parts) { - nextComma = SplitToNext(chars[offSet..], ','); + string part = rawPart; + string breakValue = string.Empty; - ReadOnlySpan @break = SplitToNext(nextComma, '-', true); - ReadOnlySpan rest = nextComma[..(nextComma.Length - (@break.Length == 0 ? 0 : @break.Length + 1))]; - ReadOnlySpan count = SplitToNext(rest, 'x'); - ReadOnlySpan txCount = SplitToNext(rest, 'x', true); - - result.Add(new(int.Parse(count), txCount.Length == 0 ? 1 : int.Parse(txCount), new string(@break))); + int breakIndex = part.IndexOf('-'); + if (breakIndex >= 0) + { + breakValue = part[(breakIndex + 1)..].Trim(); + part = part[..breakIndex]; + } - offSet += nextComma.Length + 1; - if (offSet > chars.Length) + int blobIndex = part.IndexOf('x'); + if (blobIndex < 0) { - break; + blobIndex = part.IndexOf('X'); } + string txCountValue = blobIndex >= 0 ? part[..blobIndex] : part; + string blobCountValue = blobIndex >= 0 ? part[(blobIndex + 1)..] : string.Empty; + + if (!int.TryParse(txCountValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out int txCount) || txCount <= 0) + throw new ArgumentException($"Invalid transaction count in option '{rawPart}'.", nameof(options)); + + int blobCount = 1; + if (blobCountValue.Length > 0 && (!int.TryParse(blobCountValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out blobCount) || blobCount <= 0)) + throw new ArgumentException($"Invalid blob count in option '{rawPart}'.", nameof(options)); + + result.Add((txCount, blobCount, breakValue)); } - return result.ToArray(); - } - private static ReadOnlySpan SplitToNext(ReadOnlySpan line, char separator, bool returnRemainder = false) - { - int i = line.IndexOf(separator); - if (i == -1) - return returnRemainder ? ReadOnlySpan.Empty : line; - return returnRemainder ? line[(i + 1)..] : line[..i]; + return result.ToArray(); } public static void SetupDistributeCommand(Command root) @@ -171,7 +204,8 @@ public static void SetupDistributeCommand(Command root) Option keyNumberOption = new("--number") { Description = "The number of new addresses/keys to make", - HelpName = "value" + HelpName = "value", + Required = true }; Option keyFileOption = new("--keyfile") { @@ -197,6 +231,13 @@ public static void SetupDistributeCommand(Command root) command.Add(maxFeeOption); command.SetAction(async (parseResult, cancellationToken) => { + uint keyNumber = parseResult.GetValue(keyNumberOption); + if (keyNumber == 0) + { + Console.WriteLine("--number must be greater than zero."); + return; + } + IJsonRpcClient rpcClient = InitRpcClient( parseResult.GetValue(rpcUrlOption)!, SimpleConsoleLogManager.Instance.GetClassLogger()); @@ -209,9 +250,9 @@ public static void SetupDistributeCommand(Command root) FundsDistributor distributor = new( rpcClient, chainId, parseResult.GetValue(keyFileOption), SimpleConsoleLogManager.Instance); - await distributor.DitributeFunds( + await distributor.DistributeFunds( signer, - parseResult.GetValue(keyNumberOption), + keyNumber, parseResult.GetValue(maxFeeOption), parseResult.GetValue(maxPriorityFeeGasOption)); }); @@ -240,7 +281,8 @@ public static void SetupReclaimCommand(Command root) Option keyFileOption = new("--keyfile") { Description = "File of the private keys to reclaim from", - HelpName = "path" + HelpName = "path", + Required = true }; Option maxPriorityFeeGasOption = new("--maxpriorityfee") { @@ -254,6 +296,7 @@ public static void SetupReclaimCommand(Command root) }; command.Add(rpcUrlOption); + command.Add(receiverOption); command.Add(keyFileOption); command.Add(maxPriorityFeeGasOption); command.Add(maxFeeOption);