-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add identity table smart contract #2
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Just some minor suggestions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Have a few questions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
FlowIdentityTable.nodes[id] == nil: "The ID cannot already exist in the record" | ||
role >= 1 && role <= 4: "The role must be 1, 2, 3, or 4" | ||
networkingAddress.length > 0: "The networkingAddress cannot be empty" // TODO: Require exact length | ||
initialWeight > UInt64(0): "The initial weight must be greater than zero" // TODO: Max weight |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jordanschalm is there a max initialWeight
that we can enforce here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We haven't really defined a maximum value or range for weights, but maybe something that guarantees that sum(node.initialWeight) for node in identityTable
doesn't overflow uint64.
@jordanschalm should we enforce that some of the values for certain fields are not used by multiple nodes? For example, should we put in the code that it should revert if someone tries to create a |
Good point. Yes, we should enforce this for network addresses and public keys. |
I added a new draft to include the current, proposed, and previous node tables in this smart contract and I addressed all the PR comments, including changing some field Types, changing function names, adding comments, and adding pre-conditions. Can I get another review? @turbolent @zhangchiqing @jordanschalm @zhangchiqing I still feel that we shouldn't include the epoch id in this smart contract because that is going to be managed by the Epoch lifecycle contract, which this contract doesn't need to explicitly know about. As you can see now, updating the node table is handled through the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
I added sample transactions, unit tests, a templates package. I also made some changes to store the epoch ID and put the ID tables in one field based on a conversation that @zhangchiqing and I had. Let me know if you have any questions and I can explain. @jordanschalm and @zhangchiqing can you review one more time? Thank you! |
|
||
/// update the entire node table | ||
/// This will be called at the beginning of a new epoch | ||
pub fun startNewEpoch() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to take the newEpochID
as input, so that the FlowIdentityTable can also verify it, and ensure everything is synced.
pub fun startNewEpoch() { | |
pub fun startNewEpoch(Uint64 newEpochID) { | |
pre { | |
newEpochID == FlowIdentityTable.currentEpochID + UInt64(1): "invalid new epoch ID" | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can see, giving the epoch ID as an argument is exactly the same as not doing it, so I don't believe this is necessary. The admin would still be able to call this as many times as they want either way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see Leo's point here. There should be one source of truth for the current epoch counter among the smart contracts. If that source of truth is the EpochManager (sorry, I forgot what you actually called this josh 😅), then that contract should pass the epoch counter along to this one, rather than both determining it independently. Of course adding 1 to a counter isn't so complicated, but I agree it's better that the synchronization between the two is explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the source of truth is in EpochContract
. Adding the newEpochID and the pre check seems unnecessary, but it allows us to catch potential bug earlier and reason about the problem easier. So if there was a unknown bug somewhere, we could be sure that the state transaction of the identity table should be correct, because we've explicitly checked, rather than assuming EpochContract didn't make mistake of what's the current epoch the identity table was cached.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we said that we wanted the identity table contract to be the source of truth for the epoch counter? I was thinking that the the Epoch Contract wouldn't have a field for that and would just get it from this contract
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we said that we wanted the identity table contract to be the source of truth for the epoch counter?
Oh no, that's not what I meant.
EpochContract should own the source of truth for what is the current epoch ID. The identity table has a copy of the current epoch ID. When start a new epoch, the EpochContract and IdentityTable will sync what's the current epoch ID, so that addProposedNode
/removeProposedNode
can trust that the the copy of the current EpochID is consistent with EpochContract's
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also remove the trust completely by adding the epochID to addProposedNode
, and adding a check. That way we are 100% sure that when proposed nodes are updated the epoch ID was consistent across IdentityTable
, Admin
and EpochContract
:
pub fun removeProposedNode(_ proposedEpochID: UInt64, _ nodeID: String): Node? {
pre { proposedEpochID == FlowIdentityTable.getProposedEpochID(): "Invalid proposedEpochID" }
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I added those parameters to all the functions that change the state to make it consistent. 👍
/// Add a new node to the proposed table, or update an existing one | ||
pub fun addProposedNode(_ newNode: Node) { | ||
// Remove the proposed epoch table from the record | ||
let proposedNodeTable = FlowIdentityTable.nodes[FlowIdentityTable.currentEpochID+UInt64(1)]! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we make a method for FlowIdentityTable.currentEpochID+UInt64(1)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What kind of method? I'm not sure what you mean
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Leo means methods for getting current/proposed tables, like getCurrentEpochTable
and getProposedEpochTable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats already in there, below the admin resource. Or am I still misinterpreting?
/// Add a new node to the proposed table, or update an existing one | ||
pub fun addProposedNode(_ newNode: Node) { | ||
// Remove the proposed epoch table from the record | ||
let proposedNodeTable = FlowIdentityTable.nodes[FlowIdentityTable.currentEpochID+UInt64(1)]! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Leo means methods for getting current/proposed tables, like getCurrentEpochTable
and getProposedEpochTable
|
||
/// Add a new node to the proposed table, or update an existing one | ||
pub fun addProposedNode(_ newNode: Node) { | ||
// Remove the proposed epoch table from the record |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mainly just out of curiosity: FlowIdentityTable.nodes
isn't a resource, does the value in the mapping still get "removed"? Or is it copied?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we just used an assignment operator, then it would be copied, but since we use remove
in the remove function, it actually removes it from the dictionary
|
||
/// update the entire node table | ||
/// This will be called at the beginning of a new epoch | ||
pub fun startNewEpoch() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see Leo's point here. There should be one source of truth for the current epoch counter among the smart contracts. If that source of truth is the EpochManager (sorry, I forgot what you actually called this josh 😅), then that contract should pass the epoch counter along to this one, rather than both determining it independently. Of course adding 1 to a counter isn't so complicated, but I agree it's better that the synchronization between the two is explicit.
proposedNodeTable[newNode.id] = newNode | ||
|
||
// Save the proposed epoch table back to the epoch record | ||
FlowIdentityTable.nodes[FlowIdentityTable.currentEpochID+UInt64(1)] = proposedNodeTable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does proposedNodeTable[newNode.id] = newNode
already updated the record in place?
If not, does cadence allow you to do this without making a local copy?
FlowIdentityTable.nodes[FlowIdentityTable.currentEpochID+UInt64(1)]![newNode.id] = newNode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does update the record in place, but doing it without making a local copy isn't possible in Cadence yet because of how we handle optionals. I'll speak to bastian to see if we can add a workaround
|
||
/// The proposed nodes for the next epoch are explicitly not changed | ||
/// because the proposed identity table will stay the same for the next | ||
/// epoch because we assume most nodes will stay in |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes good point. I'll add that.
Ok I've made all the changes that were requested. Hopefully this is the last review we need @zhangchiqing @jordanschalm . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
// pre { | ||
// startingEpochCounter > UInt64(0): "Must set the epoch ID as greater than zero" | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this commented code intentional? If it is, maybe add an additional comment why it is not enabled, or if not just remove it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, I was testing some things out in the init function and forgot to clean it up. Thanks for pointing that out!
// Using this for testing. Need two admins for different contracts | ||
self.account.save(<-create Admin(), to: /storage/flowIdentityTableAdmin2) | ||
|
||
let path: Path = /storage/flowID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unused code
/// Add a new node to the proposed table, or update an existing one | ||
pub fun addProposedNode(epochCounter: UInt64, _ newNode: Node) { | ||
pre { | ||
epochCounter == FlowIdentityTable.currentEpochCounter + UInt64(1): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FlowIdentityTable.currentEpochCounter + UInt64(1)
is used in a lot of places, maybe consider adding a function and using it instead:
fun proposedEpoch(): UInt64 {
return self.currentEpochCounter + UInt64(1)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion! I'll add that
transactions/idTable/create_node.cdc
Outdated
} | ||
|
||
execute { | ||
let newNode = FlowIdentityTable.Node(id: id, role: role, networkingAddress: networkingAddress, networkingKey: networkingKey, stakingKey: stakingKey, initialWeight: initialWeight) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider chopping this down to make it easier to read:
let newNode = FlowIdentityTable.Node(id: id, role: role, networkingAddress: networkingAddress, networkingKey: networkingKey, stakingKey: stakingKey, initialWeight: initialWeight) | |
let newNode = FlowIdentityTable.Node( | |
id: id, | |
role: role, | |
networkingAddress: networkingAddress, | |
networkingKey: networkingKey, | |
stakingKey: stakingKey, | |
initialWeight: initialWeight | |
) |
|
||
/// Initialize the node record to be empty | ||
init() { | ||
self.currentEpochCounter = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about letting EpicContract to specify the currentEpochCounter on init? And we will just check that its >= 1
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Epoch Contract doesn't deploy this contract, we have to deploy it manually, so there is basically no difference between hardcoding it here and making it an argument, right?
"The Epoch counter must be for the proposed Epoch" | ||
} | ||
|
||
let node = self.removeProposedNode(epochCounter: epochCounter, nodeID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since addProposedNode
will update the node, can we read the node instead of removing the node?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we can't do that because the init function of a node checks that it isn't already in use, so we have to remove it, modify, and put it back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean reading the node and updating it will trigger the init
function of it? I'm not clear how it works.
pub resource Admin { | ||
|
||
/// Add a new node to the proposed table, or update an existing one | ||
pub fun addProposedNode(epochCounter: UInt64, _ newNode: Node) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it allows both insert and update, consider saveProposedNode
or upsertProposedNode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It actually doesn't allow updating a node any more, so I'll keep it as add
, and I removed that part of the comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
Curious how does it prevent from updating a node? Do we need a check or a test case for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The init
function of the Node
struct ensures that the node ID doesn't already exist in a pre-condition on line 57, so it would prevent it before it is even created. And I have a test case for it already. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me know. Nice work! I left some optional suggestions
FlowIdentityTable.nodes[FlowIdentityTable.proposedEpochCounter()] = FlowIdentityTable.nodes[FlowIdentityTable.currentEpochCounter]! | ||
|
||
// Erase the records of the epoch before the previous epoch | ||
FlowIdentityTable.nodes[FlowIdentityTable.currentEpochCounter-UInt64(2)] = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a delete
key word for map?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there is. @turbolent ?
pub resource Admin { | ||
|
||
/// Add a new node to the proposed table, or update an existing one | ||
pub fun addProposedNode(epochCounter: UInt64, _ newNode: Node) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
Curious how does it prevent from updating a node? Do we need a check or a test case for that?
|
||
/// Update the initial weight of one of the proposed nodes | ||
/// This will be called at the end of the staking auction when all | ||
/// of the nodes stakes have been finally committed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it will only be called once at the end of the staking auction, would it make sense to allow multiple updates?
pub fun updateInitialWeight(epochCounter: UInt64, newWeights: { nodeID: UInt64 }) {
I even think we could merge updateInitWeight
and startNewEpoch
to be:
commitAndStartNewEpoch(newWeights: { nodeID: UInt64 }, newEpochCounter: UInt64)
So that
- Less calls to the Identity Contract
- Less
pre
checks on epochCounter - Not possible to
updateInitialWeight
for the same node for multiple times (currently allows)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be possible, but we don't update the initial weight when the staking auction ends, so it wouldn't go in the start new epoch funtion, we'd have to make a new function. I think it is fine to keep it the way it is though. I'm not concerned about it.
"The Epoch counter must be for the proposed Epoch" | ||
} | ||
|
||
let node = self.removeProposedNode(epochCounter: epochCounter, nodeID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean reading the node and updating it will trigger the init
function of it? I'm not clear how it works.
Adding the first draft of the Flow Identity table smart contract for epochs. It contains the minimum functionality for managing the Flow identity table:
I'll start writing tests for it once I get sign-off from the various stakeholders.
Closes https://github.com/dapperlabs/flow-go/issues/4531