Creating a blockchain structure using Liferay Objects
Andre Fabbro
February 20, 2024
35 Minute Read
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.
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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")
}
}
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)
}
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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':
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':
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:
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.
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.
This website uses cookies and similar tools, some of which are provided by third parties (together “tools”). These tools enable us and the third parties to access and record certain user-related and activity data and to track your interactions with this website. These tools and the informationcollected are used to operate and secure this website, enhance performance, enable certain website features and functionality, analyze and improve website performance, and personalize user experience.
If you click “Accept All”, you allow the deployment of all these tools and collection of the information by us and the third parties for all these purposes.
If you click “Decline All” your IP address and other information may still be collected but only by tools (including third party tools) that are necessary to operate, secure and enable default website features and functionalities. Review and change your preferences by clicking the “Configurations” at any time.