Skip to content

Commit

Permalink
vsp: Add per ticket authentication
Browse files Browse the repository at this point in the history
Add the ability to change vote preverences using a new authorization
scheme. A timestamp signed with a tickets's user commitment address's
private key is used to prove ownership of a ticket. This data is sent
using a different header and comma separated values.
  • Loading branch information
JoeGruffins committed Mar 17, 2020
1 parent 13b0c81 commit c6a718e
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 11 deletions.
17 changes: 17 additions & 0 deletions decred/decred/dcr/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -1733,6 +1733,20 @@ def setNewPool(self, pool):
for utxo in bc.UTXOs([addr]):
self.addUTXO(utxo)
self.updateSpentTickets(checkTxids)
for txid in checkTxids:
tx = None
try:
tx = self.blockchain.tx(txid)
except Exception:
if txid in self.mempool:
tx = self.mempool[txid]
else:
# Should never make it here.
log.error(f"unknown ticket: {txid}")
continue
if tx and tx.isTicket() and txid not in pool.tickets:
pool.tickets.append(txid)
self.vspDB[pool.apiKey] = pool
self.updateStakeStats()
self.signals.balance(self.calcBalance())

Expand Down Expand Up @@ -1880,6 +1894,9 @@ def purchaseTickets(self, qty, price):
# Add all tickets
for tx in txs[1]:
self.addMempoolTx(tx)
if tx.txid() not in pool.tickets:
pool.tickets.append(tx.txid())
self.vspDB[pool.apiKey] = pool
# Store the txids.
self.spendUTXOs(spentUTXOs)
for utxo in newUTXOs:
Expand Down
104 changes: 98 additions & 6 deletions decred/decred/dcr/txscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,34 @@
# 01000000 00000000 = Apply fees rule
SStxRevFractionFlag = 0x4000

# compactSigSize is the size of a compact signature. It consists of a
# compact signature recovery code byte followed by the R and S components
# serialized as 32-byte big-endian values. 1+32*2 = 65.
# for the R and S components. 1+32+32=65.
compactSigSize = 65

# compactSigMagicOffset is a value used when creating the compact signature
# recovery code inherited from Bitcoin and has no meaning, but has been
# retained for compatibility. For historical purposes, it was originally
# picked to avoid a binary representation that would allow compact
# signatures to be mistaken for other components.
compactSigMagicOffset = 27

# compactSigCompPubKey is a value used when creating the compact signature
# recovery code to indicate the original public key was compressed.
compactSigCompPubKey = 4

# pubKeyRecoveryCodeOddnessBit specifies the bit that indicates the oddess
# of the Y coordinate of the random point calculated when creating a
# signature.
pubKeyRecoveryCodeOddnessBit = 1 << 0

# pubKeyRecoveryCodeOverflowBit specifies the bit that indicates the X
# coordinate of the random point calculated when creating a signature was
# >= N, where N is the order of the group.
pubKeyRecoveryCodeOverflowBit = 1 << 1


# A couple of hashing functions from the crypto module.
mac = crypto.mac
hashH = crypto.hashH
Expand Down Expand Up @@ -2099,25 +2127,34 @@ def signRFC6979(privateKey, inHash):
"""
N = Curve.N
k = nonceRFC6979(privateKey, inHash, ByteArray(b""), ByteArray(b""))
recoveryCode = 0

inv = crypto.modInv(k, N)
r = Curve.scalarBaseMult(k)[0] % N
kG = Curve.scalarBaseMult(k)
r = kG[0] % N

if r == 0:
raise DecredError("calculated R is zero")

if kG[1] & 1:
recoveryCode += 1
if kG[0] > N:
recoveryCode += 4

e = hashToInt(inHash)
s = privateKey.int() * r
s += e
s *= inv
s = s % N

if (N >> 1) > 1:
s = N - s
if s == 0:
raise DecredError("calculated S is zero")

return Signature(r, s)
if s > N / 2:
s = N - s
recoveryCode ^= 1

return Signature(r, s), recoveryCode


def putVarInt(val):
Expand Down Expand Up @@ -2199,6 +2236,32 @@ def addData(data):
return b


def signCompact(key, inHash, isCompressedKey):
"""
SignCompact produces a compact signature of the data in hash with the given
private key on the secp256k1 curve. The isCompressedKey parameter specifies
if the given signature should reference a compressed public key or not.
Compact signature format:
<1-byte compact sig recovery code><32-byte R><32-byte S>
The compact sig recovery code is the value 27 + public key recovery code + 4
if the compact signature was created with a compressed public key.
"""
# Create the signature and associated pubkey recovery code and calculate
# the compact signature recovery code.
sig, recoveryCode = signRFC6979(key, inHash)
compactSigRecoveryCode = compactSigMagicOffset + recoveryCode
if isCompressedKey:
compactSigRecoveryCode += compactSigCompPubKey

# Output <compactSigRecoveryCode><32-byte R><32-byte S>.
b = ByteArray(compactSigRecoveryCode)
b += ByteArray(sig.r)
b += ByteArray(sig.s)
return b


def signatureScript(tx, idx, subscript, hashType, privKey, compress):
"""
SignatureScript creates an input signature script for tx to spend coins sent
Expand Down Expand Up @@ -2236,8 +2299,8 @@ def rawTxInSignature(tx, idx, subScript, hashType, key):
versions.
"""
sigHash = calcSignatureHash(subScript, hashType, tx, idx, None)
sig = signRFC6979(key, sigHash).serialize()
return sig + ByteArray(hashType)
sig, _ = signRFC6979(key, sigHash)
return sig.serialize() + ByteArray(hashType)


def calcSignatureHash(script, hashType, tx, idx, cachedPrefix):
Expand Down Expand Up @@ -2717,6 +2780,35 @@ def extractPkScriptAddrs(version, pkScript, netParams):
return NonStandardTy, [], 0


def addrFromSStxPkScrCommitment(pkScript, netParams):
"""
AddrFromSStxPkScrCommitment extracts a P2SH or P2PKH address from a ticket
commitment pkScript.
"""
if len(pkScript) < SStxPKHMinOutSize:
raise DecredError("short read of sstx commit pkscript")

# The MSB of the encoded amount specifies if the output is P2SH. Since
# it is encoded with little endian, the MSB is in final byte in the encoded
# amount.
#
# This is a faster equivalent of:
#
# amtBytes := script[22:30]
# amtEncoded := binary.LittleEndian.Uint64(amtBytes)
# isP2SH := (amtEncoded & uint64(1<<63)) != 0
isP2SH = pkScript[29] & 0x80 != 0

# The 20 byte PKH or SH.
hashBytes = pkScript[2:22]

# Return the correct address type.
if isP2SH:
return crypto.newAddressScriptHashFromHash(hashBytes, netParams)

return crypto.newAddressPubKeyHash(hashBytes, netParams, crypto.STEcdsaSecp256k1)


def sign(chainParams, tx, idx, subScript, hashType, keysource, sigType):
scriptClass, addresses, nrequired = extractPkScriptAddrs(
DefaultScriptVersion, subScript, chainParams
Expand Down
63 changes: 59 additions & 4 deletions decred/decred/dcr/vsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
DcrdataClient.endpointList() for available endpoints.
"""

import base64
import time

from decred import DecredError
Expand Down Expand Up @@ -159,7 +160,7 @@ class VotingServiceProvider(object):
the VSP API.
"""

def __init__(self, url, apiKey, netName, purchaseInfo=None):
def __init__(self, url, apiKey, netName, purchaseInfo=None, tickets=None):
"""
Args:
url (string): The stake pool URL.
Expand All @@ -176,6 +177,8 @@ def __init__(self, url, apiKey, netName, purchaseInfo=None):
self.apiKey = apiKey
self.net = nets.parse(netName)
self.purchaseInfo = purchaseInfo
# a list of ticket txid purchased through this vsp
self.tickets = tickets if tickets else []
self.stats = None
self.err = None

Expand All @@ -184,21 +187,22 @@ def blob(vsp):
"""Satisfies the encode.Blobber API"""
pi = PurchaseInfo.blob(vsp.purchaseInfo) if vsp.purchaseInfo else None
return (
encode.BuildyBytes(0)
encode.BuildyBytes(1)
.addData(vsp.url.encode("utf-8"))
.addData(vsp.apiKey.encode("utf-8"))
.addData(vsp.net.Name.encode("utf-8"))
.addData(encode.filterNone(pi))
.addData(encode.blobStrList(vsp.tickets))
.b
)

@staticmethod
def unblob(b):
"""Satisfies the encode.Blobber API"""
ver, d = encode.decodeBlob(b)
if ver != 0:
if ver != 1:
raise AssertionError("invalid version for VotingServiceProvider %d" % ver)
if len(d) != 4:
if len(d) != 5:
raise AssertionError(
"wrong number of pushes for VotingServiceProvider. wanted 4, got %d"
% len(d)
Expand All @@ -212,6 +216,7 @@ def unblob(b):
apiKey=d[1].decode("utf-8"),
netName=d[2].decode("utf-8"),
purchaseInfo=pi,
tickets=encode.unblobStrList(d[4]),
)

def serialize(self):
Expand Down Expand Up @@ -259,6 +264,28 @@ def headers(self):
"""
return {"Authorization": "Bearer %s" % self.apiKey}

def headersV3(self, acct, txid):
"""
Make the API request headers.
Returns:
object: The headers as a Python object.
"""
now = str(int(time.time()))
txOut = acct.blockchain.tx(txid).txOut[3]
addr = txscript.addrFromSStxPkScrCommitment(txOut.pkScript, acct.net)
msg = "Decred Signed Message:\n"
msgBA = txscript.putVarInt(len(msg))
msgBA += ByteArray(msg.encode())
msgBA += txscript.putVarInt(len(now))
msgBA += ByteArray(now.encode())
hashedMsg = crypto.hashH(msgBA.bytes())
sig = self.getSignature(acct, hashedMsg.bytes(), addr)
b64Sig = base64.b64encode(sig.bytes())
return {
"Authorization": f'TicketAuth SignedTimestamp={now},Signature={b64Sig.decode("utf-8")},TicketHash={txid}'
}

def validate(self, addr):
"""
Validate performs some checks that the PurchaseInfo provided by the
Expand Down Expand Up @@ -342,6 +369,14 @@ def getPurchaseInfo(self):
self.err = res
raise DecredError("unexpected response from 'getpurchaseinfo': %r" % (res,))

def getSignature(self, acct, msg, addr):
"""
Sign msg with the private key belonging to addr.
"""
privKey = acct.privKeyForAddress(addr.string())
sig = txscript.signCompact(privKey.key, msg, True)
return sig

def updatePurchaseInfo(self):
"""
Update purchase info if older than PURCHASE_INFO_LIFE.
Expand All @@ -362,6 +397,26 @@ def getStats(self):
return self.stats
raise DecredError("unexpected response from 'stats': %s" % repr(res))

def setVoteBitsV3(self, voteBits, acct):
"""
Set the vote preference on the VotingServiceProvider.
Returns:
bool: True on success. DecredError raised on error.
"""
txid = self.tickets[0]
data = {"VoteBits": voteBits}
res = tinyhttp.post(
self.apiPath("voting"),
data,
headers=self.headersV3(acct, txid),
urlEncode=True,
)
if resultIsSuccess(res):
self.purchaseInfo.voteBits = voteBits
return True
raise DecredError("unexpected response from 'voting': %s" % repr(res))

def setVoteBits(self, voteBits):
"""
Set the vote preference on the VotingServiceProvider.
Expand Down
2 changes: 1 addition & 1 deletion tinywallet/tinywallet/screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -2173,7 +2173,7 @@ def func(idx):
def changeVote():
app.emitSignal(ui.WORKING_SIGNAL)
try:
pools[0].setVoteBits(voteBits)
pools[0].setVoteBitsV3(voteBits, acct)
app.appWindow.showSuccess("vote choices updated")
dropdown.lastIndex = idx
except Exception as e:
Expand Down

0 comments on commit c6a718e

Please sign in to comment.