diff --git a/build.gradle.kts b/build.gradle.kts index 4ab447c..b0b9200 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,11 +42,13 @@ dependencies { libs.bundles.kotlinLibs, libs.bundles.provenance, libs.bundles.grpc, + libs.bundles.bouncycastle, + + libs.figure.hdwallet, libs.reflections, libs.commons, libs.protobuf, - libs.bouncycastle, // third party plugins that this plugin will apply libs.shadow, diff --git a/example-kotlin/build.gradle b/example-kotlin/build.gradle index 2d58a3f..105a31b 100644 --- a/example-kotlin/build.gradle +++ b/example-kotlin/build.gradle @@ -22,6 +22,7 @@ allprojects { repositories { mavenLocal() mavenCentral() + maven { url "https://s01.oss.sonatype.org/content/groups/staging/" } maven { url 'https://javadoc.jitpack.io' } } } diff --git a/example-kotlin/contracts/build.gradle b/example-kotlin/contracts/build.gradle index b9406ef..6ec6451 100644 --- a/example-kotlin/contracts/build.gradle +++ b/example-kotlin/contracts/build.gradle @@ -12,7 +12,7 @@ plugins { dependencies { api project(':protos') - implementation("io.provenance.scope:contract-base:0.7.0-rc1") + implementation("io.provenance.scope:contract-base:0.7.0-rc2") } publishing { diff --git a/example-kotlin/protos/build.gradle b/example-kotlin/protos/build.gradle index cceff08..02d4502 100644 --- a/example-kotlin/protos/build.gradle +++ b/example-kotlin/protos/build.gradle @@ -22,7 +22,7 @@ sourceSets.main.java.srcDirs += 'build/generated/source/proto/main/java' sourceSets.main.java.srcDirs += 'src/main/kotlin' dependencies { - implementation("io.provenance.scope:contract-base:0.7.0-rc1") + implementation("io.provenance.scope:contract-base:0.7.0-rc2") api "com.google.protobuf:protobuf-java:$protobuf_version" api "com.google.protobuf:protobuf-java-util:$protobuf_version" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1abc545..1b9c51e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,16 @@ [versions] kotlin = "1.9.10" reflections = "0.9.10" -provenance-scope-sdk = "0.7.0-rc1" +provenance-scope-sdk = "0.7.0-rc2" provenance-client = "2.4.0-rc1" provenance-protobuf = "1.5.0" +figure-hdwallet = "0.4.3" grpc = "1.58.0" commons = "2.14.0" protobuf = "3.24.4" bouncycastle = "1.70" shadow = "8.1.1" -kethereum = "0.83.4" +kethereum = "0.86.0" jackson = "2.15.3" kotest = "5.7.2" kotest-4 = "4.4.+" @@ -25,13 +26,16 @@ provenance-scope = { module = "io.provenance.scope:util", version.ref = "provena provenance-client = { module = "io.provenance.client:pb-grpc-client-kotlin", version.ref = "provenance-client" } provenance-protobuf = { module = "io.provenance.protobuf:pb-proto-java", version.ref = "provenance-protobuf" } +figure-hdwallet = { module = "tech.figure.hdwallet:hdwallet", version.ref = "figure-hdwallet" } + grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" } commons = { module = "commons-io:commons-io", version.ref = "commons" } protobuf = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } -bouncycastle = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" } +bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" } +bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" } shadow = { module = "com.github.johnrengelman:shadow", version.ref = "shadow" } @@ -51,10 +55,9 @@ kotest-runner4 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kot [bundles] kotlinLibs = ["kotlinLibs-std", "kotlinLibs-reflect"] -provenance = ["provenance-sdk", "provenance-scope", -# "provenance-client", # todo: add back on once this is used - "provenance-protobuf"] +provenance = ["provenance-sdk", "provenance-scope", "provenance-client", "provenance-protobuf"] grpc = ["grpc-protobuf", "grpc-stub", "grpc-netty-shaded"] kethereum = ["kethereum-crypto", "kethereum-crypto-api", "kethereum-crypto-bouncycastle"] jackson = ["jackson-core", "jackson-databind", "jackson-datatype-jsr310", "jackson-kotlin", "jackson-annotations"] -kotest = ["kotest-runner", "kotest-assertions", "kotest-property"] \ No newline at end of file +kotest = ["kotest-runner", "kotest-assertions", "kotest-property"] +bouncycastle = ["bouncycastle-bcprov", "bouncycastle-bcpkix"] \ No newline at end of file diff --git a/src/main/kotlin/io/provenance/p8e/plugin/Bootstrapper.kt b/src/main/kotlin/io/provenance/p8e/plugin/Bootstrapper.kt index 6031d1c..73d5c17 100644 --- a/src/main/kotlin/io/provenance/p8e/plugin/Bootstrapper.kt +++ b/src/main/kotlin/io/provenance/p8e/plugin/Bootstrapper.kt @@ -3,6 +3,7 @@ package io.provenance.p8e.plugin import com.google.protobuf.ByteString import com.google.protobuf.Message import io.grpc.ManagedChannelBuilder +import io.provenance.client.grpc.BaseReqSigner import io.provenance.metadata.v1.ContractSpecification import io.provenance.metadata.v1.ContractSpecificationRequest import io.provenance.metadata.v1.DefinitionType @@ -15,7 +16,6 @@ import io.provenance.metadata.v1.MsgWriteScopeSpecificationRequest import io.provenance.metadata.v1.RecordSpecification import io.provenance.metadata.v1.ScopeSpecification import io.provenance.metadata.v1.ScopeSpecificationRequest -import io.provenance.scope.contract.annotations.ScopeSpecification as ScopeSpecificationReference import io.provenance.scope.contract.annotations.ScopeSpecificationDefinition import io.provenance.scope.contract.proto.Commons.ProvenanceReference import io.provenance.scope.contract.proto.Specifications.ContractSpec @@ -55,6 +55,7 @@ import java.security.PublicKey import java.security.Security import java.util.UUID import java.util.concurrent.TimeUnit +import io.provenance.scope.contract.annotations.ScopeSpecification as ScopeSpecificationReference fun ContractSpec.uuid(): UUID = toByteArray().sha256LoBytes().toUuid() fun ContractSpec.hashString(): String = toByteArray().sha256LoBytes().base64EncodeString() @@ -139,7 +140,7 @@ internal class Bootstrapper( val encryptionKeyPair = getKeyPair(location.encryptionPrivateKey!!) val signingKeyPair = getKeyPair(location.signingPrivateKey!!) - val pbSigner = signingKeyPair.toSignerMeta() + val pbSigner = BaseReqSigner(JavaKeyPairSigner(signingKeyPair, config.mainNet)) val pbAddress = getAddress(signingKeyPair.public, config.mainNet) val affiliate = Affiliate( signingKeyRef = DirectKeyRef(signingKeyPair.public, signingKeyPair.private), @@ -221,7 +222,7 @@ internal class Bootstrapper( .build() }.chunked(location.txBatchSize.toInt()).forEach { batch -> try { - client.writeTx(pbAddress, pbSigner, batch.toTxBody()) + client.writeTx(pbSigner, batch.toTxBody()) } catch (e: Exception) { project.logger.info("sent messages = $batch") throw e @@ -347,7 +348,7 @@ internal class Bootstrapper( messages.chunked(location.txBatchSize.toInt()).forEach { batch -> try { - client.writeTx(pbAddress, pbSigner, batch.toTxBody()) + client.writeTx(pbSigner, batch.toTxBody()) } catch (e: Exception) { project.logger.info("sent messages = $batch") throw e @@ -384,7 +385,7 @@ internal class Bootstrapper( contractSpecToScopeSpecMessages.chunked(location.txBatchSize.toInt()).forEach { batch -> try { - client.writeTx(pbAddress, pbSigner, batch.toTxBody()) + client.writeTx(pbSigner, batch.toTxBody()) } catch (e: Exception) { project.logger.info("sent messages = $batch") throw e diff --git a/src/main/kotlin/io/provenance/p8e/plugin/JavaKeyPairSigner.kt b/src/main/kotlin/io/provenance/p8e/plugin/JavaKeyPairSigner.kt new file mode 100644 index 0000000..5dda416 --- /dev/null +++ b/src/main/kotlin/io/provenance/p8e/plugin/JavaKeyPairSigner.kt @@ -0,0 +1,26 @@ +package io.provenance.p8e.plugin + +import com.google.protobuf.ByteString +import cosmos.crypto.secp256k1.Keys +import io.provenance.client.grpc.Signer +import io.provenance.scope.encryption.util.getAddress +import tech.figure.hdwallet.common.hashing.sha256 +import tech.figure.hdwallet.ec.extensions.toECPrivateKey +import tech.figure.hdwallet.ec.extensions.toECPublicKey +import tech.figure.hdwallet.signer.BCECSigner +import java.security.KeyPair + +class JavaKeyPairSigner(private val keyPair: KeyPair, private val mainNet: Boolean) : Signer { + override fun address(): String = keyPair.public.getAddress(mainNet) + + override fun pubKey(): Keys.PubKey = Keys.PubKey + .newBuilder() + .setKey(ByteString.copyFrom(keyPair.public.toECPublicKey().compressed())) + .build() + + override fun sign(data: ByteArray): ByteArray = + BCECSigner() + .sign(keyPair.private.toECPrivateKey(), data.sha256()) + .encodeAsBTC() + .toByteArray() +} \ No newline at end of file diff --git a/src/main/kotlin/io/provenance/p8e/plugin/ProvenanceClient.kt b/src/main/kotlin/io/provenance/p8e/plugin/ProvenanceClient.kt index 880c481..3f6b027 100644 --- a/src/main/kotlin/io/provenance/p8e/plugin/ProvenanceClient.kt +++ b/src/main/kotlin/io/provenance/p8e/plugin/ProvenanceClient.kt @@ -10,16 +10,10 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.google.protobuf.Any import com.google.protobuf.ByteString import com.google.protobuf.Message -import cosmos.auth.v1beta1.Auth -import cosmos.auth.v1beta1.QueryOuterClass import cosmos.base.v1beta1.CoinOuterClass import cosmos.crypto.secp256k1.Keys import cosmos.tx.signing.v1beta1.Signing -import cosmos.tx.v1beta1.ServiceGrpc -import cosmos.tx.v1beta1.ServiceOuterClass import cosmos.tx.v1beta1.ServiceOuterClass.BroadcastMode -import cosmos.tx.v1beta1.ServiceOuterClass.BroadcastTxRequest -import cosmos.tx.v1beta1.ServiceOuterClass.SimulateRequest import cosmos.tx.v1beta1.TxOuterClass.AuthInfo import cosmos.tx.v1beta1.TxOuterClass.Fee import cosmos.tx.v1beta1.TxOuterClass.ModeInfo @@ -28,12 +22,17 @@ import cosmos.tx.v1beta1.TxOuterClass.SignerInfo import cosmos.tx.v1beta1.TxOuterClass.Tx import cosmos.tx.v1beta1.TxOuterClass.TxBody import io.grpc.ManagedChannel -import io.grpc.Status -import io.grpc.StatusRuntimeException -import io.provenance.client.protobuf.extensions.getTx +import io.provenance.client.common.extensions.toCoin +import io.provenance.client.common.gas.prices.cachedGasPrice +import io.provenance.client.common.gas.prices.constGasPrice +import io.provenance.client.grpc.BaseReq +import io.provenance.client.grpc.BaseReqSigner +import io.provenance.client.grpc.GasEstimationMethod +import io.provenance.client.grpc.PbClient +import io.provenance.client.grpc.floatingGasPrices +import io.provenance.client.grpc.nodeFloorGasPrice import io.provenance.metadata.v1.ContractSpecificationRequest import io.provenance.metadata.v1.ContractSpecificationResponse -import io.provenance.metadata.v1.QueryGrpc import io.provenance.metadata.v1.ScopeSpecificationRequest import io.provenance.metadata.v1.ScopeSpecificationResponse import io.provenance.scope.objectstore.util.sha256 @@ -45,19 +44,11 @@ import org.kethereum.crypto.impl.ec.EllipticCurveSigner import org.slf4j.Logger import java.io.IOException import java.math.BigInteger +import java.net.URI import java.security.KeyPair import java.util.concurrent.TimeUnit import kotlin.math.ceil - - data class GasEstimate(val estimate: Long, val feeAdjustment: Double = DEFAULT_FEE_ADJUSTMENT, val gasPrice: Double? = null) { - companion object { - private const val DEFAULT_FEE_ADJUSTMENT = 1.25 - private const val DEFAULT_GAS_PRICE = 1905.00 - } - - fun limit() = ceil(estimate * feeAdjustment).toLong() - fun fees() = ceil(estimate * feeAdjustment * (gasPrice ?: DEFAULT_GAS_PRICE)).toLong() -} +import kotlin.time.Duration.Companion.hours fun Collection.toTxBody(): TxBody = TxBody.newBuilder() .addAllMessages(this.map { it.toAny() }) @@ -65,48 +56,35 @@ fun Collection.toTxBody(): TxBody = TxBody.newBuilder() fun Message.toAny(typeUrlPrefix: String = "") = Any.pack(this, typeUrlPrefix) class ProvenanceClient(channel: ManagedChannel, val logger: Logger, val location: P8eLocationExtension) { - - private val metadataClient = QueryGrpc.newBlockingStub(channel) - private val serviceClient = ServiceGrpc.newBlockingStub(channel) - private val authClient = cosmos.auth.v1beta1.QueryGrpc.newBlockingStub(channel) + private val inner = PbClient( + location.chainId!!, + URI(location.provenanceUrl!!), + floatingGasPrices( + GasEstimationMethod.MSG_FEE_CALCULATION, + constGasPrice( + location.txGasPrice?.takeIf{ gasPriceString -> gasPriceString.isNotBlank() }?.toDouble()?.toCoin("nhash")?.also { logger.info("Using provided gas price of ${it.amount}${it.denom}") } ?: + CoinOuterClass.Coin.newBuilder().setAmount("19050").setDenom("nhash").build().also { logger.info("Using default gas price of ${it.amount}${it.denom}") } + ) + ) + ) fun scopeSpecification(request: ScopeSpecificationRequest): ScopeSpecificationResponse = - metadataClient.withDeadlineAfter(10, TimeUnit.SECONDS).scopeSpecification(request) + inner.metadataClient.withDeadlineAfter(10, TimeUnit.SECONDS).scopeSpecification(request) fun contractSpecification(request: ContractSpecificationRequest): ContractSpecificationResponse = - metadataClient.withDeadlineAfter(10, TimeUnit.SECONDS).contractSpecification(request) + inner.metadataClient.withDeadlineAfter(10, TimeUnit.SECONDS).contractSpecification(request) private class SequenceMismatch(message: String): Exception(message) - fun writeTx(address: String, signer: SignerMeta, txBody: TxBody) { + fun writeTx(signer: BaseReqSigner, txBody: TxBody) { retryForException(SequenceMismatch::class.java, 5) { - val accountInfo = authClient.withDeadlineAfter(10, TimeUnit.SECONDS) - .account( - QueryOuterClass.QueryAccountRequest.newBuilder() - .setAddress(address) - .build() - ).run { account.unpack(Auth.BaseAccount::class.java) } - val signedSimulateTx = - signTx(txBody, accountInfo.accountNumber, accountInfo.sequence, signer) - val estimate = serviceClient.withDeadlineAfter(10, TimeUnit.SECONDS) - .simulate(SimulateRequest.newBuilder().setTx(signedSimulateTx).build()) - .let { GasEstimate(it.gasInfo.gasUsed, gasPrice = location.txGasPrice?.takeIf{ gasPriceString -> gasPriceString.isNotBlank() }?.toDouble()) } - - logger.trace("signed tx = $signedSimulateTx") - - val signedTx = signTx( + val response = inner.estimateAndBroadcastTx( txBody, - accountInfo.accountNumber, - accountInfo.sequence, - signer, - gasEstimate = estimate.copy(feeAdjustment = location.txFeeAdjustment.toDouble()) + signers = listOf(signer), + mode = BroadcastMode.BROADCAST_MODE_BLOCK, // faux block, will poll in background + gasAdjustment = location.txFeeAdjustment.toDouble(), + txHashHandler = { logger.trace("Preparing to broadcast $it") } ) - val response = serviceClient.withDeadlineAfter(20, TimeUnit.SECONDS) - .broadcastTx( - BroadcastTxRequest.newBuilder() - .setTxBytes(ByteString.copyFrom(signedTx.toByteArray())) - .setMode(BroadcastMode.BROADCAST_MODE_SYNC) - .build() - ) + if (response.txResponse.code != 0) { val message = "error broadcasting tx (code ${response.txResponse.code}, rawLog: ${response.txResponse.rawLog})" if (response.txResponse.rawLog.contains("account sequence mismatch")) { @@ -116,29 +94,7 @@ class ProvenanceClient(channel: ManagedChannel, val logger: Logger, val location } logger.info("sent tx = ${response.txResponse.txhash}") - lateinit var tx: ServiceOuterClass.GetTxResponse - var numPolls = 0 - do { - tx = try { - if (++numPolls > 25) { - throw Exception("Exceeded maximum number of polls for transaction ${response.txResponse.txhash}") - } - serviceClient.getTx(response.txResponse.txhash) - } catch (e: StatusRuntimeException) { - if (e.status.code != Status.NOT_FOUND.code) { - throw e - } - ServiceOuterClass.GetTxResponse.getDefaultInstance() - } - if (tx.txResponse.code > 0) { - // transaction errored - logger.warn("Could not persist batch: ${tx.txResponse}") - throw Exception("transaction error (code ${tx.txResponse.code}, rawLog: ${tx.txResponse.rawLog})") - } - Thread.sleep(1000) - } while (tx.txResponse.height <= 0) - - logger.trace("tx response = ${tx.txResponse}") + logger.trace("tx response = ${response.txResponse}") } } @@ -160,166 +116,4 @@ class ProvenanceClient(channel: ManagedChannel, val logger: Logger, val location } throw lastException ?: Exception("retry limit reached without a last exception: should not get here") } - - private fun signTx( - body: TxBody, - accountNumber: Long, - sequenceNumber: Long, - signer: SignerMeta, - gasEstimate: GasEstimate = GasEstimate(0), - ): Tx { - val authInfo = AuthInfo.newBuilder() - .setFee( - Fee.newBuilder() - .addAmount( - CoinOuterClass.Coin.newBuilder() - .setDenom("nhash") - .setAmount(gasEstimate.fees().toString()) - .build() - ).setGasLimit(gasEstimate.limit()) - ) - .addSignerInfos( - SignerInfo.newBuilder() - .setPublicKey( - Keys.PubKey.newBuilder() - .setKey(ByteString.copyFrom(signer.compressedPublicKey)) - .build() - .toAny() - ) - .setModeInfo( - ModeInfo.newBuilder() - .setSingle(ModeInfo.Single.newBuilder().setMode(Signing.SignMode.SIGN_MODE_DIRECT).build()) - .build() - ) - .setSequence(sequenceNumber) - .build() - ).build() - - val signatures = SignDoc.newBuilder() - .setBodyBytes(body.toByteString()) - .setAuthInfoBytes(authInfo.toByteString()) - .setChainId(location.chainId) - .setAccountNumber(accountNumber) - .build() - .toByteArray() - .let { signer.sign(it) } - .map { ByteString.copyFrom(it.signature) } - - return Tx.newBuilder() - .setBody(body) - .setAuthInfo(authInfo) - .addAllSignatures(signatures) - .build() - } -} - -typealias SignerFn = (ByteArray) -> List -object PbSigner { - fun signerFor(keyPair: KeyPair): SignerFn = { bytes -> - bytes.sha256().let { - val privateKey = (keyPair.private as BCECPrivateKey).s - StdSignature( - pub_key = StdPubKey("tendermint/PubKeySecp256k1", (keyPair.public as BCECPublicKey).q.getEncoded(true)), - signature = EllipticCurveSigner().sign(it, privateKey, true).encodeAsBTC() - ) - }.let { - listOf(it) - } - } -} - -data class SignerMeta(val compressedPublicKey: ByteArray, val sign: SignerFn) { - override fun equals(other: kotlin.Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as SignerMeta - - if (!compressedPublicKey.contentEquals(other.compressedPublicKey)) return false - if (sign != other.sign) return false - - return true - } - - override fun hashCode(): Int { - var result = compressedPublicKey.contentHashCode() - result = 31 * result + sign.hashCode() - return result - } -} - -fun KeyPair.toSignerMeta() = SignerMeta((public as BCECPublicKey).q.getEncoded(true), PbSigner.signerFor(this)) - -data class StdSignature( - val pub_key: StdPubKey, - val signature: ByteArray -) - -@JsonDeserialize(using = PubKeyDeserializer::class) -data class StdPubKey( - val type: String, - @JsonAlias("data") - val value: ByteArray?= ByteArray(0) -) - -/** - * Public key used to be a string, now it is an object, since all Markers are accounts, - * and they still send the public_key is sent as a empty string. - * This deserialize just checks that if the json string is empty, it returns a null so that the - * default value can be used. - */ -internal class PubKeyDeserializer : JsonDeserializer() { - @Throws(IOException::class, JsonProcessingException::class) - override fun deserialize(jsonParser: JsonParser, context: DeserializationContext?): StdPubKey? { - val node: JsonNode = jsonParser.readValueAsTree() - return if (node.asText().isEmpty() && node.toList().isEmpty()) { - null - } else { - StdPubKey(node.get("type").asText(),node.get("value").asText().toByteArray(Charsets.UTF_8)) - } - } -} - -/** - * encodeAsBTC returns the ECDSA signature as a ByteArray of r || s, - * where both r and s are encoded into 32 byte big endian integers. - */ -fun ECDSASignature.encodeAsBTC(): ByteArray { - // Canonicalize - In order to remove malleability, - // we set s = curve_order - s, if s is greater than curve.Order() / 2. - var sigS = this.s - if (sigS > HALF_CURVE_ORDER) { - sigS = CURVE.n.subtract(sigS) - } - - val sBytes = sigS.getUnsignedBytes() - val rBytes = this.r.getUnsignedBytes() - - require(rBytes.size <= 32) { "cannot encode r into BTC Format, size overflow (${rBytes.size} > 32)" } - require(sBytes.size <= 32) { "cannot encode s into BTC Format, size overflow (${sBytes.size} > 32)" } - - val signature = ByteArray(64) - // 0 pad the byte arrays from the left if they aren't big enough. - System.arraycopy(rBytes, 0, signature, 32 - rBytes.size, rBytes.size) - System.arraycopy(sBytes, 0, signature, 64 - sBytes.size, sBytes.size) - - return signature -} - - -private val HALF_CURVE_ORDER = CURVE.n.shiftRight(1) -const val ZERO = 0x0.toByte() - -/** - * Returns the bytes from a BigInteger as an unsigned version by truncating a byte if needed. - */ -fun BigInteger.getUnsignedBytes(): ByteArray { - val bytes = this.toByteArray(); - - if (bytes[0] == ZERO) - { - return bytes.drop(1).toByteArray() - } - - return bytes; -} +} \ No newline at end of file