This project can be used to create a virtual currency with the following properties:
- centralized: no consensus mecanism, no transaction fees.
- secured with cryptography: based on ECDSA NIST P-256 (which is compatible with all recent browsers).
- privacy centered: even with a full database access, it's impossible to know who owns what.
- In the database, we have a list of coinIds associated with a public key.
- These coins have a fixed facial value (modulo of their identifier, see code).
- Whoever possess the private key associated with the coin can sign a message to allow its transfer to another public key.
=> Coins have the same properties as the cash for privacy! Drawback: users have to store their own private keys (one for each coin).
$ git clone https://github.com/forevertz/centralized-crypto-coin.git
$ cd centralized-crypto-coin
$ yarn run dev
$ git clone https://github.com/forevertz/centralized-crypto-coin.git
$ cd centralized-crypto-coin
$ docker-compose up -d
# Recommended: install an SSL certificate and allow only https
# Then go to https://yourhost:3001/keys/init and save the control key in a safe storage
# Your public API is now running on https://yourhost:3000
For POST requests, headers must include:
X-Public-Key
: compressed base64 public key associated with the control private key (generated with/keys/init
)X-Signature
:BASE64(SIGN(SHA256(POST payload)))
Content-Type
:application/json
orapplication/json; charset=utf-8
Note: Content-Length
is limited to 1000 code points.
- GET /keys/init: Generates initial control and response key pairs
- POST /keys/control: Set new control public key
- POST /coins/create: Create new coins
POST /keys/control
const ECDSA = require('ecdsa-secp256r1')
;(async function() {
const currentControlKey = await ECDSA.fromJWK({
// use your own control key
kty: 'EC',
crv: 'P-256',
x: '4YdUIhIDncVu5tScgjxthiXOO_el11FWb56gR3qnhVQ',
y: 'UyEvWOJbMZa9PtggGeRC9iQcAzOZZsyXpFE1qaF6jFk',
d: 'TYVI2fW-nHSPGCx0MhWasg2Ggiyl1E_Kq4D1A5LmkxU'
})
const newControlKey = await ECDSA.generateKey()
const message = JSON.stringify({
newPublicKey: await newControlKey.toBase64CompressedPublicKey()
})
try {
const response = await fetch('https://yourdomain:3001/keys/control', {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
'X-Public-Key': await currentControlKey.toBase64CompressedPublicKey(),
'X-Signature': await currentControlKey.hashAndSign(message)
}),
body: message
})
// You should save the new control key!
console.log(await response.json())
} catch (error) {
console.error(error)
}
})()
POST /coins/create
const ECDSA = require('ecdsa-secp256r1')
;(async function() {
const currentControlKey = await ECDSA.fromJWK({
// use your own control key
kty: 'EC',
crv: 'P-256',
x: '4YdUIhIDncVu5tScgjxthiXOO_el11FWb56gR3qnhVQ',
y: 'UyEvWOJbMZa9PtggGeRC9iQcAzOZZsyXpFE1qaF6jFk',
d: 'TYVI2fW-nHSPGCx0MhWasg2Ggiyl1E_Kq4D1A5LmkxU'
})
const message = JSON.stringify({
amount: 24 // between 1 and 500
})
try {
const response = await fetch('https://yourdomain:3001/coins/create', {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
'X-Public-Key': await currentControlKey.toBase64CompressedPublicKey(),
'X-Signature': await currentControlKey.hashAndSign(message)
}),
body: message
})
// Save the key pairs of the created coins
console.log(await response.json())
} catch (error) {
console.error(error)
}
})()
For POST requests, headers must include:
X-Public-Key
: compressed base64 public key associated with one of user's private keyX-Signature
:BASE64(SIGN(SHA256(POST payload)))
Content-Type
:application/json
orapplication/json; charset=utf-8
Note: Content-Length
is limited to 1000 code points.
- GET /doc: List all API endpoints
- GET /time: Get server time (ISO 8601)
- GET /keys/response: Get the response compressed ECDSA public key
- GET /coins/value: Get the value of the given coins
- POST /coins/transfer: Transfer the ownership of coins by changing their public key
GET /coins/value
fetch('https://yourdomain:3000/coins/value?ids=[1,2,3,4,5]')
.then(r => r.json())
.then(console.log)
.catch(console.error)
POST /coins/transfer
Let's say Alice wants to send Bob coins 3 and 9.
# Alice
const ECDSA = require('ecdsa-secp256r1')
;(async function() {
const aliceCoinKeys = {
3: await ECDSA.fromJWK({ kty: 'EC', crv: 'P-256' /* ... coin 3 current private key */ }),
9: await ECDSA.fromJWK({ kty: 'EC', crv: 'P-256' /* ... coin 9 current private key */ })
}
const signForTransfer = async (coinId, privateKey, timestamp) => {
const publicKey = await privateKey.toBase64CompressedPublicKey()
return privateKey.hashAndSign(`${coinId}-${publicKey}-${timestamp}`)
}
const now = Math.floor(Date.now() / 1000)
const messageToBob = [
{ coinId: 3, timestamp: now - 1, signature: await signForTransfer(3, aliceCoinKeys[3], now - 1) },
{ coinId: 9, timestamp: now + 2, signature: await signForTransfer(9, aliceCoinKeys[9], now + 2) }
]
/*
Send message to Bob!
Note: timestamps can be scrambled to improve privacy, but server will only accept them
if they have less than 2 minutes difference with its current time.
*/
})()
# Bob
const ECDSA = require('ecdsa-secp256r1')
;(async function(messageFromAlice) {
const randomKey = await ECDSA.generateKey() // does not really matter which key Bob uses
const message = JSON.stringify(
messageFromAlice.map(({ coinId, timestamp, signature }) => {
const newPrivateKey = await ECDSA.generateKey()
// You should save the new private key associated with the coinId!
const newPublicKey = await newPrivateKey.toBase64CompressedPublicKey()
return { coinId, timestamp, signature, newPublicKey }
})
)
try {
const response = await fetch('https://yourdomain:3000/coins/transfer', {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
'X-Public-Key': await randomKey.toBase64CompressedPublicKey(),
'X-Signature': await randomKey.hashAndSign(message)
}),
body: message
})
console.log(await response.json())
} catch (error) {
console.error(error)
}
})()