This site is a mirror of ama.ne.jp.

スマートコントラクトで射精管理

/* この記事は、ライフ 人間と科学シリーズ(Plume)の閉鎖処理の一環として、投稿した情報の整理を行うために書かれた。 */

概要

スマートコントラクト は、中央管理に依存しないプログラムの実行環境を提供する仕組みである。スマートコントラクトを用いることで契約の実行や検証を自動で行えるようになり、契約に基づく 射精管理 のような状況を実現するのに役立つ。本記事では、管理者が射精の制限期間を増減できる単純な状況を想定し、Ethereumネットワーク上で動作する射精管理コントラクトの実装例を提示する。

前提知識

スマートコントラクト

スマートコントラクトとは、中央管理に依存せずトランザクション上で契約の実行や検証を行うための仕組みである。特に、チューリング完全なプログラムを実行可能なEthereumネットワークは分散型アプリケーションが動作するプラットフォームとして広く使われている。

スマートコントラクトの実行結果は、ネットワーク参加者が採掘を通じてブロックチェーンに記録されていくため、ブロックチェーンの改ざん耐性を限度にその正当性が保証される。そのため、中央集権的な実行環境では容易に書き換えられるような処理でも安全に実行できる。例えば、「ゴミやたばこのポイ捨てを行ったアカウントから健康寿命を4時間取り上げ、ゴミを拾ったアカウントに3時間、難病のアカウントに1時間移し替える」ような処理を、四則演算と代入を使った素朴な表現で実装できる。

healthyLife[illegal] = healthyLife[illegal].sub(4 hours);
healthyLife[legal] = healthyLife[legal].add(3 hours);
healthyLife[disease] = healthyLife[disease].add(1 hours);

射精管理

射精管理とは、射精の権限を他人に渡して自由な射精を制限されること、また他人から射精の権限を受け取って自由な射精を制限することである。権限を受け渡すには、契約書への署名や宣誓の録音・撮影で精神的な制限を与えたり、貞操帯などで物理的な制限を与えるなどの手段があるが、これに限らない。

しかし、単なる契約書は、法的な係争では役立つものの実際に射精することを禁止できるわけではない。また、貞操帯などを用いた物理的制限も、必ずしも契約の範囲内で管理を行えるよう設計されているわけではない。このような状況で契約書と貞操帯を接続するために、スマートコントラクトが役立つ。

この記事では便宜上、射精を管理する「管理者」と射精を管理される「被管理者」を分けて進めるが、必ずしも2人以上の人間が必要というわけではなく、管理者の肉体的あるいは精神的存在の有無はさほど重要ではない。また、射精管理という語の定義上では被管理者の意思に反して(何度も)強制的に射精させるケースも含むが、この記事では主に被管理者の射精欲求にかかわらず射精を禁止するケースのみを扱う。

射精管理コントラクト

ここからは、スマートコントラクトで射精管理を実施する単純な例について考える。この例では、1人の管理者が1人の被管理者に管理期間を経て1回限りの射精許可を与える場合を想定している。そのため、以下のような複数の管理者・被管理者を束ねるケースについては別に考える。

  • 複数の管理者が1人の被管理者の射精をロックし、過半数の許可で射精できるようなケース
  • 1人の管理者が複数の被管理者の射精をロックし、最も多くの管理費を支払った被管理者のみが射精できるようなケース

周期的な管理を必要とするケースでは、この記事で示す1回限りの射精許可を繰り返せばよさそうだが、射精回数より管理費の支払い回数が少ない場合などは新たな実装を考える必要がある。

ここで示す実装の全体はamane-katagiri/domimiの通りである。

登場人物・仕様

_射精管理コントラクトのシーケンス図

被管理者

被管理者 は射精管理の対象となっている者である。少なくとも射精管理を受けている間は 射精管理デバイス を装着しており、 管理期間 内は射精することができない。管理期間が終了するまで待ち、管理期間が追加されないように、また管理期間より前に射精許可が得られるように行動する。

被管理者は、 管理者 が示した条件の下で 管理開始費 を支払って管理を開始し、 管理期間終了後に無料で、あるいは終了前に 管理中止費 を支払ってリセットできるコントラクトを作成する。被管理者は以下の操作を行うことができる。

  • 管理者が設定されていない場合
    • いつでも: 管理開始費を支払ってコントラクトに署名済みの管理情報を設定できる
  • 管理者が設定されている場合
    • 管理期間内: 管理中止費を支払って管理情報をリセットできる
    • 管理期間外: 無料で管理情報をリセットできる
    • いつでも: 管理期間を追加できる

管理者

管理者 は射精管理を実施する者である。コントラクトを通じて射精管理デバイスを操作し、被管理者が管理期間内に射精することを禁止する。管理期間は自由に増減でき、射精許可を条件に、また管理期間の追加をちらつかせて被管理者との支配・被支配関係を確立する。

管理者は、被管理者に指定した管理期間や管理開始費でコントラクトを開始させ、一定の範囲で自由に管理期間を追加し、または管理期間終了前に任意にリセットする。管理者は以下の操作を行うことができる。

  • 管理者が設定されていない場合
    • いつでも: 署名済みの情報を渡して自らを管理者として設定するようリクエストできる
  • 管理者として設定されている場合
    • いつでも: 管理情報をリセットできる
    • いつでも: 管理期間を追加できる

このうち、被管理者が実行するインセンティブがない、あるいは管理者が実行する方がふさわしい処理は管理者自身で行う。実行にかかる費用は手数料として被管理者から差し引かれる。

射精管理デバイス

射精管理デバイス は、被管理者に装着して物理的に射精を制限するための装置である。管理者のみが自由に管理を開始・終了できる可用性とともに、被管理者が不正に解錠・射精しないための機密性を備えている。

典型的には、南京錠でロックする古典的な貞操帯や、QIUIなどのスマート貞操帯がある。さらに、鍵を保管するためのChronoVaultなどの補助的な装置を用いることもできる。これらの装置は、 射精管理サーバ が操作するためのインターフェースを提供しなければならない。

射精管理サーバ

射精管理サーバ は、管理者の代わりに射精管理デバイスの操作を行う第三者である。射精許可のための秘密の情報を保持しており、利便性のためにデプロイの代行やコミュニケーションの補助を行うかもしれない。ただし、分散型アプリケーションの利点を保つため、サーバはできるだけ小さく保つべきである。

射精管理サーバは、主に被管理者のリクエストに応じて、コントラクトに管理者が設定されていなければ射精許可を行う。射精許可の実装はオフチェーンであり、例えば射精管理デバイスによって以下のような処理を行う。

  • 南京錠の数字を通知する(ChastiKey
  • スマート貞操帯に解除リクエストを送る

実装

スマートコントラクト

管理情報の保持

コントラクトは以下のような管理情報を保持する。このうち、管理期間、管理開始費、管理中止費、管理実行費、1回あたりの最大延長期間は管理者が示したパラメータで設定される。管理開始時刻は、管理開始時にブロックの時刻で設定される。

// 管理詳細情報
struct Management {
    // 管理者
    address payable master;
    // 管理開始時刻(UNIX時間)
    uint start;
    // 管理期間(秒)
    uint period;
    // 管理開始費(wei)
    uint initialCost;
    // 管理中止費(wei)
    uint abortCost;
    // 管理者自身で実行する操作1回あたりの手数料(wei)
    uint execCost;
    // 1回あたりの最大延長期間(秒)
    uint maxExtent;
}

コントラクトの所有者は常に被管理者となり、コントラクトのデプロイおよびほとんどの処理で自らgasを負担する。

初期状態では管理者と被管理者が同一であり、これにより管理者が存在しない状態を表現する。すなわち、このコントラクトでは 自己管理は管理ではない ということになる。

// コントラクトの所有者(=被管理者)
address payable owner;

constructor() payable {
    owner = payable(msg.sender);
    m.master = payable(msg.sender);
}

function isUnderManagement() public view returns(bool) {
    return m.master != owner;
}

function reset() internal onlyWithMaster onlyOwnerOrMaster {
    m = Management(owner, 0, 0, 0, 0, 0, 0);
}
管理開始

射精管理は常に被管理者と管理者の合意の上で開始できる。コントラクト上では、新たな管理者から受け取った管理情報に対する署名を、被管理者が管理開始関数に渡すことで表現する。

管理情報は、コントラクトで保持する管理詳細情報と、管理開始時点でコントラクトに預けられているべき最小残高を含んでいる。開始時点で最小残高に満たなければ、管理を開始することはできない。コントラクトの最小残高は、管理開始時点での管理者による管理期間の追加可能回数を示している。

前述の通り、被管理者自身を管理者として設定することはできない。

// 開始署名のリプレイ攻撃を防ぐ nonce の使用済みリスト
mapping(uint => bool) usedNonces;

// 管理開始
function start(
    address payable _master, uint _initialCost, uint _period,
    uint _abortCost, uint _execCost, uint _maxExtent,
    uint minBalance, uint nonce, bytes memory signature
) public payable onlyWithoutMaster onlyOwner {
    // 残高 >= 最小残高 >= 初期費用
    require(
        minBalance >= _initialCost,
        "must be minBalance >= _initialCost"
    );
    require(
        address(this).balance >= minBalance,
        "must deposit at least minBalance"
    );

    require(_master != owner, "new _master must not be owner");

    require(!usedNonces[nonce], "this nonce is already used");
    usedNonces[nonce] = true;

    bytes32 message = prefixed(
        keccak256(
            abi.encodePacked(
                _initialCost, _period, _abortCost, _execCost, _maxExtent,
                minBalance, nonce, this
            )
        )
    );
    require(
        recoverSigner(message, signature) == _master,
        "signer is not new _master"
    );

    m.master = _master;
    m.start = block.timestamp;
    m.initialCost = _initialCost;
    m.period = _period;
    m.abortCost = _abortCost;
    m.execCost = _execCost;
    m.maxExtent = _maxExtent;

    acquire(m.initialCost);
}
管理期間の追加

管理期間の増減は、管理者が効率的に上下関係を構築するための重要な権利である。そのため、コントラクト上で管理者による自由な管理期間の追加および中止をサポートする。

ただし、悪意ある管理者が被管理者の意図に反して(半)永久にロックすることを防ぐため、管理期間の追加やその回数にある程度の上限を設定すべきである。また、管理実行費を窃取するために管理期間の追加を濫発することを防ぐため、コントラクトの残高を必要最小限に保つ必要がある。

このコントラクトでは、管理期間の追加に限って被管理者自身でも行うことができる。

function extendPeriod(uint _extent) public onlyWithMaster onlyOwnerOrMaster {
    // 被管理者は上限なく期間を追加できる
    // 管理者は maxExtent を上限として追加できる
    require(
        isSentByOwner() || _extent <= m.maxExtent,
        "must be _extent <= maxExtent"
    );

    if (isSentByMaster()) {
        acquire(m.execCost);
    }
    m.period = m.period.add(_extent);
}
管理期間の終了に伴う・または管理者による管理情報のリセット

被管理者は、管理期間満了後にいつでもその管理情報を消去できる。一方、管理者は管理期間にかかわらずいつでもその管理情報を消去できる。管理者による射精許可を条件とした駆け引き、搾取などは常にコントラクト外で行われる。

function exit() public onlyWithMaster onlyOwnerOrMaster {
    // 管理者はいつでも管理を終了できる
    // 被管理者は管理期間満了後に管理を終了できる
    require(
        isSentByMaster() || !isOnPeriod(),
        "you cannot exit on management period"
    );

    if (isSentByMaster()) {
        acquire(m.execCost);
    }
    reset();
}
管理中止に伴う管理情報のリセット

やむを得ない事情で管理を中止しなければならない場合は、事前に取り決めた管理中止費を支払って被管理者自身で管理情報をリセットする。管理を中止しても、これまで管理者に支払われた費用を取り戻すことはできず、あくまで緊急的な措置としての効果しかない。

前述の通り、管理者自身が管理を中止したい場合はexitを用いる。

function abort() public payable onlyWithMaster onlyOnPeriod onlyOwner {
    acquire(m.abortCost);
    reset();
}
支払い処理

悪意ある管理者による永久ロックを防ぐため、withdrawalパターンで支払いと引き出しを分割する。

まず、支払われた費用を後から引き出すために、コントラクトで誰にいくら支払ったかを記録しておく必要がある。

// 管理者が受け取る金額のリスト
mapping(address => uint) payouts;
// 全ての管理者が受け取る金額の合計
uint payoutTotal;

支払い時は即座にtransfterせず、単にコントラクトの残高をロックすることで引き出しに備える。

function acquire(uint amount) internal onlyWithMaster onlyOwnerOrMaster {
    require(
        payoutTotal.add(amount) <= address(this).balance,
        "must be payoutTotal + amount <= balance"
    );
    payouts[m.master] = payouts[m.master].add(amount);
    payoutTotal = payoutTotal.add(amount);
}

ロックされた残高はいつでも引き出すことができる。被管理者の手で 差し出させる か、管理者自身がgasを払って引き出すことができるが、後者が必要なのは被管理者が連絡を絶ったなど緊急の状況に限られるだろう。

function withdraw(address payable receiver) public {
    // 被管理者は任意の管理者の管理手数料を送金できる
    // 管理者は自らに支払われた管理手数料を送金できる
    require(
        isSentByOwner() || receiver == msg.sender,
        "you can withdraw only your own balance"
    );

    require(payouts[receiver] > 0, "receiver not found");

    uint amount = payouts[receiver];
    delete payouts[receiver];
    payoutTotal = payoutTotal.sub(amount);
    receiver.transfer(amount);
}

被管理者は、非管理下の場合に限ってロックされていない残高を取り戻すことができる。管理下で残高を引き出すことができないのは、コントラクトの残高が被管理者に突きつけられた銃の残弾数、つまり管理者自身による管理期間延長可能回数と同義のためである。

function refund() public onlyWithoutMaster onlyOwner {
    uint amount = address(this).balance.sub(payoutTotal);
    owner.transfer(amount);
}

同様に、非管理下でも管理手数料が全て払い出されていない場合はselfdestructできない。

function kill() public onlyWithoutMaster onlyOwner {
    require(
        payoutTotal == 0,
        "must be called after all the management costs are payed out"
    );
    selfdestruct(owner);
}

射精管理サーバ

単純化のため、与えられたコントラクトが管理下になければ固定の番号を返すだけのサーバを考える。

router.post('/unlock', async function(req, res) {
  try {
    console.log(req.body);
    const contract = new web3.eth.Contract(abi, req.body.address);
    if (await contract.methods.isUnderManagement().call()) {
      res.send('your padlock number is 1234');
      return;
    }
    res.send('not allowed');
  } catch (e) {
    console.error(e);
    res.send('error');
  }
});

動作確認

README.mdの通りにdocker-composeで各サーバを立ち上げ、Ganache上でコントラクトのデプロイを行った。

  • 被管理者 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 (100 ETH)
  • 管理者 0xf741496DA568BBd24924b49D15644b5adacA5C77 (100 ETH)
    • 秘密鍵: 0xb61517e265dbfdef2dcb4bd36025c143093c8a91adb79fbb29a46f2af5c0caf5
  • 射精管理コントラクト 0x3cd8792E537cd4D389Cb589E136800F3D59aE0e2

初期状態で解錠されていること

http://localhost:8000/owner/#0x3cd8792E537cd4D389Cb589E136800F3D59aE0e2 で初期表示および「保証金確認」を行った。

  1. 被管理者のアドレス: init(ok)> owner address: 0x3F43d089c033b3e7664e1B44334a0C2629f968e7
  2. コントラクトのアドレス: init(ok)> your contract address: 0x3cd8792E537cd4D389Cb589E136800F3D59aE0e2
  3. 被管理者の残高1: checkBalance(ok)> you have 99961167840000000000 wei
  4. ロック状態の確認: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is unlocked (server said: your padlock number is 1234)
  5. コントラクト上の残高2: checkDeposit(ok)> deposit on contract: 0 wei

管理者が管理開始用の署名を生成できること

http://localhost:8000/master/#0x3cd8792E537cd4D389Cb589E136800F3D59aE0e2 で署名を生成した。

1 ETH = 1000000000000000000 weiより、設定値は以下の通りである。

  • 管理開始費: 1 ETH (≒374656 JPY)
  • 管理中止費: 10 ETH (≒3746560 JPY)
  • 管理手数料: 0.1 ETH (≒37466 JPY)
  • 最小残高: 2 ETH (≒749312 JPY)

_GUIにおけるgenerateSignatureのパラメータ入力例

{
  "master": "0xf741496DA568BBd24924b49D15644b5adacA5C77",
  "period": "100",
  "initialCost": "1000000000000000000",
  "abortCost": "10000000000000000000",
  "execCost": "100000000000000000",
  "maxExtent": "100",
  "minBalance": "2000000000000000000",
  "nonce": "1378399312",
  "signature":"0xc9089f53a76352d460517e1ae5bfed3296428e33dbc11bd67f786068a0fcee79225e5b98be90c89e1df4bd476a1ce3bad2220b0ac1d7066a9e59259bc592586e1c"
}

被管理者が署名を使って管理を開始できること

_GUIにおけるstartのパラメータ入力例

  1. 管理開始前: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is unlocked (server said: your padlock number is 1234)
  2. 「管理開始」呼び出し: start(ok)> 0xec99c9360ed2465ce78e9913e5b0f58d758f994f3847b12d05b84cd5f7049431
  3. 管理開始後: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is locked by 0xf741496DA568BBd24924b49D15644b5adacA5C77 until 2021-10-02T16:09:07.000Z (server said: not allowed)

管理期間中に被管理者が管理を終了できないこと

管理期間中は被管理者から「管理終了」を使用できない。

  1. ロック状態の確認: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is locked by 0xf741496DA568BBd24924b49D15644b5adacA5C77 until 2021-10-02T16:09:07.000Z (server said: not allowed)
  2. 「管理終了」失敗: exit(error)> Error: Returned error: VM Exception while processing transaction: revert you cannot exit on management period

管理状態で管理期間を延長できること

被管理者および管理者が「100秒延長」を行った。

被管理者
  1. 管理期間の確認: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is locked by 0xf741496DA568BBd24924b49D15644b5adacA5C77 until 2021-10-02T16:09:07.000Z (server said: not allowed)
  2. 「100秒延長」呼び出し: extendPeriod(ok)> 0xe764b02686fadd7d381d9f1f16cee9815524cc134d2f5ba555bc203c56cfe1fa
  3. 管理期間の確認(+100s): checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is locked by 0xf741496DA568BBd24924b49D15644b5adacA5C77 until 2021-10-02T16:10:47.000Z (server said: not allowed)
管理者
  1. 管理期間の確認: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is locked by 0xf741496DA568BBd24924b49D15644b5adacA5C77 until 2021-10-02T16:10:47.000Z (server said: not allowed)
  2. 「100秒延長」呼び出し: extendPeriod(ok)> 0x227db532cb06e875e62f6c8139f9f44072833a2531f1114dc2e474f560833b18
  3. 管理期間の確認(+100s): checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is locked by 0xf741496DA568BBd24924b49D15644b5adacA5C77 until 2021-10-02T16:12:27.000Z (server said: not allowed)

管理期間終了後に被管理者が管理を終了できること

被管理者が「管理終了」を行った。

  1. 管理終了前: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is locked by 0xf741496DA568BBd24924b49D15644b5adacA5C77 until 2021-10-02T16:12:27.000Z (server said: not allowed)
  2. 「管理終了」呼び出し: exit(ok)> 0xb8ebd1de47cd3e6e82314f5817d87f58634bd74976a395ae866e383b3cd28d9e
  3. 管理終了後: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is unlocked (server said: your padlock number is 1234)

被管理者が管理者に管理費を送金できること

被管理者が「管理費を送金」を行った。

  1. 管理費送金前: checkDeposit(ok)> deposit on contract: 3000000000000000000 wei
  2. 「管理費を送金」呼び出し: withdraw(ok)> 0xaeb28acb2b8055a3ffd16cb37df8ab337ee8ef1d19a2ea89d8b3d5c20bb9095b
  3. 管理費送金後(-1.1 ETH): checkDeposit(ok)> deposit on contract: 1900000000000000000 wei

内訳は管理開始費(1 ETH)と管理期間延長時の管理手数料(0.1 ETH)である。

被管理者が管理を中止できること

被管理者が「管理中止」を行った。

  1. 管理中止前: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is locked by 0xf741496DA568BBd24924b49D15644b5adacA5C77 until 2021-10-02T16:17:47.000Z (server said: not allowed)
  2. 管理費送金前: checkDeposit(ok)> deposit on contract: 12900000000000000000 wei
  3. 「管理中止」呼び出し: abort(ok)> 0x7cb82cb7c987a2d10faa97ec577083a13034b864f72c9d6c8ca143e6c5ab7853
  4. 「管理費を送金」呼び出しwithdraw(ok)> 0x72c0de005b3c3c27474c99b13824eb9a0d42076e50ee30ce76437c1b2a127fc1
  5. 管理費送金後(-11 ETH): checkDeposit(ok)> deposit on contract: 1900000000000000000 wei
  6. 管理中止後: checkLocked(ok)> 0x3F43d089c033b3e7664e1B44334a0C2629f968e7 is unlocked (server said: your padlock number is 1234)

内訳は管理開始費(1 ETH)と管理中止費(10 ETH)である。

今後の展望

  • MetaMaskなどを用いたより便利で実用的な実装の提供
  • メタトランザクションを用いたコストの削減
  • 代行手数料の導入による射精管理サーバの収益化
  • 支払いにステーブルコインを用いた管理費の安定化
  • 自らEthereumネットワークを監視する非中央集権型射精管理デバイスの開発
  • stewpot氏によるスマートコントラクト射精管理のテキスト執筆

  1. 初期状態は100 ETHだが、コントラクトのデプロイにgasを使用したので少し減った。 

  2. コントラクトに送金することで保証金を追加できる。 

「読んだ」を押すと、あなたがボタンを押した事実を明示的に通知してこのページに戻ります。
このページに戻ってからブラウザの「戻る」ボタンを押すと、何度か同じページが表示されることがあります。