DID: Decentralized identifiers (DIDs) (XLS-40): (#4636)

Implement native support for W3C DIDs.

Add a new ledger object: `DID`.

Add two new transactions:
1. `DIDSet`: create or update the `DID` object.
2. `DIDDelete`: delete the `DID` object.

This meets the requirements specified in the DID v1.0 specification
currently recommended by the W3C Credentials Community Group.

The DID format for the XRP Ledger conforms to W3C DID standards.
The objects can be created and owned by any XRPL account holder.
The transactions can be integrated by any service, wallet, or application.
This commit is contained in:
Mayukha Vadari
2023-10-18 16:01:12 -04:00
committed by tequ
parent 31b61b5b94
commit ae39ac3cfb
30 changed files with 1062 additions and 7 deletions

View File

@@ -531,6 +531,7 @@ target_sources (rippled PRIVATE
src/ripple/app/tx/impl/CreateTicket.cpp
src/ripple/app/tx/impl/DeleteAccount.cpp
src/ripple/app/tx/impl/DepositPreauth.cpp
src/ripple/app/tx/impl/DID.cpp
src/ripple/app/tx/impl/Escrow.cpp
src/ripple/app/tx/impl/GenesisMint.cpp
src/ripple/app/tx/impl/Import.cpp
@@ -812,6 +813,7 @@ if (tests)
src/test/app/DeliverMin_test.cpp
src/test/app/DepositAuth_test.cpp
src/test/app/Discrepancy_test.cpp
src/test/app/DID_test.cpp
src/test/app/DNS_test.cpp
src/test/app/Escrow_test.cpp
src/test/app/FeeVote_test.cpp
@@ -977,6 +979,7 @@ if (tests)
src/test/jtx/impl/check.cpp
src/test/jtx/impl/delivermin.cpp
src/test/jtx/impl/deposit.cpp
src/test/jtx/impl/did.cpp
src/test/jtx/impl/envconfig.cpp
src/test/jtx/impl/fee.cpp
src/test/jtx/impl/flags.cpp

View File

@@ -0,0 +1,226 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/app/tx/impl/DID.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/ApplyView.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/st.h>
namespace ripple {
/*
DID
======
Decentralized Identifiers (DIDs) are a new type of identifier that enable
verifiable, self-sovereign digital identity and are designed to be
compatible with any distributed ledger or network. This implementation
conforms to the requirements specified in the DID v1.0 specification
currently recommended by the W3C Credentials Community Group
(https://www.w3.org/TR/did-core/).
*/
//------------------------------------------------------------------------------
NotTEC
DIDSet::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureDID))
return temDISABLED;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (!ctx.tx.isFieldPresent(sfURI) &&
!ctx.tx.isFieldPresent(sfDIDDocument) && !ctx.tx.isFieldPresent(sfData))
return temEMPTY_DID;
if (ctx.tx.isFieldPresent(sfURI) && ctx.tx[sfURI].empty() &&
ctx.tx.isFieldPresent(sfDIDDocument) && ctx.tx[sfDIDDocument].empty() &&
ctx.tx.isFieldPresent(sfData) && ctx.tx[sfData].empty())
return temEMPTY_DID;
auto isTooLong = [&](auto const& sField, std::size_t length) -> bool {
if (auto field = ctx.tx[~sField])
return field->length() > length;
return false;
};
if (isTooLong(sfURI, maxDIDURILength) ||
isTooLong(sfDIDDocument, maxDIDDocumentLength) ||
isTooLong(sfData, maxDIDAttestationLength))
return temMALFORMED;
return preflight2(ctx);
}
TER
addSLE(
ApplyContext& ctx,
std::shared_ptr<SLE> const& sle,
AccountID const& owner)
{
auto const sleAccount = ctx.view().peek(keylet::account(owner));
if (!sleAccount)
return tefINTERNAL;
// Check reserve availability for new object creation
{
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
auto const reserve =
ctx.view().fees().accountReserve((*sleAccount)[sfOwnerCount] + 1);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
}
// Add ledger object to ledger
ctx.view().insert(sle);
// Add ledger object to owner's page
{
auto page = ctx.view().dirInsert(
keylet::ownerDir(owner), sle->key(), describeOwnerDir(owner));
if (!page)
return tecDIR_FULL;
(*sle)[sfOwnerNode] = *page;
}
adjustOwnerCount(ctx.view(), sleAccount, 1, ctx.journal);
ctx.view().update(sleAccount);
return tesSUCCESS;
}
TER
DIDSet::doApply()
{
// Edit ledger object if it already exists
Keylet const didKeylet = keylet::did(account_);
if (auto const sleDID = ctx_.view().peek(didKeylet))
{
auto update = [&](auto const& sField) {
if (auto const field = ctx_.tx[~sField])
{
if (field->empty())
{
sleDID->makeFieldAbsent(sField);
}
else
{
(*sleDID)[sField] = *field;
}
}
};
update(sfURI);
update(sfDIDDocument);
update(sfData);
if (!sleDID->isFieldPresent(sfURI) &&
!sleDID->isFieldPresent(sfDIDDocument) &&
!sleDID->isFieldPresent(sfData))
{
return tecEMPTY_DID;
}
ctx_.view().update(sleDID);
return tesSUCCESS;
}
// Create new ledger object otherwise
auto const sleDID = std::make_shared<SLE>(didKeylet);
(*sleDID)[sfAccount] = account_;
auto set = [&](auto const& sField) {
if (auto const field = ctx_.tx[~sField]; field && !field->empty())
(*sleDID)[sField] = *field;
};
set(sfURI);
set(sfDIDDocument);
set(sfData);
return addSLE(ctx_, sleDID, account_);
}
NotTEC
DIDDelete::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureDID))
return temDISABLED;
if (ctx.tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
return preflight2(ctx);
}
TER
DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner)
{
auto const sle = ctx.view().peek(sleKeylet);
if (!sle)
return tecNO_ENTRY;
return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal);
}
TER
DIDDelete::deleteSLE(
ApplyView& view,
std::shared_ptr<SLE> sle,
AccountID const owner,
beast::Journal j)
{
// Remove object from owner directory
if (!view.dirRemove(
keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
{
JLOG(j.fatal()) << "Unable to delete DID Token from owner.";
return tefBAD_LEDGER;
}
auto const sleOwner = view.peek(keylet::account(owner));
if (!sleOwner)
return tecINTERNAL;
adjustOwnerCount(view, sleOwner, -1, j);
view.update(sleOwner);
// Remove object from ledger
view.erase(sle);
return tesSUCCESS;
}
TER
DIDDelete::doApply()
{
return deleteSLE(ctx_, keylet::did(account_), account_);
}
} // namespace ripple

View File

@@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_DID_H_INCLUDED
#define RIPPLE_TX_DID_H_INCLUDED
#include <ripple/app/tx/impl/Transactor.h>
namespace ripple {
class DIDSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit DIDSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
class DIDDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit DIDDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner);
static TER
deleteSLE(
ApplyView& view,
std::shared_ptr<SLE> sle,
AccountID const owner,
beast::Journal j);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <ripple/app/tx/impl/DID.h>
#include <ripple/app/tx/impl/DeleteAccount.h>
#include <ripple/app/tx/impl/DepositPreauth.h>
#include <ripple/app/tx/impl/SetSignerList.h>
@@ -163,6 +164,18 @@ removeGeneric(
return tesSUCCESS;
}
TER
removeDIDFromLedger(
Application& app,
ApplyView& view,
AccountID const& account,
uint256 const& delIndex,
std::shared_ptr<SLE> const& sleDel,
beast::Journal j)
{
return DIDDelete::deleteSLE(view, sleDel, account, j);
}
// Return nullptr if the LedgerEntryType represents an obligation that can't
// be deleted. Otherwise return the pointer to the function that can delete
// the non-obligation
@@ -183,6 +196,8 @@ nonObligationDeleter(LedgerEntryType t)
return removeNFTokenOfferFromLedger;
case ltURI_TOKEN:
return removeGeneric;
case ltDID:
return removeDIDFromLedger;
default:
return nullptr;
}

View File

@@ -511,6 +511,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltBRIDGE:
case ltXCHAIN_OWNED_CLAIM_ID:
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
case ltDID:
break;
default:
invalidTypeAdded_ = true;

View File

@@ -34,6 +34,7 @@
#include <ripple/app/tx/impl/CreateCheck.h>
#include <ripple/app/tx/impl/CreateOffer.h>
#include <ripple/app/tx/impl/CreateTicket.h>
#include <ripple/app/tx/impl/DID.h>
#include <ripple/app/tx/impl/DeleteAccount.h>
#include <ripple/app/tx/impl/DepositPreauth.h>
#include <ripple/app/tx/impl/Escrow.h>
@@ -185,6 +186,10 @@ with_txn_type(TxType txnType, F&& f)
return f.template operator()<XChainAddAccountCreateAttestation>();
case ttXCHAIN_ACCOUNT_CREATE_COMMIT:
return f.template operator()<XChainCreateAccountCommit>();
case ttDID_SET:
return f.template operator()<DIDSet>();
case ttDID_DELETE:
return f.template operator()<DIDDelete>();
default:
throw UnknownTxnType(txnType);
}

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 86;
static constexpr std::size_t numFeatures = 87;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -374,6 +374,7 @@ extern uint256 const featureClawback;
extern uint256 const featureAMM;
extern uint256 const featureXChainBridge;
extern uint256 const fixDisallowIncomingV1;
extern uint256 const featureDID;
} // namespace ripple

View File

@@ -314,6 +314,9 @@ xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq);
Keylet
xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
Keylet
did(AccountID const& account) noexcept;
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:

View File

@@ -204,6 +204,12 @@ enum LedgerEntryType : std::uint16_t
*/
ltAMM = 0x0079,
/** The ledger object which tracks the DID.
\sa keylet::did
*/
ltDID = 0x4449,
//---------------------------------------------------------------------------
/** A special type, matching any ledger entry type.

View File

@@ -83,6 +83,15 @@ std::uint16_t constexpr maxTransferFee = 50000;
/** The maximum length of a URI inside an NFT */
std::size_t constexpr maxTokenURILength = 256;
/** The maximum length of a Data element inside a DID */
std::size_t constexpr maxDIDDocumentLength = 256;
/** The maximum length of a URI inside a DID */
std::size_t constexpr maxDIDURILength = 256;
/** The maximum length of an Attestation inside a DID */
std::size_t constexpr maxDIDAttestationLength = 256;
/** The maximum length of a domain */
std::size_t constexpr maxDomainLength = 256;

View File

@@ -546,6 +546,8 @@ extern SF_VL const sfCreateCode;
extern SF_VL const sfMemoType;
extern SF_VL const sfMemoData;
extern SF_VL const sfMemoFormat;
extern SF_VL const sfDIDDocument;
extern SF_VL const sfData;
// variable length (uncommon)
extern SF_VL const sfFulfillment;

View File

@@ -135,6 +135,7 @@ enum TEMcodes : TERUnderlyingType {
temXCHAIN_BRIDGE_NONDOOR_OWNER,
temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT,
temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT,
temEMPTY_DID,
temHOOK_DATA_TOO_LARGE,
};
@@ -345,6 +346,7 @@ enum TECcodes : TERUnderlyingType {
tecTOO_MANY_REMARKS = 189,
tecINCOMPLETE = 190,
// 191: tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR
tecEMPTY_DID = 192,
tecLAST_POSSIBLE_ENTRY = 255,
};

View File

@@ -191,6 +191,12 @@ enum TxType : std::uint16_t
/** This transactions creates a sidechain */
ttXCHAIN_CREATE_BRIDGE = 57,
/** This transaction type creates or updates a DID */
ttDID_SET = 58,
/** This transaction type deletes a DID */
ttDID_DELETE = 59,
/* A note attaching transactor that allows the owner or issuer (on a object by object basis) to attach remarks */
ttREMARKS_SET = 94,

View File

@@ -479,7 +479,8 @@ REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(XChainBridge, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX(fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(DID, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.

View File

@@ -80,6 +80,7 @@ enum class LedgerNameSpace : std::uint16_t {
BRIDGE = 'H',
XCHAIN_CLAIM_ID = 'Q',
XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K',
DID = 'I',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -510,6 +511,12 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq)
seq)};
}
Keylet
did(AccountID const& account) noexcept
{
return {ltDID, indexHash(LedgerNameSpace::DID, account)};
}
} // namespace keylet
} // namespace ripple

View File

@@ -424,6 +424,19 @@ LedgerFormats::LedgerFormats()
{sfPreviousTxnLgrSeq, soeREQUIRED}
},
commonFields);
add(jss::DID,
ltDID,
{
{sfAccount, soeREQUIRED},
{sfDIDDocument, soeOPTIONAL},
{sfURI, soeOPTIONAL},
{sfData, soeOPTIONAL},
{sfOwnerNode, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED}
},
commonFields);
// clang-format on
}

View File

@@ -319,6 +319,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookReturnString, "HookReturnString", VL,
CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, 24);
CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25);
CONSTRUCT_TYPED_SFIELD(sfBlob, "Blob", VL, 26);
CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 27);
CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 28);
CONSTRUCT_TYPED_SFIELD(sfRemarkValue, "RemarkValue", VL, 98);
CONSTRUCT_TYPED_SFIELD(sfRemarkName, "RemarkName", VL, 99);

View File

@@ -119,6 +119,7 @@ transResults()
MAKE_ERROR(tecXCHAIN_SELF_COMMIT, "Account cannot commit funds to itself."),
MAKE_ERROR(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR, "Bad public key account pair in an xchain transaction."),
MAKE_ERROR(tecXCHAIN_CREATE_ACCOUNT_DISABLED, "This bridge does not support account creation."),
MAKE_ERROR(tecEMPTY_DID, "The DID object did not have a URI or DIDDocument field."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
@@ -191,6 +192,7 @@ transResults()
MAKE_ERROR(temBAD_WEIGHT, "Malformed: Weight must be a positive value."),
MAKE_ERROR(temDST_IS_SRC, "Destination may not be source."),
MAKE_ERROR(temDST_NEEDED, "Destination not specified."),
MAKE_ERROR(temEMPTY_DID, "Malformed: No DID data provided."),
MAKE_ERROR(temINVALID, "The transaction is ill-formed."),
MAKE_ERROR(temINVALID_FLAG, "The transaction has an invalid flag."),
MAKE_ERROR(temREDUNDANT, "The transaction is redundant."),

View File

@@ -598,6 +598,17 @@ TxFormats::TxFormats()
{sfSignatureReward, soeREQUIRED},
},
commonFields);
add(jss::DIDSet,
ttDID_SET,
{
{sfDIDDocument, soeOPTIONAL},
{sfURI, soeOPTIONAL},
{sfData, soeOPTIONAL},
},
commonFields);
add(jss::DIDDelete, ttDID_DELETE, {}, commonFields);
}
TxFormats const&

View File

@@ -73,6 +73,9 @@ JSS(Clawback); // transaction type.
JSS(ClaimReward); // transaction type.
JSS(ClearFlag); // field.
JSS(CreateCode); // field.
JSS(DID); // ledger type.
JSS(DIDDelete); // transaction type.
JSS(DIDSet); // transaction type.
JSS(DeliverMin); // in: TransactionSign
JSS(DepositPreauth); // transaction and ledger type.
JSS(Destination); // in: TransactionSign; field.
@@ -334,6 +337,7 @@ JSS(destination_currencies); // in: PathRequest, RipplePathFind
JSS(destination_tag); // in: PathRequest
// out: AccountChannels
JSS(details); // out: Manifest, server_info
JSS(did); // in: LedgerEntry
JSS(dir_entry); // out: DirectoryEntryIterator
JSS(dir_index); // out: DirectoryEntryIterator
JSS(dir_root); // out: DirectoryEntryIterator

View File

@@ -754,6 +754,16 @@ doLedgerEntry(RPC::JsonContext& context)
}
}
}
else if (context.params.isMember(jss::did))
{
expectedType = ltDID;
auto const account =
parseBase58<AccountID>(context.params[jss::did].asString());
if (!account || account->isZero())
jvResult[jss::error] = "malformedAddress";
else
uNodeIndex = keylet::did(*account).key;
}
else
{
if (context.params.isMember("params") &&

View File

@@ -1113,7 +1113,7 @@ chooseLedgerEntryType(Json::Value const& params)
std::pair<RPC::Status, LedgerEntryType> result{RPC::Status::OK, ltANY};
if (params.isMember(jss::type))
{
static constexpr std::array<std::pair<char const*, LedgerEntryType>, 26>
static constexpr std::array<std::pair<char const*, LedgerEntryType>, 27>
types{
{{jss::account, ltACCOUNT_ROOT},
{jss::amendments, ltAMENDMENTS},
@@ -1141,7 +1141,8 @@ chooseLedgerEntryType(Json::Value const& params)
{jss::bridge, ltBRIDGE},
{jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID},
{jss::xchain_owned_create_account_claim_id,
ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}}};
ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
{jss::did, ltDID}}};
auto const& p = params[jss::type];
if (!p.isString())

View File

@@ -148,13 +148,14 @@ public:
env.close();
// Give carol a deposit preauthorization, an offer, a ticket,
// and a signer list. Even with all that she's still deletable.
// a signer list, and a DID. Even with all that she's still deletable.
env(deposit::auth(carol, becky));
std::uint32_t const carolOfferSeq{env.seq(carol)};
env(offer(carol, gw["USD"](51), XRP(51)));
std::uint32_t const carolTicketSeq{env.seq(carol) + 1};
env(ticket::create(carol, 1));
env(signers(carol, 1, {{alice, 1}, {becky, 1}}));
env(did::setValid(carol));
// Deleting should fail with TOO_SOON, which is a relatively
// cheap check compared to validating the contents of her directory.

406
src/test/app/DID_test.cpp Normal file
View File

@@ -0,0 +1,406 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/basics/strHex.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
#include <algorithm>
#include <iterator>
namespace ripple {
namespace test {
// Helper function that returns the owner count of an account root.
std::uint32_t
ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct)
{
std::uint32_t ret{0};
if (auto const sleAcct = env.le(acct))
ret = sleAcct->at(sfOwnerCount);
return ret;
}
bool
checkVL(Slice const& result, std::string expected)
{
Serializer s;
s.addRaw(result);
return s.getString() == expected;
}
struct DID_test : public beast::unit_test::suite
{
void
testEnabled(FeatureBitset features)
{
testcase("featureDID Enabled");
using namespace jtx;
// If the DID amendment is not enabled, you should not be able
// to set or delete DIDs.
Env env{*this, features - featureDID};
Account const alice{"alice"};
env.fund(XRP(5000), alice);
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
env(did::setValid(alice), ter(temDISABLED));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
env(did::del(alice), ter(temDISABLED));
env.close();
}
void
testAccountReserve(FeatureBitset features)
{
// Verify that the reserve behaves as expected for minting.
testcase("DID Account Reserve");
using namespace test::jtx;
Env env{*this, features};
Account const alice{"alice"};
// Fund alice enough to exist, but not enough to meet
// the reserve for creating a DID.
auto const acctReserve = env.current()->fees().accountReserve(0);
auto const incReserve = env.current()->fees().increment;
env.fund(acctReserve, alice);
env.close();
BEAST_EXPECT(env.balance(alice) == acctReserve);
BEAST_EXPECT(ownerCount(env, alice) == 0);
// alice does not have enough XRP to cover the reserve for a DID
env(did::setValid(alice), ter(tecINSUFFICIENT_RESERVE));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
// Pay alice almost enough to make the reserve for a DID.
env(pay(env.master, alice, incReserve + drops(19)));
BEAST_EXPECT(env.balance(alice) == acctReserve + incReserve + drops(9));
env.close();
// alice still does not have enough XRP for the reserve of a DID.
env(did::setValid(alice), ter(tecINSUFFICIENT_RESERVE));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
// Pay alice enough to make the reserve for a DID.
env(pay(env.master, alice, drops(11)));
env.close();
// Now alice can create a DID.
env(did::setValid(alice));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 1);
// alice deletes her DID.
env(did::del(alice));
BEAST_EXPECT(ownerCount(env, alice) == 0);
env.close();
}
void
testSetInvalid(FeatureBitset features)
{
testcase("Invalid DIDSet");
using namespace jtx;
using namespace std::chrono;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(5000), alice);
env.close();
//----------------------------------------------------------------------
// preflight
// invalid flags
BEAST_EXPECT(ownerCount(env, alice) == 0);
env(did::setValid(alice), txflags(0x00010000), ter(temINVALID_FLAG));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
// no fields
env(did::set(alice), ter(temEMPTY_DID));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
// all empty fields
env(did::set(alice),
did::uri(""),
did::document(""),
did::data(""),
ter(temEMPTY_DID));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
// uri is too long
const std::string longString(257, 'a');
env(did::set(alice), did::uri(longString), ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
// document is too long
env(did::set(alice), did::document(longString), ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
// attestation is too long
env(did::set(alice),
did::document("data"),
did::data(longString),
ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
// Modifying a DID to become empty is checked in testSetModify
}
void
testDeleteInvalid(FeatureBitset features)
{
testcase("Invalid DIDDelete");
using namespace jtx;
using namespace std::chrono;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(5000), alice);
env.close();
//----------------------------------------------------------------------
// preflight
// invalid flags
BEAST_EXPECT(ownerCount(env, alice) == 0);
env(did::del(alice), txflags(0x00010000), ter(temINVALID_FLAG));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
//----------------------------------------------------------------------
// doApply
// DID doesn't exist
env(did::del(alice), ter(tecNO_ENTRY));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
}
void
testSetValidInitial(FeatureBitset features)
{
testcase("Valid Initial DIDSet");
using namespace jtx;
using namespace std::chrono;
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const charlie{"charlie"};
Account const dave{"dave"};
Account const edna{"edna"};
Account const francis{"francis"};
Account const george{"george"};
env.fund(XRP(5000), alice, bob, charlie, dave, edna, francis);
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 0);
BEAST_EXPECT(ownerCount(env, charlie) == 0);
// only URI
env(did::set(alice), did::uri("uri"));
BEAST_EXPECT(ownerCount(env, alice) == 1);
// only DIDDocument
env(did::set(bob), did::document("data"));
BEAST_EXPECT(ownerCount(env, bob) == 1);
// only Data
env(did::set(charlie), did::data("data"));
BEAST_EXPECT(ownerCount(env, charlie) == 1);
// URI + Data
env(did::set(dave), did::uri("uri"), did::data("attest"));
BEAST_EXPECT(ownerCount(env, dave) == 1);
// URI + DIDDocument
env(did::set(edna), did::uri("uri"), did::document("data"));
BEAST_EXPECT(ownerCount(env, edna) == 1);
// DIDDocument + Data
env(did::set(francis), did::document("data"), did::data("attest"));
BEAST_EXPECT(ownerCount(env, francis) == 1);
// URI + DIDDocument + Data
env(did::set(george),
did::uri("uri"),
did::document("data"),
did::data("attest"));
BEAST_EXPECT(ownerCount(env, george) == 1);
}
void
testSetModify(FeatureBitset features)
{
testcase("Modify DID with DIDSet");
using namespace jtx;
using namespace std::chrono;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(5000), alice);
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
auto const ar = env.le(alice);
// Create DID
std::string const initialURI = "uri";
{
env(did::set(alice), did::uri(initialURI));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(sleDID);
BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI));
BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument));
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
}
// Try to delete URI, fails because no elements are set
{
env(did::set(alice), did::uri(""), ter(tecEMPTY_DID));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI));
BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument));
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
}
// Set DIDDocument
std::string const initialDocument = "data";
{
env(did::set(alice), did::document(initialDocument));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI));
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument));
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
}
// Set Data
std::string const initialData = "attest";
{
env(did::set(alice), did::data(initialData));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI));
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument));
BEAST_EXPECT(checkVL((*sleDID)[sfData], initialData));
}
// Remove URI
{
env(did::set(alice), did::uri(""));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(!sleDID->isFieldPresent(sfURI));
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument));
BEAST_EXPECT(checkVL((*sleDID)[sfData], initialData));
}
// Remove Data
{
env(did::set(alice), did::data(""));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(!sleDID->isFieldPresent(sfURI));
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument));
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
}
// Remove Data + set URI
std::string const secondURI = "uri2";
{
env(did::set(alice), did::uri(secondURI), did::document(""));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(checkVL((*sleDID)[sfURI], secondURI));
BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument));
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
}
// Remove URI + set DIDDocument
std::string const secondDocument = "data2";
{
env(did::set(alice), did::uri(""), did::document(secondDocument));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(!sleDID->isFieldPresent(sfURI));
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], secondDocument));
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
}
// Remove DIDDocument + set Data
std::string const secondData = "randomData";
{
env(did::set(alice), did::document(""), did::data(secondData));
BEAST_EXPECT(ownerCount(env, alice) == 1);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(!sleDID->isFieldPresent(sfURI));
BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument));
BEAST_EXPECT(checkVL((*sleDID)[sfData], secondData));
}
// Delete DID
{
env(did::del(alice));
BEAST_EXPECT(ownerCount(env, alice) == 0);
auto const sleDID = env.le(keylet::did(alice.id()));
BEAST_EXPECT(!sleDID);
}
}
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{
supported_amendments() | FeatureBitset{featureDID}};
testEnabled(all);
testAccountReserve(all);
testSetInvalid(all);
testDeleteInvalid(all);
testSetModify(all);
}
};
BEAST_DEFINE_TESTSUITE(DID, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -581,7 +581,7 @@ class NFToken0_test : public beast::unit_test::suite
ter(temMALFORMED));
//----------------------------------------------------------------------
// preflight
// preclaim
// Non-existent issuer.
env(token::mint(alice, 0u),

View File

@@ -35,6 +35,7 @@
#include <test/jtx/check.h>
#include <test/jtx/delivermin.h>
#include <test/jtx/deposit.h>
#include <test/jtx/did.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/genesis.h>

104
src/test/jtx/did.h Normal file
View File

@@ -0,0 +1,104 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2019 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_DID_H_INCLUDED
#define RIPPLE_TEST_JTX_DID_H_INCLUDED
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/owners.h>
namespace ripple {
namespace test {
namespace jtx {
/** DID operations. */
namespace did {
Json::Value
set(jtx::Account const& account);
Json::Value
setValid(jtx::Account const& account);
/** Sets the optional DIDDocument on a DIDSet. */
class document
{
private:
std::string document_;
public:
explicit document(std::string const& u) : document_(strHex(u))
{
}
void
operator()(jtx::Env&, jtx::JTx& jtx) const
{
jtx.jv[sfDIDDocument.jsonName] = document_;
}
};
/** Sets the optional URI on a DIDSet. */
class uri
{
private:
std::string uri_;
public:
explicit uri(std::string const& u) : uri_(strHex(u))
{
}
void
operator()(jtx::Env&, jtx::JTx& jtx) const
{
jtx.jv[sfURI.jsonName] = uri_;
}
};
/** Sets the optional Attestation on a DIDSet. */
class data
{
private:
std::string data_;
public:
explicit data(std::string const& u) : data_(strHex(u))
{
}
void
operator()(jtx::Env&, jtx::JTx& jtx) const
{
jtx.jv[sfData.jsonName] = data_;
}
};
Json::Value
del(jtx::Account const& account);
} // namespace did
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

67
src/test/jtx/impl/did.cpp Normal file
View File

@@ -0,0 +1,67 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2019 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/jss.h>
#include <test/jtx/did.h>
namespace ripple {
namespace test {
namespace jtx {
/** DID operations. */
namespace did {
Json::Value
set(jtx::Account const& account)
{
Json::Value jv;
jv[jss::TransactionType] = jss::DIDSet;
jv[jss::Account] = to_string(account.id());
jv[jss::Flags] = tfUniversal;
return jv;
}
Json::Value
setValid(jtx::Account const& account)
{
Json::Value jv;
jv[jss::TransactionType] = jss::DIDSet;
jv[jss::Account] = to_string(account.id());
jv[jss::Flags] = tfUniversal;
jv[sfURI.jsonName] = strHex(std::string{"uri"});
return jv;
}
Json::Value
del(jtx::Account const& account)
{
Json::Value jv;
jv[jss::TransactionType] = jss::DIDDelete;
jv[jss::Account] = to_string(account.id());
jv[jss::Flags] = tfUniversal;
return jv;
}
} // namespace did
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -599,6 +599,7 @@ public:
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::uri_token), 0));
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::hook), 0));
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::amm), 0));
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::did), 0));
// gw mints an NFT so we can find it.
uint256 const nftID{token::getNextID(env, gw, 0u, tfTransferable)};
@@ -863,6 +864,26 @@ public:
BEAST_EXPECT(
payChan[sfSettleDelay.jsonName].asUInt() == 24 * 60 * 60);
}
{
// gw creates a DID that we can look for in the ledger.
Json::Value jvDID;
jvDID[jss::TransactionType] = jss::DIDSet;
jvDID[jss::Flags] = tfUniversal;
jvDID[jss::Account] = gw.human();
jvDID[sfURI.jsonName] = strHex(std::string{"uri"});
env(jvDID);
env.close();
}
{
// Find the DID.
Json::Value const resp = acct_objs(gw, jss::did);
BEAST_EXPECT(acct_objs_is_size(resp, 1));
auto const& did = resp[jss::result][jss::account_objects][0u];
BEAST_EXPECT(did[sfAccount.jsonName] == gw.human());
BEAST_EXPECT(did[sfURI.jsonName] == strHex(std::string{"uri"}));
}
// Make gw multisigning by adding a signerList.
env(jtx::signers(gw, 6, {{alice, 7}}));
env.close();
@@ -890,7 +911,7 @@ public:
auto const& ticket = resp[jss::result][jss::account_objects][0u];
BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human());
BEAST_EXPECT(ticket[sfLedgerEntryType.jsonName] == jss::Ticket);
BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 10);
BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 11);
}
{
// Create a uri token.

View File

@@ -2148,6 +2148,57 @@ public:
}
}
void
testLedgerEntryDID()
{
testcase("ledger_entry Request DID");
using namespace test::jtx;
using namespace std::literals::chrono_literals;
Env env{*this};
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
// Lambda to create a DID.
auto didCreate = [](test::jtx::Account const& account) {
Json::Value jv;
jv[jss::TransactionType] = jss::DIDSet;
jv[jss::Account] = account.human();
jv[sfDIDDocument.jsonName] = strHex(std::string{"data"});
jv[sfURI.jsonName] = strHex(std::string{"uri"});
return jv;
};
env(didCreate(alice));
env.close();
std::string const ledgerHash{to_string(env.closed()->info().hash)};
{
// Request the DID using its index.
Json::Value jvParams;
jvParams[jss::did] = alice.human();
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(
jrr[jss::node][sfDIDDocument.jsonName] ==
strHex(std::string{"data"}));
BEAST_EXPECT(
jrr[jss::node][sfURI.jsonName] == strHex(std::string{"uri"}));
}
{
// Request an index that is not a DID.
Json::Value jvParams;
jvParams[jss::did] = env.master.human();
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "entryNotFound", "");
}
}
void
testLedgerEntryInvalidParams(unsigned int apiVersion)
{
@@ -2897,6 +2948,7 @@ public:
testNoQueue();
testQueue();
testLedgerAccountsOption();
testLedgerEntryDID();
test::jtx::forAllApiVersions(std::bind_front(
&LedgerRPC_test::testLedgerEntryInvalidParams, this));