bitcoin-payment-channel 0.3.0.1 → 0.4.0.0
raw patch · 13 files changed
+504/−133 lines, 13 filesdep +string-conversionsdep −base58string
Dependencies added: string-conversions
Dependencies removed: base58string
Files
- bitcoin-payment-channel.cabal +17/−14
- src/Data/Bitcoin/PaymentChannel.hs +18/−21
- src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Script.hs +9/−6
- src/Data/Bitcoin/PaymentChannel/Internal/Error.hs +20/−6
- src/Data/Bitcoin/PaymentChannel/Internal/Payment.hs +6/−5
- src/Data/Bitcoin/PaymentChannel/Internal/Serialization.hs +76/−19
- src/Data/Bitcoin/PaymentChannel/Internal/State.hs +24/−11
- src/Data/Bitcoin/PaymentChannel/Internal/Types.hs +29/−11
- src/Data/Bitcoin/PaymentChannel/Internal/Util.hs +14/−0
- src/Data/Bitcoin/PaymentChannel/Movable.hs +190/−0
- src/Data/Bitcoin/PaymentChannel/Types.hs +10/−7
- src/Data/Bitcoin/PaymentChannel/Util.hs +9/−6
- test/Main.hs +82/−27
bitcoin-payment-channel.cabal view
@@ -1,5 +1,5 @@ name: bitcoin-payment-channel-version: 0.3.0.1+version: 0.4.0.0 synopsis: Library for working with Bitcoin payment channels description: A Bitcoin payment channel allows secure and instant transfer of bitcoins from one@@ -31,22 +31,23 @@ location: git://github.com/runeksvendsen/bitcoin-payment-channel.git library- exposed-modules: Data.Bitcoin.PaymentChannel,- Data.Bitcoin.PaymentChannel.Types,- Data.Bitcoin.PaymentChannel.Util,+ exposed-modules: Data.Bitcoin.PaymentChannel+ Data.Bitcoin.PaymentChannel.Types+ Data.Bitcoin.PaymentChannel.Util+ Data.Bitcoin.PaymentChannel.Movable -- Exposed for testing purposes Data.Bitcoin.PaymentChannel.Internal.State other-modules: Data.Bitcoin.PaymentChannel.Internal.Error- Data.Bitcoin.PaymentChannel.Internal.Payment,- Data.Bitcoin.PaymentChannel.Internal.Refund,- Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Amount,+ Data.Bitcoin.PaymentChannel.Internal.Payment+ Data.Bitcoin.PaymentChannel.Internal.Refund+ Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Amount Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script- Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Util,- Data.Bitcoin.PaymentChannel.Internal.Bitcoin.LockTime,- Data.Bitcoin.PaymentChannel.Internal.Settlement,- Data.Bitcoin.PaymentChannel.Internal.Types,- Data.Bitcoin.PaymentChannel.Internal.Serialization,+ Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Util+ Data.Bitcoin.PaymentChannel.Internal.Bitcoin.LockTime+ Data.Bitcoin.PaymentChannel.Internal.Settlement+ Data.Bitcoin.PaymentChannel.Internal.Types+ Data.Bitcoin.PaymentChannel.Internal.Serialization Data.Bitcoin.PaymentChannel.Internal.Util ghc-options: -W@@ -62,7 +63,8 @@ text >= 1.2.0 && < 1.3.0, time >= 1.5.0 && < 1.7.0, aeson >= 0.11.0 && < 0.12.0,- scientific >= 0.3.0 && < 0.4.0+ scientific >= 0.3.0 && < 0.4.0,+ string-conversions >= 0.4 && < 0.5 hs-source-dirs: src @@ -77,7 +79,8 @@ haskoin-core >= 0.4.0 && < 1.0.0, bytestring, base16-bytestring, base64-bytestring,- base58string, hexstring, text,+ hexstring, text,+ string-conversions, time, cereal, aeson,
src/Data/Bitcoin/PaymentChannel.hs view
@@ -96,7 +96,7 @@ where import Data.Bitcoin.PaymentChannel.Internal.Types- (PaymentTxConfig(..), Payment(..), pcsValueLeft)+ (PaymentTxConfig(..), Payment(..), FullPayment(..), pcsClientChangeVal) import Data.Bitcoin.PaymentChannel.Internal.State (newPaymentChannelState, updatePaymentChannelState) import qualified Data.Bitcoin.PaymentChannel.Internal.State as S@@ -124,10 +124,11 @@ -> (HC.Hash256 -> HC.Signature) -- ^Used to sign payments from sender. When given a 'HC.Hash256', produces a signature that verifies against sender PubKey. -> HC.Address -- ^ Value sender/client change address -> BitcoinAmount -- ^ Value of initial payment. Must be greater than or equal to 'minimumInitialPayment'- -> (BitcoinAmount, Payment, SenderPaymentChannel) -- ^Initial payment amount (may be capped), initial payment, and new sender state object+ -> (BitcoinAmount, FullPayment, SenderPaymentChannel) -- ^Initial payment amount (may be capped), initial payment, and new sender state object channelWithInitialPaymentOf cp fundInf signFunc sendAddr amount = let pConf = CPaymentTxConfig sendAddr- (CPayment _ tmpSig) = createPayment cp fundInf sendAddr amount signFunc+ (CFullPayment (CPayment _ tmpSig) _ _ _) =+ createPayment cp fundInf sendAddr amount signFunc in flip sendPayment amount $ CSenderPaymentChannel (newPaymentChannelState cp fundInf pConf tmpSig) signFunc@@ -136,24 +137,20 @@ sendPayment :: SenderPaymentChannel -- ^Sender state object -> BitcoinAmount -- ^ Amount to send (the actual payment amount is capped so that that, at minimu, the client change value equals the dust limit)- -> (BitcoinAmount, Payment, SenderPaymentChannel) -- ^ Actual amount sent, payment, and updated sender state object+ -> (BitcoinAmount, FullPayment, SenderPaymentChannel) -- ^ Actual amount sent, payment, and updated sender state object sendPayment (CSenderPaymentChannel cs signFunc) amountToSend = let- valSent = pcsValueLeft cs - newSenderValue- newSenderValue = max (S.pcsDustLimit cs) (pcsValueLeft cs - amountToSend)- payment = paymentFromState cs newSenderValue signFunc+ valSent = pcsClientChangeVal cs - newSenderValue+ newSenderValue = max (S.pcsDustLimit cs) (pcsClientChangeVal cs - amountToSend)+ fullPay = paymentFromState cs newSenderValue signFunc in- case updatePaymentChannelState cs payment of+ case updatePaymentChannelState cs fullPay of Right newCS -> (valSent- ,payment+ ,fullPay ,CSenderPaymentChannel newCS signFunc)- Left (BadPaymentValue val) ->- error $ "BUG: 'createPayment' created value-backtracking tx of" ++ show val- Left DustOutput ->- error "BUG: 'createPayment' created dust output"- Left e ->- error $ "BUG: 'updatePaymentChannelState' returned unknown error: " ++ show e+ Left e ->+ error $ "BUG: bad payment" ++ show e -- |Produces a Bitcoin transaction which sends all channel funds back to the sender. -- Will not be accepted by the Bitcoin network until the expiration time specified in@@ -175,13 +172,13 @@ ChannelParameters -- ^ Specifies channel sender and receiver, plus channel expiration date -> FundingTxInfo -- ^ Holds information about the transaction used to fund the channel -> HC.Address -- ^ Value sender/client change address- -> Payment -- ^Initial channel payment+ -> FullPayment -- ^Initial channel payment -> Either PayChanError (BitcoinAmount, ReceiverPaymentChannel) -- ^Error or: value_received plus state object-channelFromInitialPayment cp fundInf sendAddr payment@(CPayment _ sig) =+channelFromInitialPayment cp fundInf sendAddr fp@(CFullPayment (CPayment _ sig) _ _ _) = let pConf = CPaymentTxConfig sendAddr in- flip recvPayment payment $ CReceiverPaymentChannel+ flip recvPayment fp $ CReceiverPaymentChannel -- Create a new state with the unverified signature; then verify the same signature in 'recvPayment' (newPaymentChannelState cp fundInf pConf sig) @@ -190,14 +187,14 @@ -- the amount received with this 'Payment' and a new state object. recvPayment :: ReceiverPaymentChannel -- ^Receiver state object- -> Payment -- ^Payment to verify and register+ -> FullPayment -- ^Payment to verify and register -> Either PayChanError (BitcoinAmount, ReceiverPaymentChannel) -- ^Value received plus new receiver state object-recvPayment rpc@(CReceiverPaymentChannel oldState) paymnt =+recvPayment rpc@(CReceiverPaymentChannel oldState) fp@(CFullPayment paymnt _ _ _) = let verifySenderPayment hash pk sig = HC.verifySig hash sig (getPubKey pk) in if verifyPaymentSigFromState oldState verifySenderPayment paymnt then- updatePaymentChannelState oldState paymnt >>=+ updatePaymentChannelState oldState fp >>= (\newState -> Right ( S.channelValueLeft oldState - S.channelValueLeft newState , rpc { rpcState = newState })
src/Data/Bitcoin/PaymentChannel/Internal/Bitcoin/Script.hs view
@@ -1,4 +1,4 @@--- {-# OPTIONS_GHC -fwarn-incomplete-patterns -Werror #-}+{-# LANGUAGE OverloadedStrings #-} module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script where @@ -8,11 +8,9 @@ import qualified Network.Haskoin.Internals as HI import qualified Network.Haskoin.Crypto as HC-import Network.Haskoin.Script- (Script(..), SigHash(..), ScriptOp(..), opPushData)- import qualified Data.ByteString as B + valReceiverSigHash = SigAll True -- |Generates OP_CHECKLOCKTIMEVERIFY redeemScript, which can be redeemed in two ways:@@ -23,14 +21,19 @@ let -- Note: HI.encodeInt encodes values up to and including 2^31-1 as 4 bytes -- and values 2^31 through 2^32-1 (upper limit) as 5 bytes.- encodeScriptInt = B.pack . HI.encodeInt . fromIntegral . toWord32+ encodeScriptInt = convertEmptyPush . opPushData . B.pack .+ HI.encodeInt . fromIntegral . toWord32+ -- A push of an empty byte string and OP_0 are one and the same,+ -- but haskoin-core deserializes into OP_0.+ convertEmptyPush (OP_PUSHDATA "" OPCODE) = OP_0+ convertEmptyPush whatever = whatever serverPubKey = getPubKey serverPK clientPubKey = getPubKey clientPK in Script [OP_IF, opPushData $ serialize serverPubKey, OP_CHECKSIGVERIFY, OP_ELSE,- opPushData $ encodeScriptInt lockTime, op_CHECKLOCKTIMEVERIFY, OP_DROP,+ encodeScriptInt $ lockTime, op_CHECKLOCKTIMEVERIFY, OP_DROP, OP_ENDIF, opPushData $ serialize clientPubKey, OP_CHECKSIG]
src/Data/Bitcoin/PaymentChannel/Internal/Error.hs view
@@ -1,17 +1,31 @@+{-# LANGUAGE DeriveGeneric #-}+ module Data.Bitcoin.PaymentChannel.Internal.Error where -import Data.Bitcoin.PaymentChannel.Internal.Types (BitcoinAmount)+import Data.Bitcoin.PaymentChannel.Internal.Types+import Data.Bitcoin.PaymentChannel.Internal.Util+import GHC.Generics data PayChanError = SigVerifyFailed | BadPaymentValue BitcoinAmount |- DustOutput |- InternalError String+ OutPointMismatch OutPoint |+ ChangeAddrMismatch Address |+ RedeemScriptMismatch Script |+ DustOutput BitcoinAmount |+ PartialPaymentBadValue BitcoinAmount+ deriving Generic instance Show PayChanError where show SigVerifyFailed = "signature verification failed" show (BadPaymentValue valDiff) = "out-of-order payment (assigns " ++ show valDiff ++ " less value to receiver)"- show DustOutput = "dust output in payment transaction"- show (InternalError e) = "Internal error: " ++ e-+ show (OutPointMismatch op) = "unexpected outpoint. expected: " ++ show op+ show (ChangeAddrMismatch addr) = "unexpected change address. expected: " ++ show addr+ show (RedeemScriptMismatch scr) = "unexpected redeem script. expected: " +++ cs (serHex scr)+ show (PartialPaymentBadValue expVal) = "partial payment change value mismatch." +++ " payment 1/2 with change value " ++ show expVal +++ " was accepted, expecting second payment change value to match that."+ show (DustOutput limit) = "server dust limit of " ++ show limit +++ " not respected by client change output"
src/Data/Bitcoin/PaymentChannel/Internal/Payment.hs view
@@ -62,7 +62,7 @@ PaymentChannelState -> BitcoinAmount -- ^ sender change value (subtract 'n' from current sender change value to get payment of value 'n') -> (HC.Hash256 -> HC.Signature) -- ^ signing function- -> Payment+ -> FullPayment paymentFromState (CPaymentChannelState cp fti (CPaymentTxConfig changeAddr) _ _) = createPayment cp fti changeAddr @@ -73,18 +73,19 @@ -> HC.Address -> BitcoinAmount -- ^ sender change value (subtract 'n' from current sender change value to get payment of value 'n') -> (HC.Hash256 -> HC.Signature) -- ^ signing function- -> Payment --ClientSignedPayment+ -> FullPayment createPayment cp (CFundingTxInfo hash idx val) changeAddr changeVal signFunc = let- unsignedPayment = UnsignedPayment+ unsignedPayment@(UnsignedPayment op _ script _ _) = UnsignedPayment (HT.OutPoint hash idx) val (getRedeemScript cp) changeAddr changeVal sigHash = if changeVal /= 0 then HS.SigSingle True else HS.SigNone True hashToSign = getHashForSigning unsignedPayment sigHash sig = signFunc hashToSign+ payment = CPayment changeVal (CPaymentSignature sig sigHash) in- CPayment changeVal (CPaymentSignature sig sigHash)- -- ClientSignedPayment unsignedPayment (CPaymentSignature sig sigHash)+ CFullPayment payment op script changeAddr+ verifyPaymentSig :: ChannelParameters
src/Data/Bitcoin/PaymentChannel/Internal/Serialization.hs view
@@ -5,11 +5,13 @@ module Data.Bitcoin.PaymentChannel.Internal.Serialization where import Data.Bitcoin.PaymentChannel.Internal.Types-import Data.Bitcoin.PaymentChannel.Internal.Util (deserEither, toHexString)-import Data.Aeson (Value(Number), FromJSON(..), ToJSON(..), withText, withScientific)-import Data.Aeson.Types (Parser)+import Data.Bitcoin.PaymentChannel.Internal.Util+import Data.Bitcoin.PaymentChannel.Internal.Error+import qualified Network.Haskoin.Transaction as HT+import Data.Aeson+import Data.Aeson.Types (Parser, Pair) import Data.Scientific (Scientific, scientific, toBoundedInteger)-import Data.Text.Encoding (decodeLatin1, encodeUtf8)+import Data.Text.Encoding (decodeUtf8, encodeUtf8) import qualified Data.Serialize as Bin import qualified Data.Serialize.Put as BinPut import qualified Data.Serialize.Get as BinGet@@ -20,8 +22,10 @@ import Data.Word (Word64) import Data.EitherR (fmapL) import Data.Typeable-+import Debug.Trace +-- Generic PayChanError instance+instance Bin.Serialize PayChanError --- JSON deriving instance ToJSON SendPubKey@@ -38,11 +42,47 @@ fmap (parseBitcoinLocktime . fromIntegral) . parseJSONInt instance ToJSON Payment where- toJSON = toJSON . txtB64Encode+ toJSON = object . toJSONObject -instance FromJSON Payment where- parseJSON = withText "Payment" txtB64Decode+toJSONObject :: Payment -> [Pair]+toJSONObject (CPayment changeVal (CPaymentSignature sig flag)) =+ [ "change_value" .= changeVal+ , "signature_data" .= String (serHex sig)+ , "sighash_flag" .= String (serHex flag)+ ] +parseJSONObject :: Object -> Parser Payment+parseJSONObject o = CPayment+ <$> o .: "change_value"+ <*> (CPaymentSignature <$>+ (o .: "signature_data" >>= withText "SigDataHex" deserHex) <*>+ (o .: "sighash_flag" >>= withText "SigHashFlagHex" deserHex))++instance ToJSON FullPayment where+ toJSON CFullPayment {+ fpPayment = payment,+ fpOutPoint = (HT.OutPoint txid vout), fpRedeemScript = script,+ fpChangeAddr = addr } =+ object $+ toJSONObject payment +++ [ "funding_txid" .= txid+ , "funding_vout" .= vout+ , "redeem_script" .= String (serHex script)+ , "change_address" .= addr+ ]++instance FromJSON FullPayment where+ parseJSON = withObject "FullPayment" parseFullPayment++parseFullPayment :: Object -> Parser FullPayment+parseFullPayment o = CFullPayment+ <$> parseJSONObject o+ <*> (HT.OutPoint <$>+ o .: "funding_txid" <*>+ o .: "funding_vout")+ <*> (o .: "redeem_script" >>= withText "RedeemScriptHex" deserHex)+ <*> o .: "change_address"+ instance ToJSON BitcoinAmount where toJSON amt = Number $ scientific (fromIntegral $ toInteger amt) 0@@ -59,18 +99,25 @@ --- Binary+instance Bin.Serialize ChanScript where+ put (ChanScript s) =+ BinPut.putWord16be scriptBSLen >>+ BinPut.putByteString scriptBS+ where scriptBS = Bin.encode s+ scriptBSLen = fromIntegral $ B.length scriptBS+ get = either error ChanScript . Bin.decode <$>+ (BinGet.getWord16be >>=+ BinGet.getByteString . fromIntegral)+ deriving instance Bin.Serialize SendPubKey deriving instance Bin.Serialize RecvPubKey instance Bin.Serialize PaymentChannelState where put (CPaymentChannelState par fti payConf valLeft sig) =- Bin.put par >> Bin.put fti >> Bin.put payConf >> Bin.put valLeft- >> BinPut.putWord8 1 >> Bin.put sig -- Keep the 0x01 Word8 for format backwards compatibility+ Bin.put par >> Bin.put fti >> Bin.put payConf >>+ Bin.put valLeft >> Bin.put sig get = CPaymentChannelState <$> Bin.get <*> Bin.get <*>- Bin.get <*> Bin.get <*> (BinGet.getWord8 >>=- \w -> case w of- 1 -> Bin.get- _ -> error "empty PaymentChannelState no longer supported")+ Bin.get <*> Bin.get <*> Bin.get instance Bin.Serialize ChannelParameters where put (CChannelParameters pks pkr lt dustLimit) =@@ -88,21 +135,31 @@ get = CPaymentTxConfig <$> Bin.get instance Bin.Serialize Payment where- put (CPayment val sig) = Bin.put val >> Bin.put sig+ put (CPayment val sig) =+ Bin.put val >> Bin.put sig get = CPayment <$> Bin.get <*> Bin.get +instance Bin.Serialize FullPayment where+ put (CFullPayment p op script addr) =+ Bin.put p >> Bin.put op >> Bin.put (ChanScript script) >> Bin.put addr+ get = CFullPayment <$> Bin.get <*> Bin.get <*> fmap getScript Bin.get <*> Bin.get+ instance Bin.Serialize PaymentSignature where- put ps = Bin.put (psSig ps) >>- Bin.put (psSigHash ps)+ put (CPaymentSignature sig sigHash) =+ Bin.put sig >> Bin.put sigHash get = CPaymentSignature <$> Bin.get <*> Bin.get - --- Misc. instance Show Payment where show (CPayment val sig) = "<Payment: valLeft=" ++ show val ++ ", sig=" ++ toHexString (Bin.encode sig) ++ ">" +instance Show FullPayment where+ show (CFullPayment p op script addr) =+ "<FullPayment: payment = " ++ show p ++ " " +++ show (op, script, addr) ++ ">"+ -- Needed to convert from Scientific instance Bounded BitcoinAmount where minBound = BitcoinAmount 0@@ -122,7 +179,7 @@ concatErr e = fmapL (e ++) txtB64Encode :: Bin.Serialize a => a -> T.Text-txtB64Encode = decodeLatin1 . b64Encode+txtB64Encode = decodeUtf8 . b64Encode txtB64Decode :: (Typeable a, Bin.Serialize a) => T.Text -> Parser a txtB64Decode = either fail return . b64Decode . encodeUtf8
src/Data/Bitcoin/PaymentChannel/Internal/State.hs view
@@ -5,14 +5,15 @@ import Data.Bitcoin.PaymentChannel.Internal.Types import Data.Bitcoin.PaymentChannel.Internal.Error import Data.Bitcoin.PaymentChannel.Internal.Util (addressToScriptPubKeyBS)+import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script import qualified Network.Haskoin.Transaction as HT import qualified Network.Haskoin.Crypto as HC import qualified Network.Haskoin.Script as HS pcsChannelTotalValue = ftiOutValue . pcsFundingTxInfo-pcsValueTransferred cs = pcsChannelTotalValue cs - pcsValueLeft cs-pcsChannelValueLeft = pcsValueLeft+pcsValueTransferred cs = pcsChannelTotalValue cs - pcsClientChangeVal cs+pcsChannelValueLeft = pcsClientChangeVal pcsClientPubKey = cpSenderPubKey . pcsParameters pcsServerPubKey = cpReceiverPubKey . pcsParameters pcsDustLimit = cpDustLimit . pcsParameters@@ -20,6 +21,7 @@ pcsClientChangeAddress = ptcSenderChangeAddress . pcsPaymentConfig pcsClientChangeScriptPubKey = addressToScriptPubKeyBS . pcsClientChangeAddress pcsLockTime = cpLockTime . pcsParameters+pcsPrevOut (CPaymentChannelState _ (CFundingTxInfo h i _) _ _ _) = OutPoint h i pcsChannelID :: PaymentChannelState -> HT.OutPoint pcsChannelID pcs = HT.OutPoint (ftiHash fti) (ftiOutIndex fti)@@ -37,12 +39,16 @@ pcs { pcsPaymentConfig = newPayConf } where newPayConf = pConf { ptcSenderChangeAddress = addr } --- |We subtract the specified "dust" limit from the total available value,--- in order to avoid creating a Bitcoin transaction that won't circulate+setFundingSource :: PaymentChannelState -> FundingTxInfo -> PaymentChannelState+setFundingSource pcs fti =+ pcs { pcsFundingTxInfo = fti }++-- |We subtract the specified "dust" limit from the total available value.+-- This avoids creating a Bitcoin transaction that won't circulate -- in the Bitcoin P2P network. channelValueLeft :: PaymentChannelState -> BitcoinAmount channelValueLeft pcs@(CPaymentChannelState (CChannelParameters _ _ _ dustLimit) _ _ _ _) =- pcsValueLeft pcs - dustLimit+ pcsClientChangeVal pcs - dustLimit -- |Returns 'True' if all available channel value has been transferred, 'False' otherwise channelIsExhausted :: PaymentChannelState -> Bool@@ -55,17 +61,24 @@ pcsParameters = channelParameters, pcsFundingTxInfo = fundingTxInfo, pcsPaymentConfig = paymentConfig,- pcsValueLeft = ftiOutValue fundingTxInfo,+ pcsClientChangeVal = ftiOutValue fundingTxInfo, pcsPaymentSignature = paySig } --- |Update state with verified payment. Check value (including dust limit).+-- |Update state with verified payment. updatePaymentChannelState :: PaymentChannelState- -> Payment+ -> FullPayment -> Either PayChanError PaymentChannelState-updatePaymentChannelState (CPaymentChannelState cp fun pconf oldSenderVal _)- payment@(CPayment newSenderVal _)+updatePaymentChannelState (CPaymentChannelState cp fun@(CFundingTxInfo h i _)+ pconf@(CPaymentTxConfig addr) oldSenderVal _)+ (CFullPayment payment@(CPayment newSenderVal _) payOP payScript payChgAddr)+ | (HT.outPointHash payOP /= h) || (HT.outPointIndex payOP /= i) =+ Left $ OutPointMismatch $ OutPoint h i+ | payChgAddr /= addr =+ Left $ ChangeAddrMismatch addr+ | payScript /= getRedeemScript cp =+ Left $ RedeemScriptMismatch $ getRedeemScript cp | newSenderVal <= oldSenderVal = CPaymentChannelState cp fun pconf newSenderVal . cpSignature <$> checkDustLimit cp payment@@ -74,5 +87,5 @@ checkDustLimit :: ChannelParameters -> Payment -> Either PayChanError Payment checkDustLimit (CChannelParameters _ _ _ dustLimit) payment@(CPayment senderChangeVal _) | senderChangeVal < dustLimit =- Left DustOutput+ Left $ DustOutput dustLimit | otherwise = Right payment
src/Data/Bitcoin/PaymentChannel/Internal/Types.hs view
@@ -1,10 +1,13 @@--- {-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-} module Data.Bitcoin.PaymentChannel.Internal.Types ( module Data.Bitcoin.PaymentChannel.Internal.Types , module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Amount , module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.LockTime+ , module Network.Haskoin.Transaction+ , module Network.Haskoin.Crypto+ , module Network.Haskoin.Script ) where@@ -13,12 +16,15 @@ import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Amount import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.LockTime -import qualified Network.Haskoin.Transaction as HT-import qualified Network.Haskoin.Crypto as HC-import qualified Network.Haskoin.Script as HS-import Data.Typeable-import Data.Word-+import Network.Haskoin.Transaction+import Network.Haskoin.Crypto+import Network.Haskoin.Script+import qualified Network.Haskoin.Transaction as HT+import qualified Network.Haskoin.Crypto as HC+import qualified Network.Haskoin.Script as HS+import Data.Typeable+import Data.Word+import GHC.Generics defaultDustLimit = 700 :: BitcoinAmount defaultMinChanSize = defaultDustLimit * 2@@ -33,8 +39,8 @@ -- |Retrieved by looking at the in-blockchain funding transaction pcsFundingTxInfo :: FundingTxInfo, pcsPaymentConfig :: PaymentTxConfig,- -- |Value left to send (starts at @`-` dustLimit . ftiOutValue . pcsFundingTxInfo@)- pcsValueLeft :: BitcoinAmount,+ -- |Client change value+ pcsClientChangeVal :: BitcoinAmount, -- |Signature over payment transaction of value 'pcsValueLeft' pcsPaymentSignature :: PaymentSignature } deriving (Eq, Show, Typeable)@@ -70,11 +76,21 @@ -- |Used to transfer value from sender to receiver. data Payment = CPayment { -- |Channel value remaining ('pcsValueLeft' of the state from which this payment was made)- cpChannelValueLeft :: BitcoinAmount,+ cpClientChange :: BitcoinAmount -- |Payment signature- cpSignature :: PaymentSignature+ , cpSignature :: PaymentSignature } deriving (Eq, Typeable) +-- |Contains all information required to construct the payment transaction+data FullPayment = CFullPayment {+ fpPayment :: Payment+ -- |The payment transaction redeems this outpoint+ , fpOutPoint :: HT.OutPoint+ , fpRedeemScript :: HS.Script+ -- |Client change output address in the payment tx+ , fpChangeAddr :: HC.Address+} deriving (Eq, Typeable)+ -- |Contains payment signature plus sig hash flag byte data PaymentSignature = CPaymentSignature { psSig :: HC.Signature@@ -86,6 +102,8 @@ ,psSigHash :: HS.SigHash } deriving (Eq, Show, Typeable) +-- |Wraps a Network.Haskoin.Script.Script+newtype ChanScript = ChanScript { getScript :: HS.Script } deriving (Eq, Show) -- Never confuse sender/receiver pubkey again: let compiler check newtype SendPubKey = MkSendPubKey {
src/Data/Bitcoin/PaymentChannel/Internal/Util.hs view
@@ -5,6 +5,7 @@ module Data.Bitcoin.PaymentChannel.Internal.Util , module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Util , module Data.Bitcoin.PaymentChannel.Internal.Bitcoin.LockTime+, cs ) where @@ -20,7 +21,11 @@ import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as C+import qualified Data.Text as T+import qualified Data.Aeson.Types as JSON import qualified Network.Haskoin.Crypto as HC+import Data.String.Conversions (cs)+import Data.Text.Encoding (decodeUtf8, encodeUtf8) mapLeft f = either (Left . f) Right@@ -34,6 +39,9 @@ toHexBS :: B.ByteString -> B.ByteString toHexBS = B16.encode +fromHexBS :: B.ByteString -> B.ByteString+fromHexBS = fst . B16.decode+ fromHexString :: String -> B.ByteString fromHexString hexStr = case (B16.decode . C.pack) hexStr of@@ -61,4 +69,10 @@ toHexString leftoverBS where offset = B.length bs - B.length leftoverBS +deserHex :: (Typeable a, Bin.Serialize a) => T.Text -> JSON.Parser a+deserHex = either+ (fail . ("failed to decode hex: " ++)) return .+ deserEither . fromHexBS . encodeUtf8 +serHex :: Bin.Serialize a => a -> T.Text+serHex = decodeUtf8 . toHexBS . Bin.encode
+ src/Data/Bitcoin/PaymentChannel/Movable.hs view
@@ -0,0 +1,190 @@+{-|+Module : Data.Bitcoin.PaymentChannel.Movable+Description : Bitcoin payment channel library+License : PublicDomain+Maintainer : runesvend@gmail.com+Stability : experimental+Portability : POSIX++TODO+-}++module Data.Bitcoin.PaymentChannel.Movable where++import Data.Bitcoin.PaymentChannel+import Data.Bitcoin.PaymentChannel.Types+import Data.Bitcoin.PaymentChannel.Internal.Types+import qualified Data.Bitcoin.PaymentChannel.Internal.State as S+import qualified Network.Haskoin.Crypto as HC+import qualified Data.Serialize as Bin+import Control.Applicative ((<|>))++-- |A ReceiverPaymentChannel whose received value can be redeemed while+-- keeping the channel open, by switching between two different OutPoints+-- in the FundingTxInfo.+data MovableChan =+ Settled {+ beginVal :: BitcoinAmount+ , beginState :: ReceiverPaymentChannel+ } | Unsettled {+ channels :: ChannelPair+ -- If payment 1/2 has been accepted, accept 2/2 with 'receiveSecondPayment pp'+ , finishPay :: Maybe PartialPayment+ } -- deriving (Show)++data ChannelPair = ChannelPair {+ beginVal' :: BitcoinAmount+ , oldState :: ReceiverPaymentChannel+ , newState :: ReceiverPaymentChannel+}++-- |We wrap the state in which only payment 1/2 has been received in a data+-- type, in order to be able to serialize it to disk.+data PartialPayment =+ NewNeedsUpdate ChannelPair BitcoinAmount+ | OldNeedsUpdate ChannelPair BitcoinAmount++instance PaymentChannel MovableChan where+ valueToMe = channelValueLeft . fst . getStateForClosing+ getChannelState = rpcState . fst . getStateForClosing+ _setChannelState (Settled v rpc) s = Settled v (rpc { rpcState = s })+ _setChannelState (Unsettled (ChannelPair v old new) fp) s =+ Unsettled (ChannelPair v (setState s old) (setState s new)) fp+ where setState s rpc = rpc { rpcState = s }++newMovableChan ::+ ChannelParameters+ -> FundingTxInfo+ -> FullPayment+ -> Either PayChanError (BitcoinAmount, MovableChan, BitcoinAmount)+newMovableChan cp fti@(CFundingTxInfo _ _ chanVal)+ fullPayment@(CFullPayment _ _ _ payChgAddr) =+ checkChangeAddr >>= channelFromInitialPayment cp fti desiredChangeAddr >>=+ \(payAmount, rpc) -> Right (payAmount, Settled chanVal rpc, channelValueLeft rpc)+ where desiredChangeAddr = getFundingAddress cp+ checkChangeAddr =+ if payChgAddr /= desiredChangeAddr then+ Left $ ChangeAddrMismatch desiredChangeAddr+ else+ Right fullPayment++getStateForClosing :: MovableChan -> (ReceiverPaymentChannel,BitcoinAmount)+getStateForClosing (Settled v rpc) = (rpc,v)+getStateForClosing (Unsettled (ChannelPair v _ newRpc) _) = (newRpc,v)++getStateByInfo :: MovableChan -> BitcoinLockTime -> OutPoint -> Maybe (ReceiverPaymentChannel,BitcoinAmount)+getStateByInfo mc lt op = case mc of+ (Settled v rpc) -> checkInfo rpc v+ (Unsettled (ChannelPair v old new) _) -> checkInfo new v <|> checkInfo old v+ where checkInfo rpc v =+ if getChannelID rpc == op &&+ getExpirationDate rpc == lt then+ Just (rpc,v)+ else+ Nothing++moveChannel ::+ MovableChan+ -> (HC.Hash256 -> HC.Signature) -- ^ Function which produces a signature which verifies against 'cpReceiverPubKey'+ -> HC.Address -- ^ Receiver destination address. Funds sent over the channel will be sent to this address, the rest back to the client change address (an argument to 'channelWithInitialPaymentOf').+ -> BitcoinAmount -- ^ Bitcoin transaction fee+ -> Maybe (Tx, MovableChan)+moveChannel Unsettled{} _ _ _ = Nothing+moveChannel (Settled v rpc) signFunc destAddr txFee =+ -- If we haven't received any value, moving the channel makes no sense+ if valueToMe rpc > 0 then+ Just (settleTx, Unsettled (ChannelPair v rpc newRpc) Nothing)+ else+ Nothing+ where settleTx = getSettlementBitcoinTx rpc signFunc destAddr txFee+ newRpc = CReceiverPaymentChannel $ S.setFundingSource (getChannelState rpc) fti+ fti = CFundingTxInfo (txHash settleTx)+ -- Client output always at index zero+ 0 (senderChangeValue rpc)++markAsSettled ::+ MovableChan+ -> Maybe MovableChan+markAsSettled Settled{} = Nothing+markAsSettled (Unsettled _ (Just _)) = Nothing+markAsSettled (Unsettled (ChannelPair v _ newRpc) Nothing) =+ if valueToMe newRpc > 0 then+ Just $ Settled v newRpc+ else+ Nothing++recvSinglePayment ::+ MovableChan+ -> FullPayment+ -> Either PayChanError (BitcoinAmount, MovableChan, BitcoinAmount)+recvSinglePayment (Settled v rpc) fp = recvPayment rpc fp >>=+ \(a, newRpc) -> Right (a, Settled v newRpc, channelValueLeft newRpc)+-- Accept payment 1/2+recvSinglePayment (Unsettled cp@(ChannelPair _ old new) Nothing) fp@(CFullPayment _ op _ _)+ | S.pcsPrevOut (getChannelState old) == op = oldRecvPay cp fp+ | S.pcsPrevOut (getChannelState new) == op = newRecvPay cp fp+ | otherwise = Left . OutPointMismatch . S.pcsPrevOut . getChannelState $ new+-- If we've already received paymed 1/2, we receive 2/2 with this+recvSinglePayment (Unsettled _ (Just pp)) fp = receiveSecondPayment pp fp++oldRecvPay ::+ ChannelPair+ -> FullPayment+ -> Either PayChanError (BitcoinAmount, MovableChan, BitcoinAmount)+oldRecvPay (ChannelPair v old new) fp = recvPayment old fp >>=+ -- Update old state, and leave behind a function that updates the new state+ \(amt, newOld) -> Right+ (amt, Unsettled (ChannelPair v newOld new)+ (Just $ NewNeedsUpdate (ChannelPair v newOld new) amt),+ channelValueLeft newOld)++newRecvPay ::+ ChannelPair+ -> FullPayment+ -> Either PayChanError (BitcoinAmount, MovableChan, BitcoinAmount)+newRecvPay (ChannelPair v old new) fp = recvPayment new fp >>=+ -- Update new state, and leave behind a function that updates the old state+ \(amt, newNew) -> Right+ (amt, Unsettled (ChannelPair v old newNew)+ (Just $ OldNeedsUpdate (ChannelPair v old newNew) amt),+ channelValueLeft newNew)++receiveSecondPayment ::+ PartialPayment+ -> FullPayment+ -> Either PayChanError (BitcoinAmount, MovableChan, BitcoinAmount)+receiveSecondPayment (OldNeedsUpdate cp amt) fp =+ checkChangeValueMatch amt fp >>= oldRecvPay cp >>=+ \(amt, mc, vLeft) -> Right (amt, mc { finishPay = Nothing }, vLeft)+receiveSecondPayment (NewNeedsUpdate cp amt) fp =+ checkChangeValueMatch amt fp >>= newRecvPay cp >>=+ \(amt, mc, vLeft) -> Right (amt, mc { finishPay = Nothing }, vLeft)++-- | We want to make sure that payment 1/2 and 2/2 are of equal value+checkChangeValueMatch :: BitcoinAmount -> FullPayment -> Either PayChanError FullPayment+checkChangeValueMatch firstPayVal fp@(CFullPayment (CPayment val _) _ _ _) =+ if val /= firstPayVal then Left $ PartialPaymentBadValue firstPayVal else Right fp+++instance Bin.Serialize MovableChan where+ put (Settled bVal rpc) =+ Bin.putWord8 0x01 >> Bin.put bVal >> Bin.put rpc+ put (Unsettled cPair mPP) =+ Bin.putWord8 0x02 >> Bin.put cPair >> Bin.put mPP+ get = Bin.getWord8 >>= \w -> case w of+ 0x01 -> Settled <$> Bin.get <*> Bin.get+ 0x02 -> Unsettled <$> Bin.get <*> Bin.get+ n -> fail $ "unknown start byte: " ++ show n++instance Bin.Serialize ChannelPair where+ put (ChannelPair bVal old new) =+ Bin.put bVal >> Bin.put old >> Bin.put new+ get = ChannelPair <$> Bin.get <*> Bin.get <*> Bin.get++instance Bin.Serialize PartialPayment where+ put (NewNeedsUpdate cp amt) = Bin.putWord8 0x01 >> Bin.put cp >> Bin.put amt+ put (OldNeedsUpdate cp amt) = Bin.putWord8 0x02 >> Bin.put cp >> Bin.put amt+ get = Bin.getWord8 >>= \w -> case w of+ 0x01 -> NewNeedsUpdate <$> Bin.get <*> Bin.get+ 0x02 -> OldNeedsUpdate <$> Bin.get <*> Bin.get+ n -> fail $ "unknown start byte: " ++ show n
src/Data/Bitcoin/PaymentChannel/Types.hs view
@@ -15,7 +15,8 @@ PaymentChannel(..), SenderPaymentChannel(..), ReceiverPaymentChannel(..),-Payment,+Payment,cpSignature,+FullPayment(..), FundingTxInfo(..), ChannelParameters(..), PayChanError(..),@@ -33,12 +34,6 @@ where import Data.Bitcoin.PaymentChannel.Internal.Types- (BitcoinAmount(..),- PaymentChannelState(..), Payment(..)- ,FundingTxInfo(..), ChannelParameters(..),- SendPubKey(..), RecvPubKey(..), IsPubKey(..),- defaultDustLimit,- BitcoinLockTime(..), fromDate, usesBlockHeight) import Data.Bitcoin.PaymentChannel.Internal.Serialization import qualified Data.Bitcoin.PaymentChannel.Internal.State as S@@ -48,6 +43,7 @@ import Data.Aeson as JSON -- (FromJSON, ToJSON) import qualified Network.Haskoin.Crypto as HC import qualified Network.Haskoin.Transaction as HT+-- import qualified Network.Haskoin.Transaction as HS -- |Get various information about an open payment channel.@@ -56,24 +52,31 @@ valueToMe :: a -> BitcoinAmount -- |Retrieve internal state object getChannelState :: a -> PaymentChannelState+ getChannelID :: a -> HT.OutPoint getExpirationDate :: a -> BitcoinLockTime+ getSenderPubKey :: a -> SendPubKey getNewestPayment :: a -> Payment+ getNewestSig :: a -> HC.Signature -- |Return True if channel expires earlier than given expiration date expiresBefore :: BitcoinLockTime -> a -> Bool -- |For internal use _setChannelState :: a -> PaymentChannelState -> a channelValueLeft :: a -> BitcoinAmount+ senderChangeValue :: a -> BitcoinAmount -- |Returns 'True' if all available channel value has been transferred, 'False' otherwise channelIsExhausted :: a -> Bool getChannelID = S.pcsChannelID . getChannelState getExpirationDate = S.pcsExpirationDate . getChannelState+ getSenderPubKey = S.pcsClientPubKey . getChannelState+ senderChangeValue = pcsClientChangeVal . getChannelState channelValueLeft = S.channelValueLeft . getChannelState channelIsExhausted = S.channelIsExhausted . getChannelState expiresBefore expDate chan = getExpirationDate chan < expDate getNewestPayment pcs = S.pcsGetPayment (getChannelState pcs)+ getNewestSig = psSig . cpSignature . getNewestPayment -- |State object for the value sender data SenderPaymentChannel = CSenderPaymentChannel {
src/Data/Bitcoin/PaymentChannel/Util.hs view
@@ -10,12 +10,13 @@ module Data.Bitcoin.PaymentChannel.Util (-getFundingAddress,+getFundingAddress,getRedeemScript, setSenderChangeAddress, serialize, deserEither, BitcoinLockTime, parseBitcoinLocktime, toWord32, fromDate, parseJSONInt,+pGetSig,fpGetSig, unsafeUpdateRecvState )@@ -23,11 +24,9 @@ import Data.Bitcoin.PaymentChannel.Internal.Types (PaymentChannelState(..),- Payment(..))--- import Data.Bitcoin.PaymentChannel.Internal.State--- (ptcSenderChangeAddress)+ Payment(..), PaymentSignature(..)) import Data.Bitcoin.PaymentChannel.Internal.Bitcoin.Script- (getP2SHFundingAddress)+ (getP2SHFundingAddress, getRedeemScript) import Data.Bitcoin.PaymentChannel.Internal.Util (parseBitcoinLocktime, toWord32, deserEither, serialize) import Data.Bitcoin.PaymentChannel.Internal.State@@ -58,6 +57,10 @@ -- verified the signature, and it just needs to be stored. unsafeUpdateRecvState :: ReceiverPaymentChannel -> Payment -> ReceiverPaymentChannel unsafeUpdateRecvState (CReceiverPaymentChannel s) (CPayment val sig) =- CReceiverPaymentChannel $ s { pcsValueLeft = val, pcsPaymentSignature = sig}+ CReceiverPaymentChannel $ s { pcsClientChangeVal = val, pcsPaymentSignature = sig} +fpGetSig :: FullPayment -> HC.Signature+fpGetSig = psSig . cpSignature . fpPayment +pGetSig :: Payment -> HC.Signature+pGetSig = psSig . cpSignature
test/Main.hs view
@@ -1,18 +1,24 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} module Main where -import Data.Bitcoin.PaymentChannel-import Data.Bitcoin.PaymentChannel.Types-import Data.Bitcoin.PaymentChannel.Util++import Data.Bitcoin.PaymentChannel+import Data.Bitcoin.PaymentChannel.Types+import Data.Bitcoin.PaymentChannel.Util import qualified Data.Bitcoin.PaymentChannel.Internal.State as S -import qualified Network.Haskoin.Transaction as HT-import qualified Network.Haskoin.Crypto as HC-import Network.Haskoin.Test+import qualified Network.Haskoin.Transaction as HT+import qualified Network.Haskoin.Crypto as HC+import qualified Network.Haskoin.Script as HS+import Network.Haskoin.Test+import qualified Data.Aeson as JSON+import qualified Data.Serialize as Bin+import Data.Typeable import Test.QuickCheck-+import Debug.Trace dUST_LIMIT = 700 :: BitcoinAmount mIN_CHANNEL_SIZE = 1400 :: BitcoinAmount@@ -43,33 +49,50 @@ (recvAmount : recvList) f +newtype ChanScript = ChanScript HS.Script deriving (Eq,Show,Bin.Serialize)+ instance Arbitrary ArbChannelPair where- arbitrary = mkChanPair+ arbitrary = fmap fst mkChanPair -mkChanPair :: Gen ArbChannelPair -- (Either String ArbChannelPair)+instance Arbitrary FullPayment where+ arbitrary = fmap snd mkChanPair++instance Arbitrary ChannelParameters where+ arbitrary = fmap fst mkChanParams++instance Arbitrary ChanScript where+ arbitrary = ChanScript . getRedeemScript <$> arbitrary++mkChanParams :: Gen (ChannelParameters, (HC.PrvKey, HC.PrvKey))+mkChanParams = do+ -- sender key pair+ ArbitraryPubKey sendPriv sendPK <- arbitrary+ -- receiver key pair+ ArbitraryPubKey recvPriv recvPK <- arbitrary+ -- expiration date+ lockTime <- arbitrary+ return (CChannelParameters+ (MkSendPubKey sendPK) (MkRecvPubKey recvPK) lockTime dUST_LIMIT,+ (sendPriv, recvPriv))++mkChanPair :: Gen (ArbChannelPair, FullPayment) mkChanPair = do- -- sender key pair- ArbitraryPubKey sendPriv sendPK <- arbitrary- -- receiver key pair- ArbitraryPubKey recvPriv recvPK <- arbitrary- -- expiration date- lockTime <- arbitrary+ (cp, (sendPriv, recvPriv)) <- mkChanParams fti <- arbitrary- let cp = CChannelParameters- (MkSendPubKey sendPK) (MkRecvPubKey recvPK) lockTime dUST_LIMIT -- value of first payment initPayAmount <- arbitrary -- fromIntegral <$> choose (0, chanAmount)-+ -- create states let (initPayActualAmount,paymnt,sendChan) = channelWithInitialPaymentOf- cp fti (flip HC.signMsg sendPriv) (HC.pubKeyAddr sendPK) initPayAmount+ cp fti (flip HC.signMsg sendPriv) (getFundingAddress cp) initPayAmount let eitherRecvChan = channelFromInitialPayment- cp fti (HC.pubKeyAddr sendPK) paymnt-+ cp fti (getFundingAddress cp) paymnt case eitherRecvChan of Left e -> error (show e)- Right (initRecvAmount,recvChan) -> return $ ArbChannelPair- sendChan recvChan [initPayActualAmount] [initRecvAmount]- (flip HC.signMsg recvPriv)+ Right (initRecvAmount,recvChan) -> return+ (ArbChannelPair+ sendChan recvChan [initPayActualAmount] [initRecvAmount]+ (flip HC.signMsg recvPriv),+ paymnt) instance Arbitrary BitcoinLockTime where arbitrary = fmap parseBitcoinLocktime arbitrary@@ -112,12 +135,44 @@ checkGoodClientValue clientValueIsGood && statesMatch && recvSendAmountMatch -testChanPair :: ArbChannelPair -> [BitcoinAmount] -> Bool-testChanPair arbChanPair paymentAmountList =++jsonSerDeser :: (Show a, Eq a, JSON.FromJSON a, JSON.ToJSON a) => a -> Bool+jsonSerDeser fp =+ maybe False checkEquals decodedObj+ where json = JSON.encode fp+ decodedObj = JSON.decode json+ checkEquals serDeserVal =+ if serDeserVal /= fp then+ error ("Ser/deser mismatch.\nOriginal: " ++ show fp ++ "\nCopy: " ++ show decodedObj)+ else+ True++binSerDeser :: (Typeable a, Show a, Eq a, Bin.Serialize a) => a -> Bool+binSerDeser fp =+ checkEquals decodeRes+ where bs = Bin.encode fp+ decodeRes = deserEither bs+ checkEquals serDeserRes = case serDeserRes of+ Left e -> error $ "Serialize/deserialize error: " ++ show e+ Right res -> if res /= fp then+ error ("Ser/deser mismatch.\nOriginal: " ++ show fp ++ "\nCopy: " ++ show res)+ else+ True++testPaymentSession :: ArbChannelPair -> [BitcoinAmount] -> Bool+testPaymentSession arbChanPair paymentAmountList = checkChanPair (runChanPair arbChanPair paymentAmountList) +testPaymentJSON = jsonSerDeser :: FullPayment -> Bool+testPaymentBin = binSerDeser :: FullPayment -> Bool+testScriptBin = binSerDeser :: ChanScript -> Bool+ main :: IO ()-main = quickCheckWith stdArgs testChanPair+main = do+ quickCheckWith stdArgs testPaymentSession+ quickCheckWith stdArgs testPaymentJSON+ quickCheckWith stdArgs testPaymentBin+-- quickCheckWith stdArgs testScriptBin