-
Notifications
You must be signed in to change notification settings - Fork 2.3k
TIP #1: Migrations as Modular Scripts #138
Comments
Any possibily for a suicide/selfdestruct feature for removing unwanted contracts. |
|
First, a couple clarifications:
I'm really excited about this realm of thought. Truffle as it stands is a fantastic way to build simple Dapps, but for sustainable contracts, this is important stuff to deal with. I think the migration structure is clever, and I like how it allows multi-phase architectures to be constructed. Of course, since this is such a big change, I'd just like to make sure we get as many concerns on the table as possible before committing to the work. First off, if we're going to adopt migrations, it seems like we might as well have the ability to roll them back. This could be achieved by storing an array of the previous migration addresses, or maybe just the ability for a future migration to point back to an older one. Whatever the method, I think we'd regret if we could only migrate to new contracts. Secondly, as a member of the Ethereum community who loves its distributed nature, leaving one contract creator with the keys to change the contract at all times seems like a re-centralization of power. A user, even on reviewing the contract, has no guarantee that the contract won't be spontaneously updated to different terms. I guess this doesn't need to be an issue, as long as we keep the possibility for different types of migrations. Some migrations may require a vote of an approving board, which is essentially the premise of Boardroom by @SilentCicero. I guess I'm just making sure that the migration structure is open-ended enough that people could write their own migration contracts, which might require an asynchronous block step of waiting for a third party to sign off on a change. |
Thanks @FlySwatter. Responses inline:
The example migrations provided were meant to be completely separate codebases, existing at different stages of a project's lifecycle. However, they could be the same for example purposes. In the first example, no contract exists on the network yet -- this is the project's initial deploy. Here, most everyone would need to deploy new contracts like shown in the first example. The second example is a hypothetical future migration on a different project, where a hub and spoke architecture exists and some changes have been made to the spoke, thus necessitating a new deploy of the spoke and an update of the Hub.
Correct. Again, this is a hypothetical situation but one that is likely to happen in real life (I've coded dapps like this before).
It's important to note that the examples given are arbitrary code. A migration doesn't necessarily create new contracts -- just as with Rails migrations, a migration could move data around, update a value, remove an item from the list, etc. Given this, it's unclear how to create an automatic way to rollback a migration. At best we can do what Rails does, and have an
Can you explain this in more detail? You bring up a good point in that the Migrator contract needs an owner, and should only be updated by the owner, but don't believe that was your main point. In my proposal above, the creator of all contracts -- including the Migrator contract -- are owned by the person deploying them. You can already set the deployment address in Truffle. If you want your Migrator contract to be owned by multiple groups, you can edit the contract yourself to do so. In fact, you can even migrate the Migrator contract, but maybe we should add hooks to make that easier.
The world is your oyster @FlySwatter. :) The migrator contract just holds migration state: i.e., the last migration that has been run. It doesn't specify what the migration actually is. For that, you must specify it in your migration code, which as stated above can be anything you want.
Again, the Migrator contract is owned by you and you can edit it however you want, as long as it upholds a simple API. If you'd like to change it so that it has multiple owners, you can. |
Cool, thanks for the thorough answer, @tcoulter! I'm still on the fence re: up/down migrations, but it sounds like you're designing this migration path to be generic enough that it can be structured in whatever way a person wants, so it sounds good! |
Would it make sense to address these constraints:
separately from updates/migrations? I don't see that they are inextricably related. |
@chrisclark I'm not sure it makes sense to (or put differently, this is a way of addressing that). We could create a system that allows for that and works in addition to Note that, if it's not clear, the |
Great ideas. This addresses one of the biggest unanswered questions I've had with Ethereum in general: how to update imperfect code. Having said that, I AM concerned with having the OPTION to keep the contract immutable (in an easy way). Could there be maybe a flag upon project initialization that somehow specifies this? Either by disabling the feature, or maybe automatically writing a migration contract with rules that it can't be updated? |
Hi @zmitton. I'm not sure I understand. By definition contracts deployed to the Ethereum network are immutable, and there's no way to get around this. The closest option available is to create a "pointer" contract which contains an address of the actual contract meant to service a dapp. When you want to update the dapp's code, you deploy a completely new contract and then update the pointer (an example of this is the Hub and Spoke, above). But all of this is not a function of Truffle; the migrations above are just examples. If you want this pointer functionality you'll have to build it into your dapp yourself. |
At the risk of stating the obvious, I can't imagine not using a timestamp as the prefix. They have the double-benefit of sorting your migrations in sequence, as well as ensuring the migration filenames are unique. |
It seems like in this situation, it should either auto-create the Migrator contract, or possibly prompt the user about whether a Migrator contract should be created. Otherwise, it seems like it's leaving the dapp in a bad state. |
For new projects (i.e., That said, "Migrator" should probably be renamed as it's not doing the migration. It's just recording the state. Perhaps MigrationState. |
That sounds awesome, @tcoulter. I'm really excited about that feature :) |
I think this is fine. Being able to migrate "down" in Rails is nice, but I've never actually seen it used in production. I've only ever seen it used to revert/reset local schema changes while developing migrations and/or new features. |
I agree @redsquirrel. Do we need that development benefit in our case, however? I was thinking about getting around that issue with something like this:
This will just start from scratch. When used on the TestRPC, things will be quick. This assumes A) your migrations aren't doing heavy processing, and so won't take awhile (i.e., you won't be merging thousands of rows like in Rails); and B) you won't have hundreds of migrations. That said, the up/down interface could easily be added in later with little effort if ever it was needed. |
@tcoulter I imagine it would be nice to be able to re-run all migrations (assuming you're on TestRPC) to be able to get yourself out of a bad state, as well as ensuring that your migrations do actually play nice with each other. In general, though, I'd punt most of these questions until you get feedback from people using migrations in the wild. (In other words, keep it simple. 😄) |
Ya, I'm in the same boat. On the verge of implementing it to get better feedback. So far response is positive, and so comes down to implementation. |
@tcoulter Is there a branch we can watch? 👀 |
Just pushed the branch |
I've created a pull request that should make watching easier: #143 |
While developing the migrations feature, I’m finding that migrations can be made easier if there were an object to help you with the transactions. I’ve recently whipped up an example — not quite a prototype — where a migration could look something like this: module.exports = function(accounts) {
deployer.autolink();
// and/or
deployer.link(SomeLibrary, [
SomeContract,
AnotherContract
]);
deployer.new(SomeContract);
deployer.new(AnotherContract);
deployer.then(function() {
return SomeContract.deployed().callSomeFunction(5);
});
deployer.then(function() {
// Do more complex things.
});
}; Here, the The point of all this is to provide a mechanism for people to write their own complex deployments without making a tool that's equally complex, while at the same time making deployments as easy as possible. Thoughts? |
Maybe we could follow Rails Can we have a fallback function to call in case of errors during migration? migration('Adding "users" mapping to contract"', function(args, helper, accounts) {
//to be used to change the value of a flag or bind a new contract address to a contract
helper.up(function(done) {
let oldContract = Contract.at(args.address);
oldContract.change
})
//to be used when something is done and cannot be undone or at least easily undone
helper.change(function(done) {
let oldContract = Contract.at(args.address);
UpdatedContract.new()
.then(function(_newContract) {
return oldContract.upgradeTo(_newContract.address);
})
.then(function(txHash) {
console.log('Contract upgraded!');
done();
})
.catch(done);
});
//to be used to undo what "up" is doing
helper.down(function(done) {
});
helper.onError(function() {
//notify the user, fix something on the blockchain, create a log entry somewhere, something like that
});
}); |
@thiagodelgado111 Couple things: Having an up/down concept is a good one, in general, but undefined behavior will occur whenever a migration errors. For instance, imagine a migration that, when migrating up, makes 20 transactions (possibly an exaggeration). Say transaction 11 fails: This means the whole migration fails. Since we're dealing with a blockchain, you can't revert those transactions, which means you're in an unknown state. If you migrate down from there, it's likely that your down will fail as well since the down expects the environment to be in a specific state, which it is not. This is a similar problem with rollbacks: How do you rollback a migration that can't be reverted? The only way is human interaction, where you have to determine the best possible rollback yourself - and it's entirely situationally dependent. Which means - to recap - we could have a down and a rollback, but they're unlikely to work in most cases, and so in most cases are very useless. The only way around this that I can see is to have one transaction per migration, but this becomes untenable - though I can imagine some people going this route. Also, in my experience with other migration systems (i.e., Rails) Given the above, out of the gate I'm likely not going to add in support for down and rollback, but we can consider those features once more people use the first implementation and we get feedback from users. Thanks, as always! |
Hi, @tcoulter! Thanks for reading my comment and answering it :) When I thought about having a Anyway, I agree it might not be really useful. Would you still consider instead having:
var myAppData = require('myapp/libs/data');
migration('Adding "users" mapping to contract"', function(accounts) {
helper.setup(function(done) {
migrationData.params = myAppData.getParams();
})
helper.change(function(done) {
let oldContract = Contract.at(args.address);
var newContract;
UpdatedContract.new()
.then(function(_newContract) {
newContract = _newContract;
return oldContract.upgradeTo(_newContract.address);
})
.then(function() {
return newContract.use(migrationData.params);
})
.then(function(txHash) {
console.log('Contract upgraded!');
done();
})
.catch(done);
});
helper.onError(function() {
//notify the user, fix something on the blockchain, create a log entry somewhere, something like that
});
}); Also, I'm glad I ended up helping you to have the automatic revert idea with TestRPC hahaha 😄 |
@thiagodelgado111 The deployer system is still being fleshed out, but currently it works like this: A "migration" is a set of steps that need to be performed. In your migration, you build these steps and add them to the list - you don't actually execute the steps. The steps are executed behind the scenes after your migration has finished queueing them. Given that, your notion of var myAppData = require('myapp/libs/data');
module.exports = function(deployer) {
// Get the existing contract address before the deploy starts.
var existingAddress = Contract.address;
// Replace the version of Contract. deploy() will do this automatically
// if it has previously been deployed in an earlier migration.
deployer.deploy(Contract);
deployer.then(function() {
// Call the upgradeTo function on the existing contract.
Contract.at(existingAddress).upgradeTo(Contract.address);
}).then(function() {
// Call the use() function on the new contract.
// Remember: A new version was deployed in the previous step.
Contract.deployed().use(myAppData.getParams());
});
}); As you can see, the structure is very linear, and cleaner than typical Pudding though it's taking advantage of Pudding constructs considerably. Note that I removed your The only feature I haven't implemented that you suggested is the |
Cool, @tcoulter! I think you're right about leaving the task of sending an e-mail to the CI tool, I just thought it might be useful to have something like this! Thanks for answering! 😃 |
This has been added in Truffle 2.0. Closing! Thanks for everyone's feedback! |
Introduction
Truffle's current deploy process only allows for a very strict type of contract: One that doesn't take constructor parameters, and one that's meant to exist as a singleton. For many people these restrictions are limiting and confusing, as deployment for different styles of contracts is needed. Furthermore, no matter the contract, Truffle's deploy process fails to address the needs of future deployments. This proposal aims to provide a solution that allows for deploying complex contracts in a way that doesn't introduce that complexity back into Truffle itself; as well, this proposal will outline a method for managing multiple deployments across the lifetime of a project.
Currently, the only recourse for developers who want to perform complex deployments is to run after_deploy scripts. These are scripts that can be run with
truffle exec
, and will be run directly after deployment and whenever a deployment is needed. The benefit ofafter_deploy
scripts is they can run arbitrary code during deployment, allowing for as complex of a deployment as the user needs. The negatives, on the other hand, is thatafter_deploy
scripts are hard to use and that the addresses of deployed contracts within the scripts are not saved.after_deploy
scripts, as well, don't address future deployments, as the initial scripts written would assume contracts related to the dapp don't already exist on the desired network. When a future deployment is needed, when a project's contracts do exist -- which need to be modified or updated as a course of the deployment -- the user would be required to manage those scripts depending on the environment in a way that's tedious and error prone.Solution: Migrations
To address all of the issues mentioned above, let me introduce the concept of Migrations. This idea is heavily influenced by Rail's ActiveRecord migrations, but is much lighter weight.
1461005828324_initial_deploy.js
. Here, the numbered prefix was produced bynew Date().getTime()
, but it can theoretically be any number.done()
callback when finished, outlined below. This structure allows users to execute complex deployment steps without running into the same issues withafter_deploy
scripts mentioned above.truffle init
). If the Migrator contract doesn't exist or is deleted by the user, Truffle will instead run all migrations in order, and will not save the migiration state upon completion.migrations
. Truffle will determine which migrations need to be run based on the network's saved migration state as well as the files within themigrations
directory.deploy
configuration from the project'struffle.js
file. Thetruffle deploy
command will now run the migrations instead of deploying contracts specified within thedeploy
configuration.Discussion
One cause of contention is saving the migration state on the blockchain. Theoretically this state could be saved in a file within the target environment. The benefits of having it on the blockchain is that in my experience, it's easier to manage and the migrator contract could be edited to act differently if the user desired. On the other hand, the largest negative is that deploying and interacting with the Migrator contract would cost real Ether on the public network. I'm open to other's opinions on this (as well as anything else) and would appreciate your thoughts.
Another cause for contention is that as of now, migrations aren't atomic. If a migration fails, there's no way to automatically revert the transactions within the migration that succeeded, as you can with Rails. This likely isn't a feature that can be provided due to the nature of the Ethereum network, but ideas welcome.
Lastly, there is currently no notion of migrating both "up" and "down" as provided in Rails -- currently they only happen in one direction. I would love to hear if this feature would be useful.
Code & Examples
Here are some example migrations as well as proposed solidity code for the migrator contract:
Example Migration: Initial deploy
This will deploy a new contract to the network and save that contract's address.
File name:
1461005828324_initial_deploy.js
Example Migration: Upgrading a Contract
This will expect a currently deployed contract and update a value (Hub & Spoke model).
File name:
1461005828324_update_spoke.js
Proposed Migrator Contract
Effectively pseudo-code. Subject to change.
Feedback
All feedback is welcome. This TIP will remain active until a sufficient consensys is reached. Please leave all comments on github. Thanks!
The text was updated successfully, but these errors were encountered: