diff --git a/pom.xml b/pom.xml index 0b6ba93..e6ca626 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,33 @@ commons-lang3 3.12.0 + + net.java.dev.jna + jna + 5.8.0 + + + org.apache.tuweni + tuweni-bytes + 2.0.0 + + + error_prone_annotations + com.google.errorprone + + + + + org.apache.tuweni + tuweni-units + 2.0.0 + + + guava + com.google.guava + + + io.grpc @@ -87,6 +114,10 @@ animal-sniffer-annotations org.codehaus.mojo + + guava + com.google.guava + @@ -114,6 +145,10 @@ animal-sniffer-annotations org.codehaus.mojo + + guava + com.google.guava + diff --git a/src/main/java/irita/sdk/crypto/eth/AbstractRLPOutput.java b/src/main/java/irita/sdk/crypto/eth/AbstractRLPOutput.java new file mode 100644 index 0000000..99c6f0c --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/AbstractRLPOutput.java @@ -0,0 +1,192 @@ +package irita.sdk.crypto.eth; + +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.MutableBytes; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +abstract class AbstractRLPOutput implements RLPOutput { + /* + * The algorithm implemented works as follows: + * + * Values written to the output are accumulated in the 'values' list. When a list is started, it + * is indicated by adding a specific marker in that list (LIST_MARKER). + * While this is gathered, we also incrementally compute the size of the payload of every list of + * that output. Those sizes are stored in 'payloadSizes': when all the output has been added, + * payloadSizes[i] will contain the size of the (encoded) payload of the ith list in 'values' + * (that is, the list that starts at the ith LIST_MARKER in 'values'). + * + * With that information gathered, encoded() can write its output in a single walk of 'values': + * values can encoded directly, and every time we read a list marker, we use the corresponding + * payload size to write the proper prefix and continue. + * + * The main remaining aspect is how the values of 'payloadSizes' are computed. Computing the size + * of a list without nesting inside is easy: simply add the encoded size of any newly added value + * to the running size. The difficulty is with nesting: when we start a new list, we need to + * track both the sizes of the previous list and the new one. To deal with that, we use the small + * stack 'parentListStack': it stores the index in 'payloadSizes' of every currently "open" lists. + * In other words, payloadSises[parentListStack[stackSize - 1]] corresponds to the size of the + * current list, the one to which newly added value are currently written (until the next call + * to 'endList()' that is, while payloadSises[parentListStack[stackSize - 2]] would be the size + * of the parent list, .... + * + * Note that when a new value is added, we add its size only the currently running list. We should + * add that size to that of any parent list as well, but we do so indirectly when a list is + * finished: when 'endList()' is called, we add the size of the full list we just finished (and + * whose size we have now have completely) to its parent size. + * + * Side-note: this class internally and informally use "element" to refer to a non list items. + */ + + private static final Bytes LIST_MARKER = Bytes.wrap(new byte[0]); + + private final List values = new ArrayList<>(); + // For every value i in values, rlpEncoded.get(i) will be true only if the value stored is an + // already encoded item. + private final BitSet rlpEncoded = new BitSet(); + + // First element is the total size of everything (the encoding may be a single non-list item, so + // this handle that case more easily; we need that value to size out final output). Following + // elements holds the size of the payload of the ith list in 'values'. + private int[] payloadSizes = new int[8]; + private int listsCount = 1; // number of lists current in 'values' + 1. + + private int[] parentListStack = new int[4]; + private int stackSize = 1; + + private int currentList() { + return parentListStack[stackSize - 1]; + } + + @Override + public void writeBytes(final Bytes v) { + checkState( + stackSize > 1 || values.isEmpty(), "Terminated RLP output, cannot add more elements"); + values.add(v); + payloadSizes[currentList()] += RLPEncodingHelpers.elementSize(v); + } + + public void writeRaw(final Bytes v) { + checkState( + stackSize > 1 || values.isEmpty(), "Terminated RLP output, cannot add more elements"); + values.add(v); + // Mark that last value added as already encoded. + rlpEncoded.set(values.size() - 1); + payloadSizes[currentList()] += v.size(); + } + + @Override + public void startList() { + values.add(LIST_MARKER); + ++listsCount; // we'll add a new element to payloadSizes + ++stackSize; // and to the list stack. + + // Resize our lists if necessary. + if (listsCount > payloadSizes.length) { + payloadSizes = Arrays.copyOf(payloadSizes, (payloadSizes.length * 3) / 2); + } + if (stackSize > parentListStack.length) { + parentListStack = Arrays.copyOf(parentListStack, (parentListStack.length * 3) / 2); + } + + // The new current list size is store in the slot we just made room for by incrementing + // listsCount + parentListStack[stackSize - 1] = listsCount - 1; + } + + @Override + public void endList() { + checkState(stackSize > 1, "LeaveList() called with no prior matching startList()"); + + final int current = currentList(); + final int finishedListSize = RLPEncodingHelpers.listSize(payloadSizes[current]); + --stackSize; + + // We just finished an item of our parent list, add it to that parent list size now. + final int newCurrent = currentList(); + payloadSizes[newCurrent] += finishedListSize; + } + + /** + * Computes the final encoded data size. + * + * @return The size of the RLP-encoded data written to this output. + * @throws IllegalStateException if some opened list haven't been closed (the output is not valid + * as is). + */ + public int encodedSize() { + checkState(stackSize == 1, "A list has been entered (startList()) but not left (endList())"); + return payloadSizes[0]; + } + + /** + * Write the rlp encoded value to the provided {@link MutableBytes} + * + * @param mutableBytes the value to which the rlp-data will be written + */ + public void writeEncoded(final MutableBytes mutableBytes) { + // Special case where we encode only a single non-list item (note that listsCount is initially + // set to 1, so listsCount == 1 really mean no list explicitly added to the output). + if (listsCount == 1) { + // writeBytes make sure we cannot have more than 1 value without a list + assert values.size() == 1; + final Bytes value = values.get(0); + + final int finalOffset; + // Single non-list value. + if (rlpEncoded.get(0)) { + value.copyTo(mutableBytes, 0); + finalOffset = value.size(); + } else { + finalOffset = RLPEncodingHelpers.writeElement(value, mutableBytes, 0); + } + checkState( + finalOffset == mutableBytes.size(), + "Expected single element RLP encode to be of size %s but was of size %s.", + mutableBytes.size(), + finalOffset); + return; + } + + int offset = 0; + int listIdx = 0; + for (int i = 0; i < values.size(); i++) { + final Bytes value = values.get(i); + if (value == LIST_MARKER) { + final int payloadSize = payloadSizes[++listIdx]; + offset = RLPEncodingHelpers.writeListHeader(payloadSize, mutableBytes, offset); + } else if (rlpEncoded.get(i)) { + value.copyTo(mutableBytes, offset); + offset += value.size(); + } else { + offset = RLPEncodingHelpers.writeElement(value, mutableBytes, offset); + } + } + + checkState( + offset == mutableBytes.size(), + "Expected RLP encoding to be of size %s but was of size %s.", + mutableBytes.size(), + offset); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/AbstractSECP256.java b/src/main/java/irita/sdk/crypto/eth/AbstractSECP256.java new file mode 100644 index 0000000..417dd84 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/AbstractSECP256.java @@ -0,0 +1,111 @@ +package irita.sdk.crypto.eth; + + +import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.math.ec.ECAlgorithms; +import org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.Optional; + +public abstract class AbstractSECP256 implements SignatureAlgorithm { + + protected static final int PRIVATE_KEY_BYTE_LENGTH = 32; + protected static final int PUBLIC_KEY_BYTE_LENGTH = 64; + protected static final int SIGNATURE_BYTE_LENGTH = 65; + + public static final String PROVIDER = "BC"; + + protected final ECDomainParameters curve; + protected final BigInteger halfCurveOrder; + + protected final KeyPairGenerator keyPairGenerator; + protected final BigInteger curveOrder; + + final BigInteger prime; + + protected AbstractSECP256(final String curveName, final BigInteger prime) { + this.prime = prime; + Security.addProvider(new BouncyCastleProvider()); + + final X9ECParameters params = SECNamedCurves.getByName(curveName); + curve = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + curveOrder = curve.getN(); + halfCurveOrder = curveOrder.shiftRight(1); + try { + keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER); + } catch (final Exception e) { + throw new RuntimeException(e); + } + final ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(curveName); + try { + keyPairGenerator.initialize(ecGenParameterSpec, SecureRandomProvider.createSecureRandom()); + } catch (final InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + // Decompress a compressed public key (x co-ord and low-bit of y-coord). + protected ECPoint decompressKey(final BigInteger xBN, final boolean yBit) { + final X9IntegerConverter x9 = new X9IntegerConverter(); + final byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(curve.getCurve())); + compEnc[0] = (byte) (yBit ? 0x03 : 0x02); + // TODO: Find a better way to handle an invalid point compression here. + // Currently ECCurve#decodePoint throws an IllegalArgumentException. + return curve.getCurve().decodePoint(compEnc); + } + + protected BigInteger recoverFromSignature( + final int recId, final BigInteger r, final BigInteger s, final Bytes32 dataHash) { + assert (recId >= 0); + assert (r.signum() >= 0); + assert (s.signum() >= 0); + assert (dataHash != null); + + final BigInteger n = curve.getN(); // Curve order. + final BigInteger i = BigInteger.valueOf((long) recId / 2); + final BigInteger x = r.add(i.multiply(n)); + if (x.compareTo(prime) >= 0) { + return null; + } + final ECPoint R = decompressKey(x, (recId & 1) == 1); + if (!R.multiply(n).isInfinity()) { + return null; + } + final BigInteger e = dataHash.toUnsignedBigInteger(); + final BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n); + final BigInteger rInv = r.modInverse(n); + final BigInteger srInv = rInv.multiply(s).mod(n); + final BigInteger eInvrInv = rInv.multiply(eInv).mod(n); + final ECPoint q = ECAlgorithms.sumOfTwoMultiplies(curve.getG(), eInvrInv, R, srInv); + + if (q.isInfinity()) { + return null; + } + + final byte[] qBytes = q.getEncoded(false); + // We remove the prefix + return new BigInteger(1, Arrays.copyOfRange(qBytes, 1, qBytes.length)); + } + + @Override + public Optional recoverPublicKeyFromSignature( + final Bytes32 dataHash, final SECPSignature signature) { + final BigInteger publicKeyBI = + recoverFromSignature(signature.getRecId(), signature.getR(), signature.getS(), dataHash); + return Optional.of(SECPPublicKey.create(publicKeyBI, ALGORITHM)); + } + +} + + diff --git a/src/main/java/irita/sdk/crypto/eth/Address.java b/src/main/java/irita/sdk/crypto/eth/Address.java new file mode 100644 index 0000000..8cd27fa --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/Address.java @@ -0,0 +1,31 @@ +package irita.sdk.crypto.eth; + +import com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.DelegatingBytes; + +public class Address extends DelegatingBytes { + + public static final int SIZE = 20; + + public Address(final Bytes bytes) { + super(bytes); + } + + public static Address wrap(final Bytes value) { + return new Address(value); + } + + + public static Address extract(final Bytes32 hash) { + return wrap(hash.slice(12, 20)); + } + + + @JsonCreator + public static Address fromHexString(final String str) { + if (str == null) return null; + return wrap(Bytes.fromHexStringLenient(str, SIZE)); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/Hash.java b/src/main/java/irita/sdk/crypto/eth/Hash.java new file mode 100644 index 0000000..47c6114 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/Hash.java @@ -0,0 +1,51 @@ +package irita.sdk.crypto.eth; + + +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.DelegatingBytes32; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.function.Supplier; + +public class Hash extends DelegatingBytes32 { + + private Hash(final Bytes32 bytes) { + super(bytes); + } + public static final String KECCAK256_ALG = "KECCAK-256"; + + + private static final Supplier KECCAK256_SUPPLIER = + Suppliers.memoize(() -> messageDigest(KECCAK256_ALG)); + + private static MessageDigest messageDigest(final String algorithm) { + try { + return MessageDigest.getInstance(algorithm); + } catch (final NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private static byte[] digestUsingAlgorithm( + final Bytes input, final Supplier digestSupplier) { + try { + final MessageDigest digest = (MessageDigest) digestSupplier.get().clone(); + input.update(digest); + return digest.digest(); + } catch (final CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + public static Bytes32 keccak256(final Bytes input) { + return Bytes32.wrap(digestUsingAlgorithm(input, KECCAK256_SUPPLIER)); + } + + public static Hash hash(final Bytes value) { + return new Hash(keccak256(value)); + } + +} diff --git a/src/main/java/irita/sdk/crypto/eth/LegacyTransaction.java b/src/main/java/irita/sdk/crypto/eth/LegacyTransaction.java new file mode 100644 index 0000000..401b91e --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/LegacyTransaction.java @@ -0,0 +1,175 @@ +package irita.sdk.crypto.eth; + + +import irita.sdk.util.Bech32Utils; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.bytes.MutableBytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.web3j.utils.Numeric; +import proto.ethermint.evm.v1.Tx; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.function.Consumer; + +import static com.google.common.base.Preconditions.checkArgument; + +public class LegacyTransaction { + + public BigInteger curveOrder; + + private long nonce; + + private Optional gasPrice; + + private Optional maxPriorityFeePerGas; + + private Optional maxFeePerGas; + + private long gasLimit; + + private Optional
to; + + private Wei value; + + private SECPSignature signature; + + private Bytes payload; + + private Optional chainId; + + private Optional v; + + private volatile Bytes32 hashNoSignature; + + protected volatile Address sender; + + private TransactionType transactionType; + + private final SignatureAlgorithm instance = null; + + public SignatureAlgorithm getInstance() { + + if (instance != null) { + return instance; + } + return SignatureAlgorithmType.DEFAULT_SIGNATURE_ALGORITHM_TYPE.get(); + } + + public LegacyTransaction(Tx.LegacyTx legacyTx) { + this.transactionType = TransactionType.FRONTIER; + this.nonce = legacyTx.getNonce(); + this.gasPrice = Optional.of(Wei.of(Long.parseLong(legacyTx.getGasPrice()))); + this.maxFeePerGas = Optional.empty(); + this.gasLimit = legacyTx.getGas(); + this.to = Optional.of(Address.fromHexString(legacyTx.getTo())); + this.value = Wei.of(Long.parseLong(legacyTx.getValue())); + this.signature = getSignature(Numeric.toBigInt(legacyTx.getR().toByteArray()), Numeric.toBigInt(legacyTx.getS().toByteArray()), new Byte((byte) (legacyTx.getV().toByteArray()[0]-27)+""), getCurveOrder(curveOrder)); + this.payload =Bytes.wrap(legacyTx.getData().toByteArray()); + this.sender =null; + this.chainId =Optional.empty(); + this.v =Optional.of(Numeric.toBigInt(legacyTx.getV().toByteArray())); + this.maxPriorityFeePerGas = Optional.empty(); + } + + + private SECPSignature getSignature(BigInteger R, BigInteger S, byte recId, BigInteger curveOrder) { + return SECPSignature.create(R, S, recId, curveOrder); + } + + public SECPSignature getSignature(){ + return signature; + } + + private BigInteger getCurveOrder (BigInteger curveOrder){ + if (curveOrder == null){ + X9ECParameters params = SECNamedCurves.getByName("secp256k1"); + ECDomainParameters curve =new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + return curve.getN(); + } + return curveOrder; + } + + public String getSender() { + if (sender == null) { + final SECPPublicKey publicKey = + getInstance() + .recoverPublicKeyFromSignature(getOrComputeSenderRecoveryHash(), signature) + .orElseThrow( + () -> + new IllegalStateException( + "Cannot recover public key from signature for " + this)); + sender = Address.extract(Hash.hash(publicKey.getEncodedBytes())); + } + String addr = sender.toHexString().substring(2); + return Bech32Utils.hexToBech32("iaa",Numeric.cleanHexPrefix(addr)); + } + + private Bytes32 getOrComputeSenderRecoveryHash() { + if (hashNoSignature == null) { + hashNoSignature = + computeSenderRecoveryHash( + transactionType, + nonce, + gasPrice.orElse(null), + maxPriorityFeePerGas.orElse(null), + maxFeePerGas.orElse(null), + gasLimit, + to, + value, + payload, + chainId); + } + return hashNoSignature; + } + + private Bytes32 computeSenderRecoveryHash(final TransactionType transactionType, final long nonce, final Wei gasPrice, final Wei maxPriorityFeePerGas, final Wei maxFeePerGas, final long gasLimit, final Optional
to, final Wei value, final Bytes payload, final Optional chainId) { + if (transactionType.requiresChainId()) { + checkArgument(chainId.isPresent(), "Transaction type %s requires chainId", transactionType); + } + final Bytes preimage = frontierPreimage(nonce, gasPrice, gasLimit, to, value, payload, chainId); + return Hash.keccak256(preimage); + } + + private Bytes frontierPreimage(final long nonce, final Wei gasPrice, final long gasLimit, final Optional
to, final Wei value, final Bytes payload, final Optional chainId) { + return encode( + rlpOutput -> { + rlpOutput.startList(); + rlpOutput.writeLongScalar(nonce); + rlpOutput.writeUInt256Scalar(gasPrice); + rlpOutput.writeLongScalar(gasLimit); + rlpOutput.writeBytes(to.map(Bytes::copy).orElse(Bytes.EMPTY)); + rlpOutput.writeUInt256Scalar(value); + rlpOutput.writeBytes(payload); + if (chainId.isPresent()) { + rlpOutput.writeBigIntegerScalar(chainId.get()); + rlpOutput.writeUInt256Scalar(UInt256.ZERO); + rlpOutput.writeUInt256Scalar(UInt256.ZERO); + } + rlpOutput.endList(); + }); + } + + public Bytes encode(final Consumer writer) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + writer.accept(out); + return out.encoded(); + } + + public class BytesValueRLPOutput extends AbstractRLPOutput { + public Bytes encoded() { + final int size = encodedSize(); + if (size == 0) { + return Bytes.EMPTY; + } + + final MutableBytes output = MutableBytes.create(size); + writeEncoded(output); + return output; + } + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/RLPEncodingHelpers.java b/src/main/java/irita/sdk/crypto/eth/RLPEncodingHelpers.java new file mode 100644 index 0000000..68bbfe2 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/RLPEncodingHelpers.java @@ -0,0 +1,107 @@ +package irita.sdk.crypto.eth; + +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.MutableBytes; + +/** + * Helper static methods to facilitate RLP encoding within this package. Neither this class + * nor any of its method are meant to be exposed publicly, they are too low level. + */ +class RLPEncodingHelpers { + private RLPEncodingHelpers() {} + + static boolean isSingleRLPByte(final Bytes value) { + return value.size() == 1 && value.get(0) >= 0; + } + + static boolean isShortElement(final Bytes value) { + return value.size() <= 55; + } + + static boolean isShortList(final int payloadSize) { + return payloadSize <= 55; + } + + /** The encoded size of the provided value. */ + static int elementSize(final Bytes value) { + if (isSingleRLPByte(value)) return 1; + + if (isShortElement(value)) return 1 + value.size(); + + return 1 + sizeLength(value.size()) + value.size(); + } + + /** The encoded size of a list given the encoded size of its payload. */ + static int listSize(final int payloadSize) { + int size = 1 + payloadSize; + if (!isShortList(payloadSize)) size += sizeLength(payloadSize); + return size; + } + + /** + * Writes the result of encoding the provided value to the provided destination (which must be big + * enough). + */ + static int writeElement(final Bytes value, final MutableBytes dest, final int destOffset) { + final int size = value.size(); + if (isSingleRLPByte(value)) { + dest.set(destOffset, value.get(0)); + return destOffset + 1; + } + + if (isShortElement(value)) { + dest.set(destOffset, (byte) (0x80 + size)); + value.copyTo(dest, destOffset + 1); + return destOffset + 1 + size; + } + + final int offset = writeLongMetadata(0xb7, size, dest, destOffset); + value.copyTo(dest, offset); + return offset + size; + } + + /** + * Writes the encoded header of a list provided its encoded payload size to the provided + * destination (which must be big enough). + */ + static int writeListHeader(final int payloadSize, final MutableBytes dest, final int destOffset) { + if (isShortList(payloadSize)) { + dest.set(destOffset, (byte) (0xc0 + payloadSize)); + return destOffset + 1; + } + + return writeLongMetadata(0xf7, payloadSize, dest, destOffset); + } + + private static int writeLongMetadata( + final int baseCode, final int size, final MutableBytes dest, final int destOffset) { + final int sizeLength = sizeLength(size); + dest.set(destOffset, (byte) (baseCode + sizeLength)); + int shift = 0; + for (int i = 0; i < sizeLength; i++) { + dest.set(destOffset + sizeLength - i, (byte) (size >> shift)); + shift += 8; + } + return destOffset + 1 + sizeLength; + } + + private static int sizeLength(final int size) { + final int zeros = Integer.numberOfLeadingZeros(size); + return 4 - (zeros / 8); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/RLPOutput.java b/src/main/java/irita/sdk/crypto/eth/RLPOutput.java new file mode 100644 index 0000000..a1f1b1d --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/RLPOutput.java @@ -0,0 +1,273 @@ +package irita.sdk.crypto.eth; + +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.MutableBytes; +import org.apache.tuweni.units.bigints.UInt256Value; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.util.function.BiConsumer; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * An output used to encode data in RLP encoding. + * + *

An RLP "value" is fundamentally an {@code Item} defined the following way: + * + *

+ *   Item ::= List | Bytes
+ *   List ::= [ Item, ... , Item ]
+ *   Bytes ::= a binary value (comprised of an arbitrary number of bytes).
+ * 
+ * + * In other words, RLP encodes binary data organized in arbitrary nested lists. + * + *

A {@link RLPOutput} thus provides methods to write both lists and binary values. A list is + * started by calling {@link #startList()} and ended by {@link #endList()}. Lists can be nested in + * other lists in arbitrary ways. Binary values can be written directly with {@link + * #writeBytes(Bytes)}, but the {@link RLPOutput} interface provides a wealth of convenience methods + * to write specific types of data with a specific encoding. + * + *

Amongst the methods to write binary data, some methods are provided to write "scalar". A + * scalar should simply be understood as a positive integer that is encoded with no leading zeros. + * In other word, if an integer is written with a "Scalar" method variant, that number will be + * encoded with the minimum number of bytes necessary to represent it. + * + *

The {@link RLPOutput} only defines the interface for writing data meant to be RLP encoded. + * Getting the finally encoded output will depend on the concrete implementation, see {@link + * BytesValueRLPOutput} for instance. + */ +public interface RLPOutput { + + /** Starts a new list. */ + void startList(); + + /** + * Ends the current list. + * + * @throws IllegalStateException if no list has been previously started with {@link #startList()} + * (or any started had already be ended). + */ + void endList(); + + /** + * Writes a new value. + * + * @param v The value to write. + */ + void writeBytes(Bytes v); + + /** + * Writes a scalar (encoded with no leading zeroes). + * + * @param v The scalar to write. + */ + default void writeUInt256Scalar(final UInt256Value v) { + writeBytes(v.trimLeadingZeros()); + } + + /** + * Writes a RLP "null", that is an empty value. + * + *

This is a shortcut for {@code writeBytes(Bytes.EMPTY)}. + */ + default void writeNull() { + writeBytes(Bytes.EMPTY); + } + + /** + * Writes a scalar (encoded with no leading zeroes). + * + * @param v The scalar to write. + * @throws IllegalArgumentException if {@code v < 0}. + */ + default void writeIntScalar(final int v) { + writeLongScalar(v); + } + + /** + * Writes a scalar (encoded with no leading zeroes). + * + * @param v The scalar to write. + * @throws IllegalArgumentException if {@code v < 0}. + */ + default void writeLongScalar(final long v) { + checkArgument(v >= 0, "Invalid negative value %s for scalar encoding", v); + writeBytes(Bytes.minimalBytes(v)); + } + + /** + * Writes a scalar (encoded with no leading zeroes). + * + * @param v The scalar to write. + * @throws IllegalArgumentException if {@code v} is a negative integer ({@code v.signum() < 0}). + */ + default void writeBigIntegerScalar(final BigInteger v) { + checkArgument(v.signum() >= 0, "Invalid negative integer %s for scalar encoding", v); + if (v.equals(BigInteger.ZERO)) { + writeBytes(Bytes.EMPTY); + return; + } + + final byte[] bytes = v.toByteArray(); + // BigInteger will not include leading zeros by contract, but it always include at least one + // bit of sign (a zero here since it's positive). What that mean is that if the first 1 of the + // resulting number is exactly on a byte boundary, then the sign bit constraint will make the + // value include one extra byte, which will be zero. In other words, they can be one zero bytes + // in practice we should ignore, but there should never be more than one. + writeBytes( + bytes.length > 1 && bytes[0] == 0 + ? Bytes.wrap(bytes, 1, bytes.length - 1) + : Bytes.wrap(bytes)); + } + + /** + * Writes a single byte value. + * + * @param b The byte to write. + */ + default void writeByte(final byte b) { + writeBytes(Bytes.of(b)); + } + + /** + * Writes a 2-bytes value. + * + *

Note that this is not a "scalar" write: the value will be encoded with exactly 2 bytes. + * + * @param s The 2-bytes short to write. + */ + default void writeShort(final short s) { + final byte[] res = new byte[2]; + res[0] = (byte) (s >> 8); + res[1] = (byte) s; + writeBytes(Bytes.wrap(res)); + } + + /** + * Writes a 4-bytes value. + * + *

Note that this is not a "scalar" write: the value will be encoded with exactly 4 bytes. + * + * @param i The 4-bytes int to write. + */ + default void writeInt(final int i) { + final MutableBytes v = MutableBytes.create(4); + v.setInt(0, i); + writeBytes(v); + } + + /** + * Writes a 8-bytes value. + * + *

Note that this is not a "scalar" write: the value will be encoded with exactly 8 bytes. + * + * @param l The 8-bytes long to write. + */ + default void writeLong(final long l) { + final MutableBytes v = MutableBytes.create(8); + v.setLong(0, l); + writeBytes(v); + } + + /** + * Writes a single byte value. + * + * @param b A value that must fit an unsigned byte. + * @throws IllegalArgumentException if {@code b} does not fit an unsigned byte, that is if either + * {@code b < 0} or {@code b > 0xFF}. + */ + default void writeUnsignedByte(final int b) { + writeBytes(Bytes.of(b)); + } + + /** + * Writes a 2-bytes value. + * + * @param s A value that must fit an unsigned 2-bytes short. + * @throws IllegalArgumentException if {@code s} does not fit an unsigned 2-bytes short, that is + * if either {@code s < 0} or {@code s > 0xFFFF}. + */ + default void writeUnsignedShort(final int s) { + writeBytes(Bytes.ofUnsignedShort(s)); + } + + /** + * Writes a 4-bytes value. + * + * @param i A value that must fit an unsigned 4-bytes integer. + * @throws IllegalArgumentException if {@code i} does not fit an unsigned 4-bytes int, that is if + * either {@code i < 0} or {@code i > 0xFFFFFFFFL}. + */ + default void writeUnsignedInt(final long i) { + writeBytes(Bytes.ofUnsignedInt(i)); + } + + /** + * Writes the byte representation of an inet address (so either 4 or 16 bytes long). + * + * @param address The address to write. + */ + default void writeInetAddress(final InetAddress address) { + writeBytes(Bytes.wrap(address.getAddress())); + } + + /** + * Writes a list of values of a specific class provided a function to write values of that class + * to an {@link RLPOutput}. + * + *

This is a convenience method whose result is equivalent to doing: + * + *

{@code
+     * startList();
+     * for (T v : values) {
+     *   valueWriter.accept(v, this);
+     * }
+     * endList();
+     * }
+ * + * @param values A list of value of type {@code T}. + * @param valueWriter A method that given a value of type {@code T} and an {@link RLPOutput}, + * writes this value to the output. + * @param The type of values to write. + */ + default void writeList(final Iterable values, final BiConsumer valueWriter) { + startList(); + for (final T v : values) { + valueWriter.accept(v, this); + } + endList(); + } + + /** + * Writes an empty list to the output. + * + *

This is a shortcut for doing: + * + *

{@code
+     * startList();
+     * endList();
+     * }
+ */ + default void writeEmptyList() { + startList(); + endList(); + } + +} diff --git a/src/main/java/irita/sdk/crypto/eth/SECPPublicKey.java b/src/main/java/irita/sdk/crypto/eth/SECPPublicKey.java new file mode 100644 index 0000000..e8d5fa7 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/SECPPublicKey.java @@ -0,0 +1,99 @@ +package irita.sdk.crypto.eth; + + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.MutableBytes; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.math.ec.ECPoint; + +import java.math.BigInteger; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SECPPublicKey implements java.security.PublicKey { + + public static final int BYTE_LENGTH = 64; + + private final Bytes encoded; + private final String algorithm; + + public static SECPPublicKey create(final BigInteger key, final String algorithm) { + return create(toBytes64(key.toByteArray()), algorithm); + } + + public static SECPPublicKey create(final Bytes encoded, final String algorithm) { + return new SECPPublicKey(encoded, algorithm); + } + + + private static Bytes toBytes64(final byte[] backing) { + if (backing.length == BYTE_LENGTH) { + return Bytes.wrap(backing); + } else if (backing.length > BYTE_LENGTH) { + return Bytes.wrap(backing, backing.length - BYTE_LENGTH, BYTE_LENGTH); + } else { + final MutableBytes res = MutableBytes.create(BYTE_LENGTH); + Bytes.wrap(backing).copyTo(res, BYTE_LENGTH - backing.length); + return res; + } + } + + private SECPPublicKey(final Bytes encoded, final String algorithm) { + checkNotNull(encoded); + checkNotNull(algorithm); + this.encoded = encoded; + this.algorithm = algorithm; + } + + /** + * Returns this public key as an {@link ECPoint} of Bouncy Castle, to facilitate cryptographic + * operations. + * + * @param curve The elliptic curve (e.g. SECP256K1) represented as its domain parameters + * @return This public key represented as an Elliptic Curve point. + */ + public ECPoint asEcPoint(final ECDomainParameters curve) { + // 0x04 is the prefix for uncompressed keys. + final Bytes val = Bytes.concatenate(Bytes.of(0x04), encoded); + return curve.getCurve().decodePoint(val.toArrayUnsafe()); + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof SECPPublicKey)) { + return false; + } + + final SECPPublicKey that = (SECPPublicKey) other; + return this.encoded.equals(that.encoded) && this.algorithm.equals(that.algorithm); + } + + @Override + public byte[] getEncoded() { + return encoded.toArrayUnsafe(); + } + + public Bytes getEncodedBytes() { + return encoded; + } + + @Override + public String getAlgorithm() { + return algorithm; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public int hashCode() { + return encoded.hashCode(); + } + + @Override + public String toString() { + return encoded.toString(); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/SECPSignature.java b/src/main/java/irita/sdk/crypto/eth/SECPSignature.java new file mode 100644 index 0000000..2385158 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/SECPSignature.java @@ -0,0 +1,136 @@ +package irita.sdk.crypto.eth; + + +import com.google.common.base.Suppliers; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.MutableBytes; +import org.apache.tuweni.units.bigints.UInt256; + +import java.math.BigInteger; +import java.util.Objects; +import java.util.function.Supplier; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class SECPSignature { + + public static final int BYTES_REQUIRED = 65; + /** + * The recovery id to reconstruct the public key used to create the signature. + * + *

The recId is an index from 0 to 3 which indicates which of the 4 possible keys is the + * correct one. Because the key recovery operation yields multiple potential keys, the correct key + * must either be stored alongside the signature, or you must be willing to try each recId in turn + * until you find one that outputs the key you are expecting. + */ + private final byte recId; + + private final BigInteger r; + private final BigInteger s; + + public final Supplier encoded = Suppliers.memoize(this::_encodedBytes); + + public SECPSignature(final BigInteger r, final BigInteger s, final byte recId) { + this.r = r; + this.s = s; + this.recId = recId; + } + + /** + * Creates a new signature object given its parameters. + * + * @param r the 'r' part of the signature. + * @param s the 's' part of the signature. + * @param recId the recovery id part of the signature. + * @param curveOrder The order (n) of the used curve + * @return the created {@link SECPSignature} object. + * @throws NullPointerException if {@code r} or {@code s} are {@code null}. + * @throws IllegalArgumentException if any argument is invalid (for instance, {@code v} is neither + * 27 or 28). + */ + public static SECPSignature create( + final BigInteger r, final BigInteger s, final byte recId, final BigInteger curveOrder) { + checkNotNull(r); + checkNotNull(s); + checkInBounds("r", r, curveOrder); + checkInBounds("s", s, curveOrder); + if (recId != 0 && recId != 1) { + throw new IllegalArgumentException( + "Invalid 'recId' value, should be 0 or 1 but got " + recId); + } + return new SECPSignature(r, s, recId); + } + + private static void checkInBounds( + final String name, final BigInteger i, final BigInteger curveOrder) { + if (i.compareTo(BigInteger.ONE) < 0) { + throw new IllegalArgumentException( + String.format("Invalid '%s' value, should be >= 1 but got %s", name, i)); + } + + if (i.compareTo(curveOrder) >= 0) { + throw new IllegalArgumentException( + String.format("Invalid '%s' value, should be < %s but got %s", curveOrder, name, i)); + } + } + + public static SECPSignature decode(final Bytes bytes, final BigInteger curveOrder) { + checkArgument( + bytes.size() == BYTES_REQUIRED, "encoded SECP256K1 signature must be 65 bytes long"); + + final BigInteger r = bytes.slice(0, 32).toUnsignedBigInteger(); + final BigInteger s = bytes.slice(32, 32).toUnsignedBigInteger(); + final byte recId = bytes.get(64); + return SECPSignature.create(r, s, recId, curveOrder); + } + + public Bytes encodedBytes() { + return encoded.get(); + } + + private Bytes _encodedBytes() { + final MutableBytes bytes = MutableBytes.create(BYTES_REQUIRED); + UInt256.valueOf(r).copyTo(bytes, 0); + UInt256.valueOf(s).copyTo(bytes, 32); + bytes.set(64, recId); + return bytes; + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof SECPSignature)) { + return false; + } + + final SECPSignature that = (SECPSignature) other; + return this.r.equals(that.r) && this.s.equals(that.s) && this.recId == that.recId; + } + + @Override + public int hashCode() { + return Objects.hash(r, s, recId); + } + + public byte getRecId() { + return recId; + } + + public BigInteger getR() { + return r; + } + + public BigInteger getS() { + return s; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Signature").append("{"); + sb.append("r=").append(r).append(", "); + sb.append("s=").append(s).append(", "); + sb.append("recId=").append(recId); + return sb.append("}").toString(); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/SecureRandomProvider.java b/src/main/java/irita/sdk/crypto/eth/SecureRandomProvider.java new file mode 100644 index 0000000..ea37181 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/SecureRandomProvider.java @@ -0,0 +1,21 @@ +package irita.sdk.crypto.eth; + +import java.security.SecureRandom; + +public class SecureRandomProvider { + private static final SecureRandom publicSecureRandom = secureRandom(); + + // Returns a shared instance of secure random intended to be used where the value is used publicly + public static SecureRandom publicSecureRandom() { + return publicSecureRandom; + } + + public static SecureRandom createSecureRandom() { + return secureRandom(); + } + + @SuppressWarnings("DoNotCreateSecureRandomDirectly") + private static SecureRandom secureRandom() { + return new SecureRandom(); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/SignatureAlgorithm.java b/src/main/java/irita/sdk/crypto/eth/SignatureAlgorithm.java new file mode 100644 index 0000000..27c783b --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/SignatureAlgorithm.java @@ -0,0 +1,14 @@ +package irita.sdk.crypto.eth; + +import org.apache.tuweni.bytes.Bytes32; + +import java.util.Optional; + +public interface SignatureAlgorithm { + // needs to be known at compile time otherwise triggers InsecureCryptoUsage error + String ALGORITHM = "ECDSA"; + + Optional recoverPublicKeyFromSignature( + final Bytes32 dataHash, final SECPSignature signature); + +} diff --git a/src/main/java/irita/sdk/crypto/eth/SignatureAlgorithmType.java b/src/main/java/irita/sdk/crypto/eth/SignatureAlgorithmType.java new file mode 100644 index 0000000..414b9f3 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/SignatureAlgorithmType.java @@ -0,0 +1,74 @@ +package irita.sdk.crypto.eth; + + +import com.google.common.collect.ImmutableMap; +import irita.sdk.crypto.eth.libsecp256k1.SECP256K1; + +import java.util.Iterator; +import java.util.Map; +import java.util.function.Supplier; + +public class SignatureAlgorithmType { + + private static final String DEFAULT_EC_CURVE_NAME = "secp256k1"; + private static final ImmutableMap> SUPPORTED_ALGORITHMS = + ImmutableMap.of(DEFAULT_EC_CURVE_NAME, SECP256K1::new); + + public static final Supplier DEFAULT_SIGNATURE_ALGORITHM_TYPE = + SUPPORTED_ALGORITHMS.get(DEFAULT_EC_CURVE_NAME); + + private final Supplier instantiator; + + private SignatureAlgorithmType(final Supplier instantiator) { + this.instantiator = instantiator; + } + + public static SignatureAlgorithmType create(final String ecCurve) + throws IllegalArgumentException { + if (!isValidType(ecCurve)) { + throw new IllegalArgumentException(invalidTypeErrorMessage(ecCurve)); + } + + return new SignatureAlgorithmType(SUPPORTED_ALGORITHMS.get(ecCurve)); + } + + public static SignatureAlgorithmType createDefault() { + return new SignatureAlgorithmType(DEFAULT_SIGNATURE_ALGORITHM_TYPE); + } + + public SignatureAlgorithm getInstance() { + return instantiator.get(); + } + + public static boolean isValidType(final String ecCurve) { + return SUPPORTED_ALGORITHMS.containsKey(ecCurve); + } + + + private static String invalidTypeErrorMessage(final String invalidEcCurve) { + return new StringBuilder() + .append(invalidEcCurve) + .append(" is not in the list of valid elliptic curves ") + .append(getEcCurvesListAsString()) + .toString(); + } + + private static String getEcCurvesListAsString() { + Iterator>> it = + SUPPORTED_ALGORITHMS.entrySet().iterator(); + + StringBuilder ecCurveListBuilder = new StringBuilder(); + ecCurveListBuilder.append("["); + + while (it.hasNext()) { + ecCurveListBuilder.append(it.next().getKey()); + + if (it.hasNext()) { + ecCurveListBuilder.append(", "); + } + } + ecCurveListBuilder.append("]"); + + return ecCurveListBuilder.toString(); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/TransactionType.java b/src/main/java/irita/sdk/crypto/eth/TransactionType.java new file mode 100644 index 0000000..af97c22 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/TransactionType.java @@ -0,0 +1,56 @@ +package irita.sdk.crypto.eth; + +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.Arrays; +import java.util.EnumSet; + +public enum TransactionType { + FRONTIER(0xf8 /* doesn't end up being used as we don't serialize legacy txs with their type */), + ACCESS_LIST(0x01), + EIP1559(0x02); + + private static final EnumSet LEGACY_FEE_MARKET_TRANSACTION_TYPES = + EnumSet.of(TransactionType.FRONTIER, TransactionType.ACCESS_LIST); + + private final int typeValue; + + TransactionType(final int typeValue) { + this.typeValue = typeValue; + } + + public byte getSerializedType() { + return (byte) this.typeValue; + } + + public int compareTo(final Byte b) { + return Byte.valueOf(getSerializedType()).compareTo(b); + } + + public static TransactionType of(final int serializedTypeValue) { + return Arrays.stream(TransactionType.values()) + .filter(transactionType -> transactionType.typeValue == serializedTypeValue) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + String.format("Unsupported transaction type %x", serializedTypeValue))); + } + + public boolean requiresChainId() { + return !this.equals(FRONTIER); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/Wei.java b/src/main/java/irita/sdk/crypto/eth/Wei.java new file mode 100644 index 0000000..f3ca71f --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/Wei.java @@ -0,0 +1,79 @@ +package irita.sdk.crypto.eth; + +/* + * Copyright contributors to Hyperledger Besu + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import org.apache.tuweni.units.bigints.BaseUInt256Value; +import org.apache.tuweni.units.bigints.UInt256; + +import java.math.BigInteger; + +/** A particular quantity of Wei, the Ethereum currency. */ +public final class Wei extends BaseUInt256Value{ + + public static final Wei ZERO = of(0); + + public static final Wei ONE = of(1); + + Wei(final UInt256 value) { + super(value, Wei::new); + } + + private Wei(final long v) { + this(UInt256.valueOf(v)); + } + + private Wei(final BigInteger v) { + this(UInt256.valueOf(v)); + } + + private Wei(final String hexString) { + this(UInt256.fromHexString(hexString)); + } + + public static Wei of(final long value) { + return new Wei(value); + } + + public static Wei of(final BigInteger value) { + return new Wei(value); + } + + + public static Wei ofNumber(final Number value) { + return new Wei((BigInteger) value); + } + + + public static Wei fromHexString(final String str) { + return new Wei(str); + } + + public static Wei fromEth(final long eth) { + return Wei.of(BigInteger.valueOf(eth).multiply(BigInteger.TEN.pow(18))); + } + + @Override + public String toHexString() { + return super.toHexString(); + } + + @Override + public String toShortHexString() { + return super.isZero() ? "0x0" : super.toShortHexString(); + } + +} diff --git a/src/main/java/irita/sdk/crypto/eth/libsecp256k1/LibSecp256k1.java b/src/main/java/irita/sdk/crypto/eth/libsecp256k1/LibSecp256k1.java new file mode 100644 index 0000000..8a1c675 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/libsecp256k1/LibSecp256k1.java @@ -0,0 +1,308 @@ +package irita.sdk.crypto.eth.libsecp256k1; + + +import com.sun.jna.*; +import com.sun.jna.Structure.FieldOrder; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.LongByReference; +import com.sun.jna.ptr.PointerByReference; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; + +public class LibSecp256k1 implements Library { + + /* Flags to pass to secp256k1_context_create */ + public static final int SECP256K1_CONTEXT_VERIFY = 0x0101; + public static final int SECP256K1_CONTEXT_SIGN = 0x0201; + + /* Flag to pass to secp256k1_ec_pubkey_serialize. */ + public static final int SECP256K1_EC_UNCOMPRESSED = 0x0002; + + public static final PointerByReference CONTEXT = createContext(); + + private static PointerByReference createContext() { + try { + Native.register(LibSecp256k1.class, "secp256k1"); + final PointerByReference context = + secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN); + if (Boolean.parseBoolean(System.getProperty("secp256k1.randomize", "true"))) { + // randomization requested or not explicitly disabled + if (secp256k1_context_randomize(context, new SecureRandom().generateSeed(32)) != 1) { + // there was an error, don't preserve the context + return null; + } + } + return context; + } catch (final Throwable t) { + return null; + } + } + + /** + * A pointer to a function to deterministically generate a nonce + * + *

Except for test cases, this function should compute some cryptographic hash of the message, + * the algorithm, the key and the attempt. + */ + public interface secp256k1_nonce_function extends Callback { + + /** + * @param nonce32 (output) Pointer to a 32-byte array to be filled by the function. + * @param msg32 The 32-byte message hash being verified (will not be NULL). + * @param key32 Pointer to a 32-byte secret key (will not be NULL) + * @param algo16 Pointer to a 16-byte array describing the signature * algorithm (will be NULL + * for ECDSA for compatibility). + * @param data Arbitrary data pointer that is passed through. + * @param attempt How many iterations we have tried to find a nonce. This will almost always be + * 0, but different attempt values are required to result in a different nonce. + * @return 1 if a nonce was successfully generated. 0 will cause signing to fail. + */ + int apply( + Pointer nonce32, Pointer msg32, Pointer key32, Pointer algo16, Pointer data, int attempt); + } + + /** + * Opaque data structure that holds a parsed and valid public key. + * + *

The exact representation of data inside is implementation defined and not guaranteed to be + * portable between different platforms or versions. It is however guaranteed to be 64 bytes in + * size, and can be safely copied/moved. If you need to convert to a format suitable for storage, + * transmission, or comparison, use secp256k1_ec_pubkey_serialize and secp256k1_ec_pubkey_parse. + */ + @FieldOrder({"data"}) + public static class secp256k1_pubkey extends Structure { + public byte[] data = new byte[64]; + } + + /** + * Opaque data structured that holds a parsed ECDSA signature. + * + *

The exact representation of data inside is implementation defined and not guaranteed to be + * portable between different platforms or versions. It is however guaranteed to be 64 bytes in + * size, and can be safely copied/moved. If you need to convert to a format suitable for storage, + * transmission, or comparison, use the secp256k1_ecdsa_signature_serialize_* and + * secp256k1_ecdsa_signature_parse_* functions. + */ + @FieldOrder({"data"}) + public static class secp256k1_ecdsa_signature extends Structure { + public byte[] data = new byte[64]; + } + + /** + * Opaque data structured that holds a parsed ECDSA signature, supporting pubkey recovery. + * + *

The exact representation of data inside is implementation defined and not guaranteed to be + * portable between different platforms or versions. It is however guaranteed to be 65 bytes in + * size, and can be safely copied/moved. If you need to convert to a format suitable for storage + * or transmission, use the secp256k1_ecdsa_signature_serialize_* and + * secp256k1_ecdsa_signature_parse_* functions. + * + *

Furthermore, it is guaranteed that identical signatures (including their recoverability) + * will have identical representation, so they can be memcmp'ed. + */ + @FieldOrder({"data"}) + public static class secp256k1_ecdsa_recoverable_signature extends Structure { + public byte[] data = new byte[65]; + } + + /** + * Create a secp256k1 context object (in dynamically allocated memory). + * + *

This function uses malloc to allocate memory. It is guaranteed that malloc is called at most + * once for every call of this function. If you need to avoid dynamic memory allocation entirely, + * see the functions in secp256k1_preallocated.h. + * + *

See also secp256k1_context_randomize. + * + * @param flags which parts of the context to initialize. + * @return a newly created context object. + */ + public static native PointerByReference secp256k1_context_create(final int flags); + + /** + * Parse a variable-length public key into the pubkey object. + * + *

This function supports parsing compressed (33 bytes, header byte 0x02 or 0x03), uncompressed + * (65 bytes, header byte 0x04), or hybrid (65 bytes, header byte 0x06 or 0x07) format public + * keys. + * + * @return 1 if the public key was fully valid. 0 if the public key could not be parsed or is + * invalid. + * @param ctx a secp256k1 context object. + * @param pubkey (output) pointer to a pubkey object. If 1 is returned, it is set to a parsed + * version of input. If not, its value is undefined. + * @param input pointer to a serialized public key + * @param inputlen length of the array pointed to by input + */ + public static native int secp256k1_ec_pubkey_parse( + final PointerByReference ctx, + final secp256k1_pubkey pubkey, + final byte[] input, + final long inputlen); + + /** + * Serialize a pubkey object into a serialized byte sequence. + * + * @return 1 always. + * @param ctx a secp256k1 context object. + * @param output (output) a pointer to a 65-byte (if compressed==0) or 33-byte (if compressed==1) + * byte array to place the serialized key in. + * @param outputlen (input/output) a pointer to an integer which is initially set to the size of + * output, and is overwritten with the written size. + * @param pubkey a pointer to a secp256k1_pubkey containing an initialized public key. + * @param flags SECP256K1_EC_COMPRESSED if serialization should be in compressed format, otherwise + * SECP256K1_EC_UNCOMPRESSED. + */ + public static native int secp256k1_ec_pubkey_serialize( + final PointerByReference ctx, + final ByteBuffer output, + final LongByReference outputlen, + final secp256k1_pubkey pubkey, + final int flags); + + /** + * Parse an ECDSA signature in compact (64 bytes) format. + * + *

The signature must consist of a 32-byte big endian R value, followed by a 32-byte big endian + * S value. If R or S fall outside of [0..order-1], the encoding is invalid. R and S with value 0 + * are allowed in the encoding. + * + *

After the call, sig will always be initialized. If parsing failed or R or S are zero, the + * resulting sig value is guaranteed to fail validation for any message and public key. + * + * @return 1 when the signature could be parsed, 0 otherwise. + * @param ctx a secp256k1 context object. + * @param sig (output) a pointer to a signature object + * @param input64 a pointer to the 64-byte array to parse + */ + public static native int secp256k1_ecdsa_signature_parse_compact( + final PointerByReference ctx, final secp256k1_ecdsa_signature sig, final byte[] input64); + + /** + * Verify an ECDSA signature. + * + *

To avoid accepting malleable signatures, only ECDSA signatures in lower-S form are accepted. + * + *

If you need to accept ECDSA signatures from sources that do not obey this rule, apply + * secp256k1_ecdsa_signature_normalize to the signature prior to validation, but be aware that + * doing so results in malleable signatures. + * + *

For details, see the comments for that function. + * + * @return 1 if it is a correct signature, 0 if it is an incorrect or unparseable signature. + * @param ctx a secp256k1 context object, initialized for verification. + * @param sig the signature being verified (cannot be NULL) + * @param msg32 the 32-byte message hash being verified (cannot be NULL) + * @param pubkey pointer to an initialized public key to verify with (cannot be NULL) + */ + public static native int secp256k1_ecdsa_verify( + final PointerByReference ctx, + final secp256k1_ecdsa_signature sig, + final byte[] msg32, + final secp256k1_pubkey pubkey); + + /** + * Compute the public key for a secret key. + * + * @return 1 if secret was valid, public key stores, 0 if secret was invalid, try again. + * @param ctx pointer to a context object, initialized for signing (cannot be NULL) + * @param pubkey (output) pointer to the created public key (cannot be NULL) + * @param seckey pointer to a 32-byte private key (cannot be NULL) + */ + public static native int secp256k1_ec_pubkey_create( + final PointerByReference ctx, final secp256k1_pubkey pubkey, final byte[] seckey); + + /** + * Updates the context randomization to protect against side-channel leakage. While secp256k1 code + * is written to be constant-time no matter what secret values are, it's possible that a future + * compiler may output code which isn't, and also that the CPU may not emit the same radio + * frequencies or draw the same amount power for all values. + * + *

This function provides a seed which is combined into the blinding value: that blinding value + * is added before each multiplication (and removed afterwards) so that it does not affect + * function results, but shields against attacks which rely on any input-dependent behaviour. + * + *

This function has currently an effect only on contexts initialized for signing because + * randomization is currently used only for signing. However, this is not guaranteed and may + * change in the future. It is safe to call this function on contexts not initialized for signing; + * then it will have no effect and return 1. + * + *

You should call this after secp256k1_context_create or secp256k1_context_clone (and + * secp256k1_context_preallocated_create or secp256k1_context_clone, resp.), and you may call this + * repeatedly afterwards. + * + * @param ctx pointer to a context object (cannot be NULL) + * @param seed32 pointer to a 32-byte random seed (NULL resets to initial state) + * @return Returns 1 if randomization successfully updated or nothing to randomize or 0 if an + * error occured + */ + public static native int secp256k1_context_randomize( + final PointerByReference ctx, final byte[] seed32); + + /** + * Parse a compact ECDSA signature (64 bytes + recovery id). + * + * @return 1 when the signature could be parsed, 0 otherwise + * @param ctx a secp256k1 context object + * @param sig (output) a pointer to a signature object + * @param input64 a pointer to a 64-byte compact signature + * @param recid the recovery id (0, 1, 2 or 3) + */ + public static native int secp256k1_ecdsa_recoverable_signature_parse_compact( + final PointerByReference ctx, + final secp256k1_ecdsa_recoverable_signature sig, + final byte[] input64, + final int recid); + + /** + * Serialize an ECDSA signature in compact format (64 bytes + recovery id). + * + * @param ctx a secp256k1 context object + * @param output64 (output) a pointer to a 64-byte array of the compact signature (cannot be NULL) + * @param recid (output) a pointer to an integer to hold the recovery id (can be NULL). + * @param sig a pointer to an initialized signature object (cannot be NULL) + */ + public static native void secp256k1_ecdsa_recoverable_signature_serialize_compact( + final PointerByReference ctx, + final ByteBuffer output64, + final IntByReference recid, + final secp256k1_ecdsa_recoverable_signature sig); + + /** + * Create a recoverable ECDSA signature. + * + * @return 1 if signature created, 0 if the nonce generation function failed or the private key + * was invalid. + * @param ctx pointer to a context object, initialized for signing (cannot be NULL) + * @param sig (output) pointer to an array where the signature will be placed (cannot be NULL) + * @param msg32 the 32-byte message hash being signed (cannot be NULL) + * @param seckey pointer to a 32-byte secret key (cannot be NULL) + * @param noncefp pointer to a nonce generation function. If NULL, + * secp256k1_nonce_function_default is used + * @param ndata pointer to arbitrary data used by the nonce generation function (can be NULL) + */ + public static native int secp256k1_ecdsa_sign_recoverable( + final PointerByReference ctx, + final secp256k1_ecdsa_recoverable_signature sig, + final byte[] msg32, + final byte[] seckey, + final secp256k1_nonce_function noncefp, + final Pointer ndata); + + /** + * Recover an ECDSA public key from a signature. + * + * @return 1 if public key successfully recovered (which guarantees a correct signature), 0 + * otherwise. + * @param ctx pointer to a context object, initialized for verification (cannot be NULL) + * @param pubkey (output) pointer to the recovered public key (cannot be NULL) + * @param sig pointer to initialized signature that supports pubkey recovery (cannot be NULL) + * @param msg32 the 32-byte message hash assumed to be signed (cannot be NULL) + */ + public static native int secp256k1_ecdsa_recover( + final PointerByReference ctx, + final secp256k1_pubkey pubkey, + final secp256k1_ecdsa_recoverable_signature sig, + final byte[] msg32); +} diff --git a/src/main/java/irita/sdk/crypto/eth/libsecp256k1/SECP256K1.java b/src/main/java/irita/sdk/crypto/eth/libsecp256k1/SECP256K1.java new file mode 100644 index 0000000..d7882e2 --- /dev/null +++ b/src/main/java/irita/sdk/crypto/eth/libsecp256k1/SECP256K1.java @@ -0,0 +1,103 @@ +package irita.sdk.crypto.eth.libsecp256k1; + + +import com.sun.jna.ptr.LongByReference; +import irita.sdk.crypto.eth.AbstractSECP256; +import irita.sdk.crypto.eth.SECPPublicKey; +import irita.sdk.crypto.eth.SECPSignature; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Optional; + +/* + * Adapted from the BitcoinJ ECKey (Apache 2 License) implementation: + * https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/ECKey.java + * + * + * Adapted from the web3j (Apache 2 License) implementations: + * https://github.com/web3j/web3j/crypto/src/main/java/org/web3j/crypto/*.java + */ +public class SECP256K1 extends AbstractSECP256 { + + //private static final Logger LOG = LogManager.getLogger(); + + private boolean useNative; + + public static final String CURVE_NAME = "secp256k1"; + + public SECP256K1() { + super(CURVE_NAME, SecP256K1Curve.q); + + // use the native library implementation, if it is available + useNative = LibSecp256k1.CONTEXT != null; + if (!useNative) { + //LOG.info("Native secp256k1 not available"); + } + } + + @Override + public Optional recoverPublicKeyFromSignature( + final Bytes32 dataHash, final SECPSignature signature) { + if (useNative) { + Optional result = recoverFromSignatureNative(dataHash, signature); + if (!result.isPresent()) { + throw new IllegalArgumentException("Could not recover public key"); + } else { + return result; + } + } else { + return super.recoverPublicKeyFromSignature(dataHash, signature); + } + } + + + @Override + protected BigInteger recoverFromSignature( + final int recId, final BigInteger r, final BigInteger s, final Bytes32 dataHash) { + if (useNative) { + return recoverFromSignatureNative(dataHash, new SECPSignature(r, s, (byte) recId)) + .map(key -> new BigInteger(1, key.getEncoded())) + .orElse(null); + } else { + return super.recoverFromSignature(recId, r, s, dataHash); + } + } + + private Optional recoverFromSignatureNative( + final Bytes32 dataHash, final SECPSignature signature) { + + // parse the sig + final LibSecp256k1.secp256k1_ecdsa_recoverable_signature parsedSignature = + new LibSecp256k1.secp256k1_ecdsa_recoverable_signature(); + final Bytes encodedSig = signature.encodedBytes(); + if (LibSecp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact( + LibSecp256k1.CONTEXT, + parsedSignature, + encodedSig.slice(0, 64).toArrayUnsafe(), + encodedSig.get(64)) + == 0) { + throw new IllegalArgumentException("Could not parse signature"); + } + + // recover the key + final LibSecp256k1.secp256k1_pubkey newPubKey = new LibSecp256k1.secp256k1_pubkey(); + if (LibSecp256k1.secp256k1_ecdsa_recover( + LibSecp256k1.CONTEXT, newPubKey, parsedSignature, dataHash.toArrayUnsafe()) + == 0) { + return Optional.empty(); + } + + // parse the key + final ByteBuffer recoveredKey = ByteBuffer.allocate(65); + final LongByReference keySize = new LongByReference(recoveredKey.limit()); + LibSecp256k1.secp256k1_ec_pubkey_serialize( + LibSecp256k1.CONTEXT, recoveredKey, keySize, newPubKey, LibSecp256k1.SECP256K1_EC_UNCOMPRESSED); + + return Optional.of( + SECPPublicKey.create(Bytes.wrapByteBuffer(recoveredKey).slice(1), ALGORITHM)); + } +} diff --git a/src/main/java/irita/sdk/crypto/eth/libsecp256k1/libsecp256k1.so b/src/main/java/irita/sdk/crypto/eth/libsecp256k1/libsecp256k1.so new file mode 100644 index 0000000..ea7b473 Binary files /dev/null and b/src/main/java/irita/sdk/crypto/eth/libsecp256k1/libsecp256k1.so differ diff --git a/src/main/java/linux-x86-64/libsecp256k1.so b/src/main/java/linux-x86-64/libsecp256k1.so new file mode 100644 index 0000000..ea7b473 Binary files /dev/null and b/src/main/java/linux-x86-64/libsecp256k1.so differ diff --git a/src/test/java/irita/sdk/ClientTest.java b/src/test/java/irita/sdk/ClientTest.java index 8407bae..b3c7a07 100644 --- a/src/test/java/irita/sdk/ClientTest.java +++ b/src/test/java/irita/sdk/ClientTest.java @@ -4,6 +4,7 @@ import irita.sdk.client.BaseClient; import irita.sdk.client.IritaClient; import irita.sdk.constant.enums.BroadcastMode; +import irita.sdk.crypto.eth.LegacyTransaction; import irita.sdk.model.*; import irita.sdk.model.block.BlockDetail; import irita.sdk.model.tx.Condition; @@ -85,32 +86,24 @@ public void queryTxs() throws IOException, NoSuchMethodException, InvocationTarg @Test @Disabled public void queryTxFeePayer() throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { - String hash = "1308492A88B0F2E09D4DFF8E9483EDE462EC293405B1A94272291808D0E382B2";// + String hash = "7C47C95BE8F8794E89D2104313411193D8270F94147ED08E354C74704140A00B";// ResultQueryTx resultQueryTx = client.getBaseClient().queryTx(hash); assertNotNull(resultQueryTx); - // 判断是否是智能合约交易 if (resultQueryTx.getTx().getBody().getMsgs().get(0) instanceof proto.ethermint.evm.v1.Tx.MsgEthereumTx) { - // 从交易结果中解析交易的签名者,并根据以下逻辑过滤出签名者信息 - String senderAddr = resultQueryTx.getResult().getEvents().stream() - .filter(e -> e.getType().equals("message")) - .map(Events::getAttributes) - .reduce((attributes, attributes2) -> { - attributes.addAll(attributes2); - return attributes; - }) - .orElse(Collections.emptyList()) - .stream() - .filter(attribute -> "sender".equals(attribute.getKey()) && attribute.getValue().startsWith("0x")) - .map(attribute -> Bech32Utils.hexToBech32("iaa", Numeric.cleanHexPrefix(attribute.getValue()))) - .distinct().findFirst().orElse(""); - System.out.println("付钱的人是:" + senderAddr); + List messageList = resultQueryTx.getTx().getBody().getMsgs(); + for (GeneratedMessageV3 generatedMessageV3 : messageList) { + proto.ethermint.evm.v1.Tx.MsgEthereumTx msgEthereumTx = proto.ethermint.evm.v1.Tx.MsgEthereumTx.parseFrom(generatedMessageV3.toByteString()); + proto.ethermint.evm.v1.Tx.LegacyTx legacyTx = proto.ethermint.evm.v1.Tx.LegacyTx.parseFrom(msgEthereumTx.getData().getValue()); + LegacyTransaction legacyTransaction = new LegacyTransaction(legacyTx); + String addr = legacyTransaction.getSender(); + System.out.println("付钱的人是:"+ addr); + } return; } String granter = resultQueryTx.getTx().getAuthInfo().getFee().getGranter(); String payer = resultQueryTx.getTx().getAuthInfo().getFee().getPayer(); String txSigner = KeyUtils.parseAddrFromPubKeyAny(resultQueryTx.getTx().getAuthInfo().getSignerInfos(0).getPublicKey()); - // granter/payer/txSigner优先级按序排列 System.out.println("付钱的人是:" + (!"".equals(granter) ? granter : (!"".equals(payer) ? payer : txSigner))); }