mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Add transaction signing basics.
Most fields aren't supported yet and there aren't any nice external APIs, but my test script successfully serializes and signs an XRP Payment, so this seems like a good time to make a commit.
This commit is contained in:
@@ -851,6 +851,17 @@ Remote.prototype.request_wallet_accounts = function (seed) {
|
||||
return request;
|
||||
};
|
||||
|
||||
Remote.prototype.request_sign = function (secret, tx_json) {
|
||||
utils.assert(this.trusted); // Don't send secrets.
|
||||
|
||||
var request = new Request(this, 'sign');
|
||||
|
||||
request.message.secret = secret;
|
||||
request.message.tx_json = tx_json;
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
// Submit a transaction.
|
||||
Remote.prototype.submit = function (transaction) {
|
||||
var self = this;
|
||||
@@ -1134,7 +1145,9 @@ Remote.prototype.request_ripple_path_find = function (src_account, dst_account,
|
||||
request.message.source_account = UInt160.json_rewrite(src_account);
|
||||
request.message.destination_account = UInt160.json_rewrite(dst_account);
|
||||
request.message.destination_amount = Amount.json_rewrite(dst_amount);
|
||||
request.message.source_currencies = source_currencies.map(function (ci) {
|
||||
|
||||
if (source_currencies) {
|
||||
request.message.source_currencies = source_currencies.map(function (ci) {
|
||||
var ci_new = {};
|
||||
|
||||
if ('issuer' in ci)
|
||||
@@ -1145,6 +1158,7 @@ Remote.prototype.request_ripple_path_find = function (src_account, dst_account,
|
||||
|
||||
return ci_new;
|
||||
});
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
// Seed support
|
||||
//
|
||||
|
||||
var sjcl = require('../../build/sjcl');
|
||||
var utils = require('./utils');
|
||||
var jsbn = require('./jsbn');
|
||||
|
||||
var BigInteger = jsbn.BigInteger;
|
||||
|
||||
var Base = require('./base').Base;
|
||||
var Base = require('./base').Base,
|
||||
UInt256 = require('./uint256').UInt256;
|
||||
|
||||
var Seed = function () {
|
||||
// Internal form: NaN or BigInteger
|
||||
@@ -20,9 +22,9 @@ Seed.json_rewrite = function (j) {
|
||||
|
||||
// Return a new Seed from j.
|
||||
Seed.from_json = function (j) {
|
||||
return 'string' === typeof j
|
||||
? (new Seed()).parse_json(j)
|
||||
: j.clone();
|
||||
return (j instanceof Seed)
|
||||
? j.clone()
|
||||
: (new Seed()).parse_json(j);
|
||||
};
|
||||
|
||||
Seed.is_valid = function (j) {
|
||||
@@ -86,4 +88,42 @@ Seed.prototype.to_json = function () {
|
||||
return output;
|
||||
};
|
||||
|
||||
function append_int(a, i) {
|
||||
return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff);
|
||||
}
|
||||
|
||||
function firstHalfOfSHA512(bytes) {
|
||||
return sjcl.bitArray.bitSlice(
|
||||
sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)),
|
||||
0, 256
|
||||
);
|
||||
}
|
||||
|
||||
function SHA256_RIPEMD160(bits) {
|
||||
return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
|
||||
}
|
||||
|
||||
Seed.prototype.generate_private = function (account_id) {
|
||||
// XXX If account_id is given, should loop over keys until we find the right one
|
||||
|
||||
var seq = 0;
|
||||
|
||||
var private_gen, public_gen, i = 0;
|
||||
do {
|
||||
private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i)));
|
||||
i++;
|
||||
} while (!sjcl.ecc.curves.c256.r.greaterEquals(private_gen));
|
||||
|
||||
public_gen = sjcl.ecc.curves.c256.G.mult(private_gen);
|
||||
|
||||
var sec;
|
||||
i = 0;
|
||||
do {
|
||||
sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i)));
|
||||
i++;
|
||||
} while (!sjcl.ecc.curves.c256.r.greaterEquals(sec));
|
||||
|
||||
return UInt256.from_bn(sec);
|
||||
};
|
||||
|
||||
exports.Seed = Seed;
|
||||
|
||||
144
src/js/serializedobject.js
Normal file
144
src/js/serializedobject.js
Normal file
@@ -0,0 +1,144 @@
|
||||
var binformat = require('./binformat'),
|
||||
sjcl = require('../../build/sjcl'),
|
||||
extend = require('extend'),
|
||||
stypes = require('./serializedtypes');
|
||||
|
||||
var UInt256 = require('./uint256').UInt256;
|
||||
|
||||
var SerializedObject = function () {
|
||||
this.buffer = [];
|
||||
this.pointer = 0;
|
||||
};
|
||||
|
||||
SerializedObject.from_json = function (obj) {
|
||||
var typedef;
|
||||
var so = new SerializedObject();
|
||||
|
||||
// Create a copy of the object so we don't modify it
|
||||
obj = extend({}, obj);
|
||||
|
||||
if ("number" === typeof obj.TransactionType) {
|
||||
obj.TransactionType = SerializedObject.lookup_type_tx(obj.TransactionType);
|
||||
|
||||
if (!obj.TransactionType) {
|
||||
throw new Error("Transaction type ID is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
if ("string" === typeof obj.TransactionType) {
|
||||
typedef = binformat.tx[obj.TransactionType].slice();
|
||||
|
||||
obj.TransactionType = typedef.shift();
|
||||
} else if ("undefined" !== typeof obj.LedgerEntryType) {
|
||||
// XXX: TODO
|
||||
throw new Error("Ledger entry binary format not yet implemented.");
|
||||
} else throw new Error("Object to be serialized must contain either " +
|
||||
"TransactionType or LedgerEntryType.");
|
||||
|
||||
so.serialize(typedef, obj);
|
||||
|
||||
return so;
|
||||
};
|
||||
|
||||
SerializedObject.prototype.append = function (bytes) {
|
||||
this.buffer = this.buffer.concat(bytes);
|
||||
this.pointer += bytes.length;
|
||||
};
|
||||
|
||||
SerializedObject.prototype.to_bits = function ()
|
||||
{
|
||||
return sjcl.codec.bytes.toBits(this.buffer);
|
||||
};
|
||||
|
||||
SerializedObject.prototype.to_hex = function () {
|
||||
return sjcl.codec.hex.fromBits(this.to_bits()).toUpperCase();
|
||||
};
|
||||
|
||||
SerializedObject.prototype.serialize = function (typedef, obj)
|
||||
{
|
||||
// Ensure canonical order
|
||||
typedef = SerializedObject._sort_typedef(typedef.slice());
|
||||
|
||||
// Serialize fields
|
||||
for (var i = 0, l = typedef.length; i < l; i++) {
|
||||
var spec = typedef[i];
|
||||
this.serialize_field(spec, obj);
|
||||
}
|
||||
};
|
||||
|
||||
SerializedObject.prototype.signing_hash = function (prefix)
|
||||
{
|
||||
var sign_buffer = new SerializedObject();
|
||||
stypes.Int32.serialize(sign_buffer, prefix);
|
||||
sign_buffer.append(this.buffer);
|
||||
return sign_buffer.hash_sha512_half();
|
||||
};
|
||||
|
||||
SerializedObject.prototype.hash_sha512_half = function ()
|
||||
{
|
||||
var bits = sjcl.codec.bytes.toBits(this.buffer),
|
||||
hash = sjcl.bitArray.bitSlice(sjcl.hash.sha512.hash(bits), 0, 256);
|
||||
|
||||
return UInt256.from_hex(sjcl.codec.hex.fromBits(hash));
|
||||
};
|
||||
|
||||
SerializedObject.prototype.serialize_field = function (spec, obj)
|
||||
{
|
||||
spec = spec.slice();
|
||||
|
||||
var name = spec.shift(),
|
||||
presence = spec.shift(),
|
||||
field_id = spec.shift(),
|
||||
Type = spec.shift();
|
||||
|
||||
if ("undefined" !== typeof obj[name]) {
|
||||
console.log(name, Type.id, field_id);
|
||||
this.append(SerializedObject.get_field_header(Type.id, field_id));
|
||||
|
||||
try {
|
||||
Type.serialize(this, obj[name]);
|
||||
} catch (e) {
|
||||
// Add field name to message and rethrow
|
||||
e.message = "Error serializing '"+name+"': "+e.message;
|
||||
throw e;
|
||||
}
|
||||
} else if (presence === binformat.REQUIRED) {
|
||||
throw new Error('Missing required field '+name);
|
||||
}
|
||||
};
|
||||
|
||||
SerializedObject.get_field_header = function (type_id, field_id)
|
||||
{
|
||||
var buffer = [0];
|
||||
if (type_id > 0xf) buffer.push(type_id & 0xff);
|
||||
else buffer[0] += (type_id & 0xf) << 4;
|
||||
|
||||
if (field_id > 0xf) buffer.push(field_id & 0xff);
|
||||
else buffer[0] += field_id & 0xf;
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
function sort_field_compare(a, b) {
|
||||
// Sort by type id first, then by field id
|
||||
return a[3].id !== b[3].id ?
|
||||
a[3].id - b[3].id :
|
||||
a[2] - b[2];
|
||||
};
|
||||
SerializedObject._sort_typedef = function (typedef) {
|
||||
return typedef.sort(sort_field_compare);
|
||||
};
|
||||
|
||||
SerializedObject.lookup_type_tx = function (id) {
|
||||
for (var i in binformat.tx) {
|
||||
if (!binformat.tx.hasOwnProperty(i)) continue;
|
||||
|
||||
if (binformat.tx[i][0] === id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
exports.SerializedObject = SerializedObject;
|
||||
@@ -1,10 +1,55 @@
|
||||
var SerializedType = function () {
|
||||
/**
|
||||
* Type definitions for binary format.
|
||||
*
|
||||
* This file should not be included directly. Instead, find the format you're
|
||||
* trying to parse or serialize in binformat.js and pass that to
|
||||
* SerializedObject.parse() or SerializedObject.serialize().
|
||||
*/
|
||||
|
||||
var extend = require('extend'),
|
||||
utils = require('./utils'),
|
||||
sjcl = require('../../build/sjcl');
|
||||
|
||||
var amount = require('./amount'),
|
||||
UInt160 = amount.UInt160,
|
||||
Amount = amount.Amount;
|
||||
|
||||
// Shortcuts
|
||||
var hex = sjcl.codec.hex,
|
||||
bytes = sjcl.codec.bytes;
|
||||
|
||||
var SerializedType = function (methods) {
|
||||
extend(this, methods);
|
||||
};
|
||||
|
||||
SerializedType.prototype.serialize_hex = function (so, hexData) {
|
||||
var byteData = bytes.fromBits(hex.toBits(hexData));
|
||||
this.serialize_varint(so, byteData.length);
|
||||
so.append(byteData);
|
||||
};
|
||||
|
||||
SerializedType.prototype.serialize_varint = function (so, val) {
|
||||
if (val < 0) {
|
||||
throw new Error("Variable integers are unsigned.");
|
||||
}
|
||||
if (val <= 192) {
|
||||
so.append([val]);
|
||||
} else if (val <= 12,480) {
|
||||
val -= 193;
|
||||
so.append([193 + (val >>> 8), val & 0xff]);
|
||||
} else if (val <= 918744) {
|
||||
val -= 12481;
|
||||
so.append([
|
||||
241 + (val >>> 16),
|
||||
val >>> 8 & 0xff,
|
||||
val & 0xff
|
||||
]);
|
||||
} else throw new Error("Variable integer overflow.");
|
||||
};
|
||||
|
||||
exports.Int8 = new SerializedType({
|
||||
serialize: function (so, val) {
|
||||
return so.append([val & 0xff]);
|
||||
so.append([val & 0xff]);
|
||||
},
|
||||
parse: function (so) {
|
||||
return so.read(1)[0];
|
||||
@@ -13,7 +58,10 @@ exports.Int8 = new SerializedType({
|
||||
|
||||
exports.Int16 = new SerializedType({
|
||||
serialize: function (so, val) {
|
||||
// XXX
|
||||
so.append([
|
||||
val >>> 8 & 0xff,
|
||||
val & 0xff
|
||||
]);
|
||||
},
|
||||
parse: function (so) {
|
||||
// XXX
|
||||
@@ -22,7 +70,12 @@ exports.Int16 = new SerializedType({
|
||||
|
||||
exports.Int32 = new SerializedType({
|
||||
serialize: function (so, val) {
|
||||
// XXX
|
||||
so.append([
|
||||
val >>> 24 & 0xff,
|
||||
val >>> 16 & 0xff,
|
||||
val >>> 8 & 0xff,
|
||||
val & 0xff
|
||||
]);
|
||||
},
|
||||
parse: function (so) {
|
||||
// XXX
|
||||
@@ -67,7 +120,62 @@ exports.Hash160 = new SerializedType({
|
||||
|
||||
exports.Amount = new SerializedType({
|
||||
serialize: function (so, val) {
|
||||
// XXX
|
||||
var amount = Amount.from_json(val);
|
||||
if (!amount.is_valid()) {
|
||||
throw new Error("Not a valid Amount object.");
|
||||
}
|
||||
|
||||
// Amount (64-bit integer)
|
||||
if (amount.is_native()) {
|
||||
var valueHex = amount._value.toString(16);
|
||||
|
||||
// Enforce correct length (64 bits)
|
||||
if (valueHex.length > 16) {
|
||||
throw new Error('Value out of bounds');
|
||||
}
|
||||
while (valueHex.length < 16) {
|
||||
valueHex = "0" + valueHex;
|
||||
}
|
||||
|
||||
var valueBytes = bytes.fromBits(hex.toBits(valueHex));
|
||||
// Clear most significant two bits - these bits should already be 0 if
|
||||
// Amount enforces the range correctly, but we'll clear them anyway just
|
||||
// so this code can make certain guarantees about the encoded value.
|
||||
valueBytes[0] &= 0x3f;
|
||||
if (!amount.is_negative()) valueBytes[0] |= 0x40;
|
||||
|
||||
so.append(valueBytes);
|
||||
} else {
|
||||
// XXX
|
||||
throw new Error("Non-native amounts not implemented!");
|
||||
}
|
||||
|
||||
if (!amount.is_native()) {
|
||||
// Currency (160-bit hash)
|
||||
var currency = amount.currency().to_json();
|
||||
if ("string" === typeof currency && currency.length === 3) {
|
||||
var currencyCode = currency.toUpperCase(),
|
||||
currencyData = utils.arraySet(20, 0);
|
||||
|
||||
if (!/^[A-Z]{3}$/.test(currencyCode)) {
|
||||
throw new Error('Invalid currency code');
|
||||
}
|
||||
|
||||
currencyData[12] = currencyCode.charCodeAt(0) & 0xff;
|
||||
currencyData[13] = currencyCode.charCodeAt(1) & 0xff;
|
||||
currencyData[14] = currencyCode.charCodeAt(2) & 0xff;
|
||||
|
||||
var currencyBits = bytes.toBits(currencyData),
|
||||
currencyHash = sjcl.hash.ripemd160.hash(currencyBits);
|
||||
|
||||
so.append(bytes.fromBits(currencyHash));
|
||||
} else {
|
||||
throw new Error('Tried to serialize invalid/unimplemented currency type.');
|
||||
}
|
||||
|
||||
// Issuer (160-bit hash)
|
||||
// XXX
|
||||
}
|
||||
},
|
||||
parse: function (so) {
|
||||
// XXX
|
||||
@@ -76,7 +184,8 @@ exports.Amount = new SerializedType({
|
||||
|
||||
exports.VariableLength = new SerializedType({
|
||||
serialize: function (so, val) {
|
||||
// XXX
|
||||
if ("string" === typeof val) this.serialize_hex(so, val);
|
||||
else throw new Error("Unknown datatype.");
|
||||
},
|
||||
parse: function (so) {
|
||||
// XXX
|
||||
@@ -85,7 +194,8 @@ exports.VariableLength = new SerializedType({
|
||||
|
||||
exports.Account = new SerializedType({
|
||||
serialize: function (so, val) {
|
||||
// XXX
|
||||
var account = UInt160.from_json(val);
|
||||
this.serialize_hex(so, account.to_hex());
|
||||
},
|
||||
parse: function (so) {
|
||||
// XXX
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
|
||||
var serializer = {};
|
||||
|
||||
serializer.addUInt16 = function(value) {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
addUInt16(value.charCodeAt(0));
|
||||
break;
|
||||
|
||||
case 'integer':
|
||||
for (i = 16/8; i; i -=1) {
|
||||
raw.push(value & 255);
|
||||
value >>= 8;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw 'UNEXPECTED_TYPE';
|
||||
}
|
||||
};
|
||||
|
||||
serializer.addUInt160 = function(value) {
|
||||
switch (typeof value) {
|
||||
case 'array':
|
||||
raw.concat(value);
|
||||
break;
|
||||
|
||||
case 'integer':
|
||||
for (i = 160/8; i; i -=1) {
|
||||
raw.push(value & 255);
|
||||
value >>= 8;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw 'UNEXPECTED_TYPE';
|
||||
}
|
||||
};
|
||||
|
||||
serializer.getSHA512Half = function() {
|
||||
};
|
||||
|
||||
// vim:sw=2:sts=2:ts=8:et
|
||||
28
src/js/sjcl-custom/sjcl-ecdsa-der.js
Normal file
28
src/js/sjcl-custom/sjcl-ecdsa-der.js
Normal file
@@ -0,0 +1,28 @@
|
||||
sjcl.ecc.ecdsa.secretKey.prototype.signDER = function(hash, paranoia) {
|
||||
return this.encodeDER(this.sign(hash, paranoia));
|
||||
};
|
||||
|
||||
sjcl.ecc.ecdsa.secretKey.prototype.encodeDER = function(rs) {
|
||||
var w = sjcl.bitArray,
|
||||
R = this._curve.r,
|
||||
l = R.bitLength(),
|
||||
r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)).toBits(),
|
||||
s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)).toBits();
|
||||
|
||||
var rb = sjcl.codec.bytes.fromBits(r),
|
||||
sb = sjcl.codec.bytes.fromBits(s);
|
||||
|
||||
var buffer = [].concat(
|
||||
0x30,
|
||||
4 + rb.length + sb.length,
|
||||
0x02,
|
||||
rb.length,
|
||||
rb,
|
||||
0x02,
|
||||
sb.length,
|
||||
sb
|
||||
);
|
||||
|
||||
return sjcl.codec.bytes.toBits(buffer);
|
||||
};
|
||||
|
||||
@@ -43,10 +43,16 @@
|
||||
// - may or may not forward.
|
||||
//
|
||||
|
||||
var Amount = require('./amount').Amount;
|
||||
var Currency = require('./amount').Currency;
|
||||
var UInt160 = require('./amount').UInt160;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var sjcl = require('../../build/sjcl');
|
||||
|
||||
var Amount = require('./amount').Amount;
|
||||
var Currency = require('./amount').Currency;
|
||||
var UInt160 = require('./amount').UInt160;
|
||||
var Seed = require('./seed').Seed;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var SerializedObject = require('./serializedobject').SerializedObject;
|
||||
|
||||
var config = require('./config');
|
||||
|
||||
var SUBMIT_MISSING = 4; // Report missing.
|
||||
var SUBMIT_LOST = 8; // Give up tracking.
|
||||
@@ -112,6 +118,11 @@ Transaction.flags = {
|
||||
},
|
||||
};
|
||||
|
||||
Transaction.formats = require('./binformat').tx;
|
||||
|
||||
Transaction.HASH_SIGN = 0x53545800;
|
||||
Transaction.HASH_SIGN_TESTNET = 0x73747800;
|
||||
|
||||
Transaction.prototype.consts = {
|
||||
'telLOCAL_ERROR' : -399,
|
||||
'temMALFORMED' : -299,
|
||||
@@ -156,6 +167,30 @@ Transaction.prototype.set_state = function (state) {
|
||||
}
|
||||
};
|
||||
|
||||
Transaction.prototype.serialize = function () {
|
||||
return SerializedObject.from_json(this.tx_json);
|
||||
};
|
||||
|
||||
Transaction.prototype.signing_hash = function () {
|
||||
var prefix = config.testnet
|
||||
? Transaction.HASH_SIGN_TESTNET
|
||||
: Transaction.HASH_SIGN;
|
||||
|
||||
return SerializedObject.from_json(this.tx_json).signing_hash(prefix);
|
||||
};
|
||||
|
||||
Transaction.prototype.sign = function () {
|
||||
var seed = Seed.from_json(this._secret),
|
||||
priv = seed.generate_private(this.tx_json.Account),
|
||||
hash = this.signing_hash();
|
||||
|
||||
var key = new sjcl.ecc.ecdsa.secretKey(sjcl.ecc.curves['c256'], priv.to_bn()),
|
||||
sig = key.signDER(hash.to_bits(), 0),
|
||||
hex = sjcl.codec.hex.fromBits(sig).toUpperCase();
|
||||
|
||||
this.tx_json.TxnSignature = hex;
|
||||
};
|
||||
|
||||
// Submit a transaction to the network.
|
||||
// XXX Don't allow a submit without knowing ledger_index.
|
||||
// XXX Have a network canSubmit(), post events for following.
|
||||
@@ -355,21 +390,21 @@ Transaction.prototype.transfer_rate = function (rate) {
|
||||
// --> flags: undefined, _flag_, or [ _flags_ ]
|
||||
Transaction.prototype.set_flags = function (flags) {
|
||||
if (flags) {
|
||||
var transaction_flags = Transaction.flags[this.tx_json.TransactionType];
|
||||
var transaction_flags = Transaction.flags[this.tx_json.TransactionType];
|
||||
|
||||
if (undefined == this.tx_json.Flags) // We plan to not define this field on new Transaction.
|
||||
this.tx_json.Flags = 0;
|
||||
|
||||
var flag_set = 'object' === typeof flags ? flags : [ flags ];
|
||||
var flag_set = 'object' === typeof flags ? flags : [ flags ];
|
||||
|
||||
for (index in flag_set) {
|
||||
var flag = flag_set[index];
|
||||
for (var index in flag_set) {
|
||||
if (!flag_set.hasOwnProperty(index)) continue;
|
||||
|
||||
if (flag in transaction_flags)
|
||||
{
|
||||
this.tx_json.Flags += transaction_flags[flag];
|
||||
}
|
||||
else {
|
||||
var flag = flag_set[index];
|
||||
|
||||
if (flag in transaction_flags) {
|
||||
this.tx_json.Flags += transaction_flags[flag];
|
||||
} else {
|
||||
// XXX Immediately report an error or mark it.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ var trace = function(comment, func) {
|
||||
};
|
||||
|
||||
var arraySet = function (count, value) {
|
||||
var a = new Array(count);
|
||||
var i;
|
||||
var i, a = new Array(count);
|
||||
|
||||
for (i = 0; i != count; i += 1)
|
||||
for (i = 0; i < count; i++) {
|
||||
a[i] = value;
|
||||
}
|
||||
|
||||
return a;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user