Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add stake integration schema #38

Merged
merged 10 commits into from
Aug 8, 2024
30 changes: 30 additions & 0 deletions example/recover_public_key_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:typed_data';

import 'package:witnet/crypto.dart';
import 'package:witnet/src/utils/transformations/transformations.dart';
import 'package:witnet/witnet.dart';

// abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
String xprvString =
"xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvu566tn6";

void main() {
// import a xprv
Xprv xprv = Xprv.fromXprv(xprvString);
var expectedKey = bytesToHex(xprv.privateKey.publicKey.point.encode());

// sign a message
String messageStr = "Hello Witnet!";
Uint8List messageBytes = sha256(data: stringToBytes(messageStr));
WitSignature signature = xprv.privateKey.signature(bytesToHex(messageBytes));

// recover a public key from a signature
WitPublicKey recoveredKey = WitPublicKey.recover(signature, messageBytes);

try {
assert(expectedKey == bytesToHex(recoveredKey.point.encode()),
"error: Message not signed by expected Public Key");
} catch (e) {
print(e);
}
}
84 changes: 84 additions & 0 deletions example/stake_transaction_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// import 'package:witnet/node.dart';
import 'package:witnet/schema.dart';
import 'package:witnet/src/constants.dart';
import 'package:witnet/src/utils/transformations/transformations.dart';
import 'package:witnet/witnet.dart';

var outputPointer = OutputPointer.fromString(
'0000000000000000000000000000000000000000000000000000000000000000:0');

void main() async {
/// connect to local node rpc
// NodeClient nodeClient = NodeClient(address: "127.0.0.1", port: 21338);

// String mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
/// load node xprv for the default mnemonic
Xprv masterNode = Xprv.fromXprv(
"xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvu566tn6");

Xprv withdrawer = masterNode /
KEYPATH_PURPOSE /
KEYPATH_COIN_TYPE /
KEYPATH_ACCOUNT /
EXTERNAL_KEYCHAIN /
0;

/// The 20 byte Public Key Hash of the withdrawer
String pkh = bytesToHex(withdrawer.privateKey.publicKey.publicKeyHash);

/// The authorization by the node
KeyedSignature authorization = signHash(pkh, masterNode.privateKey);

/// Build the Stake Key
StakeKey stakeKey = StakeKey(
validator: authorization.publicKey.pkh,
withdrawer: PublicKeyHash.fromAddress(withdrawer.address.address),
);

/// build stake transaction body
StakeBody body = StakeBody(
inputs: [
Input(outputPointer: outputPointer),
],
output: StakeOutput(
value: MINIMUM_STAKEABLE_AMOUNT_WITS,
key: stakeKey,
authorization: authorization,
),
);

/// build and sign stake transaction
StakeTransaction stake = StakeTransaction(
body: body,
signatures: [signHash(body.transactionId, masterNode.privateKey)]);

/// The Stake Transaction ID
print(stake.transactionID);

/// send stake transaction
/// var response = await nodeClient.inventory(stake.jsonMap());
///
UnstakeBody unstakeBody = UnstakeBody(
operator: PublicKeyHash.fromAddress(withdrawer.address.address),
withdrawal: ValueTransferOutput.fromJson({
"pkh": withdrawer.address.address,
"time_lock": 0,
"value": 1,
}));

KeyedSignature unstakeSignature =
signHash(bytesToHex(unstakeBody.hash), masterNode.privateKey);
UnstakeTransaction unstake =
UnstakeTransaction(body: unstakeBody, signature: unstakeSignature);

print(unstake.transactionID);
}

/// Sign Hash
KeyedSignature signHash(String hash, WitPrivateKey privateKey) {
final sig = privateKey.signature(hash);
return KeyedSignature(
publicKey: PublicKey(bytes: privateKey.publicKey.encode()),
signature: Signature(secp256k1: Secp256k1Signature(der: sig.encode())),
);
}
5 changes: 4 additions & 1 deletion lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ export 'src/constants.dart'
KEYPATH_PURPOSE,
KEYPATH_COIN_TYPE,
EXTERNAL_KEYCHAIN,
INTERNAL_KEYCHAIN;
INTERNAL_KEYCHAIN,
STAKE_OUTPUT_WEIGHT,
UNSTAKE_OUTPUT_WEIGHT,
MINIMUM_STAKEABLE_AMOUNT_WITS;
3 changes: 3 additions & 0 deletions lib/src/constants.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const INPUT_SIZE = 133;
const OUTPUT_SIZE = 36;
const STAKE_OUTPUT_WEIGHT = 105;
const UNSTAKE_OUTPUT_WEIGHT = 153;
const MINIMUM_STAKEABLE_AMOUNT_WITS = 10000;
const COMMIT_WEIGHT = 400;
const REVEAL_WEIGHT = 200;
const TALLY_WEIGHT = 100;
Expand Down
79 changes: 77 additions & 2 deletions lib/src/crypto/secp256k1/public_key.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import "dart:typed_data" show Uint8List;

import 'package:witnet/src/crypto/secp256k1/signature.dart';

import 'secp256k1.dart'
show Point, hexToPoint, hexToPointFromCompress, pointToHexInCompress;
show
Point,
Secp256k1,
hexToPoint,
hexToPointFromCompress,
pointToHexInCompress;
import 'private_key.dart' show WitPrivateKey;
import '../crypto.dart' show sha256;

import 'package:witnet/utils.dart' show bech32, bytesToHex, hexToBytes;
import 'package:witnet/utils.dart'
show bech32, bytesToBigInt, bytesToHex, hexToBytes;

class WitPublicKey {
final Point point;
Expand All @@ -31,6 +39,26 @@ class WitPublicKey {
return privateKey.publicKey;
}

factory WitPublicKey.recover(WitSignature signature, Uint8List message,
[int? recoveryId = null]) {
if (recoveryId != null) {
if (recoveryId >= 0 && recoveryId <= 3) {
return WitPublicKey(_recoverPublicKey(recoveryId, signature, message));
} else {
throw ArgumentError("invalid Recovery ID: 0-3... $recoveryId");
}
} else {
for (int recId = 0; recId <= 3; recId++) {
Point recoveredKey = _recoverPublicKey(recId, signature, message);
WitPublicKey publicKey = WitPublicKey(recoveredKey);
if (signature.verify(publicKey, bytesToHex(message))) {
return publicKey;
}
}
throw ArgumentError('Could not calculate recovery ID');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are at this points, it means the signature is invalid. What do you think about adding that information to the error?

Suggested change
throw ArgumentError('Could not calculate recovery ID');
throw ArgumentError('Could not calculate recovery ID. Invalid signature');

}
}

Uint8List encode({bool compressed = true}) {
return hexToBytes(pointToHexInCompress(point));
}
Expand All @@ -47,3 +75,50 @@ class WitPublicKey {
return bech32.encodeAddress('wit', publicKeyHash);
}
}

Point _recoverPublicKey(
int recoveryId, WitSignature signature, Uint8List message) {
BigInt z = bytesToBigInt(message);
if (signature.R >= Secp256k1.n || signature.S >= Secp256k1.n) {
throw ArgumentError("Invalid Signature");
}

// calculate x coordinate of point R
BigInt x = signature.R + BigInt.from(recoveryId / 2) * Secp256k1.n;
if (x >= Secp256k1.p) {
throw ArgumentError("invalid x-coordinate");
}

// decompress point R from the x coordinate
Point r = _decompressKey(x, (recoveryId % 2) == 1);

BigInt e = z % Secp256k1.n;
BigInt eInv = (Secp256k1.n - e) % Secp256k1.n;
BigInt rInv = signature.R.modInverse(Secp256k1.n);
BigInt srInv = (signature.S * rInv) % Secp256k1.n;
BigInt eInvrInv = (eInv * rInv) % Secp256k1.n;

// Q = r^-1 (sR - eG)
Point q = (r * srInv) + (Secp256k1.G * eInvrInv);

return q;
}

_decompressKey(BigInt xBn, bool yBit) {
var x = xBn;

// y^2 = x^3 + ax + b (mod p)
var alpha =
(x.modPow(BigInt.from(3), Secp256k1.p) + BigInt.from(7) % Secp256k1.p);

// y = sqrt(y^2) (mod p)
var beta = (alpha.modPow((Secp256k1.p + BigInt.one) >> 2, Secp256k1.p));

// select the correct y based on the yBit
var y = beta;
if ((beta.isEven ? 0 : 1) != (yBit ? 1 : 0)) {
y = Secp256k1.p - y;
}

return Point(x, y);
}
24 changes: 24 additions & 0 deletions lib/src/crypto/secp256k1/secp256k1.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class Point {
Point operator +(Point other) {
return addDiffPoint(this, other, Secp256k1.p);
}

Point operator *(BigInt other) {
return pointMultiply(this, other, Secp256k1.p, Secp256k1.a);
}
}

Point bigIntToPoint(BigInt n) {
Expand Down Expand Up @@ -93,6 +97,26 @@ Point addDiffPoint(Point point1, Point point2, BigInt modNum) {
return Point(x3, y3);
}

/// double-and-add method for point multiplication.
Point pointMultiply(Point point, BigInt k, BigInt modNum, BigInt a) {
Point result = Point(BigInt.zero, BigInt.zero);
Point addend = point;

while (k > BigInt.zero) {
if (k.isOdd) {
if (result.x == BigInt.zero && result.y == BigInt.zero) {
result = addend;
} else {
result = addDiffPoint(result, addend, modNum);
}
}
addend = addSamePoint(addend, modNum, a);
k = k >> 1; // k = k / 2
}

return result;
}

Point getPointByBigInt(BigInt n, BigInt p, BigInt a, Point pointG) {
var bin = n.toRadixString(2);
var nextPoint = pointG;
Expand Down
3 changes: 3 additions & 0 deletions lib/src/crypto/secp256k1/signature.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ class WitSignature {
return WitSignature(r, s);
}

WitPublicKey publicKey(Uint8List message) =>
WitPublicKey.recover(this, message);

Uint8List encode() {
Uint8List _r = bigIntToBytes(R);
Uint8List _s = bigIntToBytes(S);
Expand Down
22 changes: 22 additions & 0 deletions lib/src/schema/keyed_signature.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,28 @@ class KeyedSignature extends GeneratedMessage {
signature: Signature.fromJson(json["signature"]),
);

factory KeyedSignature.fromAuthorization(
String authorization, String withdrawerAddress) {
PublicKeyHash pkh = PublicKeyHash.fromAddress(withdrawerAddress);
Uint8List authBytes = hexToBytes(authorization);
int recoveryId = authBytes[0];
BigInt r = bytesToBigInt(authBytes.sublist(1, 33));
BigInt s = bytesToBigInt(authBytes.sublist(33, 65));

WitSignature signature = WitSignature(r, s);
WitPublicKey validatorKey = WitPublicKey.recover(
signature,
hexToBytes(pkh.hex.padRight(64, '0')),
recoveryId,
);

return KeyedSignature(
publicKey: PublicKey(bytes: validatorKey.encode()),
signature:
Signature(secp256k1: Secp256k1Signature(der: signature.encode())),
);
}

String get rawJson => json.encode(jsonMap());

Map<String, dynamic> jsonMap({bool asHex = false}) => {
Expand Down
4 changes: 4 additions & 0 deletions lib/src/schema/public_key.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class PublicKey extends GeneratedMessage {

Uint8List get pbBytes => writeToBuffer();

PublicKeyHash get pkh => PublicKeyHash(
hash: sha256(data: Uint8List.fromList(publicKey)).sublist(0, 20),
);

@TagNumber(1)
List<int> get publicKey => $_getN(0);
@TagNumber(1)
Expand Down
12 changes: 11 additions & 1 deletion lib/src/schema/schema.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:convert' show json;
import 'dart:typed_data' show Uint8List;
import 'package:protobuf/protobuf.dart';
import 'package:witnet/src/crypto/secp256k1/public_key.dart';
import 'package:witnet/src/crypto/secp256k1/signature.dart';
import 'package:witnet/src/utils/transformations/transformations.dart';
import 'package:fixnum/fixnum.dart' show Int64;
import 'package:witnet/crypto.dart' show sha256;
Expand All @@ -16,7 +18,9 @@ import 'package:witnet/constants.dart'
REVEAL_WEIGHT,
TALLY_WEIGHT,
INPUT_SIZE,
OUTPUT_SIZE;
OUTPUT_SIZE,
STAKE_OUTPUT_WEIGHT,
UNSTAKE_OUTPUT_WEIGHT;

import '../../radon.dart' show radToCbor, cborToRad;

Expand Down Expand Up @@ -54,6 +58,12 @@ part 'reveal_body.dart';
part 'reveal_transaction.dart';
part 'secp256k1_signature.dart';
part 'signature.dart';
part 'stake_body.dart';
part 'stake_key.dart';
part 'stake_output.dart';
part 'stake_transaction.dart';
part 'unstake_body.dart';
part 'unstake_transaction.dart';
part 'string_pair.dart';
part 'super_block.dart';
part 'super_block_vote.dart';
Expand Down
Loading