Overview
Quorum is an enterprise-focused Ethereum client that supports private transactions through privacy managers like Tessera. Private transactions allow specific participants to see transaction details while keeping them hidden from others on the network.
Key Concepts
Private Transaction Flow
- Transaction Creation: Encrypt payload for specific participants
- Privacy Manager: Handles encryption and key management (Tessera)
- Blockchain: Stores transaction hash, encrypted payload off-chain
- Participant Access: Only authorized nodes can decrypt and view details
Dependencies
<dependencies> <!-- Web3j Quorum --> <dependency> <groupId>org.web3j</groupId> <artifactId>quorum</artifactId> <version>1.7.0</version> </dependency> <!-- Tessera client --> <dependency> <groupId>com.jpmorgan.quorum</groupId> <artifactId>tessera-client</artifactId> <version>1.0.0</version> </dependency> <!-- Ethereum core --> <dependency> <groupId>org.web3j</groupId> <artifactId>core</artifactId> <version>4.9.7</version> </dependency> <!-- JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> </dependencies>
Core Implementation
1. Quorum Service Configuration
@Component public class QuorumConfig { @Value("${quorum.rpc.url}") private String rpcUrl; @Value("${quorum.private.key}") private String privateKey; @Value("${tessera.url}") private String tesseraUrl; @Bean public Quorum quorumClient() throws IOException { return Quorum.build(new HttpService(rpcUrl)); } @Bean public Credentials credentials() { return Credentials.create(privateKey); } @Bean public TesseraClient tesseraClient() { return new TesseraClient(tesseraUrl); } } 2. Private Transaction Manager
@Service public class PrivateTransactionService { private final Quorum quorum; private final Credentials credentials; private final TesseraClient tesseraClient; public PrivateTransactionService(Quorum quorum, Credentials credentials, TesseraClient tesseraClient) { this.quorum = quorum; this.credentials = credentials; this.tesseraClient = tesseraClient; } // Send private transaction to specific participants public String sendPrivateTransaction( String to, String data, List<String> privateFor, BigInteger value) throws Exception { // Get transaction count for nonce BigInteger nonce = quorum.ethGetTransactionCount( credentials.getAddress(), DefaultBlockParameterName.LATEST ).send().getTransactionCount(); // Create raw private transaction RawPrivateTransaction rawTx = RawPrivateTransaction.createTransaction( nonce, BigInteger.valueOf(0), // gasPrice BigInteger.valueOf(3000000), // gasLimit to, value, data, privateFor ); // Sign the transaction byte[] signedTx = PrivateTransactionEncoder.signMessage(rawTx, credentials); // Send to Quorum EthSendTransaction response = quorum.ethSendRawPrivateTransaction( Numeric.toHexString(signedTx) ).send(); if (response.hasError()) { throw new RuntimeException("Transaction failed: " + response.getError().getMessage()); } return response.getTransactionHash(); } // Send private transaction with contract deployment public String deployPrivateContract( String bytecode, List<String> privateFor) throws Exception { BigInteger nonce = quorum.ethGetTransactionCount( credentials.getAddress(), DefaultBlockParameterName.LATEST ).send().getTransactionCount(); RawPrivateTransaction rawTx = RawPrivateTransaction.createContractTransaction( nonce, BigInteger.valueOf(0), BigInteger.valueOf(4700000), BigInteger.ZERO, bytecode, privateFor ); byte[] signedTx = PrivateTransactionEncoder.signMessage(rawTx, credentials); EthSendTransaction response = quorum.ethSendRawPrivateTransaction( Numeric.toHexString(signedTx) ).send(); return response.getTransactionHash(); } } 3. Enhanced Private Transaction with Payload Management
@Service public class EnhancedPrivateTransactionService { private final Quorum quorum; private final TesseraClient tesseraClient; public EnhancedPrivateTransactionService(Quorum quorum, TesseraClient tesseraClient) { this.quorum = quorum; this.tesseraClient = tesseraClient; } // Send private transaction with encrypted payload public PrivateTransactionResult sendPrivateTransactionWithPayload( PrivateTransactionRequest request) throws Exception { // Encrypt payload for participants EncryptedPayload encryptedPayload = encryptPayload( request.getPayload(), request.getPrivateFor() ); // Store encrypted payload in Tessera String payloadId = tesseraClient.storePayload(encryptedPayload); // Create transaction with payload reference String transactionData = buildTransactionData(payloadId, request.getFunctionCall()); RawPrivateTransaction rawTx = RawPrivateTransaction.createTransaction( request.getNonce(), request.getGasPrice(), request.getGasLimit(), request.getTo(), request.getValue(), transactionData, request.getPrivateFor() ); byte[] signedTx = PrivateTransactionEncoder.signMessage(rawTx, request.getCredentials()); EthSendTransaction response = quorum.ethSendRawPrivateTransaction( Numeric.toHexString(signedTx) ).send(); return new PrivateTransactionResult( response.getTransactionHash(), payloadId, request.getPrivateFor() ); } private EncryptedPayload encryptPayload(String payload, List<String> participants) { // Generate symmetric key for payload encryption String symmetricKey = generateSymmetricKey(); // Encrypt payload with symmetric key String encryptedPayload = encryptWithSymmetricKey(payload, symmetricKey); // Encrypt symmetric key for each participant Map<String, String> participantKeys = new HashMap<>(); for (String participant : participants) { String encryptedKey = encryptKeyForParticipant(symmetricKey, participant); participantKeys.put(participant, encryptedKey); } return new EncryptedPayload(encryptedPayload, participantKeys); } private String buildTransactionData(String payloadId, String functionCall) { JSONObject data = new JSONObject(); data.put("payloadId", payloadId); data.put("function", functionCall); data.put("timestamp", Instant.now().toString()); return data.toString(); } } Smart Contract Integration
1. Private Contract Wrapper
public class PrivateContractWrapper { private final Quorum quorum; private final String contractAddress; private final List<String> privateFor; public PrivateContractWrapper(Quorum quorum, String contractAddress, List<String> privateFor) { this.quorum = quorum; this.contractAddress = contractAddress; this.privateFor = privateFor; } // Call private contract function public String callPrivateFunction( String functionSignature, List<Type> parameters, Credentials credentials) throws Exception { String encodedFunction = FunctionEncoder.encode( new Function(functionSignature, parameters, Collections.emptyList()) ); BigInteger nonce = quorum.ethGetTransactionCount( credentials.getAddress(), DefaultBlockParameterName.LATEST ).send().getTransactionCount(); RawPrivateTransaction rawTx = RawPrivateTransaction.createTransaction( nonce, BigInteger.ZERO, BigInteger.valueOf(1000000), contractAddress, BigInteger.ZERO, encodedFunction, privateFor ); byte[] signedTx = PrivateTransactionEncoder.signMessage(rawTx, credentials); EthSendTransaction response = quorum.ethSendRawPrivateTransaction( Numeric.toHexString(signedTx) ).send(); return response.getTransactionHash(); } // Read private state (uses eth_call) public List<Type> readPrivateState( String functionSignature, List<Type> parameters, String fromAddress) throws Exception { String encodedFunction = FunctionEncoder.encode( new Function(functionSignature, parameters, Collections.emptyList()) ); org.web3j.protocol.core.methods.request.Transaction transaction = org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction( fromAddress, contractAddress, encodedFunction); // For private transactions, we need to use eth_storageRoot or similar // This is a simplified example EthCall response = quorum.ethCall(transaction, DefaultBlockParameterName.LATEST).send(); return FunctionReturnDecoder.decode(response.getValue(), new Function(functionSignature, parameters, Collections.emptyList()).getOutputParameters()); } } 2. Private State Management
@Service public class PrivateStateService { private final Quorum quorum; private final TesseraClient tesseraClient; public PrivateStateService(Quorum quorum, TesseraClient tesseraClient) { this.quorum = quorum; this.tesseraClient = tesseraClient; } // Get private transaction receipt public PrivateTransactionReceipt getPrivateTransactionReceipt(String transactionHash) throws Exception { EthGetTransactionReceipt receiptResponse = quorum.ethGetTransactionReceipt(transactionHash).send(); if (!receiptResponse.getTransactionReceipt().isPresent()) { throw new RuntimeException("Transaction receipt not found"); } TransactionReceipt receipt = receiptResponse.getTransactionReceipt().get(); // For private transactions, we might need to fetch additional data from Tessera String privatePayload = fetchPrivatePayload(transactionHash); return new PrivateTransactionReceipt(receipt, privatePayload); } // Check if transaction is private public boolean isPrivateTransaction(String transactionHash) throws Exception { EthGetTransactionResponse response = quorum.ethGetTransactionByHash(transactionHash).send(); if (!response.getTransaction().isPresent()) { return false; } Transaction tx = response.getTransaction().get(); // In Quorum, private transactions have specific markers return tx.getInput().startsWith("0xee") || (tx.getV() != null && tx.getV().equals(BigInteger.valueOf(37))); } // Get participants for private transaction public List<String> getTransactionParticipants(String transactionHash) throws Exception { // This would typically involve querying Tessera or the privacy manager PrivateTransactionPayload payload = tesseraClient.getPayload(transactionHash); return payload.getParticipants(); } } Advanced Features
1. Multi-Party Private Transactions
@Service public class MultiPartyPrivateService { private final Map<String, Quorum> quorumNodes; private final TesseraClient tesseraClient; public MultiPartyPrivateService(TesseraClient tesseraClient) { this.quorumNodes = new HashMap<>(); this.tesseraClient = tesseraClient; } public void addNode(String nodeId, String rpcUrl) { quorumNodes.put(nodeId, Quorum.build(new HttpService(rpcUrl))); } // Send transaction to multiple Quorum nodes with different privacy groups public Map<String, String> sendToMultipleParties( PrivateTransactionRequest request, Map<String, List<String>> nodePrivacyGroups) throws Exception { Map<String, String> results = new HashMap<>(); for (Map.Entry<String, List<String>> entry : nodePrivacyGroups.entrySet()) { String nodeId = entry.getKey(); List<String> privacyGroup = entry.getValue(); Quorum node = quorumNodes.get(nodeId); if (node != null) { String txHash = sendToNode(node, request, privacyGroup); results.put(nodeId, txHash); } } return results; } private String sendToNode(Quorum node, PrivateTransactionRequest request, List<String> privacyGroup) throws Exception { RawPrivateTransaction rawTx = RawPrivateTransaction.createTransaction( request.getNonce(), request.getGasPrice(), request.getGasLimit(), request.getTo(), request.getValue(), request.getData(), privacyGroup ); byte[] signedTx = PrivateTransactionEncoder.signMessage(rawTx, request.getCredentials()); EthSendTransaction response = node.ethSendRawPrivateTransaction( Numeric.toHexString(signedTx) ).send(); return response.getTransactionHash(); } } 2. Privacy Group Management
@Service public class PrivacyGroupService { private final TesseraClient tesseraClient; private final Map<String, PrivacyGroup> privacyGroups; public PrivacyGroupService(TesseraClient tesseraClient) { this.tesseraClient = tesseraClient; this.privacyGroups = new HashMap<>(); } // Create new privacy group public String createPrivacyGroup(String name, List<String> participants, String description) throws Exception { PrivacyGroup group = new PrivacyGroup(name, participants, description); String groupId = generateGroupId(participants); // Register with Tessera tesseraClient.createPrivacyGroup(groupId, group); privacyGroups.put(groupId, group); return groupId; } // Add participant to privacy group public void addToPrivacyGroup(String groupId, String participant) throws Exception { PrivacyGroup group = privacyGroups.get(groupId); if (group != null && !group.getParticipants().contains(participant)) { group.getParticipants().add(participant); tesseraClient.updatePrivacyGroup(groupId, group); } } // Remove participant from privacy group public void removeFromPrivacyGroup(String groupId, String participant) throws Exception { PrivacyGroup group = privacyGroups.get(groupId); if (group != null) { group.getParticipants().remove(participant); tesseraClient.updatePrivacyGroup(groupId, group); } } // Find transactions for privacy group public List<String> getGroupTransactions(String groupId) throws Exception { return tesseraClient.getTransactionsForGroup(groupId); } private String generateGroupId(List<String> participants) { // Sort participants for consistent ID generation List<String> sorted = new ArrayList<>(participants); Collections.sort(sorted); String concatenated = String.join("", sorted); return Hashing.sha256().hashString(concatenated, StandardCharsets.UTF_8).toString(); } } Security and Monitoring
1. Transaction Security Layer
@Service public class TransactionSecurityService { private final Quorum quorum; private final TesseraClient tesseraClient; public TransactionSecurityService(Quorum quorum, TesseraClient tesseraClient) { this.quorum = quorum; this.tesseraClient = tesseraClient; } // Validate private transaction before sending public ValidationResult validatePrivateTransaction(PrivateTransactionRequest request) { ValidationResult result = new ValidationResult(); // Check participant validity for (String participant : request.getPrivateFor()) { if (!isValidParticipant(participant)) { result.addError("Invalid participant: " + participant); } } // Check gas limits if (request.getGasLimit().compareTo(BigInteger.valueOf(10000000)) > 0) { result.addError("Gas limit too high"); } // Check data size for private transactions if (request.getData().length() > 100000) { // arbitrary limit result.addError("Transaction data too large for private transaction"); } return result; } // Monitor private transaction lifecycle public void monitorPrivateTransaction(String transactionHash) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() -> { try { TransactionReceipt receipt = getTransactionReceipt(transactionHash); if (receipt != null) { handleTransactionMined(receipt); scheduler.shutdown(); } } catch (Exception e) { log.error("Error monitoring transaction: " + transactionHash, e); } }, 0, 5, TimeUnit.SECONDS); // Check every 5 seconds } private boolean isValidParticipant(String participant) { // Validate participant format and existence return participant.matches("^[a-fA-F0-9]{40}$"); // Basic Ethereum address format } } 2. Audit and Compliance
@Service public class PrivateTransactionAuditService { private final Quorum quorum; private final AuditRepository auditRepository; public PrivateTransactionAuditService(Quorum quorum, AuditRepository auditRepository) { this.quorum = quorum; this.auditRepository = auditRepository; } // Log private transaction for compliance public void auditPrivateTransaction(PrivateTransactionRequest request, String transactionHash, String userId) { AuditRecord record = new AuditRecord( UUID.randomUUID().toString(), userId, request.getTo(), transactionHash, Instant.now(), request.getPrivateFor(), "PRIVATE_TX_SENT" ); auditRepository.save(record); } // Generate compliance report public ComplianceReport generateComplianceReport(String timeframe, String organization) { List<AuditRecord> records = auditRepository.findByTimeframeAndOrganization( timeframe, organization); ComplianceReport report = new ComplianceReport(); report.setTotalTransactions(records.size()); Map<String, Integer> participantCount = new HashMap<>(); for (AuditRecord record : records) { for (String participant : record.getParticipants()) { participantCount.merge(participant, 1, Integer::sum); } } report.setParticipantDistribution(participantCount); return report; } } Configuration and Properties
application.yml
quorum: rpc: url: http://localhost:8545 private: key: ${PRIVATE_KEY:0x...} tessera: url: http://localhost:9081 third-party-url: http://localhost:9081 privacy: default-participants: - "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73" - "0x627306090abab3a6e1400e9345bc60c78a8bef57" security: max-gas-limit: 10000000 max-data-size: 100000 Testing
@SpringBootTest class PrivateTransactionTest { @Autowired private PrivateTransactionService privateTxService; @Test void testSendPrivateTransaction() throws Exception { List<String> privateFor = Arrays.asList( "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73", "0x627306090abab3a6e1400e9345bc60c78a8bef57" ); String txHash = privateTxService.sendPrivateTransaction( "0x...", // to address "0x...", // data privateFor, BigInteger.ZERO ); assertNotNull(txHash); assertTrue(txHash.startsWith("0x")); } @Test void testPrivateContractDeployment() throws Exception { String bytecode = "0x606060..."; // Contract bytecode List<String> privateFor = Arrays.asList("0x..."); String txHash = privateTxService.deployPrivateContract(bytecode, privateFor); assertNotNull(txHash); } } Best Practices
- Key Management: Use secure key storage (HSM, cloud KMS)
- Participant Validation: Validate all participants before sending
- Gas Optimization: Set appropriate gas limits for private transactions
- Error Handling: Implement comprehensive error handling for privacy manager communication
- Monitoring: Monitor private transaction lifecycle and privacy manager health
- Backup: Regularly backup Tessera data and configuration
- Compliance: Maintain audit trails for regulatory requirements
This implementation provides a comprehensive foundation for working with Quorum private transactions in Java, covering transaction creation, privacy groups, security, and compliance aspects.