RLP.java
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.rlp;
import static java.lang.String.format;
import java.util.function.Consumer;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.MutableBytes;
/** Static methods to work with RLP encoding/decoding. */
public abstract class RLP {
private RLP() {}
/** The RLP encoding of a single empty value, also known as RLP null. */
public static final Bytes NULL = encodeOne(Bytes.EMPTY);
public static final Bytes EMPTY_LIST;
// RLP encoding requires payloads to be less thatn 2^64 bytes in length
// As a result, the longest RLP strings will have a prefix composed of 1 byte encoding the type
// of string followed by at most 8 bytes describing the length of the string
public static final int MAX_PREFIX_SIZE = 9;
static {
final BytesValueRLPOutput out = new BytesValueRLPOutput();
out.startList();
out.endList();
EMPTY_LIST = out.encoded();
}
/**
* Creates a new {@link RLPInput} suitable for decoding the provided RLP encoded value.
*
* <p>The created input is strict, in that exceptions will be thrown for any malformed input,
* either by this method or by future reads from the returned input.
*
* @param encoded The RLP encoded data for which to create a {@link RLPInput}.
* @return A newly created {@link RLPInput} to decode {@code encoded}.
* @throws MalformedRLPInputException if {@code encoded} doesn't contain a single RLP encoded item
* (item that can be a list itself). Note that more deeply nested corruption/malformation of
* the input will not be detected by this method call, but will be later when the input is
* read.
*/
public static RLPInput input(final Bytes encoded) {
return input(encoded, false);
}
public static RLPInput input(final Bytes encoded, final boolean lenient) {
return new BytesValueRLPInput(encoded, lenient);
}
/**
* Creates a {@link RLPOutput}, pass it to the provided consumer for writing, and then return the
* RLP encoded result of that writing.
*
* <p>This method is a convenience method that is mostly meant for use with class that have a
* method to write to an {@link RLPOutput}. For instance:
*
* <pre>{@code
* class Foo {
* public void writeTo(RLPOutput out) {
* //... write some data to out ...
* }
* }
*
* Foo f = ...;
* // RLP encode f
* Bytes encoded = RLPs.encode(f::writeTo);
* }</pre>
*
* @param writer A method that given an {@link RLPOutput}, writes some data to it.
* @return The RLP encoding of the data written by {@code writer}.
*/
public static Bytes encode(final Consumer<RLPOutput> writer) {
final BytesValueRLPOutput out = new BytesValueRLPOutput();
writer.accept(out);
return out.encoded();
}
/**
* Encodes a single binary value into RLP.
*
* <p>This is equivalent (but possibly more efficient) to:
*
* <pre>
* {
* @code
* BytesValueRLPOutput out = new BytesValueRLPOutput();
* out.writeBytes(value);
* return out.encoded();
* }
* </pre>
*
* <p>So note in particular that the value is encoded as is (and so not as a scalar in
* particular).
*
* @param value The value to encode.
* @return The RLP encoding containing only {@code value}.
*/
public static Bytes encodeOne(final Bytes value) {
if (RLPEncodingHelpers.isSingleRLPByte(value)) return value;
final MutableBytes res = MutableBytes.create(RLPEncodingHelpers.elementSize(value));
RLPEncodingHelpers.writeElement(value, res, 0);
return res;
}
/**
* Decodes an RLP-encoded value assuming it contains a single non-list item.
*
* <p>This is equivalent (but possibly more efficient) to:
*
* <pre>{@code
* return input(value).readBytes();
* }</pre>
*
* <p>So note in particular that the value is decoded as is (and so not as a scalar in
* particular).
*
* @param encodedValue The encoded RLP value.
* @return The single value encoded in {@code encodedValue}.
* @throws RLPException if {@code encodedValue} is not a valid RLP encoding or if it does not
* contains a single non-list item.
*/
public static Bytes decodeOne(final Bytes encodedValue) {
if (encodedValue.size() == 0) {
throw new RLPException("Invalid empty input for RLP decoding");
}
final int prefix = encodedValue.get(0) & 0xFF;
final RLPDecodingHelpers.Kind kind = RLPDecodingHelpers.Kind.of(prefix);
if (kind.isList()) {
throw new RLPException(format("Invalid input: value %s is an RLP list", encodedValue));
}
if (kind == RLPDecodingHelpers.Kind.BYTE_ELEMENT) {
return encodedValue;
}
final int offset;
final int size;
if (kind == RLPDecodingHelpers.Kind.SHORT_ELEMENT) {
offset = 1;
size = prefix - 0x80;
} else {
final int sizeLength = prefix - 0xb7;
if (1 + sizeLength > encodedValue.size()) {
throw new RLPException(
format(
"Malformed RLP input: not enough bytes to read size of "
+ "long item in %s: expected %d bytes but only %d",
encodedValue, sizeLength + 1, encodedValue.size()));
}
offset = 1 + sizeLength;
size = RLPDecodingHelpers.extractSize((index) -> encodedValue.get(index), 1, sizeLength);
}
if (offset + size != encodedValue.size()) {
throw new RLPException(
format(
"Malformed RLP input: %s should be of size %d according to "
+ "prefix byte but of size %d",
encodedValue, offset + size, encodedValue.size()));
}
return encodedValue.slice(offset, size);
}
/**
* Validates that the provided value is a valid RLP encoding.
*
* @param encodedValue The value to check.
* @throws RLPException if {@code encodedValue} is not a valid RLP encoding.
*/
public static void validate(final Bytes encodedValue) {
final RLPInput in = input(encodedValue);
while (!in.isDone()) {
if (in.nextIsList()) {
in.enterList();
} else if (in.isEndOfCurrentList()) {
in.leaveList();
} else {
// Skip does as much validation as can be done in general, without allocating anything.
in.skipNext();
}
}
}
/**
* Given a {@link Bytes} containing rlp-encoded data, determines the full length of the encoded
* value (including the prefix) by inspecting the prefixed metadata.
*
* @param value the rlp-encoded byte string
* @return the length of the encoded data, according to the prefixed metadata
*/
public static int calculateSize(final Bytes value) {
return RLPDecodingHelpers.rlpElementMetadata((index) -> value.get((int) index), value.size(), 0)
.getEncodedSize();
}
}