Creating a Blockchain in Liferay

Creating a blockchain structure using Liferay Objects

Caption

Introduction

The Liferay platform has been used for the development of corporate applications by many companies for several years. This is because it stands out for its flexibility in extending its ready-made components, broadly meeting the needs of business applications, such as security, clustering, indexing, and internationalization.

However, directly building corporate systems on the platform can complicate future updates due to changes in the internal API. Liferay Inc. has, therefore, focused on features that allow extension to external runtimes, minimizing the impact of updates on the API. Concurrently, the platform encourages the development of low-code applications, facilitating the updating of versions.

In this context, this article aims to explore these low-code features of Liferay, demonstrating the construction of a blockchain and the mining of a fictitious cryptocurrency, RayCoin, in addition to presenting a system for managing digital wallets and transactions, highlighting Liferay's adaptability to various business needs.

Blockchain Fundamentals

Before we proceed with the implementation using Liferay, it's crucial to understand the fundamental concepts of the blockchain data structure. This structure is essentially a sequence of interconnected blocks, starting with the initial block, known as the "genesis block". Each subsequent block maintains a reference to its predecessor through a cryptographic hash, generated from the block's information and including the previous block's hash. This methodology ensures that the blockchain is a reliable and virtually immutable data structure.


The connection between consecutive blocks in the blockchain is established through a hash, which is calculated from the information contained in the block itself and incorporates the hash of the preceding block. Thus, the hash of each block is derived from a dataset that includes the current block's information along with the previous block's hash. This approach, despite its apparent simplicity, is profoundly ingenious, giving the blockchain its characteristic of being a highly secure and practically immutable data structure.

Understanding the fundamental concept of blockchain, it is essential to detail the components and attributes that make up this structure. The central element is the Block, with the following attributes:


 
  • Index: Identifies the block's position in the chain.
  • Header: Contains the calculated hash of the block.
  • Previous Hash: Refers to the hash of the preceding block, connecting it in the chain sequence.
  • Nonce: A unique number used to find the solution to the cryptographic problem that allows the block to be mined, adjusting to the current level of difficulty.
  • Transactions: A set of all transactions included in the block, detailing transfers of value between different wallet addresses.


Therefore, a block not only stores its intrinsic attributes but also encapsulates multiple transactions. These transactions, representing debit and credit movements between digital wallets, are fundamental to the immutability of the blockchain, as the hash of each block is calculated by incorporating these data. In the context of this article, in building a cryptocurrency system, blocks contain transactions involving RayCoin, evidencing transfers between digital wallets associated with this fictitious cryptocurrency.


Continuing with the detailing of the blockchain elements, each transaction within a block is defined by specific attributes that ensure its integrity and traceability:

  • Transaction Id: Serves as a unique identifier for each transaction, ensuring its uniqueness within the chain.
  • Address From: Denotes the digital wallet address from where the values will be debited, initiating the transfer of funds.
  • Address To: Specifies the transaction's destination, i.e., the digital wallet address where the values will be credited.
  • Signature: Represents the digital signature generated by the sender, essential for verifying the transaction's authenticity and authorizing the debit.
  • Amount: Quantifies the value of the transaction, expressed in the cryptocurrency's unit, facilitating the understanding of the transferred volume.
  • Status: Indicates the current condition of the transaction within the system, which can vary between states such as approved, pending, or denied, reflecting its processing and validation.

This detailed structuring of transactions not only reinforces the security and transparency of the blockchain system but also facilitates the tracking and auditing of all movements.

Finally, we address the representation of a digital wallet, a fundamental element for blockchain users, enabling the management and transaction of cryptocurrency units. The conception of a digital wallet is straightforward, encompassing essential attributes for its functionality and security:


The digital wallet is quite simple, composed of the following attributes:

  • Name: Informative designation of the wallet, facilitating identification by the user.
  • Public Key: Acts as the address for sending and receiving cryptocurrency units, functioning as a public identifier of the wallet.
  • Private Key: Essential for signing transactions, ensuring the authorship and security of financial movements. This is sensitive data, known exclusively to the wallet's owner.

This article proposes the construction of a blockchain composed of blocks that record transfer transactions of the cryptocurrency RayCoin between different wallet addresses. Understanding how each block is formed, through the mining process, is crucial. Traditionally, mining involves solving a complex cryptographic problem that requires significant computational effort. In most blockchain networks, the node that solves this challenge first propagates the new block to the others, initiating a consensus mechanism to validate the block's authenticity. If validated, the block is added to all participants' blockchain. Several consensus mechanisms exist, with Proof-of-Work (PoW) being one of the most well-known. For the purposes of this article, we will focus on constructing a single node of the blockchain, without the ambition of developing a complete network, and, consequently, without implementing a consensus algorithm.

The generation of blocks occurs when a defined number of transactions is reached, triggering the mining process that incorporates these transactions into the new block. This block is then added to the chain, containing its own hash, calculated from the included transactions and the previous block's hash. As part of the mining process, units of RayCoin are credited to the miner's wallet as a reward, a value also defined by the system. Thus, the blockchain grows with the continuous addition of new blocks, reflecting the received RayCoin transactions.


 

Project Plan

After understanding the fundamental concepts of blockchain technology, it is imperative to establish a clear strategy for project implementation. In this context, we will explore the resources and functionalities provided by the Liferay platform, which will form the basis for developing a comprehensive solution. Recognized for its robustness and adaptability, the Liferay platform offers a series of low-code tools that allow for the rapid construction and customization of applications, without compromising the complexity or security necessary for a blockchain project. Our goal is to demonstrate how these resources can be effectively used to create a functional blockchain structure, including the mining of a fictitious cryptocurrency and the management of digital wallets, leveraging the full capabilities of Liferay.

Multi-tenancy

To simulate the process of buying and selling RayCoin, the multi-tenancy functionality of Liferay will be adopted, which allows for the creation of multiple logical instances within a single physical instance. This feature is crucial for structuring the project, where two distinct instances will be configured:

  • RayCoin Instance: Aimed at building the blockchain itself, this instance is responsible for receiving transactions and mining the blocks, which will later be organized in the chain.
  • XChangeRay Instance: Designed to simulate an external service, this instance functions as a cryptocurrency exchange, where users can manage their RayCoin digital wallets and initiate transactions. The goal is to facilitate user interaction with the blockchain, allowing for the sending of transactions to the first instance in a simplified and intuitive manner.


 

Liferay Objects

To effectively represent the entities necessary for our blockchain solution in Liferay, we will employ a feature called Liferay Objects: https://learn.liferay.com/w/dxp/building-applications/objects

This functionality is a cornerstone of the low-code concept offered by the platform, allowing for the creation of custom objects that represent entities for the development of customized solutions. Through Liferay Objects, the platform provides an integrated approach to all the necessary layers for the persistence of these objects in the context of business logic, including the automatic generation of an integration layer via RestFul or GraphQL protocols. The Liferay Objects framework stands out for its versatility and power, enabling a wide range of customizations and extensions.

In this project, the objects to be created to compose our blockchain solution include:

  • Blockchain: Object that encapsulates the main information of the blockchain.
  • Block: Represents the individual blocks of the chain, containing transactions, their unique hash, and the reference to the hash of the previous block.
  • Transaction: Used to record transactions and associate them with the corresponding blocks after mining.
  • Wallet: Allows users to create and manage their digital wallets, essential for signing the transactions sent to the blockchain.
  • Wallet Balance: Stores the balances of wallets, facilitating the consultation of balances without the need to traverse the entire block chain for transaction validation.

Implementation

After an initial understanding of the main functionalities that will support the implementation, we are ready to begin the project setup. The first step involves activating a Liferay instance in a local environment. For this article, we will use Liferay version GA109. The instance can be easily set up in a Docker environment using the following command:

    
docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.109-ga109
    

In this project, we intend to create two logical instances in Liferay, each operating under a distinct Virtual Host, thereby allowing each instance to respond to a specific address:

  • RayCoin Instance: Will be the core of the Blockchain, responsible for processing transactions and mining blocks, forming the block chain.
  • XChangeRay Instance: Will simulate an external service, functioning as a cryptocurrency exchange, where users can manage their RayCoin digital wallets and execute transactions.

To configure the Virtual Hosts in the local environment, it is necessary to edit the /etc/hosts file (Linux) or C:/Windows/System32/Drivers/etc/hosts (Windows), including the following line:

127.0.0.1 raycoin.local xchangeray.local

This directs accesses to the addresses http://raycoin.local:8080/ or http://xchangeray.local:8080/ to http://localhost:8080/, allowing the running Liferay physical instance to recognize and load the appropriate logical instance based on the accessed Virtual Host. With Liferay active, accessing http://raycoin.local:8080/, the main instance will be loaded. After logging into this instance, navigate to Control Panel -> Virtual Instances, and in the instance with Web Id equal to "liferay.com", edit the Virtual Host to include "raycoin.local".


 

Next, proceed with creating a new virtual instance, entering the necessary information:

  • Web Id: xchangeray
  • Virtual Host: xchangeray.local
  • Mail Domain: liferay.com
  • Virtual Instance Initializer: Blank Site


XChangeRay Instance

Wallet Object

After creating the new instance, access http://xchangeray.local:8080/ and log in as an administrator, then go to the menu Control Panel -> Objects to create the first object. Access the option to add a new custom object (button '+') and include the Wallet object information:

  • Label: Wallet
  • Plural Label: Wallets


 

When creating a new Object in Liferay, we are essentially defining the records that will compose this Object. Initially, Liferay keeps these definitions in a draft state, allowing adjustments and refinements until the user finalizes the settings and opts to publish the Object.

Upon accessing the panel of the newly created Object, a range of tabs becomes available for exploration: Details, Fields, Relationships, Layouts, Actions, Views, Validations, and State Manager. Each of these tabs unlocks distinct features, offering substantial flexibility for composing complex solutions. For example, in the Fields tab, it's possible to create custom fields that specifically align with the business logic of the application. This level of customization emphasizes the power and versatility of the Objects framework within Liferay.



To create a custom field, go to the Fields tab, trigger the '+' button (Add Object Field) and fill in the field information.


 

Therefore, for this Wallet object, create the following custom fields:

Label Field Name Type Mandatory
Name name Text Yes
Private Key privateKey Long Text No
Public Key publicKey Long Text No


When defining custom fields for the Wallet object, it becomes essential to establish a logic for the automatic generation of public and private keys every time a new Wallet is created. This functionality is managed by the "Actions" tab of the object, allowing the insertion of business logics at different stages of the object's lifecycle, such as its creation or update.

Specifically for the Wallet object, it is necessary to generate a new private key and a public key at the act of creating the object. For this, go to the "Actions" tab, trigger the "Add Object Action" button, filling in the "Action Label" field with "SetKeys".


 

Navigate to the "Action Builder" tab. Here, we define the logic to be executed, choose "On After Add" in the "Trigger" option so that the action occurs immediately after creating a new record.


 

Select the "Groovy Script" option for the "Action" section. This Groovy script, leveraging the available attributes of the object, executes the desired logic, including the generation of keys.


 

Upon selecting "Groovy Script," note that a field opens up for including the script.


 

To generate the keys, we will implement in this script a class named Wallet, as below:

class Wallet {

	def KeyPair keyPair

	Wallet() {
		this.keyPair = this.generateKeyPair()
	}

	String getPrivateKey() {
		return Base64.encoder.encodeToString(keyPair.getPrivate().getEncoded())
	}

	String getPublicKey() {
		return Base64.encoder.encodeToString(keyPair.getPublic().getEncoded())
	}

	private KeyPair generateKeyPair() {
		def keyGen = KeyPairGenerator.getInstance("EC")
		def ecSpec = new ECGenParameterSpec("secp256r1")
		keyGen.initialize(ecSpec, new SecureRandom())
		return keyGen.generateKeyPair()
	}
}

With this class, when a new Wallet object is created, the method generateKeyPair() will automatically be called, generating the KeyPair using classes from the java.security package. Then, the methods getPrivateKey() and getPublicKey() will return the private and public keys, respectively, already in string format, to be stored in the corresponding fields of the object.

To store the keys in the object, we will use the updateObjectEntry() method from the ObjectEntryLocalServiceUtil class of Liferay's Objects API. The code should be as follows:  

def wallet = new Wallet()

def userId = Long.valueOf(creator)
def entryValues = [privateKey: wallet.privateKey, publicKey: wallet.publicKey]

ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, entryValues, new ServiceContext())

From this code, it's observable that the object fields can be passed through a Map, in this case, entryValues, where we include the public and private keys for the object with the id that is in the context (variable id). The final script should be as follows:


import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.security.spec.ECGenParameterSpec

import com.liferay.object.service.ObjectEntryLocalServiceUtil
import com.liferay.portal.kernel.service.ServiceContext

def wallet = new Wallet()

def userId = Long.valueOf(creator)
def entryValues = [privateKey: wallet.privateKey, publicKey: wallet.publicKey]

ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, entryValues, new ServiceContext())

class Wallet {

	def KeyPair keyPair

	Wallet() {
		this.keyPair = this.generateKeyPair()
	}

	String getPrivateKey() {
		return Base64.encoder.encodeToString(keyPair.getPrivate().getEncoded())
	}

	String getPublicKey() {
		return Base64.encoder.encodeToString(keyPair.getPublic().getEncoded())
	}

	private KeyPair generateKeyPair() {
		def keyGen = KeyPairGenerator.getInstance("EC")
		def ecSpec = new ECGenParameterSpec("secp256r1")
		keyGen.initialize(ecSpec, new SecureRandom())
		return keyGen.generateKeyPair()
	}
}

Copy the code and add it to the script field, and then after saving, you will notice that the new Action has been included in the Wallet object.


 

Next, go to the Details tab and change the following fields:

  • Entry Title Field: Name
  • Panel Link: Custom Apps

Disable the "Enable Categorization of Object entries" option, and then click the "Publish" button. This will publish the object. Note that Liferay already creates a default view of the object in the Applications -> Custom Apps menu option, as selected in the Details tab of the object.



Upon entering this option, click the "Add Wallet" button.

Fill in a name, leave the Private Key and Public Key fields empty, and click the "Save" button.


 

Note that when adding a new record, the Groovy script code is triggered, generating the public and private keys for the Wallet.

Now let's add the wallets widget so that site users can create their own wallets. For this, go to the main site of XChangeRay and add a new page called "My Wallets" through the Site Builder -> Pages menu.




 

Next, in the page editing screen, select the "Widgets" tab from the left sidebar, go to the "Objects" category, and add the Wallets widget to the page.


 

Now, to make the page visible to site users, go to the "Permissions" option of the "My Wallets" page and remove the "View" permission for "Guest" users, then add the "View" permission for "User".

This way, only registered site users can view this page. Now, it is still necessary to add permission for authenticated users to create their own wallets. For this, go to the Control Panel -> Roles menu, select the "Users" option. Next, select the "Define Permissions" tab, search for "Wallets", and select the "View" permission under the "Application Permissions" category and "Add Object Entry" under "Resource Permissions", then click "Save".


 

When creating digital wallets in Liferay, the platform automatically links each wallet to its corresponding user, ensuring that only the owner can view and manage their wallets. This access control is an integrated functionality of Liferay's Objects, significantly simplifying permission management.

Try creating a new user and access the "My Wallets" page to add digital wallets. You will observe that, when creating a new wallet, it is exclusively associated with the creating user, reinforcing security and privacy. If a second user is added to the system, they will not have visibility or access to the first user's wallets. Thus, each user maintains complete control over their own wallets, being able to create, edit, and remove records as needed, without interference or visibility from other users.

This method of direct association between users and their digital wallets highlights Liferay's approach to data security and customizing the user experience, facilitating the implementation of complex functionalities intuitively and securely.
 

 



Transaction Object

After creating the Wallet object, the next step is to establish the Transaction object, which consolidates the execution of transactions of RayCoin units from one wallet to another. This process will allow the user to select one of their wallets for debit and specify the recipient wallet's address for credit. To facilitate this operation, we will create a new object named "Transaction". It is important to note that the recipient wallet may not belong to the user conducting the transaction.

To add a layer of security and authentication to the transactions, we will implement a Groovy script within the "Actions" tab of the Transaction object. This script will be responsible for digitally signing the transaction using the private key of the wallet selected by the user. After the signature, the transaction will be sent to another Liferay instance via the Restful API, ensuring the integrity and truthfulness of the operation.

Therefore, select the option to create a new object called Transaction. Add the following custom fields to this object:

Label Field Name Type Mandatory
To Address toAddress Long Text Yes
Amount amount Precision Decimal Yes
Signature signature Long Text No

 

Next, we will link the Transaction object to the Wallet object, so that the user can select from which wallet they wish to have their RayCoins debited. Select the "Relationships" tab, then "Add Object Relationship," and fill in with the following information:

  • Label: Wallet
  • Name: wallet
  • Type: One to Many
  • One Record Of: Wallet
  • Many Records Of: Transaction


 

Note that upon saving, a new field named Wallet will appear in the "Fields" tab. Edit this field to set the "Mandatory" option to true, which will require the user to select a source wallet when creating a new transaction.


 

Next, select the "Actions" tab and add a new action named "Process Transaction," select the trigger type "On After Add" and action type "Groovy Script" as was done during the creation of the Wallet object. In this script, we first create a Wallet class that retrieves a wallet from the "walletId" parameter and stores the public and private keys, as below:

class Wallet {

	def privateKey
	def publicKey

	Wallet(walletId) {
		def objEntry = ObjectEntryLocalServiceUtil.getObjectEntry(walletId)
		this.privateKey = objEntry.getValues().get("privateKey")
		this.publicKey = objEntry.getValues().get("publicKey")
	}

	String getPrivateKey() {
		return privateKey
	}

	String getPublicKey() {
		return publicKey
	}
} 

Next, we will create a class named "Transaction," as below:

class Transaction {

	def toAddress
	def amount
	Wallet wallet
	def transactionData
	def signature

	Transaction(toAddress, amount, wallet) {
		this.toAddress = toAddress
		this.amount = amount
		this.wallet = wallet
		this.transactionData = "${this.wallet.publicKey}${toAddress}${amount}"
	}

	String getSignature() {
		return signature
	}

	void sign() {
		PrivateKey privateKey = stringToPrivateKey(wallet.privateKey)
		byte[] signedMessage = sign(transactionData, privateKey)
		signature = Base64.getEncoder().encodeToString(signedMessage)
	}

	byte[] sign(String data, PrivateKey privateKey) {
		def signatureInstance = Signature.getInstance("SHA256withECDSA")
		signatureInstance.initSign(privateKey)
		signatureInstance.update(data.bytes)
		return signatureInstance.sign()
	}

	PrivateKey stringToPrivateKey(String privateKeyAsString) throws NoSuchAlgorithmException, InvalidKeySpecException {
		byte[] privateKeyBytes = Base64.decoder.decode(privateKeyAsString)
		KeyFactory kf = KeyFactory.getInstance("EC")
		PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes)
		return kf.generatePrivate(keySpec)
	}
}

 

This class is designed to encapsulate the details of a transaction, receiving in its constructor essential parameters for executing a RayCoins transfer:

  • toAddress: The destination address where the RayCoins will be credited.
  • amount: The quantity of RayCoins to be transferred.
  • wallet: The source wallet from where the RayCoins will be debited.

The sign() method is responsible for implementing the necessary logic to sign the transaction. It uses the private key of the source wallet, based on the transactionData, which compiles the essential data of the transaction: the source address, destination address, and the amount to be transferred. This approach ensures the authenticity of the transaction by encrypting the data with the private key of the source wallet.

After generating the signature, it's crucial to incorporate it into the previously retrieved Transaction object:

// Create Wallet and Transaction objects
Wallet wallet = new Wallet(r_wallet_c_walletId)
Transaction transaction = new Transaction(toAddress, amount, wallet)

// Sign the transaction
transaction.sign()

// Update the object entry with the new signature
def userId = Long.valueOf(creator)
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signature: transaction.signature], new ServiceContext())


Note that there are variables corresponding to the Object fields that are made available in the context of the script, such as r_wallet_c_walletId, which corresponds to the identifier of the Wallet object selected by the user for the transaction creation. This field is used by the Wallet class to retrieve the related record and then retrieve the privateKey that will be used in generating the signature. This signature will also be validated by the raycoin instance when it is sent. At the end of the algorithm, the ObjectEntryLocalServiceUtil class is used to store the signature in the corresponding field of the created object.

Finally, the script would be as follows:

import java.security.KeyFactory
import java.security.NoSuchAlgorithmException
import java.security.PrivateKey
import java.security.Signature
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import javax.json.Json
import javax.json.JsonObject
import org.apache.http.HttpEntity
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.util.EntityUtils

import com.liferay.object.service.ObjectEntryLocalServiceUtil
import com.liferay.portal.kernel.service.ServiceContext

Wallet wallet = new Wallet(r_wallet_c_walletId)
Transaction transaction = new Transaction(toAddress, amount, wallet)

transaction.sign()

def userId = Long.valueOf(creator)
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signature: transaction.signature], new ServiceContext())

class Transaction {

	def toAddress
	def amount
	Wallet wallet
	def transactionData
	def signature

	Transaction(toAddress, amount, wallet) {
		this.toAddress = toAddress
		this.amount = amount
		this.wallet = wallet
		this.transactionData = "${this.wallet.publicKey}${toAddress}${amount}"
	}

	String getSignature() {
		return signature
	}

	void sign() {
		PrivateKey privateKey = stringToPrivateKey(wallet.privateKey)
		byte[] signedMessage = sign(transactionData, privateKey)
		signature = Base64.getEncoder().encodeToString(signedMessage)
	}

	byte[] sign(String data, PrivateKey privateKey) {
		def signatureInstance = Signature.getInstance("SHA256withECDSA")
		signatureInstance.initSign(privateKey)
		signatureInstance.update(data.bytes)
		return signatureInstance.sign()
	}

	PrivateKey stringToPrivateKey(String privateKeyAsString) throws NoSuchAlgorithmException, InvalidKeySpecException {
		byte[] privateKeyBytes = Base64.decoder.decode(privateKeyAsString)
		KeyFactory kf = KeyFactory.getInstance("EC")
		PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes)
		return kf.generatePrivate(keySpec)
	}
}

class Wallet {

	def privateKey
	def publicKey

	Wallet(walletId) {
		def objEntry = ObjectEntryLocalServiceUtil.getObjectEntry(walletId)
		this.privateKey = objEntry.getValues().get("privateKey")
		this.publicKey = objEntry.getValues().get("publicKey")
	}

	String getPrivateKey() {
		return privateKey
	}

	String getPublicKey() {
		return publicKey
	}
}

After saving the script, select the Details tab, choose "Custom Apps" in the "Panel Link" option, disable the "Enable Categorization of Object entries" option, and then publish the object. Note that the Transactions option will now be available in the Applications -> Custom Apps menu.


 

Add a new Transaction and see that the Signature field will be automatically generated.


 

Similar to the process of creating the Wallet object, it is necessary to add a new page called "My Transactions" on the XChangeRay site. This step includes configuring appropriate permissions for the User Role, allowing the addition of new entries. Subsequently, the Transactions widget should be incorporated into the new page, analogous to what was done on the "My Wallets" page. This way, users will be able to enter their transactions directly.


Up to now, transactions created on the XChangeRay instance are not sent to the Blockchain (RayCoin) instance, due to the absence of the necessary objects in the other instance. However, it is possible to anticipate and prepare the code for sending transactions. This is done by editing the Groovy script of the Transactions object, incorporating the sendToBlockchain(postUrl) method into the Transaction class:

class Transaction {

    . . .

    void sendToBlockchain(postUrl) {

        JsonObject jsonParams = Json.createObjectBuilder()
                .add("fromAddress", this.wallet.publicKey)
                .add("toAddress", toAddress)
                .add("amount", amount)
                .add("signature", signature)
                .add("signatureValid", true)
                .add("transactionData", transactionData)
                .add("transactionStatus", "pending")
                .build()

        String json = jsonParams.toString()

        HttpPost httpPost = new HttpPost(postUrl)

        httpPost.setHeader('Content-Type', 'application/json')
        httpPost.setEntity(new StringEntity(json))

        HttpClient httpClient = HttpClientBuilder.create().build()
        httpClient.execute(httpPost)
    }
}

This method performs a POST request to the specified endpoint in "postUrl," which will correspond to the Transaction object's endpoint in the RayCoin instance. This endpoint, which will be detailed in subsequent sections of this article, is anticipated as: "http://raycoin.local:8080/o/c/transactions/". Therefore, at the beginning of the script, include the call to the sendToBlockchain method, passing the URL of the endpoint:

transaction.sendToBlockchain("http://raycoin.local:8080/o/c/transactions/")

Thus, it would be as follows:

Wallet wallet = new Wallet(r_wallet_c_walletId)
Transaction transaction = new Transaction(toAddress, amount, wallet)

transaction.sign()

def userId = Long.valueOf(creator)
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signature: transaction.signature], new ServiceContext())

transaction.sendToBlockchain("http://raycoin.local:8080/o/c/transactions/")

The complete final version of the script can be found at the following link: https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/xchangeray/objects/transaction/AddTransaction.groovy

RayCoin Instance

Creating Objects

After setting up the XChangeRay instance with its objects, it's time to turn our attention to the main instance, RayCoin, to configure the objects that will form the backbone of our business logic. Access the URL http://raycoin.local:8080/ and log in as an administrator. In the Control Panel, find and select the "Objects" option to start creating the necessary objects for this instance. The objects and their fields should be configured as follows:

Block:

Label Field Name Type Mandatory
Index index Integer Yes
Previous Hash previousHash Long Text Yes
Nounce nounce Integer Yes

 

Transaction:

Label Field Name Type Mandatory
From Address fromAddress Long Text Yes
To Address toAddress Long Text Yes
Amount amount Precision Decimal Yes
Signature signature Long Text Yes
Signature Valid signatureValid Boolean No

 

Wallet Balance:

Label Field Name Type Mandatory
Address address Long Text Yes
Balance balance Precision Decimal Yes

 

Blockchain:

Label Field Name Type Mandatory
Name name Text Yes
Max Pending Transactions maxPendingTransactions Integer Yes
Reward Address rewardAddress Long Text Yes
Reward Value rewardValue Precision Decimal Yes
Authorization authorization Long Text Yes
Blockchain URL BlockchainURL Text Yes

 

For each object created, access the "Details" tab, adjust the "Custom Apps" value in the "Panel Link" option, disable "Enable Categorization of Object entries," and finally, publish the object by clicking on "Publish".

Relationships

After establishing the objects, it's necessary to configure the relationships between them, simulating the links of a database. In Liferay, these relationships can be many-to-many or one-to-many. For our application, a Blockchain object will contain multiple Blocks, and each Block, in turn, will house multiple Transactions. To configure these relationships, edit the Transaction object, select the "Relationships" tab, and add a new relationship with the information:

  • Label: Block
  • Name: block
  • Type: One to Many
  • One Record Of: Block
  • Many Records Of: Transaction


 

Go back to the "Fields" tab and note that a new field named "Block" has been created, with the type "Relationship". As transactions initially do not belong to any block when they are recorded, this field does not need to be mandatory.

Now go to the Block object's editing screen, navigate to the "Relationships" tab, and you will see that the relationship created in the Transaction object also appears on this screen.

Click the "+" button to then include the relationship between the Block and the Blockchain, fill in the following information:

  • Label: Blockchain
  • Name: blockchain
  • Type: One to Many
  • One Record Of: Blockchain
  • Many Records Of: Block

Now in the "Fields" tab, see the "Relationship" type field named Blockchain, change this field to make it mandatory by marking the "Mandatory" field.


 

Access Permissions

Finally, it is crucial to set access permissions to determine which objects will be public for reading and/or writing. This setting is vital, especially to allow external systems, such as XChangeRay, to interact with the blockchain by creating Transactions and querying Blocks and Wallet Balances. Access the "Control Panel" menu, go to "Roles," select "Guest" under "Regular Roles," and in the "Define Permissions" tab, search for the term "Transactions," and check the "Add Object Entry" option in the "TRANSACTIONS" category and "View" in the "TRANSACTION" category, both under "RESOURCE PERMISSIONS".


 

After saving, search for the term "Blocks" and select only the "View" option in the "BLOCK" category also under "RESOURCE PERMISSIONS".


 

Next, do the same procedure for "Wallets Balances," marking only the "View" option.


 

Initial Objects Data

After creating the objects, their relationships, and access control through the permission system, we have obtained the fundamental structure of the Blockchain. Let's now create the first records of these Objects, which will serve as the basis for the next data that will be received by the API.

Navigate to "Applications" and choose "Custom Apps," select "Blockchains," and use the "+" button to add a new record. Fill in the information as follows:

  • Authorization: Use for authentication in calls to the Liferay RestFul API for non-public methods. For example, to check the need for mining after a transaction. Use "Basic" authentication by converting the username and password to base64. For example, for the user 'test@liferay.com' with password 'test', the value will be "Basic dGVzdEBsaWZlcmF5LmNvbTp0ZXN0".
  • BlockchainURL: Enter the endpoint of the RayCoin instance, such as "http://raycoin.local:8080".
  • Max Pending Transactions: Define the number of pending transactions required to start the mining process, for example, "3".
  • Reward Address: Use the public key of a wallet generated for the miner, for example, the public key can be "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKutm5pE/nTelGk9p2mk9HpULdy3ID2tWTx/2O+D65dsAzA1MQNSdfF6RwGnCZfik++V6trwdCPTmomz0rWD73g==".
  • Reward Value: Set the mining reward value, such as "10".


 

After saving this record, proceed to the "Blocks" object and add the initial block, the genesis block, with the following information:

  • Hash: Use "0" for the initial hash.
  • Index: Position "0" for the first block.
  • Nonce: Value "0", indicative of being the genesis.
  • Previous Hash: Enter "none," since it is the first block.
  • Blockchain: Select the previously created "RayCoin" option.


 

Having done that, let's now create a record for "Transaction," representing the initial transaction with an allocation of 500,000 RayCoins to a specific wallet, let's use the following wallet in this example:

  • Name: XChangeRay Wallet
  • Private Key: MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCC6yL3me8HgFbKaTdx83TsDab8EptOc1qTV8xMZvfVktQ==
  • Public Key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8QK/c9ZSIcVMN8HYfPcZ2cW/CXfN7c8hqFdazgE1lsFmes8UeIVovMgivL2qyeZAOuECrE8eg7HixuYcwhNFOw==

For this, create a new record in "Transactions" with the following attributes:

  • Amount: 500000
  • From Address: none
  • Block: 0
  • Signature: 0
  • To Address: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8QK/c9ZSIcVMN8HYfPcZ2cW/CXfN7c8hqFdazgE1lsFmes8UeIVovMgivL2qyeZAOuECrE8eg7HixuYcwhNFOw==


 

Object Actions

After establishing the fundamental structure and initial records, we are ready to integrate essential business logic through scripts associated with the "Actions" functionality of the objects. For each type of object involved, we will develop four scripts, detailed below:

Blockchain Object:

  • Mine Pending Transactions: This action, configured as standalone in the Blockchain object, diverges from actions linked to the object's lifecycle, such as the one implemented for the Wallet object in XChangeRay. Instead, this action becomes available for activation via API. Its primary function is to check if the number of pending transactions is sufficient to initiate a mining process. If affirmative, it proceeds to mine a new block that, once validated, is added to the blockchain.
  • Compute Balances: Also a standalone action in the Blockchain object, aimed at updating the Wallet Balance object records. It calculates the balance of each wallet, aggregating this data to facilitate future balance queries.

Transaction Object:

  • Validate Transaction: This action is triggered when a new Transaction is created. Its purpose is to validate the transaction's signature and verify if the originating wallet has sufficient funds to carry out the transaction.
  • Trigger Blockchain: This action, also triggered upon the creation of a new Transaction, aims to activate the Mine Pending Transactions method of the Blockchain object. This method assesses whether it is the appropriate moment to start the mining process.

Validate Transaction

To begin implementing the "Validate Transaction" functionality, go to the "Objects" section in the Control Panel, navigate to "Object -> Objects," select the "Transaction" object and proceed to the "Actions" tab. Add a new action named "Validate Transaction." In the "Action Builder" interface, choose "On After Add" in "Trigger" and select "Groovy Script" in "Action" to start drafting the script.

Initially, we will create a class called TransactionValidator, incorporating attributes that mirror those of the "Transaction" object, in addition to support attributes. The initial structure of the class will be:

class TransactionValidator {

    long companyId
    String fromAddress
    String transactionData
    String signature
    BigDecimal amount

    TransactionValidator(companyId, amount, fromAddress, transactionData, signature) {
        this.companyId = companyId
        this.amount = amount
        this.fromAddress = fromAddress
        this.transactionData = transactionData
        this.signature = signature
    }

Next, we will develop a method within this class to validate the signature associated with the object, using classes from the java.security package, available in the JVM. This process is similar to the one used for generating public and private keys and signing in the XChangeRay instance. However, in this context, we focus on validation:

void validateSignature() throws Exception {
    if (this.fromAddress == 'none') {
        return
    }

    byte[] signatureBytes = Base64.getDecoder().decode(this.signature)

    if (signatureBytes == null || signatureBytes.length == 0) {
        throw new RuntimeException("No signature found in this transaction")
    }

    def keyFactory = KeyFactory.getInstance("EC")
    def publicKeySpec = new X509EncodedKeySpec(Base64.decoder.decode(this.fromAddress))
    def publicKey = keyFactory.generatePublic(publicKeySpec)

    def signatureInstance = Signature.getInstance("SHA256withECDSA")
    signatureInstance.initVerify(publicKey)
    signatureInstance.update(this.transactionData.bytes)

    if(!signatureInstance.verify(signatureBytes)) {
        throw new Exception("Signature validation failed")
    }
}

Then, we will develop a method to verify if the originating wallet has a sufficient balance for the transaction. This process requires querying the "Wallet Balance" object to access the balances processed by the blockchain. We will use the "Blockchain" object identifier to find the appropriate address for the HTTP call, retrieving the URL of the RayCoin instance and completing it with the specific endpoint for the "Wallet Balances" API.

First, it's necessary to access the information contained in the "Blockchain" object, which stores the RayCoin instance URL. This is done through the ObjectDefinitionLocalServiceUtil class, allowing the retrieval of the "Blockchain" object definition identifier with the following code:

def blockChainObjDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(companyId, "C_Blockchain")

With this identifier in hand, we access all records of the "Blockchain" type and select the first one, considering the existence of only one record of this type:

def blockChainObj = ObjectEntryLocalServiceUtil.getObjectEntries(0, blockChainObjDef.objectDefinitionId, 0, 1).get(0)

From this object, we extract the value of the "blockchainURL" field, which provides the prefix of the address needed to perform HTTP calls:

def blockchainUrl = blockChainObj.values.get("blockchainURL")

However, this value represents only the beginning of the endpoint. To complete it, it's necessary to add the specific path of the API that manages the "Wallet Balance" objects, resulting in /o/c/walletbalances. To discover the available endpoints for each object created on the Liferay platform, the platform offers an API catalog accessible at /o/api/. By visiting http://raycoin.local:8080/o/api/, it's possible to explore and select the desired object, thus obtaining detailed information about the available endpoints.


 

This Liferay feature is extremely powerful as it allows the platform to be manipulated in a consistent and flexible manner, enabling both read and write operations, and also allowing the application of filters, sorting, searching, among other features. With this, developers can create remote applications in any technology and use the API as part of their business rules.

Therefore, to validate the balance, the script should contain a GET call to the "Wallet Balance" object endpoint, using a filter by the "Address" field to identify the balance of the originating wallet. The result of this call is processed to verify the availability of sufficient funds for the transaction:

def encodedFilter = URLEncoder.encode("address eq '${fromAddress}'", "UTF-8")
def getUrl = "${blockchainUrl}/o/c/walletbalances/?filter=${encodedFilter}"

HttpGet httpGet = new HttpGet(getUrl)
httpGet.setHeader('Content-Type', 'application/json')

HttpClient httpClient = HttpClientBuilder.create().build()
HttpResponse getResponse = httpClient.execute(httpGet)
String getResponseBody = EntityUtils.toString(getResponse.getEntity())
JsonObject getResponseJson = Json.createReader(new StringReader(getResponseBody)).readObject()

After the execution of the call, if the wallet does not have sufficient funds, an exception will be thrown, indicating insufficient balance. This process ensures that only valid transactions are processed on the blockchain:

JsonArray walletBalancesJsonArray = getResponseJson.getJsonArray("items")
if(walletBalancesJsonArray.empty) {
    throw new Exception("Wallet doesn't have enough funds")
} else {
    JsonObject walletBalance = walletBalancesJsonArray.getJsonObject(0)
    def balance = new BigDecimal(walletBalance.getJsonNumber("balance").toString())
    if(amount > balance) {
        throw new Exception("Wallet doesn't have enough funds")
    }
}

Thus, the complete method should be as follows:

void validateBalance() {

    def blockChainObjDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(companyId, "C_Blockchain")
    def blockChainObj = ObjectEntryLocalServiceUtil.getObjectEntries(0, blockChainObjDef.objectDefinitionId, 0, 1).get(0)
    def blockchainUrl = blockChainObj.values.get("blockchainURL")

    def encodedFilter = URLEncoder.encode("address eq '${fromAddress}'", "UTF-8")
    def getUrl = "${blockchainUrl}/o/c/walletbalances/?filter=${encodedFilter}"

    HttpGet httpGet = new HttpGet(getUrl)
    httpGet.setHeader('Content-Type', 'application/json')

    HttpClient httpClient = HttpClientBuilder.create().build()
    HttpResponse getResponse = httpClient.execute(httpGet)
    String getResponseBody = EntityUtils.toString(getResponse.getEntity())
    JsonObject getResponseJson = Json.createReader(new StringReader(getResponseBody)).readObject()

    JsonArray walletBalancesJsonArray = getResponseJson.getJsonArray("items")
    if(walletBalancesJsonArray.empty) {
        throw new Exception("Wallet doesn't have enough funds")
    } else {
        JsonObject walletBalance = walletBalancesJsonArray.getJsonObject(0)
        def balance = new BigDecimal(walletBalance.getJsonNumber("balance").toString())
        if(amount > balance) {
            throw new Exception("Wallet doesn't have enough funds")
        }
    }
}

It is now also necessary to create a method to update the transaction status. We will use the "status" field, a standard attribute in all object definitions in Liferay. It's important to recognize that changing the status should occur after the record creation, as by default, all objects are initially marked as "APPROVED" in the absence of a defined workflow. To simplify and avoid the complexity of introducing a workflow process, we opt to modify the status asynchronously, with a delay of three seconds after including the record in the system. The implementation of this method is described below:

void updateTransactionStatus(userId, id, status) {
    Thread.start {
        sleep(3000) // Sleep for 3 seconds
        ObjectEntryLocalServiceUtil.updateStatus(userId, id, status, new ServiceContext())
    }
}

With the TransactionValidator class now complete with all necessary methods, we can proceed with the transaction validation logic as follows:

  • Transaction Origin Validation: Initially, we check if the source address is 'none', which would indicate a reward transaction, exempting it from validations.
  • Creation of the TransactionValidator Object: Next, we instantiate the TransactionValidator object, filling its essential attributes with the transaction data.
  • Validation Process: Within a try-catch block, we perform the signature validation and wallet balance check. If both validations are successful, we update the transaction status to 'PENDING'. If any validation fails, the status is changed to 'DENIED'.
try {
    // first, set the signatureValid to false
    ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signatureValid: false], new ServiceContext())
    
    // then, validate the signature
    transactionValidator.validateSignature()
    
    // set the signatureValid to true
    ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signatureValid: true], new ServiceContext())
    
    // validate if the wallet has enough funds for this transaction
    transactionValidator.validateBalance()
    
    // set the transaction status to pending
    transactionValidator.updateTransactionStatus(userId, id, WorkflowConstants.STATUS_PENDING)
        
} catch (Exception e) {
    // as the signature wasn't validated or the wallet doesn't have funds, set the status to denied
    transactionValidator.updateTransactionStatus(userId, id, WorkflowConstants.STATUS_DENIED)
}

The complete code of the script can be found at the following address: https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/transaction/ValidateTransaction.groovy

Trigger Blockchain

Within the same "Transaction" object, add a new Action titled "Trigger Blockchain," setting it up with the type "On After Add" and selecting "Groovy Script" as the Action type. This Action will introduce the logic that, with every new transaction received by the API, will trigger the "Mine Pending Transactions" action on the "Blockchain" object, which will be detailed later.

Initially, the action will check if the transaction's originating address is 'none', indicating a reward transaction that does not require further processing:

if(fromAddress == 'none') return

Next, the script retrieves information from the "Blockchain" object to access the instance's URL and the value of the "Authorization" attribute, necessary for authentication in HTTP calls, given that the "Blockchain" object has access restrictions:

def obj = ObjectEntryLocalServiceUtil.getObjectEntry(id)
def objDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(obj.companyId, "C_Blockchain")
def objectsEntries = ObjectEntryLocalServiceUtil.getObjectEntries(
        0, objDef.objectDefinitionId, 0, 1)
def objEntry = objectsEntries.get(0)
def authheader = objEntry.values.get("authorization")
def blockchainUrl = objEntry.values.get("blockchainURL")

Considering that the action on the "Blockchain" object will be of the "standalone" type, the API call will use the PUT method. The call's URL is constructed by concatenating the blockchain's URL with the specific API endpoint to trigger the standalone action:

def putUrl = "${blockchainUrl}/o/c/blockchains/${objEntry.objectEntryId}/object-actions/minePendingTransactions"

To ensure that the call is executed asynchronously and after the successful inclusion of the record, the script runs within a new Thread, with an initial delay of 5 seconds:

Thread.start {
    sleep(5000) // Sleep for 5 seconds

    HttpClient httpClient = HttpClientBuilder.create().build()
    HttpPut httpPut = new HttpPut(putUrl)

    httpPut.setHeader('Content-Type', 'application/json')
    httpPut.setHeader('Authorization', "$authheader")

    HttpResponse putResponse = httpClient.execute(httpPut)
}

The complete code of the script can be checked at the following address: https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/transaction/CheckPendingTransactions.groovy

Compute Balances

To integrate the logic of updating balances in the Liferay blockchain, first access the Blockchain object definition through the control panel and add a new Action called "Compute Balances". In this context, choose "Standalone" as the "Trigger" type and select "Groovy Script" as the action to be executed.


 

We will develop a class called BlockchainBalance, which will incorporate essential attributes such as companyId and userId, in addition to an additional parameter objectDefinitionId to record the Wallet Balance object definition ID:

class BlockchainBalance {

    long companyId
    long userId
    long objectDefinitionId

    BlockchainBalance(long companyId, long userId) {
        this.companyId = companyId
        this.userId = userId
        this.objectDefinitionId = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(
                this.companyId, "C_WalletBalance").getObjectDefinitionId()
    }
}

We will implement a method removeAllBalances() to clear all balance records, preparing the system for a balance update based on Blockchain transactions:

void removeAllBalances() {
    def balanceEntries = ObjectEntryLocalServiceUtil.getObjectEntries(
            0, objectDefinitionId, QueryUtil.ALL_POS, QueryUtil.ALL_POS)
    balanceEntries.each { b ->
        ObjectEntryLocalServiceUtil.deleteObjectEntry(b.objectEntryId)
    }
}

The computeBalance() method will be responsible for first clearing existing balances and then calculating and recording updated balances based on approved transactions. This includes initializing the 'none' wallet with a significant balance for reward distribution and updating balances according to processed transactions:

void computeBalance() {

    removeAllBalances()
    
    Map balances = new HashMap()
    
    def objTransactionDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(
            this.companyId, "C_Transaction")
    List approvedTransactions = ObjectEntryLocalServiceUtil.getObjectEntries(
            0, objTransactionDef.objectDefinitionId, WorkflowConstants.STATUS_APPROVED, QueryUtil.ALL_POS, QueryUtil.ALL_POS)
    
    balances.put("none", 1000000000)
    
    approvedTransactions.each { t ->
        def v = ObjectEntryLocalServiceUtil.getValues(t.objectEntryId)
        def fromAddress = v.get("fromAddress")
        def toAddress = v.get("toAddress")
        def amount = v.get("amount")
    
        if(balances.get(fromAddress) == null) {
            balances.put(fromAddress, new BigDecimal(0))
        }
        if(balances.get(toAddress) == null) {
            balances.put(toAddress, new BigDecimal(0))
        }
        balances.put(fromAddress, balances.get(fromAddress) - amount)
        balances.put(toAddress, balances.get(toAddress) + amount)
    }
    
    balances.each { address, balance ->
        def walletBalanceValues = [address: address, balance: balance]
        ObjectEntryLocalServiceUtil.addObjectEntry(
                userId, 0, objectDefinitionId, walletBalanceValues, new ServiceContext())
    }
}

Finalize by creating an instance of BlockchainBalance and invoking the computeBalance() method to effectively update the wallet balances:

def obj = ObjectEntryLocalServiceUtil.getObjectEntry(id)
balance = new BlockchainBalance(obj.getCompanyId(), Long.valueOf(creator))
balance.computeBalance()

The complete code of this script can be accessed at the following address: https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/blockchain/ComputeBalances.groovy

Mine Pending Transactions

To effectively implement block mining and its inclusion in the blockchain, we will add a new action to the Blockchain object called "Mine Pending Transactions". This action will be of the "Standalone" type and will use "Groovy Script" as its execution method.

Within the script, we initially retrieve all transactions with 'PENDING' status:

def userId = Long.valueOf(creator)
def user = UserLocalServiceUtil.getUserById(userId)

def objDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(user.companyId, "C_Transaction")
List pendingTransactions = ObjectEntryLocalServiceUtil.getObjectEntries(
        0, objDef.objectDefinitionId, WorkflowConstants.STATUS_PENDING, QueryUtil.ALL_POS, QueryUtil.ALL_POS)

We check if the number of pending transactions exceeds the limit defined in the maxPendingTransactions attribute of the Blockchain object. If so, we proceed to mine a new block:

if(pendingTransactions.size() > maxPendingTransactions) {
    def blockchain = new Blockchain(
            user.companyId, userId, id, rewardAddress, rewardValue, pendingTransactions, authorization, blockchainURL)
    blockchain.minePendingTransactions()
}

Thus, the Blockchain class can start with the following attributes:

class Blockchain {
    long companyId
    long userId
    long blockchainId
    String rewardAddress
    BigDecimal rewardValue
    List pendingTransactions
    String authorization
    String blockchainURL
}

We will also need two other support classes, to handle blocks and transactions, therefore we create a Block class:

class Block {
    long blockchainId
    int index
    String timestamp
    List transactions
    String previousHash
    String hash
    int nonce
}

And also a Transaction class:

class Transaction {
    Long id
    String fromAddress
    String toAddress
    BigDecimal amount
    String signature
}

In this Transaction class, we will add methods to return the transaction data in the format as we have used in other classes, in addition to the methods toJson(), toJsonArray(), and toString(), which we will use as the basis for calculating the Block Hash:

String getTransactionData() {
    return "${fromAddress}${toAddress}${amount}"
}
JsonObject toJson() {
    JsonBuilderFactory factory = Json.createBuilderFactory(null);
    JsonObject transactionJson = factory.createObjectBuilder()
            .add("fromAddress", fromAddress)
            .add("toAddress", toAddress)
            .add("amount", amount)
            .add("signature", signature)
            .add("id", id)
            .build();
    return transactionJson;
}
static JsonArray toJsonArray(List transactions) {
    JsonBuilderFactory factory = Json.createBuilderFactory(null);
    JsonArrayBuilder arrayBuilder = factory.createArrayBuilder();
    for (Transaction transaction : transactions) {
        arrayBuilder.add(transaction.toJson());
    }
    return arrayBuilder.build();
}
String toString() {
    return toJson().toString();
}

In addition, we will still need methods to save a new transaction, and another to delete a transaction:

ObjectEntry save(long companyId, long userId) {
    def values = [fromAddress: fromAddress, toAddress: toAddress, amount: amount, signature: signature]
    def objDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(companyId, "C_Transaction")
	ObjectEntry obj = ObjectEntryLocalServiceUtil.addObjectEntry(
			userId, 0, objDef.objectDefinitionId, values, new ServiceContext())
	this.id = obj.objectEntryId
	return obj
}
void delete() {
	ObjectEntryLocalServiceUtil.deleteObjectEntry(this.id)
}

Finally, two more methods, the first to update the transaction status, and another to associate the transaction with a block:

ObjectEntry updateStatus(long userId, int status) {
    return ObjectEntryLocalServiceUtil.updateStatus(userId, this.id, status, new ServiceContext())
}
ObjectEntry addToBlock(long userId, long blockId) {
    def values = [r_transactions_c_blockId: blockId]
    return ObjectEntryLocalServiceUtil.updateObjectEntry(userId, this.id, values, new ServiceContext())
}

With the Transaction class complete, we will now include four more methods in the Block class, the first is to save the object:

ObjectEntry save(long companyId, long userId) {
    def values = [index: index, hash: hash, previousHash: previousHash, nonce: nonce, r_blockchain_c_blockchainId: blockchainId]
    def objDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(companyId, "C_Block")
    return ObjectEntryLocalServiceUtil.addObjectEntry(
            userId, 0, objDef.objectDefinitionId, values, new ServiceContext())
}

Then, three other methods that are involved in the mining process of the block:

String calculateHash() {
    return md5("${index}${timestamp}${Transaction.toJsonArray(transactions)}${previousHash}${nonce}")
}
void mineBlock(int difficulty) {
    while (!hash[0..<difficulty].every { it == '0' }) {
        nonce++
        hash = calculateHash()
    }
}
String md5(String input) {
    MessageDigest digest = MessageDigest.getInstance("MD5")
    byte[] hash = digest.digest(input.getBytes("UTF-8"))
    return hash.encodeHex().toString()
}

The mineBlock() method is essential for the block mining process in the blockchain, determined by the difficulty parameter, an integer that defines the complexity of the task. The higher the difficulty value, the greater the computational effort required to find a valid hash for the block, based on the combination of various elements like the block index, timestamp, the list of transactions, the previous block's hash, and the nonce (an attempt counter). The goal is to generate a hash that starts with a specific sequence of zeros, meeting the established difficulty criterion. This mechanism, although simplified in this example using the MD5 algorithm for hash generation, illustrates the fundamental concept behind mining in blockchains. In more advanced implementations, more complex cryptographic algorithms and mathematical problems are used to enhance the security and integrity of the chain. However, a simplified approach was chosen in this case to facilitate understanding and demonstration of the mining process.

Returning to the Blockchain class, we will implement a utility method for making RestFul API calls:

private JsonArray fetchItemsFromApi(url) {
    HttpGet httpGet = new HttpGet(url)
    httpGet.setHeader('Content-Type', 'application/json')
    HttpClient httpClient = HttpClientBuilder.create().build()
    HttpResponse getResponse = httpClient.execute(httpGet)
    String getResponseBody = EntityUtils.toString(getResponse.getEntity())
    JsonObject getResponseJson = Json.createReader(new StringReader(getResponseBody)).readObject()
    return getResponseJson.getJsonArray("items")
}

And another method to retrieve the balance of a specific wallet:

private BigDecimal getWalletBalance(String address) {
    def encodedFilter = URLEncoder.encode("address eq '${address}'", "UTF-8")
    def getUrl = "${this.blockchainURL}/o/c/walletbalances/?filter=${encodedFilter}"
    JsonArray walletBalancesJsonArray = this.fetchItemsFromApi(getUrl)
    if(walletBalancesJsonArray.empty) {
        return new BigDecimal(0)
    } else {
        JsonObject walletBalance = walletBalancesJsonArray.getJsonObject(0)
        return new BigDecimal(walletBalance.getJsonNumber("balance").toString())
    }
}

It is still necessary to have two other methods, one to retrieve the latest block in the chain, and another to retrieve a block from its Previous Hash attribute:

private JsonArray getLatestBlockJson() {

    def encodedSort = URLEncoder.encode("id:desc", "UTF-8")
    def getUrl = "${this.blockchainURL}/o/c/blocks/?sort=${encodedSort}&pageSize=1"

    return this.fetchItemsFromApi(getUrl)
}

private JsonArray getBlockWithPreviousHashJson(String previousHash) {

    def encodedFilter = URLEncoder.encode("previousHash eq '${previousHash}'", "UTF-8")
    def getUrl = "${this.blockchainURL}/o/c/blocks/?filter=${encodedFilter}"

    return this.fetchItemsFromApi(getUrl)
}

After that, we will implement the final method, called minePendingTransactions(), which will be responsible for implementing the entire business logic. We create an internal list of Transactions to receive the values from the list of pending transactions. Next, we create a reward transaction, save this transaction in the database, and also add it to the same list:

void minePendingTransactions() {
    List transactions = new ArrayList()
    pendingTransactions.each { pt ->
        if(pt.getValues().get("signatureValid") == true) {
            transactions << new Transaction(
                    pt.getValues().get("fromAddress"),
                    pt.getValues().get("toAddress"),
                    pt.getValues().get("amount"),
                    pt.getValues().get("signature"),
                    pt.objectEntryId)
        }
    }
    def rewardTransaction = new Transaction("none", rewardAddress, rewardValue)
    rewardTransaction.save(companyId, userId)
    transactions << rewardTransaction

Then retrieve the Hash of the last block in the chain, as well as its index, to increment it and use it as a parameter in the block we need to create:

JsonObject latestBlockJson = this.latestBlockJson.getJsonObject(0)
def index = latestBlockJson.getJsonNumber("index").intValue()+1
def previousHash = latestBlockJson.getString("hash")

To ensure that the transactions are not processed by another mining process, which may occur in parallel, we will change the status of all transactions to 'SCHEDULED':

transactions.each { t ->
    t.updateStatus(userId, WorkflowConstants.STATUS_SCHEDULED)
}

Next, we can create the new block and then execute the method for mining, using a difficulty level of 2, meaning the generated hash must contain two zeros at the beginning:

def block = new Block(index, new Date().toString(), transactions, previousHash, blockchainId)
def difficulty = 2
block.mineBlock(difficulty)

After the mining process, we check if another block that was mined before this one and placed in the same position exists, if there is, we must abandon the mining, deleting the reward transaction, and returning the status of the transactions to 'PENDING':

def blockWithPreviousHashJson = this.getBlockWithPreviousHashJson(previousHash)
if(blockWithPreviousHashJson != null && blockWithPreviousHashJson.size() > 0) {
    rewardTransaction.delete()
    transactions.each { t ->
        t.updateStatus(userId, WorkflowConstants.STATUS_PENDING)
    }
    // stop the execution
    return
}

If everything went right, we can persist the block in the system:

ObjectEntry blockMined = block.save(companyId, userId)

To efficiently manage and update the balances of the wallets involved in the transactions within the block mining process, we will implement the following logic using a HashMap. This map will store the current balances of all wallets impacted by the transactions, facilitating the validation of sufficient funds and the correct association to the mined block. Each transaction will be individually assessed to determine if the originating wallet has sufficient funds for the transaction. Otherwise, the status of the transaction will be marked as 'DENIED'. Approved transactions will receive the status 'APPROVED':

Map walletBalances = new HashMap()
transactions.each { t ->
    walletBalances.put(t.fromAddress, this.getWalletBalance(t.fromAddress))
}
transactions.each { t ->
    t.addToBlock(userId, blockMined.objectEntryId)
    BigDecimal balance = walletBalances.get(t.fromAddress)
    if(balance < t.amount) {
        println "Wallet $t.fromAddress doesn't have sufficient coins"
        t.updateStatus(userId, WorkflowConstants.STATUS_DENIED)
    } else {
        walletBalances.put(t.fromAddress, balance - t.amount)
        t.updateStatus(userId, WorkflowConstants.STATUS_APPROVED)
    }
    if(walletBalances.containsKey(t.toAddress)) {
        walletBalances.put(t.toAddress, (walletBalances.get(t.toAddress)+t.amount))
    }
}

After the successful completion of the mining process and transaction validation, we will update the reward transaction's status to 'APPROVED' and associate it with the mined block. To ensure data consistency and avoid potential execution conflicts, updating the wallet balances will be performed asynchronously, with a brief delay:

def computeBalanceUrl = "${blockchainURL}/o/c/blockchains/${blockchainId}/object-actions/computeBalances"

Thread.start {
    sleep(3000)

    rewardTransaction.updateStatus(userId, WorkflowConstants.STATUS_APPROVED)
    rewardTransaction.addToBlock(userId, blockMined.objectEntryId)

    HttpClient httpClient = HttpClientBuilder.create().build()
    HttpPut httpPut = new HttpPut(computeBalanceUrl)
    httpPut.setHeader('Content-Type', 'application/json')
    httpPut.setHeader('Authorization', "$authorization")
    HttpResponse putResponse = httpClient.execute(httpPut)
}

The complete code, including all necessary classes for the implementation, is available at the following link: https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/blockchain/MinePendingTransactions.groovy

Tests

With the setup of the necessary actions complete, it is now possible to start creating new transactions using the XChangeRay instance. But first, go to the Blockchain in Applications -> Custom Apps -> Blockchain menu and execute the Compute Balances method from the Blockchain record in order to compute the initial balance of the wallets.


 

After this, create some wallets and transactions from the XChangeRay instance. This process will allow the direct observation of how transactions are processed and integrated into the RayCoin instance. It is recommended to start with the "XChangeRay Wallet" as it initially holds a significant amount of coins, facilitating the execution of test transactions. Thus, create some wallets and transactions in this instance.


 

It is also possible to follow the blocks records.


 

And also the wallet balances through the Wallet Balances object.


 

Artifacts

All the code for this project is available on the GitHub repository: https://github.com/andrefabbro/blockchain-demo/

Moreover, the repository contains examples of implementing a blockchain in Groovy, serving as the basis for the scripts developed in this project. These examples can be found in the folder: https://github.com/andrefabbro/blockchain-demo/tree/main/src/main/groovy/com/liferay/poc/blockchain/example

You can also find the definition of all the objects used in this article, which can be easily imported into a Liferay instance of the same version, at the following link: https://github.com/andrefabbro/blockchain-demo/tree/main/objects

For those who prefer a more direct approach and want to try the demo showcased in this article without going through the whole configuration process, it is feasible to start an instance of Liferay version 7.4.3.109-ga109, connect it to a local MySQL database, and import the DUMP available at: https://github.com/andrefabbro/blockchain-demo/tree/main/mysql-dump

This DUMP includes all the object definitions, scripts, and sample data necessary to simulate the blockchain described in this article, providing an efficient way to visualize the application in action.

Conclusion

The popularity of blockchain, initially spurred by the cryptocurrency market, barely scratches the surface of its disruptive potential. Beyond the realm of digital currencies, blockchain has a wide array of applications in various sectors, from smart contracts, supply chains, and logistics, to real estate registry, digital identity, and electronic voting. These applications underscore the innovative capability of blockchain to offer robust solutions for traditional challenges, transforming processes in a myriad of industries.

While this article focused on exploring the development of a blockchain utilizing the native functionalities of Liferay, specifically through Objects, it is important to highlight that the Liferay platform is equipped with other tools and extensions, such as Client Extensions (available at: https://learn.liferay.com/w/dxp/building-applications/client-extensions), enabling the implementation of additional business logics. Therefore, Liferay presents itself as a versatile and powerful platform, capable of supporting the development of complex solutions, allowing developers to concentrate their efforts on the specific requirements of each business.

This article aims not only to demonstrate the applicability of blockchain technology in diverse contexts but also to emphasize the flexibility of Liferay as a development tool. The possibilities are broad and promising, paving the way for innovations that can reshape the fabric of various sectors, enhancing efficiency, transparency, and security.

Blogs