Skip to content

Commit

Permalink
Add support for DID attributes
Browse files Browse the repository at this point in the history
Implemented as a stream of events, as per EIP-1056.
By applying all changes to the attributes of a user, the currently valid
set of attributes can be obtained.

This greatly reduces the complexity of the smart contract.
As the attributes are only read from off-chain applications, this is
feasible.

See: ethereum/EIPs#1056
See: https://eips.ethereum.org/EIPS/eip-1056
  • Loading branch information
juliusrickert committed Sep 11, 2020
1 parent bf4a313 commit cbfd581
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 3 deletions.
97 changes: 96 additions & 1 deletion contracts/UserRegistry.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity ^0.5.0;
pragma solidity >= 0.5.0 <= 0.7.0;

import { Delegation } from "./Delegation.sol";

Expand All @@ -15,6 +15,14 @@ contract UserRegistry {
event UserTransferred(bytes32 name);
//event UserDeleted(bytes32);

event DIDAttributeChanged(
bytes32 userName,
bytes32 attrName,
bytes value, // Not JSON, but custom, more efficient encoding
uint validTo, // Used to limit time or invalidate attribute
uint previousChange // Used to query all change events
);

struct User {
bytes32 name;
bytes agentId;
Expand All @@ -24,6 +32,13 @@ contract UserRegistry {

mapping (bytes32 => User) public users;

// Number of the block where the last change to event-based storage of a user (by name) occured.
mapping (bytes32 => uint) changed;

// Nonce to prevent replay attacks (mapped by owner)
// Owner needs to include nonce in signature
mapping (address => uint) nonce;

modifier onlyOwnName(bytes32 name) {
require(name != 0, "Empty name is not owned by anyone.");
require(users[name].owner == msg.sender, "Sender does not own name.");
Expand Down Expand Up @@ -51,6 +66,13 @@ contract UserRegistry {
return users[userName].owner == claimedOwner;
}

// onlyOwner is the modifier version of isOwner
modifier onlyOwner(address claimedOwner, bytes32 userName) {
require(isOwner(claimedOwner, userName));

_;
}

function register(bytes32 name, bytes memory agentId, bytes memory publicKey) public {
_register(User(name, agentId, publicKey, msg.sender));
}
Expand Down Expand Up @@ -108,4 +130,77 @@ contract UserRegistry {
users[name].owner = newOwner;
emit UserTransferred(name);
}

// _setAttribute adds an DID attribute to a user.
// It references the block of the last change to the user's attributes.
// This allows us to quickly iterate over a user's change events to build the attribute objects.
//
// Existence of user verified by onlyOwner
function _setAttribute(
bytes32 userName,
address actor,
bytes32 attrName,
bytes memory value,
uint validity
) internal onlyOwner(actor, userName) {
emit DIDAttributeChanged(userName, attrName, value, now + validity, changed[userName]);
changed[userName] = block.number;
}

function setAttribute(
bytes32 userName,
bytes32 attrName,
bytes memory value,
uint validity
) public {
_setAttribute(userName, msg.sender, attrName, value, validity);
}

function delegatedSetAttribute(
bytes32 userName,
bytes32 attrName,
bytes memory value,
uint validity,
address consentee,
bytes memory consentSignature
) public onlyOwner(consentee, userName) {
// first 8 chars of keccak("setAttribute(bytes32,bytes32,bytes,uint)")
bytes memory methodId = hex"5516e043";
bytes memory args = abi.encode(userName, attrName, value, validity, nonce[consentee]);
Delegation.checkConsent(methodId, args, consentee, consentSignature);
nonce[consentee]++;

_setAttribute(userName, consentee, attrName, value, validity);
}

// _revokeAttribute revokes an attribute by setting its validTo to 0
// Cannot "remove" an attribute since it was written to the blockchain.
// Wording chosen to avoid confusion.
function _revokeAttribute(
bytes32 userName,
address actor,
bytes32 attrName
) internal onlyOwner(actor, userName) {
emit DIDAttributeChanged(userName, attrName, "", 0, changed[userName]);
changed[userName] = block.number;
}

function revokeAttribute(bytes32 userName, bytes32 attrName) public {
_revokeAttribute(userName, msg.sender, attrName);
}

function delegatedRevokeAttribute(
bytes32 userName,
bytes32 attrName,
address consentee,
bytes memory consentSignature
) public {
// first 8 chars of keccak("revokeAttribute(bytes32,bytes32)")
bytes memory methodId = hex"61991dbe";
bytes memory args = abi.encode(userName, attrName, nonce[consentee]);
Delegation.checkConsent(methodId, args, consentee, consentSignature);
nonce[consentee]++;

_revokeAttribute(userName, consentee, attrName);
}
}
63 changes: 61 additions & 2 deletions test/UserRegistryTest.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require('web3')

require('./support/setup')

const UserRegistryContract = artifacts.require('UserRegistry')
Expand All @@ -15,6 +17,12 @@ const agent = {
publicKey: web3.utils.utf8ToHex('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUpwmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ51s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQAB')
}

const service = {
attrName: web3.utils.utf8ToHex('did/svc/LearningLayers'),
value: web3.utils.utf8ToHex('https://api.learning-layers.eu/o/oauth2/'),
validity: web3.utils.numberToHex(1000 * 365 * 86400) // a long time
}

contract('UserRegistryContract', accounts => {
let userRegistry

Expand Down Expand Up @@ -44,7 +52,7 @@ contract('UserRegistryContract', accounts => {
return Promise.all([
registrationResult.should.nested.include({
'logs[0].event': 'UserRegistered',
'logs[0].args.name': '0x416c696365000000000000000000000000000000000000000000000000000000' // web3.fromAscii(agent.name, 64) // broken in web3 v0.2x.x
'logs[0].args.name': web3.utils.padRight(agent.name, 64)
}),
(userRegistry.nameIsTaken(agent.name)).should.eventually.be.true,
(userRegistry.nameIsAvailable(agent.name)).should.eventually.be.false
Expand All @@ -66,8 +74,59 @@ contract('UserRegistryContract', accounts => {
return Promise.all([
transferResult.should.nested.include({
'logs[0].event': 'UserTransferred',
'logs[0].args.name': '0x416c696365000000000000000000000000000000000000000000000000000000' // web3.fromAscii(agent.name, 64) // broken
'logs[0].args.name': web3.utils.padRight(agent.name, 64)
})
])
})

// DID
it('sets attribute', async () => {
await userRegistry.register(agent.name, agent.id, agent.publicKey)
const setAttrResult = await userRegistry.setAttribute(agent.name, service.attrName, service.value, service.validity)

return Promise.all([
setAttrResult.should.nested.include({
'logs[0].event': 'DIDAttributeChanged',
'logs[0].args.userName': web3.utils.padRight(agent.name, 64),
'logs[0].args.attrName': web3.utils.padRight(service.attrName, 64),
'logs[0].args.value': web3.utils.padRight(service.value, 64)
}),
parseInt(setAttrResult.logs[0].args.validTo).should.be.at.most(Date.now() + 1000 * 365 * 86400)
])
})

it('sets previousChange correctly when setting an attribute', async () => {
await userRegistry.register(agent.name, agent.id, agent.publicKey)
const setAttrResult1 = await userRegistry.setAttribute(agent.name, service.attrName, service.value, service.validity)
const setAttrResult2 = await userRegistry.setAttribute(agent.name, service.attrName, service.value, service.validity)

return Promise.all([
setAttrResult2.logs[0].args.previousChange.should.be.bignumber.equal(web3.utils.toBN(setAttrResult1.receipt.blockNumber))
])
})

it('revokes attribute', async () => {
await userRegistry.register(agent.name, agent.id, agent.publicKey)
await userRegistry.setAttribute(agent.name, service.attrName, service.value, service.validity)
const revokeAttrResult = await userRegistry.revokeAttribute(agent.name, service.attrName)

return Promise.all([
revokeAttrResult.should.nested.include({
'logs[0].event': 'DIDAttributeChanged',
'logs[0].args.userName': web3.utils.padRight(agent.name, 64),
'logs[0].args.attrName': web3.utils.padRight(service.attrName, 64)
}),
revokeAttrResult.logs[0].args.validTo.should.be.bignumber.equal(web3.utils.toBN(0))
])
})

it('sets previousChange correctly when revoking an attribute', async () => {
await userRegistry.register(agent.name, agent.id, agent.publicKey)
const setAttrResult = await userRegistry.setAttribute(agent.name, service.attrName, service.value, service.validity)
const revokeAttrResult = await userRegistry.revokeAttribute(agent.name, service.attrName)

return Promise.all([
revokeAttrResult.logs[0].args.previousChange.should.be.bignumber.equal(web3.utils.toBN(setAttrResult.receipt.blockNumber))
])
})
})

0 comments on commit cbfd581

Please sign in to comment.