Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data access #64

Open
wants to merge 17 commits into
base: os-locator
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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,33 @@ 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()) {
val envelopeDataAccess = envelope.data.input.affiliateSharesList.map {
wbaker-figure marked this conversation as resolved.
Show resolved Hide resolved
affiliateService.getAddress(
it.toPublicKey(), chaincodeProperties.mainNet
wbaker-figure marked this conversation as resolved.
Show resolved Hide resolved
)
}
val existingScopeDataAccess = provenanceGrpcService.retrieveScopeData(envelope.data.input.scope.uuid.value).scope.scope.dataAccessList
// Only perform job if data access will be updated
if (envelopeDataAccess.any { it !in existingScopeDataAccess }) {
p8e.Jobs.MsgAddScopeDataAccessRequest.newBuilder()
.addAllDataAccess(envelopeDataAccess)
.addAllSigners(envelope.data.result.signaturesList.map {
it.signer.signingPublicKey.toPublicKey().let {
affiliateService.getAddress(it, 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() }
wbaker-figure marked this conversation as resolved.
Show resolved Hide resolved
?.let { dataAccessService.addDataAccess(it) }
}
}
} else {
log.warn("Skipping ES indexing for stale scope")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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

@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
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)[0].amount.toLong() / 1000000000 < 10) {
wbaker-figure marked this conversation as resolved.
Show resolved Hide resolved
waitForTx {
transferHash(affiliateAccount.address, 10000000000)
}
}
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 @@ -60,8 +60,10 @@ class EnvelopeService(
val signingKeyPair = affiliateService.getSigningKeyPair(publicKey)
val pen = Pen(signingKeyPair.private, signingKeyPair.public)

val affiliateShares = affiliateService.getShares(publicKey);
// Update the envelope for invoker and recitals with correct signing and encryption keys.
val envelope = env.toBuilder()
.addAllAffiliateShares(affiliateShares.map{ it.publicKey.toPublicKeyProto() })
.apply {
if(env.contract.startTime == Timestamp.getDefaultInstance()) {
contractBuilder.startTime = OffsetDateTime.now().toProtoTimestampProv()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import io.provenance.engine.crypto.toSignerMeta
import io.provenance.engine.util.toP8e
import io.provenance.metadata.v1.ContractSpecificationRequest
import io.provenance.metadata.v1.ScopeRequest
import io.provenance.metadata.v1.ScopeResponse
import io.provenance.p8e.shared.extension.logger
import io.provenance.p8e.shared.service.AffiliateService
import io.provenance.pbc.clients.roundUp
Expand All @@ -39,10 +40,11 @@ import org.springframework.stereotype.Service
import java.net.URI
import java.security.KeyPair
import java.util.concurrent.TimeUnit
import java.util.logging.Level
import java.util.logging.Logger
import cosmos.base.tendermint.v1beta1.ServiceGrpc as NodeGrpc
import cosmos.tx.v1beta1.ServiceGrpc as TxGrpc
import cosmos.bank.v1beta1.QueryGrpc as BankQueryGrpc
import cosmos.bank.v1beta1.QueryOuterClass.QueryAllBalancesRequest
import cosmos.base.v1beta1.CoinOuterClass.Coin
import io.provenance.metadata.v1.QueryGrpc as MetadataQueryGrpc

@Service
Expand Down Expand Up @@ -75,6 +77,7 @@ class ProvenanceGrpcService(
.keepAliveTimeout(20, TimeUnit.SECONDS)
.build()
}
private val bankClient = BankQueryGrpc.newBlockingStub(channel)

private val txService = TxGrpc.newBlockingStub(channel)
private val accountService = QueryGrpc.newBlockingStub(channel)
Expand Down Expand Up @@ -161,14 +164,7 @@ class ProvenanceGrpcService(
}

fun retrieveScope(address: String): ContractScope.Scope {
val scopeResponse = metadataQueryService.scope(
ScopeRequest.newBuilder()
.setScopeId(address)
.setIncludeSessions(true)
.setIncludeRecords(true)
.build()
)

val scopeResponse = retrieveScopeData(address)
val contractSpecHashLookup = scopeResponse.sessionsList
.map { it.contractSpecIdInfo.contractSpecAddr }
.toSet()
Expand All @@ -181,6 +177,21 @@ class ProvenanceGrpcService(

return scopeResponse.toP8e(contractSpecHashLookup, affiliateService)
}

fun retrieveScopeData(address: String): ScopeResponse {
val scopeResponse = metadataQueryService.scope(
ScopeRequest.newBuilder()
.setScopeId(address)
.setIncludeSessions(true)
.setIncludeRecords(true)
.build()
)

return scopeResponse
}

fun getAccountCoins(bech32Address: String): List<Coin> =
bankClient.allBalances(QueryAllBalancesRequest.newBuilder().setAddress(bech32Address).build()).balancesList
}

fun Collection<Message>.toTxBody(): TxBody = TxBody.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import io.provenance.p8e.shared.domain.ScopeSpecificationRecord
import io.provenance.p8e.shared.service.AffiliateService
import org.jetbrains.exposed.sql.transactions.transaction
import java.lang.Long.max
import java.nio.ByteBuffer
import java.util.*

const val PROV_METADATA_PREFIX_CONTRACT_SPEC: Byte = 0x03
const val PROV_METADATA_PREFIX_SCOPE_ADDR: Byte = 0x00

fun PartyType.toProv() = when (this) {
PartyType.SERVICER -> ProvenancePartyType.PARTY_TYPE_SERVICER
Expand All @@ -41,6 +44,15 @@ fun ContractSpec.toProvHash(): String {
return String(provHash.base64Encode())
}

fun UUID.asBytes(): ByteArray {
val b = ByteBuffer.wrap(ByteArray(16))
b.putLong(mostSignificantBits)
b.putLong(leastSignificantBits)
return b.array()
}

fun UUID.toAddress(prefix: Byte): ByteArray = (listOf(prefix) + this.asBytes().toList()).toByteArray()

fun ContractSpec.toProv(): io.provenance.metadata.v1.p8e.ContractSpec = io.provenance.metadata.v1.p8e.ContractSpec.parseFrom(toByteArray())

fun Contracts.Contract.toProv(): io.provenance.metadata.v1.p8e.Contract = io.provenance.metadata.v1.p8e.Contract.parseFrom(toByteArray()).run {
Expand Down
2 changes: 2 additions & 0 deletions p8e-proto-internal/src/main/proto/p8e/contract_scope.proto
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ message Envelope {
// Scope snapshot for executing
Scope scope = 8;

repeated PublicKey affiliate_shares = 10;

Status status = 9;

enum Status {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class AffiliateService(
}

@Cacheable(PUBLIC_KEY_TO_ADDRESS)
private fun getAddress(publicKey: PublicKey, mainNet: Boolean): String =
fun getAddress(publicKey: PublicKey, mainNet: Boolean): String =
publicKey.let {
(it as BCECPublicKey).q.getEncoded(true)
}.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.provenance.p8e.shared.service

import io.p8e.util.toPublicKeyProto
import io.provenance.p8e.shared.domain.JobRecord
import org.jetbrains.exposed.sql.transactions.transaction
import java.security.PublicKey
import org.springframework.stereotype.Service
import p8e.Jobs

@Service
class DataAccessService {
fun addDataAccess(dataAccess: Jobs.MsgAddScopeDataAccessRequest) = transaction {
JobRecord.create(Jobs.P8eJob.newBuilder()
.setMsgAddScopeDataAccessRequest(dataAccess)
.build()
)
}
}
Loading