Skip to content

Commit

Permalink
Check the revocationpolicy when revoking a token.
Browse files Browse the repository at this point in the history
  • Loading branch information
kantp committed Feb 2, 2024
1 parent 208de87 commit c1f2ef4
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/RevocationPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ class RevocationPolicy extends Struct ({
};
}

export { RevocationPolicy };
export { RevocationPolicy };
4 changes: 4 additions & 0 deletions src/SoulboundErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@ const SoulboundErrors = {
'Cannot issue token, because it has already been issued.',
wrongPolicy:
'RevocationPolicy does not match what is expected by the issuer',
missingHolderSignature:
'This requires a signature from the token holder',
unrevocable:
'This token cannot be revoked as per the RevocationPolicy',
};
export { SoulboundErrors };
97 changes: 69 additions & 28 deletions src/SoulboundToken.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
Field,
SmartContract, state, State, method, Signature, Struct, MerkleMap, Bool, MerkleMapWitness,
SmartContract, state, State, method, Signature, Struct, MerkleMap, Bool, MerkleMapWitness, PublicKey,
} from 'o1js';
import { RevocationPolicy } from './RevocationPolicy';
import { SoulboundMetadata, SoulboundRequest } from './SoulboundMetadata';
Expand Down Expand Up @@ -44,15 +44,17 @@ class SoulboundToken
// In this example, all tokens from this contract can be
// revoked according to the same policy
@state(RevocationPolicy) revocationPolicy = State<RevocationPolicy>();
@state(PublicKey) issuerKey = State<PublicKey>();

@method init(): void {
super.init();
const emptyMap = new MerkleMap;
this.root.set(emptyMap.getRoot());
}

public initialise(revocationPolicy: RevocationPolicy): void {
@method initialise(revocationPolicy: RevocationPolicy, issuerKey: PublicKey): void {
this.revocationPolicy.set(revocationPolicy);
this.issuerKey.set(issuerKey);
}

/** Issue a token
Expand Down Expand Up @@ -103,43 +105,82 @@ class SoulboundToken
this.root.set(newRoot);
}

/** Revoke an existing token
*
* This does not yet check the `RevocationPolicy` of the token.
*/
@method public revoke(
request: SoulboundRequest,
witness: MerkleMapWitness
) {
this.root.requireEquals(this.root.get());
this.revocationPolicy.requireEquals(this.revocationPolicy.get());

// check that the token is issued and has not yet been revoked
this.verifyAgainstRoot(this.root.get(), request.metadata, witness);

// TODO: check revocation policy

// update the merkle root to have the token revoked
const [root, _] = witness.computeRootAndKey(TokenState.types.revoked);
this.root.set(root);
}

/** Verify that a token exists and is not revoked */
@method public verify(metadata: SoulboundMetadata,
witness: MerkleMapWitness
) {
//note: we could require a signature from the owner,
// if we do not want anyone to be able to validate
this.root.requireEquals(this.root.get());
this.verifyAgainstRoot(this.root.get(), metadata, witness)
}
//note: we could require a signature from the owner,
// if we do not want anyone to be able to validate
this.root.requireEquals(this.root.get());
this.verifyAgainstRoot(this.root.get(), metadata, witness)
}

verifyAgainstRoot(expextedRoot: Field, metadata: SoulboundMetadata, witness: MerkleMapWitness) {
const expectedKey = metadata.hash();
const [root, key] = witness.computeRootAndKey(TokenState.types.issued)
expextedRoot.assertEquals(root, SoulboundErrors.invalidToken);
expectedKey.assertEquals(key, SoulboundErrors.invalidToken);
}

@method public revokeHolder(
request: SoulboundRequest,
witness: MerkleMapWitness,
holderSignature: Signature
) {
request.metadata.revocationPolicy.type.assertEquals(
RevocationPolicy.types.holderOnly
);
holderSignature.verify(
request.metadata.holderKey,
SoulboundRequest.toFields(request)
);
this.internalRevoke(request.metadata, witness);
}
@method revokeIssuer(
request: SoulboundRequest,
witness: MerkleMapWitness,
issuerSignature: Signature
) {
request.metadata.revocationPolicy.type.assertEquals(
RevocationPolicy.types.issuerOnly
);
issuerSignature.verify(
this.issuerKey.getAndRequireEquals(),
SoulboundRequest.toFields(request)
);
this.internalRevoke(request.metadata, witness);
}
@method revokeBoth(
request: SoulboundRequest,
witness: MerkleMapWitness,
holderSignature: Signature,
issuerSignature: Signature
) {
request.metadata.revocationPolicy.type.assertEquals(
RevocationPolicy.types.both
);
holderSignature.verify(
request.metadata.holderKey,
SoulboundRequest.toFields(request)
);
issuerSignature.verify(
this.issuerKey.getAndRequireEquals(),
SoulboundRequest.toFields(request)
);
this.internalRevoke(request.metadata, witness);
}
private internalRevoke(
metadata: SoulboundMetadata,
witness: MerkleMapWitness
) {
const currentRoot = this.root.getAndRequireEquals();
metadata.revocationPolicy.type.assertEquals(
this.revocationPolicy.getAndRequireEquals().type
);
this.verifyAgainstRoot(currentRoot, metadata, witness);
const [newRoot, _] = witness.computeRootAndKey(TokenState.types.revoked);
this.root.set(newRoot);
}
}

export { SoulboundToken, TokenState };
19 changes: 14 additions & 5 deletions test/SoulboundToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const accountCreationFee = 0;
const proofsEnabled = false;
const enforceTransactionLimits = false;

const revocationPolicy = new RevocationPolicy({type: RevocationPolicy.types.issuerOnly});
const revocationPolicy = new RevocationPolicy({type: RevocationPolicy.types.both});

describe('SoulboundToken', () => {

Expand Down Expand Up @@ -109,7 +109,10 @@ describe('SoulboundToken', () => {
metadata: validMetadata,
type: SoulboundRequest.types.revokeToken
});
await driver.revoke(revokeRequest);
const revokeSignature = Signature.create(
holderKey, SoulboundRequest.toFields(revokeRequest)
);
await driver.revoke(revokeRequest, revokeSignature);
});
it('fails to verify a revoked token', async () => {
await driver.deploy();
Expand All @@ -127,7 +130,10 @@ describe('SoulboundToken', () => {
metadata: validMetadata,
type: SoulboundRequest.types.revokeToken
});
await driver.revoke(revokeRequest);
const revokeSignature = Signature.create(
holderKey, SoulboundRequest.toFields(revokeRequest)
);
await driver.revoke(revokeRequest, revokeSignature);
await expect(async () => {
await driver.verify(validMetadata)
}).rejects.toThrow(SoulboundErrors.invalidToken)
Expand All @@ -139,8 +145,11 @@ describe('SoulboundToken', () => {
type: SoulboundRequest.types.revokeToken
});
await expect(async () => {
await driver.revoke(revokeRequest)
}).rejects.toThrow(SoulboundErrors.invalidToken);
const revokeSignature = Signature.create(
holderKey, SoulboundRequest.toFields(revokeRequest)
);
await driver.revoke(revokeRequest, revokeSignature);
}).rejects.toThrow(SoulboundErrors.invalidToken);
});

});
Expand Down
49 changes: 44 additions & 5 deletions test/SoulboundTokenDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Account, MerkleMap, Mina, PrivateKey, PublicKey, Signature } from "o1js
import { SoulboundMetadata , SoulboundRequest } from "../src/SoulboundMetadata";
import { SoulboundToken, TokenState } from "../src/SoulboundToken";
import { RevocationPolicy } from "../src";
import { SoulboundErrors } from "../src/SoulboundErrors";

type Account = {publicKey: PublicKey; privateKey: PrivateKey;}

Expand All @@ -27,11 +28,15 @@ class SoulboundTokenDriver{

public async deploy() {
const tx = await Mina.transaction(this.feePayerAccount.publicKey, () => {
this.issuer.initialise(this.revocationPolicy)
this.issuer.deploy();
});
await tx.prove();
await tx.sign([this.feePayerAccount.privateKey, this.issuerKey]).send();
const tx2 = await Mina.transaction(this.feePayerAccount.publicKey, () => {
this.issuer.initialise(this.revocationPolicy, this.issuerKey.toPublicKey())
});
await tx2.prove();
await tx2.sign([this.feePayerAccount.privateKey, this.issuerKey]).send();
}

public async issue(request: SoulboundRequest, signature: Signature) {
Expand All @@ -46,12 +51,46 @@ class SoulboundTokenDriver{
this.tokenMap.set(key, TokenState.types.issued);
}

public async revoke(request: SoulboundRequest) {
public async revoke(request: SoulboundRequest, holderSignature?: Signature) {
const key = request.metadata.hash();
const witness = this.tokenMap.getWitness(key);
const tx = await Mina.transaction(this.feePayerAccount.publicKey, () => {
this.issuer.revoke(request, witness);
});
let tx: Mina.Transaction;
switch (request.metadata.revocationPolicy.type) {
case RevocationPolicy.types.holderOnly:
if (typeof holderSignature !== undefined) {
tx = await Mina.transaction(this.feePayerAccount.publicKey, () => {
this.issuer.revokeHolder(request, witness, holderSignature!);
})
} else { throw(SoulboundErrors.missingHolderSignature) }
break;
case RevocationPolicy.types.issuerOnly:
// In a real world application, here would be some business logic
// to determine if the revocation should be permitted
const issuerSignature = Signature.create(
this.issuerKey,
SoulboundRequest.toFields(request));
tx = await Mina.transaction(this.feePayerAccount.publicKey, () => {
this.issuer.revokeIssuer(request, witness, issuerSignature);
})
break;
case RevocationPolicy.types.both:
// Again, insert a check whether the issuer wants to agree to
// the token being revoked
if (typeof holderSignature !== undefined) {
const issuerSignature = Signature.create(
this.issuerKey,
SoulboundRequest.toFields(request));
tx = await Mina.transaction(this.feePayerAccount.publicKey, () => {
this.issuer.revokeBoth(request, witness, holderSignature!, issuerSignature);
})
} else { throw(SoulboundErrors.missingHolderSignature) }
break;
case RevocationPolicy.types.neither:
throw(SoulboundErrors.unrevocable);
default:
throw('unexpected value for revocationPolicy')
break;
}
await tx.prove();
await tx.sign([this.feePayerAccount.privateKey]).send();
// Update the off-chain map as well
Expand Down

0 comments on commit c1f2ef4

Please sign in to comment.