diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/config/AppConfig.kt b/p8e-api/src/main/kotlin/io/provenance/engine/config/AppConfig.kt index 1c68575e..79932cac 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/config/AppConfig.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/config/AppConfig.kt @@ -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 @@ -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}") } } diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/event/IndexHandler.kt b/p8e-api/src/main/kotlin/io/provenance/engine/event/IndexHandler.kt index 5bf0bfb6..e82f1ab7 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/event/IndexHandler.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/event/IndexHandler.kt @@ -18,6 +18,12 @@ 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 @@ -25,6 +31,7 @@ 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 @@ -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 { @@ -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( @@ -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") } @@ -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 { + 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) } + } + } } diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/grpc/v1/AffiliateGrpc.kt b/p8e-api/src/main/kotlin/io/provenance/engine/grpc/v1/AffiliateGrpc.kt index f6133751..6002eaf0 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/grpc/v1/AffiliateGrpc.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/grpc/v1/AffiliateGrpc.kt @@ -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 @@ -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), @@ -55,6 +51,21 @@ class AffiliateGrpc( responseObserver.complete() } + override fun shares( + request: Empty, + responseObserver: StreamObserver + ) { + // 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 diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/grpc/v1/ObjectGrpc.kt b/p8e-api/src/main/kotlin/io/provenance/engine/grpc/v1/ObjectGrpc.kt index 4173da90..0ea07bb1 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/grpc/v1/ObjectGrpc.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/grpc/v1/ObjectGrpc.kt @@ -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 diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/service/DataAccessChaincodeService.kt b/p8e-api/src/main/kotlin/io/provenance/engine/service/DataAccessChaincodeService.kt new file mode 100644 index 00000000..74e01319 --- /dev/null +++ b/p8e-api/src/main/kotlin/io/provenance/engine/service/DataAccessChaincodeService.kt @@ -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()) + + // 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 + } +} diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/service/EnvelopeService.kt b/p8e-api/src/main/kotlin/io/provenance/engine/service/EnvelopeService.kt index cdceafbc..14c0ceea 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/service/EnvelopeService.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/service/EnvelopeService.kt @@ -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 @@ -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( diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/service/ProvenanceGrpcService.kt b/p8e-api/src/main/kotlin/io/provenance/engine/service/ProvenanceGrpcService.kt index a5b5ef4c..83859fcd 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/service/ProvenanceGrpcService.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/service/ProvenanceGrpcService.kt @@ -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 @@ -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 @@ -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) @@ -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() @@ -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 = + bankClient.allBalances(QueryAllBalancesRequest.newBuilder().setAddress(bech32Address).build()).balancesList } fun Collection.toTxBody(): TxBody = TxBody.newBuilder() diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/util/ProvenanceProtoExtensions.kt b/p8e-api/src/main/kotlin/io/provenance/engine/util/ProvenanceProtoExtensions.kt index 55caeac6..4f098e3c 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/util/ProvenanceProtoExtensions.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/util/ProvenanceProtoExtensions.kt @@ -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 @@ -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 { diff --git a/p8e-common/src/main/kotlin/io/p8e/grpc/client/SigningAndEncryptionPublicKeysClient.kt b/p8e-common/src/main/kotlin/io/p8e/grpc/client/SigningAndEncryptionPublicKeysClient.kt index 27c9498a..06e1bb24 100644 --- a/p8e-common/src/main/kotlin/io/p8e/grpc/client/SigningAndEncryptionPublicKeysClient.kt +++ b/p8e-common/src/main/kotlin/io/p8e/grpc/client/SigningAndEncryptionPublicKeysClient.kt @@ -1,7 +1,9 @@ package io.p8e.grpc.client +import com.google.protobuf.Empty import io.grpc.ManagedChannel import io.p8e.proto.Affiliate +import io.p8e.proto.Affiliate.AffiliateSharesResponse import io.p8e.proto.AffiliateServiceGrpc import java.util.concurrent.TimeUnit @@ -20,10 +22,15 @@ class SigningAndEncryptionPublicKeysClient ( .register(request) } + fun shares() : AffiliateSharesResponse { + return client.withDeadlineAfter(deadlineMs, TimeUnit.MILLISECONDS) + .shares(Empty.getDefaultInstance()) + } + fun whitelistClass( whitelist: Affiliate.AffiliateContractWhitelist ) { client.withDeadlineAfter(deadlineMs, TimeUnit.MILLISECONDS) .whitelistClass(whitelist) } -} \ No newline at end of file +} diff --git a/p8e-engine/src/main/kotlin/io/p8e/engine/ContractEngine.kt b/p8e-engine/src/main/kotlin/io/p8e/engine/ContractEngine.kt index 70e58554..1b898e3e 100644 --- a/p8e-engine/src/main/kotlin/io/p8e/engine/ContractEngine.kt +++ b/p8e-engine/src/main/kotlin/io/p8e/engine/ContractEngine.kt @@ -72,7 +72,6 @@ class ContractEngine( keyPair, memoryClassLoader, pen, - affiliateService.getSharePublicKeys(listOf(keyPair.public)), scope, signingKeyPair, spec @@ -86,7 +85,6 @@ class ContractEngine( keyPair: KeyPair, memoryClassLoader: MemoryClassLoader, pen: Pen, - shares: AffiliateSharePublicKeys, scope: Scope?, signingKeyPair: KeyPair, spec: ContractSpec @@ -125,23 +123,24 @@ class ContractEngine( contract.validateAgainst(spec) when (contract.type!!) { - Contracts.ContractType.CHANGE_SCOPE -> { - if (scope != null && - envelope.status == Envelope.Status.CREATED && - contract.invoker.encryptionPublicKey == keyPair.public.toPublicKeyProto() - ) { - val audience = scope.partiesList.map { it.signer.signingPublicKey } - .plus(contract.recitalsList.map { it.signer.signingPublicKey }) - .map { it.toPublicKey() } - .let { it + affiliateService.getSharePublicKeys(it).value } - .toSet() - - log.debug("Change scope ownership - adding ${audience.map { it.toHex() }} [scope: ${scope.uuid.value}] [executionUuid: ${envelope.executionUuid.value}]") - - this.getScopeData(keyPair, definitionService, scope) - .threadedMap(executor) { definitionService.save(keyPair, it, signingKeyPair, audience) } - } else { } - } + // Contracts.ContractType.CHANGE_SCOPE -> { + // if (scope != null && + // envelope.status == Envelope.Status.CREATED && + // contract.invoker.encryptionPublicKey == keyPair.public.toPublicKeyProto() + // ) { + // val audience = scope.partiesList.map { it.signer.signingPublicKey } + // .plus(contract.recitalsList.map { it.signer.signingPublicKey }) + // .map { it.toPublicKey() } + // .let { it + affiliateService.getSharePublicKeys(it).value } + // .toSet() + + // log.debug("Change scope ownership - adding ${audience.map { it.toHex() }} [scope: ${scope.uuid.value}] [executionUuid: ${envelope.executionUuid.value}]") + + // this.getScopeData(keyPair, definitionService, scope) + // .threadedMap(executor) { definitionService.save(keyPair, it, signingKeyPair, audience) } + // } else { } + // } + Contracts.ContractType.CHANGE_SCOPE -> throw IllegalStateException("CHANGE_SCOPE contract type is not implemented") Contracts.ContractType.FACT_BASED, Contracts.ContractType.UNRECOGNIZED -> Unit } as Unit @@ -187,7 +186,7 @@ class ContractEngine( definitionService, prerequisite.fact.name, result, - contract.toAudience(scope, shares), + contract.toAudience(envelope, scope), signingKeyPair, keyPair, scope @@ -228,7 +227,7 @@ class ContractEngine( definitionService, function.fact.name, result, - contract.toAudience(scope, shares), + contract.toAudience(envelope, scope), signingKeyPair, keyPair, scope @@ -342,9 +341,8 @@ class ContractEngine( data class ResultSetter(val setter: () -> Unit) -fun Contract.toAudience(scope: Scope?, shares: AffiliateSharePublicKeys): Set = recitalsList +fun Contract.toAudience(envelope: Envelope, scope: Scope?): Set = recitalsList .filter { it.hasSigner() } - //.map { it.signer.encryptionPublicKeyPem } .map { it.signer.encryptionPublicKey } .plus( scope?.partiesList @@ -354,7 +352,7 @@ fun Contract.toAudience(scope: Scope?, shares: AffiliateSharePublicKeys): Set Collection.threadedMap(executor: ExecutorService, fn: (T) -> K): Collection = diff --git a/p8e-proto-internal/src/main/proto/p8e/affiliate.proto b/p8e-proto-internal/src/main/proto/p8e/affiliate.proto index 3a866367..fef1ced4 100644 --- a/p8e-proto-internal/src/main/proto/p8e/affiliate.proto +++ b/p8e-proto-internal/src/main/proto/p8e/affiliate.proto @@ -12,6 +12,7 @@ option java_outer_classname = "Affiliate"; service AffiliateService { rpc Register (AffiliateRegisterRequest) returns (google.protobuf.Empty) {}; + rpc Shares (google.protobuf.Empty) returns (AffiliateSharesResponse) {}; rpc WhitelistClass (AffiliateContractWhitelist) returns (google.protobuf.Empty) {}; } @@ -20,6 +21,10 @@ message AffiliateRegisterRequest { PrivateKey encryption_private_key = 2; } +message AffiliateSharesResponse { + repeated PublicKey shares = 1; +} + message AffiliateContractWhitelist { string classname = 1; google.protobuf.Timestamp start_time = 2; diff --git a/p8e-proto-internal/src/main/proto/p8e/contract_scope.proto b/p8e-proto-internal/src/main/proto/p8e/contract_scope.proto index 2b91e0dd..ab0e1d07 100644 --- a/p8e-proto-internal/src/main/proto/p8e/contract_scope.proto +++ b/p8e-proto-internal/src/main/proto/p8e/contract_scope.proto @@ -50,6 +50,8 @@ message Envelope { // Scope snapshot for executing Scope scope = 8; + repeated PublicKey affiliate_shares = 10; + Status status = 9; enum Status { diff --git a/p8e-sdk/src/main/kotlin/io/p8e/ContractManager.kt b/p8e-sdk/src/main/kotlin/io/p8e/ContractManager.kt index 87421306..e03a3c0b 100644 --- a/p8e-sdk/src/main/kotlin/io/p8e/ContractManager.kt +++ b/p8e-sdk/src/main/kotlin/io/p8e/ContractManager.kt @@ -57,6 +57,7 @@ import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey import java.time.Duration +import java.time.Instant import java.time.OffsetDateTime import java.util.ServiceLoader import java.util.UUID @@ -90,7 +91,7 @@ class ContractManager( /** * Create a new ContractManager for a given Party */ - fun create(keyPair: KeyPair, url: String? = null, deadlineMs: Long = 60000): ContractManager { + fun create(keyPair: KeyPair, url: String? = null, deadlineMs: Long = 60_000): ContractManager { val apiUrl = url ?: System.getenv("API_URL") ?: "http://localhost:8080/engine" val uri = URI(apiUrl) val customTrustStore = System.getenv("TRUST_STORE_PATH")?.let(::File) @@ -154,6 +155,22 @@ class ContractManager( private val heartbeatExecutor = ThreadPoolFactory.newScheduledThreadPool(1, "heartbeat-%d") private val heartbeatManagerExecutor = ThreadPoolFactory.newScheduledThreadPool(1, "heartbeat-manager-%d") + private var shares: List? = null + private var sharesFetchTime = Instant.now() + + fun affiliateShares(): List = + if (shares == null) { + shares = client.shares() + sharesFetchTime = Instant.now() + shares!! + } else if (Instant.now().minusSeconds(10 * 60).isAfter(sharesFetchTime)) { + shares = client.shares() + sharesFetchTime = java.time.Instant.now() + shares!! + } else { + shares!! + } + init { val logger = LogFactory.getLog("org.apache.http.wire") when (logger) { diff --git a/p8e-sdk/src/main/kotlin/io/p8e/client/EngineClient.kt b/p8e-sdk/src/main/kotlin/io/p8e/client/EngineClient.kt index 15798a77..78d2e3d0 100644 --- a/p8e-sdk/src/main/kotlin/io/p8e/client/EngineClient.kt +++ b/p8e-sdk/src/main/kotlin/io/p8e/client/EngineClient.kt @@ -29,6 +29,7 @@ import io.p8e.util.toPublicKeyProto import io.p8e.util.toOffsetDateTimeProv import io.p8e.util.toProtoTimestampProv import io.p8e.util.toProtoUuidProv +import io.p8e.util.toPublicKey import io.provenance.p8e.encryption.ecies.ECUtils import java.io.InputStream import java.lang.reflect.Method @@ -43,6 +44,8 @@ interface P8eClient { fun register(enc: Affiliate.AffiliateRegisterRequest) + fun shares(): List + fun whitelistClass(whitelist: AffiliateContractWhitelist) fun getContracts(groupUuid: UUID): EnvelopeCollection @@ -119,6 +122,9 @@ abstract class EventMonitorClient( override fun register(enc: Affiliate.AffiliateRegisterRequest) = signingAndEncryptionPublicKeysClient.register(enc) + override fun shares() = signingAndEncryptionPublicKeysClient.shares().sharesList + .map { it.toPublicKey() } + override fun whitelistClass(whitelist: AffiliateContractWhitelist) = signingAndEncryptionPublicKeysClient.whitelistClass(whitelist) override fun getContracts(groupUuid: UUID): EnvelopeCollection = envelopeClient.getAllByGroupUuid(groupUuid) diff --git a/p8e-sdk/src/main/kotlin/io/p8e/proxy/Contract.kt b/p8e-sdk/src/main/kotlin/io/p8e/proxy/Contract.kt index 2399e240..29813ddf 100644 --- a/p8e-sdk/src/main/kotlin/io/p8e/proxy/Contract.kt +++ b/p8e-sdk/src/main/kotlin/io/p8e/proxy/Contract.kt @@ -437,16 +437,13 @@ class Contract( this.stagedContract = populateContract() - val permissionUpdater = PermissionUpdater( - contractManager, - this.stagedContract, - this.stagedContract.toAudience(envelope.scope) - ) - - permissionUpdater.saveConstructorArguments() - // Build the envelope for this execution this.executionEnvelope = envelope.toBuilder() + .also { builder -> + if (builder.affiliateSharesCount == 0) { + builder.addAllAffiliateShares(contractManager.affiliateShares().map { it.toPublicKeyProto() }) + } + } .setExecutionUuid(this.stagedExecutionUuid) .setContract(this.stagedContract) .also { builder -> @@ -459,8 +456,17 @@ class Contract( .clearSignatures() .build() + val permissionUpdater = PermissionUpdater( + contractManager, + this.stagedContract, + this.stagedContract.toAudience(executionEnvelope, envelope.scope) + ) + + permissionUpdater.saveConstructorArguments() + // TODO This probably should be removed since we can't on the fly install specs. It would have // to get saved during bootstrap addSpec + // TODO get affiliate shares on the specs before save to object-store saveSpec(contractManager) executed.set(true) @@ -471,7 +477,7 @@ class Contract( } fun saveSpec(contractManager: ContractManager) { - contractManager.saveProto(spec, audience = stagedContract.toAudience(envelope.scope)) + contractManager.saveProto(spec, audience = stagedContract.toAudience(this.executionEnvelope, envelope.scope)) } fun isCompleted(): Boolean { @@ -761,12 +767,13 @@ class Contract( } } - private fun Contracts.Contract.toAudience(scope: Scope): Set { + private fun Contracts.Contract.toAudience(envelope: Envelope, scope: Scope): Set { return recitalsList.plus(scope.partiesList) .filter { it.hasSigner() } .map { it.signer.encryptionPublicKey.publicKeyBytes } .filterNot { it.isEmpty } .map { ECUtils.convertBytesToPublicKey(it.toByteArray()) } + .plus(envelope.affiliateSharesList.map { it.toPublicKey() }) .toSet() } diff --git a/p8e-shared/src/main/kotlin/io/provenance/p8e/shared/service/AffiliateService.kt b/p8e-shared/src/main/kotlin/io/provenance/p8e/shared/service/AffiliateService.kt index 8453a2e0..fe53a453 100644 --- a/p8e-shared/src/main/kotlin/io/provenance/p8e/shared/service/AffiliateService.kt +++ b/p8e-shared/src/main/kotlin/io/provenance/p8e/shared/service/AffiliateService.kt @@ -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 { @@ -255,12 +255,6 @@ class AffiliateService( .whitelistData } - fun getSharePublicKeys(publicKeys: Collection): AffiliateSharePublicKeys = - AffiliateShareRecord.findByAffiliates(publicKeys) - .map { it.typedPublicKey() } - .toSet() - .let(::AffiliateSharePublicKeys) - fun getShares(affiliatePublicKey: PublicKey): List = AffiliateShareRecord.findByAffiliate(affiliatePublicKey).toList() fun addShare(affiliatePublicKey: PublicKey, publicKey: PublicKey) = AffiliateShareRecord.insert(affiliatePublicKey, publicKey) diff --git a/p8e-shared/src/main/kotlin/io/provenance/p8e/shared/service/DataAccessService.kt b/p8e-shared/src/main/kotlin/io/provenance/p8e/shared/service/DataAccessService.kt new file mode 100644 index 00000000..46a62141 --- /dev/null +++ b/p8e-shared/src/main/kotlin/io/provenance/p8e/shared/service/DataAccessService.kt @@ -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() + ) + } +} diff --git a/p8e-shared/src/main/proto/p8e/jobs.proto b/p8e-shared/src/main/proto/p8e/jobs.proto index 060bfb86..e848e952 100644 --- a/p8e-shared/src/main/proto/p8e/jobs.proto +++ b/p8e-shared/src/main/proto/p8e/jobs.proto @@ -7,9 +7,22 @@ import "p8e/public_key.proto"; message P8eJob { oneof job { AddAffiliateOSLocator addAffiliateOSLocator = 1; + MsgAddScopeDataAccessRequest msgAddScopeDataAccessRequest = 2; } } message AddAffiliateOSLocator { PublicKey publicKey = 1; } + +message MsgAddScopeDataAccessRequest { + // scope MetadataAddress for updating data access + bytes scope_id = 1; + + // AccAddress addresses to be added to scope + repeated string data_access = 2; + // signers is the list of address of those signing this request. + repeated string signers = 3; + + PublicKey publicKey = 4; +} \ No newline at end of file