Lección 1

多重签名合约入门

多重签名(多签)合约,也被称为“M-of-N”合约,是一种用于增加区块链环境中交易的安全性和灵活性的关键机制,通过要求在交易执行之前获得多方批准来改变对资产的控制方式。“M-of-N”指的是N个当事方中必须有M个当事方批准交易才能使其有效。

多重签名合约原理

多重签名合约提供了一种创建对资产的共享控制的方法。典型的用例包括托管服务、企业账户管理、共同签署财务协议等。这些合约尤其适用于需要集体决策的组织或团体。

多重签名合约的设计理念是防篡改,并能够防止单点故障。即使一方的密钥被泄露,攻击者也无法在未经其他方批准的情况下执行交易,从而提高了安全性。

多重签名合约可以被认为是保险箱的数字等价物,需要多个密钥才能打开。在创建合约时,协议规定了密钥总数(N)和打开保险箱所需的最小密钥数(M)。

多重签名合约可以有许多不同的配置,具体取决于M和N的值:

  • 1-of-N:总数中的一方可以批准交易。此配置与没有多重签名的普通交易相同。为方便起见,它可能用于存在多个密钥的情况,但任何一个密钥都可以批准交易。
  • N-of-N:所有各方都必须批准交易。此配置具有最高级别的安全性,但如果一方丢失密钥或拒绝批准交易,可能会出现问题。
  • M-of-N(其中M<N):总数中的M方必须批准交易。这种配置是实践中经常使用的一种,因为它在安全性和灵活性之间取得了平衡。

区块链中的多重签名合约

在区块链中,多重签名合约被广泛用于增强交易安全性、支持复杂的治理机制或灵活控制区块链资产。其中一些示例包括:

  • 钱包:多重签名钱包用于保护资产安全。它们需要多方签署交易,从而提供了额外的安全性,防止了盗窃、外部黑客攻击和内部威胁。
  • 去中心化自治组织(DAO):DAO经常使用多重签名合约来执行其治理规则。针对提案的投票以多重签名交易的方式执行,DAO成员充当签署者。只有在提案获得足够的票数时才会执行。
  • 跨链操作:在跨链操作中,多重签名合约可以充当资产的托管方。当资产从一个区块链移动到另一个区块链时,起始链上的多重签名合约可以确保资产被安全锁定,直到在另一个链上的操作得到确认。
    尽管多重签名合约的实现在不同的区块链上可能有所不同,但核心概念保持不变:在执行交易之前需要多方批准。这一额外的安全层使多重签名合约成为区块链和加密货币领域的重要工具。

代码示例:使用SmartPy编写和部署多重签名合约

我们将通过代码演示三种不同的多重签名合约实现:

Lambda合约

Lambda合约非常灵活,具有广泛的用途,需要多个签名才能执行任意lambda函数。

Python
import smartpy as sp


@sp.module
def main():
    operation_lambda: type = sp.lambda_(sp.unit, sp.unit, with_operations=True)

    class MultisigLambda(sp.Contract):
        """Multiple members vote for executing lambdas.

        This contract can be originated with a list of addresses and a number of
        required votes. Any member can submit as much lambdas as he wants and vote
        for active proposals. When a lambda reaches the required votes, its code is
        called and the output operations are executed. This allows this contract to
        do anything that a contract can do: transferring tokens, managing assets,
        administrating another contract...

        When a lambda is applied, all submitted lambdas until now are inactivated.
        The members can still submit new lambdas.
        """

        def __init__(self, members, required_votes):
            """Constructor

            Args:
                members (sp.set of sp.address): people who can submit and vote
                    for lambda.
                required_votes (sp.nat): number of votes required
            """
            assert required_votes <= sp.len(
                members
            ), "required_votes must be <= len(members)"
            self.data.lambdas = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, operation_lambda]
            )
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
            )
            self.data.nextId = 0
            self.data.inactiveBefore = 0
            self.data.members = sp.cast(members, sp.set[sp.address])
            self.data.required_votes = sp.cast(required_votes, sp.nat)

        @sp.entrypoint
        def submit_lambda(self, lambda_):
            """Submit a new lambda to the vote.

            Submitting a proposal does not imply casting a vote in favour of it.

            Args:
                lambda_(sp.lambda with operations): lambda proposed to vote.
            Raises:
                `You are not a member`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            self.data.lambdas[self.data.nextId] = lambda_
            self.data.votes[self.data.nextId] = sp.set()
            self.data.nextId += 1

        @sp.entrypoint
        def vote_lambda(self, id):
            """Vote for a lambda.

            Args:
                id(sp.nat): id of the lambda to vote for.
            Raises:
                `You are not a member`, `The lambda is inactive`, `Lambda not found`

            There is no vote against or pass. If someone disagrees with a lambda
            they can avoid to vote.
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            assert id >= self.data.inactiveBefore, "The lambda is inactive"
            assert self.data.lambdas.contains(id), "Lambda not found"
            self.data.votes[id].add(sp.sender)
            if sp.len(self.data.votes[id]) >= self.data.required_votes:
                self.data.lambdas[id]()
                self.data.inactiveBefore = self.data.nextId

        @sp.onchain_view()
        def get_lambda(self, id):
            """Return the corresponding lambda.

            Args:
                id (sp.nat): id of the lambda to get.

            Return:
                pair of the lambda and a boolean showing if the lambda is active.
            """
            return (self.data.lambdas[id], id >= self.data.inactiveBefore)


# if "templates" not in __name__:


@sp.module
def test():
    class Administrated(sp.Contract):
        def __init__(self, admin):
            self.data.admin = admin
            self.data.value = sp.int(0)

        @sp.entrypoint
        def set_value(self, value):
            assert sp.sender == self.data.admin
            self.data.value = value


@sp.add_test(name="MultisigLambda basic scenario", is_default=True)
def basic_scenario():
    """Use the multisigLambda as an administrator of an example contract.

    Tests:
    - Origination
    - Lambda submission
    - Lambda vote
    """
    sc = sp.test_scenario([main, test])
    sc.h1("Basic scenario.")

    member1 = sp.test_account("member1")
    member2 = sp.test_account("member2")
    member3 = sp.test_account("member3")
    members = sp.set([member1.address, member2.address, member3.address])

    sc.h2("MultisigLambda: origination")
    c1 = main.MultisigLambda(members, 2)
    sc += c1

    sc.h2("Administrated: origination")
    c2 = test.Administrated(c1.address)
    sc += c2

    sc.h2("MultisigLambda: submit_lambda")

    def set_42(params):
        administrated = sp.contract(sp.TInt, c2.address, entrypoint="set_value")
        sp.transfer(sp.int(42), sp.tez(0), administrated.open_some())

    lambda_ = sp.build_lambda(set_42, with_operations=True)
    c1.submit_lambda(lambda_).run(sender=member1)

    sc.h2("MultisigLambda: vote_lambda")
    c1.vote_lambda(0).run(sender=member1)
    c1.vote_lambda(0).run(sender=member2)

    # We can check that the administrated contract received the transfer.
    sc.verify(c2.data.value == 42)

多重签名行动(MultisigAction)合约

多重签名行动合约引入了提案投票的概念。在此合约中,签署者可以对要执行的某些操作进行投票,如果达到要求的票数,则执行提议的操作。

Python
import smartpy as sp


@sp.module
def main():
    # Internal administration action type specification
    InternalAdminAction: type = sp.variant(
        addSigners=sp.list[sp.address],
        changeQuorum=sp.nat,
        removeSigners=sp.list[sp.address],
    )

    class MultisigAction(sp.Contract):
        """A contract that can be used by multiple signers to administrate other
        contracts. The administrated contracts implement an interface that make it
        possible to explicit the administration process to non expert users.

        Signers vote for proposals. A proposal is a list of a target with a list of
        action. An action is a simple byte but it is intended to be a pack value of
        a variant. This simple pattern make it possible to build a UX interface
        that shows the content of a proposal or build one.
        """

        def __init__(self, quorum, signers):
            self.data.inactiveBefore = 0
            self.data.nextId = 0
            self.data.proposals = sp.cast(
                sp.big_map(),
                sp.big_map[
                    sp.nat,
                    sp.list[sp.record(target=sp.address, actions=sp.list[sp.bytes])],
                ],
            )
            self.data.quorum = sp.cast(quorum, sp.nat)
            self.data.signers = sp.cast(signers, sp.set[sp.address])
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
            )

        @sp.entrypoint
        def send_proposal(self, proposal):
            """Signer-only. Submit a proposal to the vote.

            Args:
                proposal (sp.list of sp.record of target address and action): List\
                    of target and associated administration actions.
            """
            assert self.data.signers.contains(sp.sender), "Only signers can propose"
            self.data.proposals[self.data.nextId] = proposal
            self.data.votes[self.data.nextId] = sp.set()
            self.data.nextId += 1

        @sp.entrypoint
        def vote(self, pId):
            """Vote for one or more proposals

            Args:
                pId (sp.nat): Id of the proposal.
            """
            assert self.data.signers.contains(sp.sender), "Only signers can vote"
            assert self.data.votes.contains(pId), "Proposal unknown"
            assert pId >= self.data.inactiveBefore, "The proposal is inactive"
            self.data.votes[pId].add(sp.sender)

            if sp.len(self.data.votes.get(pId, default=sp.set())) >= self.data.quorum:
                self._onApproved(pId)

        @sp.private(with_storage="read-write", with_operations=True)
        def _onApproved(self, pId):
            """Inlined function. Logic applied when a proposal has been approved."""
            proposal = self.data.proposals.get(pId, default=[])
            for p_item in proposal:
                contract = sp.contract(sp.list[sp.bytes], p_item.target)
                sp.transfer(
                    p_item.actions,
                    sp.tez(0),
                    contract.unwrap_some(error="InvalidTarget"),
                )
            # Inactivate all proposals that have been already submitted.
            self.data.inactiveBefore = self.data.nextId

        @sp.entrypoint
        def administrate(self, actions):
            """Self-call only. Administrate this contract.

            This entrypoint must be called through the proposal system.

            Args:
                actions (sp.list of sp.bytes): List of packed variant of \
                    `InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
            """
            assert (
                sp.sender == sp.self_address()
            ), "This entrypoint must be called through the proposal system."

            for packed_actions in actions:
                action = sp.unpack(packed_actions, InternalAdminAction).unwrap_some(
                    error="Bad actions format"
                )
                with sp.match(action):
                    with sp.case.changeQuorum as quorum:
                        self.data.quorum = quorum
                    with sp.case.addSigners as added:
                        for signer in added:
                            self.data.signers.add(signer)
                    with sp.case.removeSigners as removed:
                        for address in removed:
                            self.data.signers.remove(address)
                # Ensure that the contract never requires more quorum than the total of signers.
                assert self.data.quorum <= sp.len(
                    self.data.signers
                ), "More quorum than signers."


if "templates" not in __name__:

    @sp.add_test(name="Basic scenario", is_default=True)
    def test():
        signer1 = sp.test_account("signer1")
        signer2 = sp.test_account("signer2")
        signer3 = sp.test_account("signer3")

        s = sp.test_scenario(main)
        s.h1("Basic scenario")

        s.h2("Origination")
        c1 = main.MultisigAction(
            quorum=2,
            signers=sp.set([signer1.address, signer2.address]),
        )
        s += c1

        s.h2("Proposal for adding a new signer")
        target = sp.to_address(
            sp.contract(sp.TList(sp.TBytes), c1.address, "administrate").open_some()
        )
        action = sp.pack(
            sp.set_type_expr(
                sp.variant("addSigners", [signer3.address]), main.InternalAdminAction
            )
        )
        c1.send_proposal([sp.record(target=target, actions=[action])]).run(
            sender=signer1
        )

        s.h2("Signer 1 votes for the proposal")
        c1.vote(0).run(sender=signer1)
        s.h2("Signer 2 votes for the proposal")
        c1.vote(0).run(sender=signer2)

        s.verify(c1.data.signers.contains(signer3.address))

多重签名视图(MultisigView)合约

多重签名视图合约也采用了一个投票机制。该合约允许成员提交并投票支持任意字节。一旦提案获得所需的票数,就可以通过视图确认其状态。

Python
import smartpy as sp


@sp.module
def main():
    class MultisigView(sp.Contract):
        """Multiple members vote for arbitrary bytes.

        This contract can be originated with a list of addresses and a number of
        required votes. Any member can submit as many bytes as they want and vote
        for active proposals.

        Any bytes that reached the required votes can be confirmed via a view.
        """

        def __init__(self, members, required_votes):
            """Constructor

            Args:
                members (sp.set of sp.address): people who can submit and vote for
                    lambda.
                required_votes (sp.nat): number of votes required
            """
            assert required_votes <= sp.len(
                members
            ), "required_votes must be <= len(members)"
            self.data.proposals = sp.cast(sp.big_map(), sp.big_map[sp.bytes, sp.bool])
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.bytes, sp.set[sp.address]]
            )
            self.data.members = sp.cast(members, sp.set[sp.address])
            self.data.required_votes = sp.cast(required_votes, sp.nat)

        @sp.entrypoint
        def submit_proposal(self, bytes):
            """Submit a new proposal to the vote.

            Submitting a proposal does not imply casting a vote in favour of it.

            Args:
                bytes(sp.bytes): bytes proposed to vote.
            Raises:
                `You are not a member`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            self.data.proposals[bytes] = False
            self.data.votes[bytes] = sp.set()

        @sp.entrypoint
        def vote_proposal(self, bytes):
            """Vote for a proposal.

            There is no vote against or pass. If one disagrees with a proposal they
            can avoid to vote. Warning: old non-voted proposals never become
            obsolete.

            Args:
                id(sp.bytes): bytes of the proposal.
            Raises:
                `You are not a member`, `Proposal not found`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            assert self.data.proposals.contains(bytes), "Proposal not found"
            self.data.votes[bytes].add(sp.sender)
            if sp.len(self.data.votes[bytes]) >= self.data.required_votes:
                self.data.proposals[bytes] = True

        @sp.onchain_view()
        def is_voted(self, id):
            """Returns a boolean indicating whether the proposal has been voted on.

            Args:
                id (sp.bytes): bytes of the proposal
            Return:
                (sp.bool): True if the proposal has been voted, False otherwise.
            """
            return self.data.proposals.get(id, error="Proposal not found")


if "templates" not in __name__:

    @sp.add_test(name="MultisigView basic scenario", is_default=True)
    def basic_scenario():
        """A scenario with a vote on the multisigView contract.

        Tests:
        - Origination
        - Proposal submission
        - Proposal vote
        """
        sc = sp.test_scenario(main)
        sc.h1("Basic scenario.")

        member1 = sp.test_account("member1")
        member2 = sp.test_account("member2")
        member3 = sp.test_account("member3")
        members = sp.set([member1.address, member2.address, member3.address])

        sc.h2("Origination")
        c1 = main.MultisigView(members, 2)
        sc += c1

        sc.h2("submit_proposal")
        c1.submit_proposal(sp.bytes("0x42")).run(sender=member1)

        sc.h2("vote_proposal")
        c1.vote_proposal(sp.bytes("0x42")).run(sender=member1)
        c1.vote_proposal(sp.bytes("0x42")).run(sender=member2)

        # We can check that the proposal has been validated.
        sc.verify(c1.is_voted(sp.bytes("0x42")))

在以上三个合约中,每个合约都采用了不同的机制来实现多重签名控制,能够灵活地满足你的区块链用例的具体需求。

如何通过SmartPy在线平台运行多重签名合约

要运行我们用SmartPy编写的多重签名合约,可以按照以下步骤操作:

  1. 进入SmartPy IDE,网址https://smartpy.io/ide

  2. 将合约代码粘贴到编辑器中。你可以替换掉原有的代码。

  3. 要运行合约,请单击顶部面板上的“Run”按钮。

  4. 运行合约后,你可以在右侧的“Output”面板中查看执行情况。此处将显示每个操作的详细信息,包括提案、投票和批准。

  5. 要在Tezos网络上部署合约,你首先需要对其进行编译,只需单击顶部面板上的“Compile”按钮。

  6. 编译完成后,你可以单击“Deploy Michelson Contract”按钮,将合约部署到测试网上。你需要提供一个具有足够资金支付部署的gas费用的Tezos账户的密钥。

  7. 合约部署完成后,你将获得区块链上合约的地址。你可以使用此地址进行交易,实现与合约的交互。

  8. 要在合约中提交提案或投票,你可以使用合约代码中定义的入口点,如submit_proposalvote_proposal。这些入口点可以直接从你创建的交易中调用。

需要注意的是,虽然我们可以通过SmartPy IDE在模拟区块链上测试合约,但将合约部署到实际的Tezos网络将产生gas费,必须使用Tezos网络的原生加密货币XTZ支付。

Descargo de responsabilidad
* La inversión en criptomonedas implica riesgos significativos. Proceda con precaución. El curso no pretende ser un asesoramiento de inversión.
* El curso ha sido creado por el autor que se ha unido a Gate Learn. Cualquier opinión compartida por el autor no representa a Gate Learn.
Catálogo
Lección 1

多重签名合约入门

多重签名(多签)合约,也被称为“M-of-N”合约,是一种用于增加区块链环境中交易的安全性和灵活性的关键机制,通过要求在交易执行之前获得多方批准来改变对资产的控制方式。“M-of-N”指的是N个当事方中必须有M个当事方批准交易才能使其有效。

多重签名合约原理

多重签名合约提供了一种创建对资产的共享控制的方法。典型的用例包括托管服务、企业账户管理、共同签署财务协议等。这些合约尤其适用于需要集体决策的组织或团体。

多重签名合约的设计理念是防篡改,并能够防止单点故障。即使一方的密钥被泄露,攻击者也无法在未经其他方批准的情况下执行交易,从而提高了安全性。

多重签名合约可以被认为是保险箱的数字等价物,需要多个密钥才能打开。在创建合约时,协议规定了密钥总数(N)和打开保险箱所需的最小密钥数(M)。

多重签名合约可以有许多不同的配置,具体取决于M和N的值:

  • 1-of-N:总数中的一方可以批准交易。此配置与没有多重签名的普通交易相同。为方便起见,它可能用于存在多个密钥的情况,但任何一个密钥都可以批准交易。
  • N-of-N:所有各方都必须批准交易。此配置具有最高级别的安全性,但如果一方丢失密钥或拒绝批准交易,可能会出现问题。
  • M-of-N(其中M<N):总数中的M方必须批准交易。这种配置是实践中经常使用的一种,因为它在安全性和灵活性之间取得了平衡。

区块链中的多重签名合约

在区块链中,多重签名合约被广泛用于增强交易安全性、支持复杂的治理机制或灵活控制区块链资产。其中一些示例包括:

  • 钱包:多重签名钱包用于保护资产安全。它们需要多方签署交易,从而提供了额外的安全性,防止了盗窃、外部黑客攻击和内部威胁。
  • 去中心化自治组织(DAO):DAO经常使用多重签名合约来执行其治理规则。针对提案的投票以多重签名交易的方式执行,DAO成员充当签署者。只有在提案获得足够的票数时才会执行。
  • 跨链操作:在跨链操作中,多重签名合约可以充当资产的托管方。当资产从一个区块链移动到另一个区块链时,起始链上的多重签名合约可以确保资产被安全锁定,直到在另一个链上的操作得到确认。
    尽管多重签名合约的实现在不同的区块链上可能有所不同,但核心概念保持不变:在执行交易之前需要多方批准。这一额外的安全层使多重签名合约成为区块链和加密货币领域的重要工具。

代码示例:使用SmartPy编写和部署多重签名合约

我们将通过代码演示三种不同的多重签名合约实现:

Lambda合约

Lambda合约非常灵活,具有广泛的用途,需要多个签名才能执行任意lambda函数。

Python
import smartpy as sp


@sp.module
def main():
    operation_lambda: type = sp.lambda_(sp.unit, sp.unit, with_operations=True)

    class MultisigLambda(sp.Contract):
        """Multiple members vote for executing lambdas.

        This contract can be originated with a list of addresses and a number of
        required votes. Any member can submit as much lambdas as he wants and vote
        for active proposals. When a lambda reaches the required votes, its code is
        called and the output operations are executed. This allows this contract to
        do anything that a contract can do: transferring tokens, managing assets,
        administrating another contract...

        When a lambda is applied, all submitted lambdas until now are inactivated.
        The members can still submit new lambdas.
        """

        def __init__(self, members, required_votes):
            """Constructor

            Args:
                members (sp.set of sp.address): people who can submit and vote
                    for lambda.
                required_votes (sp.nat): number of votes required
            """
            assert required_votes <= sp.len(
                members
            ), "required_votes must be <= len(members)"
            self.data.lambdas = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, operation_lambda]
            )
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
            )
            self.data.nextId = 0
            self.data.inactiveBefore = 0
            self.data.members = sp.cast(members, sp.set[sp.address])
            self.data.required_votes = sp.cast(required_votes, sp.nat)

        @sp.entrypoint
        def submit_lambda(self, lambda_):
            """Submit a new lambda to the vote.

            Submitting a proposal does not imply casting a vote in favour of it.

            Args:
                lambda_(sp.lambda with operations): lambda proposed to vote.
            Raises:
                `You are not a member`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            self.data.lambdas[self.data.nextId] = lambda_
            self.data.votes[self.data.nextId] = sp.set()
            self.data.nextId += 1

        @sp.entrypoint
        def vote_lambda(self, id):
            """Vote for a lambda.

            Args:
                id(sp.nat): id of the lambda to vote for.
            Raises:
                `You are not a member`, `The lambda is inactive`, `Lambda not found`

            There is no vote against or pass. If someone disagrees with a lambda
            they can avoid to vote.
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            assert id >= self.data.inactiveBefore, "The lambda is inactive"
            assert self.data.lambdas.contains(id), "Lambda not found"
            self.data.votes[id].add(sp.sender)
            if sp.len(self.data.votes[id]) >= self.data.required_votes:
                self.data.lambdas[id]()
                self.data.inactiveBefore = self.data.nextId

        @sp.onchain_view()
        def get_lambda(self, id):
            """Return the corresponding lambda.

            Args:
                id (sp.nat): id of the lambda to get.

            Return:
                pair of the lambda and a boolean showing if the lambda is active.
            """
            return (self.data.lambdas[id], id >= self.data.inactiveBefore)


# if "templates" not in __name__:


@sp.module
def test():
    class Administrated(sp.Contract):
        def __init__(self, admin):
            self.data.admin = admin
            self.data.value = sp.int(0)

        @sp.entrypoint
        def set_value(self, value):
            assert sp.sender == self.data.admin
            self.data.value = value


@sp.add_test(name="MultisigLambda basic scenario", is_default=True)
def basic_scenario():
    """Use the multisigLambda as an administrator of an example contract.

    Tests:
    - Origination
    - Lambda submission
    - Lambda vote
    """
    sc = sp.test_scenario([main, test])
    sc.h1("Basic scenario.")

    member1 = sp.test_account("member1")
    member2 = sp.test_account("member2")
    member3 = sp.test_account("member3")
    members = sp.set([member1.address, member2.address, member3.address])

    sc.h2("MultisigLambda: origination")
    c1 = main.MultisigLambda(members, 2)
    sc += c1

    sc.h2("Administrated: origination")
    c2 = test.Administrated(c1.address)
    sc += c2

    sc.h2("MultisigLambda: submit_lambda")

    def set_42(params):
        administrated = sp.contract(sp.TInt, c2.address, entrypoint="set_value")
        sp.transfer(sp.int(42), sp.tez(0), administrated.open_some())

    lambda_ = sp.build_lambda(set_42, with_operations=True)
    c1.submit_lambda(lambda_).run(sender=member1)

    sc.h2("MultisigLambda: vote_lambda")
    c1.vote_lambda(0).run(sender=member1)
    c1.vote_lambda(0).run(sender=member2)

    # We can check that the administrated contract received the transfer.
    sc.verify(c2.data.value == 42)

多重签名行动(MultisigAction)合约

多重签名行动合约引入了提案投票的概念。在此合约中,签署者可以对要执行的某些操作进行投票,如果达到要求的票数,则执行提议的操作。

Python
import smartpy as sp


@sp.module
def main():
    # Internal administration action type specification
    InternalAdminAction: type = sp.variant(
        addSigners=sp.list[sp.address],
        changeQuorum=sp.nat,
        removeSigners=sp.list[sp.address],
    )

    class MultisigAction(sp.Contract):
        """A contract that can be used by multiple signers to administrate other
        contracts. The administrated contracts implement an interface that make it
        possible to explicit the administration process to non expert users.

        Signers vote for proposals. A proposal is a list of a target with a list of
        action. An action is a simple byte but it is intended to be a pack value of
        a variant. This simple pattern make it possible to build a UX interface
        that shows the content of a proposal or build one.
        """

        def __init__(self, quorum, signers):
            self.data.inactiveBefore = 0
            self.data.nextId = 0
            self.data.proposals = sp.cast(
                sp.big_map(),
                sp.big_map[
                    sp.nat,
                    sp.list[sp.record(target=sp.address, actions=sp.list[sp.bytes])],
                ],
            )
            self.data.quorum = sp.cast(quorum, sp.nat)
            self.data.signers = sp.cast(signers, sp.set[sp.address])
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
            )

        @sp.entrypoint
        def send_proposal(self, proposal):
            """Signer-only. Submit a proposal to the vote.

            Args:
                proposal (sp.list of sp.record of target address and action): List\
                    of target and associated administration actions.
            """
            assert self.data.signers.contains(sp.sender), "Only signers can propose"
            self.data.proposals[self.data.nextId] = proposal
            self.data.votes[self.data.nextId] = sp.set()
            self.data.nextId += 1

        @sp.entrypoint
        def vote(self, pId):
            """Vote for one or more proposals

            Args:
                pId (sp.nat): Id of the proposal.
            """
            assert self.data.signers.contains(sp.sender), "Only signers can vote"
            assert self.data.votes.contains(pId), "Proposal unknown"
            assert pId >= self.data.inactiveBefore, "The proposal is inactive"
            self.data.votes[pId].add(sp.sender)

            if sp.len(self.data.votes.get(pId, default=sp.set())) >= self.data.quorum:
                self._onApproved(pId)

        @sp.private(with_storage="read-write", with_operations=True)
        def _onApproved(self, pId):
            """Inlined function. Logic applied when a proposal has been approved."""
            proposal = self.data.proposals.get(pId, default=[])
            for p_item in proposal:
                contract = sp.contract(sp.list[sp.bytes], p_item.target)
                sp.transfer(
                    p_item.actions,
                    sp.tez(0),
                    contract.unwrap_some(error="InvalidTarget"),
                )
            # Inactivate all proposals that have been already submitted.
            self.data.inactiveBefore = self.data.nextId

        @sp.entrypoint
        def administrate(self, actions):
            """Self-call only. Administrate this contract.

            This entrypoint must be called through the proposal system.

            Args:
                actions (sp.list of sp.bytes): List of packed variant of \
                    `InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
            """
            assert (
                sp.sender == sp.self_address()
            ), "This entrypoint must be called through the proposal system."

            for packed_actions in actions:
                action = sp.unpack(packed_actions, InternalAdminAction).unwrap_some(
                    error="Bad actions format"
                )
                with sp.match(action):
                    with sp.case.changeQuorum as quorum:
                        self.data.quorum = quorum
                    with sp.case.addSigners as added:
                        for signer in added:
                            self.data.signers.add(signer)
                    with sp.case.removeSigners as removed:
                        for address in removed:
                            self.data.signers.remove(address)
                # Ensure that the contract never requires more quorum than the total of signers.
                assert self.data.quorum <= sp.len(
                    self.data.signers
                ), "More quorum than signers."


if "templates" not in __name__:

    @sp.add_test(name="Basic scenario", is_default=True)
    def test():
        signer1 = sp.test_account("signer1")
        signer2 = sp.test_account("signer2")
        signer3 = sp.test_account("signer3")

        s = sp.test_scenario(main)
        s.h1("Basic scenario")

        s.h2("Origination")
        c1 = main.MultisigAction(
            quorum=2,
            signers=sp.set([signer1.address, signer2.address]),
        )
        s += c1

        s.h2("Proposal for adding a new signer")
        target = sp.to_address(
            sp.contract(sp.TList(sp.TBytes), c1.address, "administrate").open_some()
        )
        action = sp.pack(
            sp.set_type_expr(
                sp.variant("addSigners", [signer3.address]), main.InternalAdminAction
            )
        )
        c1.send_proposal([sp.record(target=target, actions=[action])]).run(
            sender=signer1
        )

        s.h2("Signer 1 votes for the proposal")
        c1.vote(0).run(sender=signer1)
        s.h2("Signer 2 votes for the proposal")
        c1.vote(0).run(sender=signer2)

        s.verify(c1.data.signers.contains(signer3.address))

多重签名视图(MultisigView)合约

多重签名视图合约也采用了一个投票机制。该合约允许成员提交并投票支持任意字节。一旦提案获得所需的票数,就可以通过视图确认其状态。

Python
import smartpy as sp


@sp.module
def main():
    class MultisigView(sp.Contract):
        """Multiple members vote for arbitrary bytes.

        This contract can be originated with a list of addresses and a number of
        required votes. Any member can submit as many bytes as they want and vote
        for active proposals.

        Any bytes that reached the required votes can be confirmed via a view.
        """

        def __init__(self, members, required_votes):
            """Constructor

            Args:
                members (sp.set of sp.address): people who can submit and vote for
                    lambda.
                required_votes (sp.nat): number of votes required
            """
            assert required_votes <= sp.len(
                members
            ), "required_votes must be <= len(members)"
            self.data.proposals = sp.cast(sp.big_map(), sp.big_map[sp.bytes, sp.bool])
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.bytes, sp.set[sp.address]]
            )
            self.data.members = sp.cast(members, sp.set[sp.address])
            self.data.required_votes = sp.cast(required_votes, sp.nat)

        @sp.entrypoint
        def submit_proposal(self, bytes):
            """Submit a new proposal to the vote.

            Submitting a proposal does not imply casting a vote in favour of it.

            Args:
                bytes(sp.bytes): bytes proposed to vote.
            Raises:
                `You are not a member`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            self.data.proposals[bytes] = False
            self.data.votes[bytes] = sp.set()

        @sp.entrypoint
        def vote_proposal(self, bytes):
            """Vote for a proposal.

            There is no vote against or pass. If one disagrees with a proposal they
            can avoid to vote. Warning: old non-voted proposals never become
            obsolete.

            Args:
                id(sp.bytes): bytes of the proposal.
            Raises:
                `You are not a member`, `Proposal not found`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            assert self.data.proposals.contains(bytes), "Proposal not found"
            self.data.votes[bytes].add(sp.sender)
            if sp.len(self.data.votes[bytes]) >= self.data.required_votes:
                self.data.proposals[bytes] = True

        @sp.onchain_view()
        def is_voted(self, id):
            """Returns a boolean indicating whether the proposal has been voted on.

            Args:
                id (sp.bytes): bytes of the proposal
            Return:
                (sp.bool): True if the proposal has been voted, False otherwise.
            """
            return self.data.proposals.get(id, error="Proposal not found")


if "templates" not in __name__:

    @sp.add_test(name="MultisigView basic scenario", is_default=True)
    def basic_scenario():
        """A scenario with a vote on the multisigView contract.

        Tests:
        - Origination
        - Proposal submission
        - Proposal vote
        """
        sc = sp.test_scenario(main)
        sc.h1("Basic scenario.")

        member1 = sp.test_account("member1")
        member2 = sp.test_account("member2")
        member3 = sp.test_account("member3")
        members = sp.set([member1.address, member2.address, member3.address])

        sc.h2("Origination")
        c1 = main.MultisigView(members, 2)
        sc += c1

        sc.h2("submit_proposal")
        c1.submit_proposal(sp.bytes("0x42")).run(sender=member1)

        sc.h2("vote_proposal")
        c1.vote_proposal(sp.bytes("0x42")).run(sender=member1)
        c1.vote_proposal(sp.bytes("0x42")).run(sender=member2)

        # We can check that the proposal has been validated.
        sc.verify(c1.is_voted(sp.bytes("0x42")))

在以上三个合约中,每个合约都采用了不同的机制来实现多重签名控制,能够灵活地满足你的区块链用例的具体需求。

如何通过SmartPy在线平台运行多重签名合约

要运行我们用SmartPy编写的多重签名合约,可以按照以下步骤操作:

  1. 进入SmartPy IDE,网址https://smartpy.io/ide

  2. 将合约代码粘贴到编辑器中。你可以替换掉原有的代码。

  3. 要运行合约,请单击顶部面板上的“Run”按钮。

  4. 运行合约后,你可以在右侧的“Output”面板中查看执行情况。此处将显示每个操作的详细信息,包括提案、投票和批准。

  5. 要在Tezos网络上部署合约,你首先需要对其进行编译,只需单击顶部面板上的“Compile”按钮。

  6. 编译完成后,你可以单击“Deploy Michelson Contract”按钮,将合约部署到测试网上。你需要提供一个具有足够资金支付部署的gas费用的Tezos账户的密钥。

  7. 合约部署完成后,你将获得区块链上合约的地址。你可以使用此地址进行交易,实现与合约的交互。

  8. 要在合约中提交提案或投票,你可以使用合约代码中定义的入口点,如submit_proposalvote_proposal。这些入口点可以直接从你创建的交易中调用。

需要注意的是,虽然我们可以通过SmartPy IDE在模拟区块链上测试合约,但将合约部署到实际的Tezos网络将产生gas费,必须使用Tezos网络的原生加密货币XTZ支付。

Descargo de responsabilidad
* La inversión en criptomonedas implica riesgos significativos. Proceda con precaución. El curso no pretende ser un asesoramiento de inversión.
* El curso ha sido creado por el autor que se ha unido a Gate Learn. Cualquier opinión compartida por el autor no representa a Gate Learn.