-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
1,347 additions
and
179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
with-expecter: True | ||
testonly: True | ||
dir: pkg/mocks | ||
mockname: "Mock{{.InterfaceName}}" | ||
outpkg: mocks | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,91 +1,176 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.13; | ||
|
||
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
|
||
contract Nodes is Ownable { | ||
constructor() Ownable(msg.sender) {} | ||
/** | ||
A NFT contract for XMTP Node Operators. | ||
The deployer of this contract is responsible for minting NFTs and assinging them to node operators. | ||
All nodes on the network periodically check this contract to determine which nodes they should connect to. | ||
*/ | ||
contract Nodes is ERC721, Ownable { | ||
constructor() ERC721("XMTP Node Operator", "XMTP") Ownable(msg.sender) {} | ||
|
||
// uint16 counter so that we cannot create more than 65k IDs | ||
// The ERC721 standard expects the tokenID to be uint256 for standard methods unfortunately | ||
uint16 private _nodeIdCounter; | ||
|
||
// A node, as stored in the internal mapping | ||
struct Node { | ||
bytes signingKeyPub; | ||
string httpAddress; | ||
uint256 originatorId; | ||
bytes mtlsCert; | ||
bool isHealthy; | ||
// Maybe we want a TLS cert separate from the public key for MTLS authenticated connections? | ||
} | ||
|
||
event NodeUpdate( | ||
bytes publicKey, | ||
string httpAddress, | ||
uint256 originatorId, | ||
bool isHealthy | ||
); | ||
struct NodeWithId { | ||
uint16 nodeId; | ||
Node node; | ||
} | ||
|
||
// List of public keys | ||
bytes[] publicKeys; | ||
event NodeUpdated(uint256 nodeId, Node node); | ||
|
||
// Mapping of publicKey to node | ||
mapping(bytes => Node) public nodes; | ||
// Mapping of token ID to Node | ||
mapping(uint256 => Node) private _nodes; | ||
|
||
/** | ||
Add a node to the network | ||
Mint a new node NFT and store the metadata in the smart contract | ||
*/ | ||
function addNode( | ||
bytes calldata publicKey, | ||
address to, | ||
bytes calldata signingKeyPub, | ||
string calldata httpAddress, | ||
bytes calldata mtlsCert | ||
) public onlyOwner returns (uint16) { | ||
uint16 nodeId = _nodeIdCounter; | ||
_mint(to, nodeId); | ||
_nodes[nodeId] = Node(signingKeyPub, httpAddress, mtlsCert, true); | ||
_emitNodeUpdate(nodeId); | ||
_nodeIdCounter++; | ||
|
||
return nodeId; | ||
} | ||
|
||
/** | ||
Override the built in transferFrom function to block NFT owners from transferring | ||
node ownership. | ||
NFT owners are only allowed to update their HTTP address and MTLS cert. | ||
*/ | ||
function transferFrom( | ||
address from, | ||
address to, | ||
uint256 tokenId | ||
) public override { | ||
require( | ||
_msgSender() == owner(), | ||
"Only the contract owner can transfer Node ownership" | ||
); | ||
super.transferFrom(from, to, tokenId); | ||
} | ||
|
||
/** | ||
Allow a NFT holder to update the HTTP address of their node | ||
*/ | ||
function updateHttpAddress( | ||
uint256 tokenId, | ||
string calldata httpAddress | ||
) public onlyOwner { | ||
) public { | ||
require( | ||
bytes(nodes[publicKey].httpAddress).length == 0, | ||
"Node already exists" | ||
_msgSender() == ownerOf(tokenId), | ||
"Only the owner of the Node NFT can update its http address" | ||
); | ||
_nodes[tokenId].httpAddress = httpAddress; | ||
_emitNodeUpdate(tokenId); | ||
} | ||
|
||
require(bytes(httpAddress).length != 0, "HTTP address is required"); | ||
/** | ||
Allow a NFT holder to update their own MTLS cert | ||
*/ | ||
function updateMtlsCert(uint256 tokenId, bytes calldata mtlsCert) public { | ||
require( | ||
_msgSender() == ownerOf(tokenId), | ||
"Only the owner of the Node NFT can update its mtls cert" | ||
); | ||
_nodes[tokenId].mtlsCert = mtlsCert; | ||
_emitNodeUpdate(tokenId); | ||
} | ||
|
||
/** | ||
The contract owner may update the health status of the node. | ||
No one else is allowed to call this function. | ||
*/ | ||
function updateHealth(uint256 tokenId, bool isHealthy) public onlyOwner { | ||
// Make sure that the token exists | ||
_requireOwned(tokenId); | ||
_nodes[tokenId].isHealthy = isHealthy; | ||
_emitNodeUpdate(tokenId); | ||
} | ||
|
||
/** | ||
Get a list of healthy nodes with their ID and metadata | ||
*/ | ||
function healthyNodes() public view returns (NodeWithId[] memory) { | ||
uint16 totalNodeCount = _nodeIdCounter; | ||
uint256 healthyCount = 0; | ||
|
||
// First, count the number of healthy nodes | ||
for (uint256 i = 0; i < totalNodeCount; i++) { | ||
if (_nodeExists(i) && _nodes[i].isHealthy) { | ||
healthyCount++; | ||
} | ||
} | ||
|
||
nodes[publicKey] = Node({ | ||
httpAddress: httpAddress, | ||
originatorId: publicKeys.length + 1, | ||
isHealthy: true | ||
}); | ||
// Create an array to store healthy nodes | ||
NodeWithId[] memory healthyNodesList = new NodeWithId[](healthyCount); | ||
uint256 currentIndex = 0; | ||
|
||
publicKeys.push(publicKey); | ||
// Populate the array with healthy nodes | ||
for (uint16 i = 0; i < totalNodeCount; i++) { | ||
if (_nodeExists(i) && _nodes[i].isHealthy) { | ||
healthyNodesList[currentIndex] = NodeWithId({ | ||
nodeId: i, | ||
node: _nodes[i] | ||
}); | ||
currentIndex++; | ||
} | ||
} | ||
|
||
emit NodeUpdate(publicKey, httpAddress, publicKeys.length, true); | ||
return healthyNodesList; | ||
} | ||
|
||
/** | ||
The contract owner can use this function to mark a node as unhealthy | ||
triggering all other nodes to stop replicating to/from this node | ||
Get all nodes regardless of their health status | ||
*/ | ||
function markNodeUnhealthy(bytes calldata publicKey) public onlyOwner { | ||
require( | ||
bytes(nodes[publicKey].httpAddress).length != 0, | ||
"Node does not exist" | ||
); | ||
nodes[publicKey].isHealthy = false; | ||
function allNodes() public view returns (NodeWithId[] memory) { | ||
uint16 totalNodeCount = _nodeIdCounter; | ||
NodeWithId[] memory allNodesList = new NodeWithId[](totalNodeCount); | ||
|
||
emit NodeUpdate( | ||
publicKey, | ||
nodes[publicKey].httpAddress, | ||
nodes[publicKey].originatorId, | ||
false | ||
); | ||
for (uint16 i = 0; i < totalNodeCount; i++) { | ||
allNodesList[i] = NodeWithId({nodeId: i, node: _nodes[i]}); | ||
} | ||
|
||
return allNodesList; | ||
} | ||
|
||
/** | ||
The contract owner can use this function to mark a node as healthy | ||
triggering all other nodes to | ||
Get a node's metadata by ID | ||
*/ | ||
function markNodeHealthy(bytes calldata publicKey) public onlyOwner { | ||
require( | ||
bytes(nodes[publicKey].httpAddress).length != 0, | ||
"Node does not exist" | ||
); | ||
nodes[publicKey].isHealthy = true; | ||
function getNode(uint256 tokenId) public view returns (Node memory) { | ||
_requireOwned(tokenId); | ||
return _nodes[tokenId]; | ||
} | ||
|
||
emit NodeUpdate( | ||
publicKey, | ||
nodes[publicKey].httpAddress, | ||
nodes[publicKey].originatorId, | ||
true | ||
); | ||
function _emitNodeUpdate(uint256 tokenId) private { | ||
emit NodeUpdated(tokenId, _nodes[tokenId]); | ||
} | ||
|
||
function _nodeExists(uint256 tokenId) private view returns (bool) { | ||
address owner = _ownerOf(tokenId); | ||
return owner != address(0); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.