From 7e76ecbb69726778fe9ad35d2378bc970f3e4319 Mon Sep 17 00:00:00 2001 From: Evan Saulpaugh Date: Thu, 26 Sep 2024 10:41:57 -0500 Subject: [PATCH] disallow additional instances of UnitType (except BigDecimalTypes); write message to System.err before throwing IllegalStateException; --- .../com/esaulpaugh/headlong/abi/ABIType.java | 7 +- .../com/esaulpaugh/headlong/abi/Address.java | 2 +- .../esaulpaugh/headlong/abi/AddressType.java | 9 +- .../headlong/abi/BigDecimalType.java | 6 + .../headlong/abi/BigIntegerType.java | 6 + .../esaulpaugh/headlong/abi/BooleanType.java | 3 + .../com/esaulpaugh/headlong/abi/ByteType.java | 3 + .../com/esaulpaugh/headlong/abi/IntType.java | 3 + .../com/esaulpaugh/headlong/abi/LongType.java | 6 + .../esaulpaugh/headlong/abi/TypeFactory.java | 84 +---------- .../com/esaulpaugh/headlong/abi/UnitType.java | 137 ++++++++++++++++++ .../esaulpaugh/headlong/abi/EqualsTest.java | 19 ++- 12 files changed, 196 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/esaulpaugh/headlong/abi/ABIType.java b/src/main/java/com/esaulpaugh/headlong/abi/ABIType.java index 9d1f1cdb..e8b7105b 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/ABIType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/ABIType.java @@ -83,7 +83,8 @@ public abstract class ABIType { ) || (c == ByteType.class && /* enforce singleton */ ByteType.INSTANCE == null) ; if (!permitted) { - throw new IllegalStateException ("lol no"); + System.err.println("unexpected instance creation rejected: " + c.getName()); + throw illegalState("class not permitted"); } } @@ -358,4 +359,8 @@ static String padLabel(int leftPadding, String unpadded) { } return padded.toString(); } + + static IllegalStateException illegalState(String msg) { + return new IllegalStateException(msg); + } } diff --git a/src/main/java/com/esaulpaugh/headlong/abi/Address.java b/src/main/java/com/esaulpaugh/headlong/abi/Address.java index 46abfd95..6c2980b7 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/Address.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/Address.java @@ -29,7 +29,7 @@ public final class Address { private static final int PREFIX_LEN = 2; - private static final int ADDRESS_DATA_BYTES = TypeFactory.ADDRESS_BIT_LEN / Byte.SIZE; + private static final int ADDRESS_DATA_BYTES = 160 / Byte.SIZE; private static final int ADDRESS_HEX_CHARS = ADDRESS_DATA_BYTES * FastHex.CHARS_PER_BYTE; private static final int ADDRESS_LEN_CHARS = PREFIX_LEN + ADDRESS_HEX_CHARS; private static final int HEX_RADIX = 16; diff --git a/src/main/java/com/esaulpaugh/headlong/abi/AddressType.java b/src/main/java/com/esaulpaugh/headlong/abi/AddressType.java index 602857c8..e8cc5317 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/AddressType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/AddressType.java @@ -20,12 +20,17 @@ /** The {@link ABIType} for {@link Address}. Corresponds to the "address" type. */ public final class AddressType extends UnitType
{ + private static final int ADDRESS_BIT_LEN = 160; + static final AddressType INSTANCE = new AddressType(); + static { + UnitType.ensureInitialized(); + } - private static final BigIntegerType ADDRESS_INNER = new BigIntegerType("ADDRESS_INNER", TypeFactory.ADDRESS_BIT_LEN, true); + private static final BigIntegerType ADDRESS_INNER = new BigIntegerType("ADDRESS_INNER", ADDRESS_BIT_LEN, true); private AddressType() { - super("address", Address.class, TypeFactory.ADDRESS_BIT_LEN, true); + super("address", Address.class, ADDRESS_BIT_LEN, true); } @Override diff --git a/src/main/java/com/esaulpaugh/headlong/abi/BigDecimalType.java b/src/main/java/com/esaulpaugh/headlong/abi/BigDecimalType.java index 4c169aba..2c73ca13 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/BigDecimalType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/BigDecimalType.java @@ -21,6 +21,12 @@ /** Represents a decimal type such as fixed or ufixed. */ public final class BigDecimalType extends UnitType { + static final Object __ = ""; + + static { + UnitType.ensureInitialized(); + } + final int scale; BigDecimalType(String canonicalTypeString, int bitLength, int scale, boolean unsigned) { diff --git a/src/main/java/com/esaulpaugh/headlong/abi/BigIntegerType.java b/src/main/java/com/esaulpaugh/headlong/abi/BigIntegerType.java index b1910564..e4a78980 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/BigIntegerType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/BigIntegerType.java @@ -21,6 +21,12 @@ /** Represents an integer type such as uint64 or int256. */ public final class BigIntegerType extends UnitType { + static final Object __ = ""; + + static { + UnitType.ensureInitialized(); + } + BigIntegerType(String canonicalType, int bitLength, boolean unsigned) { super(canonicalType, BigInteger.class, bitLength, unsigned); } diff --git a/src/main/java/com/esaulpaugh/headlong/abi/BooleanType.java b/src/main/java/com/esaulpaugh/headlong/abi/BooleanType.java index 84b15c4c..b5fc63c0 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/BooleanType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/BooleanType.java @@ -21,6 +21,9 @@ public final class BooleanType extends UnitType { static final BooleanType INSTANCE = new BooleanType(); + static { + UnitType.ensureInitialized(); + } private static final byte[] BOOLEAN_FALSE = new byte[UNIT_LENGTH_BYTES]; private static final byte[] BOOLEAN_TRUE = new byte[UNIT_LENGTH_BYTES]; diff --git a/src/main/java/com/esaulpaugh/headlong/abi/ByteType.java b/src/main/java/com/esaulpaugh/headlong/abi/ByteType.java index 2d210a8e..15bb908d 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/ByteType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/ByteType.java @@ -21,6 +21,9 @@ public final class ByteType extends ABIType { static final ByteType INSTANCE = new ByteType(); + static { + UnitType.ensureInitialized(); + } private ByteType() { super("BYTE", Byte.class, false); diff --git a/src/main/java/com/esaulpaugh/headlong/abi/IntType.java b/src/main/java/com/esaulpaugh/headlong/abi/IntType.java index 720eb02c..1d7340d8 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/IntType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/IntType.java @@ -22,6 +22,9 @@ public final class IntType extends UnitType { static final IntType UINT21 = new IntType("uint21", 21, true); // small bit length for Denial-of-Service protection static final IntType UINT31 = new IntType("uint31", 31, true); + static { + UnitType.ensureInitialized(); + } IntType(String canonicalType, int bitLength, boolean unsigned) { super(canonicalType, Integer.class, bitLength, unsigned); diff --git a/src/main/java/com/esaulpaugh/headlong/abi/LongType.java b/src/main/java/com/esaulpaugh/headlong/abi/LongType.java index 4f8bf58b..a889a379 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/LongType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/LongType.java @@ -22,6 +22,12 @@ /** Represents a long integer type such as int40, int64, uint32, or uint56. */ public final class LongType extends UnitType { + static final Object __ = ""; + + static { + UnitType.ensureInitialized(); + } + LongType(String canonicalType, int bitLength, boolean unsigned) { super(canonicalType, Long.class, bitLength, unsigned); } diff --git a/src/main/java/com/esaulpaugh/headlong/abi/TypeFactory.java b/src/main/java/com/esaulpaugh/headlong/abi/TypeFactory.java index 441dc86a..686f9f9d 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/TypeFactory.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/TypeFactory.java @@ -18,95 +18,21 @@ import com.esaulpaugh.headlong.util.Integers; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import static com.esaulpaugh.headlong.abi.ArrayType.DYNAMIC_LENGTH; -import static com.esaulpaugh.headlong.abi.ArrayType.STRING_ARRAY_CLASS; -import static com.esaulpaugh.headlong.abi.ArrayType.STRING_CLASS; /** Creates the appropriate {@link ABIType} object for a given type string. */ public final class TypeFactory { - private TypeFactory() {} - - static final int ADDRESS_BIT_LEN = 160; - - private static final int FIXED_BIT_LEN = 128; - private static final int FIXED_SCALE = 18; - - private static final int FUNCTION_BYTE_LEN = 24; - - private static final int MAX_LENGTH_CHARS = 2_000; - - private static final Map> BASE_TYPE_MAP; - private static final Map> LEGACY_BASE_TYPE_MAP; - static { - final Map> local = new HashMap<>(256); - - // optimized insertion order - local.put("string", new ArrayType("string", STRING_CLASS, ByteType.INSTANCE, DYNAMIC_LENGTH, STRING_ARRAY_CLASS, ABIType.FLAGS_NONE)); - local.put("bool", BooleanType.INSTANCE); - - for (int n = 1; n <= 32; n++) { - mapByteArray(local, "bytes" + n, n); - } - mapByteArray(local, "function", FUNCTION_BYTE_LEN); - mapByteArray(local, "bytes", DYNAMIC_LENGTH); - - for (int n = 8; n <= 24; n += 8) mapInt(local, "uint" + n, n, true); - for (int n = 32; n <= 56; n += 8) mapLong(local, "uint" + n, n, true); - for (int n = 64; n <= 256; n += 8) mapBigInteger(local, "uint" + n, n, true); - - local.put("uint", local.get("uint256")); - - mapBigInteger(local, "int256", 256, false); - local.put("int", local.get("int256")); - - for (int n = 8; n <= 32; n += 8) mapInt(local, "int" + n, n, false); - for (int n = 40; n <= 64; n += 8) mapLong(local, "int" + n, n, false); - for (int n = 72; n < 256; n += 8) mapBigInteger(local, "int" + n, n, false); - - local.put("address", AddressType.INSTANCE); - - local.put("fixed128x18", new BigDecimalType("fixed128x18", FIXED_BIT_LEN, FIXED_SCALE, false)); - local.put("ufixed128x18", new BigDecimalType("ufixed128x18", FIXED_BIT_LEN, FIXED_SCALE, true)); - - local.put("decimal", local.get("int168")); - local.put("fixed", local.get("fixed128x18")); - local.put("ufixed", local.get("ufixed128x18")); - - final Map> localLegacy = new HashMap<>(256); - for (Map.Entry> e : local.entrySet()) { - ABIType value = e.getValue(); - if (value instanceof ArrayType) { - final ArrayType at = value.asArrayType(); - value = new ArrayType<>(at.canonicalType, at.clazz, ByteType.INSTANCE, at.getLength(), at.arrayClass(), ABIType.FLAG_LEGACY_DECODE); - } - localLegacy.put(e.getKey(), value); - } - - BASE_TYPE_MAP = Collections.unmodifiableMap(local); - LEGACY_BASE_TYPE_MAP = Collections.unmodifiableMap(localLegacy); + UnitType.ensureInitialized(); } - private static void mapInt(Map> map, String type, int bitLen, boolean unsigned) { - map.put(type, new IntType(type, bitLen, unsigned)); - } - - private static void mapLong(Map> map, String type, int bitLen, boolean unsigned) { - map.put(type, new LongType(type, bitLen, unsigned)); - } + private static final int MAX_LENGTH_CHARS = 2_000; - private static void mapBigInteger(Map> map, String type, int bitLen, boolean unsigned) { - map.put(type, new BigIntegerType(type, bitLen, unsigned)); - } + static final int ADDRESS_BIT_LEN = 160; - private static void mapByteArray(Map> map, String type, int arrayLen) { - map.put(type, new ArrayType(type, byte[].class, ByteType.INSTANCE, arrayLen, byte[][].class, ABIType.FLAGS_NONE)); - } + private TypeFactory() {} /** * Creates an {@link ABIType}. If the compiler can't infer the return type, use a type witness. @@ -162,7 +88,7 @@ private static ABIType buildUnchecked(final String rawType, final String[] el if (rawType.charAt(0) == '(') { return baseType != null ? baseType : parseTupleType(rawType, elementNames, flags); } else { - ABIType t = ((flags & ABIType.FLAG_LEGACY_DECODE) != 0 ? LEGACY_BASE_TYPE_MAP : BASE_TYPE_MAP).get(rawType); + ABIType t = (flags & ABIType.FLAG_LEGACY_DECODE) != 0 ? UnitType.getLegacy(rawType) : UnitType.get(rawType); return t != null ? t : tryParseFixed(rawType); } } catch (StringIndexOutOfBoundsException ignored) { // e.g. type equals "" or "82]" or "[]" or "[1]" diff --git a/src/main/java/com/esaulpaugh/headlong/abi/UnitType.java b/src/main/java/com/esaulpaugh/headlong/abi/UnitType.java index 0137efb2..db33ce42 100644 --- a/src/main/java/com/esaulpaugh/headlong/abi/UnitType.java +++ b/src/main/java/com/esaulpaugh/headlong/abi/UnitType.java @@ -19,10 +19,20 @@ import java.math.BigInteger; import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import static com.esaulpaugh.headlong.abi.ArrayType.DYNAMIC_LENGTH; +import static com.esaulpaugh.headlong.abi.ArrayType.STRING_ARRAY_CLASS; +import static com.esaulpaugh.headlong.abi.ArrayType.STRING_CLASS; /** Superclass for any 256-bit ("unit") Contract ABI type. Usually numbers or boolean. Not for arrays or tuples. */ public abstract class UnitType extends ABIType { // J generally extends Number or is Boolean + private static final AtomicLong INSTANCE_COUNT = new AtomicLong(0L); + public static final int UNIT_LENGTH_BYTES = 256 / Byte.SIZE; private final long minLong; @@ -34,6 +44,22 @@ public abstract class UnitType extends ABIType { // J generally extends Nu UnitType(String canonicalType, Class clazz, int bitLength, boolean unsigned) { super(canonicalType, clazz, false); + if (!(this instanceof BigDecimalType)) { + // 69 non-BigDecimalType entries in BASE_TYPE_MAP + // - 3 which are only aliases to instances already counted (int, uint, decimal) + // + 0 unique instances in LEGACY_BASE_TYPE_MAP + // + 3 instances not in the maps (uint21, uint31, and ADDRESS_INNER) + // = + final int instanceLimit = 69; + if (INSTANCE_COUNT.incrementAndGet() > instanceLimit) { + INSTANCE_COUNT.decrementAndGet(); + System.err.println("unexpected instance creation rejected by " + UnitType.class.getName()); + throw illegalState("instance not permitted"); + } + } else if (Integers.mod(bitLength, Byte.SIZE) != 0) { + System.err.println("unexpected bit length rejected"); + throw illegalState("bit length not permitted"); + } this.bitLength = bitLength; this.unsigned = unsigned; this.min = minValue(); @@ -172,4 +198,115 @@ final BigInteger decodeValid(ByteBuffer bb, byte[] unitBuffer) { validateBigInt(bi); return bi; } +//====================================================================================================================== + private static final int FIXED_BIT_LEN = 128; + private static final int FIXED_SCALE = 18; + + private static final int FUNCTION_BYTE_LEN = 24; + + private static final Map> BASE_TYPE_MAP = new HashMap<>(256); + private static final Map> LEGACY_BASE_TYPE_MAP = new HashMap<>(256); + + /* called from TypeFactory */ + static ABIType get(String rawType) { + return BASE_TYPE_MAP.get(rawType); + } + + /* called from TypeFactory */ + static ABIType getLegacy(String rawType) { + return LEGACY_BASE_TYPE_MAP.get(rawType); + } + + static void ensureInitialized() { + // trigger initialization of all subclasses + final Integer h = AddressType.INSTANCE.hashCode() + + BooleanType.INSTANCE.hashCode() + + ByteType.INSTANCE.hashCode() + + IntType.UINT21.hashCode() + + IntType.UINT31.hashCode() + + LongType.__.hashCode() + + BigDecimalType.__.hashCode() + + BigIntegerType.__.hashCode(); + initMaps(h); // synchronized + } + + private static synchronized void initMaps(Object h) { + Objects.requireNonNull(h); + + final int empty = 0; + final int full = 108; + final int size = BASE_TYPE_MAP.size(); + final int legacySize = LEGACY_BASE_TYPE_MAP.size(); + if (size != empty || legacySize != empty) { + if (size == full && legacySize == full) { + return; + } + IllegalStateException ise = illegalState("bad map size"); + ise.printStackTrace(); + throw ise; + } + + final Map> map = BASE_TYPE_MAP; + + // optimized insertion order + map.put("string", new ArrayType("string", STRING_CLASS, ByteType.INSTANCE, DYNAMIC_LENGTH, STRING_ARRAY_CLASS, ABIType.FLAGS_NONE)); + map.put("bool", BooleanType.INSTANCE); + + for (int n = 1; n <= 32; n++) { + mapByteArray(map, "bytes" + n, n); + } + mapByteArray(map, "function", FUNCTION_BYTE_LEN); + mapByteArray(map, "bytes", DYNAMIC_LENGTH); + + for (int n = 8; n <= 24; n += 8) mapInt(map, "uint" + n, n, true); + for (int n = 32; n <= 56; n += 8) mapLong(map, "uint" + n, n, true); + for (int n = 64; n <= 256; n += 8) mapBigInteger(map, "uint" + n, n, true); + + map.put("uint", map.get("uint256")); + + mapBigInteger(map, "int256", 256, false); + map.put("int", map.get("int256")); + + for (int n = 8; n <= 32; n += 8) mapInt(map, "int" + n, n, false); + for (int n = 40; n <= 64; n += 8) mapLong(map, "int" + n, n, false); + for (int n = 72; n < 256; n += 8) mapBigInteger(map, "int" + n, n, false); + + map.put("address", AddressType.INSTANCE); + + map.put("fixed128x18", new BigDecimalType("fixed128x18", FIXED_BIT_LEN, FIXED_SCALE, false)); + map.put("ufixed128x18", new BigDecimalType("ufixed128x18", FIXED_BIT_LEN, FIXED_SCALE, true)); + + map.put("decimal", map.get("int168")); + map.put("fixed", map.get("fixed128x18")); + map.put("ufixed", map.get("ufixed128x18")); + + for (Map.Entry> e : map.entrySet()) { + ABIType value = e.getValue(); + if (value instanceof ArrayType) { + final ArrayType at = value.asArrayType(); + value = new ArrayType<>(at.canonicalType, at.clazz, ByteType.INSTANCE, at.getLength(), at.arrayClass(), ABIType.FLAG_LEGACY_DECODE); + } + LEGACY_BASE_TYPE_MAP.put(e.getKey(), value); + } + + if (BASE_TYPE_MAP.size() != full || LEGACY_BASE_TYPE_MAP.size() != full) { + throw new AssertionError(); + } + } + + private static void mapInt(Map> map, String type, int bitLen, boolean unsigned) { + map.put(type, new IntType(type, bitLen, unsigned)); + } + + private static void mapLong(Map> map, String type, int bitLen, boolean unsigned) { + map.put(type, new LongType(type, bitLen, unsigned)); + } + + private static void mapBigInteger(Map> map, String type, int bitLen, boolean unsigned) { + map.put(type, new BigIntegerType(type, bitLen, unsigned)); + } + + private static void mapByteArray(Map> map, String type, int arrayLen) { + map.put(type, new ArrayType(type, byte[].class, ByteType.INSTANCE, arrayLen, byte[][].class, ABIType.FLAGS_NONE)); + } } diff --git a/src/test/java/com/esaulpaugh/headlong/abi/EqualsTest.java b/src/test/java/com/esaulpaugh/headlong/abi/EqualsTest.java index 706d9f99..4eac837f 100644 --- a/src/test/java/com/esaulpaugh/headlong/abi/EqualsTest.java +++ b/src/test/java/com/esaulpaugh/headlong/abi/EqualsTest.java @@ -20,7 +20,6 @@ import com.esaulpaugh.headlong.util.Strings; import com.esaulpaugh.headlong.util.WrappedKeccak; import com.joemelsha.crypto.hash.Keccak; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.lang.reflect.Constructor; @@ -231,27 +230,34 @@ public void testFlagsEquals() { ); } + private static final String UNEXPECTED_CLASS = "class not permitted"; + @Test public void testSubclassingConstraints() throws Throwable { + // should cause the following to print to System.err: +// unexpected instance creation rejected: com.esaulpaugh.headlong.abi.AddressType +// unexpected instance creation rejected: com.esaulpaugh.headlong.abi.BooleanType +// unexpected instance creation rejected: com.esaulpaugh.headlong.abi.ByteType +// unexpected instance creation rejected: com.esaulpaugh.headlong.abi.EqualsTest$1 { final Constructor constructor = AddressType.class.getDeclaredConstructor(); constructor.setAccessible(true); - assertThrownWithCause(InvocationTargetException.class, IllegalStateException.class, "lol no", constructor::newInstance); + assertThrownWithCause(InvocationTargetException.class, IllegalStateException.class, UNEXPECTED_CLASS, constructor::newInstance); } { final Constructor constructor = BooleanType.class.getDeclaredConstructor(); constructor.setAccessible(true); - assertThrownWithCause(InvocationTargetException.class, IllegalStateException.class, "lol no", constructor::newInstance); + assertThrownWithCause(InvocationTargetException.class, IllegalStateException.class, UNEXPECTED_CLASS, constructor::newInstance); } { final Constructor constructor = ByteType.class.getDeclaredConstructor(); constructor.setAccessible(true); - assertThrownWithCause(InvocationTargetException.class, IllegalStateException.class, "lol no", constructor::newInstance); + assertThrownWithCause(InvocationTargetException.class, IllegalStateException.class, UNEXPECTED_CLASS, constructor::newInstance); } assertThrown( IllegalStateException.class, - "lol no", + UNEXPECTED_CLASS, () -> new UnitType
("lol pwned", Address.class, TypeFactory.ADDRESS_BIT_LEN, true) { static final long HACKER_TIME = 26448843480000L; @@ -275,6 +281,7 @@ Address decode(ByteBuffer buffer, byte[] unitBuffer) { } } }); + System.out.println("Constraints verified."); } private static void assertThrownWithCause(Class clazz, Class causeClazz, String internedMsg, TestUtils.CustomRunnable r) throws Throwable { @@ -282,7 +289,7 @@ private static void assertThrownWithCause(Class clazz, Clas r.run(); } catch (Throwable t) { if (clazz.isInstance(t) && causeClazz.isInstance(t.getCause()) && t.getCause().getMessage() == internedMsg) { - Assertions.assertThrowsExactly(clazz, r::run); +// Assertions.assertThrowsExactly(clazz, r::run); return; } throw t;