Урок 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支付。

Відмова від відповідальності
* Криптоінвестиції пов'язані зі значними ризиками. Дійте обережно. Курс не є інвестиційною консультацією.
* Курс створений автором, який приєднався до Gate Learn. Будь-яка думка, висловлена автором, не є позицією Gate Learn.
Каталог
Урок 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支付。

Відмова від відповідальності
* Криптоінвестиції пов'язані зі значними ризиками. Дійте обережно. Курс не є інвестиційною консультацією.
* Курс створений автором, який приєднався до Gate Learn. Будь-яка думка, висловлена автором, не є позицією Gate Learn.