Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Rlp Laravel Package

web3p/rlp

web3p/rlp is a PHP library for Recursive Length Prefix (RLP) encoding and decoding, the serialization format used throughout Ethereum. Encode arrays, strings, and integers, or decode 0x-prefixed hex payloads, through a simple API. MIT licensed and installable via Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and smoke-test.

composer require web3p/rlp

Ignore the README's "set minimum stability to dev" line — that is a leftover from the pre-1.0 era. 0.4.x is tagged and stable, so a plain composer require web3p/rlp (or a pinned "web3p/rlp": "^0.4") works. The package is pure PHP with no extensions, tested on PHP 7.1–8.4.

Verify in php artisan tinker:

use Web3p\RLP\RLP;

$rlp = new RLP();
$rlp->encode('dog');          // "83646f67"
$rlp->encode(['dog']);        // "c483646f67"
$rlp->encode('');             // "80"
$rlp->encode([]);             // "c0"
$rlp->decode('0xc483646f67'); // ["646f67"]

Register it in the container. There is no facade and no config; the class is stateless, so a singleton binding is enough:

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(\Web3p\RLP\RLP::class, fn () => new \Web3p\RLP\RLP());
}

First use case you will hit: building the signing payload for an Ethereum transaction. Everything before the signature — nonce, gas fields, to, value, data, chain id — has to be RLP-serialized, hashed with keccak256, and signed. This package does only the serialization step:

$payload = $rlp->encode([
    '0x' . dechex($chainId),           // chain id
    '0x' . dechex($nonce),             // nonce
    '0x' . dechex($maxPriorityFeePerGas),
    '0x' . dechex($maxFeePerGas),
    '0x' . dechex($gasLimit),
    $to,                               // 0x-prefixed 20-byte address, or '' for contract creation
    '0x' . dechex($valueWei),
    $data,                             // 0x-prefixed calldata
    [],                                // access list
]);
// $payload is hex WITHOUT 0x — hash that with a keccak256 lib, then sign with secp256k1.

If you want the whole transaction object rather than raw bytes, use web3p/ethereum-tx, which wraps this package. Reach for web3p/rlp directly when you are decoding already-signed raw transactions or assembling custom payloads.

Implementation Patterns

Wrap it in a single application service, not scattered calls. The bare API is four methods wide (encode, decode, Str::encode, Str::decodeHex) but every caller needs the same guards. One seam pays for itself in tests:

namespace App\Services;

use Web3p\RLP\RLP;

final class Rlp
{
    public function __construct(private RLP $rlp) {}

    /** @return string hex without 0x */
    public function encode(mixed $value): string
    {
        return $this->rlp->encode($value);
    }

    /** @return string hex WITH 0x, ready for JSON-RPC */
    public function encodeRpc(mixed $value): string
    {
        return '0x' . $this->rlp->encode($value);
    }

    /** @return string[] hex without 0x */
    public function decode(string $hex): array
    {
        return $this->rlp->decode(str_starts_with($hex, '0x') ? $hex : '0x' . $hex);
    }
}

Model quantity vs data explicitly. This is the single most important day-to-day decision, because RLP itself does not distinguish them — Ethereum does:

  • Quantity (nonce, gas, value, chain id): pass an int or a canonical 0x hex string with no leading zeros. Zero must be passed as 0, which encodes to 80. Passing '0x00' encodes a one-byte string 00 and will produce an invalid transaction.
  • Data / fixed-size bytes (address, hash, calldata, storage key): pass a 0x-prefixed hex string of the correct length. 0x0000…0001 (20 bytes) encodes as 94 + 20 bytes; leading zero bytes survive as of 0.4.0.
  • Lists (access list, nested tuples): nested PHP arrays.

Encode a TransactionData DTO into that array in one place, and unit-test it against known vectors rather than asserting on the string inside controllers.

Decode raw transactions with positional destructuring and padding. decode() returns an array of hex strings with leading zero bytes stripped, so every fixed-width field needs re-padding:

[$nonce, $gasPrice, $gasLimit, $to, $value, $data, $v, $r, $s] = $rlp->decode($rawTxHex);

$to = str_pad($to, 40, '0', STR_PAD_LEFT);     // 20-byte address
$r  = str_pad($r, 64, '0', STR_PAD_LEFT);      // 32-byte signature component

Wrap this in a RawTransaction::fromHex() value object so padding bugs surface in one test instead of five call sites. Remember EIP-1559 raw transactions have a different shape (chainId, nonce, maxPriorityFee, maxFee, gasLimit, to, value, data, accessList, yParity, r, s) — branch on the first byte (0x01/0x02 typed envelope vs >= 0xc0 legacy list).

Validate at the boundary. Hex strings from HTTP requests are untrusted. A form-request rule plus a try/catch around decode() is the standard shape, because 0.4.0 made the decoder strict:

try {
    $fields = $rlp->decode($request->string('raw_tx')->toString());
} catch (\InvalidArgumentException|\RuntimeException $e) {
    throw ValidationException::withMessages(['raw_tx' => 'Malformed RLP payload.']);
}

Use Str for the display layer. Str::decodeHex($hex) turns a decoded field into a UTF-8 string; Str::encode() goes the other way. Do not call hex2bin() directly on untrusted decoded data and echo it — decoded values can contain arbitrary bytes.

Testing pattern: keep a table of cross-language vectors (ethereumjs/rlp produces the same bytes) — ''→80, []→c0, 0→80, 'dog'→83646f67, ['cat','dog']→c88363617483646f67, and a known mainnet raw transaction round-trip. That table catches almost every regression when you bump versions.

Gotchas and Tips

  • Asymmetric 0x handling. encode() returns hex without 0x; decode() requires 0x (odd-length input throws). Every "invalid hex" bug I've seen with this package traces back to this. Prefix at the service boundary, not at each call site.
  • decode() always returns an array, even for a top-level scalar: decode('0x83646f67') gives ['646f67'], not '646f67'. Destructure with that in mind.
  • Leading zeros are stripped on decode. 0x00…01 comes back as 1. Always str_pad() fixed-width fields before persisting or comparing. Conversely, do not re-encode a decoded value straight back without re-padding — round-tripping is not byte-identical for data fields.
  • Numeric strings are treated as quantities. '12345' is interpreted as a number; '0x3039' is a hex string. Never pass a decimal numeric string intending bytes — a "hash" that happens to look numeric will be mangled. Always use 0x for data.
  • Big integers must be hex. Anything above PHP_INT_MAX must be passed as a 0x hex string; decimals throw InvalidArgumentException in 0.4.0. WeChat-style amounts in wei routinely exceed this — use bcmath/gmp to convert, then pass hex.
  • Negative, fractional, and out-of-range numbers throw. If your app historically passed -1, 1.5, or a float in exponent form, 0.4.0 rejects them instead of silently producing 80. Add a cast/validation layer rather than relying on implicit coercion.
  • The 0.4.0 decoder is strict. Truncated data, list items longer than their parent list, trailing bytes after the top-level item, and non-canonical lengths (long form below 56 bytes, or lengths with leading zero bytes) all throw RuntimeException. This is correct per the spec, but it means payloads produced by buggy third-party encoders that previously "worked" will now fail. Do not loosen the decoder — fix the producer, or quarantine the edge case with an explicit, logged fallback.
  • encode() output is not zero-prefixed and is not 0x-prefixed. When storing in MySQL use a binary column with hex2bin($encoded) (halves the size and avoids collation case-folding), and bin2hex() on read.
  • No hashing, no signing, no Ethereum structure. This is byte serialization only. Pair with a keccak256 implementation and secp256k1 signer (or web3p/ethereum-tx / web3p/web3.php) for a complete flow. Do not expect address checksumming or ABI encoding here.
  • Untrusted input has no depth or size guard. Deeply nested lists recurse; a multi-megabyte payload is decoded in-memory in pure PHP. Enforce a max-length check and a max nesting depth in your wrapper before calling decode() on anything user-supplied.
  • Performance. Pure-PHP string concatenation; encoding batches of thousands of small items is measurably slower than JS/Rust equivalents. Cache encoded static payloads (e.g. prebuilt calldata for a contract method with fixed arguments) and avoid re-encoding inside loops.
  • Extension points are limited by design. The class has no options, no streaming, no custom type handlers. When you need behaviour (canonicalisation, padding, DTO mapping), compose a wrapper — do not subclass RLP and override encode(), because the recursion calls $this->encode() internally and you will break nested encoding semantics if you change the contract.
  • Debugging tip. Hex-compare against ethers.js in a scratch Node process. If the byte strings match, the problem is your input modelling (quantity vs data), not the library. If they diverge, check leading-zero handling and whether you passed a decimal-looking string.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route