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.