Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e56b621
feat: add stake integration schema
parodyBit Dec 12, 2023
3829eba
feat: update staking schema
parodyBit May 17, 2024
53f99e2
fix: stake / unstake schema
parodyBit May 27, 2024
3742223
feat: recover public key from signature
parodyBit May 29, 2024
ca71952
feat: add fromAuthorization method
parodyBit May 31, 2024
90b26d9
run dart format
parodyBit May 31, 2024
27661db
fix: schema constructor and recovery id
parodyBit Jun 4, 2024
e602db0
fix: schema constructor
parodyBit Jun 4, 2024
52b026c
fix: stake/unstake schema
parodyBit Jun 4, 2024
2e8f99a
fix: decode authorization
parodyBit Jun 4, 2024
d5508bd
fix: throw socket exception message
gabaldon Nov 18, 2024
930339a
feat: add 2.0 methods to NodeClient
parodyBit Nov 22, 2024
517b2c5
feat: add fee and nonce to unstake body
parodyBit Dec 3, 2024
fb93b59
fix: unstake schema
parodyBit Dec 5, 2024
00d810c
fix: testnet as .env variable
parodyBit Dec 13, 2024
a2d74c9
feat: add .env example
parodyBit Dec 13, 2024
2bfe999
add validator pkh to authorization
parodyBit Jan 10, 2025
1a435ca
feat: add stake and unstake to explorer API
parodyBit Feb 25, 2025
1283b95
fix: parse correct data structure from explorer
parodyBit Feb 28, 2025
4606465
fix(stake_body): encode change correctly if null
parodyBit Mar 5, 2025
727e05b
chore: remove testnet param because it is an env var now
parodyBit Mar 7, 2025
52f6570
fix: add unstake stake to hash method
parodyBit Mar 10, 2025
00e95aa
feat: add protocol version and stakes explorer api endpoints
parodyBit Mar 11, 2025
725525f
fix: nonce endpoint
parodyBit Mar 11, 2025
16328a4
fix: allow null fee when parsing from json
parodyBit Mar 12, 2025
3d489be
chore: bump 0.4.4
parodyBit Mar 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENVIRONMENT = 'testnet'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ doc/api/
dev/

**/.DS_Store

.env
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;
9 changes: 8 additions & 1 deletion lib/explorer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,11 @@ export 'src/network/explorer/explorer_api.dart'
TransactionType,
TransactionUtxo,
TxStatusLabel,
ValueTransferInfo;
ValueTransferInfo,
StakeInfo,
StakeInput,
AddressStakes,
AddressStake,
AddressUnstake,
AddressUnstakes,
UnstakeInfo;
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
3 changes: 2 additions & 1 deletion lib/src/crypto/address.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:witnet/data_structures.dart'
Utxo,
UtxoPool,
UtxoSelectionStrategy;
import 'package:witnet/src/utils/dotenv.dart';

import 'secp256k1/private_key.dart' show WitPrivateKey;
import 'secp256k1/public_key.dart' show WitPublicKey;
Expand Down Expand Up @@ -46,7 +47,7 @@ class Address {
factory Address.fromPublicKeyHash({required Uint8List hash}) {
return Address(
address: bech32.encode(Bech32(
hrp: 'wit',
hrp: DotEnvUtil().testnet ? 'twit' : 'wit',
data: Uint8List.fromList(
convertBits(data: hash.toList(), from: 8, to: 5, pad: false)))),
publicKeyHash: PublicKeyHash(hash: hash),
Expand Down
12 changes: 12 additions & 0 deletions lib/src/crypto/hd_wallet/extended_private_key.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:typed_data';

import 'package:witnet/schema.dart';
import 'package:witnet/src/crypto/hd_wallet/extended_key.dart';
import 'package:witnet/src/crypto/secp256k1/secp256k1.dart';
import 'package:witnet/src/utils/bech32/exceptions.dart';
Expand Down Expand Up @@ -206,6 +207,17 @@ class Xprv extends ExtendedKey {
return bech1;
}

PublicKeyHash get publicKeyHash =>
PublicKeyHash.fromAddress(privateKey.publicKey.address);

KeyedSignature signHash(String hash) {
final sig = privateKey.signature(hash);
return KeyedSignature(
publicKey: PublicKey(bytes: privateKey.publicKey.encode()),
signature: Signature(secp256k1: Secp256k1Signature(der: sig.encode())),
);
}

@override
Xprv child({required BigInt index}) {
bool hardened = index >= BigInt.from(1 << 31);
Expand Down
83 changes: 79 additions & 4 deletions lib/src/crypto/secp256k1/public_key.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import "dart:typed_data" show Uint8List;

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

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 +40,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');
}
}

Uint8List encode({bool compressed = true}) {
return hexToBytes(pointToHexInCompress(point));
}
Expand All @@ -43,7 +72,53 @@ class WitPublicKey {
return sha256(data: encode()).sublist(0, 20);
}

String get address {
return bech32.encodeAddress('wit', publicKeyHash);
String get address => bech32.encodeAddress(
DotEnvUtil().testnet ? 'twit' : '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
Loading