Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ import io.provenance.engine.grpc.interceptors.JwtServerInterceptor
import io.provenance.engine.grpc.interceptors.UnhandledExceptionInterceptor
import io.provenance.engine.index.query.Operation
import io.provenance.engine.index.query.OperationDeserializer
import io.provenance.engine.service.DataDogMetricCollector
import io.provenance.engine.service.JobHandlerService
import io.provenance.engine.service.JobHandlerServiceFactory
import io.provenance.engine.service.LogFileMetricCollector
import io.provenance.engine.service.MetricsService
import io.provenance.engine.service.OSLocatorChaincodeService
import io.provenance.engine.service.*
import io.provenance.p8e.shared.util.KeyClaims
import io.provenance.p8e.shared.util.TokenManager
import io.provenance.p8e.shared.state.EnvelopeStateEngine
Expand Down Expand Up @@ -217,9 +212,11 @@ class AppConfig : WebMvcConfigurer {
}

@Bean
fun jobHandlerServiceFactory(osLocatorChaincodeService: OSLocatorChaincodeService): JobHandlerServiceFactory = { payload ->
fun jobHandlerServiceFactory(osLocatorChaincodeService: OSLocatorChaincodeService,
dataAccessChaincodeService: DataAccessChaincodeService): JobHandlerServiceFactory = { payload ->
when (payload.jobCase) {
Jobs.P8eJob.JobCase.ADDAFFILIATEOSLOCATOR -> osLocatorChaincodeService
Jobs.P8eJob.JobCase.MSGADDSCOPEDATAACCESSREQUEST -> dataAccessChaincodeService
else -> throw IllegalArgumentException("No handler registered for job of type ${payload.jobCase.name}")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ import io.provenance.p8e.shared.domain.EnvelopeRecord
import io.provenance.p8e.shared.index.ScopeEventType
import io.provenance.p8e.shared.util.P8eMDC
import io.p8e.proto.Util.UUID
import io.provenance.engine.config.ChaincodeProperties
import io.provenance.engine.service.ProvenanceGrpcService
import io.provenance.engine.util.PROV_METADATA_PREFIX_SCOPE_ADDR
import io.provenance.engine.util.toAddress
import io.provenance.metadata.v1.MsgAddScopeDataAccessRequest
import io.provenance.p8e.shared.service.DataAccessService
import org.elasticsearch.action.DocWriteRequest.OpType
import org.elasticsearch.action.index.IndexRequest
import org.elasticsearch.client.RequestOptions
import org.elasticsearch.client.RestHighLevelClient
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.MDC
import org.springframework.stereotype.Component
import java.security.KeyPair
import java.security.PublicKey
import java.time.OffsetDateTime
import kotlin.math.max
Expand All @@ -46,8 +53,11 @@ class IndexHandler(
private val esClient: RestHighLevelClient,
private val eventService: EventService,
private val protoIndexer: ProtoIndexer,
private val affiliateService: AffiliateService
) {
private val affiliateService: AffiliateService,
private val dataAccessService: DataAccessService,
private val chaincodeProperties: ChaincodeProperties,
private val provenanceGrpcService: ProvenanceGrpcService,
) {
private val log = logger()

init {
Expand Down Expand Up @@ -97,7 +107,6 @@ class IndexHandler(
} as ContractScope.RecordGroup

val document = baseDocument.copy(classname = recordGroup.classname, specification = recordGroup.specification)

transaction {
with(scope.lastEvent) {
EnvelopeRecord.findByExecutionUuid(
Expand All @@ -114,6 +123,10 @@ class IndexHandler(
envelope.data.result.contract.recitalsList.map { it.signer.encryptionPublicKey.toPublicKey().toSha512Hex() }
), RequestOptions.DEFAULT
)
// If the env is the invoker, create the data access message and put into a job.
if(envelope.isInvoker == true && envelope.data.input.affiliateSharesList.isNotEmpty()) {
updateDataAccess(envelope)
}
} else {
log.warn("Skipping ES indexing for stale scope")
}
Expand Down Expand Up @@ -169,4 +182,40 @@ class IndexHandler(
request.opType(OpType.INDEX)
return request
}

private fun updateDataAccess(envelope: EnvelopeRecord) {
val scopeData = provenanceGrpcService.retrieveScopeData(envelope.data.input.scope.uuid.value)
val envelopeDataAccess = envelope.data.input.affiliateSharesList.map {
affiliateSharePublicKey ->
affiliateService.getAddress(
affiliateSharePublicKey.toPublicKey(), chaincodeProperties.mainNet
)
}.filter {
it !in scopeData.scope.scope.dataAccessList
}
// Only perform job if data access will be updated
if (scopeData.scope.scope.ownersCount > 1) {
log.error("Multiparty contracts aren't currently supported for adding data access.")
}
else if (envelopeDataAccess.isNotEmpty()) {
p8e.Jobs.MsgAddScopeDataAccessRequest.newBuilder()
.addAllDataAccess(envelopeDataAccess)
.addAllSigners(envelope.data.result.signaturesList.map {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By any chance did you test this with a multiparty contract? I'm not positive, but I think the only signer we want is the scope owner. In the case for our scopes that's always a single invoker. If you only ran single party contracts, the envelope.data.result.signaturesList should only contain the invoker so it's not a big deal. It would take a multiparty contract to see if this fails or not as is.

signature ->
signature.signer.signingPublicKey.toPublicKey().let {
signerPublicKey ->
affiliateService.getAddress(signerPublicKey, chaincodeProperties.mainNet)
}
})
.setScopeId(
envelope.data.input.ref.scopeUuid.value.toUuidProv().toAddress(
PROV_METADATA_PREFIX_SCOPE_ADDR
).toByteString()
)
.setPublicKey(envelope.data.input.contract.invoker.encryptionPublicKey)
.build().takeIf { envelope.data.input.affiliateSharesList.isNotEmpty() }
?.let {
dataAccessService.addDataAccess(it) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import io.p8e.grpc.complete
import io.p8e.grpc.publicKey
import io.p8e.proto.Affiliate
import io.p8e.proto.Affiliate.AffiliateContractWhitelist
import io.p8e.proto.Affiliate.AffiliateSharesResponse
import io.p8e.proto.AffiliateServiceGrpc.AffiliateServiceImplBase
import io.p8e.util.computePublicKey
import io.p8e.util.toHex
import io.p8e.util.toPrivateKey
import io.p8e.util.toPublicKeyProto
import io.provenance.p8e.shared.extension.logger
import io.provenance.engine.grpc.interceptors.JwtServerInterceptor
import io.provenance.engine.grpc.interceptors.UnhandledExceptionInterceptor
import io.provenance.p8e.shared.service.AffiliateService
import io.provenance.engine.service.MailboxService
import io.provenance.p8e.shared.util.P8eMDC
import org.jetbrains.exposed.sql.transactions.transaction
import org.lognet.springboot.grpc.GRpcService
Expand All @@ -41,11 +42,6 @@ class AffiliateGrpc(

log.info("Saving affiliate encryption key: ${ecPublicKey.toHex()}")

// TODO rethink this in decentralized system
// if (mailboxService.encryptionPublicKeyExists(ecPublicKey)) {
// throw IllegalArgumentException("Derived encryption public key [${ecPublicKey.toHex()}] is not allowed, please choose another.")
// }

transaction {
affiliateService.save(
signingKeyPair = KeyPair(publicKey, privateKey),
Expand All @@ -55,6 +51,21 @@ class AffiliateGrpc(
responseObserver.complete()
}

override fun shares(
request: Empty,
responseObserver: StreamObserver<AffiliateSharesResponse>
) {
// TODO public key needs to be the authenticated public key
val sharePublicKeys = transaction { affiliateService.getShares(publicKey()).map { it.publicKey } }

responseObserver.onNext(
AffiliateSharesResponse.newBuilder()
.addAllShares(sharePublicKeys.map { it.toPublicKeyProto() })
.build()
)
responseObserver.onCompleted()
}

override fun whitelistClass(
request: AffiliateContractWhitelist,
responseObserver: StreamObserver<Empty>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ class ObjectGrpc(
val msg = request.message.toByteArray()
val encryptionKeyPair = transaction { affiliateService.getEncryptionKeyPair(publicKey()) }
val signingKeyPair = transaction { affiliateService.getSigningKeyPair(publicKey()) }
val affiliateShares = transaction { affiliateService.getSharePublicKeys(request.toAudience().plus(publicKey())) }

// Update the dime's audience list to use encryption public keys.
val audience = request.toAudience().plus(affiliateShares.value).map {
val audience = request.toAudience().map {
transaction {
try {
affiliateService.getEncryptionKeyPair(it).public
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package io.provenance.engine.service

import cosmos.bank.v1beta1.Tx
import cosmos.base.abci.v1beta1.Abci
import cosmos.base.v1beta1.CoinOuterClass
import cosmos.tx.v1beta1.ServiceOuterClass
import io.p8e.crypto.Hash
import io.p8e.util.*
import io.provenance.engine.crypto.toSignerMeta
import io.provenance.engine.config.ChaincodeProperties
import io.provenance.engine.crypto.Account
import io.provenance.engine.crypto.Bech32
import io.provenance.engine.crypto.toBech32Data
import io.provenance.p8e.shared.extension.logger
import io.provenance.metadata.v1.*
import io.provenance.p8e.shared.service.AffiliateService
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.jetbrains.exposed.sql.transactions.transaction
import org.springframework.stereotype.Service
import p8e.Jobs
import java.security.KeyPair
import java.security.PublicKey

// One to One Billion ratio from hash to nhash
private const val HASH_TO_NHASH = 1000000000L

@Service
class DataAccessChaincodeService(
private val chaincodeProperties: ChaincodeProperties,
private val p8eAccount: Account,
private val provenanceGrpcService: ProvenanceGrpcService,
private val affiliateService: AffiliateService,
private val chaincodeInvokeService: ChaincodeInvokeService,
) : JobHandlerService {
private val log = logger()

override fun handle(payload: Jobs.P8eJob) {
val msgAddScopeDataAccessRequest = MsgAddScopeDataAccessRequest.newBuilder().addAllDataAccess(payload.msgAddScopeDataAccessRequest.dataAccessList)
.addAllSigners(payload.msgAddScopeDataAccessRequest.signersList)
.setScopeId(payload.msgAddScopeDataAccessRequest.scopeId)
.build()
val publicKey = payload.msgAddScopeDataAccessRequest.publicKey.toPublicKey()

val affiliate =
transaction { affiliateService.get(publicKey) }.orThrowNotFound("Affiliate with public key ${publicKey.toHex()} not found")
val affiliateKeyPair =
KeyPair(affiliate.publicKey.value.toJavaPublicKey(), affiliate.privateKey.toJavaPrivateKey())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to add SmartKey support here as well ... probably setup data under KeyRef data class (hopefully this branch goes out before mine so you don't have to worry about it) :)

Essentially instead of a KeyPair, we have KeyRef that contains key information such as public key, maybe private key?, UUID (SmartKey way of identifying a private key stored on their system).


// Estimate transaction cost
val affiliateAddress = publicKey.toBech32Address(chaincodeProperties.mainNet)
val affiliateAccount = provenanceGrpcService.accountInfo(affiliateAddress)

val estimate = provenanceGrpcService.estimateTx(
msgAddScopeDataAccessRequest.toTxBody(),
affiliateAccount.accountNumber,
affiliateAccount.sequence
)

// perform and wait for hash transfer to complete if account has less than 10 hash
if ((provenanceGrpcService.getAccountCoins(affiliateAddress).find {it.denom == "nhash"}?.amount?.toLong()
?: 0L) / HASH_TO_NHASH < 10
) {
waitForTx {
transferHash(affiliateAccount.address, 100 * HASH_TO_NHASH)
}
}
val resp = provenanceGrpcService.batchTx(
msgAddScopeDataAccessRequest.toTxBody(),
affiliateAccount.accountNumber,
affiliateAccount.sequence,
estimate,
affiliateKeyPair.toSignerMeta()
)
if (resp.txResponse.code != 0) {
// adding extra raw logging during exceptional cases so that we can see what typical responses look like while this interface is new
log.info("Abci.TxResponse from chain ${resp.txResponse}")

val errorMessage = "${resp.txResponse.code} - ${resp.txResponse.rawLog}"

throw IllegalStateException(errorMessage)
}
}

fun transferHash(toAddress: String, amount: Long) = Tx.MsgSend.newBuilder()
.addAllAmount(listOf(
CoinOuterClass.Coin.newBuilder()
.setAmount(amount.toString())
.setDenom("nhash")
.build()
)).setFromAddress(p8eAccount.bech32Address())
.setToAddress(toAddress)
.build().let {
chaincodeInvokeService.batchTx(it.toTxBody())
}

fun waitForTx(block: () -> ServiceOuterClass.BroadcastTxResponse): Abci.TxResponse {
val txResponse = block()
val txHash = txResponse.txResponse.txhash

if (txResponse.txResponse.code != 0) {
throw Exception("Error submitting transaction [code = ${txResponse.txResponse.code}, codespace = ${txResponse.txResponse.codespace}, raw_log = ${txResponse.txResponse.rawLog}]")
}

val maxAttempts = 5
log.info("Waiting for transaction to complete [hash = $txHash]")
for (i in 1 .. maxAttempts) {
Thread.sleep(2500)
val response = try {
provenanceGrpcService.getTx(txHash)
} catch (t: Throwable) {
log.info("Error fetching transaction [hash = $txHash, message = ${t.message}]")
continue
}

when {
response.code == 0 -> {
log.info("Transaction complete [hash = $txHash]")
return response
}
response.code > 0 -> throw Exception("Transaction Failed with log ${response.rawLog}")
else -> continue // todo: what are the failure conditions, non-0 code... tx not found... under which conditions might it eventually succeed, not found needs a retry?
}
}
throw Exception("Failed to fetch transaction after $maxAttempts attempts [hash = $txHash]")
}

// todo: this should really be somewhere more shared... but p8e-util where other key conversion extensions are doesn't have the Hash class...
private fun PublicKey.toBech32Address(mainNet: Boolean): String =
(this as BCECPublicKey).q.getEncoded(true)
.let {
Hash.sha256hash160(it)
}.let {
val prefix = if (mainNet) Bech32.PROVENANCE_MAINNET_ACCOUNT_PREFIX else Bech32.PROVENANCE_TESTNET_ACCOUNT_PREFIX
it.toBech32Data(prefix).address
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import io.p8e.proto.Events.P8eEvent.Event.ENVELOPE_MAILBOX_OUTBOUND
import io.p8e.proto.PK
import io.p8e.util.*
import io.provenance.p8e.shared.domain.EnvelopeRecord
import io.provenance.p8e.shared.domain.EnvelopeTable
import io.provenance.p8e.shared.domain.ScopeRecord
import io.provenance.engine.extension.*
import io.provenance.engine.grpc.v1.toEvent
Expand Down Expand Up @@ -62,33 +61,33 @@ class EnvelopeService(

// Update the envelope for invoker and recitals with correct signing and encryption keys.
val envelope = env.toBuilder()
.apply {
if(env.contract.startTime == Timestamp.getDefaultInstance()) {
contractBuilder.startTime = OffsetDateTime.now().toProtoTimestampProv()
}
contractBuilder
.clearInvoker()
.setInvoker(
PK.SigningAndEncryptionPublicKeys.newBuilder()
.setEncryptionPublicKey(affiliateService.getEncryptionKeyPair(env.contract.invoker.encryptionPublicKey.toPublicKey()).public.toPublicKeyProto())
.setSigningPublicKey(affiliateService.getSigningKeyPair(env.contract.invoker.signingPublicKey.toPublicKey()).public.toPublicKeyProto())
.build()
)
.clearRecitals()
.addAllRecitals(
env.contract.recitalsList.map {
it.toBuilder()
.setSignerRole(it.signerRole)
.setAddress(it.address)
.setSigner(
PK.SigningAndEncryptionPublicKeys.newBuilder()
.setSigningPublicKey(affiliateService.getSigningKeyPair(it.signer.signingPublicKey.toPublicKey()).public.toPublicKeyProto())
.setEncryptionPublicKey(affiliateService.getEncryptionKeyPair(it.signer.encryptionPublicKey.toPublicKey()).public.toPublicKeyProto())
.build()
).build()
}
)
}.build()
.apply {
if(env.contract.startTime == Timestamp.getDefaultInstance()) {
contractBuilder.startTime = OffsetDateTime.now().toProtoTimestampProv()
}
contractBuilder
.clearInvoker()
.setInvoker(
PK.SigningAndEncryptionPublicKeys.newBuilder()
.setEncryptionPublicKey(affiliateService.getEncryptionKeyPair(env.contract.invoker.encryptionPublicKey.toPublicKey()).public.toPublicKeyProto())
.setSigningPublicKey(affiliateService.getSigningKeyPair(env.contract.invoker.signingPublicKey.toPublicKey()).public.toPublicKeyProto())
.build()
)
.clearRecitals()
.addAllRecitals(
env.contract.recitalsList.map {
it.toBuilder()
.setSignerRole(it.signerRole)
.setAddress(it.address)
.setSigner(
PK.SigningAndEncryptionPublicKeys.newBuilder()
.setSigningPublicKey(affiliateService.getSigningKeyPair(it.signer.signingPublicKey.toPublicKey()).public.toPublicKeyProto())
.setEncryptionPublicKey(affiliateService.getEncryptionKeyPair(it.signer.encryptionPublicKey.toPublicKey()).public.toPublicKeyProto())
.build()
).build()
}
)
}.build()

val result = timed("EnvelopeService_contractEngine_handle") {
ContractEngine(osClient, affiliateService).handle(
Expand Down
Loading