部署投票合约并与之交互

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

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

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

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

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

Listing 16-7 shows the Vote contract in detail:

/// @dev Core Library Imports for the Traits outside the Starknet Contract
use core::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 core::starknet::ContractAddress;
    use core::starknet::get_caller_address;
    use core::starknet::storage::{
        StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess,
        StorageMapWriteAccess, Map
    };

    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: Map::<ContractAddress, bool>,
        registered_voter: Map::<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
    #[abi(embed_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) {
                self.emit(UnauthorizedAttempt { unauthorized_address: address, });
            }

            assert!(is_voter, "USER_NOT_REGISTERED");
            assert!(can_vote, "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)
        }
    }
}

Listing 16-7: A voting smart contract

Deploying, Calling and Invoking the Voting Contract

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

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

  • Calling contracts: Interacting with external functions that only read from the state. These functions do not alter the state of the network, so they don't require fees or signing.
  • Invoking contracts: Interacting with external functions that can write to the state. These functions do alter the state of the network and require fees and signing.

We will setup a local development node using katana to deploy the voting contract. Then, we'll interact with the contract by calling and invoking its functions. You can also use the Goerli Testnet instead of katana. However, we recommend using katana for local development and testing. You can find the complete tutorial for katana in the "Katana: A Local Node" chapter of the Starknet Book.

The katana Local Starknet Node

katana is designed to support local development by the Dojo team. It will allow you to do everything you need to do with Starknet, but locally. It is a great tool for development and testing.

To install katana from the source code, please refer to the "Basic Installation" chapter of the Starknet Book.

Note: Please verify that the version of katana match the specified version provided below.

$ katana --version
katana 0.6.0

To upgrade katana version, refer to the "Basic Installation" chapter of the Starknet Book.

一旦安装了 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
...

Before we can interact with the voting contract, we need to prepare the voter and admin accounts on Starknet. Each voter account must be registered and sufficiently funded for voting. For a more detailed understanding of how accounts operate with Account Abstraction, refer to the "Account Abstraction" chapter of the Starknet Book.

Smart Wallets for Voting

Aside from Scarb you will need to have Starkli installed. Starkli is a command line tool that allows you to interact with Starknet. You can find the installation instructions in the ["Basic Installation"][starkli installation] chapter of the Starknet Book.

Note: Please verify that the version of starkli match the specified version provided below.

$ starkli --version
0.2.9 (0535f44)

To upgrade starkli to 0.2.9, use the starkliup -v 0.2.9 command, or simply starkliup which installed the latest stable version.

For each smart wallet we'll use, we must create a Signer within the encrypted keystore and an Account Descriptor. This process is also detailed in the ["Testnet Deployment"][signer creation] chapter of the 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 installation]: https://book.starknet.io/ch02-01-basic-installation.html#starkli-installation) [signer creation]: https://book.starknet.io/ch02-05-testnet-deployment.html?highlight=signer#creating-a-signer

Contract Deployment

在部署之前,我们需要声明合约。我们可以使用 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 的最新版本。

The class hash of the contract is: 0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52. You can declare this contract on Sepolia testnet and see that the class hash will correspond.

--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 Eligibility Verification

在我们的投票合约中,我们有两个函数来验证投票人的资格,即 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(即假),确认该账户没有投票资格。

Casting a Vote

既然我们已经确定了如何验证选民资格,那么我们就可以投票了!投票时,我们要与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, "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

Visualizing Vote Outcomes

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

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

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