What is the purpose of full nodes storing merkle roots? Why don't they save the transactions only? The hashes could be calculated only when a lightnode asks for them and the merkle roots could be saved only be the lightnodes. Am I missing something here?
I suspect you are confusing merkle roots and merkle trees.
A merkle tree is the binary tree of hashes. The leaves are individual transactions, which are hashed, and then the hashes hashed together until only one hash remains. That remaining hash is the merkle root. A merkle branch is a particular path down the merkle tree from the merkle root to a transaction.
Full nodes store the merkle root because it is part of the block header. The block header contains the merkle root so that the block hash itself will also commit to the transactions contained within the block. While the merkle root itself isn't generally used elsewhere, as part of the block header, it frequently needs to be sent out to other nodes which are downloading blocks. It is expensive to have to recompute the merkle root every time, so instead we just keep it so that blocks can be sent out quickly.
Furthermore, by storing the complete block header, when the transactions themselves are discarded in pruning, the block headers are kept and so that the header chain is still tracked. Without the transactions, it would not be possible to verify this header chain again if the merkle root were not being stored.
Full nodes do not store the entire merkle tree or any merkle branches. These are unnecessary for normal operation. When a SPV client asks for a merkle branch, it is computed on the fly.
Full nodes do not maintain the blockchain only for servicing lightweight nodes. The primary reason is for its own security so as to ensure that no blocks that it has received over the network have been tampered with and they all meet the consensus requirements.
In order to verify that the blocks are linked going back to the genesis block, the full node will have to calculate the block header. Block header of a given block contains the previous block header hash and merkle root amongst other fields. The merkle root acts like an identifier to all the transactions included in the block as changing even a slightest component of the transaction can completely change the merkle root. So a full node verifies the transactions, and calculates the merkle root from those transaction hashes. Then this merkle root is used amongst other block header fields like previous block header hash to calculate the current block header hash. When verifying the next block in the blockchain, the node will ensure that the previous block header hash field in the block matches the one it had calculated for the last block. This allows the full node to ensure that all the blocks are linked in a chain without any tampering.
Moreover, the full nodes sync with one another headers first. In simple words, when a node boots up (at start of even after a pause) it would sync with the rest of the network syncing the headers of the blockchain first. And merkle root is part of the block header.
why the merkle root must be saved and not calculated on the fly during the verification/validation process
The merkle root is a part of the block header. When mining, it is the block header that miners must construct and then hash, in an attempt to find a valid block. So when mining, a miner must commmit to the transactions that will be included in their blocks. By this mechanism, after a valid proof-of-work has been found, a miner cannot alter which transactions are included in the block.
In other words, the merkle root ties the transactions in the block to the proof-of-work, thus securing the history and ordering of transactions in a way that is relatively simple and computationally non-intensive.
I’ll add a second answer, that is an analogy instead of a technical explanation. As analogies go, it isn’t perfect, but I think it works well enough.
Consider a situation in which you must fill in a lottery card, by picking 5 numbers. This lottery card is then stamped by the lottery authority, so that you cannot change your numbers later to claim a false win. Obviously, it is critically important that you commit to the numbers before the lottery draw takes place. If you could change the numbers later, then the lottery would be broken.
This is similar to bitcoin mining: selecting numbers is like selecting transactions, and then the ‘stamp of approval’ is like the merkle root being committed to the header that hashes to a valid proof of work. Finding a valid proof of work is the mechanism by which an ordering of transactions is selected.
So, without the merkle root being included in the header, the proof of work would not be related to the transactions at all. It would be like miners were just submitting blank lottery cards, that they could change the numbers on later. Since the point of a blockchain is to commit to an ordering of transactions, it is important to commit to the transaction ordering prior to the valid block being found. There is no way to do this afterwards, in a cryptographically secure manner.