In this edition of Road to Neo3, we’ll be looking into the development process behind a new feature that is intended to improve the smart contract system for developers by resolving issues with script hashes. On Neo3, contracts are instead represented by universally unique identifiers (UUIDs) which remain consistent even if the contract is updated and avoid the need for resource-heavy data migration.
In order for a smart contract to be invoked by a user, the node must first know where to look in its storage to find the code to execute. On Neo2, storage keys take the form of script hashes, which are derived by hashing the instruction set (opcodes) of a given contract. This is done first with SHA256, then the result is hashed again with RIPEMD160 to produce the script hash.
These script hashes can also be prefixed with the current Neo version number and run through Base58Check encoding, turning it into the more readable public address format that typical Neo users will be more familiar with.
Although this approach helps ensure that different contracts will not end up with the same script hash, it introduces a new problem; updating a contract through the migration process means all stored data must be moved over to the new contract script hash. As Neo core developer Erik Zhang explains, this event could potentially carry a large overhead:
“Currently, when a smart contract is upgraded, all stored data is migrated to the new contract. If there is a large amount of data in the contract, the workload of the migration will be very large and may cause a DoS attack.”
Additionally, the change of script hash itself also poses an issue. Service providers that have integrated a particular contract will need to manually update their services to use the new script hash after a successful contract migration. This adds overhead for entities such as exchanges, wallet providers, and other contracts that interact via dynamic invocation.
The mechanism behind script hash derivation causes another side effect—old contracts cannot be redeployed. Since the input instruction set is identical in both contracts, the same script hash is produced after hashing, and the deployment will fail due to the match.
After introducing the concerns with the current logic behind data migration, Zhang proposed contract redirection as a potential solution:
“Instead of migrating data directly during each contract upgrade, a redirection record is created. When the contract reads the data, it can find the original contract hash based on the redirection record, thus accessing the correct data.”
Mengyu Liu, a fellow NGD developer, helped flesh out the proposal with additional details, noting that the heavy resource consumption stems from the need for the new contract to update the script hash data prefix of storage keys in order to inherit data from the old contract.
Using the redirection solution, the data prefix no longer needs to be updated. Instead, a record can be created which redirects contract data modifications to the original contract. In essence, the new contract can read and write data directly from the original contract storage, removing the need to migrate data at all.
Work began on the implementation of this solution, which involved the creation of two new contract attributes, redirection and isDeleted, required to handle contract deletion logic and to provide the original script hash to use for redirects.
During this time, further issues were identified with the proposed solution. These include the inability to deploy old contracts, and the inability to invoke methods from the original contract after the migration. This means that as mentioned earlier, third party providers would still need to update their services to use the new contract hash.
Not satisfied with the shortcomings of the chosen approach, Zhang highlighted an alternate approach to the issues with script hashes; universally unique identifiers. In the next RTN3 article, we’ll follow the development and provide details on the final implementation of contract UUIDs.