Skip to content

Commit

Permalink
Stardust DID Method Proof-of-Concept (#940)
Browse files Browse the repository at this point in the history
* Stardust Alias Output DID Method proof-of-concept

* Add helper functions

* Add resolution proof-of-concept, use state metadata instead

* Simplify builder usage

* Use state index as sentinel value

* Add TODO

* Rename identity-stardust to identity_stardust

* Update to latest iota-client commit, remove unused dependencies

* Fix toml formatting

* Add identity_stardust to GitHub Actions CI

* Fix build-and-test-stardust run condition
  • Loading branch information
cycraig authored Jul 12, 2022
1 parent 6657c1e commit 324efeb
Show file tree
Hide file tree
Showing 10 changed files with 501 additions and 3 deletions.
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' }}
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

0 comments on commit 324efeb

Please sign in to comment.