Do a POW for objects

This commit is contained in:
Kagami Hiiragi 2015-01-30 22:45:45 +03:00
parent 05658e9e56
commit 2615c25425
5 changed files with 171 additions and 50 deletions

View File

@ -19,6 +19,7 @@ var promise = require("./platform").promise;
var bmcrypto = require("./crypto"); var bmcrypto = require("./crypto");
var Address = require("./address"); var Address = require("./address");
var structs = require("./structs"); var structs = require("./structs");
var POW = require("./pow");
var util = require("./_util"); var util = require("./_util");
var var_int = structs.var_int; var var_int = structs.var_int;
@ -45,11 +46,11 @@ exports.getType = function(buf) {
}; };
/** /**
* Try to get type of the given object message payoad. * Try to get type of the given object message payload.
* Note that this function doesn't do any validation because it is * Note that this function doesn't do any validation because it is
* already provided by `object.decodePayload` routine. Normally you call * already provided by `object.decodePayload` routine. Normally you call
* this for each incoming object message and then call decode function * this for each incoming object message payload and then call decode
* of the appropriate object handler. * function of the appropriate object handler.
* @param {Buffer} buf - Buffer that starts with object message payload * @param {Buffer} buf - Buffer that starts with object message payload
* @return {?integer} Object's type if any * @return {?integer} Object's type if any
*/ */
@ -61,6 +62,33 @@ exports.getPayloadType = function(buf) {
return buf.readUInt32BE(16, true); return buf.readUInt32BE(16, true);
}; };
// Prepend nonce to a given object without nonce.
function prependNonce(obj, opts) {
return new promise(function(resolve) {
assert(obj.length <= 262136, "object message payload is too big");
opts = objectAssign({}, opts);
var nonce, target, powp;
if (opts.skipPow) {
nonce = new Buffer(8);
nonce.fill(0);
resolve(Buffer.concat([nonce, obj]));
} else {
opts.payloadLength = obj.length + 8; // Compensate for nonce
target = POW.getTarget(opts);
powp = POW.doAsync({target: target, data: obj})
.then(function(nonce) {
// TODO(Kagami): We may want to receive nonce as a Buffer from
// POW module to skip conversion step.
var payload = new Buffer(opts.payloadLength);
util.writeUInt64BE(payload, nonce, 0, true);
obj.copy(payload, 8);
return payload;
});
resolve(powp);
}
});
}
/** /**
* `getpubkey` object. When a node has the hash of a public key (from an * `getpubkey` object. When a node has the hash of a public key (from an
* address) but not the public key itself, it must send out a request * address) but not the public key itself, it must send out a request
@ -130,7 +158,6 @@ var getpubkey = exports.getpubkey = {
* @return {Promise.<Buffer>} A promise that contains encoded message * @return {Promise.<Buffer>} A promise that contains encoded message
* payload when fulfilled. * payload when fulfilled.
*/ */
// FIXME(Kagami): Do a POW.
encodePayloadAsync: function(opts) { encodePayloadAsync: function(opts) {
return new promise(function(resolve) { return new promise(function(resolve) {
opts = objectAssign({}, opts); opts = objectAssign({}, opts);
@ -142,10 +169,8 @@ var getpubkey = exports.getpubkey = {
opts.version = to.version; opts.version = to.version;
opts.stream = to.stream; opts.stream = to.stream;
opts.objectPayload = to.version < 4 ? to.ripe : to.getTag(); opts.objectPayload = to.version < 4 ? to.ripe : to.getTag();
// POW calculation here. var obj = object.encodePayloadWithoutNonce(opts);
var nonce = new Buffer(8); resolve(prependNonce(obj, opts));
opts.nonce = nonce;
resolve(object.encodePayload(opts));
}); });
}, },
}; };
@ -333,7 +358,6 @@ var pubkey = exports.pubkey = {
* @return {Promise.<Buffer>} A promise that contains encoded message * @return {Promise.<Buffer>} A promise that contains encoded message
* payload when fulfilled. * payload when fulfilled.
*/ */
// FIXME(Kagami): Do a POW.
encodePayloadAsync: function(opts) { encodePayloadAsync: function(opts) {
return new promise(function(resolve) { return new promise(function(resolve) {
opts = objectAssign({}, opts); opts = objectAssign({}, opts);
@ -366,10 +390,7 @@ var pubkey = exports.pubkey = {
from.encPublicKey.slice(1), from.encPublicKey.slice(1),
]); ]);
obj = object.encodePayloadWithoutNonce(opts); obj = object.encodePayloadWithoutNonce(opts);
// POW calculation here. return resolve(prependNonce(obj, opts));
var nonce = new Buffer(8);
obj = Buffer.concat([nonce, obj]);
return resolve(obj);
} }
var pubkeyData = [ var pubkeyData = [
@ -387,17 +408,9 @@ var pubkey = exports.pubkey = {
pubkeyp = bmcrypto pubkeyp = bmcrypto
.sign(from.signPrivateKey, obj) .sign(from.signPrivateKey, obj)
.then(function(sig) { .then(function(sig) {
// POW calculation here.
var nonce = new Buffer(8);
// Append signature to the encoded object and we are done. // Append signature to the encoded object and we are done.
obj = Buffer.concat([ obj = Buffer.concat([obj, var_int.encode(sig.length), sig]);
nonce, return prependNonce(obj, opts);
obj,
var_int.encode(sig.length),
sig,
]);
assert(obj.length <= 262144, "object message payload is too big");
return obj;
}); });
return resolve(pubkeyp); return resolve(pubkeyp);
} }
@ -413,12 +426,9 @@ var pubkey = exports.pubkey = {
dataToEnc = Buffer.concat(dataToEnc); dataToEnc = Buffer.concat(dataToEnc);
return bmcrypto.encrypt(from.getPubkeyPublicKey(), dataToEnc); return bmcrypto.encrypt(from.getPubkeyPublicKey(), dataToEnc);
}).then(function(enc) { }).then(function(enc) {
// POW calculation here.
var nonce = new Buffer(8);
// Concat object header with ecnrypted data and we are done. // Concat object header with ecnrypted data and we are done.
obj = Buffer.concat([nonce, obj, enc]); obj = Buffer.concat([obj, enc]);
assert(obj.length <= 262144, "object message payload is too big"); return prependNonce(obj, opts);
return obj;
}); });
resolve(pubkeyp); resolve(pubkeyp);
}); });
@ -663,7 +673,6 @@ var msg = exports.msg = {
* @return {Promise.<Buffer>} A promise that contains encoded message * @return {Promise.<Buffer>} A promise that contains encoded message
* payload when fulfilled. * payload when fulfilled.
*/ */
// FIXME(Kagami): Do a POW.
encodePayloadAsync: function(opts) { encodePayloadAsync: function(opts) {
return new promise(function(resolve) { return new promise(function(resolve) {
// Deal with options. // Deal with options.
@ -722,12 +731,12 @@ var msg = exports.msg = {
dataToEnc = Buffer.concat(dataToEnc); dataToEnc = Buffer.concat(dataToEnc);
return bmcrypto.encrypt(to.encPublicKey, dataToEnc); return bmcrypto.encrypt(to.encPublicKey, dataToEnc);
}).then(function(enc) { }).then(function(enc) {
// POW calculation here.
var nonce = new Buffer(8);
// Concat object header with ecnrypted data and we are done. // Concat object header with ecnrypted data and we are done.
obj = Buffer.concat([nonce, obj, enc]); obj = Buffer.concat([obj, enc]);
assert(obj.length <= 262144, "object message payload is too big"); // TODO(Kagami): Merge receiver's trials/extra bytes options
return obj; // so we can calculate right POW (now we need to pass them to
// opts manually).
return prependNonce(obj, opts);
}); });
resolve(msgp); resolve(msgp);
}); });
@ -941,7 +950,6 @@ var broadcast = exports.broadcast = {
* @return {Promise.<Buffer>} A promise that contains encoded message * @return {Promise.<Buffer>} A promise that contains encoded message
* payload when fulfilled. * payload when fulfilled.
*/ */
// FIXME(Kagami): Do a POW.
encodePayloadAsync: function(opts) { encodePayloadAsync: function(opts) {
return new promise(function(resolve) { return new promise(function(resolve) {
// Deal with options. // Deal with options.
@ -986,12 +994,8 @@ var broadcast = exports.broadcast = {
dataToEnc = Buffer.concat(dataToEnc); dataToEnc = Buffer.concat(dataToEnc);
return bmcrypto.encrypt(from.getBroadcastPublicKey(), dataToEnc); return bmcrypto.encrypt(from.getBroadcastPublicKey(), dataToEnc);
}).then(function(enc) { }).then(function(enc) {
// POW calculation here. obj = Buffer.concat([obj, enc]);
var nonce = new Buffer(8); return prependNonce(obj, opts);
// Concat object header with ecnrypted data and we are done.
obj = Buffer.concat([nonce, obj, enc]);
assert(obj.length <= 262144, "object message payload is too big");
return obj;
}); });
resolve(broadp); resolve(broadp);
}); });

View File

@ -54,9 +54,9 @@ exports.getTarget = function(opts) {
}; };
exports.pow = function(opts) { exports.pow = function(opts) {
var poolSize = opts.poolSize || os.cpus().length;
// TODO(Kagami): Allow to cancel a POW (see `platform.browser.js`). // TODO(Kagami): Allow to cancel a POW (see `platform.browser.js`).
return new promise(function(resolve, reject) { return new promise(function(resolve, reject) {
var poolSize = opts.poolSize || os.cpus().length;
worker.powAsync( worker.powAsync(
poolSize, poolSize,
opts.target, opts.target,

View File

@ -16,9 +16,10 @@ var util = require("./_util");
* Calculate target * Calculate target
* @param {Object} opts - Target options * @param {Object} opts - Target options
* @return {number} Target. * @return {number} Target.
* @static
*/ */
// Just a wrapper around platform-specific implementation. // Just a wrapper around platform-specific implementation.
exports.getTarget = function(opts) { var getTarget = exports.getTarget = function(opts) {
var payloadLength = opts.payloadLength || opts.payload.length; var payloadLength = opts.payloadLength || opts.payload.length;
return platform.getTarget({ return platform.getTarget({
ttl: opts.ttl, ttl: opts.ttl,
@ -36,6 +37,10 @@ exports.getTarget = function(opts) {
exports.check = function(opts) { exports.check = function(opts) {
var initialHash; var initialHash;
var nonce; var nonce;
var target = opts.target;
if (target === undefined) {
target = getTarget(opts);
}
if (opts.payload) { if (opts.payload) {
nonce = opts.payload.slice(0, 8); nonce = opts.payload.slice(0, 8);
initialHash = bmcrypto.sha512(opts.payload.slice(8)); initialHash = bmcrypto.sha512(opts.payload.slice(8));
@ -51,8 +56,8 @@ exports.check = function(opts) {
} }
initialHash = opts.initialHash; initialHash = opts.initialHash;
} }
var targetHi = Math.floor(opts.target / 4294967296); var targetHi = Math.floor(target / 4294967296);
var targetLo = opts.target % 4294967296; var targetLo = target % 4294967296;
var dataToHash = Buffer.concat([nonce, initialHash]); var dataToHash = Buffer.concat([nonce, initialHash]);
var resultHash = bmcrypto.sha512(bmcrypto.sha512(dataToHash)); var resultHash = bmcrypto.sha512(bmcrypto.sha512(dataToHash));
var trialHi = resultHash.readUInt32BE(0, true); var trialHi = resultHash.readUInt32BE(0, true);
@ -69,13 +74,17 @@ exports.check = function(opts) {
/** /**
* Do a POW. * Do a POW.
* @param {Object} opts - Proof of work options * @param {Object} opts - Proof of work options
* @return {Promise.<Buffer>} A promise that contains computed nonce for * @param {?Buffer} opts.data - Object message payload without nonce to
* get the initial hash from
* @param {?Buffer} opts.initialHash - Or already computed initial hash
* @param {number} opts.target - POW target
* @return {Promise.<number>} A promise that contains computed nonce for
* the given target when fulfilled. * the given target when fulfilled.
*/ */
exports.doAsync = function(opts) { exports.doAsync = function(opts) {
var initialHash; var initialHash;
if (opts.payload) { if (opts.data) {
initialHash = bmcrypto.sha512(opts.payload); initialHash = bmcrypto.sha512(opts.data);
} else { } else {
initialHash = opts.initialHash; initialHash = opts.initialHash;
} }

View File

@ -188,6 +188,14 @@ var object = exports.object = {
* @return {Buffer} Encoded payload. * @return {Buffer} Encoded payload.
*/ */
encodePayload: function(opts) { encodePayload: function(opts) {
// NOTE(Kagami): We do not try to calculate nonce here if it is not
// provided because:
// 1) It's async operation but in `structs` module all operations
// are synchronous.
// 2) It shouldn't be useful because almost all objects signatures
// include object header and POW is computed for entire object so at
// first the object header should be assembled and only then we can
// do a POW.
assert(opts.nonce.length === 8, "Bad nonce"); assert(opts.nonce.length === 8, "Bad nonce");
// NOTE(Kagami): This may be a bit inefficient since we allocate // NOTE(Kagami): This may be a bit inefficient since we allocate
// twice. // twice.

104
test.js
View File

@ -191,6 +191,16 @@ describe("Common structures", function() {
encoded = Buffer.concat([encoded, Buffer(300000)]); encoded = Buffer.concat([encoded, Buffer(300000)]);
expect(object.decodePayload.bind(null, encoded)).to.throw(/too big/i); expect(object.decodePayload.bind(null, encoded)).to.throw(/too big/i);
}); });
it("shouldn't decode object with insufficient nonce", function() {
expect(object.decode.bind(null, object.encode({
nonce: Buffer(8),
ttl: 100,
type: 2,
version: 1,
objectPayload: Buffer("test"),
}))).to.throw(/insufficient/i);
});
}); });
describe("var_int", function() { describe("var_int", function() {
@ -570,6 +580,7 @@ describe("Object types", function() {
return getpubkey.encodeAsync({ return getpubkey.encodeAsync({
ttl: 100, ttl: 100,
to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU", to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return getpubkey.decodeAsync(buf, skipPow); return getpubkey.decodeAsync(buf, skipPow);
@ -587,6 +598,7 @@ describe("Object types", function() {
return getpubkey.encodeAsync({ return getpubkey.encodeAsync({
ttl: 100, ttl: 100,
to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z", to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return getpubkey.decodeAsync(buf, skipPow); return getpubkey.decodeAsync(buf, skipPow);
@ -599,6 +611,33 @@ describe("Object types", function() {
expect(res.tag.toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a"); expect(res.tag.toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a");
}); });
}); });
it("shouldn't decode getpubkey with insufficient nonce", function(done) {
return getpubkey.encodeAsync({
ttl: 100,
to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z",
skipPow: true,
}).then(getpubkey.decodeAsync).catch(function(err) {
expect(err.message).to.match(/insufficient/i);
done();
});
});
if (allTests) {
it("should encode and decode getpubkey with nonce", function() {
this.timeout(300000);
return getpubkey.encodePayloadAsync({
ttl: 100,
to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z",
}).then(function(payload) {
expect(POW.check({ttl: 100, payload: payload})).to.be.true;;
return getpubkey.decodePayloadAsync(payload);
}).then(function(res) {
expect(res.ttl).to.be.at.most(100);
expect(res.tag.toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a");
});
});
}
}); });
describe("pubkey", function() { describe("pubkey", function() {
@ -607,6 +646,7 @@ describe("Object types", function() {
ttl: 123, ttl: 123,
from: from, from: from,
to: "BM-onhypnh1UMhbQpmvdiPuG6soLLytYJAfH", to: "BM-onhypnh1UMhbQpmvdiPuG6soLLytYJAfH",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return pubkey.decodeAsync(buf, skipPow); return pubkey.decodeAsync(buf, skipPow);
@ -627,6 +667,7 @@ describe("Object types", function() {
ttl: 456, ttl: 456,
from: from, from: from,
to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU", to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return pubkey.decodeAsync(buf, skipPow); return pubkey.decodeAsync(buf, skipPow);
@ -645,7 +686,7 @@ describe("Object types", function() {
}); });
it("should encode and decode pubkey v4", function() { it("should encode and decode pubkey v4", function() {
return pubkey.encodeAsync({ttl: 789, from: from, to: from}) return pubkey.encodeAsync({ttl: 789, from: from, to: from, skipPow: true})
.then(function(buf) { .then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return pubkey.decodeAsync(buf, {needed: from, skipPow: true}); return pubkey.decodeAsync(buf, {needed: from, skipPow: true});
@ -663,6 +704,20 @@ describe("Object types", function() {
expect(bufferEqual(res.tag, from.getTag())).to.be.true; expect(bufferEqual(res.tag, from.getTag())).to.be.true;
}); });
}); });
if (allTests) {
it("should encode and decode pubkey with nonce", function() {
this.timeout(300000);
return pubkey.encodePayloadAsync({ttl: 789, from: from, to: from})
.then(function(payload) {
expect(POW.check({ttl: 789, payload: payload})).to.be.true;;
return pubkey.decodePayloadAsync(payload, {needed: from});
}).then(function(res) {
expect(res.ttl).to.be.at.most(789);
expect(bufferEqual(res.tag, from.getTag())).to.be.true;
});
});
}
}); });
describe("msg", function() { describe("msg", function() {
@ -672,6 +727,7 @@ describe("Object types", function() {
from: from, from: from,
to: from, to: from,
message: "test", message: "test",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return msg.decodeAsync(buf, {identities: [from], skipPow: true}); return msg.decodeAsync(buf, {identities: [from], skipPow: true});
@ -701,6 +757,7 @@ describe("Object types", function() {
from: fromV2, from: fromV2,
to: fromV2, to: fromV2,
message: "test", message: "test",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return msg.decodeAsync(buf, {identities: [fromV2], skipPow: true}); return msg.decodeAsync(buf, {identities: [fromV2], skipPow: true});
@ -730,6 +787,7 @@ describe("Object types", function() {
from: from, from: from,
to: from, to: from,
message: "test", message: "test",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
return msg.decodeAsync(buf, {identities: [], skipPow: true}); return msg.decodeAsync(buf, {identities: [], skipPow: true});
}).catch(function(err) { }).catch(function(err) {
@ -746,6 +804,7 @@ describe("Object types", function() {
encoding: msg.SIMPLE, encoding: msg.SIMPLE,
subject: "Тема", subject: "Тема",
message: "Сообщение", message: "Сообщение",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
return msg.decodeAsync(buf, {identities: [from], skipPow: true}); return msg.decodeAsync(buf, {identities: [from], skipPow: true});
}).then(function(res) { }).then(function(res) {
@ -761,11 +820,30 @@ describe("Object types", function() {
from: from, from: from,
to: from, to: from,
message: Buffer(300000), message: Buffer(300000),
skipPow: true,
}).catch(function(err) { }).catch(function(err) {
expect(err.message).to.match(/too big/i); expect(err.message).to.match(/too big/i);
done(); done();
}); });
}); });
if (allTests) {
it("should encode and decode msg with nonce", function() {
this.timeout(300000);
return msg.encodePayloadAsync({
ttl: 111,
from: from,
to: from,
message: "test",
}).then(function(payload) {
expect(POW.check({ttl: 111, payload: payload})).to.be.true;;
return msg.decodePayloadAsync(payload, {identities: from});
}).then(function(res) {
expect(res.ttl).to.be.at.most(111);
expect(res.message).to.equal("test");
});
});
}
}); });
describe("broadcast", function() { describe("broadcast", function() {
@ -774,6 +852,7 @@ describe("Object types", function() {
ttl: 987, ttl: 987,
from: fromV3, from: fromV3,
message: "test", message: "test",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return broadcast.decodeAsync(buf, {subscriptions: fromV3, skipPow: true}); return broadcast.decodeAsync(buf, {subscriptions: fromV3, skipPow: true});
@ -801,6 +880,7 @@ describe("Object types", function() {
ttl: 999, ttl: 999,
from: fromV2, from: fromV2,
message: "test", message: "test",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return broadcast.decodeAsync(buf, {subscriptions: fromV2, skipPow: true}); return broadcast.decodeAsync(buf, {subscriptions: fromV2, skipPow: true});
@ -828,11 +908,12 @@ describe("Object types", function() {
ttl: 101, ttl: 101,
from: from, from: from,
message: "キタ━━━(゜∀゜)━━━!!!!!", message: "キタ━━━(゜∀゜)━━━!!!!!",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
expect(message.decode(buf).command).to.equal("object"); expect(message.decode(buf).command).to.equal("object");
return broadcast.decodeAsync(buf, {subscriptions: [from], skipPow: true}); return broadcast.decodeAsync(buf, {subscriptions: [from], skipPow: true});
}).then(function(res) { }).then(function(res) {
expect(res.ttl).to.be.at.most(987); expect(res.ttl).to.be.at.most(101);
expect(res.type).to.equal(object.BROADCAST); expect(res.type).to.equal(object.BROADCAST);
expect(res.version).to.equal(5); expect(res.version).to.equal(5);
expect(res.stream).to.equal(1); expect(res.stream).to.equal(1);
@ -855,6 +936,7 @@ describe("Object types", function() {
ttl: 101, ttl: 101,
from: from, from: from,
message: "test", message: "test",
skipPow: true,
}).then(function(buf) { }).then(function(buf) {
return broadcast.decodeAsync(buf, { return broadcast.decodeAsync(buf, {
subscriptions: [fromV3], subscriptions: [fromV3],
@ -871,11 +953,29 @@ describe("Object types", function() {
ttl: 101, ttl: 101,
from: from, from: from,
message: Buffer(300000), message: Buffer(300000),
skipPow: true,
}).catch(function(err) { }).catch(function(err) {
expect(err.message).to.match(/too big/i); expect(err.message).to.match(/too big/i);
done(); done();
}); });
}); });
if (allTests) {
it("should encode and decode broadcast with nonce", function() {
this.timeout(300000);
return broadcast.encodePayloadAsync({
ttl: 101,
from: from,
message: "test",
}).then(function(payload) {
expect(POW.check({ttl: 101, payload: payload})).to.be.true;;
return broadcast.decodePayloadAsync(payload, {subscriptions: from});
}).then(function(res) {
expect(res.ttl).to.be.at.most(101);
expect(res.message).to.equal("test");
});
});
}
}); });
}); });