Contract Upgrades #1642
Replies: 2 comments 5 replies
-
Decoupling code and accountsIf we decouple execution-logic, auth-logic, execution-state, and auth-state, upgrades can be a lot easier to reason about, thus making both audit and authoring easier. Address
Account
Auth-Function
There should be a default auth-function (logic and state) provided if the user does not provide their own function. We can actually start by having a limited set of auth-functions and extend them further in the future. Here's one for ed25519 signature verification, for example use wasmlanche::{borsh::BorshDeserialize, Context};
use ed25519_dalek::{Signature, VerifyingKey, Verifier};
extern "C" fn is_actor_authorized(ctx: &[u8], _state: &[u8]) -> bool {
let Ok(ctx) = Context::try_from_slice(ctx) else {
return false;
};
let (Ok(key), Ok(signature)) = (VerifyingKey::try_from(ctx.actor()), Signature::try_from(ctx.signature())) else {
return false;
};
match key.verify(ctx.msg(), &signature) {
Ok(_) => true,
Err(_) => false,
}
} Here's another for code-ids allow-list use wasmlanche::{borsh::BorshDeserialize, Context, CodeId};
extern "C" fn is_code_authorized(ctx: &[u8], state: &[u8]) -> bool {
let Ok(ctx) = Context::try_from_slice(ctx) else {
return false;
};
let Ok(allow_list) = <[CodeId; 5]>::try_from_slice(state) else {
return false;
};
ctx.code_id().map(|code_id| allow_list.contains(code_id)).unwrap_or(false)
} Future ExtensionMore properties can be added to an account to extend its functionality in the future Actions
Code should be callable by Address OR Account-ID from contracts or from transactionsInstead of having a Further research is required to understand the repercussions of enabling this behaviour. Additional LayerIn the future, I think it would make sense to layer a protocol-level name-service on top of addresses that has the following: Name Service
|
Beta Was this translation helpful? Give feedback.
-
My initial thoughts were outlined in #1578 and I'll repost here. My design looks at contract upgrades from a contract <-> host function level rather than at the action level. This means, we don't need any additional actions to support this design(not saying this is bad or good). Currently, a contract's state space is prefixed by its address. flowchart TD
State(Global State)
ContractA("Contract A Address")
ContractB("Contract B Address")
ContractC("Contract C Address")
StateA("Contract A State Space")
StateB("Contract B State Space")
StateC("Contract C State Space")
State --> ContractA
State --> ContractB
State --> ContractC
ContractA --> StateA
ContractB --> StateB
ContractC --> StateC
style State stroke:#f9f,stroke-width:4px
style ContractA stroke:#bbf,stroke-width:2px
style ContractB stroke:#bbf,stroke-width:2px
style ContractC stroke:#bbf,stroke-width:2px
style StateA stroke:#bfb,stroke-width:2px
style StateB stroke:#bfb,stroke-width:2px
style StateC stroke:#bfb,stroke-width:2px
The state space of a contract should be decoupled with its code. This way, contracts would have much greater flexibility in interacting with state, and offer a very clean and explicit interface for managing state access and ownership. StateSpace would become a property of contracts, with their own read/write/own permissions. Initially every contract would be instantiated with it's own state space, but would be able to move access and control as it pleases. flowchart TD
State(State)
Space1("State Space 0x1a2b")
Space2("State Space 0x3c4d")
Space3("State Space 0x5e6f")
State --> Space1
State --> Space2
State --> Space3
style State stroke:#f9f,stroke-width:4px
style Space1 stroke:#bbf,stroke-width:2px
style Space2 stroke:#bbf,stroke-width:2px
style Space3 stroke:#bbf,stroke-width:2px
classDiagram
%% Defining the classes
class ContractA {
StateSpace: 0x1a2b
}
class ContractB {
StateSpace: 0x3c4d
}
class ContractC {
StateSpace: 0x5e6f
}
class ContractD {
StateSpace: nil
}
class StateSpace {
owner: Address
writers: map[Address]bool
statePrefix: []byte
}
This design gives contracts much more flexibility regarding state access as contracts can share state and move ownership. A A lightweight implementation of this would expose one additional host function extern fn set_state_access(access: AccessControl)
enum AccessControl {
// gives `contract` write access to this contracts StateSpace
// calling contract must have ownership of its StateSpace
Write(Contract)
// moves ownership to `contract`,
// calling contract must have ownership,
Own(Contract)
// removes all access `contract` has to calling contract
Remove(Contract)
// delegates access control to `contract`
Delegate(Contract)
} Upgrading contracts are quite easy. Contract A
pub fn upgrade(new: Address) {
// validate caller & other logic...
// change owner
set_state_access(AccessControl::own(address))
}
Contract B
pub fn init(contractA: Address) {
// Allow contractA to move ownership to ContractB
set_state_access(AccessControl::delegate(contractA))
} In this example, |
Beta Was this translation helpful? Give feedback.
-
As is apparent on multiple blockchains and blockchain-frameworks, there is demand for upgradeable contracts.
What is an upgradeable contract?
Nothing precludes a smart-contract author from deploying a new contract and calling the new contract version-(n + 1), but what happens when another contract has hardcoded its address? That other contract is calling old code and needs to also release version-(n + 1). It's untenable to have to release a new contract every time a "parent" contract is updated (dependent-contract calls parent-contract).
Existing Solutions
Since the problem seems to relate to the calling code and the address of the contract, it's not surprising that the general community has centered around a type of proxy pattern. There are two types of proxies:
It seems that with the benefit of hindsight, the community of blockchain builders seems to be converging on building the proxy into protocol itself.
Proposals to follow...
Assumptions
Nice to haves
It would be nice to have an explicit way to deal with upgrades as a caller
Beta Was this translation helpful? Give feedback.
All reactions