女子高生になりたい

はるみちゃんのブログだよ。えへへ。

Plasmaについてチームで共有したのでその時の資料を共有します

Plasma全然わかんないんですけど、チームでPlasmaについて少し話したのでその時の資料を共有します。

間違っている部分とか結構あると思うんで、なにかあればTwitterにDMなど貰えると嬉しいです。 さかた(JK) (@a1234224576a) | Twitter

speakerdeck.com

うーん、難しいなあ・・ PlasmaCash、Debit、XTあたりはもっとちゃんとEthereumResearch追って理解してきます。

CognitoIDPool+CognitoUserPool+外部IDProviderで独自認証とSNS認証を実装し、アカウントに対してDynamoDBの行レベルアクセス制限をかける

内容

メールアドレス+パスワード認証、Facebook認証によりサインインし、AWSサービスの一時的なアクセス権限を取得する。

それを使って、DynamoDBにアクセスしてデータを取得する。

ただし、DynamoDBには自分のIDに該当するデータしか取得することはできないように制限する。

以上を実現しました。

用語

CognitoIDプール(フェデレーティッドアイデンティティ

ユーザに対してユニークなIDを生成する。

このIDにより、権限が制限された一時的なAWS認証情報を取得して、別のAWSサービスにアクセスすることができる

CognitoUserプール

ユーザのサインイン、サインアップなどの機能を提供する。

多要素認証や二段階認証、プロフィール管理など便利な機能が多い。

IDプール外部IDプロバイダー(今回はFacebook

CognitoIDプールから得るユニークなIDをFacebookなどのソーシャルアカウント認証に基づいて取得するもの

サンプルアプリのリポジトリ

実装したコードは以下に置いてあります。

Info.plistにあるFacebookAppID/FacebookDisplayNameとConstant.swiftにある定数は自分の環境のものに書き換えてください。

github.com

ロジックフロー

具体的な実装に関してはリポジトリを参照してください。

(1) LoggedInViewControllerが生成される

storyBoardからrootViewControllerに指定してあるLoggedInViewControllerがまずはインスタンス化されて画面に表示されます


(2) LoggedInViewControllerのviewDidLoadで「CognitoClient.sharedInstance.credentialProvider.getIdentityId()」がcallされる

refreshメソッドの中の「CognitoClient.sharedInstance.fetchId()」は内部で.credentialProvider.getIdentityIdをcallしています。


(3)MyIdProbiderの「logins」がcallされる

.credentialProvider.getIdentityIdがcallされると、内部でAWSIdentityProviderManagerの.loginsがcallされます。

ここではじめて、どのアカウントでもサインインしていない事が発覚します。(サインインしている場合はIDが返されます)


(4)ログイン画面が表示される

どのアカウントでもサインインしていない場合、loginsの下の方にある「.userpool.currentUser()?.getDetails()」がcallされます。

これにより、サインインしていない場合は「AWSCognitoIdentityInteractiveAuthenticationDelegateのstartPasswordAuthentication()」がcallされます。

AWSCognitoIdentityInteractiveAuthenticationDelegateは今回AppDelegate内で準拠しています。

デリゲートの設定はCognitoClient.swiftから行なっていますので参照してみてください。


(5)ログインに成功するとdismissし、LoggedInViewControllerにIDが表示される

ログインに成功すると、AWSCognitoIdentityPasswordAuthenticationのdidCompleteStepWithErrorがcallされます。

この中でdismissとrefreshをしています。

ざっくりとした流れはこんな感じです。

LoggedInViewControllerのfetchボタンをタップすると、データが存在する場合は値を取得できるかと思います。

DynamoDBの行レベルアクセス制限を実現するためのIAMの設定

ドキュメント: Amazon DynamoDB: Amazon CognitoID に基づいた DynamoDB への行レベルのアクセスを許可する - AWS Identity and Access Management

IAM Management Consoleへ行き、新規でポリシーを作ります。名前は何でも構いません。

JSONタブを選択し、ドキュメントに書かれているように以下のJSONを記載し、作成します。
<TABLE-NAME>は書き換える)

ACCOUNTNUMBERはアカウント設定に書かれているアカウントIDの事です。

{
     "Version": "2012-10-17",
     "Statement": [
         {
             "Effect": "Allow",
             "Action": [
                 "dynamodb:DeleteItem",
                 "dynamodb:GetItem",
                 "dynamodb:PutItem",
                 "dynamodb:Query",
                 "dynamodb:UpdateItem"
             ],
             "Resource": [
                 "arn:aws:dynamodb:<REGION>:<ACCOUNTNUMBER>:table/<TABLE-NAME>"
             ],
             "Condition": {
                 "ForAllValues:StringEquals": {
                     "dynamodb:LeadingKeys": [
                         "${cognito-identity.amazonaws.com:sub}"
                     ]
                 }
             }
         }
     ]
 }

このポリシーをアタッチしていきます。

CognitoIDプールを作成したときに、「Cognito_****Auth_Role」といったロールが作成されていると思います。(デフォルト)

そこに先ほど作ったポリシーをアタッチします。


次に、DynamoDBにテーブルを作成します。この時テーブルの名前を先ほどポリシーを作る時に<TABLE-NAME>で指定した名前にすることを忘れないでください。

そして、パーティションキーにCognitoIDプールから得られるユーザのIDを入れます。これでユーザは自分のIDの行しか読み込むことができなくなります。

(サンプルアプリをそのまま動かすには、パーティションキーの名前を「userid」にして、その他のカラムは全てstring型にしておいてください)

AWS KeyManagementService

AWS KeyManagementServiceについて調査したので、そのときのメモ書きを置いておきます。

KeyManagementServiceというサービス単体で存在しているわけではないので、サービス検索でKMSなど入力しても出てこないので注意><

IAM→暗号化キーからKMSのコンソールにいくことができます。

What's KMS?

・データの暗号化に使用される暗号化キーの作成と管理を容易にするマネージド型サービス
AWS KMS で作成したマスターキーは、FIPS 140-2 検証済み暗号化モジュールによって保護
(FIPS・・連邦情報処理標準、FIPS140-2は暗号モジュールのためのセキュリティ要求)

KMSに統合されている他のAWSサービスの一覧はこちら。大体対応していることがわかります。
https://aws.amazon.com/jp/kms/details/#integration

マスターキーとデータキー

公開鍵暗号化方式ではないので、公開鍵/秘密鍵といった用語は出てきません。

データキーでデータの暗号化/復号化どちらも可能です。
そのデータキー自体を暗号化/復号化するためのものがマスターキーになります。

KMSではこのマスターキーを管理(作成、更新、無効化、有効化)する仕組みを提供しています。
マスターキーは自身のローカルストレージなどに保存されることはありません。常にKMSを経由して使用します。

データキーに関しましては、作成とデータキー自体の暗号化/復号化が可能です。
データキーは作成した段階で、暗号化済みデータキーと平文のデータキーがダウンロードされます。
平文データキーが漏れると誰でもデータを復号化できてしまいますので、暗号化処理が終わったら破棄するようにしましょう。
(逆に、暗号化済みデータキーを破棄するとデータを復号できなくなります)

How to use KMS?

KMS APIPython経由で叩いて動作を試してみます。

データキー(平文/暗号化済み)の作成

#coding: utf-8
import boto3

kms_client = boto3.client('kms')

key_id = 'arn:aws:kms:us-east-1:****:key/******'

response = kms_client.generate_data_key(
    KeyId=key_id,
    KeySpec='AES_256'
)

plaintext_key = response['Plaintext']

encrypted_key = response['CiphertextBlob']



key_idですが、ARNじゃなくてKeyIdそのものでも、エイリアス名でも良いらしいです。
(ただしエイリアス名の時はalias/をつける)

・・・すると、何かエラーが出ました。

botocore.errorfactory.NotFoundException: An error occurred (NotFoundException) when calling the GenerateDataKey operation: Invalid arn

マスターキーを作成したリージョンと作業中のリージョンが違うのが原因みたいです。
この点を修正すると、うまく通りました。

plaintext_key、encrypted_keyを出力すると、バイト列が出てきます。

暗号化/復号化

次に、平文のデータキーを使ってデータを暗号化して見ます。
Encrypt用のAPIが用意されているので、それを使って暗号化していきます。

response = kms_client.encrypt(
    KeyId=key_id,
    Plaintext=b'hoge_password'
)

encrypted = response['CiphertextBlob']

これで、暗号化された文字列を取得できます。

encryptedを複合してみます。

decryptResponse = kms_client.decrypt(
    CiphertextBlob=encrypted,
)

decrypted = decryptResponse['Plaintext']

暗号化文字列の中にメタデータとしてどのマスターキーが使われたかなどの情報があるようですね。
暗号化コンテキストが存在する場合は、復号化のときも同じコンテキストを指定しないと復号に失敗します。

DynamoDBとの連携

次は、DynamoDBに書き込むデータを暗号化してみます。

DynamoDBでテーブルを作る時に「保管時の暗号化」というオプションがあるので、これを有効にします。
こうすることで、該当テーブル内のすべて(テーブル、インデックス)のデータが暗号化されます。
また、全てのテーブルの暗号化で単一のサービスデフォルトキーを使用します。
DynamoDB ストリームのデータは暗号化されません。

この状態で適当なデータを書き込んで見ます。

import boto3
from boto3.dynamodb.conditions import Key, Attr
 
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('TestKMS')

with table.batch_writer() as batch:
    batch.put_item(
        Item={
            'id': '1',
            'password': 'hoge_password'
        }
    )
    batch.put_item(
        Item={
            'id': '2',
            'password': 'hoge_hoge_password'
        }
    )

scanしてみます。

response = table.scan()
print(response["Items"])
[{'id': '2', 'password': 'hoge_hoge_password'}, {'id': '1', 'password': 'hoge_password'}]

Boto3が暗黙的に復号してくれるようなので、様子はわからないですね。。

また、IAMの暗号化キーへ行ってみると「aws/dynamodb」というエイリアスがついたマスターキーが生成されていることがわかります。


全データだけでなく、特定カラムのみ暗号化させたい場合や、DynamoDB ストリームを使う場合、マスターキーをローテションさせたい場合などは直接暗号化済み文字列を流してやる必要があるようです。

uportを使ったデモアプリ、イーサリアム神社を公開しました

なかなかuportを活用したサービス、アプリは多くないと思います。

そこで今回、upotを体験できるようなデモアプリを作ってみました。

それが、イーサリアム神社です。

アプリ自体はとても単純なものです。

uportでログインし、etherを添えて神様にお祈りするだけ。

トランザクションの署名と神の声を聞くためにuportを活用します。


Ropstenネットワークで動作しています。

また、ボタンを教えてからuportが反応するまでにややラグがあることに注意です。



こちらから体験できます。よかったら遊んでみて下さい。 https://ethtemple.netlify.com/

f:id:sakata_harumi:20180815234640p:plain

github.com

uPort活用するためにとりあえずデモアプリを紐解く

uPortとは

uPortは個人に紐づく情報をブロックチェーン上に保存しておき、こちらが承認した相手に対してのみ一部を公開するといった仕組みを実現するものになります。

例えば、今までなら住所などの個人情報をAmazon楽天、その他ECサイト全てにおいて提供する必要がありましたが、uPortを介す事によって他社は個人情報を管理する必要がなくなります。

つまり、今までの「ECサイトに個人情報を登録しECサイト側が個人情報を管理」から、「ブロックチェーン上に個人情報を登録し、ECサイト側が必要な情報にアクセスする許可をユーザが出す」といった形に変わります。

既にスイスの一部ではこのuPortを利用した認証が活用されはじめているようですね。

consensysmediajapan.com

具体的な仕組みはどうなっているの、とかブロックチェーン上に個人情報を記録して大丈夫なの、とかそういった事は以下の記事が良くまとまっていて良さそうです。

zoom-blc.com

本記事では、仕組みではなく実際に実装していくにはどうしたら良いかをデモアプリから紐解いて行こうと思います。

以降は個人のスマートフォンにuPortのアプリがインストールされていることを前提とします。

デモアプリの起動→接続

uport-projectではuPortの動作を体験するデモアプリが公開されています。こちらを利用します。

github.com

https://demo.uport.meにアクセスすればすぐに体験できるのですが、これではコードをいじる事ができないのでローカルで起動しましょう。

$ git clone git@github.com:uport-project/demo.git
$ yarn install
$ npm run start

(10系のnodeを使っているとyarn installでこけたので、6.5まで下げました)

上記のコマンドを実行すると、localhost:3000でアプリが立ち上がったと思います。

アクセスすると以下のような画面が表示されます。

f:id:sakata_harumi:20180814144540p:plain

「Connect With uPort」をクリックするとQRコードが出るのでそれをスマホアプリのuPortで読み取ります。

以下のモーダルが出現し、どのような個人情報をリクエストしているのかがわかります。Continueを押して許可しましょう。

f:id:sakata_harumi:20180814144815p:plain

ここまでのコードを追っていきたいと思います。

まず、uPortの初期化はuportSetup.jsに記載されています。

import { Connect, SimpleSigner } from 'uport-connect'

const uport = new Connect('uPort Demo', {
  clientId: 'xxxxxxxxxxxxxxxxx',
  signer: SimpleSigner('xxxxxxxxxxxxx')
})

const web3 = uport.getWeb3()
export { web3, uport }

Connectオブジェクトを生成するときにどのネットワークへアクセスするかを決定しています。

clientIdとsignerはuPortのUport App Managerから生成します。デフォルトネットワークはrinkebyのようです。

次は、最初のViewであるcomponents/Welcome.jsです。

こちらはボタンを押すと以下の関数が呼ばれます。

connectUport () {
    uport.requestCredentials(
      { requested: ['name', 'phone', 'country', 'avatar'],
        notifications: true }
    ).then((credentials) => {
        console.log({credentials})
        this.props.actions.connectUport(credentials)
    })
  }

どうやら、uport.requestCredentialsをcallするとQRコードのモーダルが表示されるようです。 (この見た目を変えるために自分たちのURIをインジェクションすることもできます)

requestedで要求する情報を指定しているのがわかります。

この場合ですと、credentialsは以下のようなobjectが入ってきます。

{
    credentials: {
        address: "*****",
        avatar: {
            uri: "*****"
        },
        country: "*****",
        did: "*****"",
        name: "*****",
        networkAddress: "*****",
        phone: "*****",
        publicEncKey: "*****",
        publicKey: "*****",
        publishToken: "*****"
    }
}

要求した要素であるname,phone,country,avatarが返ってきているのがわかります。

ちなみに、存在しない要素をリクエストした場合はその要素は返ってきません。特にエラーになることはないのでクライアント側でうまくバリデーションが必要そうです。

トランザクションの発行

接続が完了すると、以下の画面に遷移します。

f:id:sakata_harumi:20180814153432p:plain

ここではスマートコントラクトに対してトランザクションを発行する事ができます。

トランザクションの送信に成功すると、Your current sharesがto Buyした数だけ増えることが確認できます。

Shares to Buyに適当な数値を入力して、「Buy Shares」を押してみましょう。

すると、アプリ側にトランザクションの送信許可を求めるモーダルが表示されます。

f:id:sakata_harumi:20180814153759p:plain

許可をしトランザクションが送信されブロックに取り込まれると、Your current sharesの数値が増えていると思います。

では、このあたりのコードを見ていきましょう。compenents/SignTransaction.jsです。

現在のshares数の取得

まず、現在のshares数を取得する部分を見ていきます。

getCurrentShares () {
  // TODO: Dump this check once MNID is default behavior
  const addr = checkAddressMNID(this.props.uport.networkAddress)
  const actions = this.props.actions
  getShares(addr, actions)
}

checkAddressMNIDですが、以下のようになっています。

const mnid = require('mnid')

function checkAddressMNID (addr) {
  if (mnid.isMNID(addr)) {
    return mnid.decode(addr).address
  } else {
    return addr
  }
}

まず、これは何でしょうか・・。

mnidはuport-projectの1つでリポジトリは↓になります。

github.com

どうやらmnidは「Multi Network Identifier」の略で、ネットワークが違う事が原因のアドレスの誤りを防ぐために存在しているようです。

例えば、ropstenテストネットワークに0xaaというアドレスがあったとして、そこに送金すると仮定します。

この時、誤ってmain-netの0xaaに向かって送信してしまうと、ETHが消失してしまいます。

これを防ぐ仕組みがmnidです。やっていることはnetworkIdとaddressをまとめてエンコーディングするだけです。

mnid.encode({
  network: '0x1',
  address: '0x00521965e7bd230323c423d96c657db5b79d099f'
})

//return: '2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX'

mnidをデコードするとnetworkIdとaddressが手に入りますので、networkIdがコントラクトを実行しているネットワークと一致するかどうかを検証すれば間違った送信を防ぐことができます。

ただこのアプリでは、単に入力された値がmnidだったらそこからアドレスを取り出しているだけで、ネットワークIDの検証はしていないようです。

さて、本題のgetShares(addr, actions)ですがこの関数の中身は以下です(一部抜粋)。SharesContractを呼び出しているだけですね。

SharesContract.getShares.call(addr, (error, sharesNumber) => { 数値をセット});

SharesContractは以下でセットアップしています。

function SharesContractSetup () {
  let SharesABI = web3.eth.contract([{"constant":false,"inputs":[{"name":"share","type":"uint256"}],"name":"updateShares","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"addr","type":"address"}],"name":"getShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}])
  let SharesContractObj = SharesABI.at('xxxxxxxxxxxxxxxxxx')
  return SharesContractObj
}

const SharesContract = SharesContractSetup()

export default SharesContract

特筆することはなさそうです。jsonInterfaceとコントラクトのアドレスを渡しているだけですね。

ただ、web3.jsの1系ではAPIが変わっているのでそこだけ注意です。

web3.eth.Contract — web3.js 1.0.0 documentation

また、値を読み取っているだけですのでトランザクションの発行は必要ありません。

トランザクションの発行(sharesのアップデート)

sharesのアップデート部分では、buyShares関数が呼ばれます。部分的に抜粋します。

SharesContract.updateShares(sharesNumber, (error, txHash) => {
  console.log('updateShares')
  if (error) { this.props.actions.buySharesERROR(error) }
  waitForMined(addr, txHash, { blockNumber: null }, actions,
    () => {
      this.props.actions.buySharesPENDING()
    },
    (total) => {
      console.log('waitForMined complete')
      this.props.actions.buySharesSUCCESS(txHash, total)
    }
  )
})

こちらもSharesContract.updateSharesをcallしているだけですね。 uPortで生成したweb3オブジェクトを使うと、普通にコントラクトにトランザクションを発行するだけで認証リクエストがアプリに届くようです!すごい!

waitForMinedはトランザクションが取り込まれるまで定期的に状態をポーリングして待機しているだけですので割愛します。

証明トークンを受け取る

先ほどの画面からNextを押すと、以下の画面に遷移します。

f:id:sakata_harumi:20180814163241p:plain

いずれかの「Get」を押すと、アプリに証明トークン(という訳でいいのかわからないが)を受け入れるかどうかの通知が届きます。

f:id:sakata_harumi:20180814164246p:plain

受け入れた証明トークンはVerificationsタブに表示されます。

また、アプリ側からは証明トークンを要求することが可能になります。

connectUport () {
  uport.requestCredentials(
    { requested: ['name', 'phone', 'country', 'avatar', 'Relationship'],
      notifications: true }
  ).then((credentials) => {
      console.log({credentials})
      this.props.actions.connectUport(credentials)
  })
}

上記のコードを実行すると、Relationshipトークンが存在する場合以下のようにレスポンスにRelationshipが含まれた状態で返ってきます。

f:id:sakata_harumi:20180814165318p:plain

これは、uport.attestCredentialsメソッドにより実現できるようです。 以下は期限が30日のRelationship証明トークンを発行する例です。

uport.attestCredentials({
  sub: this.props.uport.address,
  claim: {Relationship: "User"},
  exp: new Date().getTime() + 30 * 24 * 60 * 60 * 1000 // 30 days from now
})

以上が、このデモアプリで使われているuPortの機能になります。

特定の管理者に個人情報を管理されなくて良いというメリットももちろんですが、 スマホでも簡単にトランザクションに署名することができるので、uPortが使われるようになればスマホのDAppsも一般的なものになるかもしれないですね。

web3のAPIを把握するためVue.jsでEthereumWallet作ってみた

最近ERC20とERC721の実装をしたので、次はクライアントサイドの実装も一通り試したいと思いまして、簡単なEthereumのウォレットアプリを実装しました。

今回JavaScriptフレームワークはVue.jsを採用したんですが、大した理由はなく、触った事がなかったので体験しておくか、くらいのことです。

特にコードに関して言及する事はないです。。Vue.jsの使い方はまだイマイチわからないので引き続きキャッチアップしていこうかと思います><

リポジトリはこちら。

github.com

npm installした後にnpm run devで起動します。

で、接続先のホストとアドレス、アンロックするためのパスワードを入力します。 f:id:sakata_harumi:20180813224834p:plain

この画面で接続先ノードの情報、残高確認、送金が出来ます。 トランザクション履歴も表示されます。

このトランザクション履歴、web3.jsの1系ではgetTransactionsByAccountがなくて困りました。
仕方ないので、セッション中に発行したトランザクションのみを表示するようにしました。。

f:id:sakata_harumi:20180813224840p:plain

ERC721に準拠したトークンをフルスクラッチ実装する

ERC721は価値の交換が不可能なトークン。詳細は他の記事で。
NFT(Non Fungible Token)という略称を使います。

EIPはこちら。最近(一月前くらい?)StatusがFinalになったようです! EIPs/eip-721.md at master · ethereum/EIPs · GitHub

今回も学習のため自分で実装していきますが、実際に稼働するものを作るときはセキュリティ上の理由からOpenZeppelinを使って作成することをおすすめします。

また、ERC721はERC165のサブセットですので、ERC165のメソッドも実装しなければなりません。
実装するべきメソッドはERC20より多いですね・・1つずつ見ていきます。

準拠すべきメソッド・イベント

イベント

event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

NFTの所有者が変わったときに発火するイベント。
NFTが新規に生成された時や、破壊された時にも発火します。
ただし、コントラクトをデプロイするタイミングでNFTを生成する時は呼び出す必要はありません。

event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

approveが成功した時に発火するイベント。
NFTの転送が成功すると、approveはfalseに変わります

event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

全てのNFTを操作可能な権限のapproveが成功した時に発火するイベント。

メソッド

以下はERC721のインターフェース

function balanceOf(address _owner) external view returns (uint256);

NFTの所持数を返します

function ownerOf(uint256 _tokenId) external view returns (address);

NFTの所有者のアドレスを返します

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

NFTの所有権をfromからtoに変更します。
msg.senderがNFTの所有者、認証されたオペレータ、このNFTの承認済みアドレスでない場合はthrowされます。
fromがNFTの所有者でない場合、toがzeroアドレスである場合、_tokenIdが存在しない場合の時にもthrowされます。

変更が完了すると、toがスマートコントラクト(コードサイズ > 0)かどうかをチェックします。
コントラクトであれば、
toで onERC721Receivedを呼び出し、戻り値がbytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))でなければスローします。

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

先のsafeTransferFromの第三引数であるdataを””に設定して呼び出します

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

NFTの所有権をfromからtoに変更します。
呼び出し側は、NFTを受信できるかどうか、またはそれらが永久に失われる可能性があることを確認する責任があります。
msg.senderがNFTの所有者、認証されたオペレータ、このNFTの承認済みアドレスでない場合はthrowされます。
fromがNFTの所有者でない場合、toがzeroアドレス、_tokenIdが存在しない値あってもthrowされます。

function approve(address _approved, uint256 _tokenId) external payable;

_approvedのアドレスがtokenを移動することを許可します
msg.senderがNFTの所有者、認証されたオペレータでない場合はthrowされます。

function setApprovalForAll(address _operator, bool _approved) external;

オペレータが msg.senderのすべてのNFTを管理する事を許可する。もしくは許可を取り消す。
ApprovalForAllイベントを発火します。コントラクトは複数のオペレータを持てるような構造にしなければなりません。

function getApproved(uint256 _tokenId) external view returns (address);

approveしているアドレスを取得します。承認しているアドレスがない場合はzeroアドレスを返します

function isApprovedForAll(address _owner, address _operator) external view returns (bool);

指定したアドレスがオペレータとして許可されているかを確認します

以下はERC165のインターフェース
EIPs/eip-165.md at master · ethereum/EIPs · GitHub

ERC165は、スマートコントラクトがどのインターフェースを実装しているか、を公開するメソッドを定義しています

function supportsInterface(bytes4 interfaceID) external view returns (bool);

interfaceIDで指定されたinterfaceが実装されているかを確認します。

interfaceIDは、interfaceに含まれるすべての関数セレクタのXORとして定義されます。

関数セレクタの詳細についてはApplication Binary Interface Specification — Solidity 0.4.24 documentation をご確認下さい。

ERC165にあるサンプルコードをそのまま抜粋してきました。
return i.hello.selector ^ i.world.selector;でinterfaceIDを計算しています。

publicかexternalのアクセス修飾子がついた関数には、特別なメソッドとして「selector」が付与されます。
*method名が同じで引数が違うものがある場合は、selectorは使えません。なので、bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)”)と直接文字列を指定します

pragma solidity ^0.4.20;

interface Solidity101 {
    function hello() external pure;
    function world(int) external pure;
}

contract Selector {
    function calculateSelector() public pure returns (bytes4) {
        Solidity101 i;
        return i.hello.selector ^ i.world.selector;
    }
}

以上が実装必須のメソッドです。
トークンを新規作成する処理(minting)とトークンを破棄する処理(burning)は要件に含まれていません。
必要に応じて独自で実装するようになっています。

また、wallet/broker/auctionなどのアプリケーションで、safeTransferFromを呼び出す際は、

function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4); 

のcallを受け入れられるようにしておかないといけないことに留意して下さい。

Optionalなメソッドもいくつかあります。

/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
    /// @notice A descriptive name for a collection of NFTs in this contract
    function name() external view returns (string _name);

    /// @notice An abbreviated name for NFTs in this contract
    function symbol() external view returns (string _symbol);

    /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
    /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
    ///  3986. The URI may point to a JSON file that conforms to the "ERC721
    ///  Metadata JSON Schema".
    function tokenURI(uint256 _tokenId) external view returns (string);
}
interface ERC721Enumerable /* is ERC721 */ {
    /// @notice Count NFTs tracked by this contract
    /// @return A count of valid NFTs tracked by this contract, where each one of
    ///  them has an assigned and queryable owner not equal to the zero address
    function totalSupply() external view returns (uint256);

    /// @notice Enumerate valid NFTs
    /// @dev Throws if `_index` >= `totalSupply()`.
    /// @param _index A counter less than `totalSupply()`
    /// @return The token identifier for the `_index`th NFT,
    ///  (sort order not specified)
    function tokenByIndex(uint256 _index) external view returns (uint256);

    /// @notice Enumerate NFTs assigned to an owner
    /// @dev Throws if `_index` >= `balanceOf(_owner)` or if
    ///  `_owner` is the zero address, representing invalid NFTs.
    /// @param _owner An address where we are interested in NFTs owned by them
    /// @param _index A counter less than `balanceOf(_owner)`
    /// @return The token identifier for the `_index`th NFT assigned to `_owner`,
    ///   (sort order not specified)
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

実装する

コードが長くなるので、GitHub - 1234224576/ERC721TokenSample: ERC721に準拠したトークンのサンプル を参照ください・・。
ユニットテストも記述していますので、何かの参考になれば幸いです。

不十分な点が数多く存在すると思われますので、issue/PRを投げてもらえると助かります!

デプロイ

Ropstenテストネットワークに初期バージョンをデプロイしました。
SNFT: 0x4e5590c877f07e86af879bcb7a03b440d57607c4