Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stardust DID Method Proof-of-Concept #940

Merged
merged 14 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main
- dev
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
types: [ opened, synchronize, reopened, ready_for_review ]
branches:
- main
- dev
Expand Down Expand Up @@ -63,7 +63,7 @@ jobs:

build-and-test:
runs-on: ${{ matrix.os }}
needs: [check-for-run-condition, check-for-modification]
needs: [ check-for-run-condition, check-for-modification ]
if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' && needs.check-for-modification.outputs.core-modified == 'true' }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -123,6 +123,55 @@ jobs:
with:
os: ${{matrix.os}}

build-and-test-stardust:
needs: [ check-for-run-condition, check-for-modification ]
if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' && needs.check-for-modification.outputs.core-modified == 'true' }}
cycraig marked this conversation as resolved.
Show resolved Hide resolved
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
sccache-path: /home/runner/.cache/sccache
env:
SCCACHE_DIR: ${{ matrix.sccache-path }}
RUSTC_WRAPPER: sccache

steps:
- uses: actions/checkout@v2

- name: Setup Rust and cache
uses: './.github/actions/rust/rust-setup'
with:
os: ${{ runner.os }}
job: ${{ github.job }}
target-cache-enabled: false
sccache-enabled: true
sccache-path: ${{ matrix.sccache-path }}

- name: Setup sccache
uses: './.github/actions/rust/sccache/setup-sccache'
with:
os: ${{matrix.os}}

- name: Build Stardust
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path ./identity_stardust/Cargo.toml --workspace --tests --examples --release

- name: Run Stardust tests
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path ./identity_stardust/Cargo.toml --all --all-features --release

- name: Stop sccache
uses: './.github/actions/rust/sccache/stop-sccache'
with:
os: ${{matrix.os}}

build-and-test-libjose:
needs: check-for-run-condition
if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }}
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/clippy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ jobs:
args: --all-targets --all-features -- -D warnings
name: core

- name: wasm clippy check
- name: Stardust clippy check
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --manifest-path ./identity_stardust/Cargo.toml --all-targets --all-features -- -D warnings
name: stardust

- name: Wasm clippy check
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ jobs:
command: fmt
args: --all -- --check

- name: Stardust fmt check
uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path ./identity_stardust/Cargo.toml --all -- --check

- name: wasm fmt check
uses: actions-rs/cargo@v1
with:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/test-docs-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ jobs:
toolchain: nightly
args: --all-features --no-deps --workspace

- name: Test Stardust Rust Documentation
uses: actions-rs/cargo@v1
env:
RUSTDOCFLAGS: "-D warnings --cfg docsrs"
with:
command: doc
toolchain: nightly
args: --manifest-path ./identity_stardust/Cargo.toml --all-features --no-deps --workspace
40 changes: 40 additions & 0 deletions identity_stardust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[package]
name = "identity_stardust"
version = "0.6.0"
authors = ["IOTA Stiftung"]
edition = "2021"
homepage = "https://www.iota.org"
keywords = ["iota", "tangle", "stardust", "identity"]
license = "Apache-2.0"
readme = "../README.md"
repository = "https://github.com/iotaledger/identity.rs"
description = "An IOTA Ledger integration for the identity.rs library."

[workspace]

[dependencies]
identity_core = { version = "=0.6.0", path = "../identity_core", default-features = false }
identity_credential = { version = "=0.6.0", path = "../identity_credential", default-features = false }
identity_did = { version = "=0.6.0", path = "../identity_did", default-features = false }
lazy_static = { version = "1.4", default-features = false }
serde = { version = "1.0", default-features = false, features = ["std", "derive"] }
strum = { version = "0.21", features = ["derive"] }
thiserror = { version = "1.0", default-features = false }

[dependencies.iota-client]
git = "https://github.com/iotaledger/iota.rs"
rev = "4e7db070a05321c4bd7579acdcc74436865235c0" # develop branch, 2022-07-11
features = ["tls"]
default-features = false

[dev-dependencies]
anyhow = { version = "1.0.57" }
iota-crypto = { version = "0.11.0", default-features = false, features = ["bip39", "bip39-en"] }
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
tokio = { version = "1.17.0", default-features = false, features = ["macros"] }

[package.metadata.docs.rs]
# To build locally:
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
5 changes: 5 additions & 0 deletions identity_stardust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# IOTA Stardust Identity Library

This is a work-in-progress intended to replace the `did:iota` DID Method.

`cargo run --example create_did`
177 changes: 177 additions & 0 deletions identity_stardust/examples/create_did.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use identity_core::convert::ToJson;
use iota_client::bee_block::output::feature::IssuerFeature;
use iota_client::bee_block::output::feature::MetadataFeature;
use iota_client::bee_block::output::feature::SenderFeature;
use iota_client::bee_block::output::unlock_condition::GovernorAddressUnlockCondition;
use iota_client::bee_block::output::unlock_condition::StateControllerAddressUnlockCondition;
use iota_client::bee_block::output::unlock_condition::UnlockCondition;
use iota_client::bee_block::output::AliasId;
use iota_client::bee_block::output::AliasOutputBuilder;
use iota_client::bee_block::output::ByteCostConfig;
use iota_client::bee_block::output::Feature;
use iota_client::bee_block::output::Output;
use iota_client::constants::SHIMMER_TESTNET_BECH32_HRP;
use iota_client::secret::mnemonic::MnemonicSecretManager;
use iota_client::secret::SecretManager;
use iota_client::Client;

use identity_stardust::StardustDocument;

// PROBLEMS SO FAR:
// 1) Alias Id is inferred from the block, so we have to use a placeholder DID for creation.
// 2) Cannot get an Output Id back from an Alias Id (hash of Output Id), need to use Indexer API.
// 3) The Output response from the Indexer is an Output, not a Block, so cannot infer Alias ID from it (fine since we
// use the ID to retrieve the Output in the first place). The OutputDto conversion is annoying too.
// 4) The pieces needed to publish an update are fragmented (Output ID for input, amount, document), bit annoying to
// reconstruct. Use a holder struct like Holder { AliasOutput, StardustDocument } with convenience functions?
// 5) Inferred fields such as the controller and governor need to reflect in the (JSON) Document but excluded from the
// StardustDocument serialization when published. Handle with a separate `pack` function like before?

/// Demonstrate how to embed a DID Document in an Alias Output.
///
/// iota.rs alias example:
/// https://github.com/iotaledger/iota.rs/blob/f945ccf326829a418334942ae9cf53b8fab3dbe5/examples/outputs/alias.rs
///
/// iota.js mint-nft example:
/// https://github.com/iotaledger/iota.js/blob/79a71d3a2ad03be5bd6148689d083947f3b98476/packages/iota/examples/mint-nft/src/index.ts
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// let endpoint = "http://localhost:14265";
let endpoint = "https://api.alphanet.iotaledger.net";
let faucet_manual = "https://faucet.alphanet.iotaledger.net";

// ===========================================================================
// Step 1: Create or load your wallet.
// ===========================================================================

// let keypair = identity_core::crypto::KeyPair::new(identity_core::crypto::KeyType::Ed25519).unwrap();
// println!("PrivateKey: {}", keypair.private().to_string());
// let mnemonic =
// iota_client::crypto::keys::bip39::wordlist::encode(keypair.private().as_ref(),&bip39::wordlist::ENGLISH).unwrap();

// NOTE: this is just a randomly generated mnemonic, REMOVE THIS, never actually commit your seed or mnemonic.
let mnemonic = "veteran provide abstract express quick another fee dragon trend extend cotton tail dog truly angle napkin lunch dinosaur shrimp odor gain bag media mountain";
println!("Mnemonic: {}", mnemonic);
let secret_manager = SecretManager::Mnemonic(MnemonicSecretManager::try_from_mnemonic(mnemonic)?);

// Create a client instance.
let client = Client::builder()
.with_node(endpoint)?
.with_node_sync_disabled()
.finish()
.await?;

let address = client.get_addresses(&secret_manager).with_range(0..1).get_raw().await?[0];
let address_bech32 = address.to_bech32(SHIMMER_TESTNET_BECH32_HRP);
println!("Wallet address: {address_bech32}");

println!("INTERACTION REQUIRED: request faucet funds to the above wallet from {faucet_manual}");
// let faucet_auto = format!("{endpoint}/api/plugins/faucet/v1/enqueue");
// iota_client::request_funds_from_faucet(&faucet_auto, &address_bech32).await?;
// tokio::time::sleep(std::time::Duration::from_secs(15)).await;

// ===========================================================================
// Step 2: Create and publish a DID Document in an Alias Output.
// ===========================================================================

// Create an empty DID Document.
// All new Stardust DID Documents initially use a placeholder DID,
// "did:stardust:0x00000000000000000000000000000000".
let document: StardustDocument = StardustDocument::new();
println!("DID Document {:#}", document);

// Create a new Alias Output with the DID Document as state metadata.
let byte_cost_config: ByteCostConfig = client.get_byte_cost_config().await?;
let alias_output: Output = AliasOutputBuilder::new_with_minimum_storage_deposit(byte_cost_config, AliasId::null())?
.with_state_index(0)
.with_foundry_counter(0)
.with_state_metadata(document.to_json_vec()?)
.add_feature(Feature::Sender(SenderFeature::new(address)))
.add_feature(Feature::Metadata(MetadataFeature::new(vec![1, 2, 3])?))
.add_immutable_feature(Feature::Issuer(IssuerFeature::new(address)))
.add_unlock_condition(UnlockCondition::StateControllerAddress(
StateControllerAddressUnlockCondition::new(address),
))
.add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new(
address,
)))
.finish_output()?;
println!("Deposit amount: {}", alias_output.amount());

// Publish to the Tangle ledger.
let block = client
.block()
.with_secret_manager(&secret_manager)
.with_outputs(vec![alias_output])?
.finish()
.await?;
println!(
"Transaction with new alias output sent: {endpoint}/api/v2/blocks/{}",
block.id()
);
let _ = client.retry_until_included(&block.id(), None, None).await?;

// Infer DID from Alias Output block.
let did = StardustDocument::did_from_block(&block)?;
println!("DID: {did}");

// ===========================================================================
// Step 3: Resolve a DID Document.
// ===========================================================================
// iota.rs indexer example:
// https://github.com/iotaledger/iota.rs/blob/f945ccf326829a418334942ae9cf53b8fab3dbe5/examples/indexer.rs

// Extract Alias ID from DID.
let alias_id: AliasId = StardustDocument::did_to_alias_id(&did)?;
println!("Alias ID: {alias_id}");

// Query Indexer INX Plugin for the Output of the Alias ID.
let output_id = client.alias_output_id(alias_id).await?;
println!("Output ID: {output_id}");
let response = client.get_output(&output_id).await?;
let output = Output::try_from(&response.output)?;
println!("Output: {output:?}");

// The resolved DID Document replaces the placeholder DID with the correct one.
let resolved_document = StardustDocument::deserialize_from_output(&alias_id, &output)?;
println!("Resolved Document: {resolved_document:#}");

let alias_output = match output {
Output::Alias(output) => Ok(output),
_ => Err(anyhow::anyhow!("not an alias output")),
}?;

// ===========================================================================
// Step 4: Publish an updated Alias ID. (optional)
// ===========================================================================
// TODO: we could always publish twice on creation to populate the DID (could fail),
// or just infer the DID during resolution (safer).

// Update the Alias Output to contain an explicit ID and DID.
let updated_alias_output = AliasOutputBuilder::from(&alias_output) // Not adding any content, previous amount will cover the deposit.
// Set the explicit Alias ID.
.with_alias_id(alias_id)
// Update the DID Document content to replace the placeholder DID.
.with_state_metadata(resolved_document.to_json_vec()?)
// State controller updates increment the state index.
.with_state_index(alias_output.state_index() + 1)
.finish_output()?;

println!("Updated output: {updated_alias_output:?}");

let block = client
.block()
.with_secret_manager(&secret_manager)
.with_input(output_id.into())?
.with_outputs(vec![updated_alias_output])?
.finish()
.await?;

println!(
"Transaction with alias id set sent: {endpoint}/api/v2/blocks/{}",
block.id()
);
let _ = client.retry_until_included(&block.id(), None, None).await?;

Ok(())
}
21 changes: 21 additions & 0 deletions identity_stardust/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

pub type Result<T, E = Error> = core::result::Result<T, E>;

// TODO: replace all variants with specific errors?
#[derive(Debug, thiserror::Error, strum::IntoStaticStr)]
pub enum Error {
#[error("{0}")]
CoreError(#[from] identity_core::Error),
#[error("{0}")]
CredError(#[from] identity_credential::Error),
#[error("{0}")]
InvalidDID(#[from] identity_did::did::DIDError),
#[error("{0}")]
InvalidDoc(#[from] identity_did::Error),
#[error("{0}")]
ClientError(#[from] iota_client::error::Error),
#[error("{0}")]
BeeError(#[from] iota_client::bee_block::Error),
}
13 changes: 13 additions & 0 deletions identity_stardust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

#![forbid(unsafe_code)]
#![allow(clippy::upper_case_acronyms)]

pub use self::error::Error;
pub use self::error::Result;

pub use stardust_document::StardustDocument;

mod error;
mod stardust_document;
Loading