部署表决合约并与之互动

Starknet的 Vote 合约首先要通过合约的构造函数注册投票人。在此阶段,三个投票人被初始化,他们的地址被传递给内部函数 _register_voters。该函数将选民添加到合约的状态中,标记为已注册并有资格投票。

在合约中,常量 YESNO 被定义为表决选项(分别为 1 和 0)。这些常量使输入值标准化,从而方便了投票过程。

注册完成后,投票者可使用 vote 函数进行投票,选择 1(YES)或 0(NO)作为其投票。投票时,合约状态会被更新,记录投票情况并标记投票人已投票。这样可以确保投票者无法在同一提案中再次投票。投票会触发 VoteCast 事件,记录投票行为。

该合约还会监控未经授权的投票尝试。如果检测到未经授权的行为,如非注册用户试图投票或用户试图再次投票,就会发出 UnauthorizedAttempt 事件。

这些功能、状态、常量和事件共同创建了一个结构化投票系统,在Starknet环境中管理着从注册到投票、事件记录、以及结果检索的投票生命周期。常量(如 YESNO )有助于简化投票流程,而事件则在确保透明度和可追溯性方面发挥着重要作用。

/// @dev Core Library Imports for the Traits outside the Starknet Contract
use starknet::ContractAddress;

/// @dev Trait defining the functions that can be implemented or called by the Starknet Contract
#[starknet::interface]
trait VoteTrait<T> {
    /// @dev Function that returns the current vote status
    fn get_vote_status(self: @T) -> (u8, u8, u8, u8);
    /// @dev Function that checks if the user at the specified address is allowed to vote
    fn voter_can_vote(self: @T, user_address: ContractAddress) -> bool;
    /// @dev Function that checks if the specified address is registered as a voter
    fn is_voter_registered(self: @T, address: ContractAddress) -> bool;
    /// @dev Function that allows a user to vote
    fn vote(ref self: T, vote: u8);
}

/// @dev Starknet Contract allowing three registered voters to vote on a proposal
#[starknet::contract]
mod Vote {
    use starknet::ContractAddress;
    use starknet::get_caller_address;

    const YES: u8 = 1_u8;
    const NO: u8 = 0_u8;

    /// @dev Structure that stores vote counts and voter states
    #[storage]
    struct Storage {
        yes_votes: u8,
        no_votes: u8,
        can_vote: LegacyMap::<ContractAddress, bool>,
        registered_voter: LegacyMap::<ContractAddress, bool>,
    }

    /// @dev Contract constructor initializing the contract with a list of registered voters and 0 vote count
    #[constructor]
    fn constructor(
        ref self: ContractState,
        voter_1: ContractAddress,
        voter_2: ContractAddress,
        voter_3: ContractAddress
    ) {
        // Register all voters by calling the _register_voters function
        self._register_voters(voter_1, voter_2, voter_3);

        // Initialize the vote count to 0
        self.yes_votes.write(0_u8);
        self.no_votes.write(0_u8);
    }

    /// @dev Event that gets emitted when a vote is cast
    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        VoteCast: VoteCast,
        UnauthorizedAttempt: UnauthorizedAttempt,
    }

    /// @dev Represents a vote that was cast
    #[derive(Drop, starknet::Event)]
    struct VoteCast {
        voter: ContractAddress,
        vote: u8,
    }

    /// @dev Represents an unauthorized attempt to vote
    #[derive(Drop, starknet::Event)]
    struct UnauthorizedAttempt {
        unauthorized_address: ContractAddress,
    }

    /// @dev Implementation of VoteTrait for ContractState
    #[external(v0)]
    impl VoteImpl of super::VoteTrait<ContractState> {
        /// @dev Returns the voting results
        fn get_vote_status(self: @ContractState) -> (u8, u8, u8, u8) {
            let (n_yes, n_no) = self._get_voting_result();
            let (yes_percentage, no_percentage) = self._get_voting_result_in_percentage();
            (n_yes, n_no, yes_percentage, no_percentage)
        }

        /// @dev Check whether a voter is allowed to vote
        fn voter_can_vote(self: @ContractState, user_address: ContractAddress) -> bool {
            self.can_vote.read(user_address)
        }

        /// @dev Check whether an address is registered as a voter
        fn is_voter_registered(self: @ContractState, address: ContractAddress) -> bool {
            self.registered_voter.read(address)
        }

        /// @dev Submit a vote
        fn vote(ref self: ContractState, vote: u8) {
            assert(vote == NO || vote == YES, 'VOTE_0_OR_1');
            let caller: ContractAddress = get_caller_address();
            self._assert_allowed(caller);
            self.can_vote.write(caller, false);

            if (vote == NO) {
                self.no_votes.write(self.no_votes.read() + 1_u8);
            }
            if (vote == YES) {
                self.yes_votes.write(self.yes_votes.read() + 1_u8);
            }

            self.emit(VoteCast { voter: caller, vote: vote, });
        }
    }

    /// @dev Internal Functions implementation for the Vote contract
    #[generate_trait]
    impl InternalFunctions of InternalFunctionsTrait {
        /// @dev Registers the voters and initializes their voting status to true (can vote)
        fn _register_voters(
            ref self: ContractState,
            voter_1: ContractAddress,
            voter_2: ContractAddress,
            voter_3: ContractAddress
        ) {
            self.registered_voter.write(voter_1, true);
            self.can_vote.write(voter_1, true);

            self.registered_voter.write(voter_2, true);
            self.can_vote.write(voter_2, true);

            self.registered_voter.write(voter_3, true);
            self.can_vote.write(voter_3, true);
        }
    }

    /// @dev Asserts implementation for the Vote contract
    #[generate_trait]
    impl AssertsImpl of AssertsTrait {
        // @dev Internal function that checks if an address is allowed to vote
        fn _assert_allowed(ref self: ContractState, address: ContractAddress) {
            let is_voter: bool = self.registered_voter.read((address));
            let can_vote: bool = self.can_vote.read((address));

            if (can_vote == false) {
                self.emit(UnauthorizedAttempt { unauthorized_address: address, });
            }

            assert(is_voter == true, 'USER_NOT_REGISTERED');
            assert(can_vote == true, 'USER_ALREADY_VOTED');
        }
    }

    /// @dev Implement the VotingResultTrait for the Vote contract
    #[generate_trait]
    impl VoteResultFunctionsImpl of VoteResultFunctionsTrait {
        // @dev Internal function to get the voting results (yes and no vote counts)
        fn _get_voting_result(self: @ContractState) -> (u8, u8) {
            let n_yes: u8 = self.yes_votes.read();
            let n_no: u8 = self.no_votes.read();

            (n_yes, n_no)
        }

        // @dev Internal function to calculate the voting results in percentage
        fn _get_voting_result_in_percentage(self: @ContractState) -> (u8, u8) {
            let n_yes: u8 = self.yes_votes.read();
            let n_no: u8 = self.no_votes.read();

            let total_votes: u8 = n_yes + n_no;

            if (total_votes == 0_u8) {
                return (0, 0);
            }
            let yes_percentage: u8 = (n_yes * 100_u8) / (total_votes);
            let no_percentage: u8 = (n_no * 100_u8) / (total_votes);

            (yes_percentage, no_percentage)
        }
    }
}

投票智能合约

部署、调用和唤起投票合约

Starknet 体验的一部分就是部署智能合约并与之交互。

一旦部署了合约,我们就可以通过调用合约的函数与之交互:

  • 调用合约:与只读取状态的外部函数交互。这些函数不会改变网络的状态,因此不需要付费或签署。
  • 唤起合约:与可以写入状态的外部函数交互。这些函数会改变网络状态,因此需要付费或签署。

我们将使用 katana 设置一个本地开发节点来部署投票合约。然后,我们将通过调用和调用其函数与合约进行交互。你也可以使用Goerli测试网络而不是 katana。然而,我们建议在本地开发和测试中使用 katana。你可以在Starknet Book的 Local Development with Katana 章节中找到有关 katana 的完整教程。

katana 本地Starknet节点

katana旨在支持Dojo 团队的本地开发。通过它,您可以在本地完成Starknet所需的一切工作。它是开发和测试的绝佳工具。

要从源代码安装 katana,请参考Starknet Book的 Local Development with Katana 章节。

一旦安装了 katana,就可以用以下命令启动本地Starknet节点:

katana --accounts 3 --seed 0 --gas-price 250

该命令将启动一个本地 Starknet 节点,并部署 3 个账户。我们将使用这些账户部署投票合约并与之交互:

...
PREFUNDED ACCOUNTS
==================

| Account address |  0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0
| Private key     |  0x0300001800000000300000180000000000030000000000003006001800006600
| Public key      |  0x01b7b37a580d91bc3ad4f9933ed61f3a395e0e51c9dd5553323b8ca3942bb44e

| Account address |  0x033c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c
| Private key     |  0x0333803103001800039980190300d206608b0070db0012135bd1fb5f6282170b
| Public key      |  0x04486e2308ef3513531042acb8ead377b887af16bd4cdd8149812dfef1ba924d

| Account address |  0x01d98d835e43b032254ffbef0f150c5606fa9c5c9310b1fae370ab956a7919f5
| Private key     |  0x07ca856005bee0329def368d34a6711b2d95b09ef9740ebf2c7c7e3b16c1ca9c
| Public key      |  0x07006c42b1cfc8bd45710646a0bb3534b182e83c313c7bc88ecf33b53ba4bcbc
...

在我们与投票合约进行交互之前,我们需要在Starknet上准备选民和管理员账户。每个选民账户必须进行注册,并具备足够的资金进行投票。如果你想更详细地了解账户如何与账户抽象一起操作,请参考Starknet Book的 账户抽象 章节。

用于投票的智能钱包

除了Scarb之外,你还需要安装Starkli。Starkli是一个命令行工具,允许你与Starknet进行交互。你可以在Starknet Book的 环境初始化 章节中找到安装说明。

对于我们将使用的每个智能钱包,我们必须在加密的密钥库中创建一个签名者和一个账户描述符。这个过程也在Starknet Book的环境初始化 章节中详细介绍了。

我们可以为要用于投票的账户创建签名者和账户描述符。让我们在智能合约中创建一个用于投票的智能钱包。

首先,我们用私钥创建一个签名者:

starkli signer keystore from-key ~/.starkli-wallets/deployer/account0_keystore.json

然后,我们通过获取我们要使用的katana账户来创建账户描述符:

starkli account fetch <KATANA ACCOUNT ADDRESS> --rpc http://0.0.0.0:5050 --output ~/.starkli-wallets/deployer/account0_account.json

这个命令将创建一个新的 account0_account.json 文件,其中包含以下细节:

{
  "version": 1,
  "variant": {
        "type": "open_zeppelin",
        "version": 1,
        "public_key": "<SMART_WALLET_PUBLIC_KEY>"
  },
    "deployment": {
        "status": "deployed",
        "class_hash": "<SMART_WALLET_CLASS_HASH>",
        "address": "<SMART_WALLET_ADDRESS>"
  }
}

你可以用以下命令获取智能钱包的 class hash(所有智能钱包的class hash都一样)。注意使用了 --rpc 标志和 katana 提供的 RPC 端点:

starkli class-hash-at <SMART_WALLET_ADDRESS> --rpc http://0.0.0.0:5050

要获取公钥,可以使用 starkli signer keystore inspect 命令,并输入 keystore json 文件的目录:

starkli signer keystore inspect ~/.starkli-wallets/deployer/account0_keystore.json

如果您想拥有第二个和第三个投票人,用同样的过程对 account_1account_2 进行操作即可。

合约部署

在部署之前,我们需要声明合约。我们可以使用 starkli declare 命令来完成这项工作:

starkli declare target/dev/starknetbook_chapter_2_Vote.sierra.json --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json

如果你使用的编译器版本旧于Starkli使用的版本,并且在使用上述命令时遇到了 compiler-version 错误,你可以通过在命令中添加 --compiler-version x.y.z 标志来指定要使用的编译器版本。

如果你仍然遇到编译器版本的问题,请尝试使用以下命令来升级 Starkli:starkliup,以确保你正在使用 starkli 的最新版本。

合约的的class hash是0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52.您可以在 [任何区块浏览器] (https://goerli.voyager.online/class/0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52)中看到他。

--rpc标志指定要使用的 RPC 端点(由 katana提供)。--account 标志指定用于签署交易的账户。这里使用的账户是上一步创建的账户。 --keystore标记用于指定签署交易的密钥存储文件。

由于我们使用的是本地节点,因此交易将立即完成。如果您使用的是 Goerli Testnet,则需要等待交易最终完成,这通常需要几秒钟。

以下命令将部署投票合约,并将 voter_0、voter_1 和 voter_2 注册为合格投票人。这些是构造函数参数,因此请添加一个以后可以用来投票的选民账户。

starkli deploy <class_hash_of_the_contract_to_be_deployed> <voter_0_address> <voter_1_address> <voter_2_address> --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json

命令示例:

starkli deploy 0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 0x033c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c 0x01d98d835e43b032254ffbef0f150c5606fa9c5c9310b1fae370ab956a7919f5 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json

在本例中,合约已部署到特定地址:0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349。您应该会看到不同的地址。我们将使用此地址与合约进行交互。

投票人资格验证

在我们的投票合约中,我们有两个函数来验证投票人的资格,即 voter_can_voteis_voter_registered。这些函数是外部只读函数,这意味着它们不会改变合约的状态,而只是读取当前状态。

is_voter_registered函数检查特定地址是否在合约中登记为合格投票人。另一方面,voter_can_vote函数会检查特定地址的投票人当前是否有资格投票,即他们是否已登记且尚未投票。

你可以使用 starkli call 命令来调用这些函数。请注意,call命令用于只读函数,而 invoke命令用于也可以写入存储空间的函数。调用 call 命令不需要签名,而 invoke命令需要签名。

starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 voter_can_vote 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 --rpc http://0.0.0.0:5050

首先,我们添加了合约的地址,然后是要调用的函数,最后是函数的输入。在本例中,我们要检查地址为 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 的投票人是否可以投票。

由于我们提供了已登记的投票人的地址作为输入,因此结果为 1(布尔值为 true),表明该选民有资格投票。

接下来,让我们使用一个未注册的账户地址调用 is_voter_registered 函数来观察输出结果:

starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 is_voter_registered 0x44444444444444444 --rpc http://0.0.0.0:5050

对于未注册的账户地址,终端输出为 0(即假),确认该账户没有投票资格。

投票

既然我们已经确定了如何验证选民资格,那么我们就可以投票了!投票时,我们要与vote函数交互,该函数被标记为外部函数,因此必须使用 starknet invoke命令。

invoke命令的语法与 call 命令类似,但在投票时,我们需要输入 "1"(表示 "是")或 "0"(表示 "否")。当我们唤起 vote 函数时,我们会被收取一定的费用,而且交易必须由投票人签署;我们正在向合约的存储空间写入内容。

//Voting Yes
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 1 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json

//Voting No
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 0 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json

系统将提示您输入签名者的密码。输入密码后,交易将被签署并提交到Starknet网络。你将收到交易哈希值作为输出。使用 starkli 交易命令,你可以获得更多关于交易的详细信息:

starkli transaction <TRANSACTION_HASH> --rpc http://0.0.0.0:5050

这个会返回:

{
  "transaction_hash": "0x5604a97922b6811060e70ed0b40959ea9e20c726220b526ec690de8923907fd",
  "max_fee": "0x430e81",
  "version": "0x1",
  "signature": [
    "0x75e5e4880d7a8301b35ff4a1ed1e3d72fffefa64bb6c306c314496e6e402d57",
    "0xbb6c459b395a535dcd00d8ab13d7ed71273da4a8e9c1f4afe9b9f4254a6f51"
  ],
  "nonce": "0x3",
  "type": "INVOKE",
  "sender_address": "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0",
  "calldata": [
    "0x1",
    "0x5ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349",
    "0x132bdf85fc8aa10ac3c22f02317f8f53d4b4f52235ed1eabb3a4cbbe08b5c41",
    "0x0",
    "0x1",
    "0x1",
    "0x1"
  ]
}

如果您尝试用同一个签名者投票两次,则会出现错误:

Error: code=ContractError, message="Contract error"

错误信息很简略,但你可以启动 katana(我们的本地Starknet节点)的终端中查看输出,获得更多细节:

...
Transaction execution error: "Error in the called contract (0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0):
    Error at pc=0:81:
    Got an exception while executing a hint: Custom Hint Error: Execution failed. Failure reason: \"USER_ALREADY_VOTED\".
    ...

错误的关键字是 USER_ALREADY_VOTED

assert(can_vote == true, 'USER_ALREADY_VOTED');

我们可以重复上述过程,为要将用于投票的账户创建签名者和账户描述符。请记住,每个签名者都必须用私钥创建,每个账户描述符都必须用公钥、智能钱包地址和智能钱包class hash (每个投票者的class hash 都一样)创建。

starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 0 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account1_account.json --keystore ~/.starkli-wallets/deployer/account1_keystore.json

starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 1 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account2_account.json --keystore ~/.starkli-wallets/deployer/account2_keystore.json

投票结果可视化

为了检查投票结果,我们通过 starknet call命令调用另一个视图函数 get_vote_status

starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 get_vote_status --rpc http://0.0.0.0:5050

输出结果显示了 "Yes"和 "No" 票数及其相对百分比。

Last change: 2023-12-03, commit: 462ec1a