Skip to content
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

Merged
merged 12 commits into from
Aug 14, 2020
Merged

Add identity table smart contract #2

merged 12 commits into from
Aug 14, 2020

Conversation

joshuahannan
Copy link
Member

@joshuahannan joshuahannan commented Aug 3, 2020

Adding the first draft of the Flow Identity table smart contract for epochs. It contains the minimum functionality for managing the Flow identity table:

  • Struct definition for a node that holds its information.
    • are the field names and types correct?
  • Dictionary for holding all nodes' information, indexed by ID.
    • Is this the best way to store and index the info?
  • Restriction for node initialization.
    • need help with some of the pre-conditions
    • Should we try to enforce that some of the values for certain fields are not used by multiple nodes?
  • Admin resource for updating the table.
  • Public functions to read info about the nodes. (Do we want all of this info to be publicly accessible?)

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

Copy link
Member

@turbolent turbolent left a 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

contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
Copy link
Member

@zhangchiqing zhangchiqing left a 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.

contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
Copy link
Member

@jordanschalm jordanschalm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
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
Copy link
Member Author

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?

Copy link
Member

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.

@joshuahannan
Copy link
Member Author

@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 Node struct with the a networking Address or keys that already exist in the table? Or are are those allowed to be shared?

@jordanschalm
Copy link
Member

should we enforce that some of the values for certain fields are not used by multiple nodes?

Good point. Yes, we should enforce this for network addresses and public keys.

@joshuahannan
Copy link
Member Author

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 updateCurrentNodeTable function, which just sets the current node table as the value of proposed node table. The epoch lifecycle contract will be the one to enforce that this only happens when the epoch boundary and ID change happens. Let me know if you think this is still a problem and we can discuss more.

Copy link
Member

@jordanschalm jordanschalm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

@joshuahannan
Copy link
Member Author

joshuahannan commented Aug 11, 2020

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!

contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved

/// update the entire node table
/// This will be called at the beginning of a new epoch
pub fun startNewEpoch() {
Copy link
Member

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.

Suggested change
pub fun startNewEpoch() {
pub fun startNewEpoch(Uint64 newEpochID) {
pre {
newEpochID == FlowIdentityTable.currentEpochID + UInt64(1): "invalid new epoch ID"
}
}

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

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

Copy link
Member

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" }
  ...
}

Copy link
Member Author

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. 👍

contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
contracts/epochs/FlowIdentityTable.cdc Show resolved Hide resolved
/// 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)]!
Copy link
Member

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)?

Copy link
Member Author

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

Copy link
Member

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

Copy link
Member Author

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?

contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved
/// 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)]!
Copy link
Member

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
Copy link
Member

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?

Copy link
Member Author

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

contracts/epochs/FlowIdentityTable.cdc Outdated Show resolved Hide resolved

/// update the entire node table
/// This will be called at the beginning of a new epoch
pub fun startNewEpoch() {
Copy link
Member

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
Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member Author

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.

@joshuahannan
Copy link
Member Author

Ok I've made all the changes that were requested. Hopefully this is the last review we need @zhangchiqing @jordanschalm .

@joshuahannan joshuahannan changed the title DRAFT: Add identity table smart contract Add identity table smart contract Aug 13, 2020
Copy link
Member

@turbolent turbolent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

Comment on lines 201 to 203
// pre {
// startingEpochCounter > UInt64(0): "Must set the epoch ID as greater than zero"
// }
Copy link
Member

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

Copy link
Member Author

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
Copy link
Member

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):
Copy link
Member

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)
}

Copy link
Member Author

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

}

execute {
let newNode = FlowIdentityTable.Node(id: id, role: role, networkingAddress: networkingAddress, networkingKey: networkingKey, stakingKey: stakingKey, initialWeight: initialWeight)
Copy link
Member

@turbolent turbolent Aug 13, 2020

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:

Suggested change
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
Copy link
Member

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.

Copy link
Member Author

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)
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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) {
Copy link
Member

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

Copy link
Member Author

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

Copy link
Member

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?

Copy link
Member Author

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. 👍

Copy link
Member

@zhangchiqing zhangchiqing left a 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)] = {}
Copy link
Member

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?

Copy link
Member Author

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) {
Copy link
Member

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
Copy link
Member

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

  1. Less calls to the Identity Contract
  2. Less pre checks on epochCounter
  3. Not possible to updateInitialWeight for the same node for multiple times (currently allows)

Copy link
Member Author

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)
Copy link
Member

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.

@joshuahannan joshuahannan merged commit 09c4cc2 into master Aug 14, 2020
@joshuahannan joshuahannan deleted the josh/identities branch September 16, 2020 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants