Repository navigation
fix: validate the type byte of colour identifiers - #28
Conversation
azuchi
left a comment
There was a problem hiding this comment.
マージを妨げる問題は見つかりませんでした。型バイト(0xc1 / 0xc2 / 0xc3)と全ゼロペイロードの検証は tapyrus-core の OP_COLOR の実装と一致しており、不正な colorId で使用不能な出力を作れてしまう問題は塞がれています。手元でも ./scripts/check.sh(3,128 件パス)、format:ci、lint:tests が通り、ビルド後の src/ / types/ に差分が出ないことを確認しました。
インラインのコメントはすべて非ブロッキングです。この PR で対応するか後続に回すかはお任せします。diff に現れない箇所で 2 点あります。
classify のテンプレートは型バイトを見ない(ts_src/templates/coloredpubkeyhash/output.ts、coloredscripthash/output.ts)
check は長さとオペコードの位置しか見ないので、型バイト 0xc4 のスクリプトも coloredpubkeyhash と分類され、txb.addOutput(script, value) に生スクリプトで渡すと受理されます。テンプレートの check でも types.ColorId を使えば、分類と payments の判定が揃います。
同種の allocUnsafe が payments 側に残っている(ts_src/payments/cp2pkh.ts:45、cp2sh.ts:61)
toBase58Check と同じエンコード処理が lazy な address に重複していて、こちらは Buffer.allocUnsafe(54) のままです。検証済みの長さなら全バイトが上書きされるので、影響が出るのは validate: false のときだけですが、toBase58Check を呼ぶ形に寄せると修正が 1 箇所にまとまります。
| if (colored && !colorId) | ||
| throw new Error(address + ' is missing a color identifier'); | ||
| if (!colored && colorId) | ||
| throw new Error(address + ' has an unexpected color identifier'); |
There was a problem hiding this comment.
色の有無のチェックがネットワークのバージョン照合より先に走るので、別ネットワークの彩色アドレスを渡したときのエラーが誤解を招くものになります。手元で確認した結果です。
| 入力(network は prod) | この PR | master |
|---|---|---|
| dev の彩色アドレス | has an unexpected color identifier |
has no matching Script |
| dev の無彩色アドレス | has no matching Script |
has no matching Script |
実際の原因はネットワーク不一致で、色は正しく付いています。無彩色アドレスは従来どおりなので扱いも不揃いです。バージョンがこのネットワークの 4 つのいずれかに一致した後で整合性を見る順序にすると解消します(一致しなければ従来どおり has no matching Script)。
There was a problem hiding this comment.
対応した。色とバージョンの整合は、バージョンがそのネットワークの 4 つのいずれかに一致した後で見る。別ネットワークのアドレスは、カラードコインでもTPCでも従来どおり has no matching Script になる。
| @@ -81,20 +92,25 @@ export function toOutputScript(address: string, network?: Network): Buffer { | |||
| } catch (e) {} | |||
There was a problem hiding this comment.
fromBase58Check が新たに投げる has an invalid color identifier は、この空の catch に握りつぶされて has no matching Script になります。型バイト 0x00 や全ゼロペイロードのアドレスを toOutputScript に渡しても、この PR が伝えたい「色識別子が不正」という理由が利用者に届きません。バージョンと色の不整合には専用メッセージがあるので、扱いも揃っていません。base58 のデコード失敗だけを握りつぶし、色識別子のエラーはそのまま投げ直す形にできないでしょうか。
There was a problem hiding this comment.
Color Identifierのエラーだけ再 throwするように修正した。
エラー文言は定数にして、判定と throw で共有するようにした。
| ): string { | ||
| typeforce(types.tuple(types.Hash160bit, types.UInt8), arguments); | ||
| typeforce( | ||
| types.tuple(types.Hash160bit, types.UInt8, types.maybe(types.ColorId)), |
There was a problem hiding this comment.
toBase58Check は version と colorId の整合を見ないので、toOutputScript が拒否するアドレスを生成できます。
toBase58Check(hash, 0x00, validColorId)は無彩色バージョンに色付きペイロードを持つアドレスを返すtoBase58Check(hash, 0x01)は彩色バージョンで色のないアドレスを返す
追加されたフィクスチャの 2 件(1QKpvPF... と RtqvBaud...)がまさにこの形です。network を受け取らない低レベル関数なので、ここで弾くかどうかは設計判断だと思います。対応しない場合は、その旨を doc コメントに書いておくと親切です。
| /** | ||
| * A Tapyrus colour identifier: one token type byte followed by a 32 byte | ||
| * payload. tapyrus-core rejects any other type byte, and rejects an all-zero | ||
| * payload, both when decoding an address and when executing OP_COLOR. |
There was a problem hiding this comment.
記述の正確さについてです。tapyrus-core のアドレスデコード(src/key_io.cpp の DecodeDestination)が見ているのは cid.type != TokenTypes::NONE だけで、全ゼロペイロードは拒否しません。全ゼロを SCRIPT_ERR_OP_COLORID_INVALID で拒否するのは OP_COLOR の実行時のみです。
ライブラリ側が厳しい分には安全なので挙動はこのままでよいと思いますが、このコメントと PR 本文の「both when decoding an address and when running OP_COLOR」は、型バイトについてのみ両方、全ゼロペイロードについては OP_COLOR のみ、という書き方に直しておくと正確です。
あわせて、この JSDoc は ColorId 関数ではなく COLOR_ID_REISSUABLE の直前にあるため、生成された types/types.d.ts では定数の説明として出力されます。ColorId の直前に移すのがよさそうです。
There was a problem hiding this comment.
JSDoc を ColorId の直前に移し、「Tapyrus Coreではサポート外のトークンタイプはアドレスのでコード時と実行時に弾き、全ゼロは実行時のみ弾いているが、ここではどちら(トークンタイプとペイロード)も弾く」と書き直しました。
| COLOR_ID_NFT, | ||
| ]; | ||
|
|
||
| function isBuffer(value: unknown): value is Buffer { |
There was a problem hiding this comment.
細かい点です。isBuffer は Buffer.isBuffer で足ります。また COLOR_ID_LENGTH = 33 は address.ts にも同じ定義があり、payments 側は 33 / 34 のマジックナンバーのままです。定数を types.ts に集約する PR なので、長さも export して共有すると揃います。
There was a problem hiding this comment.
COLOR_ID_LENGTH を types.ts から export し、address.ts と payments の 33 / 1 + 33 を置き換えた。
isBuffer は削除したが、Buffer.isBuffer ではなく typeforce.Buffer(value) を使った。types.ts は export const Buffer = typeforce.Buffer を持つため、このファイルの中では Buffer が global ではなくそちらを指す。Buffer.isBuffer は typeforce.Buffer.isBuffer になり、呼ぶと例外になる。(TypeError: exports.Buffer.isBuffer is not a function)
| hash = hash2; | ||
| } | ||
|
|
||
| if (colorId.length > 0 && !types.ColorId(colorId)) |
There was a problem hiding this comment.
この検証は opts.validate のブロック内にあるので、validate: false ではアドレスや出力スクリプト経由の色が未検証のまま通ります。
payments.cp2pkh({ address: <型バイト 0xc4 のアドレス> }, { validate: false }).output
// -> 21c4222222... 例外なしcolorId を直接渡す経路は typeforce で常に検証されるので、address / output 経由だけ抜ける形です(cp2sh.ts も同じ)。明示的なオプトアウトなので優先度は低いですが、使用不能な出力を作らせないという PR の目的からすると、lazy な colorId の側で検証するのも一案です。
There was a problem hiding this comment.
cp2pkh と cp2sh の lazy な colorId 側
- ts_src/payments/cp2pkh.ts:77-85
- ts_src/payments/cp2sh.ts:104-112
で検証するようにした。不正なら"Invalid color identifier"を例外として投げるようにしている。
14fb1f7 to
cfb3e8c
Compare
Summary
A colour identifier is a token type byte followed by a 32 byte payload.
tapyrus-core accepts three type bytes and rejects an all-zero payload, both when
decoding an address and when running
OP_COLOR.The library checked the length and nothing else, so anything 33 bytes long
passed:
An output built from such an address can never be spent: the node refuses the
address, and the script fails when
OP_COLORruns. Any TPC sent to it is lost,and one pasted address string is enough to get there.
Details
ts_src/types.tsgainedColorId, a typeforce-compatible predicate requiring 33bytes, a type byte of
0xc1,0xc2or0xc3, and a payload that is not allzero, along with the three constants.
ts_src/metadata.tshad its own copies ofthose constants and now imports them.
It is applied in four places:
payments.cp2pkh,payments.cp2shtypef.BufferN(33), plus nothing on the colour that validation settles onaddress.fromBase58Checkaddress.toBase58CheckTwo more faults in the same functions:
toBase58Checkwrote uninitialised memory into addresses. It allocatedwith
Buffer.allocUnsafeand copied the colour in, so a short colour left therest as whatever was in memory and a long one was truncated. The checksum was
correct either way, so the caller could not notice. Now
Buffer.alloc, withthe length checked.
toOutputScriptreturnedundefinedtyped asBuffer. The version byteand the payload length were never compared, so a colour version with a 21 byte
payload produced a
cp2pkhcall with no colour, whoseoutputisundefined,passed on through
.output as Buffer. An uncoloured version with a 54 bytepayload was read as P2PKH and the colour dropped. Both are rejected now, and
the
as Buffercasts are gone.Affected areas
ts_src/types.ts,ts_src/address.ts,ts_src/payments/cp2pkh.ts,ts_src/payments/cp2sh.ts, and the constants ints_src/metadata.ts.Every existing fixture uses a
c3...colour and still passes. Code that waspassing a malformed colour was producing unspendable outputs and now gets an
error.
How to verify
3,128 tests pass. Thirteen fail against the previous code, covering the three
type bytes
0x00/0xc0/0xc4, an all-zero payload, a colour that is not 33bytes, a valid colour round-tripping through
toBase58Check/fromBase58Check,and the two version/payload mismatches.