You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tuweni.apache.org by to...@apache.org on 2019/04/26 00:24:24 UTC

[incubator-tuweni] 46/48: Add UInt32

This is an automated email from the ASF dual-hosted git repository.

toulmean pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git

commit 04d5d2f62237579308096bb7e0780a762942afae
Author: Antoine Toulme <to...@apache.org>
AuthorDate: Thu Apr 25 16:22:41 2019 -0700

    Add UInt32
---
 .../tuweni/units/bigints/BaseUInt32Value.java      | 334 +++++++++
 .../org/apache/tuweni/units/bigints/UInt32.java    | 533 +++++++++++++++
 .../apache/tuweni/units/bigints/UInt32Domain.java  |  52 ++
 .../apache/tuweni/units/bigints/UInt32Value.java   | 375 +++++++++++
 .../tuweni/units/bigints/UInt32ValueDomain.java    |  65 ++
 .../org/apache/tuweni/units/bigints/UInt32s.java   |  42 ++
 .../tuweni/units/bigints/BaseUInt32ValueTest.java  | 736 ++++++++++++++++++++
 .../apache/tuweni/units/bigints/UInt32Test.java    | 746 +++++++++++++++++++++
 8 files changed, 2883 insertions(+)

diff --git a/units/src/main/java/org/apache/tuweni/units/bigints/BaseUInt32Value.java b/units/src/main/java/org/apache/tuweni/units/bigints/BaseUInt32Value.java
new file mode 100644
index 0000000..ab82058
--- /dev/null
+++ b/units/src/main/java/org/apache/tuweni/units/bigints/BaseUInt32Value.java
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.tuweni.units.bigints;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.tuweni.bytes.Bytes;
+
+import java.math.BigInteger;
+import java.util.function.Function;
+
+/**
+ * Base class for {@link UInt32Value}.
+ *
+ * <p>
+ * This class is abstract as it is not meant to be used directly, but it has no abstract methods. As mentioned in
+ * {@link UInt32Value}, this is used to create strongly-typed type aliases of {@link UInt32}. In other words, this allow
+ * to "tag" numbers with the unit of what they represent for the type-system, which can help clarity, but also forbid
+ * mixing numbers that are mean to be of different units (the strongly-typed part).
+ *
+ * <p>
+ * This class implements {@link UInt32Value}, but also adds a few operations that take a {@link UInt32} directly, for
+ * instance {@link #multiply(UInt32)}. The rational is that multiplying a given quantity of something by a "raw" number
+ * is always meaningful, and return a new quantity of the same thing.
+ *
+ * @param <T> The concrete type of the value.
+ */
+public abstract class BaseUInt32Value<T extends UInt32Value<T>> implements UInt32Value<T> {
+
+  private final UInt32 value;
+  private final Function<UInt32, T> ctor;
+
+  /**
+   * @param value The value to instantiate this {@code UInt32Value} with.
+   * @param ctor A constructor for the concrete type.
+   */
+  protected BaseUInt32Value(UInt32 value, Function<UInt32, T> ctor) {
+    requireNonNull(value);
+    requireNonNull(ctor);
+    this.value = value;
+    this.ctor = ctor;
+  }
+
+  /**
+   * @param value An unsigned value to instantiate this {@code UInt32Value} with.
+   * @param ctor A constructor for the concrete type.
+   */
+  protected BaseUInt32Value(int value, Function<UInt32, T> ctor) {
+    requireNonNull(ctor);
+    this.value = UInt32.valueOf(value);
+    this.ctor = ctor;
+  }
+
+  /**
+   * @param value An unsigned value to instantiate this {@code UInt32Value} with.
+   * @param ctor A constructor for the concrete type.
+   */
+  protected BaseUInt32Value(BigInteger value, Function<UInt32, T> ctor) {
+    requireNonNull(value);
+    requireNonNull(ctor);
+    this.value = UInt32.valueOf(value);
+    this.ctor = ctor;
+  }
+
+  /**
+   * Return a copy of this value, or itself if immutable.
+   *
+   * <p>
+   * The default implementation of this method returns a copy using the constructor for the concrete type and the bytes
+   * returned from {@link #toBytes()}. Most implementations will want to override this method to instead return
+   * {@code this}.
+   *
+   * @return A copy of this value, or itself if immutable.
+   */
+  protected T copy() {
+    return ctor.apply(value);
+  }
+
+  /**
+   * Return the zero value for this type.
+   *
+   * <p>
+   * The default implementation of this method returns a value obtained from calling the concrete type constructor with
+   * an argument of {@link UInt32#ZERO}. Most implementations will want to override this method to instead return a
+   * static constant.
+   *
+   * @return The zero value for this type.
+   */
+  protected T zero() {
+    return ctor.apply(UInt32.ZERO);
+  }
+
+  @Override
+  public T add(T value) {
+    return add(value.toUInt32());
+  }
+
+  /**
+   * Returns a value that is {@code (this + value)}.
+   *
+   * @param value The amount to be added to this value.
+   * @return {@code this + value}
+   */
+  public T add(UInt32 value) {
+    if (value.isZero()) {
+      return copy();
+    }
+    return ctor.apply(this.value.add(value));
+  }
+
+  @Override
+  public T add(int value) {
+    if (value == 0) {
+      return copy();
+    }
+    return ctor.apply(this.value.add(value));
+  }
+
+  @Override
+  public T addMod(T value, UInt32 modulus) {
+    return addMod(value.toUInt32(), modulus);
+  }
+
+  /**
+   * Returns a value equivalent to {@code ((this + value) mod modulus)}.
+   *
+   * @param value The amount to be added to this value.
+   * @param modulus The modulus.
+   * @return {@code (this + value) mod modulus}
+   * @throws ArithmeticException {@code modulus} == 0.
+   */
+  public T addMod(UInt32 value, UInt32 modulus) {
+    return ctor.apply(this.value.addMod(value, modulus));
+  }
+
+  @Override
+  public T addMod(long value, UInt32 modulus) {
+    return ctor.apply(this.value.addMod(value, modulus));
+  }
+
+  @Override
+  public T addMod(long value, long modulus) {
+    return ctor.apply(this.value.addMod(value, modulus));
+  }
+
+  @Override
+  public T subtract(T value) {
+    return subtract(value.toUInt32());
+  }
+
+  /**
+   * Returns a value that is {@code (this - value)}.
+   *
+   * @param value The amount to be subtracted from this value.
+   * @return {@code this - value}
+   */
+  public T subtract(UInt32 value) {
+    if (value.isZero()) {
+      return copy();
+    }
+    return ctor.apply(this.value.subtract(value));
+  }
+
+  @Override
+  public T subtract(int value) {
+    if (value == 0) {
+      return copy();
+    }
+    return ctor.apply(this.value.subtract(value));
+  }
+
+  @Override
+  public T multiply(T value) {
+    return multiply(value.toUInt32());
+  }
+
+  /**
+   * Returns a value that is {@code (this * value)}.
+   *
+   * @param value The amount to multiply this value by.
+   * @return {@code this * value}
+   */
+  public T multiply(UInt32 value) {
+    if (isZero() || value.isZero()) {
+      return zero();
+    }
+    if (value.equals(UInt32.ONE)) {
+      return copy();
+    }
+    return ctor.apply(this.value.multiply(value));
+  }
+
+  @Override
+  public T multiply(int value) {
+    if (value == 0 || isZero()) {
+      return zero();
+    }
+    if (value == 1) {
+      return copy();
+    }
+    return ctor.apply(this.value.multiply(value));
+  }
+
+  @Override
+  public T multiplyMod(T value, UInt32 modulus) {
+    return multiplyMod(value.toUInt32(), modulus);
+  }
+
+  /**
+   * Returns a value that is {@code ((this * value) mod modulus)}.
+   *
+   * @param value The amount to multiply this value by.
+   * @param modulus The modulus.
+   * @return {@code (this * value) mod modulus}
+   * @throws ArithmeticException {@code value} &lt; 0 or {@code modulus} == 0.
+   */
+  public T multiplyMod(UInt32 value, UInt32 modulus) {
+    return ctor.apply(this.value.multiplyMod(value, modulus));
+  }
+
+  @Override
+  public T multiplyMod(int value, UInt32 modulus) {
+    return ctor.apply(this.value.multiplyMod(value, modulus));
+  }
+
+  @Override
+  public T multiplyMod(int value, int modulus) {
+    return ctor.apply(this.value.multiplyMod(value, modulus));
+  }
+
+  @Override
+  public T divide(T value) {
+    return divide(value.toUInt32());
+  }
+
+  /**
+   * Returns a value that is {@code (this / value)}.
+   *
+   * @param value The amount to divide this value by.
+   * @return {@code this / value}
+   * @throws ArithmeticException {@code value} == 0.
+   */
+  public T divide(UInt32 value) {
+    return ctor.apply(this.value.divide(value));
+  }
+
+  @Override
+  public T divide(int value) {
+    return ctor.apply(this.value.divide(value));
+  }
+
+  @Override
+  public T pow(UInt32 exponent) {
+    return ctor.apply(this.value.pow(exponent));
+  }
+
+  @Override
+  public T pow(long exponent) {
+    return ctor.apply(this.value.pow(exponent));
+  }
+
+  @Override
+  public T mod(UInt32 modulus) {
+    return ctor.apply(this.value.mod(modulus));
+  }
+
+  @Override
+  public T mod(int modulus) {
+    return ctor.apply(this.value.mod(modulus));
+  }
+
+  @Override
+  public int compareTo(T other) {
+    return compareTo(other.toUInt32());
+  }
+
+  /**
+   * Compare two {@link UInt32} values.
+   *
+   * @param other The value to compare to.
+   * @return A negative integer, zero, or a positive integer as this value is less than, equal to, or greater than the
+   *         specified value.
+   */
+  public int compareTo(UInt32 other) {
+    return this.value.compareTo(other);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof UInt32Value)) {
+      return false;
+    }
+    UInt32Value<?> other = (UInt32Value<?>) obj;
+    return this.value.equals(other.toUInt32());
+  }
+
+  @Override
+  public int hashCode() {
+    return value.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return value.toString();
+  }
+
+  @Override
+  public UInt32 toUInt32() {
+    return value;
+  }
+
+  @Override
+  public Bytes toBytes() {
+    return value.toBytes();
+  }
+
+  @Override
+  public Bytes toMinimalBytes() {
+    return value.toMinimalBytes();
+  }
+}
diff --git a/units/src/main/java/org/apache/tuweni/units/bigints/UInt32.java b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32.java
new file mode 100644
index 0000000..fa56ee8
--- /dev/null
+++ b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32.java
@@ -0,0 +1,533 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.tuweni.units.bigints;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.MutableBytes;
+
+import java.math.BigInteger;
+
+/**
+ * An unsigned 32-bit precision number.
+ *
+ * This is a raw {@link UInt32Value} - a 32-bit precision unsigned number of no particular unit.
+ */
+public final class UInt32 implements UInt32Value<UInt32> {
+  private final static int MAX_CONSTANT = 32;
+  private static UInt32[] CONSTANTS = new UInt32[MAX_CONSTANT + 1];
+  static {
+    CONSTANTS[0] = new UInt32(0);
+    for (int i = 1; i <= MAX_CONSTANT; ++i) {
+      CONSTANTS[i] = new UInt32(i);
+    }
+  }
+
+  /** The minimum value of a UInt32 */
+  public final static UInt32 MIN_VALUE = valueOf(0);
+  /** The maximum value of a UInt32 */
+  public final static UInt32 MAX_VALUE = new UInt32(~0);
+  /** The value 0 */
+  public final static UInt32 ZERO = valueOf(0);
+  /** The value 1 */
+  public final static UInt32 ONE = valueOf(1);
+
+  private static final BigInteger P_2_32 = BigInteger.valueOf(2).pow(32);
+
+  private final int value;
+
+  /**
+   * Return a {@code UInt32} containing the specified value.
+   *
+   * @param value The value to create a {@code UInt32} for.
+   * @return A {@code UInt32} containing the specified value.
+   * @throws IllegalArgumentException If the value is negative.
+   */
+  public static UInt32 valueOf(int value) {
+    checkArgument(value >= 0, "Argument must be positive");
+    return create(value);
+  }
+
+  /**
+   * Return a {@link UInt32} containing the specified value.
+   *
+   * @param value the value to create a {@link UInt32} for
+   * @return a {@link UInt32} containing the specified value
+   * @throws IllegalArgumentException if the value is negative or too large to be represented as a UInt32
+   */
+  public static UInt32 valueOf(BigInteger value) {
+    checkArgument(value.signum() >= 0, "Argument must be positive");
+    checkArgument(value.bitLength() <= 32, "Argument is too large to represent a UInt32");
+    return create(value.intValue());
+  }
+
+  /**
+   * Return a {@link UInt32} containing the value described by the specified bytes.
+   *
+   * @param bytes The bytes containing a {@link UInt32}.
+   * @return A {@link UInt32} containing the specified value.
+   * @throws IllegalArgumentException if {@code bytes.size() &gt; 8}.
+   */
+  public static UInt32 fromBytes(Bytes bytes) {
+    checkArgument(bytes.size() <= 8, "Argument is greater than 8 bytes");
+    return create(bytes.toInt());
+  }
+
+  /**
+   * Parse a hexadecimal string into a {@link UInt32}.
+   *
+   * @param str The hexadecimal string to parse, which may or may not start with "0x". That representation may contain
+   *        less than 8 bytes, in which case the result is left padded with zeros.
+   * @return The value corresponding to {@code str}.
+   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation or
+   *         contains more than 8 bytes.
+   */
+  public static UInt32 fromHexString(String str) {
+    return fromBytes(Bytes.fromHexStringLenient(str));
+  }
+
+  private static UInt32 create(int value) {
+    if (value >= 0 && value <= MAX_CONSTANT) {
+      return CONSTANTS[value];
+    }
+    return new UInt32(value);
+  }
+
+  private UInt32(int value) {
+    this.value = value;
+  }
+
+  @Override
+  public boolean isZero() {
+    return this.value == 0;
+  }
+
+  @Override
+  public UInt32 add(UInt32 value) {
+    if (value.value == 0) {
+      return this;
+    }
+    if (this.value == 0) {
+      return value;
+    }
+    return create(this.value + value.value);
+  }
+
+  @Override
+  public UInt32 add(int value) {
+    if (value == 0) {
+      return this;
+    }
+    return create(this.value + value);
+  }
+
+  @Override
+  public UInt32 addMod(UInt32 value, UInt32 modulus) {
+    if (modulus.isZero()) {
+      throw new ArithmeticException("addMod with zero modulus");
+    }
+    return create(toBigInteger().add(value.toBigInteger()).mod(modulus.toBigInteger()).intValue());
+  }
+
+  @Override
+  public UInt32 addMod(long value, UInt32 modulus) {
+    if (modulus.isZero()) {
+      throw new ArithmeticException("addMod with zero modulus");
+    }
+    return create(toBigInteger().add(BigInteger.valueOf(value)).mod(modulus.toBigInteger()).intValue());
+  }
+
+  @Override
+  public UInt32 addMod(long value, long modulus) {
+    if (modulus == 0) {
+      throw new ArithmeticException("addMod with zero modulus");
+    }
+    if (modulus < 0) {
+      throw new ArithmeticException("addMod unsigned with negative modulus");
+    }
+    return create(toBigInteger().add(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus)).intValue());
+  }
+
+  @Override
+  public UInt32 subtract(UInt32 value) {
+    if (value.isZero()) {
+      return this;
+    }
+    return create(this.value - value.value);
+  }
+
+  @Override
+  public UInt32 subtract(int value) {
+    return add(-value);
+  }
+
+  @Override
+  public UInt32 multiply(UInt32 value) {
+    if (this.value == 0 || value.value == 0) {
+      return ZERO;
+    }
+    if (value.value == 1) {
+      return this;
+    }
+    return create(this.value * value.value);
+  }
+
+  @Override
+  public UInt32 multiply(int value) {
+    if (value < 0) {
+      throw new ArithmeticException("multiply unsigned by negative");
+    }
+    if (value == 0 || this.value == 0) {
+      return ZERO;
+    }
+    if (value == 1) {
+      return this;
+    }
+    return create(this.value * value);
+  }
+
+  @Override
+  public UInt32 multiplyMod(UInt32 value, UInt32 modulus) {
+    if (modulus.isZero()) {
+      throw new ArithmeticException("multiplyMod with zero modulus");
+    }
+    if (this.value == 0 || value.value == 0) {
+      return ZERO;
+    }
+    if (value.value == 1) {
+      return mod(modulus);
+    }
+    return create(toBigInteger().multiply(value.toBigInteger()).mod(modulus.toBigInteger()).intValue());
+  }
+
+  @Override
+  public UInt32 multiplyMod(int value, UInt32 modulus) {
+    if (modulus.isZero()) {
+      throw new ArithmeticException("multiplyMod with zero modulus");
+    }
+    if (value == 0 || this.value == 0) {
+      return ZERO;
+    }
+    if (value == 1) {
+      return mod(modulus);
+    }
+    if (value < 0) {
+      throw new ArithmeticException("multiplyMod unsigned by negative");
+    }
+    return create(toBigInteger().multiply(BigInteger.valueOf(value)).mod(modulus.toBigInteger()).intValue());
+  }
+
+  @Override
+  public UInt32 multiplyMod(int value, int modulus) {
+    if (modulus == 0) {
+      throw new ArithmeticException("multiplyMod with zero modulus");
+    }
+    if (modulus < 0) {
+      throw new ArithmeticException("multiplyMod unsigned with negative modulus");
+    }
+    if (value == 0 || this.value == 0) {
+      return ZERO;
+    }
+    if (value == 1) {
+      return mod(modulus);
+    }
+    if (value < 0) {
+      throw new ArithmeticException("multiplyMod unsigned by negative");
+    }
+    return create(toBigInteger().multiply(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus)).intValue());
+  }
+
+  @Override
+  public UInt32 divide(UInt32 value) {
+    if (value.value == 0) {
+      throw new ArithmeticException("divide by zero");
+    }
+    if (value.value == 1) {
+      return this;
+    }
+    return create(toBigInteger().divide(value.toBigInteger()).intValue());
+  }
+
+  @Override
+  public UInt32 divide(int value) {
+    if (value == 0) {
+      throw new ArithmeticException("divide by zero");
+    }
+    if (value < 0) {
+      throw new ArithmeticException("divide unsigned by negative");
+    }
+    if (value == 1) {
+      return this;
+    }
+    if (isPowerOf2(value)) {
+      return shiftRight(log2(value));
+    }
+    return create(toBigInteger().divide(BigInteger.valueOf(value)).intValue());
+  }
+
+  @Override
+  public UInt32 pow(UInt32 exponent) {
+    return create(toBigInteger().modPow(exponent.toBigInteger(), P_2_32).intValue());
+  }
+
+  @Override
+  public UInt32 pow(long exponent) {
+    return create(toBigInteger().modPow(BigInteger.valueOf(exponent), P_2_32).intValue());
+  }
+
+  @Override
+  public UInt32 mod(UInt32 modulus) {
+    if (modulus.isZero()) {
+      throw new ArithmeticException("mod by zero");
+    }
+    return create(toBigInteger().mod(modulus.toBigInteger()).intValue());
+  }
+
+  @Override
+  public UInt32 mod(int modulus) {
+    if (modulus == 0) {
+      throw new ArithmeticException("mod by zero");
+    }
+    if (modulus < 0) {
+      throw new ArithmeticException("mod by negative");
+    }
+    return create(this.value % modulus);
+  }
+
+  /**
+   * Return a bit-wise AND of this value and the supplied value.
+   *
+   * @param value the value to perform the operation with
+   * @return the result of a bit-wise AND
+   */
+  public UInt32 and(UInt32 value) {
+    if (this.value == 0 || value.value == 0) {
+      return ZERO;
+    }
+    return create(this.value & value.value);
+  }
+
+  /**
+   * Return a bit-wise AND of this value and the supplied bytes.
+   *
+   * @param bytes the bytes to perform the operation with
+   * @return the result of a bit-wise AND
+   * @throws IllegalArgumentException if more than 8 bytes are supplied
+   */
+  public UInt32 and(Bytes bytes) {
+    checkArgument(bytes.size() <= 4, "and with more than 4 bytes");
+    if (this.value == 0) {
+      return ZERO;
+    }
+    int value = bytes.toInt();
+    if (value == 0) {
+      return ZERO;
+    }
+    return create(this.value & value);
+  }
+
+  /**
+   * Return a bit-wise OR of this value and the supplied value.
+   *
+   * @param value the value to perform the operation with
+   * @return the result of a bit-wise OR
+   */
+  public UInt32 or(UInt32 value) {
+    return create(this.value | value.value);
+  }
+
+  /**
+   * Return a bit-wise OR of this value and the supplied bytes.
+   *
+   * @param bytes the bytes to perform the operation with
+   * @return the result of a bit-wise OR
+   * @throws IllegalArgumentException if more than 8 bytes are supplied
+   */
+  public UInt32 or(Bytes bytes) {
+    checkArgument(bytes.size() <= 4, "or with more than 4 bytes");
+    return create(this.value | bytes.toInt());
+  }
+
+  /**
+   * Return a bit-wise XOR of this value and the supplied value.
+   *
+   * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left.
+   *
+   * @param value the value to perform the operation with
+   * @return the result of a bit-wise XOR
+   * @throws IllegalArgumentException if more than 8 bytes are supplied
+   */
+  public UInt32 xor(UInt32 value) {
+    return create(this.value ^ value.value);
+  }
+
+  /**
+   * Return a bit-wise XOR of this value and the supplied bytes.
+   *
+   * @param bytes the bytes to perform the operation with
+   * @return the result of a bit-wise XOR
+   * @throws IllegalArgumentException if more than 8 bytes are supplied
+   */
+  public UInt32 xor(Bytes bytes) {
+    checkArgument(bytes.size() <= 4, "xor with more than 4 bytes");
+    return create(this.value ^ bytes.toInt());
+  }
+
+  /**
+   * Return a bit-wise NOT of this value.
+   *
+   * @return the result of a bit-wise NOT
+   */
+  public UInt32 not() {
+    return create(~this.value);
+  }
+
+  /**
+   * Shift all bits in this value to the right.
+   *
+   * @param distance The number of bits to shift by.
+   * @return A value containing the shifted bits.
+   */
+  public UInt32 shiftRight(int distance) {
+    if (distance == 0) {
+      return this;
+    }
+    if (distance >= 32) {
+      return ZERO;
+    }
+    return create(this.value >>> distance);
+  }
+
+  /**
+   * Shift all bits in this value to the left.
+   *
+   * @param distance The number of bits to shift by.
+   * @return A value containing the shifted bits.
+   */
+  public UInt32 shiftLeft(int distance) {
+    if (distance == 0) {
+      return this;
+    }
+    if (distance >= 32) {
+      return ZERO;
+    }
+    return create(this.value << distance);
+  }
+
+  @Override
+  public boolean equals(Object object) {
+    if (object == this) {
+      return true;
+    }
+    if (!(object instanceof UInt32)) {
+      return false;
+    }
+    UInt32 other = (UInt32) object;
+    return this.value == other.value;
+  }
+
+  @Override
+  public int hashCode() {
+    return Long.hashCode(this.value);
+  }
+
+  @Override
+  public int compareTo(UInt32 other) {
+    return Long.compareUnsigned(this.value, other.value);
+  }
+
+  @Override
+  public int intValue() {
+    if (!fitsInt()) {
+      throw new ArithmeticException("Value does not fit a 4 byte int");
+    }
+    return this.value;
+  }
+
+  @Override
+  public long toLong() {
+    if (!fitsLong()) {
+      throw new ArithmeticException("Value does not fit a 8 byte long");
+    }
+    return toBigInteger().longValue();
+  }
+
+  @Override
+  public String toString() {
+    return String.valueOf(value);
+  }
+
+  @Override
+  public BigInteger toBigInteger() {
+    byte[] mag = new byte[4];
+    mag[0] = (byte) ((this.value >>> 24) & 0xFF);
+    mag[1] = (byte) ((this.value >>> 16) & 0xFF);
+    mag[2] = (byte) ((this.value >>> 8) & 0xFF);
+    mag[3] = (byte) (this.value & 0xFF);
+    return new BigInteger(1, mag);
+  }
+
+  @Override
+  public UInt32 toUInt32() {
+    return this;
+  }
+
+  @Override
+  public Bytes toBytes() {
+    MutableBytes bytes = MutableBytes.create(4);
+    bytes.setInt(0, this.value);
+    return bytes;
+  }
+
+  @Override
+  public Bytes toMinimalBytes() {
+    int requiredBytes = 4 - (Integer.numberOfLeadingZeros(this.value) / 8);
+    MutableBytes bytes = MutableBytes.create(requiredBytes);
+    int j = 0;
+    switch (requiredBytes) {
+      case 4:
+        bytes.set(j++, (byte) ((this.value >>> 24) & 0xFF));
+        // fall through
+      case 3:
+        bytes.set(j++, (byte) ((this.value >>> 16) & 0xFF));
+        // fall through
+      case 2:
+        bytes.set(j++, (byte) ((this.value >>> 8) & 0xFF));
+        // fall through
+      case 1:
+        bytes.set(j, (byte) (this.value & 0xFF));
+    }
+    return bytes;
+  }
+
+  @Override
+  public int numberOfLeadingZeros() {
+    return Integer.numberOfLeadingZeros(this.value);
+  }
+
+  @Override
+  public int bitLength() {
+    return 32 - Integer.numberOfLeadingZeros(this.value);
+  }
+
+  private static boolean isPowerOf2(long n) {
+    assert n > 0;
+    return (n & (n - 1)) == 0;
+  }
+
+  private static int log2(int v) {
+    assert v > 0;
+    return 63 - Long.numberOfLeadingZeros(v);
+  }
+}
diff --git a/units/src/main/java/org/apache/tuweni/units/bigints/UInt32Domain.java b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32Domain.java
new file mode 100644
index 0000000..b339c1a
--- /dev/null
+++ b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32Domain.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.tuweni.units.bigints;
+
+import com.google.common.collect.DiscreteDomain;
+
+/**
+ * A {@link DiscreteDomain} over {@link UInt32}.
+ */
+public final class UInt32Domain extends DiscreteDomain<UInt32> {
+
+  @Override
+  public UInt32 next(UInt32 value) {
+    return value.add(1);
+  }
+
+  @Override
+  public UInt32 previous(UInt32 value) {
+    return value.subtract(1);
+  }
+
+  @Override
+  public long distance(UInt32 start, UInt32 end) {
+    boolean negativeDistance = start.compareTo(end) < 0;
+    UInt32 distance = negativeDistance ? end.subtract(start) : start.subtract(end);
+    if (!distance.fitsLong()) {
+      return negativeDistance ? Long.MIN_VALUE : Long.MAX_VALUE;
+    }
+    long distanceLong = distance.toLong();
+    return negativeDistance ? -distanceLong : distanceLong;
+  }
+
+  @Override
+  public UInt32 minValue() {
+    return UInt32.MIN_VALUE;
+  }
+
+  @Override
+  public UInt32 maxValue() {
+    return UInt32.MAX_VALUE;
+  }
+}
diff --git a/units/src/main/java/org/apache/tuweni/units/bigints/UInt32Value.java b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32Value.java
new file mode 100644
index 0000000..bf24643
--- /dev/null
+++ b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32Value.java
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.tuweni.units.bigints;
+
+
+import org.apache.tuweni.bytes.Bytes;
+
+import java.math.BigInteger;
+
+/**
+ * Represents a 32-bit (8 bytes) unsigned integer value.
+ *
+ * <p>
+ * A {@link UInt32Value} is an unsigned integer value whose value can range between 0 and 2^32-1.
+ *
+ * <p>
+ * This interface defines operations for value types with a 32-bit precision range. The methods provided by this
+ * interface take parameters of the same type (and also {@code long}. This provides type safety by ensuring calculations
+ * cannot mix different {@code UInt32Value} types.
+ *
+ * <p>
+ * Where only a pure numerical 32-bit value is required, {@link UInt32} should be used.
+ *
+ * <p>
+ * It is strongly advised to extend {@link BaseUInt32Value} rather than implementing this interface directly. Doing so
+ * provides type safety in that quantities of different units cannot be mixed accidentally.
+ *
+ * @param <T> The concrete type of the value.
+ */
+public interface UInt32Value<T extends UInt32Value<T>> extends Comparable<T> {
+
+  /**
+   * @return True if this is the value 0.
+   */
+  default boolean isZero() {
+    return toBytes().isZero();
+  }
+
+  /**
+   * Returns a value that is {@code (this + value)}.
+   *
+   * @param value The amount to be added to this value.
+   * @return {@code this + value}
+   */
+  T add(T value);
+
+  /**
+   * Returns a value that is {@code (this + value)}.
+   *
+   * @param value the amount to be added to this value
+   * @return {@code this + value}
+   * @throws ArithmeticException if the result of the addition overflows
+   */
+  default T addExact(T value) {
+    T result = add(value);
+    if (compareTo(result) > 0) {
+      throw new ArithmeticException("UInt32 overflow");
+    }
+    return result;
+  }
+
+  /**
+   * Returns a value that is {@code (this + value)}.
+   *
+   * @param value The amount to be added to this value.
+   * @return {@code this + value}
+   */
+  T add(int value);
+
+  /**
+   * Returns a value that is {@code (this + value)}.
+   *
+   * @param value the amount to be added to this value
+   * @return {@code this + value}
+   * @throws ArithmeticException if the result of the addition overflows
+   */
+  default T addExact(int value) {
+    T result = add(value);
+    if ((value > 0 && compareTo(result) > 0) || (value < 0 && compareTo(result) < 0)) {
+      throw new ArithmeticException("UInt32 overflow");
+    }
+    return result;
+  }
+
+  /**
+   * Returns a value equivalent to {@code ((this + value) mod modulus)}.
+   *
+   * @param value The amount to be added to this value.
+   * @param modulus The modulus.
+   * @return {@code (this + value) mod modulus}
+   * @throws ArithmeticException {@code modulus} == 0.
+   */
+  T addMod(T value, UInt32 modulus);
+
+  /**
+   * Returns a value equivalent to {@code ((this + value) mod modulus)}.
+   *
+   * @param value The amount to be added to this value.
+   * @param modulus The modulus.
+   * @return {@code (this + value) mod modulus}
+   * @throws ArithmeticException {@code modulus} == 0.
+   */
+  T addMod(long value, UInt32 modulus);
+
+  /**
+   * Returns a value equivalent to {@code ((this + value) mod modulus)}.
+   *
+   * @param value The amount to be added to this value.
+   * @param modulus The modulus.
+   * @return {@code (this + value) mod modulus}
+   * @throws ArithmeticException {@code modulus} &le; 0.
+   */
+  T addMod(long value, long modulus);
+
+  /**
+   * Returns a value that is {@code (this - value)}.
+   *
+   * @param value The amount to be subtracted from this value.
+   * @return {@code this - value}
+   */
+  T subtract(T value);
+
+  /**
+   * Returns a value that is {@code (this - value)}.
+   *
+   * @param value the amount to be subtracted to this value
+   * @return {@code this - value}
+   * @throws ArithmeticException if the result of the subtraction overflows
+   */
+  default T subtractExact(T value) {
+    T result = subtract(value);
+    if (compareTo(result) < 0) {
+      throw new ArithmeticException("UInt32 overflow");
+    }
+    return result;
+  }
+
+  /**
+   * Returns a value that is {@code (this - value)}.
+   *
+   * @param value The amount to be subtracted from this value.
+   * @return {@code this - value}
+   */
+  T subtract(int value);
+
+  /**
+   * Returns a value that is {@code (this - value)}.
+   *
+   * @param value the amount to be subtracted to this value
+   * @return {@code this - value}
+   * @throws ArithmeticException if the result of the subtraction overflows
+   */
+  default T subtractExact(int value) {
+    T result = subtract(value);
+    if ((value > 0 && compareTo(result) < 0) || (value < 0 && compareTo(result) > 0)) {
+      throw new ArithmeticException("UInt32 overflow");
+    }
+    return result;
+  }
+
+  /**
+   * Returns a value that is {@code (this * value)}.
+   *
+   * @param value The amount to multiply this value by.
+   * @return {@code this * value}
+   */
+  T multiply(T value);
+
+  /**
+   * Returns a value that is {@code (this * value)}.
+   *
+   * @param value The amount to multiply this value by.
+   * @return {@code this * value}
+   * @throws ArithmeticException {@code value} &lt; 0.
+   */
+  T multiply(int value);
+
+  /**
+   * Returns a value that is {@code ((this * value) mod modulus)}.
+   *
+   * @param value The amount to multiply this value by.
+   * @param modulus The modulus.
+   * @return {@code (this * value) mod modulus}
+   * @throws ArithmeticException {@code value} &lt; 0 or {@code modulus} == 0.
+   */
+  T multiplyMod(T value, UInt32 modulus);
+
+  /**
+   * Returns a value that is {@code ((this * value) mod modulus)}.
+   *
+   * @param value The amount to multiply this value by.
+   * @param modulus The modulus.
+   * @return {@code (this * value) mod modulus}
+   * @throws ArithmeticException {@code value} &lt; 0 or {@code modulus} == 0.
+   */
+  T multiplyMod(int value, UInt32 modulus);
+
+  /**
+   * Returns a value that is {@code ((this * value) mod modulus)}.
+   *
+   * @param value The amount to multiply this value by.
+   * @param modulus The modulus.
+   * @return {@code (this * value) mod modulus}
+   * @throws ArithmeticException {@code value} &lt; 0 or {@code modulus} &le; 0.
+   */
+  T multiplyMod(int value, int modulus);
+
+  /**
+   * Returns a value that is {@code (this / value)}.
+   *
+   * @param value The amount to divide this value by.
+   * @return {@code this / value}
+   * @throws ArithmeticException {@code value} == 0.
+   */
+  T divide(T value);
+
+  /**
+   * Returns a value that is {@code (this / value)}.
+   *
+   * @param value The amount to divide this value by.
+   * @return {@code this / value}
+   * @throws ArithmeticException {@code value} &le; 0.
+   */
+  T divide(int value);
+
+  /**
+   * Returns a value that is {@code (this<sup>exponent</sup> mod 2<sup>32</sup>)}
+   *
+   * <p>
+   * This calculates an exponentiation over the modulus of {@code 2^32}.
+   *
+   * <p>
+   * Note that {@code exponent} is an {@link UInt32} rather than of the type {@code T}.
+   *
+   * @param exponent The exponent to which this value is to be raised.
+   * @return {@code this<sup>exponent</sup> mod 2<sup>32</sup>}
+   */
+  T pow(UInt32 exponent);
+
+  /**
+   * Returns a value that is {@code (this<sup>exponent</sup> mod 2<sup>32</sup>)}
+   *
+   * <p>
+   * This calculates an exponentiation over the modulus of {@code 2^32}.
+   *
+   * @param exponent The exponent to which this value is to be raised.
+   * @return {@code this<sup>exponent</sup> mod 2<sup>32</sup>}
+   */
+  T pow(long exponent);
+
+  /**
+   * Returns a value that is {@code (this mod modulus)}.
+   *
+   * @param modulus The modulus.
+   * @return {@code this mod modulus}.
+   * @throws ArithmeticException {@code modulus} == 0.
+   */
+  T mod(UInt32 modulus);
+
+  /**
+   * Returns a value that is {@code (this mod modulus)}.
+   *
+   * @param modulus The modulus.
+   * @return {@code this mod modulus}.
+   * @throws ArithmeticException {@code modulus} &le; 0.
+   */
+  T mod(int modulus);
+
+  /**
+   * @return True if this value fits a java {@code int} (i.e. is less or equal to {@code Integer.MAX_VALUE}).
+   */
+  default boolean fitsInt() {
+    return true;
+  }
+
+  /**
+   * @return This value as a java {@code int} assuming it is small enough to fit an {@code int}.
+   * @throws ArithmeticException If the value does not fit an {@code int}, that is if {@code
+   *     !fitsInt()}.
+   */
+  default int intValue() {
+    if (!fitsInt()) {
+      throw new ArithmeticException("Value does not fit a 4 byte int");
+    }
+    return toBytes().getInt(4);
+  }
+
+  /**
+   * @return True if this value fits a java {@code long} (i.e. is less or equal to {@code Long.MAX_VALUE}).
+   */
+  default boolean fitsLong() {
+    return true;
+  }
+
+  /**
+   * @return This value as a java {@code long} assuming it is small enough to fit a {@code long}.
+   * @throws ArithmeticException If the value does not fit a {@code long}, that is if {@code
+   *     !fitsLong()}.
+   */
+  default long toLong() {
+    if (!fitsLong()) {
+      throw new ArithmeticException("Value does not fit a 8 byte long");
+    }
+    return toBytes().getLong(0);
+  }
+
+  /**
+   * @return This value as a {@link BigInteger}.
+   */
+  default BigInteger toBigInteger() {
+    return toBytes().toUnsignedBigInteger();
+  }
+
+  /**
+   * This value represented as an hexadecimal string.
+   *
+   * <p>
+   * Note that this representation includes all the 8 underlying bytes, no matter what the integer actually represents
+   * (in other words, it can have many leading zeros). For a shorter representation that don't include leading zeros,
+   * use {@link #toShortHexString}.
+   *
+   * @return This value represented as an hexadecimal string.
+   */
+  default String toHexString() {
+    return toBytes().toHexString();
+  }
+
+  /** @return This value represented as a minimal hexadecimal string (without any leading zero). */
+  default String toShortHexString() {
+    return toBytes().toShortHexString();
+  }
+
+  /**
+   * Convert this value to a {@link UInt32}.
+   *
+   * @return This value as a {@link UInt32}.
+   */
+  UInt32 toUInt32();
+
+  /**
+   * @return The value as bytes.
+   */
+  Bytes toBytes();
+
+  /**
+   * @return The value as bytes without any leading zero bytes.
+   */
+  Bytes toMinimalBytes();
+
+  /**
+   * @return the number of zero bits preceding the highest-order ("leftmost") one-bit in the binary representation of
+   *         this value, or 32 if the value is equal to zero.
+   */
+  default int numberOfLeadingZeros() {
+    return toBytes().numberOfLeadingZeros();
+  }
+
+  /**
+   * @return The number of bits following and including the highest-order ("leftmost") one-bit in the binary
+   *         representation of this value, or zero if all bits are zero.
+   */
+  default int bitLength() {
+    return toBytes().bitLength();
+  }
+}
diff --git a/units/src/main/java/org/apache/tuweni/units/bigints/UInt32ValueDomain.java b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32ValueDomain.java
new file mode 100644
index 0000000..06a43e1
--- /dev/null
+++ b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32ValueDomain.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.tuweni.units.bigints;
+
+import java.util.function.Function;
+
+import com.google.common.collect.DiscreteDomain;
+
+/**
+ * A {@link DiscreteDomain} over a {@link UInt32Value}.
+ */
+public final class UInt32ValueDomain<T extends UInt32Value<T>> extends DiscreteDomain<T> {
+
+  private final T minValue;
+  private final T maxValue;
+
+  /**
+   * @param ctor The constructor for the {@link UInt32Value} type.
+   */
+  public UInt32ValueDomain(Function<UInt32, T> ctor) {
+    this.minValue = ctor.apply(UInt32.MIN_VALUE);
+    this.maxValue = ctor.apply(UInt32.MAX_VALUE);
+  }
+
+  @Override
+  public T next(T value) {
+    return value.add(1);
+  }
+
+  @Override
+  public T previous(T value) {
+    return value.subtract(1);
+  }
+
+  @Override
+  public long distance(T start, T end) {
+    boolean negativeDistance = start.compareTo(end) < 0;
+    T distance = negativeDistance ? end.subtract(start) : start.subtract(end);
+    if (!distance.fitsLong()) {
+      return negativeDistance ? Long.MIN_VALUE : Long.MAX_VALUE;
+    }
+    long distanceLong = distance.toLong();
+    return negativeDistance ? -distanceLong : distanceLong;
+  }
+
+  @Override
+  public T minValue() {
+    return minValue;
+  }
+
+  @Override
+  public T maxValue() {
+    return maxValue;
+  }
+}
diff --git a/units/src/main/java/org/apache/tuweni/units/bigints/UInt32s.java b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32s.java
new file mode 100644
index 0000000..b4f3324
--- /dev/null
+++ b/units/src/main/java/org/apache/tuweni/units/bigints/UInt32s.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.tuweni.units.bigints;
+
+/** Static utility methods on UInt32 values. */
+public final class UInt32s {
+  private UInt32s() {}
+
+  /**
+   * Returns the maximum of two UInt32 values.
+   *
+   * @param v1 The first value.
+   * @param v2 The second value.
+   * @return The maximum of {@code v1} and {@code v2}.
+   * @param <T> The concrete type of the two values.
+   */
+  public static <T extends UInt32Value<T>> T max(T v1, T v2) {
+    return (v1.compareTo(v2)) >= 0 ? v1 : v2;
+  }
+
+  /**
+   * Returns the minimum of two UInt32 values.
+   *
+   * @param v1 The first value.
+   * @param v2 The second value.
+   * @return The minimum of {@code v1} and {@code v2}.
+   * @param <T> The concrete type of the two values.
+   */
+  public static <T extends UInt32Value<T>> T min(T v1, T v2) {
+    return (v1.compareTo(v2)) < 0 ? v1 : v2;
+  }
+}
diff --git a/units/src/test/java/org/apache/tuweni/units/bigints/BaseUInt32ValueTest.java b/units/src/test/java/org/apache/tuweni/units/bigints/BaseUInt32ValueTest.java
new file mode 100644
index 0000000..44c0e27
--- /dev/null
+++ b/units/src/test/java/org/apache/tuweni/units/bigints/BaseUInt32ValueTest.java
@@ -0,0 +1,736 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.tuweni.units.bigints;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.tuweni.bytes.Bytes;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+// This test is in a `test` sub-package to ensure that it does not have access to package-private
+// methods within the bigints package, as it should be testing the usage of the public API.
+class BaseUInt32ValueTest {
+
+  private static class Value extends BaseUInt32Value<Value> {
+    static final Value MAX_VALUE = new Value(UInt32.MAX_VALUE);
+
+    Value(UInt32 v) {
+      super(v, Value::new);
+    }
+
+    Value(int v) {
+      super(v, Value::new);
+    }
+  }
+
+  private static Value v(int v) {
+    return new Value(v);
+  }
+
+  private static Value hv(String s) {
+    return new Value(UInt32.fromHexString(s));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addProvider")
+  void add(Value v1, Value v2, Value expected) {
+    assertValueEquals(expected, v1.add(v2));
+  }
+
+  private static Stream<Arguments> addProvider() {
+    return Stream.of(
+        Arguments.of(v(1), v(0), v(1)),
+        Arguments.of(v(5), v(0), v(5)),
+        Arguments.of(v(0), v(1), v(1)),
+        Arguments.of(v(0), v(100), v(100)),
+        Arguments.of(v(2), v(2), v(4)),
+        Arguments.of(v(100), v(90), v(190)),
+        Arguments.of(Value.MAX_VALUE, v(1), v(0)),
+        Arguments.of(Value.MAX_VALUE, v(2), v(1)),
+        Arguments.of(hv("0xFFFFFFF0"), v(1), hv("0xFFFFFFF1")),
+        Arguments.of(hv("0xFFFFFFFE"), v(1), Value.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addUInt32Provider")
+  void addUInt32(Value v1, UInt32 v2, Value expected) {
+    assertValueEquals(expected, v1.add(v2));
+  }
+
+  private static Stream<Arguments> addUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(1), UInt32.ZERO, v(1)),
+        Arguments.of(v(5), UInt32.ZERO, v(5)),
+        Arguments.of(v(0), UInt32.ONE, v(1)),
+        Arguments.of(v(0), UInt32.valueOf(100), v(100)),
+        Arguments.of(v(2), UInt32.valueOf(2), v(4)),
+        Arguments.of(v(100), UInt32.valueOf(90), v(190)),
+        Arguments.of(Value.MAX_VALUE, UInt32.valueOf(1), v(0)),
+        Arguments.of(Value.MAX_VALUE, UInt32.valueOf(2), v(1)),
+        Arguments.of(hv("0xFFFFFFF0"), UInt32.valueOf(1), hv("0xFFFFFFF1")),
+        Arguments.of(hv("0xFFFFFFFE"), UInt32.valueOf(1), Value.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addLongProvider")
+  void addLong(Value v1, int v2, Value expected) {
+    assertValueEquals(expected, v1.add(v2));
+  }
+
+  private static Stream<Arguments> addLongProvider() {
+    return Stream.of(
+        Arguments.of(v(1), 0, v(1)),
+        Arguments.of(v(5), 0, v(5)),
+        Arguments.of(v(0), 1, v(1)),
+        Arguments.of(v(0), 100, v(100)),
+        Arguments.of(v(2), 2, v(4)),
+        Arguments.of(v(100), 90, v(190)),
+        Arguments.of(Value.MAX_VALUE, 1, v(0)),
+        Arguments.of(Value.MAX_VALUE, 2, v(1)),
+        Arguments.of(hv("0xFFFFFFF0"), 1, hv("0xFFFFFFF1")),
+        Arguments.of(hv("0xFFFFFFFE"), 1, Value.MAX_VALUE),
+        Arguments.of(v(10), -5, v(5)),
+        Arguments.of(v(0), -1, Value.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addModProvider")
+  void addMod(Value v1, Value v2, UInt32 m, Value expected) {
+    assertValueEquals(expected, v1.addMod(v2, m));
+  }
+
+  private static Stream<Arguments> addModProvider() {
+    return Stream.of(
+        Arguments.of(v(0), v(1), UInt32.valueOf(2), v(1)),
+        Arguments.of(v(1), v(1), UInt32.valueOf(2), v(0)),
+        Arguments.of(Value.MAX_VALUE.subtract(2), v(1), UInt32.MAX_VALUE, Value.MAX_VALUE.subtract(1)),
+        Arguments.of(Value.MAX_VALUE.subtract(1), v(1), UInt32.MAX_VALUE, v(0)),
+        Arguments.of(v(2), v(1), UInt32.valueOf(2), v(1)),
+        Arguments.of(v(3), v(2), UInt32.valueOf(6), v(5)),
+        Arguments.of(v(3), v(4), UInt32.valueOf(2), v(1)));
+  }
+
+  @Test
+  void shouldThrowForAddModOfZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(v(1), UInt32.ZERO));
+    assertEquals("addMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("addModUInt32UInt32Provider")
+  void addModUInt32UInt32(Value v1, UInt32 v2, UInt32 m, Value expected) {
+    assertValueEquals(expected, v1.addMod(v2, m));
+  }
+
+  private static Stream<Arguments> addModUInt32UInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), UInt32.ONE, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(1), UInt32.ONE, UInt32.valueOf(2), v(0)),
+        Arguments.of(Value.MAX_VALUE.subtract(2), UInt32.ONE, UInt32.MAX_VALUE, Value.MAX_VALUE.subtract(1)),
+        Arguments.of(Value.MAX_VALUE.subtract(1), UInt32.ONE, UInt32.MAX_VALUE, v(0)),
+        Arguments.of(v(2), UInt32.ONE, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(3), UInt32.valueOf(2), UInt32.valueOf(6), v(5)),
+        Arguments.of(v(3), UInt32.valueOf(4), UInt32.valueOf(2), v(1)));
+  }
+
+  @Test
+  void shouldThrowForAddModLongUInt32OfZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, UInt32.ZERO));
+    assertEquals("addMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("addModLongUInt32Provider")
+  void addModLongUInt32(Value v1, int v2, UInt32 m, Value expected) {
+    assertValueEquals(expected, v1.addMod(v2, m));
+  }
+
+  private static Stream<Arguments> addModLongUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), 1, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(1), 1, UInt32.valueOf(2), v(0)),
+        Arguments.of(Value.MAX_VALUE.subtract(2), 1, UInt32.MAX_VALUE, Value.MAX_VALUE.subtract(1)),
+        Arguments.of(Value.MAX_VALUE.subtract(1), 1, UInt32.MAX_VALUE, v(0)),
+        Arguments.of(v(2), 1, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(2), -1, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(1), -7, UInt32.valueOf(5), v(4)));
+  }
+
+  @Test
+  void shouldThrowForAddModUInt32UInt32OfZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(UInt32.ONE, UInt32.ZERO));
+    assertEquals("addMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("addModLongLongProvider")
+  void addModLongLong(Value v1, int v2, int m, Value expected) {
+    assertValueEquals(expected, v1.addMod(v2, m));
+  }
+
+  private static Stream<Arguments> addModLongLongProvider() {
+    return Stream.of(Arguments.of(v(0), 1, 2, v(1)), Arguments.of(v(1), 1, 2, v(0)), Arguments.of(v(2), 1, 2, v(1)));
+  }
+
+  @Test
+  void shouldThrowForAddModLongLongOfZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, 0));
+    assertEquals("addMod with zero modulus", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForAddModLongLongOfNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, -5));
+    assertEquals("addMod unsigned with negative modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractProvider")
+  void subtract(Value v1, Value v2, Value expected) {
+    assertValueEquals(expected, v1.subtract(v2));
+  }
+
+  private static Stream<Arguments> subtractProvider() {
+    return Stream.of(
+        Arguments.of(v(1), v(0), v(1)),
+        Arguments.of(v(5), v(0), v(5)),
+        Arguments.of(v(2), v(1), v(1)),
+        Arguments.of(v(100), v(100), v(0)),
+        Arguments.of(v(0), v(1), Value.MAX_VALUE),
+        Arguments.of(v(1), v(2), Value.MAX_VALUE),
+        Arguments.of(Value.MAX_VALUE, v(1), hv("0xFFFFFFFE")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractUInt32Provider")
+  void subtractUInt32(Value v1, UInt32 v2, Value expected) {
+    assertValueEquals(expected, v1.subtract(v2));
+  }
+
+  private static Stream<Arguments> subtractUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(1), UInt32.ZERO, v(1)),
+        Arguments.of(v(5), UInt32.ZERO, v(5)),
+        Arguments.of(v(2), UInt32.ONE, v(1)),
+        Arguments.of(v(100), UInt32.valueOf(100), v(0)),
+        Arguments.of(v(0), UInt32.ONE, Value.MAX_VALUE),
+        Arguments.of(v(1), UInt32.valueOf(2), Value.MAX_VALUE),
+        Arguments.of(Value.MAX_VALUE, UInt32.ONE, hv("0xFFFFFFFE")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractLongProvider")
+  void subtractLong(Value v1, int v2, Value expected) {
+    assertValueEquals(expected, v1.subtract(v2));
+  }
+
+  private static Stream<Arguments> subtractLongProvider() {
+    return Stream.of(
+        Arguments.of(v(1), 0, v(1)),
+        Arguments.of(v(5), 0, v(5)),
+        Arguments.of(v(2), 1, v(1)),
+        Arguments.of(v(100), 100, v(0)),
+        Arguments.of(v(0), 1, Value.MAX_VALUE),
+        Arguments.of(v(1), 2, Value.MAX_VALUE),
+        Arguments.of(Value.MAX_VALUE, 1, hv("0xFFFFFFFE")),
+        Arguments.of(v(0), -1, v(1)),
+        Arguments.of(v(0), -100, v(100)),
+        Arguments.of(v(2), -2, v(4)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyProvider")
+  void multiply(Value v1, Value v2, Value expected) {
+    assertValueEquals(expected, v1.multiply(v2));
+  }
+
+  private static Stream<Arguments> multiplyProvider() {
+    return Stream.of(
+        Arguments.of(v(0), v(2), v(0)),
+        Arguments.of(v(1), v(2), v(2)),
+        Arguments.of(v(2), v(2), v(4)),
+        Arguments.of(v(3), v(2), v(6)),
+        Arguments.of(v(4), v(2), v(8)),
+        Arguments.of(v(10), v(18), v(180)),
+        Arguments.of(v(2), v(8), v(16)),
+        Arguments.of(v(7), v(8), v(56)),
+        Arguments.of(v(8), v(8), v(64)),
+        Arguments.of(v(17), v(8), v(136)),
+        Arguments.of(v(22), v(0), v(0)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyUInt32Provider")
+  void multiplyUInt32(Value v1, UInt32 v2, Value expected) {
+    assertValueEquals(expected, v1.multiply(v2));
+  }
+
+  private static Stream<Arguments> multiplyUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(1), UInt32.valueOf(2), v(2)),
+        Arguments.of(v(2), UInt32.valueOf(2), v(4)),
+        Arguments.of(v(3), UInt32.valueOf(2), v(6)),
+        Arguments.of(v(4), UInt32.valueOf(2), v(8)),
+        Arguments.of(v(10), UInt32.valueOf(18), v(180)),
+        Arguments.of(v(2), UInt32.valueOf(8), v(16)),
+        Arguments.of(v(7), UInt32.valueOf(8), v(56)),
+        Arguments.of(v(8), UInt32.valueOf(8), v(64)),
+        Arguments.of(v(17), UInt32.valueOf(8), v(136)),
+        Arguments.of(v(22), UInt32.ZERO, v(0)),
+        Arguments.of(hv("0xFFFFFFFF"), UInt32.valueOf(2), hv("0xFFFFFFFE")),
+        Arguments.of(hv("0xFFFFFFFF"), UInt32.valueOf(2), hv("0xFFFFFFFE")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyLongProvider")
+  void multiplyLong(Value v1, int v2, Value expected) {
+    assertValueEquals(expected, v1.multiply(v2));
+  }
+
+  private static Stream<Arguments> multiplyLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 2, v(0)),
+        Arguments.of(v(1), 2, v(2)),
+        Arguments.of(v(2), 2, v(4)),
+        Arguments.of(v(3), 2, v(6)),
+        Arguments.of(v(4), 2, v(8)),
+        Arguments.of(v(10), 18, v(180)),
+        Arguments.of(v(2), 8, v(16)),
+        Arguments.of(v(7), 8, v(56)),
+        Arguments.of(v(8), 8, v(64)),
+        Arguments.of(v(17), 8, v(136)),
+        Arguments.of(v(22), 0, v(0)),
+        Arguments.of(hv("0xFFFFFFFF"), 2, hv("0xFFFFFFFE")),
+        Arguments.of(hv("0xFFFFFFFF"), 2, hv("0xFFFFFFFE")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyLongOfNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiply(-5));
+    assertEquals("multiply unsigned by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyModProvider")
+  void multiplyMod(Value v1, Value v2, UInt32 m, Value expected) {
+    assertValueEquals(expected, v1.multiplyMod(v2, m));
+  }
+
+  private static Stream<Arguments> multiplyModProvider() {
+    return Stream.of(
+        Arguments.of(v(0), v(5), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(2), v(3), UInt32.valueOf(7), v(6)),
+        Arguments.of(v(2), v(3), UInt32.valueOf(6), v(0)),
+        Arguments.of(v(2), v(0), UInt32.valueOf(6), v(0)),
+        Arguments.of(hv("0xFFFFFFFE"), v(2), UInt32.MAX_VALUE, hv("0xFFFFFFFD")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyModOfModZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(v(1), UInt32.ZERO));
+    assertEquals("multiplyMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyModUInt32UInt32Provider")
+  void multiplyModUInt32UInt32(Value v1, UInt32 v2, UInt32 m, Value expected) {
+    assertValueEquals(expected, v1.multiplyMod(v2, m));
+  }
+
+  private static Stream<Arguments> multiplyModUInt32UInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), UInt32.valueOf(5), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(2), UInt32.valueOf(3), UInt32.valueOf(7), v(6)),
+        Arguments.of(v(2), UInt32.valueOf(3), UInt32.valueOf(6), v(0)),
+        Arguments.of(v(2), UInt32.ZERO, UInt32.valueOf(6), v(0)),
+        Arguments.of(hv("0xFFFFFFFE"), UInt32.valueOf(2), UInt32.MAX_VALUE, hv("0xFFFFFFFD")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyModUInt32UInt32OfModZero() {
+    Throwable exception =
+        assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(UInt32.valueOf(5), UInt32.ZERO));
+    assertEquals("multiplyMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyModLongUInt32Provider")
+  void multiplyModLongUInt32(Value v1, int v2, UInt32 m, Value expected) {
+    assertValueEquals(expected, v1.multiplyMod(v2, m));
+  }
+
+  private static Stream<Arguments> multiplyModLongUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), 5, UInt32.valueOf(2), v(0)),
+        Arguments.of(v(2), 3, UInt32.valueOf(7), v(6)),
+        Arguments.of(v(2), 3, UInt32.valueOf(6), v(0)),
+        Arguments.of(v(2), 0, UInt32.valueOf(6), v(0)),
+        Arguments.of(hv("0xFFFFFFFE"), 2, UInt32.MAX_VALUE, hv("0xFFFFFFFD")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongUInt32OfModZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(1, UInt32.ZERO));
+    assertEquals("multiplyMod with zero modulus", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongUInt32OfNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(-1, UInt32.valueOf(2)));
+    assertEquals("multiplyMod unsigned by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyModLongLongProvider")
+  void multiplyModLongLong(Value v1, int v2, int m, Value expected) {
+    assertValueEquals(expected, v1.multiplyMod(v2, m));
+  }
+
+  private static Stream<Arguments> multiplyModLongLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 5, 2, v(0)),
+        Arguments.of(v(2), 3, 7, v(6)),
+        Arguments.of(v(2), 3, 6, v(0)),
+        Arguments.of(v(2), 0, 6, v(0)),
+        Arguments.of(hv("0x0FFFFFFE"), 2, Integer.MAX_VALUE, hv("0x1FFFFFFC")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongLongOfModZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, 0));
+    assertEquals("multiplyMod with zero modulus", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongLongOfModNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiplyMod(5, -7));
+    assertEquals("multiplyMod unsigned with negative modulus", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongLongOfNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, 2));
+    assertEquals("multiplyMod unsigned by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("divideProvider")
+  void divide(Value v1, Value v2, Value expected) {
+    assertValueEquals(expected, v1.divide(v2));
+  }
+
+  private static Stream<Arguments> divideProvider() {
+    return Stream.of(
+        Arguments.of(v(0), v(2), v(0)),
+        Arguments.of(v(1), v(2), v(0)),
+        Arguments.of(v(2), v(2), v(1)),
+        Arguments.of(v(3), v(2), v(1)),
+        Arguments.of(v(4), v(2), v(2)),
+        Arguments.of(v(2), v(8), v(0)),
+        Arguments.of(v(7), v(8), v(0)),
+        Arguments.of(v(8), v(8), v(1)),
+        Arguments.of(v(9), v(8), v(1)),
+        Arguments.of(v(17), v(8), v(2)),
+        Arguments.of(v(1024), v(8), v(128)),
+        Arguments.of(v(1026), v(8), v(128)));
+  }
+
+  @Test
+  void shouldThrowForDivideByZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(v(0)));
+    assertEquals("divide by zero", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("divideUInt32Provider")
+  void divideUInt32(Value v1, UInt32 v2, Value expected) {
+    assertValueEquals(expected, v1.divide(v2));
+  }
+
+  private static Stream<Arguments> divideUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(1), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(2), UInt32.valueOf(2), v(1)),
+        Arguments.of(v(3), UInt32.valueOf(2), v(1)),
+        Arguments.of(v(4), UInt32.valueOf(2), v(2)),
+        Arguments.of(v(2), UInt32.valueOf(8), v(0)),
+        Arguments.of(v(7), UInt32.valueOf(8), v(0)),
+        Arguments.of(v(8), UInt32.valueOf(8), v(1)),
+        Arguments.of(v(9), UInt32.valueOf(8), v(1)),
+        Arguments.of(v(17), UInt32.valueOf(8), v(2)),
+        Arguments.of(v(1024), UInt32.valueOf(8), v(128)),
+        Arguments.of(v(1026), UInt32.valueOf(8), v(128)));
+  }
+
+  @Test
+  void shouldThrowForDivideUInt32ByZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(UInt32.ZERO));
+    assertEquals("divide by zero", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("divideLongProvider")
+  void divideLong(Value v1, int v2, Value expected) {
+    assertValueEquals(expected, v1.divide(v2));
+  }
+
+  private static Stream<Arguments> divideLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 2, v(0)),
+        Arguments.of(v(1), 2, v(0)),
+        Arguments.of(v(2), 2, v(1)),
+        Arguments.of(v(3), 2, v(1)),
+        Arguments.of(v(4), 2, v(2)),
+        Arguments.of(v(2), 8, v(0)),
+        Arguments.of(v(7), 8, v(0)),
+        Arguments.of(v(8), 8, v(1)),
+        Arguments.of(v(9), 8, v(1)),
+        Arguments.of(v(17), 8, v(2)),
+        Arguments.of(v(1024), 8, v(128)),
+        Arguments.of(v(1026), 8, v(128)));
+  }
+
+  @Test
+  void shouldThrowForDivideLongByZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(0));
+    assertEquals("divide by zero", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForDivideLongByNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(-5));
+    assertEquals("divide unsigned by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("powUInt32Provider")
+  void powUInt32(Value v1, UInt32 v2, Value expected) {
+    assertValueEquals(expected, v1.pow(v2));
+  }
+
+  private static Stream<Arguments> powUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(2), UInt32.valueOf(2), v(4)),
+        Arguments.of(v(2), UInt32.valueOf(8), v(256)),
+        Arguments.of(v(3), UInt32.valueOf(3), v(27)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("powLongProvider")
+  void powLong(Value v1, int v2, Value expected) {
+    assertValueEquals(expected, v1.pow(v2));
+  }
+
+  private static Stream<Arguments> powLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 2, v(0)),
+        Arguments.of(v(2), 2, v(4)),
+        Arguments.of(v(2), 8, v(256)),
+        Arguments.of(v(3), 3, v(27)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("modUInt32Provider")
+  void modUInt32(Value v1, UInt32 v2, Value expected) {
+    assertValueEquals(expected, v1.mod(v2));
+  }
+
+  private static Stream<Arguments> modUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(1), UInt32.valueOf(2), v(1)),
+        Arguments.of(v(2), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(3), UInt32.valueOf(2), v(1)),
+        Arguments.of(v(0), UInt32.valueOf(8), v(0)),
+        Arguments.of(v(1), UInt32.valueOf(8), v(1)),
+        Arguments.of(v(2), UInt32.valueOf(8), v(2)),
+        Arguments.of(v(3), UInt32.valueOf(8), v(3)),
+        Arguments.of(v(7), UInt32.valueOf(8), v(7)),
+        Arguments.of(v(8), UInt32.valueOf(8), v(0)),
+        Arguments.of(v(9), UInt32.valueOf(8), v(1)),
+        Arguments.of(v(1024), UInt32.valueOf(8), v(0)),
+        Arguments.of(v(1026), UInt32.valueOf(8), v(2)));
+  }
+
+  @Test
+  void shouldThrowForModUInt32ByZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(UInt32.ZERO));
+    assertEquals("mod by zero", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("modLongProvider")
+  void modLong(Value v1, int v2, Value expected) {
+    assertValueEquals(expected, v1.mod(v2));
+  }
+
+  private static Stream<Arguments> modLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 2, v(0)),
+        Arguments.of(v(1), 2, v(1)),
+        Arguments.of(v(2), 2, v(0)),
+        Arguments.of(v(3), 2, v(1)),
+        Arguments.of(v(0), 8, v(0)),
+        Arguments.of(v(1), 8, v(1)),
+        Arguments.of(v(2), 8, v(2)),
+        Arguments.of(v(3), 8, v(3)),
+        Arguments.of(v(7), 8, v(7)),
+        Arguments.of(v(8), 8, v(0)),
+        Arguments.of(v(9), 8, v(1)),
+        Arguments.of(v(1024), 8, v(0)),
+        Arguments.of(v(1026), 8, v(2)));
+  }
+
+  @Test
+  void shouldThrowForModLongByZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(0));
+    assertEquals("mod by zero", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForModLongByNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(-5));
+    assertEquals("mod by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("compareToProvider")
+  void compareTo(Value v1, Value v2, int expected) {
+    assertEquals(expected, v1.compareTo(v2));
+  }
+
+  private static Stream<Arguments> compareToProvider() {
+    return Stream.of(
+        Arguments.of(v(5), v(5), 0),
+        Arguments.of(v(5), v(3), 1),
+        Arguments.of(v(5), v(6), -1),
+        Arguments.of(hv("0x00000000"), hv("0x00000000"), 0),
+        Arguments.of(hv("0xFFFFFFFF"), hv("0xFFFFFFFF"), 0),
+        Arguments.of(hv("0x0000FFFF"), hv("0x0000FFFF"), 0),
+        Arguments.of(hv("0xFFFFFFFF"), hv("0x00000000"), 1),
+        Arguments.of(hv("0x00000000"), hv("0xFFFFFFFF"), -1),
+        Arguments.of(hv("0x0001FFFF"), hv("0x0000FFFF"), 1),
+        Arguments.of(hv("0x0000FFFE"), hv("0x0000FFFF"), -1));
+  }
+
+  @ParameterizedTest
+  @MethodSource("toBytesProvider")
+  void toBytesTest(Value value, Bytes expected) {
+    assertEquals(expected, value.toBytes());
+  }
+
+  private static Stream<Arguments> toBytesProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x00"), Bytes.fromHexString("0x00000000")),
+        Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("toMinimalBytesProvider")
+  void toMinimalBytesTest(Value value, Bytes expected) {
+    assertEquals(expected, value.toMinimalBytes());
+  }
+
+  private static Stream<Arguments> toMinimalBytesProvider() {
+    return Stream
+        .of(Arguments.of(hv("0x00"), Bytes.EMPTY), Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("numberOfLeadingZerosProvider")
+  void numberOfLeadingZeros(Value value, int expected) {
+    assertEquals(expected, value.numberOfLeadingZeros());
+  }
+
+  private static Stream<Arguments> numberOfLeadingZerosProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x00"), 32),
+        Arguments.of(hv("0x01"), 31),
+        Arguments.of(hv("0x02"), 30),
+        Arguments.of(hv("0x03"), 30),
+        Arguments.of(hv("0x0F"), 28),
+        Arguments.of(hv("0x8F"), 24));
+  }
+
+  @ParameterizedTest
+  @MethodSource("bitLengthProvider")
+  void bitLength(Value value, int expected) {
+    assertEquals(expected, value.bitLength());
+  }
+
+  private static Stream<Arguments> bitLengthProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x00"), 0),
+        Arguments.of(hv("0x01"), 1),
+        Arguments.of(hv("0x02"), 2),
+        Arguments.of(hv("0x03"), 2),
+        Arguments.of(hv("0x0F"), 4),
+        Arguments.of(hv("0x8F"), 8));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addExactProvider")
+  void addExact(Value value, Value operand) {
+    assertThrows(ArithmeticException.class, () -> value.addExact(operand));
+  }
+
+  private static Stream<Arguments> addExactProvider() {
+    return Stream.of(Arguments.of(Value.MAX_VALUE, v(1)), Arguments.of(Value.MAX_VALUE, Value.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addExactLongProvider")
+  void addExactLong(Value value, int operand) {
+    assertThrows(ArithmeticException.class, () -> value.addExact(operand));
+  }
+
+  private static Stream<Arguments> addExactLongProvider() {
+    return Stream
+        .of(Arguments.of(Value.MAX_VALUE, 3), Arguments.of(Value.MAX_VALUE, Integer.MAX_VALUE), Arguments.of(v(0), -1));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractExactProvider")
+  void subtractExact(Value value, Value operand) {
+    assertThrows(ArithmeticException.class, () -> value.subtractExact(operand));
+  }
+
+  private static Stream<Arguments> subtractExactProvider() {
+    return Stream.of(Arguments.of(v(0), v(1)), Arguments.of(v(0), Value.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractExactLongProvider")
+  void subtractExactLong(Value value, int operand) {
+    assertThrows(ArithmeticException.class, () -> value.subtractExact(operand));
+  }
+
+  private static Stream<Arguments> subtractExactLongProvider() {
+    return Stream.of(Arguments.of(v(0), 1), Arguments.of(v(0), Integer.MAX_VALUE), Arguments.of(Value.MAX_VALUE, -1));
+  }
+
+  private void assertValueEquals(Value expected, Value actual) {
+    String msg = String.format("Expected %s but got %s", expected.toHexString(), actual.toHexString());
+    assertEquals(expected, actual, msg);
+  }
+}
diff --git a/units/src/test/java/org/apache/tuweni/units/bigints/UInt32Test.java b/units/src/test/java/org/apache/tuweni/units/bigints/UInt32Test.java
new file mode 100644
index 0000000..b9d38f4
--- /dev/null
+++ b/units/src/test/java/org/apache/tuweni/units/bigints/UInt32Test.java
@@ -0,0 +1,746 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You 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.
+ */
+package org.apache.tuweni.units.bigints;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.tuweni.bytes.Bytes;
+
+import java.math.BigInteger;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class UInt32Test {
+
+  private static UInt32 v(int v) {
+    return UInt32.valueOf(v);
+  }
+
+  private static UInt32 hv(String s) {
+    return UInt32.fromHexString(s);
+  }
+
+  @Test
+  void valueOfInt() {
+    assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(-1));
+    assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(Integer.MIN_VALUE));
+    assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(~0));
+  }
+
+  @Test
+  void valueOfBigInteger() {
+    assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(BigInteger.valueOf(-1)));
+    assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(BigInteger.valueOf(2).pow(32)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addProvider")
+  void add(UInt32 v1, UInt32 v2, UInt32 expected) {
+    assertValueEquals(expected, v1.add(v2));
+  }
+
+  private static Stream<Arguments> addProvider() {
+    return Stream.of(
+        Arguments.of(v(1), v(0), v(1)),
+        Arguments.of(v(5), v(0), v(5)),
+        Arguments.of(v(0), v(1), v(1)),
+        Arguments.of(v(0), v(100), v(100)),
+        Arguments.of(v(2), v(2), v(4)),
+        Arguments.of(v(100), v(90), v(190)),
+        Arguments.of(UInt32.MAX_VALUE, v(1), v(0)),
+        Arguments.of(UInt32.MAX_VALUE, v(2), v(1)),
+        Arguments.of(hv("0xFFFFFFF0"), v(1), hv("0xFFFFFFF1")),
+        Arguments.of(hv("0xFFFFFFFE"), v(1), UInt32.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addLongProvider")
+  void addLong(UInt32 v1, int v2, UInt32 expected) {
+    assertValueEquals(expected, v1.add(v2));
+  }
+
+  private static Stream<Arguments> addLongProvider() {
+    return Stream.of(
+        Arguments.of(v(1), 0, v(1)),
+        Arguments.of(v(5), 0, v(5)),
+        Arguments.of(v(0), 1, v(1)),
+        Arguments.of(v(0), 100, v(100)),
+        Arguments.of(v(2), 2, v(4)),
+        Arguments.of(v(100), 90, v(190)),
+        Arguments.of(UInt32.MAX_VALUE, 1, v(0)),
+        Arguments.of(UInt32.MAX_VALUE, 2, v(1)),
+        Arguments.of(hv("0xFFFFFFF0"), 1, hv("0xFFFFFFF1")),
+        Arguments.of(hv("0xFFFFFFFE"), 1, UInt32.MAX_VALUE),
+        Arguments.of(v(10), -5, v(5)),
+        Arguments.of(v(0), -1, UInt32.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addModProvider")
+  void addMod(UInt32 v1, UInt32 v2, UInt32 m, UInt32 expected) {
+    assertValueEquals(expected, v1.addMod(v2, m));
+  }
+
+  private static Stream<Arguments> addModProvider() {
+    return Stream.of(
+        Arguments.of(v(0), v(1), UInt32.valueOf(2), v(1)),
+        Arguments.of(v(1), v(1), UInt32.valueOf(2), v(0)),
+        Arguments.of(UInt32.MAX_VALUE.subtract(2), v(1), UInt32.MAX_VALUE, UInt32.MAX_VALUE.subtract(1)),
+        Arguments.of(UInt32.MAX_VALUE.subtract(1), v(1), UInt32.MAX_VALUE, v(0)),
+        Arguments.of(v(2), v(1), UInt32.valueOf(2), v(1)),
+        Arguments.of(v(3), v(2), UInt32.valueOf(6), v(5)),
+        Arguments.of(v(3), v(4), UInt32.valueOf(2), v(1)));
+  }
+
+  @Test
+  void shouldThrowForAddModOfZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(v(1), UInt32.ZERO));
+    assertEquals("addMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("addModUInt32UInt32Provider")
+  void addModUInt32UInt32(UInt32 v1, UInt32 v2, UInt32 m, UInt32 expected) {
+    assertValueEquals(expected, v1.addMod(v2, m));
+  }
+
+  private static Stream<Arguments> addModUInt32UInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), UInt32.ONE, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(1), UInt32.ONE, UInt32.valueOf(2), v(0)),
+        Arguments.of(UInt32.MAX_VALUE.subtract(2), UInt32.ONE, UInt32.MAX_VALUE, UInt32.MAX_VALUE.subtract(1)),
+        Arguments.of(UInt32.MAX_VALUE.subtract(1), UInt32.ONE, UInt32.MAX_VALUE, v(0)),
+        Arguments.of(v(2), UInt32.ONE, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(3), UInt32.valueOf(2), UInt32.valueOf(6), v(5)),
+        Arguments.of(v(3), UInt32.valueOf(4), UInt32.valueOf(2), v(1)));
+  }
+
+  @Test
+  void shouldThrowForAddModLongUInt32OfZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, UInt32.ZERO));
+    assertEquals("addMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("addModLongUInt32Provider")
+  void addModLongUInt32(UInt32 v1, long v2, UInt32 m, UInt32 expected) {
+    assertValueEquals(expected, v1.addMod(v2, m));
+  }
+
+  private static Stream<Arguments> addModLongUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), 1, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(1), 1, UInt32.valueOf(2), v(0)),
+        Arguments.of(UInt32.MAX_VALUE.subtract(2), 1, UInt32.MAX_VALUE, UInt32.MAX_VALUE.subtract(1)),
+        Arguments.of(UInt32.MAX_VALUE.subtract(1), 1, UInt32.MAX_VALUE, v(0)),
+        Arguments.of(v(2), 1, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(2), -1, UInt32.valueOf(2), v(1)),
+        Arguments.of(v(1), -7, UInt32.valueOf(5), v(4)));
+  }
+
+  @Test
+  void shouldThrowForAddModUInt32UInt32OfZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(UInt32.ONE, UInt32.ZERO));
+    assertEquals("addMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("addModLongLongProvider")
+  void addModLongLong(UInt32 v1, long v2, long m, UInt32 expected) {
+    assertValueEquals(expected, v1.addMod(v2, m));
+  }
+
+  private static Stream<Arguments> addModLongLongProvider() {
+    return Stream.of(Arguments.of(v(0), 1, 2, v(1)), Arguments.of(v(1), 1, 2, v(0)), Arguments.of(v(2), 1, 2, v(1)));
+  }
+
+  @Test
+  void shouldThrowForAddModLongLongOfZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, 0));
+    assertEquals("addMod with zero modulus", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForAddModLongLongOfNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, -5));
+    assertEquals("addMod unsigned with negative modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractProvider")
+  void subtract(UInt32 v1, UInt32 v2, UInt32 expected) {
+    assertValueEquals(expected, v1.subtract(v2));
+  }
+
+  private static Stream<Arguments> subtractProvider() {
+    return Stream.of(
+        Arguments.of(v(1), v(0), v(1)),
+        Arguments.of(v(5), v(0), v(5)),
+        Arguments.of(v(2), v(1), v(1)),
+        Arguments.of(v(100), v(100), v(0)),
+        Arguments.of(v(0), v(1), UInt32.MAX_VALUE),
+        Arguments.of(v(1), v(2), UInt32.MAX_VALUE),
+        Arguments.of(UInt32.MAX_VALUE, v(1), hv("0xFFFFFFFE")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractLongProvider")
+  void subtractLong(UInt32 v1, int v2, UInt32 expected) {
+    assertValueEquals(expected, v1.subtract(v2));
+  }
+
+  private static Stream<Arguments> subtractLongProvider() {
+    return Stream.of(
+        Arguments.of(v(1), 0, v(1)),
+        Arguments.of(v(5), 0, v(5)),
+        Arguments.of(v(2), 1, v(1)),
+        Arguments.of(v(100), 100, v(0)),
+        Arguments.of(v(0), 1, UInt32.MAX_VALUE),
+        Arguments.of(v(1), 2, UInt32.MAX_VALUE),
+        Arguments.of(UInt32.MAX_VALUE, 1, hv("0xFFFFFFFE")),
+        Arguments.of(v(0), -1, v(1)),
+        Arguments.of(v(0), -100, v(100)),
+        Arguments.of(v(2), -2, v(4)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyProvider")
+  void multiply(UInt32 v1, UInt32 v2, UInt32 expected) {
+    assertValueEquals(expected, v1.multiply(v2));
+  }
+
+  private static Stream<Arguments> multiplyProvider() {
+    return Stream.of(
+        Arguments.of(v(0), v(2), v(0)),
+        Arguments.of(v(1), v(2), v(2)),
+        Arguments.of(v(2), v(2), v(4)),
+        Arguments.of(v(3), v(2), v(6)),
+        Arguments.of(v(4), v(2), v(8)),
+        Arguments.of(v(10), v(18), v(180)),
+        Arguments.of(v(2), v(8), v(16)),
+        Arguments.of(v(7), v(8), v(56)),
+        Arguments.of(v(8), v(8), v(64)),
+        Arguments.of(v(17), v(8), v(136)),
+        Arguments.of(v(22), v(0), v(0)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyLongProvider")
+  void multiplyLong(UInt32 v1, int v2, UInt32 expected) {
+    assertValueEquals(expected, v1.multiply(v2));
+  }
+
+  private static Stream<Arguments> multiplyLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 2, v(0)),
+        Arguments.of(v(1), 2, v(2)),
+        Arguments.of(v(2), 2, v(4)),
+        Arguments.of(v(3), 2, v(6)),
+        Arguments.of(v(4), 2, v(8)),
+        Arguments.of(v(10), 18, v(180)),
+        Arguments.of(v(2), 8, v(16)),
+        Arguments.of(v(7), 8, v(56)),
+        Arguments.of(v(8), 8, v(64)),
+        Arguments.of(v(17), 8, v(136)),
+        Arguments.of(v(22), 0, v(0)),
+        Arguments.of(hv("0x0FFFFFFF"), 2, hv("0x1FFFFFFE")),
+        Arguments.of(hv("0xFFFFFFFF"), 2, hv("0xFFFFFFFE")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyLongOfNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiply(-5));
+    assertEquals("multiply unsigned by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyModProvider")
+  void multiplyMod(UInt32 v1, UInt32 v2, UInt32 m, UInt32 expected) {
+    assertValueEquals(expected, v1.multiplyMod(v2, m));
+  }
+
+  private static Stream<Arguments> multiplyModProvider() {
+    return Stream.of(
+        Arguments.of(v(0), v(5), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(2), v(3), UInt32.valueOf(7), v(6)),
+        Arguments.of(v(2), v(3), UInt32.valueOf(6), v(0)),
+        Arguments.of(v(2), v(0), UInt32.valueOf(6), v(0)),
+        Arguments.of(hv("0x0FFFFFFE"), v(2), UInt32.MAX_VALUE, hv("0x1FFFFFFC")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyModOfModZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(v(1), UInt32.ZERO));
+    assertEquals("multiplyMod with zero modulus", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyModLongUInt32Provider")
+  void multiplyModLongUInt32(UInt32 v1, int v2, UInt32 m, UInt32 expected) {
+    assertValueEquals(expected, v1.multiplyMod(v2, m));
+  }
+
+  private static Stream<Arguments> multiplyModLongUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), 5, UInt32.valueOf(2), v(0)),
+        Arguments.of(v(2), 3, UInt32.valueOf(7), v(6)),
+        Arguments.of(v(2), 3, UInt32.valueOf(6), v(0)),
+        Arguments.of(v(2), 0, UInt32.valueOf(6), v(0)),
+        Arguments.of(hv("0x0FFFFFFE"), 2, UInt32.MAX_VALUE, hv("0x1FFFFFFC")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongUInt32OfModZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, UInt32.ZERO));
+    assertEquals("multiplyMod with zero modulus", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongUInt32OfNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, UInt32.valueOf(2)));
+    assertEquals("multiplyMod unsigned by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("multiplyModLongLongProvider")
+  void multiplyModLongLong(UInt32 v1, int v2, int m, UInt32 expected) {
+    assertValueEquals(expected, v1.multiplyMod(v2, m));
+  }
+
+  private static Stream<Arguments> multiplyModLongLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 5, 2, v(0)),
+        Arguments.of(v(2), 3, 7, v(6)),
+        Arguments.of(v(2), 3, 6, v(0)),
+        Arguments.of(v(2), 0, 6, v(0)),
+        Arguments.of(hv("0x0FFFFFFE"), 2, Integer.MAX_VALUE, hv("0x1FFFFFFC")));
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongLongOfModZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, 0));
+    assertEquals("multiplyMod with zero modulus", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongLongOfModNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiplyMod(5, -7));
+    assertEquals("multiplyMod unsigned with negative modulus", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForMultiplyModLongLongOfNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, 2));
+    assertEquals("multiplyMod unsigned by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("divideProvider")
+  void divide(UInt32 v1, UInt32 v2, UInt32 expected) {
+    assertValueEquals(expected, v1.divide(v2));
+  }
+
+  private static Stream<Arguments> divideProvider() {
+    return Stream.of(
+        Arguments.of(v(0), v(2), v(0)),
+        Arguments.of(v(1), v(2), v(0)),
+        Arguments.of(v(2), v(2), v(1)),
+        Arguments.of(v(3), v(2), v(1)),
+        Arguments.of(v(4), v(2), v(2)),
+        Arguments.of(v(2), v(8), v(0)),
+        Arguments.of(v(7), v(8), v(0)),
+        Arguments.of(v(8), v(8), v(1)),
+        Arguments.of(v(9), v(8), v(1)),
+        Arguments.of(v(17), v(8), v(2)),
+        Arguments.of(v(1024), v(8), v(128)),
+        Arguments.of(v(1026), v(8), v(128)));
+  }
+
+  @Test
+  void shouldThrowForDivideByZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(v(0)));
+    assertEquals("divide by zero", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("divideLongProvider")
+  void divideLong(UInt32 v1, int v2, UInt32 expected) {
+    assertValueEquals(expected, v1.divide(v2));
+  }
+
+  private static Stream<Arguments> divideLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 2, v(0)),
+        Arguments.of(v(1), 2, v(0)),
+        Arguments.of(v(2), 2, v(1)),
+        Arguments.of(v(3), 2, v(1)),
+        Arguments.of(v(4), 2, v(2)),
+        Arguments.of(v(2), 8, v(0)),
+        Arguments.of(v(7), 8, v(0)),
+        Arguments.of(v(8), 8, v(1)),
+        Arguments.of(v(9), 8, v(1)),
+        Arguments.of(v(17), 8, v(2)),
+        Arguments.of(v(1024), 8, v(128)),
+        Arguments.of(v(1026), 8, v(128)));
+  }
+
+  @Test
+  void shouldThrowForDivideLongByZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(0));
+    assertEquals("divide by zero", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForDivideLongByNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(-5));
+    assertEquals("divide unsigned by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("powUInt32Provider")
+  void powUInt32(UInt32 v1, UInt32 v2, UInt32 expected) {
+    assertValueEquals(expected, v1.pow(v2));
+  }
+
+  private static Stream<Arguments> powUInt32Provider() {
+    return Stream.of(
+        Arguments.of(v(0), UInt32.valueOf(2), v(0)),
+        Arguments.of(v(2), UInt32.valueOf(2), v(4)),
+        Arguments.of(v(2), UInt32.valueOf(8), v(256)),
+        Arguments.of(v(3), UInt32.valueOf(3), v(27)),
+        Arguments.of(hv("0xFFF0F0F0"), UInt32.valueOf(3), hv("0x19A2F000")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("powLongProvider")
+  void powLong(UInt32 v1, long v2, UInt32 expected) {
+    assertValueEquals(expected, v1.pow(v2));
+  }
+
+  private static Stream<Arguments> powLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 2, v(0)),
+        Arguments.of(v(2), 2, v(4)),
+        Arguments.of(v(2), 8, v(256)),
+        Arguments.of(v(3), 3, v(27)),
+        Arguments.of(hv("0xFFF0F0F0"), 3, hv("0x19A2F000")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("modLongProvider")
+  void modLong(UInt32 v1, int v2, UInt32 expected) {
+    assertValueEquals(expected, v1.mod(v2));
+  }
+
+  private static Stream<Arguments> modLongProvider() {
+    return Stream.of(
+        Arguments.of(v(0), 2, v(0)),
+        Arguments.of(v(1), 2, v(1)),
+        Arguments.of(v(2), 2, v(0)),
+        Arguments.of(v(3), 2, v(1)),
+        Arguments.of(v(0), 8, v(0)),
+        Arguments.of(v(1), 8, v(1)),
+        Arguments.of(v(2), 8, v(2)),
+        Arguments.of(v(3), 8, v(3)),
+        Arguments.of(v(7), 8, v(7)),
+        Arguments.of(v(8), 8, v(0)),
+        Arguments.of(v(9), 8, v(1)),
+        Arguments.of(v(1024), 8, v(0)),
+        Arguments.of(v(1026), 8, v(2)));
+  }
+
+  @Test
+  void shouldThrowForModLongByZero() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(0));
+    assertEquals("mod by zero", exception.getMessage());
+  }
+
+  @Test
+  void shouldThrowForModLongByNegative() {
+    Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(-5));
+    assertEquals("mod by negative", exception.getMessage());
+  }
+
+  @ParameterizedTest
+  @MethodSource("andProvider")
+  void and(UInt32 v1, UInt32 v2, UInt32 expected) {
+    assertValueEquals(expected, v1.and(v2));
+  }
+
+  private static Stream<Arguments> andProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"), hv("0x00000000")),
+        Arguments.of(hv("0x0000FFFF"), hv("0xFFFFFF00"), hv("0x0000FF00")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("orProvider")
+  void or(UInt32 v1, UInt32 v2, UInt32 expected) {
+    assertValueEquals(expected, v1.or(v2));
+  }
+
+  private static Stream<Arguments> orProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"), hv("0xFFFFFFFF")),
+        Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"), hv("0xFFFFFFFF")),
+        Arguments.of(hv("0x000000FF"), hv("0xFFFF0000"), hv("0xFFFF00FF")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("xorProvider")
+  void xor(UInt32 v1, UInt32 v2, UInt32 expected) {
+    assertValueEquals(expected, v1.xor(v2));
+  }
+
+  private static Stream<Arguments> xorProvider() {
+    return Stream.of(
+        Arguments.of(hv("0xFFFFFFFF"), hv("0xFFFFFFFF"), hv("0x00000000")),
+        Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"), hv("0xFFFFFFFF")),
+        Arguments.of(hv("0x0000FFFF"), hv("0xFFFFFF00"), hv("0xFFFF00FF")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("notProvider")
+  void not(UInt32 value, UInt32 expected) {
+    assertValueEquals(expected, value.not());
+  }
+
+  private static Stream<Arguments> notProvider() {
+    return Stream.of(
+        Arguments.of(hv("0xFFFFFFFF"), hv("0x00000000")),
+        Arguments.of(hv("0x00000000"), hv("0xFFFFFFFF")),
+        Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("shiftLeftProvider")
+  void shiftLeft(UInt32 value, int distance, UInt32 expected) {
+    assertValueEquals(expected, value.shiftLeft(distance));
+  }
+
+  private static Stream<Arguments> shiftLeftProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x01"), 1, hv("0x02")),
+        Arguments.of(hv("0x01"), 2, hv("0x04")),
+        Arguments.of(hv("0x01"), 8, hv("0x0100")),
+        Arguments.of(hv("0x01"), 9, hv("0x0200")),
+        Arguments.of(hv("0x01"), 16, hv("0x10000")),
+        Arguments.of(hv("0x00FF00"), 4, hv("0x0FF000")),
+        Arguments.of(hv("0x00FF00"), 8, hv("0xFF0000")),
+        Arguments.of(hv("0x00FF00"), 1, hv("0x01FE00")),
+        Arguments.of(hv("0x00000001"), 16, hv("0x00010000")),
+        Arguments.of(hv("0x00000001"), 15, hv("0x00008000")),
+        Arguments.of(hv("0xFFFFFFFF"), 23, hv("0xFF800000")),
+        Arguments.of(hv("0x0000FFFF"), 18, hv("0xFFFC0000")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("shiftRightProvider")
+  void shiftRight(UInt32 value, int distance, UInt32 expected) {
+    assertValueEquals(expected, value.shiftRight(distance));
+  }
+
+  private static Stream<Arguments> shiftRightProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x01"), 1, hv("0x00")),
+        Arguments.of(hv("0x10"), 1, hv("0x08")),
+        Arguments.of(hv("0x10"), 2, hv("0x04")),
+        Arguments.of(hv("0x10"), 8, hv("0x00")),
+        Arguments.of(hv("0x1000"), 4, hv("0x0100")),
+        Arguments.of(hv("0x1000"), 5, hv("0x0080")),
+        Arguments.of(hv("0x1000"), 8, hv("0x0010")),
+        Arguments.of(hv("0x1000"), 9, hv("0x0008")),
+        Arguments.of(hv("0x1000"), 16, hv("0x0000")),
+        Arguments.of(hv("0x00FF00"), 4, hv("0x000FF0")),
+        Arguments.of(hv("0x00FF00"), 8, hv("0x0000FF")),
+        Arguments.of(hv("0x00FF00"), 1, hv("0x007F80")),
+        Arguments.of(hv("0x100000"), 16, hv("0x000010")),
+        Arguments.of(hv("0x100000"), 15, hv("0x000020")),
+        Arguments.of(hv("0xFFFFFFFF"), 23, hv("0x000001FF")),
+        Arguments.of(hv("0xFFFFFFFF"), 202, hv("0x00000000")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("intValueProvider")
+  void intValue(UInt32 value, int expected) {
+    assertEquals(expected, value.intValue());
+  }
+
+  private static Stream<Arguments> intValueProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x"), 0),
+        Arguments.of(hv("0x00"), 0),
+        Arguments.of(hv("0x00000000"), 0),
+        Arguments.of(hv("0x01"), 1),
+        Arguments.of(hv("0x0001"), 1),
+        Arguments.of(hv("0x000001"), 1),
+        Arguments.of(hv("0x00000001"), 1),
+        Arguments.of(hv("0x0100"), 256),
+        Arguments.of(hv("0x000100"), 256),
+        Arguments.of(hv("0x00000100"), 256));
+  }
+
+  @ParameterizedTest
+  @MethodSource("longValueProvider")
+  void longValue(UInt32 value, long expected) {
+    assertEquals(expected, value.toLong());
+  }
+
+  private static Stream<Arguments> longValueProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x"), 0L),
+        Arguments.of(hv("0x00"), 0L),
+        Arguments.of(hv("0x00000000"), 0L),
+        Arguments.of(hv("0x01"), 1L),
+        Arguments.of(hv("0x0001"), 1L),
+        Arguments.of(hv("0x000001"), 1L),
+        Arguments.of(hv("0x0100"), 256L),
+        Arguments.of(hv("0x000100"), 256L),
+        Arguments.of(hv("0x00000100"), 256L),
+        Arguments.of(hv("0xFFFFFFFF"), (1L << 32) - 1));
+  }
+
+  @ParameterizedTest
+  @MethodSource("compareToProvider")
+  void compareTo(UInt32 v1, UInt32 v2, int expected) {
+    assertEquals(expected, v1.compareTo(v2));
+  }
+
+  private static Stream<Arguments> compareToProvider() {
+    return Stream.of(
+        Arguments.of(v(5), v(5), 0),
+        Arguments.of(v(5), v(3), 1),
+        Arguments.of(v(5), v(6), -1),
+        Arguments.of(hv("0x00000000"), hv("0x00000000"), 0),
+        Arguments.of(hv("0xFFFFFFFF"), hv("0xFFFFFFFF"), 0),
+        Arguments.of(hv("0x0000FFFF"), hv("0x0000FFFF"), 0),
+        Arguments.of(hv("0xFFFFFFFF"), hv("0x00000000"), 1),
+        Arguments.of(hv("0x00000000"), hv("0xFFFFFFFF"), -1),
+        Arguments.of(hv("0x0001FFFF"), hv("0x0000FFFF"), 1),
+        Arguments.of(hv("0x0000FFFE"), hv("0x0000FFFF"), -1));
+  }
+
+  @ParameterizedTest
+  @MethodSource("toBytesProvider")
+  void toBytesTest(UInt32 value, Bytes expected) {
+    assertEquals(expected, value.toBytes());
+  }
+
+  private static Stream<Arguments> toBytesProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x00"), Bytes.fromHexString("0x00000000")),
+        Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")),
+        Arguments.of(hv("0xf10000ab"), Bytes.fromHexString("0xF10000AB")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("toMinimalBytesProvider")
+  void toMinimalBytesTest(UInt32 value, Bytes expected) {
+    assertEquals(expected, value.toMinimalBytes());
+  }
+
+  private static Stream<Arguments> toMinimalBytesProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x00"), Bytes.EMPTY),
+        Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")),
+        Arguments.of(hv("0xf10000ab"), Bytes.fromHexString("0xf10000ab")),
+        Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")));
+  }
+
+  @ParameterizedTest
+  @MethodSource("numberOfLeadingZerosProvider")
+  void numberOfLeadingZeros(UInt32 value, int expected) {
+    assertEquals(expected, value.numberOfLeadingZeros());
+  }
+
+  private static Stream<Arguments> numberOfLeadingZerosProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x00"), 32),
+        Arguments.of(hv("0x01"), 31),
+        Arguments.of(hv("0x02"), 30),
+        Arguments.of(hv("0x03"), 30),
+        Arguments.of(hv("0x0F"), 28),
+        Arguments.of(hv("0x8F"), 24),
+        Arguments.of(hv("0x1000000"), 7));
+  }
+
+  @ParameterizedTest
+  @MethodSource("bitLengthProvider")
+  void bitLength(UInt32 value, int expected) {
+    assertEquals(expected, value.bitLength());
+  }
+
+  private static Stream<Arguments> bitLengthProvider() {
+    return Stream.of(
+        Arguments.of(hv("0x00"), 0),
+        Arguments.of(hv("0x01"), 1),
+        Arguments.of(hv("0x02"), 2),
+        Arguments.of(hv("0x03"), 2),
+        Arguments.of(hv("0x0F"), 4),
+        Arguments.of(hv("0x8F"), 8),
+        Arguments.of(hv("0x10000000"), 29));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addExactProvider")
+  void addExact(UInt32 value, UInt32 operand) {
+    assertThrows(ArithmeticException.class, () -> value.addExact(operand));
+  }
+
+  private static Stream<Arguments> addExactProvider() {
+    return Stream.of(Arguments.of(UInt32.MAX_VALUE, v(1)), Arguments.of(UInt32.MAX_VALUE, UInt32.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("addExactLongProvider")
+  void addExactLong(UInt32 value, int operand) {
+    assertThrows(ArithmeticException.class, () -> value.addExact(operand));
+  }
+
+  private static Stream<Arguments> addExactLongProvider() {
+    return Stream.of(
+        Arguments.of(UInt32.MAX_VALUE, 3),
+        Arguments.of(UInt32.MAX_VALUE, Integer.MAX_VALUE),
+        Arguments.of(v(0), -1));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractExactProvider")
+  void subtractExact(UInt32 value, UInt32 operand) {
+    assertThrows(ArithmeticException.class, () -> value.subtractExact(operand));
+  }
+
+  private static Stream<Arguments> subtractExactProvider() {
+    return Stream.of(Arguments.of(v(0), v(1)), Arguments.of(v(0), UInt32.MAX_VALUE));
+  }
+
+  @ParameterizedTest
+  @MethodSource("subtractExactLongProvider")
+  void subtractExactLong(UInt32 value, int operand) {
+    assertThrows(ArithmeticException.class, () -> value.subtractExact(operand));
+  }
+
+  private static Stream<Arguments> subtractExactLongProvider() {
+    return Stream.of(Arguments.of(v(0), 1), Arguments.of(v(0), Integer.MAX_VALUE), Arguments.of(UInt32.MAX_VALUE, -1));
+  }
+
+  private void assertValueEquals(UInt32 expected, UInt32 actual) {
+    String msg = String.format("Expected %s but got %s", expected.toHexString(), actual.toHexString());
+    assertEquals(expected, actual, msg);
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tuweni.apache.org
For additional commands, e-mail: commits-help@tuweni.apache.org