やる気
Ethereumネットワークコントラクトは不変です。ネットワーク(ブロックチェーン)にアップロードすると、変更できません。 ビジネスや開発の詳細にはコードの更新が必要になる場合がありますが、従来のアプローチではこれが問題になります。
アップグレードする一般的な理由
- コードエラー
- ビジネス要件の変更
- 契約作業の変更に関するコミュニティの提案の受け入れ
技術的解決策の説明
必要な機能の実装-コードを更新し、コードをコンポーネントに分割することで計画されます:
- データ -ロジックのないスマートコントラクトとデータストレージ専用のスペースを提供します。
- ビジネスロジック -ストレージからデータを抽出するロジックとその変更を記述するスマートコントラクト。
- 入り口 -不変の契約はビジネスロジックの更新の記録を保持し、現在のビジネスロジック契約へのリンクをエンドユーザーに提供します
更新されたスマートカウンター契約
現実と離婚した抽象的な例-ズームロジックが更新されたカウンターを想像してください。
- ステージ1.呼び出しごとに、カウンターは1ずつ増加します。
- ステージ2.呼び出しごとに、カウンターが10ずつ増加します。
従来のアプローチとすべてのステージの初期知識では、現在のステージを明示的に示すカウンター内のフィールドを作成する必要があります。たとえば、uint public currentState。 カウンターインクリメントメソッドが呼び出されるたびに、現在のステージがチェックされ、それに関連付けられたコードが実行されます。
function increaseCounter() public returns (uint) { if (currentState == 0) { value = value + 1; } else if (currentState == 1) { value = value + 10; } return value; }
更新された契約の機能を明確に示すために、記事の最後にその条件をまだ知らず、説明していない第3段階があることに同意します。
保管
現在のカウンター値を保存し、ビジネスロジックから分離されたデータレイヤーを実装するには、コントラクトを作成します~/contracts/base/UIntStorage.sol
:
pragma solidity ^0.4.18; contract UIntStorage { uint private value; function setValue(uint _value) external returns (uint) { value = _value; return value; } function getValue() external view returns (uint) { return value; } }
コントラクトの名前と実装が示すように、リポジトリはその使用方法について何も知らず、 uint private value
フィールドをカプセル化するタスクを実行します
ビジネスロジック
ビジネスロジックとのやり取りは、カウンターを増やして現在の値を取得するための2つのメソッド、 increaseCounter
およびgetCounter
を介して実行されることに同意します。
pragma solidity ^0.4.18; interface ICounter { function increaseCounter() public returns (uint); function getCounter() public view returns (uint); }
次に、 ICounter
インターフェイスを実装し、前述のストレージを使用する最初の段階からのビジネスロジックのスマートコントラクトについて説明しますICounter
~/contracts/examples/counter/IncrementCounter.sol
:
pragma solidity ^0.4.18; import "./ICounter.sol"; import "../../base/UIntStorage.sol"; contract IncrementCounter is ICounter { UIntStorage public counter; function IncrementCounter(address _storage) public { counter = UIntStorage(_storage); } function increaseCounter() public returns (uint) { return counter.setValue(getCounter() + 1); } function getCounter() public view returns (uint) { return counter.getValue(); } }
IncrementCounter
は、リポジトリへの参照を除いて、内部状態がない(データを保存しない) IncrementCounter
注意することが重要です。
ストレージリンクを最初の引数として、 increaseCounter
メソッドとgetCounter
メソッドに渡すことに同意する場合、ステートレスビジネスロジックを実装できます。
~/contracts/examples/counter/ICounter.sol
変更を加えます。
pragma solidity ^0.4.18; interface ICounter { function increaseCounter(address _storage) public returns (uint); function getCounter(address _storage) public view returns (uint); function validateStorage(address _storage) public view returns (bool); }
現在、ビジネスロジックメソッドは、ストアを参照する最初の引数を待機し、ストアの有効性をチェックするメソッドvalidateStorage(address _storage)
も実装していvalidateStorage(address _storage)
最初の段階の実装に変更を加えます~/contracts/examples/counter/IncrementCounter.sol
:
ソースURL
pragma solidity ^0.4.18; import "./ICounter.sol"; import "../../base/UIntStorage.sol"; contract IncrementCounter is ICounter { modifier validStorage(address _storage) { require(validateStorage(_storage)); _; } function increaseCounter(address _storage) validStorage(_storage) public returns (uint) { UIntStorage counter = UIntStorage(_storage); require(counter.isUIntStorage()); return counter.setValue(counter.getValue() + 1); } function getCounter(address _storage) validStorage(_storage) public view returns (uint) { UIntStorage counter = UIntStorage(_storage); require(counter.isUIntStorage()); return counter.getValue(); } function validateStorage(address _storage) public view returns (bool) { return UIntStorage(_storage).isUIntStorage(); } }
次の段階に進み、ビジネスロジックコントラクトを更新する前に、いくつかのテストを作成し、ビジネスロジックが計画どおりに機能することを確認します。
テスト中
このリポジトリはTruffleフレームワークのプロジェクトであり、テストに便利な機能を提供します: truffle test
。
テストの作成プロセスについては詳しく説明しませんが、このトピックに興味がある場合は、telegram @alerdenisov宛にメールをください。テストプラクティスのベストプラクティスに関する記事を作成します。
~/test/IncrementCounter.test.js
:
import expectThrow from './utils/expectThrow' const IncrementCounter = artifacts.require('./IncrementCounter.sol') const UIntStorage = artifacts.require('./UIntStorage.sol') const BoolStorage = artifacts.require('./BoolStorage.sol') contract('IncrementCounter', ([owner, user]) => { let counter, storage, fakeStorage before(async () => { storage = await UIntStorage.new() fakeStorage = await BoolStorage.new() counter = await IncrementCounter.new() }) it('Should receive 0 at begin', async () => { const currentValue = await counter.getCounter(storage.address) assert(currentValue.eq(0), `Uxpected counter value: ${currentValue.toString(10)}`) }) it('Should increase value on 1', async () => { await counter.increaseCounter(storage.address) const newValue = await counter.getCounter(storage.address) assert(newValue.eq(1), `Unxpected counter value: ${newValue.toString(10)}`) }) it('Should store 1 after increment', async () => { const storedValue = await storage.getValue() assert(storedValue.eq(1), `Unxpected stored value: ${storedValue.toString(10)}`) }) it('Should validate storage', async () => { await counter.validateStorage(storage.address) }) it('Should unvalidate fake storage', async () => { await expectThrow(counter.validateStorage(fakeStorage.address)) }) })
テストを実行すると、すべてが「正常」であることが示されます。
Contract: IncrementCounter ✓ Should receive 0 at begin ✓ Should increase value on 1 (63ms) ✓ Should store 1 after increment ✓ Should validate storage ✓ Should unvalidate fake storage 5 passing (301 ms)
しかし、実際にはそうではありません。 リポジトリとの「許可されていない」対話の中間テストを追加しましょう。
it('Should prevent non-authenticated write', async () => { await expectThrow(storage.setValue(100)) })
Contract: IncrementCounter ✓ Should receive 0 at begin ✓ Should increase value on 1 (58ms) 1) Should prevent non-authenticated write 2) Should store 1 after increment ✓ Should validate storage ✓ Should unvalidate fake storage 4 passing (330ms) 2 failing
Vaultの所有権
現在のソリューションの問題は、リポジトリが何らかの方法でレコードを制限せず、攻撃者がカウンター契約のビジネスロジックを無視してリポジトリ内のデータを変更できることです。
スマートコントラクトの主な利点は、スマートコントラクトの宣言以外の方法でデータ(状態)が変更されないことを交換の参加者に保証することです。 しかし、変更は何によっても制限されません。
タスクは 、ビジネスロジックコントラクトにのみ関連するリポジトリを作成することです。
リポジトリとの相互作用を明示的に制限するには、 zeppelin-solidity
のOwnable
パターンを使用します(パターンの詳細については、フレームワークのドキュメントを参照してください)。
Ownable
コントラクトからストレージOwnable
、 onlyOwner
修飾子をsetValue()
メソッドに追加しsetValue()
。
pragma solidity ^0.4.18; import "zeppelin-solidity/contracts/ownership/Ownable.sol"; contract UIntStorage is Ownable { uint private value; function setValue(uint _value) onlyOwner external returns (uint) { value = _value; return value; } function getValue() external view returns (uint) { return value; } function isUIntStorage() external pure returns (bool) { return true; } }
おめでとうございます、今ではストアの関連所有者のみがストアに書き込むことができます! 6つのテストのうち3つが失敗しました! テストでストレージ管理をビジネスロジックに「手動で」渡してみましょう。
before(async () => { storage = await UIntStorage.new() fakeStorage = await BoolStorage.new() counter = await IncrementCounter.new() await storage.transferOwnership(counter.address) })
これですべてのテストに合格しましたが、2番目の質問が発生します:「ビジネスロジックを更新するときにストレージの所有権を管理する方法」
ゼネラルコントローラー
共通コントローラーを実装する前に、もう1つのカウンターコントラクトを作成しますが、2番目の段階では~/contracts/examples/counter/IncrementCounterPhaseTwo.sol
:
pragma solidity ^0.4.18; import "./IncrementCounter.sol"; contract IncrementCounterPhaseTwo is IncrementCounter { function increaseCounter(address _storage) validStorage(_storage) public returns (uint) { UIntStorage counter = UIntStorage(_storage); return counter.setValue(counter.getValue() + 10); } }
カウンターとOwnable
リポジトリの2つの実装ができたので、リポジトリの別の管理を提供するために、何らかの方法で1つの実装を「尋ねる」必要があることが明らかになります。 transferStorage(address _storage, address _counter)
メソッドtransferStorage(address _storage, address _counter)
をカウンターインターフェイスに追加しtransferStorage(address _storage, address _counter)
~/contracts/examples/counter/ICounter.sol
:
pragma solidity ^0.4.18; interface ICounter { function increaseCounter(address _storage) public returns (uint); function getCounter(address _storage) public view returns (uint); function validateStorage(address _storage) public view returns (bool); function transferStorage(address _storage, address _counter) public returns (bool); }
ICounter
の最終実装では、 transferStorage
メソッドを呼び出した後、 _counter
パラメーターに渡されたアドレスにストレージの制御を与える必要があることに_counter
ます。
function transferStorage(address _storage, address _counter) validStorage(_storage) public returns (bool) { return UIntStorage(_storage).transferOwnership(_counter); }
新しいロジックに権利を転送するためのテストを追加し、ロジックを変更した後、 increaseCounter
メソッドの結果を確認しましょう。
it('Should transfer ownership', async () => { await counter.transferStorage(storage.address, secondCounter.address); }) it('Should reject increase from outdated counter', async () => { await expectThrow(counter.increaseCounter(storage.address)); }) it('Should increase counter with new logic', async () => { await secondCounter.increaseCounter(storage.address) const newValue = await secondCounter.getCounter(storage.address) assert(newValue.eq(11), `Unxpected counter value: ${newValue.toString(10)}`) })
テストを実行すると、すべてが機能しているという誤った感覚を感じることがあります。
Contract: IncrementCounter ✓ Should receive 0 at begin ✓ Should increase value on 1 (75ms) ✓ Should prevent non-authenticated write ✓ Should store 1 after increment ✓ Should validate storage ✓ Should unvalidate fake storage ✓ Should transfer ownership ✓ Should reject increase from outdated counter ✓ Should increase counter with new logic (47ms) 9 passing (500ms)
しかし、私はあなたを怒らせるために急いで、これらの変更は再び攻撃者に青信号を開きました:
it('Should reject non-authenticated transfer storage', async () => { await expectThrow(secondCounter.transferStorage(storage.address, user, { from: user })) }) it('Should reject increase from user fron previous test', async () => { await expectThrow(storage.setValue(100500, { from: user })) }) it('Should store 11 as before', async () => { const storedValue = await storage.getValue() assert(storedValue.eq(11), `Unxpected stored value: ${storedValue.toString(10)}`) })
Contract: IncrementCounter ✓ Should receive 0 at begin (46ms) ✓ Should increase value on 1 (55ms) ✓ Should prevent non-authenticated write ✓ Should store 1 after increment ✓ Should validate storage ✓ Should unvalidate fake storage ✓ Should transfer ownership ✓ Should reject increase from outdated counter ✓ Should increase counter with new logic (46ms) 1) Should reject non-authenticated transfer storage 2) Should reject increase from user fron previous test 3) Should store 11 as before 9 passing (611ms) 3 failing
ゼネラルコントローラーの主なタスクは、権利の譲渡を管理し、このプロセスをだれも防ぎます。 まず、 UIntStorage
に似たIncrementCounter
変更してOwnable
ロジックもOwnable
し、ストレージとの相互作用を制限します。
pragma solidity ^0.4.18; import "./ICounter.sol"; import "../../base/UIntStorage.sol"; contract IncrementCounter is ICounter, Ownable { modifier validStorage(address _storage) { require(validateStorage(_storage)); _; } function increaseCounter(address _storage) onlyOwner validStorage(_storage) public returns (uint) { UIntStorage counter = UIntStorage(_storage); require(counter.isUIntStorage()); return counter.setValue(counter.getValue() + 1); } function getCounter(address _storage) validStorage(_storage) public view returns (uint) { UIntStorage counter = UIntStorage(_storage); require(counter.isUIntStorage()); return counter.getValue(); } function validateStorage(address _storage) public view returns (bool) { return UIntStorage(_storage).isUIntStorage(); } function transferStorage(address _storage, address _counter) onlyOwner validStorage(_storage) public returns (bool) { UIntStorage(_storage).transferOwnership(_counter); return true; } }
コントローラーの実装を進めましょう。 コントローラーの基本要件:
1)カウンターの現在の実装の説明
2)カウンターの実装を更新する
2)実装を更新する際のストレージ権の譲渡
3)拒否は不正な更新の実装を試みます
~/contracts/examples/counter/CounterContrller.sol
:
pragma solidity ^0.4.18; import "zeppelin-solidity/contracts/ownership/Ownable.sol"; import "./ICounter.sol"; import "../../base/UIntStorage.sol"; contract CounterController is Ownable { UIntStorage public store = new UIntStorage(); ICounter public counter; event CounterUpdate(address previousCounter, address nextCounter); function updateCounter(address _counter) onlyOwner public returns (bool) { if (address(counter) != 0x0) { counter.transferStorage(store, _counter); } else { store.transferOwnership(_counter); } CounterUpdate(counter, _counter); counter = ICounter(_counter); } function increaseCounter() public returns (uint) { return counter.increaseCounter(store); } function getCounter() public view returns (uint) { return counter.getCounter(store); } }
ICounter
とgetCounter
、 ICounter
現在の実装で同様のメソッドと対話する外部メソッドにすぎません。 すべてのコントローラーロジックは、 updateCounter(address _counter)
という小さなメソッドです。
updateCounter
メソッドは、カウンター実装のアドレスを受け入れ、それを新しいカウンター実装のアドレスとして設定しますか? (状態に応じて、自分自身または前のストレージから)ストレージの権利を彼に譲渡します。
第三段階を覚えていますか? 特に2行目とは1行だけ異なるため、実装のコードは省略します。 第3段階では、カウンターはそれ自体で乗算することで値を増やします: value = value * value
。
いくつかのテストを書いて、コントローラーが機能し、それに割り当てられたタスクを実行することを確認しましょう:
import expectThrow from './utils/expectThrow' const IncrementCounter = artifacts.require('./IncrementCounter.sol') const IncrementCounterPhaseTwo = artifacts.require('./IncrementCounterPhaseTwo.sol') const MultiplyCounterPhaseThree = artifacts.require('./MultiplyCounterPhaseThree.sol') const CounterController = artifacts.require('./CounterController.sol') const UIntStorage = artifacts.require('./UIntStorage.sol') contract('CounterController', ([owner, user]) => { let controller, counterOne, counterTwo, counterThree, storage before(async () => { controller = await CounterController.new() storage = UIntStorage.at(await controller.store()) counterOne = await IncrementCounter.new() counterTwo = await IncrementCounterPhaseTwo.new() counterThree = await MultiplyCounterPhaseThree.new() await counterOne.transferOwnership(controller.address) await counterTwo.transferOwnership(controller.address) await counterThree.transferOwnership(controller.address) }) it('Shoult create storage', async () => { assert(await storage.isUIntStorage(), 'Controller doesn\'t create proper storage') }) it('Should change counter implementation', async () => { await controller.updateCounter(counterOne.address) assert(await controller.counter() === counterOne.address, `Unxpected counter in controller (${await controller.counter()} but expect ${counterOne.address})`) }) it('Should increase counter on 1', async () => { await controller.increaseCounter() const value = await controller.getCounter() assert(value.eq(1), `Unxpected counter value: ${value.toString(10)}`) }) it('Should update counter', async () => { await controller.updateCounter(counterTwo.address) assert(await controller.counter() === counterTwo.address, `Unxpected counter in controller (${await controller.counter()} but expect ${counterTwo.address})`) }) it('Should increase counter on 10 after update', async () => { await controller.increaseCounter() const value = await controller.getCounter() assert(value.eq(11), `Unxpected counter value: ${value.toString(10)}`) }) it('Should reject non-authenticated update', async () => { await expectThrow(controller.updateCounter(counterTwo.address, { from: user })) }) it('Should update on phase three and increase counter to 11*11 after execution', async () => { await controller.updateCounter(counterThree.address) await controller.increaseCounter() const value = await controller.getCounter() assert(value.eq(121), `Unxpected counter value: ${value.toString(10)}`) }) })
Contract: CounterController ✓ Shoult create storage ✓ Should change counter implementation (53ms) ✓ Should increase counter on 1 (52ms) ✓ Should update counter (55ms) ✓ Should increase counter on 10 after update (56ms) ✓ Should reject non-authenticated update ✓ Should update on phase three and increase counter to 11*11 after execution (89ms) 7 passing (684ms)
ご覧のとおり、コントローラーがタスクを実行し、カウンターのコードが更新されました。
まとめ
例の抽象性(および不条理)にもかかわらず、このアプローチは実際の契約に適用できます。 たとえば、Evogameプロジェクトのゲームロジックの更新を確実にするために、モンスターカード、バトルロジックなどを実装する契約でこのアプローチを使用します。
しかし、このアプローチには多くの重大な欠点とコメントがあります。
- トランザクションのコスト (消費されるガスの量)は増加しますが、大幅には増加しません。 計算をしたい人がいたら、近いうちに感謝するか期待しています。
- 管理者の役割が表示されますが、コントローラーへの権限を、更新を受け入れるための分散型投票のスマートコントラクトに転送することによって決定されます
- 設計の複雑さ 、1つのモノリシックコンテキストでのコードの記述は何倍も単純であり、データとメッセージフローへの注意をあまり必要としません。 ステートレスを実装するには、開発者からさらに注意が必要です。
delegatecall
介して実装を呼び出すことで解決します。delegatecall
介した状態の転送を継続する必要がある場合は、私に連絡してください。
UPD:
電報のディスカッションの@dzentotaは欠陥を指摘しましたisUIntStorage()
追加呼び出しです。 (バグ修正)[ https://github.com/alerdenisov/upgradable-contracts/blob/master/contracts/examples/counter/IncrementCounter.sol ]